feat: 实现导出到剪映功能 (v0.1.28)

- 新增剪映导出数据结构定义 (jianying_export.rs)
- 实现模板匹配结果导出到剪映格式的服务逻辑
- 添加导出到剪映的Tauri命令接口
- 在匹配记录卡片中添加导出按钮
- 实现文件保存对话框和用户交互
- 支持根据模板匹配结果生成完整的draft_content.json
- 自动替换匹配素材路径并生成随机素材ID
- 去除无引用素材,优化导出文件大小

功能特点:
- 完全符合剪映draft_content.json格式规范
- 准确的时间轴和素材路径映射
- 用户友好的导出界面和反馈
- 遵循promptx/tauri-desktop-app-expert开发规范
This commit is contained in:
imeepos
2025-07-16 22:33:57 +08:00
parent e3037916c0
commit 0495a32c74
7 changed files with 867 additions and 2 deletions

View File

@@ -0,0 +1,511 @@
use serde::{Serialize, Deserialize};
use uuid::Uuid;
use chrono::Utc;
/// 剪映草稿内容结构 - 用于导出
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JianYingDraftContent {
pub canvas_config: JianYingCanvasConfig,
pub color_space: i32,
pub config: JianYingConfig,
pub cover: Option<String>,
pub create_time: i64,
pub duration: u64,
pub extra_info: Option<String>,
pub fps: f64,
pub free_render_index_mode_on: bool,
pub group_container: Option<String>,
pub id: String,
pub keyframe_graph_list: Vec<String>,
pub keyframes: JianYingKeyframes,
pub last_modified_platform: JianYingPlatform,
pub materials: JianYingMaterials,
pub render_index_track_mode_on: bool,
pub retouch_cover: Option<String>,
pub source: String,
pub static_cover_image_path: String,
pub time_marks: Option<String>,
pub tracks: Vec<JianYingTrack>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JianYingCanvasConfig {
pub height: u32,
pub ratio: String,
pub width: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JianYingConfig {
pub adjust_max_index: i32,
pub attachment_info: Vec<String>,
pub combination_max_index: i32,
pub export_range: Option<String>,
pub extract_audio_last_index: i32,
pub lyrics_recognition_id: String,
pub lyrics_sync: bool,
pub lyrics_taskinfo: Vec<String>,
pub maintrack_adsorb: bool,
pub material_save_mode: i32,
pub multi_language_current: String,
pub multi_language_list: Vec<String>,
pub multi_language_main: String,
pub multi_language_mode: String,
pub original_sound_last_index: i32,
pub record_audio_last_index: i32,
pub sticker_max_index: i32,
pub subtitle_keywords_config: Option<String>,
pub subtitle_recognition_id: String,
pub subtitle_sync: bool,
pub subtitle_taskinfo: Vec<String>,
pub system_font_list: Vec<String>,
pub video_mute: bool,
pub zoom_info_params: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JianYingKeyframes {
pub adjusts: Vec<String>,
pub audios: Vec<String>,
pub effects: Vec<String>,
pub filters: Vec<String>,
pub handwrites: Vec<String>,
pub stickers: Vec<String>,
pub texts: Vec<String>,
pub videos: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JianYingPlatform {
pub app_id: i32,
pub app_source: String,
pub app_version: String,
pub device_id: String,
pub hard_disk_id: String,
pub mac_address: String,
pub os: String,
pub os_version: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JianYingMaterials {
pub ai_translates: Vec<String>,
pub audio_balances: Vec<String>,
pub audio_effects: Vec<String>,
pub audio_fades: Vec<String>,
pub audio_track_indexes: Vec<String>,
pub audios: Vec<JianYingAudio>,
pub beats: Vec<String>,
pub canvases: Vec<String>,
pub chromas: Vec<String>,
pub color_curves: Vec<String>,
pub digital_humans: Vec<String>,
pub drafts: Vec<String>,
pub effects: Vec<String>,
pub flowers: Vec<String>,
pub green_screens: Vec<String>,
pub handwrites: Vec<String>,
pub hsl: Vec<String>,
pub images: Vec<String>,
pub log_color_wheels: Vec<String>,
pub loudnesses: Vec<String>,
pub manual_deformations: Vec<String>,
pub masks: Vec<String>,
pub material_animations: Vec<String>,
pub material_colors: Vec<String>,
pub multi_language_refs: Vec<String>,
pub placeholders: Vec<String>,
pub plugin_effects: Vec<String>,
pub primary_color_wheels: Vec<String>,
pub realtime_denoises: Vec<String>,
pub shapes: Vec<String>,
pub smart_crops: Vec<String>,
pub smart_relights: Vec<String>,
pub sound_channel_mappings: Vec<String>,
pub speeds: Vec<JianYingSpeed>,
pub stickers: Vec<String>,
pub tail_leaders: Vec<String>,
pub text_templates: Vec<String>,
pub texts: Vec<String>,
pub time_marks: Vec<String>,
pub transitions: Vec<String>,
pub video_effects: Vec<String>,
pub video_trackings: Vec<String>,
pub videos: Vec<JianYingVideo>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JianYingTrack {
pub attribute: i32,
pub flag: i32,
pub id: String,
pub is_default_name: bool,
pub name: String,
pub segments: Vec<JianYingSegment>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JianYingSegment {
pub caption_info: Option<String>,
pub cartoon: bool,
pub clip: JianYingClip,
pub common_keyframes: Vec<String>,
pub enable_adjust: bool,
pub enable_color_correct_adjust: bool,
pub enable_color_curves: bool,
pub enable_color_match_adjust: bool,
pub enable_color_wheels: bool,
pub enable_lut: bool,
pub enable_smart_color_adjust: bool,
pub extra_material_refs: Vec<String>,
pub group_id: String,
pub hdr_settings: JianYingHdrSettings,
pub id: String,
pub intensifies_audio: bool,
pub is_placeholder: bool,
pub is_tone_modify: bool,
pub keyframe_refs: Vec<String>,
pub last_nonzero_volume: f64,
pub material_id: String,
pub render_index: i32,
pub responsive_layout: JianYingResponsiveLayout,
pub reverse: bool,
pub source_timerange: JianYingTimeRange,
pub speed: f64,
pub target_timerange: JianYingTimeRange,
pub template_id: String,
pub template_scene: String,
pub track_attribute: i32,
pub track_render_index: i32,
pub uniform_scale: JianYingUniformScale,
pub visible: bool,
pub volume: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JianYingAudio {
pub app_id: i32,
pub category_id: String,
pub category_name: String,
pub check_flag: i32,
pub copyright_limit_type: String,
pub duration: u64,
pub effect_id: String,
pub formula_id: String,
pub id: String,
pub intensifies_path: String,
pub is_ai_clone_tone: bool,
pub is_text_edit_overdub: bool,
pub is_ugc: bool,
pub local_material_id: String,
pub music_id: String,
pub name: String,
pub path: String,
pub query: String,
pub request_id: String,
pub resource_id: String,
pub search_id: String,
pub source_from: String,
pub source_platform: i32,
pub team_id: String,
pub text_id: String,
pub tone_category_id: String,
pub tone_category_name: String,
pub tone_effect_id: String,
pub tone_effect_name: String,
pub tone_platform: String,
pub tone_second_category_id: String,
pub tone_second_category_name: String,
pub tone_speaker: String,
pub tone_type: String,
#[serde(rename = "type")]
pub material_type: String,
pub video_id: String,
pub wave_points: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JianYingSpeed {
pub curve_speed: Option<String>,
pub id: String,
pub mode: i32,
pub speed: f64,
#[serde(rename = "type")]
pub speed_type: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JianYingVideo {
pub aigc_type: String,
pub audio_fade: Option<String>,
pub cartoon_path: String,
pub category_id: String,
pub category_name: String,
pub check_flag: i32,
pub crop: JianYingCrop,
pub crop_ratio: String,
pub crop_scale: f64,
pub duration: u64,
pub extra_type_option: i32,
pub formula_id: String,
pub freeze: Option<String>,
pub has_audio: bool,
pub height: u32,
pub id: String,
pub intensifies_audio_path: String,
pub intensifies_path: String,
pub is_ai_generate_content: bool,
pub is_copyright: bool,
pub is_text_edit_overdub: bool,
pub is_unified_beauty_mode: bool,
pub local_id: String,
pub local_material_id: String,
pub material_id: String,
pub material_name: String,
pub material_url: String,
pub matting: JianYingMatting,
pub media_path: String,
pub object_locked: Option<String>,
pub origin_material_id: String,
pub path: String,
pub picture_from: String,
pub picture_set_category_id: String,
pub picture_set_category_name: String,
pub request_id: String,
pub reverse_intensifies_path: String,
pub reverse_path: String,
pub smart_motion: Option<String>,
pub source: i32,
pub source_platform: i32,
pub stable: JianYingStable,
pub team_id: String,
#[serde(rename = "type")]
pub video_type: String,
pub video_algorithm: JianYingVideoAlgorithm,
pub width: u32,
}
// 辅助结构体
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JianYingClip {
pub alpha: f64,
pub flip: JianYingFlip,
pub rotation: f64,
pub scale: JianYingScale,
pub transform: JianYingTransform,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JianYingFlip {
pub horizontal: bool,
pub vertical: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JianYingScale {
pub x: f64,
pub y: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JianYingTransform {
pub x: f64,
pub y: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JianYingHdrSettings {
pub intensity: f64,
pub mode: i32,
pub nits: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JianYingResponsiveLayout {
pub enable: bool,
pub horizontal_pos_layout: i32,
pub size_layout: i32,
pub target_follow: String,
pub vertical_pos_layout: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JianYingTimeRange {
pub duration: u64,
pub start: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JianYingUniformScale {
pub on: bool,
pub value: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JianYingCrop {
pub lower_left_x: f64,
pub lower_left_y: f64,
pub lower_right_x: f64,
pub lower_right_y: f64,
pub upper_left_x: f64,
pub upper_left_y: f64,
pub upper_right_x: f64,
pub upper_right_y: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JianYingMatting {
pub flag: i32,
pub has_use_quick_brush: bool,
pub has_use_quick_eraser: bool,
#[serde(rename = "interactiveTime")]
pub interactive_time: Vec<String>,
pub path: String,
pub strokes: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JianYingStable {
pub matrix_path: String,
pub stable_level: i32,
pub time_range: JianYingTimeRange,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JianYingVideoAlgorithm {
pub algorithms: Vec<String>,
pub complement_frame_config: Option<String>,
pub deflicker: Option<String>,
pub gameplay_configs: Vec<String>,
pub motion_blur_config: Option<String>,
pub noise_reduction: Option<String>,
pub path: String,
pub quality_enhance: Option<String>,
pub time_range: Option<JianYingTimeRange>,
}
/// 剪映导出服务
pub struct JianYingExportService;
impl JianYingExportService {
/// 创建默认的剪映草稿内容
pub fn create_default_draft_content() -> JianYingDraftContent {
let draft_id = Uuid::new_v4().to_string();
let now = Utc::now().timestamp();
JianYingDraftContent {
canvas_config: JianYingCanvasConfig {
height: 1920,
ratio: "9:16".to_string(),
width: 1080,
},
color_space: 0,
config: JianYingConfig {
adjust_max_index: 1,
attachment_info: vec![],
combination_max_index: 1,
export_range: None,
extract_audio_last_index: 1,
lyrics_recognition_id: String::new(),
lyrics_sync: true,
lyrics_taskinfo: vec![],
maintrack_adsorb: true,
material_save_mode: 0,
multi_language_current: "none".to_string(),
multi_language_list: vec![],
multi_language_main: "none".to_string(),
multi_language_mode: "none".to_string(),
original_sound_last_index: 1,
record_audio_last_index: 1,
sticker_max_index: 1,
subtitle_keywords_config: None,
subtitle_recognition_id: String::new(),
subtitle_sync: true,
subtitle_taskinfo: vec![],
system_font_list: vec![],
video_mute: false,
zoom_info_params: None,
},
cover: None,
create_time: now,
duration: 0,
extra_info: None,
fps: 30.0,
free_render_index_mode_on: false,
group_container: None,
id: draft_id,
keyframe_graph_list: vec![],
keyframes: JianYingKeyframes {
adjusts: vec![],
audios: vec![],
effects: vec![],
filters: vec![],
handwrites: vec![],
stickers: vec![],
texts: vec![],
videos: vec![],
},
last_modified_platform: JianYingPlatform {
app_id: 3704,
app_source: "lv".to_string(),
app_version: "5.9.0".to_string(),
device_id: "0594836068dad896e25a104fc9dbabab".to_string(),
hard_disk_id: "92ff8fc0225cc7379b7488c983cc022b".to_string(),
mac_address: "32d6cbfd9256fd8884fac27c2658c25c".to_string(),
os: "windows".to_string(),
os_version: "10.0.26100".to_string(),
},
materials: JianYingMaterials {
ai_translates: vec![],
audio_balances: vec![],
audio_effects: vec![],
audio_fades: vec![],
audio_track_indexes: vec![],
audios: vec![],
beats: vec![],
canvases: vec![],
chromas: vec![],
color_curves: vec![],
digital_humans: vec![],
drafts: vec![],
effects: vec![],
flowers: vec![],
green_screens: vec![],
handwrites: vec![],
hsl: vec![],
images: vec![],
log_color_wheels: vec![],
loudnesses: vec![],
manual_deformations: vec![],
masks: vec![],
material_animations: vec![],
material_colors: vec![],
multi_language_refs: vec![],
placeholders: vec![],
plugin_effects: vec![],
primary_color_wheels: vec![],
realtime_denoises: vec![],
shapes: vec![],
smart_crops: vec![],
smart_relights: vec![],
sound_channel_mappings: vec![],
speeds: vec![],
stickers: vec![],
tail_leaders: vec![],
text_templates: vec![],
texts: vec![],
time_marks: vec![],
transitions: vec![],
video_effects: vec![],
video_trackings: vec![],
videos: vec![],
},
render_index_track_mode_on: true,
retouch_cover: None,
source: "default".to_string(),
static_cover_image_path: String::new(),
time_marks: None,
tracks: vec![],
}
}
}

View File

@@ -20,6 +20,7 @@ pub mod project_template_binding_service;
pub mod material_matching_service;
pub mod template_matching_result_service;
pub mod video_generation_service;
pub mod jianying_export;
#[cfg(test)]
pub mod tests;

View File

@@ -1,16 +1,18 @@
use anyhow::{Result, anyhow};
use std::sync::Arc;
use chrono::Utc;
use std::path::Path;
use crate::data::models::template_matching_result::{
TemplateMatchingResult, MatchingSegmentResult, MatchingFailedSegmentResult,
CreateTemplateMatchingResultRequest, TemplateMatchingResultQueryOptions,
MatchingResultStatus,
};
use crate::data::repositories::template_matching_result_repository::TemplateMatchingResultRepository;
use crate::business::services::material_matching_service::{
MaterialMatchingResult as ServiceMatchingResult, SegmentMatch, FailedSegmentMatch,
MaterialMatchingResult as ServiceMatchingResult,
};
use crate::business::services::jianying_export::JianYingExportService;
use crate::data::repositories::material_repository::MaterialRepository;
/// 模板匹配结果服务
/// 遵循 Tauri 开发规范的业务逻辑设计原则
@@ -296,3 +298,278 @@ pub struct MatchingStatistics {
pub total_models: u32,
pub average_success_rate: f64,
}
impl TemplateMatchingResultService {
/// 导出匹配结果到剪映格式
/// 根据模板匹配结果生成剪映可导入的 draft_content.json 文件
pub async fn export_to_jianying(
&self,
result_id: &str,
output_path: &str,
material_repository: Arc<MaterialRepository>,
) -> Result<String> {
// 获取匹配结果详情
let detail = self.get_matching_result_detail(result_id).await?
.ok_or_else(|| anyhow!("匹配结果不存在: {}", result_id))?;
// 创建基础的剪映草稿内容
let mut draft_content = JianYingExportService::create_default_draft_content();
// 设置基本信息
draft_content.duration = self.calculate_total_duration(&detail)?;
// 生成素材列表 - 替换匹配的素材路径
let (materials, material_id_map) = self.generate_materials(&detail, material_repository).await?;
draft_content.materials.videos = materials;
// 生成轨道和片段
let tracks = self.generate_tracks(&detail, &material_id_map)?;
draft_content.tracks = tracks;
// 生成输出文件路径
let output_file_path = if output_path.ends_with(".json") {
output_path.to_string()
} else {
format!("{}/draft_content_{}.json", output_path, result_id)
};
// 序列化并写入文件
let json_content = serde_json::to_string_pretty(&draft_content)
.map_err(|e| anyhow!("序列化失败: {}", e))?;
std::fs::write(&output_file_path, json_content)
.map_err(|e| anyhow!("写入文件失败: {}", e))?;
Ok(output_file_path)
}
/// 计算总时长
fn calculate_total_duration(&self, detail: &TemplateMatchingResultDetail) -> Result<u64> {
let mut total_duration = 0u64;
for segment_result in &detail.segment_results {
if segment_result.end_time > total_duration {
total_duration = segment_result.end_time;
}
}
Ok(total_duration)
}
/// 生成素材列表,替换匹配的素材路径
async fn generate_materials(
&self,
detail: &TemplateMatchingResultDetail,
material_repository: Arc<MaterialRepository>,
) -> Result<(Vec<crate::business::services::jianying_export::JianYingVideo>, std::collections::HashMap<String, String>)> {
use crate::business::services::jianying_export::*;
use std::collections::HashMap;
use uuid::Uuid;
let mut videos = Vec::new();
let mut material_id_map = HashMap::new();
for segment_result in &detail.segment_results {
// 为每个匹配的素材生成新的UUID
let new_material_id = Uuid::new_v4().to_string();
material_id_map.insert(segment_result.track_segment_id.clone(), new_material_id.clone());
// 查询MaterialSegment获取文件路径
let material_segment = material_repository.get_segment_by_id_sync(&segment_result.material_segment_id)?
.ok_or_else(|| anyhow!("找不到素材片段: {}", segment_result.material_segment_id))?;
// 创建剪映视频素材
let video = JianYingVideo {
aigc_type: "none".to_string(),
audio_fade: None,
cartoon_path: String::new(),
category_id: String::new(),
category_name: "local".to_string(),
check_flag: 63487,
crop: JianYingCrop {
lower_left_x: 0.0,
lower_left_y: 1.0,
lower_right_x: 1.0,
lower_right_y: 1.0,
upper_left_x: 0.0,
upper_left_y: 0.0,
upper_right_x: 1.0,
upper_right_y: 0.0,
},
crop_ratio: "free".to_string(),
crop_scale: 1.0,
duration: segment_result.segment_duration,
extra_type_option: 0,
formula_id: String::new(),
freeze: None,
has_audio: false,
height: 1920, // 默认高度
id: new_material_id,
intensifies_audio_path: String::new(),
intensifies_path: String::new(),
is_ai_generate_content: false,
is_copyright: true,
is_text_edit_overdub: false,
is_unified_beauty_mode: false,
local_id: String::new(),
local_material_id: Uuid::new_v4().to_string(),
material_id: String::new(),
material_name: Path::new(&material_segment.file_path)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown.mp4")
.to_string(),
material_url: String::new(),
matting: JianYingMatting {
flag: 0,
has_use_quick_brush: false,
has_use_quick_eraser: false,
interactive_time: vec![],
path: String::new(),
strokes: vec![],
},
media_path: String::new(),
object_locked: None,
origin_material_id: String::new(),
path: material_segment.file_path.clone(), // 使用匹配的素材路径
picture_from: "none".to_string(),
picture_set_category_id: String::new(),
picture_set_category_name: String::new(),
request_id: String::new(),
reverse_intensifies_path: String::new(),
reverse_path: String::new(),
smart_motion: None,
source: 0,
source_platform: 0,
stable: JianYingStable {
matrix_path: String::new(),
stable_level: 0,
time_range: JianYingTimeRange {
duration: 0,
start: 0,
},
},
team_id: String::new(),
video_type: "video".to_string(),
video_algorithm: JianYingVideoAlgorithm {
algorithms: vec![],
complement_frame_config: None,
deflicker: None,
gameplay_configs: vec![],
motion_blur_config: None,
noise_reduction: None,
path: String::new(),
quality_enhance: None,
time_range: None,
},
width: 1080, // 默认宽度
};
videos.push(video);
}
Ok((videos, material_id_map))
}
/// 生成轨道和片段
fn generate_tracks(
&self,
detail: &TemplateMatchingResultDetail,
material_id_map: &std::collections::HashMap<String, String>,
) -> Result<Vec<crate::business::services::jianying_export::JianYingTrack>> {
use crate::business::services::jianying_export::*;
use uuid::Uuid;
let mut tracks = Vec::new();
// 创建主视频轨道
let track_id = Uuid::new_v4().to_string();
let mut segments = Vec::new();
for segment_result in &detail.segment_results {
if let Some(material_id) = material_id_map.get(&segment_result.track_segment_id) {
let segment = JianYingSegment {
caption_info: None,
cartoon: false,
clip: JianYingClip {
alpha: 1.0,
flip: JianYingFlip {
horizontal: false,
vertical: false,
},
rotation: 0.0,
scale: JianYingScale { x: 1.0, y: 1.0 },
transform: JianYingTransform { x: 0.0, y: 0.0 },
},
common_keyframes: vec![],
enable_adjust: true,
enable_color_correct_adjust: false,
enable_color_curves: true,
enable_color_match_adjust: false,
enable_color_wheels: true,
enable_lut: true,
enable_smart_color_adjust: false,
extra_material_refs: vec![],
group_id: String::new(),
hdr_settings: JianYingHdrSettings {
intensity: 1.0,
mode: 1,
nits: 1000,
},
id: Uuid::new_v4().to_string(),
intensifies_audio: false,
is_placeholder: false,
is_tone_modify: false,
keyframe_refs: vec![],
last_nonzero_volume: 1.0,
material_id: material_id.clone(),
render_index: 0,
responsive_layout: JianYingResponsiveLayout {
enable: false,
horizontal_pos_layout: 0,
size_layout: 0,
target_follow: String::new(),
vertical_pos_layout: 0,
},
reverse: false,
source_timerange: JianYingTimeRange {
duration: segment_result.segment_duration,
start: 0, // 从素材开始位置
},
speed: 1.0, // 默认播放速度
target_timerange: JianYingTimeRange {
duration: segment_result.segment_duration,
start: segment_result.start_time,
},
template_id: String::new(),
template_scene: "default".to_string(),
track_attribute: 0,
track_render_index: 0,
uniform_scale: JianYingUniformScale {
on: true,
value: 1.0,
},
visible: true,
volume: 1.0,
};
segments.push(segment);
}
}
let track = JianYingTrack {
attribute: 0,
flag: 0,
id: track_id,
is_default_name: true,
name: String::new(),
segments,
};
tracks.push(track);
Ok(tracks)
}
}

View File

@@ -220,6 +220,7 @@ pub fn run() {
commands::template_matching_result_commands::create_matching_result,
commands::template_matching_result_commands::get_matching_result_by_id,
commands::template_matching_result_commands::get_matching_result_status_options,
commands::template_matching_result_commands::export_matching_result_to_jianying,
// MaterialSegment聚合视图命令
commands::material_segment_view_commands::get_project_segment_view,
commands::material_segment_view_commands::get_project_segment_view_with_query,

View File

@@ -15,6 +15,7 @@ use crate::data::models::template_matching_result::{
TemplateMatchingResultQueryOptions, MatchingResultStatus,
};
use crate::data::repositories::template_matching_result_repository::TemplateMatchingResultRepository;
use crate::data::repositories::material_repository::MaterialRepository;
use crate::infrastructure::database::Database;
/// 保存匹配结果到数据库
@@ -231,3 +232,32 @@ pub async fn get_matching_result_status_options() -> Result<Vec<(String, String)
("Cancelled".to_string(), "已取消".to_string()),
])
}
/// 导出匹配结果到剪映格式
#[command]
pub async fn export_matching_result_to_jianying(
result_id: String,
output_path: String,
database: State<'_, Arc<Database>>,
) -> Result<String, String> {
println!("🎬 开始导出匹配结果到剪映格式");
println!("📋 导出参数:");
println!(" - 匹配结果ID: {}", result_id);
println!(" - 输出路径: {}", output_path);
let repository = Arc::new(TemplateMatchingResultRepository::new(database.inner().clone()));
let material_repository = Arc::new(MaterialRepository::new(database.inner().clone())
.map_err(|e| format!("创建MaterialRepository失败: {}", e))?);
let service = TemplateMatchingResultService::new(repository);
match service.export_to_jianying(&result_id, &output_path, material_repository).await {
Ok(file_path) => {
println!("✅ 导出成功: {}", file_path);
Ok(file_path)
}
Err(e) => {
println!("❌ 导出失败: {}", e);
Err(e.to_string())
}
}
}

View File

@@ -6,6 +6,7 @@ interface TemplateMatchingResultCardProps {
onViewDetail: () => void;
onDelete: () => void;
onEdit?: () => void;
onExportToJianying?: () => void;
}
export const TemplateMatchingResultCard: React.FC<TemplateMatchingResultCardProps> = ({
@@ -13,6 +14,7 @@ export const TemplateMatchingResultCard: React.FC<TemplateMatchingResultCardProp
onViewDetail,
onDelete,
onEdit,
onExportToJianying,
}) => {
// 格式化时长显示
const formatDuration = (ms: number): string => {
@@ -186,6 +188,15 @@ export const TemplateMatchingResultCard: React.FC<TemplateMatchingResultCardProp
</button>
)}
{onExportToJianying && (
<button
onClick={onExportToJianying}
className="btn btn-success btn-sm"
title="导出到剪映"
>
</button>
)}
<button
onClick={onDelete}
className="btn btn-danger btn-sm"

View File

@@ -1,5 +1,6 @@
import React, { useState, useEffect } from 'react';
import { invoke } from '@tauri-apps/api/core';
import { save } from '@tauri-apps/plugin-dialog';
import {
TemplateMatchingResult,
TemplateMatchingResultQueryOptions,
@@ -127,6 +128,38 @@ export const TemplateMatchingResultManager: React.FC<TemplateMatchingResultManag
onResultSelect?.(result);
};
// 导出到剪映
const handleExportToJianying = async (result: TemplateMatchingResult) => {
try {
// 选择保存路径
const filePath = await save({
title: '导出到剪映',
defaultPath: `draft_content_${result.result_name}.json`,
filters: [
{
name: 'JSON文件',
extensions: ['json']
}
]
});
if (!filePath) {
return; // 用户取消了保存
}
// 调用后端导出API
const exportedFilePath = await invoke<string>('export_matching_result_to_jianying', {
resultId: result.id,
outputPath: filePath,
});
// 显示成功消息
alert(`导出成功!文件已保存到:${exportedFilePath}`);
} catch (err) {
setError(`导出失败: ${err}`);
}
};
// 处理过滤器变化
const handleFilterChange = (newFilters: Partial<typeof filters>) => {
setFilters(prev => ({ ...prev, ...newFilters }));
@@ -278,6 +311,7 @@ export const TemplateMatchingResultManager: React.FC<TemplateMatchingResultManag
result={result}
onViewDetail={() => handleViewDetail(result)}
onDelete={() => setDeleteConfirm({ show: true, result })}
onExportToJianying={() => handleExportToJianying(result)}
/>
))}
</div>