From 6eb2fc18b380a215d3c8b7352aa3649772046336 Mon Sep 17 00:00:00 2001 From: kura Date: Sat, 2 May 2026 16:36:04 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.vue | 83 +++++++++++++++++--- src/style.css | 204 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 277 insertions(+), 10 deletions(-) diff --git a/src/App.vue b/src/App.vue index db2219d..a1a4024 100644 --- a/src/App.vue +++ b/src/App.vue @@ -53,6 +53,7 @@ const MEDIA_EXTENSIONS = [ 'mpeg', 'mpg', ] as const +const FREE_API_KEY_URL = 'https://docs.bigmodel.cn/cn/guide/models/free/glm-4.7-flash' const taskStore = useTaskStore() const targetLang = ref('zh') @@ -73,6 +74,8 @@ const pending = ref(false) const feedback = ref('') const showAdvanced = ref(false) const isDragging = ref(false) +const showApiKeyDialog = ref(false) +const apiKeyUrlCopied = ref(false) let unlistenMenuAction: UnlistenFn | null = null let dragDropUnlistenFn: UnlistenFn | null = null @@ -115,6 +118,30 @@ function resetModelPaths() { feedback.value = t('app.feedback.restoredDefaults') } +function freeApiKeyHint() { + if (locale.value === 'zh-CN') { + return '开始翻译任务前需要先填写 LLM API Key。你可以选择下面任意一种方式。' + } + + return 'An LLM API Key is required before starting a translation task. Choose any of the options below.' +} + +function openApiKeyDialog() { + feedback.value = t('app.feedback.noApiKey') + apiKeyUrlCopied.value = false + showApiKeyDialog.value = true +} + +async function copyFreeApiKeyUrl() { + await navigator.clipboard.writeText(FREE_API_KEY_URL) + apiKeyUrlCopied.value = true +} + +function acknowledgeApiKeyDialog() { + showApiKeyDialog.value = false + outputMode.value = 'source' +} + async function loadDefaultModelPaths() { try { const paths = await invoke('get_default_model_paths') @@ -169,30 +196,28 @@ async function bindMenuActions() { } async function submitFiles(filePaths: string[]) { + if (outputMode.value === 'translate' && !hasTranslationKey.value) { + openApiKeyDialog() + return + } + pending.value = true feedback.value = '' try { - const fallbackToSource = outputMode.value === 'translate' && !hasTranslationKey.value - const effectiveOutputMode: OutputMode = fallbackToSource ? 'source' : outputMode.value - for (const filePath of filePaths) { await taskStore.startTask({ filePath, sourceLang: sourceLang.value === 'auto' ? null : sourceLang.value, targetLang: targetLang.value, - outputMode: effectiveOutputMode, + outputMode: outputMode.value, bilingualOutput: bilingualOutput.value, - translationConfig: effectiveOutputMode === 'translate' ? translationConfig.value : null, + translationConfig: outputMode.value === 'translate' ? translationConfig.value : null, whisperModelPath: whisperModelPath.value || null, vadModelPath: vadModelPath.value || null, }) } - if (fallbackToSource) { - feedback.value = t('app.feedback.fallbackSource') - } else { - feedback.value = t('app.feedback.submitted', { count: filePaths.length }) - } + feedback.value = t('app.feedback.submitted', { count: filePaths.length }) } catch (error) { feedback.value = error instanceof Error ? error.message : t('app.feedback.submitFailed') } finally { @@ -342,6 +367,44 @@ async function handleTranslateFromEditor() { + +
+ +
+
diff --git a/src/style.css b/src/style.css index ffdde98..2831a19 100644 --- a/src/style.css +++ b/src/style.css @@ -934,3 +934,207 @@ textarea { .drag-leave-to { opacity: 0; } + +.dialog-overlay { + position: fixed; + inset: 0; + z-index: 10000; + display: flex; + align-items: center; + justify-content: center; + padding: 20px; + background: rgba(26, 26, 46, 0.24); + backdrop-filter: blur(6px); + -webkit-backdrop-filter: blur(6px); +} + +.api-key-dialog { + width: min(520px, 100%); + padding: 20px; + border: 1px solid var(--c-border); + border-radius: var(--radius-lg); + background: var(--c-surface); + box-shadow: 0 18px 48px rgba(26, 26, 46, 0.18); +} + +.dialog-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; +} + +.dialog-header strong { + display: block; + color: var(--c-text); + font-size: 16px; + font-weight: 600; +} + +.dialog-header p { + margin: 8px 0 0; + color: var(--c-text-secondary); + font-size: 13px; + line-height: 1.6; +} + +.dialog-close { + flex: 0 0 auto; + width: 30px; + height: 30px; + border: 1px solid var(--c-border); + border-radius: var(--radius-sm); + background: transparent; + color: var(--c-text-secondary); + cursor: pointer; + font-size: 20px; + line-height: 1; +} + +.dialog-close:hover { + border-color: var(--c-border-hover); + color: var(--c-text); + background: var(--c-bg); +} + +.api-key-options { + display: grid; + gap: 10px; + margin: 16px 0 0; + padding: 0; + list-style: none; + counter-reset: api-key-option; +} + +.api-key-options li { + counter-increment: api-key-option; + display: grid; + grid-template-columns: 24px 28px minmax(0, 1fr); + align-items: center; + gap: 10px; + color: var(--c-text-secondary); + font-size: 13px; + line-height: 1.5; +} + +.api-key-options li::before { + content: counter(api-key-option) "."; + color: var(--c-text); + font-weight: 600; +} + +.option-icon { + width: 28px; + height: 28px; + border: 1px solid var(--c-border); + border-radius: var(--radius-sm); + background: var(--c-bg); + position: relative; +} + +.option-icon.self-host::before { + content: ""; + position: absolute; + left: 7px; + top: 7px; + width: 12px; + height: 12px; + border: 2px solid var(--c-text-secondary); + border-radius: 3px; +} + +.option-icon.self-host::after { + content: ""; + position: absolute; + left: 12px; + top: 3px; + width: 2px; + height: 20px; + background: var(--c-text-secondary); +} + +.option-icon.provider::before, +.option-icon.provider::after { + content: ""; + position: absolute; + left: 6px; + right: 6px; + height: 4px; + border-radius: 2px; + background: var(--c-text-secondary); +} + +.option-icon.provider::before { + top: 8px; +} + +.option-icon.provider::after { + top: 16px; +} + +.option-icon.free::before { + content: ""; + position: absolute; + left: 8px; + top: 6px; + width: 10px; + height: 14px; + border: 2px solid var(--c-success); + border-radius: 6px 6px 4px 4px; +} + +.option-icon.free::after { + content: ""; + position: absolute; + left: 10px; + top: 12px; + width: 6px; + height: 2px; + background: var(--c-success); +} + +.api-key-link { + padding: 0; + border: 0; + background: transparent; + color: var(--c-accent); + cursor: pointer; + font-weight: 600; + text-decoration: underline; + text-underline-offset: 3px; +} + +.api-key-link:hover { + color: var(--c-accent-hover); +} + +.copy-hint { + color: var(--c-success); + font-size: 12px; +} + +.dialog-actions { + display: flex; + justify-content: flex-end; + margin-top: 18px; +} + +.dialog-enter-active, +.dialog-leave-active { + transition: opacity 0.16s ease; +} + +.dialog-enter-active .api-key-dialog, +.dialog-leave-active .api-key-dialog { + transition: transform 0.16s ease; +} + +.dialog-enter-from, +.dialog-leave-to { + opacity: 0; +} + +.dialog-enter-from .api-key-dialog, +.dialog-leave-to .api-key-dialog { + transform: translateY(8px); +}