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:
511
apps/desktop/src-tauri/src/business/services/jianying_export.rs
Normal file
511
apps/desktop/src-tauri/src/business/services/jianying_export.rs
Normal 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![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ pub mod project_template_binding_service;
|
|||||||
pub mod material_matching_service;
|
pub mod material_matching_service;
|
||||||
pub mod template_matching_result_service;
|
pub mod template_matching_result_service;
|
||||||
pub mod video_generation_service;
|
pub mod video_generation_service;
|
||||||
|
pub mod jianying_export;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub mod tests;
|
pub mod tests;
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
use anyhow::{Result, anyhow};
|
use anyhow::{Result, anyhow};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
use crate::data::models::template_matching_result::{
|
use crate::data::models::template_matching_result::{
|
||||||
TemplateMatchingResult, MatchingSegmentResult, MatchingFailedSegmentResult,
|
TemplateMatchingResult, MatchingSegmentResult, MatchingFailedSegmentResult,
|
||||||
CreateTemplateMatchingResultRequest, TemplateMatchingResultQueryOptions,
|
CreateTemplateMatchingResultRequest, TemplateMatchingResultQueryOptions,
|
||||||
MatchingResultStatus,
|
|
||||||
};
|
};
|
||||||
use crate::data::repositories::template_matching_result_repository::TemplateMatchingResultRepository;
|
use crate::data::repositories::template_matching_result_repository::TemplateMatchingResultRepository;
|
||||||
use crate::business::services::material_matching_service::{
|
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 开发规范的业务逻辑设计原则
|
/// 遵循 Tauri 开发规范的业务逻辑设计原则
|
||||||
@@ -296,3 +298,278 @@ pub struct MatchingStatistics {
|
|||||||
pub total_models: u32,
|
pub total_models: u32,
|
||||||
pub average_success_rate: f64,
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -220,6 +220,7 @@ pub fn run() {
|
|||||||
commands::template_matching_result_commands::create_matching_result,
|
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_by_id,
|
||||||
commands::template_matching_result_commands::get_matching_result_status_options,
|
commands::template_matching_result_commands::get_matching_result_status_options,
|
||||||
|
commands::template_matching_result_commands::export_matching_result_to_jianying,
|
||||||
// MaterialSegment聚合视图命令
|
// MaterialSegment聚合视图命令
|
||||||
commands::material_segment_view_commands::get_project_segment_view,
|
commands::material_segment_view_commands::get_project_segment_view,
|
||||||
commands::material_segment_view_commands::get_project_segment_view_with_query,
|
commands::material_segment_view_commands::get_project_segment_view_with_query,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ use crate::data::models::template_matching_result::{
|
|||||||
TemplateMatchingResultQueryOptions, MatchingResultStatus,
|
TemplateMatchingResultQueryOptions, MatchingResultStatus,
|
||||||
};
|
};
|
||||||
use crate::data::repositories::template_matching_result_repository::TemplateMatchingResultRepository;
|
use crate::data::repositories::template_matching_result_repository::TemplateMatchingResultRepository;
|
||||||
|
use crate::data::repositories::material_repository::MaterialRepository;
|
||||||
use crate::infrastructure::database::Database;
|
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()),
|
("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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ interface TemplateMatchingResultCardProps {
|
|||||||
onViewDetail: () => void;
|
onViewDetail: () => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
onEdit?: () => void;
|
onEdit?: () => void;
|
||||||
|
onExportToJianying?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TemplateMatchingResultCard: React.FC<TemplateMatchingResultCardProps> = ({
|
export const TemplateMatchingResultCard: React.FC<TemplateMatchingResultCardProps> = ({
|
||||||
@@ -13,6 +14,7 @@ export const TemplateMatchingResultCard: React.FC<TemplateMatchingResultCardProp
|
|||||||
onViewDetail,
|
onViewDetail,
|
||||||
onDelete,
|
onDelete,
|
||||||
onEdit,
|
onEdit,
|
||||||
|
onExportToJianying,
|
||||||
}) => {
|
}) => {
|
||||||
// 格式化时长显示
|
// 格式化时长显示
|
||||||
const formatDuration = (ms: number): string => {
|
const formatDuration = (ms: number): string => {
|
||||||
@@ -186,6 +188,15 @@ export const TemplateMatchingResultCard: React.FC<TemplateMatchingResultCardProp
|
|||||||
编辑
|
编辑
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
{onExportToJianying && (
|
||||||
|
<button
|
||||||
|
onClick={onExportToJianying}
|
||||||
|
className="btn btn-success btn-sm"
|
||||||
|
title="导出到剪映"
|
||||||
|
>
|
||||||
|
导出到剪映
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={onDelete}
|
onClick={onDelete}
|
||||||
className="btn btn-danger btn-sm"
|
className="btn btn-danger btn-sm"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
import { save } from '@tauri-apps/plugin-dialog';
|
||||||
import {
|
import {
|
||||||
TemplateMatchingResult,
|
TemplateMatchingResult,
|
||||||
TemplateMatchingResultQueryOptions,
|
TemplateMatchingResultQueryOptions,
|
||||||
@@ -127,6 +128,38 @@ export const TemplateMatchingResultManager: React.FC<TemplateMatchingResultManag
|
|||||||
onResultSelect?.(result);
|
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>) => {
|
const handleFilterChange = (newFilters: Partial<typeof filters>) => {
|
||||||
setFilters(prev => ({ ...prev, ...newFilters }));
|
setFilters(prev => ({ ...prev, ...newFilters }));
|
||||||
@@ -278,6 +311,7 @@ export const TemplateMatchingResultManager: React.FC<TemplateMatchingResultManag
|
|||||||
result={result}
|
result={result}
|
||||||
onViewDetail={() => handleViewDetail(result)}
|
onViewDetail={() => handleViewDetail(result)}
|
||||||
onDelete={() => setDeleteConfirm({ show: true, result })}
|
onDelete={() => setDeleteConfirm({ show: true, result })}
|
||||||
|
onExportToJianying={() => handleExportToJianying(result)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user