From 5f5255830ff593c8e1f6dad947a833ce9784e61e Mon Sep 17 00:00:00 2001 From: kura Date: Sat, 2 May 2026 16:48:32 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E4=B8=80=E4=BA=9B=E6=8F=90?= =?UTF-8?q?=E7=A4=BA=E6=80=A7=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.vue | 59 +++++++++++++++++++++++++++++++++++++++++++++------ src/style.css | 27 ++++++++++++++++++++--- 2 files changed, 76 insertions(+), 10 deletions(-) diff --git a/src/App.vue b/src/App.vue index a1a4024..d705002 100644 --- a/src/App.vue +++ b/src/App.vue @@ -67,17 +67,20 @@ const translationConfig = ref({ apiBase: localStorage.getItem('llm.apiBase') ?? 'https://open.bigmodel.cn/api/paas/v4', apiKey: localStorage.getItem('llm.apiKey') ?? '', model: localStorage.getItem('llm.model') ?? 'GLM-4.7-Flash', - batchSize: Number(localStorage.getItem('llm.batchSize') ?? '60'), + batchSize: Number(localStorage.getItem('llm.batchSize') ?? '12'), contextSize: Number(localStorage.getItem('llm.contextSize') ?? '5'), }) const pending = ref(false) const feedback = ref('') +const feedbackTone = ref<'normal' | 'error'>('normal') const showAdvanced = ref(false) const isDragging = ref(false) const showApiKeyDialog = ref(false) const apiKeyUrlCopied = ref(false) +const authorUrlCopied = ref(false) let unlistenMenuAction: UnlistenFn | null = null let dragDropUnlistenFn: UnlistenFn | null = null +let authorCopyTimer: ReturnType | null = null const selectedTask = computed(() => taskStore.selectedTask) const hasTranslationKey = computed(() => translationConfig.value.apiKey.trim().length > 0) @@ -98,10 +101,29 @@ onUnmounted(() => { dragDropUnlistenFn() dragDropUnlistenFn = null } + if (authorCopyTimer) { + clearTimeout(authorCopyTimer) + authorCopyTimer = null + } }) watch(locale, (newLocale) => { localStorage.setItem('locale', newLocale) + if (outputMode.value === 'translate' && !hasTranslationKey.value) { + showMissingApiKeyFeedback() + } +}) + +watch([outputMode, hasTranslationKey], ([mode, hasKey]) => { + if (mode === 'translate' && !hasKey) { + showMissingApiKeyFeedback() + return + } + + if (feedbackTone.value === 'error' && feedback.value === t('app.feedback.noApiKey')) { + feedbackTone.value = 'normal' + feedback.value = '' + } }) function persistTranslationConfig() { @@ -115,9 +137,15 @@ function persistTranslationConfig() { function resetModelPaths() { whisperModelPath.value = defaultModelPaths.value?.whisperModelPath ?? '' vadModelPath.value = defaultModelPaths.value?.vadModelPath ?? '' + feedbackTone.value = 'normal' feedback.value = t('app.feedback.restoredDefaults') } +function showMissingApiKeyFeedback() { + feedbackTone.value = 'error' + feedback.value = t('app.feedback.noApiKey') +} + function freeApiKeyHint() { if (locale.value === 'zh-CN') { return '开始翻译任务前需要先填写 LLM API Key。你可以选择下面任意一种方式。' @@ -127,7 +155,7 @@ function freeApiKeyHint() { } function openApiKeyDialog() { - feedback.value = t('app.feedback.noApiKey') + showMissingApiKeyFeedback() apiKeyUrlCopied.value = false showApiKeyDialog.value = true } @@ -137,6 +165,18 @@ async function copyFreeApiKeyUrl() { apiKeyUrlCopied.value = true } +async function copyAuthorUrl() { + await navigator.clipboard.writeText('https://kuraa.cc') + authorUrlCopied.value = true + if (authorCopyTimer) { + clearTimeout(authorCopyTimer) + } + authorCopyTimer = setTimeout(() => { + authorUrlCopied.value = false + authorCopyTimer = null + }, 1400) +} + function acknowledgeApiKeyDialog() { showApiKeyDialog.value = false outputMode.value = 'source' @@ -202,6 +242,7 @@ async function submitFiles(filePaths: string[]) { } pending.value = true + feedbackTone.value = 'normal' feedback.value = '' try { @@ -227,6 +268,7 @@ async function submitFiles(filePaths: string[]) { async function handlePickFiles() { try { + feedbackTone.value = 'normal' feedback.value = '' persistTranslationConfig() const selected = await open({ @@ -336,11 +378,13 @@ async function handleExport(format: 'srt' | 'vtt' | 'ass') { async function handleRetryTranslate(taskId: string) { persistTranslationConfig() if (!translationConfig.value.apiKey.trim()) { + feedbackTone.value = 'error' feedback.value = t('app.feedback.noApiKey') return } try { await taskStore.retryTranslation(taskId, translationConfig.value) + feedbackTone.value = 'normal' feedback.value = t('app.feedback.translationStarted') } catch (error) { feedback.value = error instanceof Error ? error.message : t('app.feedback.translationFailed') @@ -388,8 +432,8 @@ async function handleTranslateFromEditor() { {{ locale === 'zh-CN' ? '推荐免费的' : 'Recommended free option:' }} - - {{ locale === 'zh-CN' ? '已复制链接' : 'Link copied' }} + + {{ apiKeyUrlCopied ? (locale === 'zh-CN' ? '已复制链接' : 'Link copied') : (locale === 'zh-CN' ? '点击复制申请链接' : 'click to copy link') }} @@ -399,7 +443,7 @@ async function handleTranslateFromEditor() {
@@ -410,7 +454,8 @@ async function handleTranslateFromEditor() {
CrossSubtitle - by kuraa + by + 🎉
@@ -460,7 +505,7 @@ async function handleTranslateFromEditor() {
- +
diff --git a/src/style.css b/src/style.css index 2831a19..d0e290e 100644 --- a/src/style.css +++ b/src/style.css @@ -157,17 +157,27 @@ textarea { align-items: center; } -.credit-line a { +.credit-line a, +.author-link { color: var(--c-text); + background: transparent; + border: 0; + padding: 0; + cursor: pointer; text-decoration: none; border-bottom: 1px solid transparent; transition: border-color var(--transition); } -.credit-line a:hover { +.credit-line a:hover, +.author-link:hover { border-bottom-color: var(--c-text); } +.author-copy-hint { + line-height: 1; +} + .workspace-toolbar, .advanced-shell { margin-top: 16px; @@ -332,6 +342,11 @@ textarea { margin: 12px 0 0; } +.status-text.error { + color: var(--c-error); + font-weight: 500; +} + .content-grid { display: grid; grid-template-columns: 300px minmax(0, 1fr); @@ -950,6 +965,8 @@ textarea { .api-key-dialog { width: min(520px, 100%); + max-height: calc(100vh - 40px); + overflow: auto; padding: 20px; border: 1px solid var(--c-border); border-radius: var(--radius-lg); @@ -1109,10 +1126,14 @@ textarea { } .copy-hint { - color: var(--c-success); + color: var(--c-text-tertiary); font-size: 12px; } +.copy-hint.copied { + color: var(--c-success); +} + .dialog-actions { display: flex; justify-content: flex-end;