完善提示
This commit is contained in:
parent
763a14265d
commit
6eb2fc18b3
83
src/App.vue
83
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<TargetLanguage>('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<DefaultModelPaths>('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() {
|
||||
</div>
|
||||
</div>
|
||||
</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">
|
||||
<div class="toolbar-main">
|
||||
<div class="toolbar-title">
|
||||
|
||||
204
src/style.css
204
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);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user