完善提示
This commit is contained in:
parent
763a14265d
commit
6eb2fc18b3
81
src/App.vue
81
src/App.vue
@ -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.fallbackSource')
|
|
||||||
} else {
|
|
||||||
feedback.value = t('app.feedback.submitted', { count: filePaths.length })
|
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">
|
||||||
|
|||||||
204
src/style.css
204
src/style.css
@ -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);
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user