|
@@ -0,0 +1,373 @@
|
|
|
+<template>
|
|
|
+ <el-dialog :visible="visible" title="校对字幕" width="580px" :close-on-click-modal="false" @close="dialogClose">
|
|
|
+ <div class="dialog-container">
|
|
|
+ <div class="lrc">
|
|
|
+ <template v-for="(list, i) in dataList">
|
|
|
+ <div v-for="({ content, mark, lrc_data }, j) in list" :key="mark" class="lrc-item">
|
|
|
+ <div class="lrc-mark" @click="selectLrcItem(i, j)">
|
|
|
+ <span :class="[lrcIndex.row === i && lrcIndex.col === j ? 'triangle' : 'lrc-line']"></span>
|
|
|
+ </div>
|
|
|
+ <div class="lrc-time">
|
|
|
+ <span>[{{ audioTimeToMMSS(lrc_data.begin_time) }}]</span>
|
|
|
+ </div>
|
|
|
+ <span class="lrc-text" v-html="content"></span>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="audio-player">
|
|
|
+ <div class="player-line">
|
|
|
+ <div class="progress-bar" :style="{ width: (audio.current_time / audio.max_time) * 100 + '%' }"></div>
|
|
|
+ <div class="progress-dot"></div>
|
|
|
+ </div>
|
|
|
+ <div class="player-controls">
|
|
|
+ <div class="timer">
|
|
|
+ <span>{{ secondFormatConversion(audio.current_time) }}</span> / <span>{{ audio_allTime }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="svg-btn">
|
|
|
+ <SvgIcon :icon-class="iconClass" size="16" @click="playAudio" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="controls">
|
|
|
+ <span class="tips">播放时按空格进行打点</span>
|
|
|
+ <el-button type="primary" @click="saveSubtitles">保存</el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <audio
|
|
|
+ :id="audioId"
|
|
|
+ :ref="audioId"
|
|
|
+ :src="url"
|
|
|
+ preload="metadata"
|
|
|
+ @loadedmetadata="onLoadedmetadata"
|
|
|
+ @timeupdate="onTimeupdate"
|
|
|
+ @canplaythrough="oncanplaythrough"
|
|
|
+ @pause="onPause"
|
|
|
+ @play="onPlay"
|
|
|
+ ></audio>
|
|
|
+ </el-dialog>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import { GetFileURLMap } from '@/api/app';
|
|
|
+import { secondFormatConversion, audioTimeToMMSS } from '@/utils/transform';
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: 'CheckSubtitles',
|
|
|
+ props: {
|
|
|
+ visible: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false,
|
|
|
+ },
|
|
|
+ audioId: {
|
|
|
+ type: String,
|
|
|
+ required: true,
|
|
|
+ },
|
|
|
+ optionList: {
|
|
|
+ type: Array,
|
|
|
+ default: () => [],
|
|
|
+ },
|
|
|
+ },
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ url: '',
|
|
|
+ audio: {
|
|
|
+ paused: true,
|
|
|
+ playing: false,
|
|
|
+ // 音频当前播放时长
|
|
|
+ current_time: 0,
|
|
|
+ // 音频最大播放时长
|
|
|
+ max_time: 0,
|
|
|
+ isPlaying: false,
|
|
|
+ loading: false,
|
|
|
+ },
|
|
|
+ play_value: 0,
|
|
|
+ dataList: [],
|
|
|
+ lrcIndex: {
|
|
|
+ row: 0,
|
|
|
+ col: 0,
|
|
|
+ },
|
|
|
+ audio_allTime: null, // 展示总时间
|
|
|
+ secondFormatConversion,
|
|
|
+ audioTimeToMMSS,
|
|
|
+ };
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ iconClass() {
|
|
|
+ return this.audio.paused ? 'paused' : 'playing';
|
|
|
+ },
|
|
|
+ },
|
|
|
+ watch: {
|
|
|
+ audioId: {
|
|
|
+ handler(val) {
|
|
|
+ if (!val) return;
|
|
|
+ GetFileURLMap({ file_id_list: [val] }).then(({ url_map }) => {
|
|
|
+ this.url = url_map[val];
|
|
|
+ });
|
|
|
+ },
|
|
|
+ immediate: true,
|
|
|
+ },
|
|
|
+ visible: {
|
|
|
+ handler(val) {
|
|
|
+ if (!val) {
|
|
|
+ this.$refs[this.audioId].pause();
|
|
|
+ }
|
|
|
+ },
|
|
|
+ },
|
|
|
+ optionList: {
|
|
|
+ handler(val) {
|
|
|
+ this.dataList = JSON.parse(JSON.stringify(val));
|
|
|
+ },
|
|
|
+ immediate: true,
|
|
|
+ deep: true,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ mounted() {
|
|
|
+ window.addEventListener('keydown', this.handleKeydown);
|
|
|
+ },
|
|
|
+ beforeDestroy() {
|
|
|
+ window.removeEventListener('keydown', this.handleKeydown);
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ dialogClose() {
|
|
|
+ this.$emit('update:visible', false);
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * 处理键盘按下事件
|
|
|
+ */
|
|
|
+ handleKeydown(event) {
|
|
|
+ if (!this.visible) return;
|
|
|
+ if (event.code === 'Space') {
|
|
|
+ event.preventDefault();
|
|
|
+ // 获取当前播放时间
|
|
|
+ const audio = this.$refs[this.audioId];
|
|
|
+ if (audio) {
|
|
|
+ const currentTime = audio.currentTime;
|
|
|
+ let obj = {
|
|
|
+ begin_time: currentTime,
|
|
|
+ end_time: currentTime + 1,
|
|
|
+ text: '',
|
|
|
+ };
|
|
|
+ const { row, col } = this.lrcIndex;
|
|
|
+ if (row === -1) {
|
|
|
+ this.$message.warning('没有可用的标记位置');
|
|
|
+ const { begin_time, end_time } =
|
|
|
+ this.dataList[this.dataList.length - 1][this.dataList[this.dataList.length - 1].length - 1].lrc_data;
|
|
|
+
|
|
|
+ // 判断最后一个标记是否有效,如果无效则更新最后一个标记的结束时间
|
|
|
+ if (begin_time + 1 === end_time) {
|
|
|
+ this.dataList[this.dataList.length - 1][
|
|
|
+ this.dataList[this.dataList.length - 1].length - 1
|
|
|
+ ].lrc_data.end_time = currentTime;
|
|
|
+ }
|
|
|
+
|
|
|
+ this.lrcIndex = {
|
|
|
+ row: -1,
|
|
|
+ col: -1,
|
|
|
+ };
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.dataList[row][col].lrc_data = obj;
|
|
|
+ // 并将上一个标记的结束时间设置为当前时间
|
|
|
+ if (col > 0) {
|
|
|
+ this.dataList[row][col - 1].lrc_data.end_time = currentTime;
|
|
|
+ }
|
|
|
+ if (col === 0 && row > 0) {
|
|
|
+ this.dataList[row - 1][this.dataList[row - 1].length - 1].lrc_data.end_time = currentTime;
|
|
|
+ }
|
|
|
+ let maxCol = this.dataList[row].length - 1;
|
|
|
+ let maxRow = this.dataList.length - 1;
|
|
|
+ let nextCol = col + 1;
|
|
|
+ let nextRow = row;
|
|
|
+ if (nextCol > maxCol) {
|
|
|
+ nextCol = 0;
|
|
|
+ nextRow += 1;
|
|
|
+ if (nextRow > maxRow) {
|
|
|
+ nextRow = -1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 更新当前标记的索引
|
|
|
+ this.lrcIndex = {
|
|
|
+ row: nextRow,
|
|
|
+ col: nextCol,
|
|
|
+ };
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ selectLrcItem(i, j) {
|
|
|
+ this.lrcIndex = {
|
|
|
+ row: i,
|
|
|
+ col: j,
|
|
|
+ };
|
|
|
+ },
|
|
|
+ playAudio() {
|
|
|
+ if (!this.url) return;
|
|
|
+ const audio = this.$refs[this.audioId];
|
|
|
+ // 暂停其他音频
|
|
|
+ let audioArr = document.getElementsByTagName('audio');
|
|
|
+ if (audioArr && audioArr.length > 0) {
|
|
|
+ for (let i = 0; i < audioArr.length; i++) {
|
|
|
+ if (audioArr[i].src === this.url) {
|
|
|
+ if (audioArr[i].id !== this.audioId) {
|
|
|
+ audioArr[i].pause();
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ audioArr[i].pause();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ audio.paused ? audio.play() : audio.pause();
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * 音频元数据加载完成
|
|
|
+ */
|
|
|
+ onLoadedmetadata(res) {
|
|
|
+ this.audio.max_time = parseInt(res.target.duration);
|
|
|
+ this.audio_allTime = secondFormatConversion(this.audio.max_time);
|
|
|
+ },
|
|
|
+ onTimeupdate(res) {
|
|
|
+ this.audio.current_time = res.target.currentTime;
|
|
|
+ this.play_value = (this.audio.current_time / this.audio.max_time) * 100;
|
|
|
+ if (this.audio.current_time * 1000 > this.ed) {
|
|
|
+ this.$refs[this.audioId].pause();
|
|
|
+ }
|
|
|
+ },
|
|
|
+ oncanplaythrough() {
|
|
|
+ this.audio.loading = false;
|
|
|
+ },
|
|
|
+ onPause() {
|
|
|
+ this.audio.paused = true;
|
|
|
+ },
|
|
|
+ onPlay() {
|
|
|
+ this.audio.paused = false;
|
|
|
+ },
|
|
|
+ saveSubtitles() {
|
|
|
+ this.$emit('saveSubtitles', this.dataList);
|
|
|
+ this.$message.success('字幕已保存');
|
|
|
+ },
|
|
|
+ },
|
|
|
+};
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+@use '@/styles/mixin.scss' as *;
|
|
|
+
|
|
|
+.dialog-container {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ row-gap: 8px;
|
|
|
+
|
|
|
+ > div {
|
|
|
+ padding: 8px 4px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .lrc {
|
|
|
+ height: 280px;
|
|
|
+ overflow-y: auto;
|
|
|
+
|
|
|
+ &-item {
|
|
|
+ display: flex;
|
|
|
+ column-gap: 8px;
|
|
|
+ align-items: stretch;
|
|
|
+
|
|
|
+ .lrc-mark {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ cursor: pointer;
|
|
|
+
|
|
|
+ .triangle {
|
|
|
+ width: 0;
|
|
|
+ height: 0;
|
|
|
+ border-top: 4px solid transparent;
|
|
|
+ border-right: 0 solid transparent;
|
|
|
+ border-bottom: 4px solid transparent;
|
|
|
+ border-left: 8px solid $main-color;
|
|
|
+ }
|
|
|
+
|
|
|
+ .lrc-line {
|
|
|
+ width: 6px;
|
|
|
+ height: 1px;
|
|
|
+ background-color: #666; // 灰色线条
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .lrc-time {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .lrc-text {
|
|
|
+ :deep p {
|
|
|
+ margin: 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .audio-player {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ row-gap: 16px;
|
|
|
+ color: #000;
|
|
|
+ background-color: #f2f3f5;
|
|
|
+ border: $border;
|
|
|
+ border-radius: 4px;
|
|
|
+
|
|
|
+ .player-line {
|
|
|
+ width: 100%;
|
|
|
+ height: 3px;
|
|
|
+ background-color: #ccc;
|
|
|
+
|
|
|
+ .progress-bar {
|
|
|
+ position: relative;
|
|
|
+ width: 0%;
|
|
|
+ height: 100%;
|
|
|
+ background-color: $main-color;
|
|
|
+
|
|
|
+ &::after {
|
|
|
+ position: absolute;
|
|
|
+ top: -2px;
|
|
|
+ right: -4px;
|
|
|
+ display: inline-block;
|
|
|
+ width: 8px;
|
|
|
+ height: 8px;
|
|
|
+ content: '';
|
|
|
+ background-color: $main-color;
|
|
|
+ border-radius: 50%;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .player-controls {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+
|
|
|
+ svg {
|
|
|
+ cursor: pointer;
|
|
|
+ }
|
|
|
+
|
|
|
+ .svg-btn {
|
|
|
+ display: flex;
|
|
|
+ flex: 1;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .controls {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ background-color: #f2f3f5;
|
|
|
+ border: $border;
|
|
|
+ border-radius: 4px;
|
|
|
+
|
|
|
+ .tips {
|
|
|
+ flex: 1;
|
|
|
+ text-align: center;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|