support api profiles

This commit is contained in:
Julian Freeman
2026-02-23 18:54:59 -04:00
parent f84ee6ced7
commit f7f7556b98
2 changed files with 120 additions and 4 deletions

View File

@@ -14,7 +14,10 @@ import {
Sun,
Moon,
User,
Type
Type,
Plus,
Save,
Play
} from 'lucide-vue-next';
import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';
@@ -23,7 +26,8 @@ import {
LANGUAGES,
DEFAULT_TEMPLATE,
SPEAKER_IDENTITY_OPTIONS,
TONE_REGISTER_OPTIONS
TONE_REGISTER_OPTIONS,
type ApiProfile
} from './stores/settings';
import pkg from '../package.json';
import { clsx, type ClassValue } from 'clsx';
@@ -50,6 +54,36 @@ const toggleTheme = () => {
const view = ref<'translate' | 'settings' | 'logs'>('translate');
// Profile Management
const newProfileName = ref('');
const isSavingProfile = ref(false);
const saveCurrentAsProfile = () => {
if (!newProfileName.value.trim()) return;
const newProfile: ApiProfile = {
id: crypto.randomUUID(),
name: newProfileName.value.trim(),
apiBaseUrl: settings.apiBaseUrl,
apiKey: settings.apiKey,
modelName: settings.modelName
};
settings.profiles.push(newProfile);
newProfileName.value = '';
isSavingProfile.value = false;
};
const applyProfile = (p: ApiProfile) => {
settings.apiBaseUrl = p.apiBaseUrl;
settings.apiKey = p.apiKey;
settings.modelName = p.modelName;
};
const deleteProfile = (id: string) => {
settings.profiles = settings.profiles.filter(p => p.id !== id);
};
// Dropdown State
const sourceDropdownOpen = ref(false);
const targetDropdownOpen = ref(false);
@@ -466,7 +500,79 @@ const translate = async () => {
<div v-else-if="view === 'settings'" class="flex-1 overflow-y-auto bg-slate-50 dark:bg-slate-950 p-6 md:p-10 min-h-0">
<div class="max-w-2xl mx-auto space-y-8">
<section>
<h2 class="text-sm font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider mb-4">模型接口配置</h2>
<h2 class="text-sm font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider mb-4">API 配置预设</h2>
<div class="bg-white dark:bg-slate-900 rounded-xl shadow-sm border dark:border-slate-800 divide-y dark:divide-slate-800">
<div v-if="settings.profiles.length === 0" class="p-8 text-center text-sm text-slate-400 dark:text-slate-600 italic">
暂无预设配置
</div>
<div
v-for="profile in settings.profiles"
:key="profile.id"
class="p-4 flex items-center justify-between group hover:bg-slate-50 dark:hover:bg-slate-800/50 transition-colors"
>
<div class="flex flex-col gap-0.5 min-w-0">
<span class="text-sm font-semibold text-slate-700 dark:text-slate-200 truncate">{{ profile.name }}</span>
<div class="flex items-center gap-2 text-[10px] text-slate-400 dark:text-slate-500 font-mono">
<span class="truncate max-w-30">{{ profile.modelName }}</span>
<span></span>
<span class="truncate">{{ profile.apiBaseUrl }}</span>
</div>
</div>
<div class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<button
@click="applyProfile(profile)"
class="p-2 text-blue-600 hover:bg-blue-50 dark:hover:bg-blue-900/30 rounded-lg transition-colors"
title="应用此配置"
>
<Play class="w-4 h-4 fill-current" />
</button>
<button
@click="deleteProfile(profile.id)"
class="p-2 text-red-600 hover:bg-red-50 dark:hover:bg-red-900/30 rounded-lg transition-colors"
title="删除"
>
<Trash2 class="w-4 h-4" />
</button>
</div>
</div>
</div>
</section>
<section>
<div class="flex items-center justify-between mb-4">
<h2 class="text-sm font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider">模型接口配置</h2>
<div v-if="!isSavingProfile" class="flex items-center gap-2">
<button
@click="isSavingProfile = true"
class="text-xs flex items-center gap-1.5 text-blue-600 dark:text-blue-400 hover:underline px-2 py-1 rounded"
>
<Save class="w-3.5 h-3.5" />
保存为预设
</button>
</div>
<div v-else class="flex items-center gap-2 bg-white dark:bg-slate-800 p-1 rounded-lg border dark:border-slate-700 shadow-sm animate-in fade-in zoom-in duration-200">
<input
v-model="newProfileName"
type="text"
placeholder="输入预设名称..."
class="text-xs px-2 py-1 bg-transparent outline-none w-32 dark:text-slate-200"
@keyup.enter="saveCurrentAsProfile"
/>
<button
@click="saveCurrentAsProfile"
:disabled="!newProfileName.trim()"
class="p-1 text-green-600 hover:bg-green-50 dark:hover:bg-green-900/30 rounded disabled:opacity-50"
>
<Check class="w-3.5 h-3.5" />
</button>
<button
@click="isSavingProfile = false; newProfileName = ''"
class="p-1 text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-700 rounded"
>
<Plus class="w-3.5 h-3.5 rotate-45" />
</button>
</div>
</div>
<div class="bg-white dark:bg-slate-900 rounded-xl shadow-sm border dark:border-slate-800 p-6 space-y-4">
<div class="space-y-2">
<label class="text-sm font-medium text-slate-700 dark:text-slate-300">API Base URL</label>
@@ -571,7 +677,7 @@ const translate = async () => {
)"
>{{ log.type === 'request' ? '请求' : log.type === 'response' ? '响应' : '错误' }}</span>
</div>
<pre class="bg-slate-50 dark:bg-slate-800/50 p-3 rounded-lg overflow-x-auto text-slate-600 dark:text-slate-300 max-h-48 leading-relaxed shadow-inner border border-slate-100 dark:border-slate-700">{{ typeof log.content === 'object' ? JSON.stringify(log.content, null, 2) : log.content }}</pre>
<pre class="bg-slate-50 dark:bg-slate-800/50 p-3 rounded-lg overflow-x-auto text-slate-600 dark:text-slate-300 max-h-56 leading-relaxed shadow-inner border border-slate-100 dark:border-slate-700">{{ typeof log.content === 'object' ? JSON.stringify(log.content, null, 2) : log.content }}</pre>
</div>
</div>
</div>

View File

@@ -54,11 +54,20 @@ Produce only the {TARGET_LANG} translation, without any additional explanations
{TEXT}`;
export interface ApiProfile {
id: string;
name: string;
apiBaseUrl: string;
apiKey: string;
modelName: string;
}
export const useSettingsStore = defineStore('settings', () => {
const isDark = useLocalStorage('is-dark', false);
const apiBaseUrl = useLocalStorage('api-base-url', 'http://localhost:11434/v1');
const apiKey = useLocalStorage('api-key', '');
const modelName = useLocalStorage('model-name', 'translategemma:12b');
const profiles = useLocalStorage<ApiProfile[]>('api-profiles', []);
const enableStreaming = useLocalStorage('enable-streaming', true);
const systemPromptTemplate = useLocalStorage('system-prompt-template', DEFAULT_TEMPLATE);
@@ -88,6 +97,7 @@ export const useSettingsStore = defineStore('settings', () => {
apiBaseUrl,
apiKey,
modelName,
profiles,
enableStreaming,
systemPromptTemplate,
sourceLang,