新增 完整的国际化(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

BIN
lang-zh-CN.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

108
main.js
View File

@@ -9,6 +9,68 @@ let mainWindow
let tray let tray
const isDev = process.argv.includes('--dev') const isDev = process.argv.includes('--dev')
// 主进程翻译
const trayTranslations = {
'zh-CN': {
showWindow: '显示主窗口',
switchApiConfig: '切换 API 配置',
exit: '退出',
tooltip: 'iFlow 设置编辑器'
},
'en-US': {
showWindow: 'Show Window',
switchApiConfig: 'Switch API Config',
exit: 'Exit',
tooltip: 'iFlow Settings Editor'
},
'ja-JP': {
showWindow: 'メインウィンドウを表示',
switchApiConfig: 'API 設定切替',
exit: '終了',
tooltip: 'iFlow 設定エディタ'
}
}
function getTrayTranslation() {
const settings = readSettings()
const lang = settings?.language || 'zh-CN'
return trayTranslations[lang] || trayTranslations['zh-CN']
}
// 错误消息翻译
const errorTranslations = {
'zh-CN': {
configNotFound: '配置文件不存在',
configNotExist: '配置 "{name}" 不存在',
configAlreadyExists: '配置 "{name}" 已存在',
cannotDeleteDefault: '不能删除默认配置',
cannotRenameDefault: '不能重命名默认配置',
switchFailed: '切换API配置失败'
},
'en-US': {
configNotFound: 'Configuration file not found',
configNotExist: 'Configuration "{name}" does not exist',
configAlreadyExists: 'Configuration "{name}" already exists',
cannotDeleteDefault: 'Cannot delete default configuration',
cannotRenameDefault: 'Cannot rename default configuration',
switchFailed: 'Failed to switch API configuration'
},
'ja-JP': {
configNotFound: '設定ファイルが存在しません',
configNotExist: 'プロファイル "{name}" が存在しません',
configAlreadyExists: 'プロファイル "{name}" が既に存在します',
cannotDeleteDefault: 'デフォルトプロファイルは削除できません',
cannotRenameDefault: 'デフォルトプロファイルは名前変更できません',
switchFailed: 'API 設定の切替に失敗しました'
}
}
function getErrorTranslation() {
const settings = readSettings()
const lang = settings?.language || 'zh-CN'
return errorTranslations[lang] || errorTranslations['zh-CN']
}
// 创建系统托盘 // 创建系统托盘
function createTray() { function createTray() {
// 获取图标路径 - 打包后需要从 extraResources 获取 // 获取图标路径 - 打包后需要从 extraResources 获取
@@ -30,7 +92,7 @@ function createTray() {
trayIcon = trayIcon.resize({ width: 16, height: 16 }) trayIcon = trayIcon.resize({ width: 16, height: 16 })
tray = new Tray(trayIcon) tray = new Tray(trayIcon)
tray.setToolTip('iFlow 设置编辑器') tray.setToolTip(getTrayTranslation().tooltip)
updateTrayMenu() updateTrayMenu()
@@ -59,9 +121,10 @@ function updateTrayMenu() {
click: () => switchApiProfileFromTray(name) click: () => switchApiProfileFromTray(name)
})) }))
const t = getTrayTranslation()
const contextMenu = Menu.buildFromTemplate([ const contextMenu = Menu.buildFromTemplate([
{ {
label: '显示主窗口', label: t.showWindow,
click: () => { click: () => {
if (mainWindow) { if (mainWindow) {
mainWindow.show() mainWindow.show()
@@ -71,12 +134,12 @@ function updateTrayMenu() {
}, },
{ type: 'separator' }, { type: 'separator' },
{ {
label: '切换 API 配置', label: t.switchApiConfig,
submenu: profileMenuItems submenu: profileMenuItems
}, },
{ type: 'separator' }, { type: 'separator' },
{ {
label: '退出', label: t.exit,
click: () => { click: () => {
app.isQuitting = true app.isQuitting = true
app.quit() app.quit()
@@ -198,6 +261,10 @@ ipcMain.on('window-close', () => {
} }
}) })
ipcMain.handle('is-maximized', () => mainWindow.isMaximized()) ipcMain.handle('is-maximized', () => mainWindow.isMaximized())
// 监听语言切换以更新托盘菜单
ipcMain.on('language-changed', () => {
updateTrayMenu()
})
// API 配置相关的字段 // API 配置相关的字段
const API_FIELDS = ['selectedAuthType', 'apiKey', 'baseUrl', 'modelName', 'searchApiKey', 'cna'] const API_FIELDS = ['selectedAuthType', 'apiKey', 'baseUrl', 'modelName', 'searchApiKey', 'cna']
// 读取设置文件 // 读取设置文件
@@ -245,12 +312,13 @@ ipcMain.handle('list-api-profiles', async () => {
ipcMain.handle('switch-api-profile', async (event, profileName) => { ipcMain.handle('switch-api-profile', async (event, profileName) => {
try { try {
const settings = readSettings() const settings = readSettings()
const t = getErrorTranslation()
if (!settings) { if (!settings) {
return { success: false, error: '配置文件不存在' } return { success: false, error: t.configNotFound }
} }
const profiles = settings.apiProfiles || {} const profiles = settings.apiProfiles || {}
if (!profiles[profileName]) { if (!profiles[profileName]) {
return { success: false, error: `配置 "${profileName}" 不存在` } return { success: false, error: t.configNotExist.replace('{name}', profileName) }
} }
// 保存当前配置到 apiProfiles如果当前配置存在 // 保存当前配置到 apiProfiles如果当前配置存在
const currentProfile = settings.currentApiProfile || 'default' const currentProfile = settings.currentApiProfile || 'default'
@@ -282,8 +350,9 @@ ipcMain.handle('switch-api-profile', async (event, profileName) => {
ipcMain.handle('create-api-profile', async (event, name) => { ipcMain.handle('create-api-profile', async (event, name) => {
try { try {
const settings = readSettings() const settings = readSettings()
const t = getErrorTranslation()
if (!settings) { if (!settings) {
return { success: false, error: '配置文件不存在' } return { success: false, error: t.configNotFound }
} }
if (!settings.apiProfiles) { if (!settings.apiProfiles) {
settings.apiProfiles = { default: {} } settings.apiProfiles = { default: {} }
@@ -295,7 +364,7 @@ ipcMain.handle('create-api-profile', async (event, name) => {
} }
} }
if (settings.apiProfiles[name]) { if (settings.apiProfiles[name]) {
return { success: false, error: `配置 "${name}" 已存在` } return { success: false, error: t.configAlreadyExists.replace('{name}', name) }
} }
// 复制当前配置到新配置 // 复制当前配置到新配置
const newConfig = {} const newConfig = {}
@@ -315,15 +384,16 @@ ipcMain.handle('create-api-profile', async (event, name) => {
ipcMain.handle('delete-api-profile', async (event, name) => { ipcMain.handle('delete-api-profile', async (event, name) => {
try { try {
const settings = readSettings() const settings = readSettings()
const t = getErrorTranslation()
if (!settings) { if (!settings) {
return { success: false, error: '配置文件不存在' } return { success: false, error: t.configNotFound }
} }
if (name === 'default') { if (name === 'default') {
return { success: false, error: '不能删除默认配置' } return { success: false, error: t.cannotDeleteDefault }
} }
const profiles = settings.apiProfiles || {} const profiles = settings.apiProfiles || {}
if (!profiles[name]) { if (!profiles[name]) {
return { success: false, error: `配置 "${name}" 不存在` } return { success: false, error: t.configNotExist.replace('{name}', name) }
} }
delete profiles[name] delete profiles[name]
settings.apiProfiles = profiles settings.apiProfiles = profiles
@@ -348,18 +418,19 @@ ipcMain.handle('delete-api-profile', async (event, name) => {
ipcMain.handle('rename-api-profile', async (event, oldName, newName) => { ipcMain.handle('rename-api-profile', async (event, oldName, newName) => {
try { try {
const settings = readSettings() const settings = readSettings()
const t = getErrorTranslation()
if (!settings) { if (!settings) {
return { success: false, error: '配置文件不存在' } return { success: false, error: t.configNotFound }
} }
if (oldName === 'default') { if (oldName === 'default') {
return { success: false, error: '不能重命名默认配置' } return { success: false, error: t.cannotRenameDefault }
} }
const profiles = settings.apiProfiles || {} const profiles = settings.apiProfiles || {}
if (!profiles[oldName]) { if (!profiles[oldName]) {
return { success: false, error: `配置 "${oldName}" 不存在` } return { success: false, error: t.configNotExist.replace('{name}', oldName) }
} }
if (profiles[newName]) { if (profiles[newName]) {
return { success: false, error: `配置 "${newName}" 已存在` } return { success: false, error: t.configAlreadyExists.replace('{name}', newName) }
} }
profiles[newName] = profiles[oldName] profiles[newName] = profiles[oldName]
delete profiles[oldName] delete profiles[oldName]
@@ -377,15 +448,16 @@ ipcMain.handle('rename-api-profile', async (event, oldName, newName) => {
ipcMain.handle('duplicate-api-profile', async (event, sourceName, newName) => { ipcMain.handle('duplicate-api-profile', async (event, sourceName, newName) => {
try { try {
const settings = readSettings() const settings = readSettings()
const t = getErrorTranslation()
if (!settings) { if (!settings) {
return { success: false, error: '配置文件不存在' } return { success: false, error: t.configNotFound }
} }
const profiles = settings.apiProfiles || {} const profiles = settings.apiProfiles || {}
if (!profiles[sourceName]) { if (!profiles[sourceName]) {
return { success: false, error: `配置 "${sourceName}" 不存在` } return { success: false, error: t.configNotExist.replace('{name}', sourceName) }
} }
if (profiles[newName]) { if (profiles[newName]) {
return { success: false, error: `配置 "${newName}" 已存在` } return { success: false, error: t.configAlreadyExists.replace('{name}', newName) }
} }
// 深拷贝配置 // 深拷贝配置
profiles[newName] = JSON.parse(JSON.stringify(profiles[sourceName])) profiles[newName] = JSON.parse(JSON.stringify(profiles[sourceName]))

103
package-lock.json generated
View File

@@ -1,13 +1,16 @@
{ {
"name": "iflow-settings-editor", "name": "iflow-settings-editor",
"version": "1.0.0", "version": "1.5.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "iflow-settings-editor", "name": "iflow-settings-editor",
"version": "1.0.0", "version": "1.5.1",
"license": "MIT", "license": "MIT",
"dependencies": {
"vue-i18n": "^9.14.5"
},
"devDependencies": { "devDependencies": {
"@icon-park/vue-next": "^1.4.2", "@icon-park/vue-next": "^1.4.2",
"@vitejs/plugin-vue": "^6.0.6", "@vitejs/plugin-vue": "^6.0.6",
@@ -22,7 +25,6 @@
"version": "7.27.1", "version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
@@ -32,7 +34,6 @@
"version": "7.28.5", "version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
@@ -42,7 +43,6 @@
"version": "7.29.2", "version": "7.29.2",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
"integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/types": "^7.29.0" "@babel/types": "^7.29.0"
@@ -68,7 +68,6 @@
"version": "7.29.0", "version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
"integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/helper-string-parser": "^7.27.1", "@babel/helper-string-parser": "^7.27.1",
@@ -417,6 +416,50 @@
"vue": "3.x" "vue": "3.x"
} }
}, },
"node_modules/@intlify/core-base": {
"version": "9.14.5",
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.14.5.tgz",
"integrity": "sha512-5ah5FqZG4pOoHjkvs8mjtv+gPKYU0zCISaYNjBNNqYiaITxW8ZtVih3GS/oTOqN8d9/mDLyrjD46GBApNxmlsA==",
"license": "MIT",
"dependencies": {
"@intlify/message-compiler": "9.14.5",
"@intlify/shared": "9.14.5"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@intlify/message-compiler": {
"version": "9.14.5",
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.14.5.tgz",
"integrity": "sha512-IHzgEu61/YIpQV5Pc3aRWScDcnFKWvQA9kigcINcCBXN8mbW+vk9SK+lDxA6STzKQsVJxUPg9ACC52pKKo3SVQ==",
"license": "MIT",
"dependencies": {
"@intlify/shared": "9.14.5",
"source-map-js": "^1.0.2"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@intlify/shared": {
"version": "9.14.5",
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.14.5.tgz",
"integrity": "sha512-9gB+E53BYuAEMhbCAxVgG38EZrk59sxBtv3jSizNL2hEWlgjBjAw1AwpLHtNaeda12pe6W20OGEa0TwuMSRbyQ==",
"license": "MIT",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@isaacs/cliui": { "node_modules/@isaacs/cliui": {
"version": "8.0.2", "version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -524,7 +567,6 @@
"version": "1.5.5", "version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@malept/cross-spawn-promise": { "node_modules/@malept/cross-spawn-promise": {
@@ -1085,7 +1127,6 @@
"version": "3.5.32", "version": "3.5.32",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.32.tgz", "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.32.tgz",
"integrity": "sha512-4x74Tbtqnda8s/NSD6e1Dr5p1c8HdMU5RWSjMSUzb8RTcUQqevDCxVAitcLBKT+ie3o0Dl9crc/S/opJM7qBGQ==", "integrity": "sha512-4x74Tbtqnda8s/NSD6e1Dr5p1c8HdMU5RWSjMSUzb8RTcUQqevDCxVAitcLBKT+ie3o0Dl9crc/S/opJM7qBGQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/parser": "^7.29.2", "@babel/parser": "^7.29.2",
@@ -1099,7 +1140,6 @@
"version": "3.5.32", "version": "3.5.32",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.32.tgz", "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.32.tgz",
"integrity": "sha512-ybHAu70NtiEI1fvAUz3oXZqkUYEe5J98GjMDpTGl5iHb0T15wQYLR4wE3h9xfuTNA+Cm2f4czfe8B4s+CCH57Q==", "integrity": "sha512-ybHAu70NtiEI1fvAUz3oXZqkUYEe5J98GjMDpTGl5iHb0T15wQYLR4wE3h9xfuTNA+Cm2f4czfe8B4s+CCH57Q==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/compiler-core": "3.5.32", "@vue/compiler-core": "3.5.32",
@@ -1110,7 +1150,6 @@
"version": "3.5.32", "version": "3.5.32",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.32.tgz", "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.32.tgz",
"integrity": "sha512-8UYUYo71cP/0YHMO814TRZlPuUUw3oifHuMR7Wp9SNoRSrxRQnhMLNlCeaODNn6kNTJsjFoQ/kqIj4qGvya4Xg==", "integrity": "sha512-8UYUYo71cP/0YHMO814TRZlPuUUw3oifHuMR7Wp9SNoRSrxRQnhMLNlCeaODNn6kNTJsjFoQ/kqIj4qGvya4Xg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/parser": "^7.29.2", "@babel/parser": "^7.29.2",
@@ -1128,18 +1167,22 @@
"version": "3.5.32", "version": "3.5.32",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.32.tgz", "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.32.tgz",
"integrity": "sha512-Gp4gTs22T3DgRotZ8aA/6m2jMR+GMztvBXUBEUOYOcST+giyGWJ4WvFd7QLHBkzTxkfOt8IELKNdpzITLbA2rw==", "integrity": "sha512-Gp4gTs22T3DgRotZ8aA/6m2jMR+GMztvBXUBEUOYOcST+giyGWJ4WvFd7QLHBkzTxkfOt8IELKNdpzITLbA2rw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/compiler-dom": "3.5.32", "@vue/compiler-dom": "3.5.32",
"@vue/shared": "3.5.32" "@vue/shared": "3.5.32"
} }
}, },
"node_modules/@vue/devtools-api": {
"version": "6.6.4",
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
"license": "MIT"
},
"node_modules/@vue/reactivity": { "node_modules/@vue/reactivity": {
"version": "3.5.32", "version": "3.5.32",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.32.tgz", "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.32.tgz",
"integrity": "sha512-/ORasxSGvZ6MN5gc+uE364SxFdJ0+WqVG0CENXaGW58TOCdrAW76WWaplDtECeS1qphvtBZtR+3/o1g1zL4xPQ==", "integrity": "sha512-/ORasxSGvZ6MN5gc+uE364SxFdJ0+WqVG0CENXaGW58TOCdrAW76WWaplDtECeS1qphvtBZtR+3/o1g1zL4xPQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/shared": "3.5.32" "@vue/shared": "3.5.32"
@@ -1149,7 +1192,6 @@
"version": "3.5.32", "version": "3.5.32",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.32.tgz", "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.32.tgz",
"integrity": "sha512-pDrXCejn4UpFDFmMd27AcJEbHaLemaE5o4pbb7sLk79SRIhc6/t34BQA7SGNgYtbMnvbF/HHOftYBgFJtUoJUQ==", "integrity": "sha512-pDrXCejn4UpFDFmMd27AcJEbHaLemaE5o4pbb7sLk79SRIhc6/t34BQA7SGNgYtbMnvbF/HHOftYBgFJtUoJUQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/reactivity": "3.5.32", "@vue/reactivity": "3.5.32",
@@ -1160,7 +1202,6 @@
"version": "3.5.32", "version": "3.5.32",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.32.tgz", "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.32.tgz",
"integrity": "sha512-1CDVv7tv/IV13V8Nip1k/aaObVbWqRlVCVezTwx3K07p7Vxossp5JU1dcPNhJk3w347gonIUT9jQOGutyJrSVQ==", "integrity": "sha512-1CDVv7tv/IV13V8Nip1k/aaObVbWqRlVCVezTwx3K07p7Vxossp5JU1dcPNhJk3w347gonIUT9jQOGutyJrSVQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/reactivity": "3.5.32", "@vue/reactivity": "3.5.32",
@@ -1173,7 +1214,6 @@
"version": "3.5.32", "version": "3.5.32",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.32.tgz", "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.32.tgz",
"integrity": "sha512-IOjm2+JQwRFS7W28HNuJeXQle9KdZbODFY7hFGVtnnghF51ta20EWAZJHX+zLGtsHhaU6uC9BGPV52KVpYryMQ==", "integrity": "sha512-IOjm2+JQwRFS7W28HNuJeXQle9KdZbODFY7hFGVtnnghF51ta20EWAZJHX+zLGtsHhaU6uC9BGPV52KVpYryMQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/compiler-ssr": "3.5.32", "@vue/compiler-ssr": "3.5.32",
@@ -1187,7 +1227,6 @@
"version": "3.5.32", "version": "3.5.32",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.32.tgz", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.32.tgz",
"integrity": "sha512-ksNyrmRQzWJJ8n3cRDuSF7zNNontuJg1YHnmWRJd2AMu8Ij2bqwiiri2lH5rHtYPZjj4STkNcgcmiQqlOjiYGg==", "integrity": "sha512-ksNyrmRQzWJJ8n3cRDuSF7zNNontuJg1YHnmWRJd2AMu8Ij2bqwiiri2lH5rHtYPZjj4STkNcgcmiQqlOjiYGg==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@xmldom/xmldom": { "node_modules/@xmldom/xmldom": {
@@ -2107,7 +2146,6 @@
"version": "3.2.3", "version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/date-fns": { "node_modules/date-fns": {
@@ -2654,7 +2692,6 @@
"version": "7.0.1", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
"integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
"dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"engines": { "engines": {
"node": ">=0.12" "node": ">=0.12"
@@ -2765,7 +2802,6 @@
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/extract-zip": { "node_modules/extract-zip": {
@@ -3920,7 +3956,6 @@
"version": "0.30.21", "version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.5" "@jridgewell/sourcemap-codec": "^1.5.5"
@@ -4080,7 +4115,6 @@
"version": "3.3.11", "version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"dev": true,
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@@ -4220,7 +4254,6 @@
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/picomatch": { "node_modules/picomatch": {
@@ -4255,7 +4288,6 @@
"version": "8.5.9", "version": "8.5.9",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz",
"integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==", "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==",
"dev": true,
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@@ -4702,7 +4734,6 @@
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
@@ -5009,7 +5040,7 @@
"version": "5.9.3", "version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true, "devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
@@ -5159,7 +5190,6 @@
"version": "3.5.32", "version": "3.5.32",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.32.tgz", "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.32.tgz",
"integrity": "sha512-vM4z4Q9tTafVfMAK7IVzmxg34rSzTFMyIe0UUEijUCkn9+23lj0WRfA83dg7eQZIUlgOSGrkViIaCfqSAUXsMw==", "integrity": "sha512-vM4z4Q9tTafVfMAK7IVzmxg34rSzTFMyIe0UUEijUCkn9+23lj0WRfA83dg7eQZIUlgOSGrkViIaCfqSAUXsMw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/compiler-dom": "3.5.32", "@vue/compiler-dom": "3.5.32",
@@ -5177,6 +5207,27 @@
} }
} }
}, },
"node_modules/vue-i18n": {
"version": "9.14.5",
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.14.5.tgz",
"integrity": "sha512-0jQ9Em3ymWngyiIkj0+c/k7WgaPO+TNzjKSNq9BvBQaKJECqn9cd9fL4tkDhB5G1QBskGl9YxxbDAhgbFtpe2g==",
"deprecated": "v9 and v10 no longer supported. please migrate to v11. about maintenance status, see https://vue-i18n.intlify.dev/guide/maintenance.html",
"license": "MIT",
"dependencies": {
"@intlify/core-base": "9.14.5",
"@intlify/shared": "9.14.5",
"@vue/devtools-api": "^6.5.0"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
},
"peerDependencies": {
"vue": "^3.0.0"
}
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@@ -91,5 +91,8 @@
"electron-builder": "^24.13.3", "electron-builder": "^24.13.3",
"vite": "^8.0.8", "vite": "^8.0.8",
"vue": "^3.4.0" "vue": "^3.4.0"
},
"dependencies": {
"vue-i18n": "^9.14.5"
} }
} }

View File

@@ -23,5 +23,10 @@ contextBridge.exposeInMainWorld('electronAPI', {
// 托盘事件监听 // 托盘事件监听
onApiProfileSwitched: (callback) => { onApiProfileSwitched: (callback) => {
ipcRenderer.on('api-profile-switched', (event, profileName) => callback(profileName)) ipcRenderer.on('api-profile-switched', (event, profileName) => callback(profileName))
},
// 语言切换通知
notifyLanguageChanged: () => {
ipcRenderer.send('language-changed')
} }
}) })

View File

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

BIN
theme-dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
theme-solarized-dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
theme-xcode.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB