crosssubtitle-ai/src/components/SubtitleEditor.vue
2026-05-01 20:30:17 +08:00

138 lines
4.7 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import { computed, ref, nextTick, watch } from 'vue'
import type { SubtitleSegment, SubtitleTask } from '../lib/types'
const props = defineProps<{
task: SubtitleTask | null
logs: string[]
}>()
const isProcessing = computed(() => {
if (!props.task) return false
return !['completed', 'failed'].includes(props.task.status)
})
const canExport = computed(() => {
return props.task?.status === 'completed' && (props.task.segments?.length ?? 0) > 0
})
const canTranslate = computed(() => {
if (!props.task) return false
return (
props.task.segments.length > 0 &&
props.task.segments.some((s) => s.sourceText)
)
})
const emit = defineEmits<{
save: [segment: SubtitleSegment]
export: [format: 'srt' | 'vtt' | 'ass']
translate: []
}>()
const sortedSegments = computed(() =>
[...(props.task?.segments ?? [])].sort((left, right) => {
if (left.start !== right.start) return left.start - right.start
if (left.end !== right.end) return left.end - right.end
return left.id.localeCompare(right.id)
}),
)
const logsExpanded = ref(false)
const logContainerRef = ref<HTMLElement | null>(null)
watch(() => props.logs.length, () => {
nextTick(() => {
if (logContainerRef.value) {
logContainerRef.value.scrollTop = logContainerRef.value.scrollHeight
}
})
})
function formatTime(seconds: number) {
const ms = Math.round(seconds * 1000)
const h = Math.floor(ms / 3600000)
const m = Math.floor((ms % 3600000) / 60000)
const s = Math.floor((ms % 60000) / 1000)
const milli = ms % 1000
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}.${String(milli).padStart(3, '0')}`
}
function updateTranslatedText(segment: SubtitleSegment, value: string) {
emit('save', { ...segment, translatedText: value })
}
</script>
<template>
<section class="panel workspace-panel">
<div class="workspace-header">
<div>
<strong>{{ task?.fileName ?? $t('editor.title') }}</strong>
<p class="panel-subtitle">
{{ task ? $t('editor.segments', { count: sortedSegments.length }) : $t('editor.selectTask') }}
</p>
</div>
<div v-if="task" class="export-actions">
<button
v-if="canTranslate"
class="button primary small"
:disabled="isProcessing"
@click="emit('translate')"
>{{ $t('editor.translate') }}</button>
<button class="button secondary small" :disabled="!canExport" @click="emit('export', 'srt')">SRT</button>
<button class="button secondary small" :disabled="!canExport" @click="emit('export', 'vtt')">VTT</button>
<button class="button secondary small" :disabled="!canExport" @click="emit('export', 'ass')">ASS</button>
</div>
</div>
<div v-if="!task" class="empty-state">
<p>{{ $t('editor.empty') }}</p>
<p style="margin-top: 6px; font-size: 11px;">{{ $t('editor.emptyHint') }}</p>
</div>
<div v-else-if="sortedSegments.length === 0" class="empty-state">
<template v-if="isProcessing">{{ $t('editor.processing') }}</template>
<template v-else-if="task?.status === 'failed'">{{ $t('editor.failed') }}</template>
<template v-else>{{ $t('editor.noSegments') }}</template>
</div>
<div v-else class="segment-list">
<article
v-for="segment in sortedSegments"
:key="segment.id"
class="segment-item"
>
<div class="task-row subtle">
<span>{{ formatTime(segment.start) }} - {{ formatTime(segment.end) }}</span>
<span class="segment-id">{{ segment.id }}</span>
</div>
<p class="source-text">{{ segment.sourceText || $t('editor.waiting') }}</p>
<textarea
class="editor-input"
:value="segment.translatedText ?? ''"
:placeholder="task.outputMode === 'translate' ? $t('editor.translation') : $t('editor.source')"
:disabled="task.outputMode === 'source'"
@change="updateTranslatedText(segment, ($event.target as HTMLTextAreaElement).value)"
/>
</article>
</div>
<div class="log-drawer" :class="{ expanded: logsExpanded }">
<button class="log-toggle" type="button" @click="logsExpanded = !logsExpanded">
<span>{{ $t('editor.logs') }}</span>
<span class="subtle">{{ logs.length }}</span>
<span class="log-chevron">{{ logsExpanded ? '' : '+' }}</span>
</button>
<div v-if="logsExpanded" class="log-panel">
<div v-if="logs.length === 0" class="empty-state">
{{ $t('editor.noLogs') }}
</div>
<div v-else ref="logContainerRef" class="log-list">
<pre v-for="(line, i) in logs" :key="i" class="log-line">{{ line }}</pre>
</div>
</div>
</div>
</section>
</template>