完善提示

This commit is contained in:
kura 2026-05-02 16:36:04 +08:00
parent 763a14265d
commit 6eb2fc18b3
2 changed files with 277 additions and 10 deletions

View File

@ -53,6 +53,7 @@ const MEDIA_EXTENSIONS = [
'mpeg', 'mpeg',
'mpg', 'mpg',
] as const ] as const
const FREE_API_KEY_URL = 'https://docs.bigmodel.cn/cn/guide/models/free/glm-4.7-flash'
const taskStore = useTaskStore() const taskStore = useTaskStore()
const targetLang = ref<TargetLanguage>('zh') const targetLang = ref<TargetLanguage>('zh')
@ -73,6 +74,8 @@ const pending = ref(false)
const feedback = ref('') const feedback = ref('')
const showAdvanced = ref(false) const showAdvanced = ref(false)
const isDragging = ref(false) const isDragging = ref(false)
const showApiKeyDialog = ref(false)
const apiKeyUrlCopied = ref(false)
let unlistenMenuAction: UnlistenFn | null = null let unlistenMenuAction: UnlistenFn | null = null
let dragDropUnlistenFn: UnlistenFn | null = null let dragDropUnlistenFn: UnlistenFn | null = null
@ -115,6 +118,30 @@ function resetModelPaths() {
feedback.value = t('app.feedback.restoredDefaults') 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() { async function loadDefaultModelPaths() {
try { try {
const paths = await invoke<DefaultModelPaths>('get_default_model_paths') const paths = await invoke<DefaultModelPaths>('get_default_model_paths')
@ -169,30 +196,28 @@ async function bindMenuActions() {
} }
async function submitFiles(filePaths: string[]) { async function submitFiles(filePaths: string[]) {
if (outputMode.value === 'translate' && !hasTranslationKey.value) {
openApiKeyDialog()
return
}
pending.value = true pending.value = true
feedback.value = '' feedback.value = ''
try { try {
const fallbackToSource = outputMode.value === 'translate' && !hasTranslationKey.value
const effectiveOutputMode: OutputMode = fallbackToSource ? 'source' : outputMode.value
for (const filePath of filePaths) { for (const filePath of filePaths) {
await taskStore.startTask({ await taskStore.startTask({
filePath, filePath,
sourceLang: sourceLang.value === 'auto' ? null : sourceLang.value, sourceLang: sourceLang.value === 'auto' ? null : sourceLang.value,
targetLang: targetLang.value, targetLang: targetLang.value,
outputMode: effectiveOutputMode, outputMode: outputMode.value,
bilingualOutput: bilingualOutput.value, bilingualOutput: bilingualOutput.value,
translationConfig: effectiveOutputMode === 'translate' ? translationConfig.value : null, translationConfig: outputMode.value === 'translate' ? translationConfig.value : null,
whisperModelPath: whisperModelPath.value || null, whisperModelPath: whisperModelPath.value || null,
vadModelPath: vadModelPath.value || null, vadModelPath: vadModelPath.value || null,
}) })
} }
if (fallbackToSource) { feedback.value = t('app.feedback.submitted', { count: filePaths.length })
feedback.value = t('app.feedback.fallbackSource')
} else {
feedback.value = t('app.feedback.submitted', { count: filePaths.length })
}
} catch (error) { } catch (error) {
feedback.value = error instanceof Error ? error.message : t('app.feedback.submitFailed') feedback.value = error instanceof Error ? error.message : t('app.feedback.submitFailed')
} finally { } finally {
@ -342,6 +367,44 @@ async function handleTranslateFromEditor() {
</div> </div>
</div> </div>
</Transition> </Transition>
<Transition name="dialog">
<div v-if="showApiKeyDialog" class="dialog-overlay" @click.self="showApiKeyDialog = false">
<section class="api-key-dialog" role="dialog" aria-modal="true" :aria-label="$t('app.llm.apiKey')">
<div class="dialog-header">
<div>
<strong>{{ $t('app.llm.apiKey') }}</strong>
<p>{{ freeApiKeyHint() }}</p>
<ol class="api-key-options">
<li>
<span class="option-icon self-host"></span>
<span>{{ locale === 'zh-CN' ? '自建大模型服务,填入兼容 OpenAI 的 API 地址和 Key。' : 'Host your own model service with an OpenAI-compatible API URL and key.' }}</span>
</li>
<li>
<span class="option-icon provider"></span>
<span>{{ locale === 'zh-CN' ? '购买成熟服务DeepSeek、ChatGPT、Gemini 都可以。' : 'Use a mature paid service such as DeepSeek, ChatGPT, or Gemini.' }}</span>
</li>
<li>
<span class="option-icon free"></span>
<span>
{{ locale === 'zh-CN' ? '推荐免费的' : 'Recommended free option:' }}
<button class="api-key-link" type="button" @click="copyFreeApiKeyUrl">GLM-4.7-Flash</button>
<span v-if="apiKeyUrlCopied" class="copy-hint">
{{ locale === 'zh-CN' ? '已复制链接' : 'Link copied' }}
</span>
</span>
</li>
</ol>
</div>
<button class="dialog-close" type="button" aria-label="Close" @click="showApiKeyDialog = false">×</button>
</div>
<div class="dialog-actions">
<button class="button" type="button" @click="acknowledgeApiKeyDialog">
{{ locale === 'zh-CN' ? '明白了' : 'Got it' }}
</button>
</div>
</section>
</div>
</Transition>
<section class="topbar panel"> <section class="topbar panel">
<div class="toolbar-main"> <div class="toolbar-main">
<div class="toolbar-title"> <div class="toolbar-title">

View File

@ -934,3 +934,207 @@ textarea {
.drag-leave-to { .drag-leave-to {
opacity: 0; 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);
}