文章目录[隐藏]
详细教程:为网站打造内嵌的在线视频剪辑与简易制作工具,通过WordPress程序的代码二次开发实现常用互联网小工具功能
引言:为什么网站需要内嵌视频编辑工具?
在当今数字内容为王的时代,视频已成为最受欢迎的内容形式之一。据统计,全球互联网用户每天观看的视频时长超过10亿小时,而超过85%的企业使用视频作为营销工具。然而,对于许多网站运营者来说,视频制作一直是个门槛——用户需要离开网站,使用专业软件编辑视频,再上传回网站,这一流程既繁琐又影响用户体验。
想象一下,如果您的WordPress网站能够直接提供在线视频剪辑功能,让用户无需离开页面即可完成视频裁剪、合并、添加字幕和特效等操作,这将是多么强大的竞争优势!无论是教育平台让学生编辑课程录像,电商网站让卖家制作产品展示视频,还是社交平台让用户创作内容,内嵌视频工具都能显著提升用户参与度和内容产出效率。
本教程将详细指导您如何通过WordPress代码二次开发,为您的网站打造一个功能完整的在线视频剪辑与制作工具。我们将从基础原理讲起,逐步实现核心功能,最终整合成一个可直接使用的解决方案。
第一部分:准备工作与环境搭建
1.1 理解WordPress插件开发基础
在开始之前,我们需要了解WordPress插件的基本结构。WordPress插件是独立的代码模块,可以扩展WordPress的功能而不修改核心代码。一个基本的插件至少包含:
- 主PHP文件(包含插件头信息)
- 必要的JavaScript和CSS文件
- 可选的资源文件(如图像、字体等)
插件头信息示例:
<?php
/**
* Plugin Name: 在线视频剪辑工具
* Plugin URI: https://yourwebsite.com/video-editor
* Description: 为WordPress网站添加在线视频剪辑功能
* Version: 1.0.0
* Author: 您的名字
* License: GPL v2 or later
*/
1.2 开发环境配置
- 本地开发环境:建议使用Local by Flywheel、XAMPP或MAMP搭建本地WordPress环境
- 代码编辑器:推荐VS Code、PHPStorm或Sublime Text
- 浏览器开发者工具:Chrome或Firefox的开发者工具是调试前端代码的必备
- 版本控制:初始化Git仓库管理代码版本
1.3 关键技术栈选择
为了实现在线视频编辑,我们需要选择合适的技术方案:
- 前端视频处理:FFmpeg.js或WebAssembly版本的FFmpeg
- 前端框架:React或Vue.js(本教程使用纯JavaScript以降低复杂度)
- 视频播放器:Video.js或plyr.js
- UI组件:自定义CSS或使用轻量级UI库
- 后端处理:PHP + WordPress REST API
1.4 创建插件基本结构
在wp-content/plugins目录下创建"video-editor-tool"文件夹,并建立以下结构:
video-editor-tool/
├── video-editor.php # 主插件文件
├── includes/ # PHP类文件
│ ├── class-video-processor.php
│ ├── class-video-library.php
│ └── class-ajax-handler.php
├── admin/ # 后台相关文件
│ ├── css/
│ ├── js/
│ └── admin-page.php
├── public/ # 前端相关文件
│ ├── css/
│ ├── js/
│ ├── libs/ # 第三方库
│ └── templates/ # 前端模板
├── assets/ # 静态资源
│ ├── images/
│ └── fonts/
└── vendor/ # 第三方PHP库
第二部分:核心视频处理功能实现
2.1 集成FFmpeg.js进行客户端视频处理
FFmpeg.js是FFmpeg的JavaScript端口,允许在浏览器中直接处理视频文件。由于视频处理是计算密集型任务,我们将在客户端进行基本操作以减少服务器压力。
步骤1:引入FFmpeg.js
<!-- 在编辑器页面添加 -->
<script src="<?php echo plugin_dir_url(__FILE__); ?>public/libs/ffmpeg/ffmpeg.min.js"></script>
步骤2:创建视频处理管理器
// public/js/video-processor.js
class VideoProcessor {
constructor() {
this.ffmpeg = null;
this.videoElements = [];
this.isFFmpegLoaded = false;
this.initFFmpeg();
}
async initFFmpeg() {
try {
// 加载FFmpeg
const { createFFmpeg, fetchFile } = FFmpeg;
this.ffmpeg = createFFmpeg({ log: true });
await this.ffmpeg.load();
this.isFFmpegLoaded = true;
console.log('FFmpeg加载成功');
} catch (error) {
console.error('FFmpeg加载失败:', error);
}
}
// 裁剪视频
async trimVideo(inputFile, startTime, endTime) {
if (!this.isFFmpegLoaded) {
throw new Error('FFmpeg未加载完成');
}
// 将视频文件写入FFmpeg文件系统
this.ffmpeg.FS('writeFile', 'input.mp4', await fetchFile(inputFile));
// 执行裁剪命令
await this.ffmpeg.run(
'-i', 'input.mp4',
'-ss', startTime.toString(),
'-to', endTime.toString(),
'-c', 'copy',
'output.mp4'
);
// 读取输出文件
const data = this.ffmpeg.FS('readFile', 'output.mp4');
return new Blob([data.buffer], { type: 'video/mp4' });
}
// 合并多个视频
async mergeVideos(videoFiles) {
// 创建文件列表
const fileList = videoFiles.map((file, index) => {
const filename = `input${index}.mp4`;
this.ffmpeg.FS('writeFile', filename, await fetchFile(file));
return `file '${filename}'`;
}).join('n');
// 写入文件列表到FFmpeg文件系统
this.ffmpeg.FS('writeFile', 'filelist.txt', fileList);
// 执行合并命令
await this.ffmpeg.run(
'-f', 'concat',
'-safe', '0',
'-i', 'filelist.txt',
'-c', 'copy',
'output.mp4'
);
const data = this.ffmpeg.FS('readFile', 'output.mp4');
return new Blob([data.buffer], { type: 'video/mp4' });
}
// 提取音频
async extractAudio(videoFile) {
this.ffmpeg.FS('writeFile', 'input.mp4', await fetchFile(videoFile));
await this.ffmpeg.run(
'-i', 'input.mp4',
'-vn',
'-acodec', 'libmp3lame',
'output.mp3'
);
const data = this.ffmpeg.FS('readFile', 'output.mp3');
return new Blob([data.buffer], { type: 'audio/mpeg' });
}
// 添加水印
async addWatermark(videoFile, watermarkImage, position = 'bottom-right') {
// 实现水印添加逻辑
// 注意:这需要更复杂的FFmpeg命令
}
}
2.2 视频时间轴与预览组件
时间轴是视频编辑器的核心组件,允许用户可视化地操作视频。
// public/js/timeline-editor.js
class TimelineEditor {
constructor(containerId, options = {}) {
this.container = document.getElementById(containerId);
this.videoElement = null;
this.timelineCanvas = null;
this.duration = 0;
this.currentTime = 0;
this.videoClips = [];
this.isDragging = false;
this.init(options);
}
init(options) {
// 创建时间轴HTML结构
this.container.innerHTML = `
<div class="video-preview-container">
<video id="preview-video" controls></video>
</div>
<div class="timeline-container">
<canvas id="timeline-canvas"></canvas>
<div class="timeline-controls">
<div class="playhead" id="playhead"></div>
<div class="trim-handle left-handle" id="trim-left"></div>
<div class="trim-handle right-handle" id="trim-right"></div>
</div>
</div>
<div class="timeline-toolbar">
<button class="btn-cut" title="剪切">✂️</button>
<button class="btn-split" title="分割">🔪</button>
<button class="btn-delete" title="删除">🗑️</button>
<button class="btn-add-text" title="添加文字">T</button>
<button class="btn-add-transition" title="添加转场">✨</button>
</div>
`;
this.videoElement = document.getElementById('preview-video');
this.timelineCanvas = document.getElementById('timeline-canvas');
this.playhead = document.getElementById('playhead');
this.setupEventListeners();
this.setupCanvas();
}
setupCanvas() {
const ctx = this.timelineCanvas.getContext('2d');
const width = this.container.offsetWidth;
const height = 120;
this.timelineCanvas.width = width;
this.timelineCanvas.height = height;
// 绘制时间轴背景
this.drawTimeline(ctx, width, height);
}
drawTimeline(ctx, width, height) {
// 绘制背景
ctx.fillStyle = '#2d2d2d';
ctx.fillRect(0, 0, width, height);
// 绘制时间刻度
const seconds = this.duration;
const pixelsPerSecond = width / seconds;
ctx.strokeStyle = '#555';
ctx.lineWidth = 1;
ctx.fillStyle = '#888';
ctx.font = '10px Arial';
for (let i = 0; i <= seconds; i++) {
const x = i * pixelsPerSecond;
// 主刻度(每秒)
ctx.beginPath();
ctx.moveTo(x, height - 20);
ctx.lineTo(x, height);
ctx.stroke();
// 时间标签
if (i % 5 === 0) {
const timeText = this.formatTime(i);
ctx.fillText(timeText, x - 10, height - 25);
}
// 次刻度(每0.5秒)
if (i < seconds) {
const midX = x + pixelsPerSecond / 2;
ctx.beginPath();
ctx.moveTo(midX, height - 15);
ctx.lineTo(midX, height);
ctx.stroke();
}
}
// 绘制视频片段
this.videoClips.forEach((clip, index) => {
this.drawVideoClip(ctx, clip, index, pixelsPerSecond, height);
});
}
drawVideoClip(ctx, clip, index, pixelsPerSecond, height) {
const startX = clip.startTime * pixelsPerSecond;
const clipWidth = (clip.endTime - clip.startTime) * pixelsPerSecond;
// 绘制片段背景
ctx.fillStyle = index % 2 === 0 ? '#4a9eff' : '#6bb7ff';
ctx.fillRect(startX, 10, clipWidth, height - 40);
// 绘制片段边框
ctx.strokeStyle = '#2a7fff';
ctx.lineWidth = 2;
ctx.strokeRect(startX, 10, clipWidth, height - 40);
// 绘制片段标签
ctx.fillStyle = 'white';
ctx.font = '12px Arial';
ctx.fillText(`片段 ${index + 1}`, startX + 5, 30);
// 绘制时长
const durationText = this.formatTime(clip.endTime - clip.startTime);
ctx.fillText(durationText, startX + 5, 50);
}
formatTime(seconds) {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}
setupEventListeners() {
// 视频时间更新事件
this.videoElement.addEventListener('timeupdate', () => {
this.updatePlayheadPosition();
});
// 时间轴点击事件
this.timelineCanvas.addEventListener('click', (e) => {
const rect = this.timelineCanvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const time = (x / this.timelineCanvas.width) * this.duration;
this.videoElement.currentTime = time;
this.updatePlayheadPosition();
});
// 拖拽事件
this.setupDragEvents();
}
updatePlayheadPosition() {
if (!this.duration) return;
const percentage = (this.videoElement.currentTime / this.duration) * 100;
this.playhead.style.left = `${percentage}%`;
}
setupDragEvents() {
// 实现拖拽逻辑
// 包括播放头拖拽、片段拖拽、裁剪手柄拖拽等
}
loadVideo(videoFile) {
return new Promise((resolve, reject) => {
const url = URL.createObjectURL(videoFile);
this.videoElement.src = url;
this.videoElement.onloadedmetadata = () => {
this.duration = this.videoElement.duration;
this.videoClips = [{
startTime: 0,
endTime: this.duration,
file: videoFile
}];
this.setupCanvas();
resolve();
};
this.videoElement.onerror = reject;
});
}
}
2.3 文字与特效添加功能
// public/js/text-effects-editor.js
class TextEffectsEditor {
constructor() {
this.textTracks = [];
this.effects = [];
this.currentText = null;
}
// 添加文字轨道
addTextTrack(text, options = {}) {
const textTrack = {
id: Date.now(),
text: text,
startTime: options.startTime || 0,
duration: options.duration || 5,
style: {
fontSize: options.fontSize || '24px',
fontFamily: options.fontFamily || 'Arial',
color: options.color || '#ffffff',
backgroundColor: options.backgroundColor || 'rgba(0,0,0,0.5)',
position: options.position || 'bottom-center',
animation: options.animation || 'fade'
}
};
this.textTracks.push(textTrack);
return textTrack;
}
// 渲染文字到视频
renderTextToVideo(videoElement, textTrack) {
const overlay = document.createElement('div');
overlay.className = 'text-overlay';
Object.assign(overlay.style, {
position: 'absolute',
color: textTrack.style.color,
fontSize: textTrack.style.fontSize,
fontFamily: textTrack.style.fontFamily,
backgroundColor: textTrack.style.backgroundColor,
padding: '10px',
borderRadius: '5px',
...this.getPositionStyle(textTrack.style.position)
});
overlay.textContent = textTrack.text;
// 添加动画类
overlay.classList.add(`text-animation-${textTrack.style.animation}`);
// 添加到视频容器
const videoContainer = videoElement.parentElement;
videoContainer.style.position = 'relative';
videoContainer.appendChild(overlay);
// 设置显示时间
setTimeout(() => {
overlay.style.display = 'block';
}, textTrack.startTime * 1000);
setTimeout(() => {
overlay.style.display = 'none';
overlay.remove();
}, (textTrack.startTime + textTrack.duration) * 1000);
}
getPositionStyle(position) {
const positions = {
'top-left': { top: '10px', left: '10px' },
'top-center': { top: '10px', left: '50%', transform: 'translateX(-50%)' },
'top-right': { top: '10px', right: '10px' },
'middle-left': { top: '50%', left: '10px', transform: 'translateY(-50%)' },
'middle-center': { top: '50%', left: '50%', transform: 'translate(-50%, -50%)' },
'middle-right': { top: '50%', right: '10px', transform: 'translateY(-50%)' },
'bottom-left': { bottom: '10px', left: '10px' },
'bottom-center': { bottom: '10px', left: '50%', transform: 'translateX(-50%)' },
'bottom-right': { bottom: '10px', right: '10px' }
};
return positions[position] || positions['bottom-center'];
}
// 添加转场效果
addTransitionEffect(videoClips, transitionType = 'fade') {
const transitions = {
'fade': { duration: 1, type: 'fade' },
'slide': { duration: 1, type: 'slide', direction: 'right' },
'zoom': { duration: 1, type: 'zoom' },
'rotate': { duration: 1, type: 'rotate' }
};
this.effects.push({
type: 'transition',
transition: transitions[transitionType],
position: videoClips.length > 0 ? videoClips.length - 1 : 0
});
}
}
第三部分:WordPress后端集成
3.1 创建视频处理API端点
<?php
// includes/class-rest-api.php
class Video_Editor_REST_API {
private $namespace = 'video-editor/v1';
public function __construct() {
add_action('rest_api_init', [$this, 'register_routes']);
}
public function register_routes() {
// 上传视频端点
register_rest_route($this->namespace, '/upload', [
'methods' => 'POST',
'callback' => [$this, 'handle_video_upload'],
'permission_callback' => [$this, 'check_permission'],
'args' => [
'file' => [
'required' => true,
'validate_callback' => function($file) {
return !empty($file);
}
],
'title' => [
'required' => false,
'sanitize_callback' => 'sanitize_text_field'
]
]
]);
// 保存项目端点
register_rest_route($this->namespace, '/project/save', [
'methods' => 'POST',
'callback' => [$this, 'save_project'],
'permission_callback' => [$this, 'check_permission']
]);
// 获取项目列表
register_rest_route($this->namespace, '/projects', [
'methods' => 'GET',
'callback' => [$this, 'get_projects'],
'permission_callback' => [$this, 'check_permission']
]);
// 服务器端视频处理(用于复杂操作)
register_rest_route($this->namespace, '/process', [
'methods' => 'POST',
'callback' => [$this, 'process_video'],
'permission_callback' => [$this, 'check_permission']
]);
}
public function handle_video_upload($request) {
$files = $request->get_file_params();
if (empty($files['video_file'])) {
return new WP_Error('no_file', '没有上传文件', ['status' => 400]);
}
$file = $files['video_file'];
// 检查文件类型
$allowed_types = ['video/mp4', 'video/webm', 'video/ogg', 'video/quicktime'];
if (!in_array($file['type'], $allowed_types)) {
return new WP_Error('invalid_type', '不支持的文件格式', ['status' => 400]);
}
// 检查文件大小(限制为500MB)
$max_size = 500 * 1024 * 1024;
if ($file['size'] > $max_size) {
return new WP_Error('file_too_large', '文件太大,最大支持500MB', ['status' => 400]);
}
// 创建上传目录
$upload_dir = wp_upload_dir();
$video_dir = $upload_dir['basedir'] . '/video-editor';
if (!file_exists($video_dir)) {
wp_mkdir_p($video_dir);
}
// 生成唯一文件名
$filename = wp_unique_filename($video_dir, $file['name']);
$filepath = $video_dir . '/' . $filename;
// 移动文件
if (move_uploaded_file($file['tmp_name'], $filepath)) {
// 保存到媒体库
$attachment = [
'post_mime_type' => $file['type'],
'post_title' => $request->get_param('title') ?: preg_replace('/.[^.]+$/', '', $file['name']),
'post_content' => '',
'post_status' => 'inherit',
'guid' => $upload_dir['baseurl'] . '/video-editor/' . $filename
];
$attach_id = wp_insert_attachment($attachment, $filepath);
// 生成视频元数据
require_once(ABSPATH . 'wp-admin/includes/image.php');
$attach_data = wp_generate_attachment_metadata($attach_id, $filepath);
wp_update_attachment_metadata($attach_id, $attach_data);
// 获取视频信息
$video_info = $this->get_video_info($filepath);
return [
'success' => true,
'id' => $attach_id,
'url' => wp_get_attachment_url($attach_id),
'thumbnail' => $this->generate_thumbnail($attach_id, $filepath),
'duration' => $video_info['duration'] ?? 0,
'dimensions' => $video_info['dimensions'] ?? ['width' => 0, 'height' => 0]
];
}
return new WP_Error('upload_failed', '文件上传失败', ['status' => 500]);
}
private function get_video_info($filepath) {
if (!function_exists('exec')) {
return ['duration' => 0, 'dimensions' => ['width' => 0, 'height' => 0]];
}
// 使用FFmpeg获取视频信息
$cmd = "ffprobe -v error -show_entries format=duration -show_entries stream=width,height -of json " . escapeshellarg($filepath);
@exec($cmd, $output, $return_var);
if ($return_var === 0 && !empty($output)) {
$data = json_decode(implode('', $output), true);
return [
'duration' => $data['format']['duration'] ?? 0,
'dimensions' => [
'width' => $data['streams'][0]['width'] ?? 0,
'height' => $data['streams'][0]['height'] ?? 0
]
];
}
return ['duration' => 0, 'dimensions' => ['width' => 0, 'height' => 0]];
}
private function generate_thumbnail($attach_id, $filepath) {
$upload_dir = wp_upload_dir();
$thumb_dir = $upload_dir['basedir'] . '/video-editor/thumbs';
if (!file_exists($thumb_dir)) {
wp_mkdir_p($thumb_dir);
}
$thumb_name = 'thumb-' . $attach_id . '.jpg';
$thumb_path = $thumb_dir . '/' . $thumb_name;
// 使用FFmpeg生成缩略图
$cmd = "ffmpeg -i " . escapeshellarg($filepath) . " -ss 00:00:01 -vframes 1 -q:v 2 " . escapeshellarg($thumb_path);
@exec($cmd, $output, $return_var);
if ($return_var === 0 && file_exists($thumb_path)) {
return $upload_dir['baseurl'] . '/video-editor/thumbs/' . $thumb_name;
}
// 如果FFmpeg失败,使用默认缩略图
return plugin_dir_url(__FILE__) . '../assets/images/default-thumb.jpg';
}
public function save_project($request) {
$user_id = get_current_user_id();
$project_data = $request->get_json_params();
if (!$user_id) {
return new WP_Error('unauthorized', '用户未登录', ['status' => 401]);
}
$project_id = wp_insert_post([
'post_title' => $project_data['title'] ?: '未命名项目',
'post_content' => wp_json_encode($project_data['content']),
'post_status' => 'draft',
'post_type' => 'video_project',
'post_author' => $user_id,
'meta_input' => [
'_video_project_data' => $project_data['timeline'],
'_video_duration' => $project_data['duration'],
'_video_thumbnail' => $project_data['thumbnail']
]
]);
if (is_wp_error($project_id)) {
return $project_id;
}
return [
'success' => true,
'project_id' => $project_id,
'message' => '项目保存成功'
];
}
public function get_projects($request) {
$user_id = get_current_user_id();
if (!$user_id) {
return new WP_Error('unauthorized', '用户未登录', ['status' => 401]);
}
$args = [
'post_type' => 'video_project',
'author' => $user_id,
'posts_per_page' => 20,
'post_status' => ['draft', 'publish']
];
$projects = get_posts($args);
$formatted_projects = [];
foreach ($projects as $project) {
$formatted_projects[] = [
'id' => $project->ID,
'title' => $project->post_title,
'created' => $project->post_date,
'modified' => $project->post_modified,
'thumbnail' => get_post_meta($project->ID, '_video_thumbnail', true),
'duration' => get_post_meta($project->ID, '_video_duration', true)
];
}
return $formatted_projects;
}
public function process_video($request) {
// 服务器端复杂视频处理
$data = $request->get_json_params();
$operation = $data['operation'] ?? '';
switch ($operation) {
case 'concat':
return $this->concatenate_videos($data['videos']);
case 'compress':
return $this->compress_video($data['video'], $data['options']);
case 'add_watermark':
return $this->add_watermark_server($data['video'], $data['watermark']);
default:
return new WP_Error('invalid_operation', '不支持的操作', ['status' => 400]);
}
}
private function concatenate_videos($video_urls) {
// 实现服务器端视频合并
// 注意:这需要服务器安装FFmpeg
}
public function check_permission($request) {
// 检查用户权限
return current_user_can('edit_posts') ||
apply_filters('video_editor_allow_anonymous', false);
}
}
3.2 创建自定义文章类型存储视频项目
<?php
// includes/class-custom-post-type.php
class Video_Project_Post_Type {
public function __construct() {
add_action('init', [$this, 'register_post_type']);
add_action('add_meta_boxes', [$this, 'add_meta_boxes']);
add_action('save_post_video_project', [$this, 'save_meta_data']);
}
public function register_post_type() {
$labels = [
'name' => '视频项目',
'singular_name' => '视频项目',
'menu_name' => '视频项目',
'add_new' => '新建项目',
'add_new_item' => '新建视频项目',
'edit_item' => '编辑项目',
'new_item' => '新项目',
'view_item' => '查看项目',
'search_items' => '搜索项目',
'not_found' => '未找到项目',
'not_found_in_trash' => '回收站中无项目'
];
$args = [
'labels' => $labels,
'public' => true,
'publicly_queryable' => true,
'show_ui' => true,
'show_in_menu' => true,
'query_var' => true,
'rewrite' => ['slug' => 'video-project'],
'capability_type' => 'post',
'has_archive' => true,
'hierarchical' => false,
'menu_position' => 20,
'menu_icon' => 'dashicons-video-alt3',
'supports' => ['title', 'editor', 'author', 'thumbnail'],
'show_in_rest' => true
];
register_post_type('video_project', $args);
}
public function add_meta_boxes() {
add_meta_box(
'video_project_meta',
'项目详情',
[$this, 'render_meta_box'],
'video_project',
'normal',
'high'
);
add_meta_box(
'video_project_preview',
'视频预览',
[$this, 'render_preview_box'],
'video_project',
'side',
'high'
);
}
public function render_meta_box($post) {
wp_nonce_field('video_project_meta', 'video_project_nonce');
$project_data = get_post_meta($post->ID, '_video_project_data', true);
$duration = get_post_meta($post->ID, '_video_duration', true);
$video_url = get_post_meta($post->ID, '_video_final_url', true);
?>
<div class="video-project-meta">
<p>
<label for="video_duration">视频时长:</label>
<input type="text" id="video_duration" name="video_duration"
value="<?php echo esc_attr($duration); ?>" readonly>
<span>秒</span>
</p>
<p>
<label for="video_url">最终视频URL:</label>
<input type="url" id="video_url" name="video_url"
value="<?php echo esc_url($video_url); ?>" class="widefat">
</p>
<p>
<label for="project_status">项目状态:</label>
<select id="project_status" name="project_status">
<option value="draft" <?php selected(get_post_status($post->ID), 'draft'); ?>>草稿</option>
<option value="publish" <?php selected(get_post_status($post->ID), 'publish'); ?>>发布</option>
<option value="processing" <?php selected(get_post_meta($post->ID, '_project_status', true), 'processing'); ?>>处理中</option>
<option value="completed" <?php selected(get_post_meta($post->ID, '_project_status', true), 'completed'); ?>>已完成</option>
</select>
</p>
<div class="project-data">
<h4>项目数据(JSON格式)</h4>
<textarea name="project_data" rows="10" class="widefat"
readonly><?php echo esc_textarea($project_data); ?></textarea>
</div>
</div>
<style>
.video-project-meta p {
margin: 15px 0;
}
.video-project-meta label {
display: inline-block;
width: 120px;
font-weight: bold;
}
.project-data textarea {
font-family: monospace;
font-size: 12px;
}
</style>
<?php
}
public function render_preview_box($post) {
$thumbnail = get_post_meta($post->ID, '_video_thumbnail', true);
$video_url = get_post_meta($post->ID, '_video_final_url', true);
?>
<div class="video-preview-sidebar">
<?php if ($thumbnail): ?>
<div class="video-thumbnail">
<img src="<?php echo esc_url($thumbnail); ?>"
alt="视频缩略图" style="max-width:100%; height:auto;">
</div>
<?php endif; ?>
<?php if ($video_url): ?>
<div class="video-preview">
<video controls style="width:100%; max-height:200px;">
<source src="<?php echo esc_url($video_url); ?>" type="video/mp4">
您的浏览器不支持视频播放
</video>
</div>
<p style="text-align:center; margin-top:10px;">
<a href="<?php echo esc_url($video_url); ?>"
class="button button-primary" target="_blank">
<span class="dashicons dashicons-external"></span>
查看完整视频
</a>
</p>
<?php else: ?>
<p class="description">视频尚未生成</p>
<button type="button" id="generate_video" class="button button-secondary">
<span class="dashicons dashicons-video-alt3"></span>
生成最终视频
</button>
<?php endif; ?>
</div>
<script>
jQuery(document).ready(function($) {
$('#generate_video').on('click', function() {
var button = $(this);
var postId = <?php echo $post->ID; ?>;
button.prop('disabled', true).text('生成中...');
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'generate_final_video',
post_id: postId,
nonce: '<?php echo wp_create_nonce('generate_video_' . $post->ID); ?>'
},
success: function(response) {
if (response.success) {
alert('视频生成成功!');
location.reload();
} else {
alert('生成失败:' + response.data);
button.prop('disabled', false).text('生成最终视频');
}
},
error: function() {
alert('请求失败,请重试');
button.prop('disabled', false).text('生成最终视频');
}
});
});
});
</script>
<?php
}
public function save_meta_data($post_id) {
// 验证nonce
if (!isset($_POST['video_project_nonce']) ||
!wp_verify_nonce($_POST['video_project_nonce'], 'video_project_meta')) {
return;
}
// 检查自动保存
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
return;
}
// 检查权限
if (!current_user_can('edit_post', $post_id)) {
return;
}
// 保存元数据
