|
|
@@ -0,0 +1,521 @@
|
|
|
+<template>
|
|
|
+ <el-dialog :visible="visible" title="校对字幕" width="70%" :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, lrc_data.begin_time / 1000)">
|
|
|
+ <span :class="[lrcIndex.row === i && lrcIndex.col === j ? 'triangle' : 'lrc-line']"></span>
|
|
|
+ </div>
|
|
|
+ <div class="lrc-time">
|
|
|
+ <span
|
|
|
+ >[
|
|
|
+ <el-input
|
|
|
+ :class="[lrcIndex.row === i && lrcIndex.col === j && inputIndex === 0 ? 'active' : '']"
|
|
|
+ v-model="lrc_data.show_begin_time"
|
|
|
+ type="text"
|
|
|
+ @blur="handleBlur(i, j, 'bg')"
|
|
|
+ ></el-input>
|
|
|
+ ]</span
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+ <div class="lrc-time">
|
|
|
+ <span
|
|
|
+ >[<el-input
|
|
|
+ :class="[lrcIndex.row === i && lrcIndex.col === j && inputIndex === 1 ? 'active' : '']"
|
|
|
+ v-model="lrc_data.show_end_time"
|
|
|
+ type="text"
|
|
|
+ @blur="handleBlur(i, j, 'ed')"
|
|
|
+ ></el-input
|
|
|
+ >]</span
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+ <!-- eslint-disable-next-line vue/no-v-html -->
|
|
|
+ <span class="lrc-text" v-html="content" @click="selectLrcItem(i, j, lrc_data.begin_time / 1000)"></span>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="audio-player">
|
|
|
+ <!-- <div class="player-line">
|
|
|
+ <div
|
|
|
+ class="progress-bar"
|
|
|
+ :style="{ width: Math.min((audio.current_time / audio.max_time) * 100, 100) + '%' }"
|
|
|
+ ></div>
|
|
|
+ <div class="progress-dot"></div>
|
|
|
+ </div> -->
|
|
|
+ <div class="player-line">
|
|
|
+ <el-slider
|
|
|
+ v-model="play_value"
|
|
|
+ :style="{ width: '100%', height: '3px' }"
|
|
|
+ :format-tooltip="formatProcessToolTip"
|
|
|
+ @change="changeCurrentTime"
|
|
|
+ />
|
|
|
+ </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';
|
|
|
+import { getArrayDepth } from '@/utils/common';
|
|
|
+
|
|
|
+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,
|
|
|
+ getArrayDepth,
|
|
|
+ inputIndex: 0, // 开始时间/结束时间索引
|
|
|
+ };
|
|
|
+ },
|
|
|
+ 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();
|
|
|
+ this.lrcIndex = {
|
|
|
+ row: 0,
|
|
|
+ col: 0,
|
|
|
+ };
|
|
|
+ }
|
|
|
+ },
|
|
|
+ },
|
|
|
+ optionList: {
|
|
|
+ handler(val) {
|
|
|
+ let _val = JSON.parse(JSON.stringify(val));
|
|
|
+ this.dataList = this.getArrayDepth(_val) === 1 ? [_val] : _val;
|
|
|
+ console.log(this.dataList);
|
|
|
+ this.dataList.forEach((list) => {
|
|
|
+ list.forEach((item) => {
|
|
|
+ this.$set(item.lrc_data, 'show_begin_time', this.audioTimeToMMSS(item.lrc_data.begin_time / 1000));
|
|
|
+ this.$set(item.lrc_data, 'show_end_time', this.audioTimeToMMSS(item.lrc_data.end_time / 1000));
|
|
|
+ });
|
|
|
+ });
|
|
|
+ },
|
|
|
+ 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 * 1000; // 转为毫秒
|
|
|
+ let obj = {
|
|
|
+ begin_time: currentTime,
|
|
|
+ end_time: currentTime + 1,
|
|
|
+ text: '',
|
|
|
+ show_begin_time: this.audioTimeToMMSS(currentTime / 1000),
|
|
|
+ show_end_time: this.audioTimeToMMSS(currentTime + 1 / 1000),
|
|
|
+ };
|
|
|
+ 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.dataList[this.dataList.length - 1][
|
|
|
+ this.dataList[this.dataList.length - 1].length - 1
|
|
|
+ ].lrc_data.show_end_time = this.audioTimeToMMSS(currentTime / 1000);
|
|
|
+ }
|
|
|
+
|
|
|
+ this.lrcIndex = {
|
|
|
+ row: -1,
|
|
|
+ col: -1,
|
|
|
+ };
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (this.inputIndex === 0) {
|
|
|
+ this.dataList[row][col].lrc_data.begin_time = currentTime;
|
|
|
+ this.dataList[row][col].lrc_data.show_begin_time = this.audioTimeToMMSS(currentTime / 1000);
|
|
|
+ this.inputIndex = 1;
|
|
|
+ } else {
|
|
|
+ this.dataList[row][col].lrc_data.end_time = currentTime;
|
|
|
+ this.dataList[row][col].lrc_data.show_end_time = this.audioTimeToMMSS(currentTime / 1000);
|
|
|
+ this.inputIndex = 0;
|
|
|
+ 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,
|
|
|
+ };
|
|
|
+ }
|
|
|
+ // this.dataList[row][col].lrc_data = obj;
|
|
|
+ // // 并将上一个标记的结束时间设置为当前时间
|
|
|
+ // if (col > 0) {
|
|
|
+ // this.dataList[row][col - 1].lrc_data.end_time = currentTime;
|
|
|
+ // this.dataList[row][col - 1].lrc_data.show_end_time = this.audioTimeToMMSS(currentTime / 1000);
|
|
|
+ // }
|
|
|
+ // if (col === 0 && row > 0) {
|
|
|
+ // this.dataList[row - 1][this.dataList[row - 1].length - 1].lrc_data.end_time = currentTime;
|
|
|
+ // this.dataList[row - 1][this.dataList[row - 1].length - 1].lrc_data.show_end_time = this.audioTimeToMMSS(
|
|
|
+ // currentTime / 1000,
|
|
|
+ // );
|
|
|
+ // }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ selectLrcItem(i, j, time) {
|
|
|
+ this.lrcIndex = {
|
|
|
+ row: i,
|
|
|
+ col: j,
|
|
|
+ };
|
|
|
+ this.audio.current_time = time;
|
|
|
+ this.changeCurrentTime((time / this.audio.max_time) * 100);
|
|
|
+ this.inputIndex = 0;
|
|
|
+ },
|
|
|
+ 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();
|
|
|
+ },
|
|
|
+ // 点击 拖拽播放音频
|
|
|
+ changeCurrentTime(value) {
|
|
|
+ let audioId = this.audioId;
|
|
|
+ this.$refs[audioId].currentTime = parseInt((value / 100) * this.audio.max_time);
|
|
|
+ this.$refs[audioId].play();
|
|
|
+ this.audio.playing = true;
|
|
|
+ },
|
|
|
+ // 进度条格式化toolTip
|
|
|
+ formatProcessToolTip(index) {
|
|
|
+ return this.audioTimeToMMSS(parseInt((this.audio.max_time / 100) * index));
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * 音频元数据加载完成
|
|
|
+ */
|
|
|
+ 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() {
|
|
|
+ const subtitles = this.getArrayDepth(this.optionList) === 1 ? this.dataList.flat() : this.dataList;
|
|
|
+ this.$emit('saveSubtitles', subtitles);
|
|
|
+ this.$message.success('字幕已保存');
|
|
|
+ },
|
|
|
+ // 手动修改时间
|
|
|
+ handleBlur(i, j, type) {
|
|
|
+ let show_value = null;
|
|
|
+ if (type === 'bg') {
|
|
|
+ show_value = this.dataList[i][j].lrc_data.show_begin_time;
|
|
|
+ } else {
|
|
|
+ show_value = this.dataList[i][j].lrc_data.show_end_time;
|
|
|
+ }
|
|
|
+ let arr = show_value.split(':');
|
|
|
+ if (arr.length === 3) {
|
|
|
+ let new_value = (arr[0] * 60 + arr[1] * 1) * 1000 + arr[2].substring(0, 3).padEnd(3, '0') * 1; // 毫秒只保留3位 不足补0
|
|
|
+ if (type === 'bg') {
|
|
|
+ this.dataList[i][j].lrc_data.begin_time = new_value;
|
|
|
+ this.dataList[i][j].lrc_data.show_begin_time = this.audioTimeToMMSS(
|
|
|
+ this.dataList[i][j].lrc_data.begin_time / 1000,
|
|
|
+ );
|
|
|
+ } else {
|
|
|
+ this.dataList[i][j].lrc_data.end_time = new_value;
|
|
|
+ this.dataList[i][j].lrc_data.show_end_time = this.audioTimeToMMSS(
|
|
|
+ this.dataList[i][j].lrc_data.end_time / 1000,
|
|
|
+ );
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ this.$message.warning('字幕时间应为00:00:000格式,请重新输入');
|
|
|
+ if (type === 'bg') {
|
|
|
+ this.dataList[i][j].lrc_data.show_begin_time = this.audioTimeToMMSS(
|
|
|
+ this.dataList[i][j].lrc_data.begin_time / 1000,
|
|
|
+ );
|
|
|
+ } else {
|
|
|
+ this.dataList[i][j].lrc_data.show_end_time = this.audioTimeToMMSS(
|
|
|
+ this.dataList[i][j].lrc_data.end_time / 1000,
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ },
|
|
|
+};
|
|
|
+</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;
|
|
|
+ min-width: 85px;
|
|
|
+
|
|
|
+ span {
|
|
|
+ display: flex;
|
|
|
+
|
|
|
+ :deep .el-input {
|
|
|
+ width: 75px;
|
|
|
+
|
|
|
+ .el-input__inner {
|
|
|
+ height: 20px;
|
|
|
+ padding: 0 1px;
|
|
|
+ line-height: 20px;
|
|
|
+ background-color: transparent;
|
|
|
+ border: none;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .active {
|
|
|
+ :deep .el-input__inner {
|
|
|
+ background-color: #bfcef2;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .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%;
|
|
|
+ max-width: 540px;
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep .el-slider__runway {
|
|
|
+ height: 6px;
|
|
|
+ padding: 0;
|
|
|
+ margin: 0;
|
|
|
+ background: #e5e5e5;
|
|
|
+ border-radius: 0;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|