add identity and tone
This commit is contained in:
137
src/App.vue
137
src/App.vue
@@ -12,11 +12,19 @@ import {
|
||||
FileText,
|
||||
ChevronDown,
|
||||
Sun,
|
||||
Moon
|
||||
Moon,
|
||||
User,
|
||||
Type
|
||||
} from 'lucide-vue-next';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import { useSettingsStore, LANGUAGES, DEFAULT_TEMPLATE } from './stores/settings';
|
||||
import {
|
||||
useSettingsStore,
|
||||
LANGUAGES,
|
||||
DEFAULT_TEMPLATE,
|
||||
SPEAKER_IDENTITY_OPTIONS,
|
||||
TONE_REGISTER_OPTIONS
|
||||
} from './stores/settings';
|
||||
import pkg from '../package.json';
|
||||
import { clsx, type ClassValue } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
@@ -45,20 +53,29 @@ const view = ref<'translate' | 'settings' | 'logs'>('translate');
|
||||
// Dropdown State
|
||||
const sourceDropdownOpen = ref(false);
|
||||
const targetDropdownOpen = ref(false);
|
||||
const speakerDropdownOpen = ref(false);
|
||||
const toneDropdownOpen = ref(false);
|
||||
|
||||
const closeAllDropdowns = () => {
|
||||
sourceDropdownOpen.value = false;
|
||||
targetDropdownOpen.value = false;
|
||||
speakerDropdownOpen.value = false;
|
||||
toneDropdownOpen.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 toggleDropdown = (type: 'source' | 'target' | 'speaker' | 'tone') => {
|
||||
const states = {
|
||||
source: sourceDropdownOpen,
|
||||
target: targetDropdownOpen,
|
||||
speaker: speakerDropdownOpen,
|
||||
tone: toneDropdownOpen
|
||||
};
|
||||
|
||||
const targetState = states[type];
|
||||
const currentValue = targetState.value;
|
||||
|
||||
closeAllDropdowns();
|
||||
targetState.value = !currentValue;
|
||||
};
|
||||
|
||||
const handleGlobalClick = (e: MouseEvent) => {
|
||||
@@ -116,6 +133,14 @@ const targetLangCode = computed({
|
||||
const sourceLang = computed(() => settings.sourceLang);
|
||||
const targetLang = computed(() => settings.targetLang);
|
||||
|
||||
const currentSpeakerLabel = computed(() => {
|
||||
return SPEAKER_IDENTITY_OPTIONS.find(opt => opt.value === settings.speakerIdentity)?.label || '男性';
|
||||
});
|
||||
|
||||
const currentToneLabel = computed(() => {
|
||||
return TONE_REGISTER_OPTIONS.find(opt => opt.value === settings.toneRegister)?.label || '正式专业';
|
||||
});
|
||||
|
||||
const swapLanguages = () => {
|
||||
const temp = { ...settings.sourceLang };
|
||||
settings.sourceLang = { ...settings.targetLang };
|
||||
@@ -150,6 +175,8 @@ const translate = async () => {
|
||||
.replace(/{SOURCE_CODE}/g, sourceLang.value.code)
|
||||
.replace(/{TARGET_LANG}/g, targetLang.value.englishName)
|
||||
.replace(/{TARGET_CODE}/g, targetLang.value.code)
|
||||
.replace(/{SPEAKER_IDENTITY}/g, settings.speakerIdentity)
|
||||
.replace(/{TONE_REGISTER}/g, settings.toneRegister)
|
||||
.replace(/{TEXT}/g, sourceText.value);
|
||||
|
||||
const requestBody = {
|
||||
@@ -328,6 +355,94 @@ const translate = async () => {
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<!-- Speaker Identity Dropdown -->
|
||||
<div class="relative lang-dropdown min-w-24">
|
||||
<button
|
||||
@click.stop="toggleDropdown('speaker')"
|
||||
class="flex items-center gap-2 px-3 py-1.5 rounded-lg hover:bg-slate-200/60 dark:hover:bg-slate-700/60 transition-colors text-sm font-medium text-slate-600 dark:text-slate-300 w-full justify-between group"
|
||||
title="说话人身份"
|
||||
>
|
||||
<div class="flex items-center gap-1.5 truncate">
|
||||
<User class="w-3.5 h-3.5 text-slate-400" />
|
||||
<span class="truncate">{{ currentSpeakerLabel }}</span>
|
||||
</div>
|
||||
<ChevronDown :class="cn('w-3.5 h-3.5 text-slate-400 transition-transform duration-200 shrink-0', speakerDropdownOpen && 'rotate-180')" />
|
||||
</button>
|
||||
|
||||
<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="speakerDropdownOpen"
|
||||
class="absolute left-0 mt-2 w-40 max-h-80 overflow-y-auto bg-white dark:bg-slate-800 rounded-xl shadow-xl border border-slate-200 dark:border-slate-700 z-50 py-2 flex flex-col custom-scrollbar"
|
||||
>
|
||||
<button
|
||||
v-for="opt in SPEAKER_IDENTITY_OPTIONS"
|
||||
:key="opt.value"
|
||||
@click="settings.speakerIdentity = opt.value; speakerDropdownOpen = false"
|
||||
:class="cn(
|
||||
'px-4 py-2 text-sm text-left transition-colors flex items-center justify-between',
|
||||
settings.speakerIdentity === opt.value ? 'bg-blue-50 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400 font-bold' : 'text-slate-600 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700/50'
|
||||
)"
|
||||
>
|
||||
{{ opt.label }}
|
||||
<Check v-if="settings.speakerIdentity === opt.value" class="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<!-- Tone & Register Dropdown -->
|
||||
<div class="relative lang-dropdown min-w-32">
|
||||
<button
|
||||
@click.stop="toggleDropdown('tone')"
|
||||
class="flex items-center gap-2 px-3 py-1.5 rounded-lg hover:bg-slate-200/60 dark:hover:bg-slate-700/60 transition-colors text-sm font-medium text-slate-600 dark:text-slate-300 w-full justify-between group"
|
||||
title="语气风格"
|
||||
>
|
||||
<div class="flex items-center gap-1.5 truncate">
|
||||
<Type class="w-3.5 h-3.5 text-slate-400" />
|
||||
<span class="truncate">{{ currentToneLabel }}</span>
|
||||
</div>
|
||||
<ChevronDown :class="cn('w-3.5 h-3.5 text-slate-400 transition-transform duration-200 shrink-0', toneDropdownOpen && 'rotate-180')" />
|
||||
</button>
|
||||
|
||||
<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="toneDropdownOpen"
|
||||
class="absolute left-0 mt-2 w-56 max-h-80 overflow-y-auto bg-white dark:bg-slate-800 rounded-xl shadow-xl border border-slate-200 dark:border-slate-700 z-50 py-2 flex flex-col custom-scrollbar"
|
||||
>
|
||||
<button
|
||||
v-for="opt in TONE_REGISTER_OPTIONS"
|
||||
:key="opt.value"
|
||||
@click="settings.toneRegister = opt.value; toneDropdownOpen = false"
|
||||
:class="cn(
|
||||
'px-4 py-2.5 text-sm text-left transition-colors flex flex-col gap-0.5',
|
||||
settings.toneRegister === opt.value ? 'bg-blue-50 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400 font-bold' : 'text-slate-600 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700/50'
|
||||
)"
|
||||
>
|
||||
<div class="flex items-center justify-between w-full">
|
||||
{{ opt.label }}
|
||||
<Check v-if="settings.toneRegister === opt.value" class="w-3.5 h-3.5" />
|
||||
</div>
|
||||
<span class="text-[10px] opacity-60 font-normal truncate">{{ opt.description }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<div class="ml-auto flex items-center gap-2">
|
||||
<button @click="copyTarget" class="p-1.5 hover:bg-slate-200 dark:hover:bg-slate-700 rounded-md transition-colors relative" title="复制结果">
|
||||
<Check v-if="showCopyFeedback" class="w-4 h-4 text-green-600" />
|
||||
@@ -403,7 +518,7 @@ const translate = async () => {
|
||||
class="w-full px-4 py-3 border dark:border-slate-700 rounded-lg bg-transparent focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 outline-none transition-all font-mono text-xs leading-relaxed text-slate-900 dark:text-slate-100"
|
||||
></textarea>
|
||||
<div class="flex flex-wrap gap-2 mt-2">
|
||||
<span v-for="tag in ['{SOURCE_LANG}', '{SOURCE_CODE}', '{TARGET_LANG}', '{TARGET_CODE}', '{TEXT}']" :key="tag" class="px-2 py-1 bg-slate-100 dark:bg-slate-800 text-[10px] font-mono rounded border dark:border-slate-700 text-slate-600 dark:text-slate-400">{{ tag }}</span>
|
||||
<span v-for="tag in ['{SOURCE_LANG}', '{SOURCE_CODE}', '{TARGET_LANG}', '{TARGET_CODE}', '{SPEAKER_IDENTITY}', '{TONE_REGISTER}', '{TEXT}']" :key="tag" class="px-2 py-1 bg-slate-100 dark:bg-slate-800 text-[10px] font-mono rounded border dark:border-slate-700 text-slate-600 dark:text-slate-400">{{ tag }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user