文章目录[隐藏]
详细教程:为WordPress网站打造内嵌在线简易视频字幕生成与编辑工具
引言:为什么网站需要内置视频字幕工具?
在当今多媒体内容主导的互联网环境中,视频已成为网站吸引和保留用户的重要手段。然而,许多网站运营者面临一个共同挑战:如何高效地为视频内容添加字幕。字幕不仅有助于听力障碍用户访问内容,还能提高视频在静音环境下的观看率,对SEO优化也有显著帮助。
传统上,为视频添加字幕需要依赖第三方工具或专业软件,过程繁琐且耗时。本文将详细介绍如何通过WordPress代码二次开发,为您的网站打造一个内嵌的在线简易视频字幕生成与编辑工具,让字幕创建变得简单高效。
第一部分:项目规划与技术选型
1.1 功能需求分析
在开始开发之前,我们需要明确工具的核心功能:
- 视频上传与预览:支持常见视频格式上传和实时预览
- 自动语音识别(ASR):将视频中的语音转换为文字
- 字幕编辑界面:直观的时间轴编辑功能
- 字幕格式支持:至少支持SRT、VTT等常用字幕格式
- 实时预览:编辑字幕时可实时查看效果
- 导出与集成:将生成的字幕与视频关联或单独导出
1.2 技术架构设计
我们将采用以下技术栈:
- 前端:HTML5、CSS3、JavaScript(使用Vue.js框架)
- 视频处理:HTML5 Video API + FFmpeg.wasm(浏览器端处理)
- 语音识别:Web Speech API(免费基础方案)或集成第三方API(如Google Cloud Speech-to-Text)
- 后端:WordPress PHP环境,使用REST API处理文件操作
- 存储:WordPress媒体库 + 自定义数据库表存储字幕数据
1.3 开发环境准备
确保您的WordPress开发环境满足以下条件:
- WordPress 5.0+(支持REST API)
- PHP 7.4+(支持最新语法特性)
- 至少256MB内存限制(用于处理视频文件)
- 启用文件上传功能(支持视频格式)
- 安装并启用必要的开发插件(如Query Monitor、Debug Bar)
第二部分:创建WordPress插件框架
2.1 初始化插件结构
首先,在WordPress的wp-content/plugins/目录下创建新文件夹video-subtitle-tool,并创建以下基础文件结构:
video-subtitle-tool/
├── video-subtitle-tool.php # 主插件文件
├── includes/
│ ├── class-database.php # 数据库处理类
│ ├── class-video-processor.php # 视频处理类
│ ├── class-subtitle-generator.php # 字幕生成类
│ └── class-api-handler.php # REST API处理类
├── admin/
│ ├── css/
│ │ └── admin-style.css # 后台样式
│ ├── js/
│ │ └── admin-script.js # 后台脚本
│ └── admin-page.php # 管理页面
├── public/
│ ├── css/
│ │ └── public-style.css # 前端样式
│ ├── js/
│ │ ├── app.js # 主Vue应用
│ │ ├── video-processor.js # 视频处理逻辑
│ │ └── subtitle-editor.js # 字幕编辑器
│ └── shortcode.php # 短代码处理
├── assets/
│ ├── ffmpeg/
│ │ └── ffmpeg-core.js # FFmpeg.wasm核心文件
│ └── icons/ # 图标资源
└── templates/
└── subtitle-editor.php # 编辑器模板
2.2 主插件文件配置
编辑video-subtitle-tool.php,添加插件基本信息:
<?php
/**
* Plugin Name: 视频字幕生成与编辑工具
* Plugin URI: https://yourwebsite.com/video-subtitle-tool
* Description: 为WordPress网站添加内嵌的在线视频字幕生成与编辑功能
* Version: 1.0.0
* Author: 您的名称
* License: GPL v2 or later
* Text Domain: video-subtitle-tool
*/
// 防止直接访问
if (!defined('ABSPATH')) {
exit;
}
// 定义插件常量
define('VST_VERSION', '1.0.0');
define('VST_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('VST_PLUGIN_URL', plugin_dir_url(__FILE__));
define('VST_MAX_FILE_SIZE', 500 * 1024 * 1024); // 500MB限制
// 自动加载类文件
spl_autoload_register(function ($class) {
$prefix = 'VST_';
$base_dir = VST_PLUGIN_DIR . 'includes/';
$len = strlen($prefix);
if (strncmp($prefix, $class, $len) !== 0) {
return;
}
$relative_class = substr($class, $len);
$file = $base_dir . 'class-' . str_replace('_', '-', strtolower($relative_class)) . '.php';
if (file_exists($file)) {
require $file;
}
});
// 初始化插件
function vst_init() {
// 检查依赖
if (!function_exists('wp_get_current_user')) {
require_once(ABSPATH . 'wp-includes/pluggable.php');
}
// 初始化数据库
VST_Database::init();
// 注册短代码
add_shortcode('video_subtitle_editor', array('VST_Shortcode', 'render_editor'));
// 初始化REST API
add_action('rest_api_init', array('VST_API_Handler', 'register_routes'));
// 加载文本域
load_plugin_textdomain('video-subtitle-tool', false, dirname(plugin_basename(__FILE__)) . '/languages/');
}
add_action('plugins_loaded', 'vst_init');
// 激活插件时执行
function vst_activate() {
require_once VST_PLUGIN_DIR . 'includes/class-database.php';
VST_Database::create_tables();
// 设置默认选项
add_option('vst_default_language', 'zh-CN');
add_option('vst_max_video_duration', 3600); // 默认最大1小时
add_option('vst_enable_auto_generation', true);
}
register_activation_hook(__FILE__, 'vst_activate');
// 停用插件时清理
function vst_deactivate() {
// 清理临时文件
VST_Video_Processor::cleanup_temp_files();
}
register_deactivation_hook(__FILE__, 'vst_deactivate');
第三部分:数据库设计与实现
3.1 创建数据库表
编辑includes/class-database.php:
<?php
class VST_Database {
public static function init() {
// 数据库版本
$db_version = '1.0';
$current_version = get_option('vst_db_version', '0');
if ($current_version !== $db_version) {
self::create_tables();
update_option('vst_db_version', $db_version);
}
}
public static function create_tables() {
global $wpdb;
$charset_collate = $wpdb->get_charset_collate();
$table_prefix = $wpdb->prefix . 'vst_';
// 视频项目表
$projects_table = $table_prefix . 'projects';
$projects_sql = "CREATE TABLE IF NOT EXISTS $projects_table (
id bigint(20) NOT NULL AUTO_INCREMENT,
user_id bigint(20) NOT NULL,
title varchar(255) NOT NULL,
video_url varchar(1000) NOT NULL,
video_duration int(11) DEFAULT 0,
language varchar(10) DEFAULT 'zh-CN',
status varchar(20) DEFAULT 'draft',
created_at datetime DEFAULT CURRENT_TIMESTAMP,
updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY user_id (user_id),
KEY status (status)
) $charset_collate;";
// 字幕表
$subtitles_table = $table_prefix . 'subtitles';
$subtitles_sql = "CREATE TABLE IF NOT EXISTS $subtitles_table (
id bigint(20) NOT NULL AUTO_INCREMENT,
project_id bigint(20) NOT NULL,
start_time int(11) NOT NULL,
end_time int(11) NOT NULL,
text text NOT NULL,
confidence float DEFAULT 1.0,
is_edited tinyint(1) DEFAULT 0,
sequence int(11) NOT NULL,
PRIMARY KEY (id),
KEY project_id (project_id),
KEY sequence (sequence)
) $charset_collate;";
// 项目设置表
$settings_table = $table_prefix . 'project_settings';
$settings_sql = "CREATE TABLE IF NOT EXISTS $settings_table (
project_id bigint(20) NOT NULL,
font_family varchar(100) DEFAULT 'Arial',
font_size int(11) DEFAULT 24,
font_color varchar(7) DEFAULT '#FFFFFF',
background_color varchar(7) DEFAULT '#00000080',
position varchar(20) DEFAULT 'bottom',
max_lines int(11) DEFAULT 2,
UNIQUE KEY project_id (project_id)
) $charset_collate;";
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
dbDelta($projects_sql);
dbDelta($subtitles_sql);
dbDelta($settings_sql);
}
public static function cleanup_temp_files() {
// 清理超过24小时的临时文件
$temp_dir = wp_upload_dir()['basedir'] . '/vst_temp/';
if (is_dir($temp_dir)) {
$files = glob($temp_dir . '*');
$now = time();
foreach ($files as $file) {
if (is_file($file)) {
if ($now - filemtime($file) >= 86400) { // 24小时
unlink($file);
}
}
}
}
}
}
第四部分:视频处理与语音识别模块
4.1 视频处理类实现
创建includes/class-video-processor.php:
<?php
class VST_Video_Processor {
/**
* 处理上传的视频文件
*/
public static function process_upload($file) {
// 安全检查
if (!self::validate_file($file)) {
return new WP_Error('invalid_file', '无效的视频文件');
}
// 创建临时目录
$upload_dir = wp_upload_dir();
$temp_dir = $upload_dir['basedir'] . '/vst_temp/';
if (!is_dir($temp_dir)) {
wp_mkdir_p($temp_dir);
}
// 生成唯一文件名
$file_name = sanitize_file_name($file['name']);
$unique_name = wp_unique_filename($temp_dir, $file_name);
$temp_path = $temp_dir . $unique_name;
// 移动文件
if (!move_uploaded_file($file['tmp_name'], $temp_path)) {
return new WP_Error('upload_failed', '文件上传失败');
}
// 获取视频信息
$video_info = self::get_video_info($temp_path);
return array(
'path' => $temp_path,
'url' => $upload_dir['baseurl'] . '/vst_temp/' . $unique_name,
'name' => $file_name,
'info' => $video_info
);
}
/**
* 验证文件
*/
private static function validate_file($file) {
$allowed_types = array('mp4', 'avi', 'mov', 'wmv', 'flv', 'webm');
$file_ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
// 检查文件类型
if (!in_array($file_ext, $allowed_types)) {
return false;
}
// 检查文件大小
$max_size = get_option('vst_max_file_size', VST_MAX_FILE_SIZE);
if ($file['size'] > $max_size) {
return false;
}
// 检查MIME类型
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime_type = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
$allowed_mimes = array(
'video/mp4',
'video/x-msvideo',
'video/quicktime',
'video/x-ms-wmv',
'video/x-flv',
'video/webm'
);
return in_array($mime_type, $allowed_mimes);
}
/**
* 获取视频信息
*/
private static function get_video_info($file_path) {
// 使用FFprobe获取视频信息(需要服务器安装FFmpeg)
if (function_exists('shell_exec')) {
$cmd = "ffprobe -v quiet -print_format json -show_format -show_streams " . escapeshellarg($file_path);
$output = shell_exec($cmd);
if ($output) {
$info = json_decode($output, true);
if ($info) {
return array(
'duration' => floatval($info['format']['duration']),
'width' => intval($info['streams'][0]['width']),
'height' => intval($info['streams'][0]['height']),
'bitrate' => intval($info['format']['bit_rate']),
'format' => $info['format']['format_name']
);
}
}
}
// 备用方案:使用PHP的getid3库
if (!class_exists('getID3')) {
require_once(ABSPATH . 'wp-admin/includes/media.php');
}
$id3 = new getID3();
$file_info = $id3->analyze($file_path);
return array(
'duration' => isset($file_info['playtime_seconds']) ? $file_info['playtime_seconds'] : 0,
'width' => isset($file_info['video']['resolution_x']) ? $file_info['video']['resolution_x'] : 0,
'height' => isset($file_info['video']['resolution_y']) ? $file_info['video']['resolution_y'] : 0,
'bitrate' => isset($file_info['bitrate']) ? $file_info['bitrate'] : 0,
'format' => isset($file_info['fileformat']) ? $file_info['fileformat'] : 'unknown'
);
}
/**
* 提取音频用于语音识别
*/
public static function extract_audio($video_path, $output_path = null) {
if (!$output_path) {
$upload_dir = wp_upload_dir();
$output_path = $upload_dir['basedir'] . '/vst_temp/audio_' . uniqid() . '.wav';
}
// 使用FFmpeg提取音频
$cmd = "ffmpeg -i " . escapeshellarg($video_path) .
" -ac 1 -ar 16000 -vn " . escapeshellarg($output_path) .
" 2>&1";
exec($cmd, $output, $return_code);
if ($return_code === 0 && file_exists($output_path)) {
return $output_path;
}
return false;
}
}
4.2 语音识别集成
创建includes/class-subtitle-generator.php:
<?php
class VST_Subtitle_Generator {
/**
* 使用Web Speech API进行语音识别(客户端方案)
*/
public static function get_browser_recognition_code() {
// 返回客户端JavaScript代码
return '
<script>
class SpeechRecognitionTool {
constructor() {
this.recognition = null;
this.isRecording = false;
this.transcript = "";
this.interimResults = [];
this.init();
}
init() {
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
if (!SpeechRecognition) {
throw new Error("浏览器不支持语音识别API");
}
this.recognition = new SpeechRecognition();
this.recognition.continuous = true;
this.recognition.interimResults = true;
this.recognition.lang = "zh-CN";
this.recognition.onresult = (event) => {
let interimTranscript = "";
let finalTranscript = "";
for (let i = event.resultIndex; i < event.results.length; i++) {
const transcript = event.results[i][0].transcript;
if (event.results[i].isFinal) {
finalTranscript += transcript;
} else {
interimTranscript += transcript;
}
}
this.interimResults = interimTranscript;
this.transcript += finalTranscript;
// 触发自定义事件
const transcriptEvent = new CustomEvent("transcriptUpdate", {
detail: {
final: finalTranscript,
interim: interimTranscript,
full: this.transcript
}
});
document.dispatchEvent(transcriptEvent);
};
this.recognition.onerror = (event) => {
console.error("语音识别错误:", event.error);
};
}
start() {
if (this.recognition && !this.isRecording) {
this.recognition.start();
this.isRecording = true;
}
}
stop() {
if (this.recognition && this.isRecording) {
this.recognition.stop();
this.isRecording = false;
}
}
第四部分:视频处理与语音识别模块(续)
4.2 语音识别集成(续)
setLanguage(lang) {
if (this.recognition) {
this.recognition.lang = lang;
}
}
getTranscript() {
return this.transcript;
}
reset() {
this.transcript = "";
this.interimResults = "";
}
}
// 导出到全局
window.VSTSpeechRecognition = SpeechRecognitionTool;
</script>
';
}
/**
* 使用第三方API进行语音识别(服务器端方案)
*/
public static function recognize_with_api($audio_path, $language = 'zh-CN') {
$api_type = get_option('vst_speech_api', 'google');
switch ($api_type) {
case 'google':
return self::google_speech_recognition($audio_path, $language);
case 'azure':
return self::azure_speech_recognition($audio_path, $language);
case 'baidu':
return self::baidu_speech_recognition($audio_path, $language);
default:
return new WP_Error('unsupported_api', '不支持的语音识别API');
}
}
/**
* Google Cloud Speech-to-Text
*/
private static function google_speech_recognition($audio_path, $language) {
$api_key = get_option('vst_google_api_key', '');
if (empty($api_key)) {
return new WP_Error('missing_api_key', '未配置Google API密钥');
}
// 读取音频文件
$audio_content = file_get_contents($audio_path);
$base64_audio = base64_encode($audio_content);
$url = "https://speech.googleapis.com/v1/speech:recognize?key=" . $api_key;
$data = array(
'config' => array(
'encoding' => 'LINEAR16',
'sampleRateHertz' => 16000,
'languageCode' => $language,
'enableWordTimeOffsets' => true,
'enableAutomaticPunctuation' => true
),
'audio' => array(
'content' => $base64_audio
)
);
$response = wp_remote_post($url, array(
'headers' => array('Content-Type' => 'application/json'),
'body' => json_encode($data),
'timeout' => 60
));
if (is_wp_error($response)) {
return $response;
}
$body = json_decode(wp_remote_retrieve_body($response), true);
if (isset($body['error'])) {
return new WP_Error('api_error', $body['error']['message']);
}
return self::format_recognition_results($body['results'], 'google');
}
/**
* 格式化识别结果
*/
private static function format_recognition_results($results, $provider) {
$subtitles = array();
$current_time = 0;
foreach ($results as $result) {
if (!isset($result['alternatives'][0])) {
continue;
}
$alternative = $result['alternatives'][0];
$text = $alternative['transcript'];
if ($provider === 'google' && isset($alternative['words'])) {
// 基于词语时间戳分割字幕
$words = $alternative['words'];
$chunks = self::split_into_subtitles($words);
foreach ($chunks as $chunk) {
$subtitles[] = array(
'start' => $chunk['start'],
'end' => $chunk['end'],
'text' => $chunk['text'],
'confidence' => $alternative['confidence'] ?? 0.8
);
}
} else {
// 简单分割:每5秒一句
$duration = 5; // 每句5秒
$sentences = self::split_text_into_sentences($text);
foreach ($sentences as $sentence) {
$subtitles[] = array(
'start' => $current_time,
'end' => $current_time + $duration,
'text' => $sentence,
'confidence' => 0.7
);
$current_time += $duration;
}
}
}
return $subtitles;
}
/**
* 将词语分割成字幕块
*/
private static function split_into_subtitles($words, $max_chars = 40) {
$chunks = array();
$current_chunk = array(
'text' => '',
'start' => null,
'end' => null,
'word_count' => 0
);
foreach ($words as $word) {
$word_text = $word['word'];
$word_start = floatval($word['startTime']['seconds']) +
(floatval($word['startTime']['nanos'] ?? 0) / 1000000000);
$word_end = floatval($word['endTime']['seconds']) +
(floatval($word['endTime']['nanos'] ?? 0) / 1000000000);
// 如果当前块为空,设置开始时间
if ($current_chunk['start'] === null) {
$current_chunk['start'] = $word_start;
}
// 检查是否应该开始新块
if (strlen($current_chunk['text'] . ' ' . $word_text) > $max_chars ||
$current_chunk['word_count'] >= 8) {
// 保存当前块
if (!empty($current_chunk['text'])) {
$current_chunk['end'] = $word_start; // 使用下一个词的开始时间作为结束
$chunks[] = $current_chunk;
}
// 开始新块
$current_chunk = array(
'text' => $word_text,
'start' => $word_start,
'end' => $word_end,
'word_count' => 1
);
} else {
// 添加到当前块
$current_chunk['text'] .= (empty($current_chunk['text']) ? '' : ' ') . $word_text;
$current_chunk['end'] = $word_end;
$current_chunk['word_count']++;
}
}
// 添加最后一个块
if (!empty($current_chunk['text'])) {
$chunks[] = $current_chunk;
}
return $chunks;
}
/**
* 将文本分割成句子
*/
private static function split_text_into_sentences($text) {
// 简单的句子分割逻辑
$sentences = preg_split('/(?<=[。!?.!?])/u', $text, -1, PREG_SPLIT_NO_EMPTY);
return array_filter(array_map('trim', $sentences));
}
}
第五部分:前端编辑器开发
5.1 编辑器HTML结构
创建templates/subtitle-editor.php:
<div id="vst-editor-app" class="vst-editor-container">
<!-- 顶部工具栏 -->
<div class="vst-toolbar">
<div class="vst-toolbar-left">
<button class="vst-btn vst-btn-primary" @click="uploadVideo">
<i class="vst-icon-upload"></i> 上传视频
</button>
<button class="vst-btn" :class="{'vst-btn-active': isRecording}" @click="toggleRecording">
<i class="vst-icon-mic"></i> {{ isRecording ? '停止识别' : '开始识别' }}
</button>
<select v-model="selectedLanguage" class="vst-select">
<option value="zh-CN">中文(简体)</option>
<option value="zh-TW">中文(繁体)</option>
<option value="en-US">English</option>
<option value="ja-JP">日本語</option>
<option value="ko-KR">한국어</option>
</select>
</div>
<div class="vst-toolbar-right">
<button class="vst-btn" @click="exportSubtitles('srt')">
<i class="vst-icon-download"></i> 导出SRT
</button>
<button class="vst-btn" @click="exportSubtitles('vtt')">
<i class="vst-icon-download"></i> 导出VTT
</button>
<button class="vst-btn vst-btn-success" @click="saveProject">
<i class="vst-icon-save"></i> 保存项目
</button>
</div>
</div>
<!-- 主编辑区 -->
<div class="vst-main-editor">
<!-- 视频播放器 -->
<div class="vst-video-container">
<video
ref="videoPlayer"
:src="videoUrl"
controls
@timeupdate="updateCurrentTime"
@loadedmetadata="onVideoLoaded"
></video>
<div class="vst-video-overlay" v-if="subtitles.length > 0">
<div class="vst-subtitle-display" :style="subtitleStyle">
{{ currentSubtitleText }}
</div>
</div>
</div>
<!-- 字幕编辑区 -->
<div class="vst-subtitle-editor">
<div class="vst-subtitle-header">
<h3>字幕编辑</h3>
<div class="vst-subtitle-actions">
<button class="vst-btn vst-btn-small" @click="addSubtitle">
<i class="vst-icon-add"></i> 添加字幕
</button>
<button class="vst-btn vst-btn-small" @click="autoSync">
<i class="vst-icon-sync"></i> 自动同步
</button>
</div>
</div>
<!-- 字幕列表 -->
<div class="vst-subtitle-list">
<div
v-for="(subtitle, index) in subtitles"
:key="index"
class="vst-subtitle-item"
:class="{'vst-subtitle-active': currentSubtitleIndex === index}"
@click="selectSubtitle(index)"
>
<div class="vst-subtitle-index">{{ index + 1 }}</div>
<div class="vst-subtitle-time">
<input
type="number"
v-model="subtitle.start"
step="0.1"
@change="updateSubtitleTime(index, 'start', $event)"
>
<span> → </span>
<input
type="number"
v-model="subtitle.end"
step="0.1"
@change="updateSubtitleTime(index, 'end', $event)"
>
</div>
<div class="vst-subtitle-text">
<textarea
v-model="subtitle.text"
@input="updateSubtitleText(index, $event)"
rows="2"
></textarea>
</div>
<div class="vst-subtitle-actions">
<button class="vst-btn-icon" @click="playSegment(index)">
<i class="vst-icon-play"></i>
</button>
<button class="vst-btn-icon" @click="removeSubtitle(index)">
<i class="vst-icon-delete"></i>
</button>
</div>
</div>
</div>
<!-- 时间轴编辑器 -->
<div class="vst-timeline-editor">
<div class="vst-timeline-header">
<span>时间轴</span>
<span class="vst-current-time">{{ formatTime(currentTime) }}</span>
</div>
<div class="vst-timeline-container" ref="timeline">
<div
v-for="(subtitle, index) in subtitles"
:key="'timeline-' + index"
class="vst-timeline-segment"
:style="{
left: (subtitle.start / videoDuration * 100) + '%',
width: ((subtitle.end - subtitle.start) / videoDuration * 100) + '%'
}"
@mousedown="startDrag(index, $event)"
>
<div class="vst-timeline-label">{{ index + 1 }}</div>
</div>
<div
class="vst-timeline-cursor"
:style="{ left: (currentTime / videoDuration * 100) + '%' }"
></div>
</div>
</div>
</div>
</div>
<!-- 设置面板 -->
<div class="vst-settings-panel" v-if="showSettings">
<div class="vst-settings-header">
<h3>字幕设置</h3>
<button class="vst-btn-icon" @click="showSettings = false">
<i class="vst-icon-close"></i>
</button>
</div>
<div class="vst-settings-content">
<div class="vst-setting-item">
<label>字体</label>
<select v-model="settings.fontFamily">
<option value="Arial">Arial</option>
<option value="Microsoft YaHei">微软雅黑</option>
<option value="SimSun">宋体</option>
<option value="SimHei">黑体</option>
</select>
</div>
<div class="vst-setting-item">
<label>字体大小</label>
<input type="range" v-model="settings.fontSize" min="12" max="48">
<span>{{ settings.fontSize }}px</span>
</div>
<div class="vst-setting-item">
<label>字体颜色</label>
<input type="color" v-model="settings.fontColor">
</div>
<div class="vst-setting-item">
<label>背景颜色</label>
<input type="color" v-model="settings.backgroundColor">
<input type="range" v-model="settings.backgroundOpacity" min="0" max="100">
<span>{{ settings.backgroundOpacity }}%</span>
</div>
<div class="vst-setting-item">
<label>位置</label>
<select v-model="settings.position">
<option value="bottom">底部</option>
<option value="top">顶部</option>
<option value="middle">中间</option>
</select>
</div>
</div>
</div>
<!-- 上传模态框 -->
<div class="vst-modal" v-if="showUploadModal">
<div class="vst-modal-content">
<div class="vst-modal-header">
<h3>上传视频</h3>
<button class="vst-btn-icon" @click="showUploadModal = false">
<i class="vst-icon-close"></i>
</button>
</div>
<div class="vst-modal-body">
<div class="vst-upload-area"
@dragover.prevent="onDragOver"
@dragleave.prevent="onDragLeave"
@drop.prevent="onDrop"
:class="{'vst-upload-dragover': isDragOver}"
>
<i class="vst-icon-upload-large"></i>
<p>拖放视频文件到这里,或</p>
<input
type="file"
ref="fileInput"
accept="video/*"
@change="onFileSelected"
hidden
>
<button class="vst-btn" @click="$refs.fileInput.click()">
选择文件
</button>
<p class="vst-upload-hint">支持 MP4, AVI, MOV, WMV, FLV, WebM 格式,最大500MB</p>
</div>
<div v-if="uploadProgress > 0" class="vst-upload-progress">
<div class="vst-progress-bar">
<div class="vst-progress-fill" :style="{ width: uploadProgress + '%' }"></div>
</div>
<span>{{ uploadProgress }}%</span>
</div>
</div>
</div>
</div>
</div>
5.2 编辑器Vue.js应用
创建public/js/app.js:
// Vue.js应用主文件
document.addEventListener('DOMContentLoaded', function() {
// 检查Vue是否已加载
if (typeof Vue === 'undefined') {
console.error('Vue.js未加载,请确保已引入Vue库');
return;
}
// 创建Vue应用
const app = Vue.createApp({
data() {
return {
// 视频相关
videoUrl: '',
videoDuration: 0,
currentTime: 0,
videoFile: null,
// 字幕数据
subtitles: [],
currentSubtitleIndex: -1,
currentSubtitleText: '',
// 识别状态
isRecording: false,
selectedLanguage: 'zh-CN',
recognitionTool: null,
// 界面状态
showSettings: false,
showUploadModal: false,
isDragOver: false,
uploadProgress: 0,
// 设置
settings: {
fontFamily: 'Microsoft YaHei',
fontSize: 24,
fontColor: '#FFFFFF',
backgroundColor: '#000000',
backgroundOpacity: 50,
position: 'bottom'
},
// 项目信息
projectId: null,
projectTitle: '未命名项目',
isSaved: false
};
},
computed: {
// 计算字幕显示样式
subtitleStyle() {
const bgColor = this.hexToRgba(
this.settings.backgroundColor,
this.settings.backgroundOpacity / 100
);
