新增 完整的国际化(i18n)支持,支持中英日三种语言

This commit is contained in:
2026-04-18 00:52:34 +08:00
parent d184cfef6e
commit 3577e139b9
13 changed files with 786 additions and 169 deletions

View File

@@ -1,17 +1,17 @@
<template>
<div class="app">
<div class="app" :class="themeClass">
<div class="titlebar">
<div class="titlebar-left">
<span class="titlebar-title">iFlow 设置编辑器</span>
<span class="titlebar-title">{{ $t('app.title') }}</span>
</div>
<div class="titlebar-controls">
<button class="titlebar-btn" @click="minimize" title="最小化">
<button class="titlebar-btn" @click="minimize" :title="$t('window.minimize')">
<svg viewBox="0 0 10 1"><line x1="0" y1="0.5" x2="10" y2="0.5" /></svg>
</button>
<button class="titlebar-btn" @click="maximize" title="最大化">
<button class="titlebar-btn" @click="maximize" :title="$t('window.maximize')">
<svg viewBox="0 0 10 10"><rect x="0.5" y="0.5" width="9" height="9" stroke-width="1" stroke="currentColor" fill="none" /></svg>
</button>
<button class="titlebar-btn close" @click="close" title="关闭">
<button class="titlebar-btn close" @click="close" :title="$t('window.close')">
<svg viewBox="0 0 10 10">
<line x1="0" y1="0" x2="10" y2="10" />
<line x1="10" y1="0" x2="0" y2="10" />
@@ -22,21 +22,21 @@
<main class="main">
<aside class="sidebar">
<div class="sidebar-section">
<div class="sidebar-title">常规</div>
<div class="sidebar-title">{{ $t('sidebar.general') }}</div>
<div class="nav-item" :class="{ active: currentSection === 'general' }" @click="showSection('general')">
<Config size="16" />
<span class="nav-item-text">基本设置</span>
<span class="nav-item-text">{{ $t('sidebar.basicSettings') }}</span>
</div>
<div class="nav-item" :class="{ active: currentSection === 'api' }" @click="showSection('api')">
<Key size="16" />
<span class="nav-item-text">API 配置</span>
<span class="nav-item-text">{{ $t('sidebar.apiConfig') }}</span>
</div>
</div>
<div class="sidebar-section">
<div class="sidebar-title">高级</div>
<div class="sidebar-title">{{ $t('sidebar.advanced') }}</div>
<div class="nav-item" :class="{ active: currentSection === 'mcp' }" @click="showSection('mcp')">
<Server size="16" />
<span class="nav-item-text">MCP 服务器</span>
<span class="nav-item-text">{{ $t('sidebar.mcpServers') }}</span>
<span class="nav-item-badge">{{ serverCount }}</span>
</div>
</div>
@@ -44,30 +44,30 @@
<div class="content">
<section v-if="currentSection === 'general'">
<div class="content-header">
<h1 class="content-title">基本设置</h1>
<p class="content-desc">配置应用程序的常规选项</p>
<h1 class="content-title">{{ $t('general.title') }}</h1>
<p class="content-desc">{{ $t('general.description') }}</p>
</div>
<div class="card">
<div class="card-title">
<Globe size="16" />
语言与界面
{{ $t('general.languageInterface') }}
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">语言</label>
<label class="form-label">{{ $t('general.language') }}</label>
<select class="form-select" v-model="settings.language">
<option value="zh-CN">简体中文</option>
<option value="en-US">English</option>
<option value="ja-JP">日本語</option>
<option value="zh-CN">{{ $t('languages.zh-CN') }}</option>
<option value="en-US">{{ $t('languages.en-US') }}</option>
<option value="ja-JP">{{ $t('languages.ja-JP') }}</option>
</select>
</div>
<div class="form-group">
<label class="form-label">主题</label>
<label class="form-label">{{ $t('general.theme') }}</label>
<select class="form-select" v-model="settings.theme">
<option value="Xcode">Xcode</option>
<option value="Dark">深色</option>
<option value="Light">浅色</option>
<option value="Solarized Dark">Solarized Dark</option>
<option value="Xcode">{{ $t('theme.xcode') }}</option>
<option value="Dark">{{ $t('theme.dark') }}</option>
<option value="Light">{{ $t('theme.light') }}</option>
<option value="Solarized Dark">{{ $t('theme.solarizedDark') }}</option>
</select>
</div>
</div>
@@ -75,21 +75,21 @@
<div class="card">
<div class="card-title">
<Setting size="16" />
其他设置
{{ $t('general.otherSettings') }}
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">启动动画</label>
<label class="form-label">{{ $t('general.bootAnimation') }}</label>
<select class="form-select" v-model="settings.bootAnimationShown">
<option :value="true">已显示</option>
<option :value="false">未显示</option>
<option :value="true">{{ $t('general.bootAnimationShown') }}</option>
<option :value="false">{{ $t('general.bootAnimationNotShown') }}</option>
</select>
</div>
<div class="form-group">
<label class="form-label">检查点保存</label>
<label class="form-label">{{ $t('general.checkpointing') }}</label>
<select class="form-select" v-model="settings.checkpointing.enabled">
<option :value="true">已启用</option>
<option :value="false">已禁用</option>
<option :value="true">{{ $t('general.enabled') }}</option>
<option :value="false">{{ $t('general.disabled') }}</option>
</select>
</div>
</div>
@@ -97,16 +97,16 @@
</section>
<section v-if="currentSection === 'api'">
<div class="content-header">
<h1 class="content-title">API 配置</h1>
<p class="content-desc">配置 AI 服务和搜索 API</p>
<h1 class="content-title">{{ $t('api.title') }}</h1>
<p class="content-desc">{{ $t('api.description') }}</p>
</div>
<div class="card">
<div class="card-title">
<Exchange size="16" />
配置文件管理
{{ $t('api.profileManagement') }}
<button class="btn btn-primary btn-sm" @click="createNewApiProfile" style="margin-left: auto">
<Add size="14" />
新建配置
{{ $t('api.newProfile') }}
</button>
</div>
<div class="profile-list">
@@ -123,17 +123,17 @@
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3,8 6,11 13,4"></polyline>
</svg>
使用中
{{ $t('api.inUse') }}
</span>
</div>
<div class="profile-actions">
<button class="action-btn" @click.stop="openApiEditDialog(profile.name)" title="编辑">
<button class="action-btn" @click.stop="openApiEditDialog(profile.name)" :title="$t('api.edit')">
<Edit size="14" />
</button>
<button class="action-btn" @click.stop="duplicateApiProfile(profile.name)" title="复制">
<button class="action-btn" @click.stop="duplicateApiProfile(profile.name)" :title="$t('api.duplicate')">
<Copy size="14" />
</button>
<button class="action-btn action-btn-danger" v-if="profile.name !== 'default'" @click.stop="deleteApiProfile(profile.name)" title="删除">
<button class="action-btn action-btn-danger" v-if="profile.name !== 'default'" @click.stop="deleteApiProfile(profile.name)" :title="$t('api.delete')">
<Delete size="14" />
</button>
</div>
@@ -143,15 +143,15 @@
</section>
<section v-if="currentSection === 'mcp'">
<div class="content-header">
<h1 class="content-title">MCP 服务器</h1>
<p class="content-desc">管理 Model Context Protocol 服务器配置</p>
<h1 class="content-title">{{ $t('mcp.title') }}</h1>
<p class="content-desc">{{ $t('mcp.description') }}</p>
</div>
<div class="form-group">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px">
<label class="form-label" style="margin: 0">服务器列表</label>
<label class="form-label" style="margin: 0">{{ $t('mcp.serverList') }}</label>
<button class="btn btn-primary" @click="addServer" style="padding: 6px 12px; font-size: 12px">
<Add size="12" />
添加服务器
{{ $t('mcp.addServerBtn') }}
</button>
</div>
<div class="server-list">
@@ -159,15 +159,15 @@
<div v-for="(config, name) in settings.mcpServers" :key="name" class="server-item" :class="{ selected: currentServerName === name }" @click="selectServer(name)">
<div class="server-info">
<div class="server-name">{{ name }}</div>
<div class="server-desc">{{ config.description || '无描述' }}</div>
<div class="server-desc">{{ config.description || $t('mcp.noDescription') }}</div>
</div>
<div class="server-status"></div>
</div>
</template>
<div v-else class="empty-state">
<Server size="48" class="empty-state-icon" />
<div class="empty-state-title">暂无 MCP 服务器</div>
<div class="empty-state-desc">点击上方按钮添加第一个服务器</div>
<div class="empty-state-title">{{ $t('mcp.noServers') }}</div>
<div class="empty-state-desc">{{ $t('mcp.addFirstServer') }}</div>
</div>
</div>
</div>
@@ -177,7 +177,7 @@
<footer class="footer">
<div class="footer-status">
<div class="footer-status-dot"></div>
<span>配置: {{ currentApiProfile || 'default' }}</span>
<span>{{ $t('api.currentConfig') }}: {{ currentApiProfile || 'default' }}</span>
</div>
</footer>
<!-- Input Dialog -->
@@ -187,8 +187,8 @@
<div v-if="showInputDialog.isConfirm" class="dialog-confirm-text">{{ showInputDialog.placeholder }}</div>
<input v-else type="text" class="form-input" v-model="inputDialogValue" :placeholder="showInputDialog.placeholder" @keyup.enter="closeInputDialog(true)" autofocus />
<div class="dialog-actions">
<button class="btn btn-secondary" @click="closeInputDialog(false)">取消</button>
<button class="btn btn-primary" @click="closeInputDialog(true)">确定</button>
<button class="btn btn-secondary" @click="closeInputDialog(false)">{{ $t('dialog.cancel') }}</button>
<button class="btn btn-primary" @click="closeInputDialog(true)">{{ $t('dialog.confirm') }}</button>
</div>
</div>
</div>
@@ -197,8 +197,8 @@
<div class="dialog api-edit-dialog" @click.stop>
<div class="dialog-header">
<div class="dialog-title">
<Add size="18" />
新建 API 配置
<Key size="18" />
{{ $t('api.createTitle') }}
</div>
<button class="side-panel-close" @click="closeApiCreateDialog">
<svg viewBox="0 0 10 10">
@@ -209,43 +209,43 @@
</div>
<div class="dialog-body">
<div class="form-group">
<label class="form-label">配置名称 <span class="form-required">*</span></label>
<input type="text" class="form-input" v-model="creatingApiData.name" placeholder="请输入配置名称" />
<label class="form-label">{{ $t('api.configName') }} <span class="form-required">*</span></label>
<input type="text" class="form-input" v-model="creatingApiData.name" :placeholder="$t('api.configNamePlaceholder')" />
</div>
<div class="form-group">
<label class="form-label">认证方式</label>
<label class="form-label">{{ $t('api.authType') }}</label>
<select class="form-select" v-model="creatingApiData.selectedAuthType">
<option value="iflow">iFlow</option>
<option value="api">API Key</option>
<option value="openai-compatible">OpenAI 兼容</option>
<option value="iflow">{{ $t('api.auth.iflow') }}</option>
<option value="api">{{ $t('api.auth.api') }}</option>
<option value="openai-compatible">{{ $t('api.auth.openaiCompatible') }}</option>
</select>
</div>
<div class="form-group">
<label class="form-label">API Key</label>
<input type="password" class="form-input" v-model="creatingApiData.apiKey" placeholder="sk-cp-XXXXX..." />
<label class="form-label">{{ $t('api.apiKey') }}</label>
<input type="password" class="form-input" v-model="creatingApiData.apiKey" :placeholder="$t('api.apiKeyPlaceholder')" />
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Base URL</label>
<input type="text" class="form-input" v-model="creatingApiData.baseUrl" placeholder="https://api.minimaxi.com/v1" />
<label class="form-label">{{ $t('api.baseUrl') }}</label>
<input type="text" class="form-input" v-model="creatingApiData.baseUrl" :placeholder="$t('api.baseUrlPlaceholder')" />
</div>
<div class="form-group">
<label class="form-label">模型名称</label>
<input type="text" class="form-input" v-model="creatingApiData.modelName" placeholder="MiniMax-M2.7" />
<label class="form-label">{{ $t('api.modelName') }}</label>
<input type="text" class="form-input" v-model="creatingApiData.modelName" :placeholder="$t('api.modelNamePlaceholder')" />
</div>
</div>
<div class="form-group">
<label class="form-label">搜索 API Key</label>
<input type="password" class="form-input" v-model="creatingApiData.searchApiKey" placeholder="sk-XXXXX..." />
<label class="form-label">{{ $t('api.searchApiKey') }}</label>
<input type="password" class="form-input" v-model="creatingApiData.searchApiKey" :placeholder="$t('api.searchApiKeyPlaceholder')" />
</div>
<div class="form-group">
<label class="form-label">CNA</label>
<input type="text" class="form-input" v-model="creatingApiData.cna" placeholder="CNA 标识" />
<label class="form-label">{{ $t('api.cna') }}</label>
<input type="text" class="form-input" v-model="creatingApiData.cna" :placeholder="$t('api.cnaPlaceholder')" />
</div>
</div>
<div class="dialog-actions">
<button class="btn btn-secondary" @click="closeApiCreateDialog">取消</button>
<button class="btn btn-primary" @click="saveApiCreate"> <Save size="14" /> 创建 </button>
<button class="btn btn-secondary" @click="closeApiCreateDialog">{{ $t('dialog.cancel') }}</button>
<button class="btn btn-primary" @click="saveApiCreate"> <Save size="14" /> {{ $t('api.create') }} </button>
</div>
</div>
</div>
@@ -255,7 +255,7 @@
<div class="dialog-header">
<div class="dialog-title">
<Key size="18" />
编辑 API 配置
{{ $t('api.editTitle') }}
</div>
<button class="side-panel-close" @click="closeApiEditDialog">
<svg viewBox="0 0 10 10">
@@ -266,39 +266,39 @@
</div>
<div class="dialog-body">
<div class="form-group">
<label class="form-label">认证方式</label>
<label class="form-label">{{ $t('api.authType') }}</label>
<select class="form-select" v-model="editingApiData.selectedAuthType">
<option value="iflow">iFlow</option>
<option value="api">API Key</option>
<option value="openai-compatible">OpenAI 兼容</option>
<option value="iflow">{{ $t('api.auth.iflow') }}</option>
<option value="api">{{ $t('api.auth.api') }}</option>
<option value="openai-compatible">{{ $t('api.auth.openaiCompatible') }}</option>
</select>
</div>
<div class="form-group">
<label class="form-label">API Key</label>
<input type="password" class="form-input" v-model="editingApiData.apiKey" placeholder="sk-cp-XXXXX..." />
<label class="form-label">{{ $t('api.apiKey') }}</label>
<input type="password" class="form-input" v-model="editingApiData.apiKey" :placeholder="$t('api.apiKeyPlaceholder')" />
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Base URL</label>
<input type="text" class="form-input" v-model="editingApiData.baseUrl" placeholder="https://api.minimaxi.com/v1" />
<label class="form-label">{{ $t('api.baseUrl') }}</label>
<input type="text" class="form-input" v-model="editingApiData.baseUrl" :placeholder="$t('api.baseUrlPlaceholder')" />
</div>
<div class="form-group">
<label class="form-label">模型名称</label>
<input type="text" class="form-input" v-model="editingApiData.modelName" placeholder="MiniMax-M2.7" />
<label class="form-label">{{ $t('api.modelName') }}</label>
<input type="text" class="form-input" v-model="editingApiData.modelName" :placeholder="$t('api.modelNamePlaceholder')" />
</div>
</div>
<div class="form-group">
<label class="form-label">搜索 API Key</label>
<input type="password" class="form-input" v-model="editingApiData.searchApiKey" placeholder="sk-XXXXX..." />
<label class="form-label">{{ $t('api.searchApiKey') }}</label>
<input type="password" class="form-input" v-model="editingApiData.searchApiKey" :placeholder="$t('api.searchApiKeyPlaceholder')" />
</div>
<div class="form-group">
<label class="form-label">CNA</label>
<input type="text" class="form-input" v-model="editingApiData.cna" placeholder="CNA 标识" />
<label class="form-label">{{ $t('api.cna') }}</label>
<input type="text" class="form-input" v-model="editingApiData.cna" :placeholder="$t('api.cnaPlaceholder')" />
</div>
</div>
<div class="dialog-actions">
<button class="btn btn-secondary" @click="closeApiEditDialog">取消</button>
<button class="btn btn-primary" @click="saveApiEdit"> <Save size="14" /> 保存 </button>
<button class="btn btn-secondary" @click="closeApiEditDialog">{{ $t('dialog.cancel') }}</button>
<button class="btn btn-primary" @click="saveApiEdit"> <Save size="14" /> {{ $t('api.save') }} </button>
</div>
</div>
</div>
@@ -308,7 +308,7 @@
<div class="side-panel-header">
<div class="side-panel-title">
<Server size="18" />
{{ isEditingServer ? '编辑服务器' : '添加服务器' }}
{{ isEditingServer ? $t('mcp.editServer') : $t('mcp.addServer') }}
</div>
<button class="side-panel-close" @click="closeServerPanel">
<svg viewBox="0 0 10 10">
@@ -319,40 +319,40 @@
</div>
<div class="side-panel-body">
<div class="form-group">
<label class="form-label">服务器名称 <span class="form-required">*</span></label>
<input type="text" class="form-input" v-model="editingServerData.name" placeholder="my-mcp-server" />
<label class="form-label">{{ $t('mcp.serverName') }} <span class="form-required">*</span></label>
<input type="text" class="form-input" v-model="editingServerData.name" :placeholder="$t('mcp.serverNamePlaceholder')" />
</div>
<div class="form-group">
<label class="form-label">描述</label>
<input type="text" class="form-input" v-model="editingServerData.description" placeholder="服务器描述信息" />
<label class="form-label">{{ $t('mcp.descriptionLabel') }}</label>
<input type="text" class="form-input" v-model="editingServerData.description" :placeholder="$t('mcp.descriptionPlaceholder')" />
</div>
<div class="form-group">
<label class="form-label">命令 <span class="form-required">*</span></label>
<input type="text" class="form-input" v-model="editingServerData.command" placeholder="npx" />
<label class="form-label">{{ $t('mcp.command') }} <span class="form-required">*</span></label>
<input type="text" class="form-input" v-model="editingServerData.command" :placeholder="$t('mcp.commandPlaceholder')" />
</div>
<div class="form-group">
<label class="form-label">工作目录</label>
<input type="text" class="form-input" v-model="editingServerData.cwd" placeholder="." />
<label class="form-label">{{ $t('mcp.workingDir') }}</label>
<input type="text" class="form-input" v-model="editingServerData.cwd" :placeholder="$t('mcp.cwdPlaceholder')" />
</div>
<div class="form-group">
<label class="form-label">参数 (每行一个)</label>
<textarea class="form-textarea" v-model="editingServerData.args" rows="4" placeholder="-y&#10;package-name"></textarea>
<label class="form-label">{{ $t('mcp.args') }}</label>
<textarea class="form-textarea" v-model="editingServerData.args" rows="4" :placeholder="$t('mcp.argsPlaceholder')"></textarea>
</div>
<div class="form-group">
<label class="form-label">环境变量 (JSON 格式)</label>
<textarea class="form-textarea" v-model="editingServerData.env" rows="3" placeholder='{"API_KEY": "xxx"}'></textarea>
<label class="form-label">{{ $t('mcp.envVars') }}</label>
<textarea class="form-textarea" v-model="editingServerData.env" rows="3" :placeholder="$t('mcp.envVarsPlaceholder')"></textarea>
</div>
</div>
<div class="side-panel-footer">
<button v-if="isEditingServer" class="btn btn-danger" @click="deleteServer">
<Delete size="14" />
删除
{{ $t('mcp.delete') }}
</button>
<div class="side-panel-footer-right">
<button class="btn btn-secondary" @click="closeServerPanel">取消</button>
<button class="btn btn-secondary" @click="closeServerPanel">{{ $t('dialog.cancel') }}</button>
<button class="btn btn-primary" @click="saveServerFromPanel">
<Save size="14" />
{{ isEditingServer ? '保存更改' : '添加服务器' }}
{{ isEditingServer ? $t('mcp.saveChanges') : $t('mcp.addServer') }}
</button>
</div>
</div>
@@ -384,7 +384,7 @@
<div class="message-dialog-title">{{ showMessageDialog.title }}</div>
<div class="message-dialog-message">{{ showMessageDialog.message }}</div>
<div class="dialog-actions">
<button class="btn btn-primary" @click="closeMessageDialog">确定</button>
<button class="btn btn-primary" @click="closeMessageDialog">{{ $t('dialog.confirm') }}</button>
</div>
</div>
</div>
@@ -392,7 +392,9 @@
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
import { useI18n } from 'vue-i18n'
import { Save, Config, Key, Server, Globe, Setting, Add, Edit, Delete, Exchange, Copy } from '@icon-park/vue-next'
const { locale, t } = useI18n()
const settings = ref({
language: 'zh-CN',
theme: 'Xcode',
@@ -472,7 +474,7 @@ const switchApiProfile = async () => {
originalSettings.value = JSON.parse(JSON.stringify(data))
modified.value = false
} else {
await showMessage({ type: 'error', title: '切换失败', message: result.error })
await showMessage({ type: 'error', title: t('api.switchFailed'), message: result.error })
}
}
// Create new API profile
@@ -496,7 +498,7 @@ const closeApiCreateDialog = () => {
const saveApiCreate = async () => {
const name = creatingApiData.value.name.trim()
if (!name) {
await showMessage({ type: 'warning', title: '错误', message: '请输入配置名称' })
await showMessage({ type: 'warning', title: t('messages.error'), message: t('messages.inputConfigName') })
return
}
const result = await window.electronAPI.createApiProfile(name)
@@ -519,24 +521,24 @@ const saveApiCreate = async () => {
await window.electronAPI.saveSettings(data)
showApiCreateDialog.value = false
await loadApiProfiles()
await showMessage({ type: 'info', title: '创建成功', message: `配置 "${name}" 已创建` })
await showMessage({ type: 'info', title: t('messages.success'), message: t('api.configCreated', { name }) })
}
} else {
await showMessage({ type: 'error', title: '创建失败', message: result.error })
await showMessage({ type: 'error', title: t('messages.error'), message: result.error })
}
}
// Delete API profile
const deleteApiProfile = async name => {
const profileName = name || currentApiProfile.value
if (profileName === 'default') {
await showMessage({ type: 'warning', title: '无法删除', message: '不能删除默认配置' })
await showMessage({ type: 'warning', title: t('messages.warning'), message: t('messages.cannotDeleteDefault') })
return
}
const confirmed = await new Promise(resolve => {
showInputDialog.value = {
show: true,
title: '删除配置',
placeholder: `确定要删除配置 "${profileName}" 吗?`,
title: t('api.delete'),
placeholder: t('messages.confirmDeleteConfig', { name: profileName }),
callback: resolve,
isConfirm: true,
}
@@ -551,9 +553,9 @@ const deleteApiProfile = async name => {
originalSettings.value = JSON.parse(JSON.stringify(data))
modified.value = false
await loadApiProfiles()
await showMessage({ type: 'info', title: '删除成功', message: `配置已删除` })
await showMessage({ type: 'info', title: t('messages.success'), message: t('api.configDeleted') })
} else {
await showMessage({ type: 'error', title: '删除失败', message: result.error })
await showMessage({ type: 'error', title: t('messages.error'), message: result.error })
}
}
@@ -573,10 +575,10 @@ const getProfileInitial = name => {
// Get profile URL for display
const getProfileUrl = name => {
if (!settings.value.apiProfiles || !settings.value.apiProfiles[name]) {
return '未配置'
return t('api.unconfigured')
}
const profile = settings.value.apiProfiles[name]
return profile.baseUrl || '未配置 Base URL'
return profile.baseUrl || t('api.noBaseUrl')
}
// Get profile icon style (gradient colors)
const profileColors = [
@@ -603,8 +605,8 @@ const duplicateApiProfile = async name => {
const newName = await new Promise(resolve => {
showInputDialog.value = {
show: true,
title: '复制配置',
placeholder: '请输入新配置的名称',
title: t('api.duplicate'),
placeholder: t('api.newConfigNamePlaceholder'),
callback: resolve,
}
})
@@ -612,9 +614,9 @@ const duplicateApiProfile = async name => {
const result = await window.electronAPI.duplicateApiProfile(name, newName)
if (result.success) {
await loadApiProfiles()
await showMessage({ type: 'info', title: '复制成功', message: `配置已复制为 "${newName}"` })
await showMessage({ type: 'info', title: t('messages.success'), message: t('api.configCopied', { name: newName }) })
} else {
await showMessage({ type: 'error', title: '复制失败', message: result.error })
await showMessage({ type: 'error', title: t('messages.error'), message: result.error })
}
}
// Open API edit dialog
@@ -690,17 +692,33 @@ const loadSettings = async () => {
watch(
settings,
() => {
async () => {
if (!isLoading.value) {
modified.value = true
// 自动保存基础设置到文件
const dataToSave = JSON.parse(JSON.stringify(settings.value))
await window.electronAPI.saveSettings(dataToSave)
}
},
{ deep: true },
)
watch(
() => settings.value.language,
newLang => {
locale.value = newLang
window.electronAPI.notifyLanguageChanged()
},
)
const showSection = section => {
currentSection.value = section
}
const serverCount = computed(() => (settings.value.mcpServers ? Object.keys(settings.value.mcpServers).length : 0))
const themeClass = computed(() => {
const theme = settings.value.theme
if (theme === 'Dark') return 'dark'
if (theme === 'Solarized Dark') return 'solarized-dark'
return ''
})
const selectServer = name => {
currentServerName.value = name
openEditServerPanel(name)
@@ -744,11 +762,11 @@ const closeServerPanel = () => {
const saveServerFromPanel = async () => {
const name = editingServerData.value.name.trim()
if (!name) {
await showMessage({ type: 'warning', title: '错误', message: '请输入服务器名称' })
await showMessage({ type: 'warning', title: t('messages.error'), message: t('mcp.inputServerName') })
return
}
if (!isEditingServer.value && settings.value.mcpServers[name]) {
await showMessage({ type: 'warning', title: '错误', message: '服务器名称已存在' })
await showMessage({ type: 'warning', title: t('messages.error'), message: t('mcp.serverNameExists') })
return
}
// 如果是编辑模式且名称改变了,需要删除旧的服务器
@@ -769,7 +787,7 @@ const saveServerFromPanel = async () => {
try {
serverConfig.env = JSON.parse(envText)
} catch (e) {
await showMessage({ type: 'error', title: '错误', message: '环境变量 JSON 格式错误' })
await showMessage({ type: 'error', title: t('messages.error'), message: t('mcp.invalidEnvJson') })
return
}
}
@@ -791,7 +809,7 @@ const deleteServer = async () => {
const serverName = isEditingServer.value ? editingServerData.value.name : currentServerName.value
if (!serverName) return
const confirmed = await new Promise(resolve => {
showInputDialog.value = { show: true, title: '删除服务器', placeholder: `确定要删除服务器 "${serverName}" 吗?`, callback: resolve, isConfirm: true }
showInputDialog.value = { show: true, title: t('mcp.delete'), placeholder: t('messages.confirmDeleteServer', { name: serverName }), callback: resolve, isConfirm: true }
})
if (!confirmed) return
delete settings.value.mcpServers[serverName]
@@ -848,11 +866,32 @@ watch(
}
},
)
// Apply theme class to body when theme changes
watch(
() => settings.value.theme,
theme => {
const cls = themeClass.value
if (cls) {
document.body.classList.add(cls)
if (cls === 'dark') document.body.classList.remove('solarized-dark')
else if (cls === 'solarized-dark') document.body.classList.remove('dark')
} else {
document.body.classList.remove('dark', 'solarized-dark')
}
},
)
onMounted(async () => {
await loadApiProfiles()
await loadSettings()
// 同步初始语言到 i18n
locale.value = settings.value.language
// 应用初始主题
const cls = themeClass.value
if (cls) {
document.body.classList.add(cls)
}
// 监听托盘切换 API 配置事件
window.electronAPI.onApiProfileSwitched(async (profileName) => {
window.electronAPI.onApiProfileSwitched(async profileName => {
currentApiProfile.value = profileName
await loadSettings()
})
@@ -937,6 +976,46 @@ body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Dark Theme Variables */
.dark,
.solarized-dark {
--bg-primary: #1a1a2e;
--bg-secondary: #16213e;
--bg-tertiary: #0f3460;
--bg-hover: #1f4068;
--text-primary: #e4e4e7;
--text-secondary: #a1a1aa;
--text-tertiary: #71717a;
--accent: #60a5fa;
--accent-hover: #3b82f6;
--accent-light: rgba(96, 165, 250, 0.15);
--border: #2d2d44;
--border-light: #232338;
--success: #34d399;
--danger: #f87171;
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -2px rgba(0, 0, 0, 0.3);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.5), 0 4px 6px -4px rgba(0, 0, 0, 0.4);
}
/* Solarized Dark specific overrides */
.solarized-dark {
--bg-primary: #002b36;
--bg-secondary: #073642;
--bg-tertiary: #094856;
--bg-hover: #0a5a6f;
--text-primary: #839496;
--text-secondary: #93a1a1;
--text-tertiary: #586e75;
--accent: #268bd2;
--accent-hover: #1a73c0;
--accent-light: rgba(38, 139, 210, 0.15);
--border: #1d3a47;
--border-light: #0d3a47;
--success: #2aa198;
--danger: #dc322f;
}
/* Scrollbar Styles */
::-webkit-scrollbar {
width: 8px;

130
src/locales/en-US.js Normal file
View File

@@ -0,0 +1,130 @@
export default {
app: {
title: 'iFlow Settings Editor'
},
window: {
minimize: 'Minimize',
maximize: 'Maximize',
close: 'Close'
},
sidebar: {
general: 'General',
basicSettings: 'Basic Settings',
apiConfig: 'API Config',
advanced: 'Advanced',
mcpServers: 'MCP Servers'
},
general: {
title: 'Basic Settings',
description: 'Configure general application options',
language: 'Language',
theme: 'Theme',
languageInterface: 'Language & Interface',
otherSettings: 'Other Settings',
bootAnimation: 'Boot Animation',
bootAnimationShown: 'Shown',
bootAnimationNotShown: 'Not Shown',
checkpointing: 'Checkpointing',
enabled: 'Enabled',
disabled: 'Disabled'
},
theme: {
xcode: 'Xcode',
dark: 'Dark',
light: 'Light',
solarizedDark: 'Solarized Dark'
},
api: {
title: 'API Configuration',
description: 'Configure AI services and search API',
currentConfig: 'Current Config',
createTitle: 'Create API Configuration',
editTitle: 'Edit API Configuration',
profileManagement: 'Profile Management',
newProfile: 'New Profile',
profileName: 'Profile Name',
configName: 'Profile Name',
configNamePlaceholder: 'Enter configuration name',
newConfigNamePlaceholder: 'Enter new configuration name',
authType: 'Auth Type',
apiKey: 'API Key',
apiKeyPlaceholder: 'sk-cp-XXXXX...',
baseUrl: 'Base URL',
baseUrlPlaceholder: 'https://api.minimaxi.com/v1',
modelName: 'Model Name',
modelNamePlaceholder: 'MiniMax-M2.7',
searchApiKey: 'Search API Key',
searchApiKeyPlaceholder: 'sk-XXXXX...',
cna: 'CNA',
cnaPlaceholder: 'CNA identifier',
inUse: 'In Use',
cancel: 'Cancel',
create: 'Create',
save: 'Save',
edit: 'Edit',
duplicate: 'Duplicate',
delete: 'Delete',
unconfigured: 'Not configured',
noBaseUrl: 'Base URL not configured',
configCreated: 'Configuration "{name}" created',
configDeleted: 'Configuration deleted',
configCopied: 'Configuration copied as "{name}"',
switchFailed: 'Switch failed',
auth: {
iflow: 'iFlow',
api: 'API Key',
openaiCompatible: 'OpenAI Compatible'
}
},
mcp: {
title: 'MCP Servers',
description: 'Manage Model Context Protocol server configurations',
serverList: 'Server List',
addServer: 'Add Server',
editServer: 'Edit Server',
serverName: 'Server Name',
serverNamePlaceholder: 'my-mcp-server',
descriptionLabel: 'Description',
descriptionPlaceholder: 'Server description',
command: 'Command',
commandPlaceholder: 'npx',
workingDir: 'Working Directory',
cwdPlaceholder: '.',
args: 'Arguments (one per line)',
argsPlaceholder: '-y\\npackage-name',
envVars: 'Environment Variables (JSON)',
envVarsPlaceholder: "e.g. API_KEY=xxx",
invalidEnvJson: 'Invalid environment variables JSON format',
noServers: 'No MCP Servers',
addFirstServer: 'Click the button above to add your first server',
noDescription: 'No description',
delete: 'Delete',
cancel: 'Cancel',
saveChanges: 'Save Changes',
addServerBtn: 'Add Server',
inputServerName: 'Please enter server name',
serverNameExists: 'Server name already exists'
},
messages: {
error: 'Error',
warning: 'Warning',
success: 'Success',
info: 'Info',
cannotDeleteDefault: 'Cannot delete default configuration',
inputConfigName: 'Please enter configuration name',
confirmDeleteConfig: 'Are you sure you want to delete configuration "{name}"?',
confirmDeleteServer: 'Are you sure you want to delete server "{name}"?'
},
dialog: {
confirm: 'Confirm',
cancel: 'Cancel'
},
footer: {
config: 'Config'
},
languages: {
'zh-CN': '简体中文',
'en-US': 'English',
'ja-JP': '日本語'
}
}

130
src/locales/index.js Normal file
View File

@@ -0,0 +1,130 @@
export default {
app: {
title: 'iFlow 设置编辑器'
},
window: {
minimize: '最小化',
maximize: '最大化',
close: '关闭'
},
sidebar: {
general: '常规',
basicSettings: '基本设置',
apiConfig: 'API 配置',
advanced: '高级',
mcpServers: 'MCP 服务器'
},
general: {
title: '基本设置',
description: '配置应用程序的常规选项',
language: '语言',
theme: '主题',
languageInterface: '语言与界面',
otherSettings: '其他设置',
bootAnimation: '启动动画',
bootAnimationShown: '已显示',
bootAnimationNotShown: '未显示',
checkpointing: '检查点保存',
enabled: '已启用',
disabled: '已禁用'
},
theme: {
xcode: 'Xcode',
dark: '深色',
light: '浅色',
solarizedDark: 'Solarized Dark'
},
api: {
title: 'API 配置',
description: '配置 AI 服务和搜索 API',
currentConfig: '当前配置',
createTitle: '新建 API 配置',
editTitle: '编辑 API 配置',
profileManagement: '配置文件管理',
newProfile: '新建配置',
profileName: '配置名称',
configName: '配置名称',
configNamePlaceholder: '请输入配置名称',
newConfigNamePlaceholder: '请输入新配置的名称',
authType: '认证方式',
apiKey: 'API Key',
apiKeyPlaceholder: 'sk-cp-XXXXX...',
baseUrl: 'Base URL',
baseUrlPlaceholder: 'https://api.minimaxi.com/v1',
modelName: '模型名称',
modelNamePlaceholder: 'MiniMax-M2.7',
searchApiKey: '搜索 API Key',
searchApiKeyPlaceholder: 'sk-XXXXX...',
cna: 'CNA',
cnaPlaceholder: 'CNA 标识',
inUse: '使用中',
cancel: '取消',
create: '创建',
save: '保存',
edit: '编辑',
duplicate: '复制',
delete: '删除',
unconfigured: '未配置',
noBaseUrl: '未配置 Base URL',
configCreated: '配置 "{name}" 已创建',
configDeleted: '配置已删除',
configCopied: '配置已复制为 "{name}"',
switchFailed: '切换失败',
auth: {
iflow: 'iFlow',
api: 'API Key',
openaiCompatible: 'OpenAI 兼容'
}
},
mcp: {
title: 'MCP 服务器',
description: '管理 Model Context Protocol 服务器配置',
serverList: '服务器列表',
addServer: '添加服务器',
editServer: '编辑服务器',
serverName: '服务器名称',
serverNamePlaceholder: 'my-mcp-server',
descriptionLabel: '描述',
descriptionPlaceholder: '服务器描述信息',
command: '命令',
commandPlaceholder: 'npx',
workingDir: '工作目录',
cwdPlaceholder: '.',
args: '参数 (每行一个)',
argsPlaceholder: '-y\\npackage-name',
envVars: '环境变量 (JSON 格式)',
envVarsPlaceholder: "例如: API_KEY=xxx",
invalidEnvJson: '环境变量 JSON 格式错误',
noServers: '暂无 MCP 服务器',
addFirstServer: '点击上方按钮添加第一个服务器',
noDescription: '无描述',
delete: '删除',
cancel: '取消',
saveChanges: '保存更改',
addServerBtn: '添加服务器',
inputServerName: '请输入服务器名称',
serverNameExists: '服务器名称已存在'
},
messages: {
error: '错误',
warning: '警告',
success: '成功',
info: '信息',
cannotDeleteDefault: '不能删除默认配置',
inputConfigName: '请输入配置名称',
confirmDeleteConfig: '确定要删除配置 "{name}" 吗?',
confirmDeleteServer: '确定要删除服务器 "{name}" 吗?'
},
dialog: {
confirm: '确定',
cancel: '取消'
},
footer: {
config: '配置'
},
languages: {
'zh-CN': '简体中文',
'en-US': 'English',
'ja-JP': '日本語'
}
}

130
src/locales/ja-JP.js Normal file
View File

@@ -0,0 +1,130 @@
export default {
app: {
title: 'iFlow 設定エディタ'
},
window: {
minimize: '最小化',
maximize: '最大化',
close: '閉じる'
},
sidebar: {
general: '一般',
basicSettings: '基本設定',
apiConfig: 'API 設定',
advanced: '詳細',
mcpServers: 'MCP サーバー'
},
general: {
title: '基本設定',
description: 'アプリケーションの一般設定を構成',
language: '言語',
theme: 'テーマ',
languageInterface: '言語とインターフェース',
otherSettings: 'その他の設定',
bootAnimation: '起動アニメーション',
bootAnimationShown: '表示済み',
bootAnimationNotShown: '未表示',
checkpointing: 'チェックポイント保存',
enabled: '有効',
disabled: '無効'
},
theme: {
xcode: 'Xcode',
dark: 'ダーク',
light: 'ライト',
solarizedDark: 'Solarized Dark'
},
api: {
title: 'API 設定',
description: 'AI サービスと検索 API を構成',
currentConfig: '現在設定',
createTitle: 'API 設定を作成',
editTitle: 'API 設定を編集',
profileManagement: 'プロファイル管理',
newProfile: '新規プロファイル',
profileName: 'プロファイル名',
configName: 'プロファイル名',
configNamePlaceholder: 'プロファイル名を入力',
newConfigNamePlaceholder: '新しいプロファイル名を入力',
authType: '認証方式',
apiKey: 'API Key',
apiKeyPlaceholder: 'sk-cp-XXXXX...',
baseUrl: 'Base URL',
baseUrlPlaceholder: 'https://api.minimaxi.com/v1',
modelName: 'モデル名',
modelNamePlaceholder: 'MiniMax-M2.7',
searchApiKey: '検索 API Key',
searchApiKeyPlaceholder: 'sk-XXXXX...',
cna: 'CNA',
cnaPlaceholder: 'CNA 識別子',
inUse: '使用中',
cancel: 'キャンセル',
create: '作成',
save: '保存',
edit: '編集',
duplicate: '複製',
delete: '削除',
unconfigured: '未設定',
noBaseUrl: 'Base URL 未設定',
configCreated: 'プロファイル "{name}" を作成しました',
configDeleted: 'プロファイルを削除しました',
configCopied: 'プロファイルを "{name}" に複製しました',
switchFailed: '切り替えに失敗しました',
auth: {
iflow: 'iFlow',
api: 'API Key',
openaiCompatible: 'OpenAI 互換'
}
},
mcp: {
title: 'MCP サーバー',
description: 'Model Context Protocol サーバー設定を管理',
serverList: 'サーバー一覧',
addServer: 'サーバーを追加',
editServer: 'サーバーを編集',
serverName: 'サーバー名',
serverNamePlaceholder: 'my-mcp-server',
descriptionLabel: '説明',
descriptionPlaceholder: 'サーバーの説明',
command: 'コマンド',
commandPlaceholder: 'npx',
workingDir: '作業ディレクトリ',
cwdPlaceholder: '.',
args: '引数1行に1つ',
argsPlaceholder: '-y\\npackage-name',
envVars: '環境変数JSON形式',
envVarsPlaceholder: "例: API_KEY=xxx",
invalidEnvJson: '環境変数の JSON 形式が無効です',
noServers: 'MCP サーバーがありません',
addFirstServer: '上のボタンをクリックして最初のサーバーを追加',
noDescription: '説明なし',
delete: '削除',
cancel: 'キャンセル',
saveChanges: '変更を保存',
addServerBtn: 'サーバーを追加',
inputServerName: 'サーバー名を入力してください',
serverNameExists: 'サーバー名は既に存在します'
},
messages: {
error: 'エラー',
warning: '警告',
success: '成功',
info: '情報',
cannotDeleteDefault: 'デフォルトプロファイルは削除できません',
inputConfigName: 'プロファイル名を入力してください',
confirmDeleteConfig: 'プロファイル "{name}" を削除してもよろしいですか?',
confirmDeleteServer: 'サーバー "{name}" を削除してもよろしいですか?'
},
dialog: {
confirm: '確認',
cancel: 'キャンセル'
},
footer: {
config: '設定'
},
languages: {
'zh-CN': '简体中文',
'en-US': 'English',
'ja-JP': '日本語'
}
}

View File

@@ -1,4 +1,21 @@
import { createApp } from 'vue';
import App from './App.vue';
import { createApp } from 'vue'
import { createI18n } from 'vue-i18n'
import App from './App.vue'
import zhCN from './locales/index.js'
import enUS from './locales/en-US.js'
import jaJP from './locales/ja-JP.js'
createApp(App).mount('#app');
const i18n = createI18n({
legacy: false,
locale: 'zh-CN',
fallbackLocale: 'zh-CN',
messages: {
'zh-CN': zhCN,
'en-US': enUS,
'ja-JP': jaJP
}
})
const app = createApp(App)
app.use(i18n)
app.mount('#app')