add more lang
This commit is contained in:
146
src/App.vue
146
src/App.vue
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
||||
import {
|
||||
Settings,
|
||||
Languages,
|
||||
@@ -9,7 +9,8 @@ import {
|
||||
ArrowRightLeft,
|
||||
Loader2,
|
||||
Check,
|
||||
FileText
|
||||
FileText,
|
||||
ChevronDown
|
||||
} from 'lucide-vue-next';
|
||||
import { fetch } from '@tauri-apps/plugin-http';
|
||||
import { useSettingsStore, LANGUAGES, DEFAULT_TEMPLATE } from './stores/settings';
|
||||
@@ -23,6 +24,40 @@ function cn(...inputs: ClassValue[]) {
|
||||
const settings = useSettingsStore();
|
||||
const view = ref<'translate' | 'settings' | 'logs'>('translate');
|
||||
|
||||
// Dropdown State
|
||||
const sourceDropdownOpen = ref(false);
|
||||
const targetDropdownOpen = ref(false);
|
||||
|
||||
const closeAllDropdowns = () => {
|
||||
sourceDropdownOpen.value = false;
|
||||
targetDropdownOpen.value = false;
|
||||
};
|
||||
|
||||
const toggleDropdown = (type: 'source' | 'target') => {
|
||||
if (type === 'source') {
|
||||
sourceDropdownOpen.value = !sourceDropdownOpen.value;
|
||||
targetDropdownOpen.value = false;
|
||||
} else {
|
||||
targetDropdownOpen.value = !targetDropdownOpen.value;
|
||||
sourceDropdownOpen.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleGlobalClick = (e: MouseEvent) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (!target.closest('.lang-dropdown')) {
|
||||
closeAllDropdowns();
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('click', handleGlobalClick);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('click', handleGlobalClick);
|
||||
});
|
||||
|
||||
// Translation State
|
||||
const sourceText = ref('');
|
||||
const targetText = ref('');
|
||||
@@ -50,9 +85,9 @@ const sourceLang = computed(() => settings.sourceLang);
|
||||
const targetLang = computed(() => settings.targetLang);
|
||||
|
||||
const swapLanguages = () => {
|
||||
const temp = sourceLangCode.value;
|
||||
sourceLangCode.value = targetLangCode.value;
|
||||
targetLangCode.value = temp;
|
||||
const temp = { ...settings.sourceLang };
|
||||
settings.sourceLang = { ...settings.targetLang };
|
||||
settings.targetLang = temp;
|
||||
};
|
||||
|
||||
const clearSource = () => {
|
||||
@@ -79,9 +114,9 @@ const translate = async () => {
|
||||
targetText.value = '';
|
||||
|
||||
const prompt = settings.systemPromptTemplate
|
||||
.replace(/{SOURCE_LANG}/g, sourceLang.value.name)
|
||||
.replace(/{SOURCE_LANG}/g, sourceLang.value.englishName)
|
||||
.replace(/{SOURCE_CODE}/g, sourceLang.value.code)
|
||||
.replace(/{TARGET_LANG}/g, targetLang.value.name)
|
||||
.replace(/{TARGET_LANG}/g, targetLang.value.englishName)
|
||||
.replace(/{TARGET_CODE}/g, targetLang.value.code)
|
||||
.replace(/{TEXT}/g, sourceText.value);
|
||||
|
||||
@@ -176,14 +211,47 @@ const translate = async () => {
|
||||
<!-- Translation View -->
|
||||
<div v-if="view === 'translate'" class="flex-1 flex flex-col md:flex-row divide-y md:divide-y-0 md:divide-x bg-white overflow-hidden">
|
||||
<!-- Source Pane -->
|
||||
<div class="flex-1 flex flex-col min-h-0">
|
||||
<div class="flex items-center gap-4 px-6 py-3 border-b bg-slate-50/50">
|
||||
<select
|
||||
v-model="sourceLangCode"
|
||||
class="bg-transparent border-none focus:ring-0 font-medium text-slate-700 cursor-pointer text-sm outline-none appearance-none"
|
||||
>
|
||||
<option v-for="lang in LANGUAGES" :key="lang.code" :value="lang.code">{{ lang.name }}</option>
|
||||
</select>
|
||||
<div class="flex-1 flex flex-col min-h-0 relative">
|
||||
<div class="flex items-center gap-3 px-6 py-3 border-b bg-slate-50/50 relative z-40">
|
||||
<!-- Custom Source Dropdown -->
|
||||
<div class="relative lang-dropdown min-w-[120px]">
|
||||
<button
|
||||
@click.stop="toggleDropdown('source')"
|
||||
class="flex items-center gap-2 px-3 py-1.5 rounded-lg hover:bg-slate-200/60 transition-colors text-sm font-semibold text-slate-700 w-full justify-between group"
|
||||
>
|
||||
<span class="truncate">{{ sourceLang.displayName }}</span>
|
||||
<ChevronDown :class="cn('w-4 h-4 text-slate-400 transition-transform duration-200', sourceDropdownOpen && 'rotate-180')" />
|
||||
</button>
|
||||
|
||||
<!-- Dropdown Menu -->
|
||||
<transition
|
||||
enter-active-class="transition duration-100 ease-out"
|
||||
enter-from-class="transform scale-95 opacity-0"
|
||||
enter-to-class="transform scale-100 opacity-100"
|
||||
leave-active-class="transition duration-75 ease-in"
|
||||
leave-from-class="transform scale-100 opacity-100"
|
||||
leave-to-class="transform scale-95 opacity-0"
|
||||
>
|
||||
<div
|
||||
v-if="sourceDropdownOpen"
|
||||
class="absolute left-0 mt-2 w-56 max-h-80 overflow-y-auto bg-white rounded-xl shadow-xl border border-slate-200 z-50 py-2 py-1 flex flex-col custom-scrollbar"
|
||||
>
|
||||
<button
|
||||
v-for="lang in LANGUAGES"
|
||||
:key="lang.code"
|
||||
@click="sourceLangCode = lang.code; sourceDropdownOpen = false"
|
||||
:class="cn(
|
||||
'px-4 py-2.5 text-sm text-left transition-colors flex items-center justify-between',
|
||||
sourceLangCode === lang.code ? 'bg-blue-50 text-blue-600 font-bold' : 'text-slate-600 hover:bg-slate-50'
|
||||
)"
|
||||
>
|
||||
{{ lang.displayName }}
|
||||
<Check v-if="sourceLangCode === lang.code" class="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<button @click="swapLanguages" class="p-1.5 hover:bg-slate-200 rounded-md transition-colors" title="交换语言">
|
||||
<ArrowRightLeft class="w-4 h-4 text-slate-500" />
|
||||
</button>
|
||||
@@ -212,14 +280,46 @@ const translate = async () => {
|
||||
</div>
|
||||
|
||||
<!-- Target Pane -->
|
||||
<div class="flex-1 flex flex-col min-h-0 bg-slate-50/30">
|
||||
<div class="flex items-center gap-4 px-6 py-3 border-b bg-slate-50/50">
|
||||
<select
|
||||
v-model="targetLangCode"
|
||||
class="bg-transparent border-none focus:ring-0 font-medium text-slate-700 cursor-pointer text-sm outline-none appearance-none"
|
||||
>
|
||||
<option v-for="lang in LANGUAGES" :key="lang.code" :value="lang.code">{{ lang.name }}</option>
|
||||
</select>
|
||||
<div class="flex-1 flex flex-col min-h-0 bg-slate-50/30 relative">
|
||||
<div class="flex items-center gap-3 px-6 py-3 border-b bg-slate-50/50 relative z-40">
|
||||
<!-- Custom Target Dropdown -->
|
||||
<div class="relative lang-dropdown min-w-[120px]">
|
||||
<button
|
||||
@click.stop="toggleDropdown('target')"
|
||||
class="flex items-center gap-2 px-3 py-1.5 rounded-lg hover:bg-slate-200/60 transition-colors text-sm font-semibold text-slate-700 w-full justify-between"
|
||||
>
|
||||
<span class="truncate">{{ targetLang.displayName }}</span>
|
||||
<ChevronDown :class="cn('w-4 h-4 text-slate-400 transition-transform duration-200', targetDropdownOpen && 'rotate-180')" />
|
||||
</button>
|
||||
|
||||
<!-- Dropdown Menu -->
|
||||
<transition
|
||||
enter-active-class="transition duration-100 ease-out"
|
||||
enter-from-class="transform scale-95 opacity-0"
|
||||
enter-to-class="transform scale-100 opacity-100"
|
||||
leave-active-class="transition duration-75 ease-in"
|
||||
leave-from-class="transform scale-100 opacity-100"
|
||||
leave-to-class="transform scale-95 opacity-0"
|
||||
>
|
||||
<div
|
||||
v-if="targetDropdownOpen"
|
||||
class="absolute left-0 mt-2 w-56 max-h-80 overflow-y-auto bg-white rounded-xl shadow-xl border border-slate-200 z-50 py-2 flex flex-col custom-scrollbar"
|
||||
>
|
||||
<button
|
||||
v-for="lang in LANGUAGES"
|
||||
:key="lang.code"
|
||||
@click="targetLangCode = lang.code; targetDropdownOpen = false"
|
||||
:class="cn(
|
||||
'px-4 py-2.5 text-sm text-left transition-colors flex items-center justify-between',
|
||||
targetLangCode === lang.code ? 'bg-blue-50 text-blue-600 font-bold' : 'text-slate-600 hover:bg-slate-50'
|
||||
)"
|
||||
>
|
||||
{{ lang.displayName }}
|
||||
<Check v-if="targetLangCode === lang.code" class="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
<div class="ml-auto flex items-center gap-2">
|
||||
<button @click="copyTarget" class="p-1.5 hover:bg-slate-200 rounded-md transition-colors relative" title="复制结果">
|
||||
<Check v-if="showCopyFeedback" class="w-4 h-4 text-green-600" />
|
||||
|
||||
@@ -3,17 +3,26 @@ import { defineStore } from 'pinia';
|
||||
import { useLocalStorage } from '@vueuse/core';
|
||||
|
||||
export interface Language {
|
||||
name: string;
|
||||
code: string;
|
||||
displayName: string; // UI 显示的中文名,如 "英语(英国)"
|
||||
englishName: string; // 文件中的第二列,用于 {SOURCE_LANG}
|
||||
code: string; // 文件中的第一列,用于 {SOURCE_CODE}
|
||||
}
|
||||
|
||||
export const LANGUAGES: Language[] = [
|
||||
{ name: '英语', code: 'en' },
|
||||
{ name: '中文', code: 'zh-Hans' },
|
||||
{ name: '日语', code: 'ja' },
|
||||
{ name: '西班牙语', code: 'es' },
|
||||
{ name: '法语', code: 'fr' },
|
||||
{ name: '德语', code: 'de' },
|
||||
{ displayName: '中文(简体)', englishName: 'Simplified Chinese', code: 'zh-Hans' },
|
||||
{ displayName: '中文(繁体)', englishName: 'Traditional Chinese', code: 'zh-Hant' },
|
||||
{ displayName: '英语', englishName: 'English', code: 'en' },
|
||||
{ displayName: '日语', englishName: 'Japanese', code: 'ja' },
|
||||
{ displayName: '韩语', englishName: 'Korean', code: 'ko' },
|
||||
{ displayName: '法语', englishName: 'French', code: 'fr' },
|
||||
{ displayName: '德语', englishName: 'German', code: 'de' },
|
||||
{ displayName: '西班牙语', englishName: 'Spanish', code: 'es' },
|
||||
{ displayName: '意大利语', englishName: 'Italian', code: 'it' },
|
||||
{ displayName: '俄语', englishName: 'Russian', code: 'ru' },
|
||||
{ displayName: '葡萄牙语', englishName: 'Portuguese', code: 'pt' },
|
||||
{ displayName: '越南语', englishName: 'Vietnamese', code: 'vi' },
|
||||
{ displayName: '泰语', englishName: 'Thai', code: 'th' },
|
||||
{ displayName: '阿拉伯语', englishName: 'Arabic', code: 'ar' },
|
||||
];
|
||||
|
||||
export const DEFAULT_TEMPLATE = `You are a professional {SOURCE_LANG} ({SOURCE_CODE}) to {TARGET_LANG} ({TARGET_CODE}) translator. Your goal is to accurately convey the meaning and nuances of the original {SOURCE_LANG} text while adhering to {TARGET_LANG} grammar, vocabulary, and cultural sensitivities.
|
||||
@@ -26,8 +35,10 @@ export const useSettingsStore = defineStore('settings', () => {
|
||||
const modelName = useLocalStorage('model-name', 'translategemma:12b');
|
||||
const enableStreaming = useLocalStorage('enable-streaming', true);
|
||||
const systemPromptTemplate = useLocalStorage('system-prompt-template', DEFAULT_TEMPLATE);
|
||||
const sourceLang = useLocalStorage('source-lang', LANGUAGES[1]); // Default Chinese
|
||||
const targetLang = useLocalStorage('target-lang', LANGUAGES[0]); // Default English
|
||||
|
||||
// 存储整个对象以保持一致性
|
||||
const sourceLang = useLocalStorage<Language>('source-lang-v2', LANGUAGES[0]);
|
||||
const targetLang = useLocalStorage<Language>('target-lang-v2', LANGUAGES[4]);
|
||||
|
||||
const logs = ref<{ timestamp: string; type: 'request' | 'response' | 'error'; content: any }[]>([]);
|
||||
|
||||
@@ -37,7 +48,7 @@ export const useSettingsStore = defineStore('settings', () => {
|
||||
type,
|
||||
content
|
||||
});
|
||||
if (logs.value.length > 20) logs.value.pop(); // Keep last 20 logs
|
||||
if (logs.value.length > 20) logs.value.pop();
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user