| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265 |
- #!/usr/bin/env python3
- import os
- content = '''<template>
- <view class="page">
- <view class="nav-bar">
- <view class="nav-content">
- <view class="nav-left" @click="goBack"><text class="back-icon">←</text></view>
- <text class="page-title">创建视频</text>
- <view class="nav-right"><text class="save-btn" @click="saveProject">保存</text></view>
- </view>
- </view>
- <view class="step-nav">
- <view v-for="(step, index) in steps" :key="index" :class="['step-item', { active: currentStep === index }]">
- <view class="step-number">{{ index + 1 }}</view>
- <text class="step-text">{{ step }}</text>
- </view>
- </view>
- <view class="step-content">
- <view v-show="currentStep === 0" class="step-panel">
- <view class="form-section">
- <text class="section-title">基本信息</text>
- <view class="form-item">
- <text class="form-label">视频标题 *</text>
- <input v-model="formData.title" class="form-input" placeholder="给视频起个标题" />
- </view>
- </view>
- <view class="form-section">
- <text class="section-title">选择配图</text>
- <view v-if="formData.config.images.length > 0" class="selected-images">
- <view v-for="(img, index) in formData.config.images" :key="index" class="selected-image-item">
- <image :src="img.url" mode="aspectFill" class="selected-image" />
- <view class="image-remove" @click="removeImage(index)">×</view>
- </view>
- </view>
- <view class="upload-area" @click="chooseImage">
- <text class="upload-icon">+</text>
- <text class="upload-text">添加图片</text>
- </view>
- </view>
- <view class="form-section">
- <text class="section-title">选择音频</text>
- <view v-if="formData.config.audio.url" class="audio-preview">
- <text class="audio-name">已选择音频</text>
- <view class="audio-remove" @click="removeAudio">更换音频</view>
- </view>
- <view v-else class="upload-area" @click="chooseAudio">
- <text class="upload-icon">+</text>
- <text class="upload-text">添加音频</text>
- </view>
- </view>
- </view>
- <view v-show="currentStep === 1" class="step-panel">
- <view class="form-section">
- <text class="section-title">视频尺寸</text>
- <view class="ratio-options">
- <view v-for="ratio in ratios" :key="ratio.value" :class="['ratio-option', { active: formData.config.video.width === ratio.width }]" @click="selectRatio(ratio)">
- <text class="ratio-label">{{ ratio.label }}</text>
- </view>
- </view>
- </view>
- <view class="form-section">
- <text class="section-title">Ken Burns 效果</text>
- <view class="switch-item">
- <text class="switch-label">启用效果</text>
- <switch :checked="formData.config.kenburns.enabled" @change="toggleKenBurns" color="#667eea" />
- </view>
- </view>
- </view>
- <view v-show="currentStep === 2" class="step-panel">
- <view class="preview-card">
- <text class="preview-title">{{ formData.title || '未命名视频' }}</text>
- <text class="meta-item">尺寸: {{ formData.config.video.width }}×{{ formData.config.video.height }}</text>
- <text class="meta-item">图片: {{ formData.config.images.length }}张</text>
- </view>
- <view v-if="!isGenerating" class="generate-section">
- <button class="generate-btn" @click="startGenerate">开始生成视频</button>
- </view>
- <view v-else class="generating-section">
- <text class="generating-text">正在生成视频... {{ generateProgress }}%</text>
- <view class="progress-bar-large">
- <view class="progress-fill-large" :style="{ width: generateProgress + '%' }"></view>
- </view>
- </view>
- </view>
- </view>
- <view class="bottom-nav">
- <view v-if="currentStep > 0" class="nav-btn prev" @click="currentStep--">← 上一步</view>
- <view v-if="currentStep < steps.length - 1" class="nav-btn next" @click="nextStep">下一步 →</view>
- </view>
- </view>
- </template>
- <script setup lang="ts">
- import { ref } from 'vue';
- import { onLoad } from '@dcloudio/uni-app';
- import { createVideoProject, generateVideo, getGenerateProgress } from '@/utils/video-generator-api';
- import { PRESET_VIDEO_CONFIGS, type VideoConfig } from '@/types/video-generator';
- const steps = ['选择素材', '配置效果', '生成视频'];
- const currentStep = ref(0);
- const projectId = ref<number | null>(null);
- const isGenerating = ref(false);
- const generateProgress = ref(0);
- const ratios = [
- { value: 'portrait', label: '竖屏', width: 720, height: 1280 },
- { value: 'landscape', label: '横屏', width: 1920, height: 1080 },
- { value: 'square', label: '方形', width: 1080, height: 1080 },
- ];
- const formData = ref({
- title: '',
- config: { ...PRESET_VIDEO_CONFIGS.portrait, audio: { url: '', volume: 1 } } as VideoConfig,
- });
- function goBack() { uni.navigateBack(); }
- function nextStep() {
- if (currentStep.value === 0) {
- if (!formData.value.title) { uni.showToast({ title: '请输入标题', icon: 'none' }); return; }
- if (formData.value.config.images.length === 0) { uni.showToast({ title: '请添加图片', icon: 'none' }); return; }
- }
- if (currentStep.value < steps.length - 1) currentStep.value++;
- }
- function selectRatio(ratio: any) {
- const preset = PRESET_VIDEO_CONFIGS[ratio.value];
- formData.value.config.video = { ...preset.video };
- }
- function toggleKenBurns(e: any) { formData.value.config.kenburns.enabled = e.detail.value; }
- function chooseImage() {
- uni.chooseImage({ count: 9, success: (res) => {
- res.tempFilePaths.forEach((path) => {
- formData.value.config.images.push({ url: path, duration: 5, transition: 'fade' });
- });
- }});
- }
- function removeImage(index: number) { formData.value.config.images.splice(index, 1); }
- function chooseAudio() {
- uni.chooseMessageFile({ count: 1, type: 'audio', success: (res) => {
- formData.value.config.audio.url = res.tempFiles[0].path;
- }});
- }
- function removeAudio() { formData.value.config.audio.url = ''; }
- async function saveProject() {
- try {
- uni.showLoading({ title: '保存中...' });
- const res = await createVideoProject({ title: formData.value.title, config: formData.value.config });
- projectId.value = res.data.data.id;
- uni.hideLoading();
- uni.showToast({ title: '保存成功', icon: 'success' });
- } catch (error) {
- uni.hideLoading();
- uni.showToast({ title: '保存失败', icon: 'none' });
- }
- }
- async function startGenerate() {
- if (!projectId.value) {
- await saveProject();
- if (!projectId.value) { uni.showToast({ title: '请先保存', icon: 'none' }); return; }
- }
- try {
- isGenerating.value = true;
- generateProgress.value = 0;
- await generateVideo(projectId.value);
- const pollInterval = setInterval(async () => {
- try {
- const res = await getGenerateProgress(projectId.value!);
- const data = res.data.data;
- generateProgress.value = data.progress;
- if (data.status === 'completed') {
- clearInterval(pollInterval);
- uni.showToast({ title: '生成成功', icon: 'success' });
- setTimeout(() => { uni.navigateTo({ url: '/pages/video-generator/preview?id=' + projectId.value }); }, 1500);
- } else if (data.status === 'failed') {
- clearInterval(pollInterval);
- isGenerating.value = false;
- uni.showToast({ title: '生成失败', icon: 'none' });
- }
- } catch (error) { console.error('获取进度失败:', error); }
- }, 2000);
- } catch (error) {
- isGenerating.value = false;
- uni.showToast({ title: '启动生成失败', icon: 'none' });
- }
- }
- onLoad((query: any) => { if (query?.id) console.log('编辑项目:', query.id); });
- </script>
- <style scoped>
- .page { min-height: 100vh; background: #f5f5f5; }
- .nav-bar { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 20px 15px; }
- .nav-content { display: flex; align-items: center; justify-content: space-between; }
- .nav-left, .nav-right { width: 40px; }
- .back-icon, .save-btn { font-size: 18px; color: white; }
- .page-title { font-size: 18px; font-weight: 600; color: white; }
- .step-nav { display: flex; background: white; padding: 15px; gap: 10px; }
- .step-item { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 5px; }
- .step-number { width: 28px; height: 28px; border-radius: 50%; background: #e0e0e0; color: #999; display: flex; align-items: center; justify-content: center; font-size: 14px; }
- .step-item.active .step-number { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; }
- .step-text { font-size: 12px; color: #666; }
- .step-content { padding: 15px; }
- .step-panel { background: white; border-radius: 12px; padding: 20px; }
- .form-section { margin-bottom: 25px; }
- .section-title { font-size: 16px; font-weight: 600; color: #333; display: block; margin-bottom: 15px; }
- .form-item { margin-bottom: 15px; }
- .form-label { font-size: 14px; color: #666; display: block; margin-bottom: 8px; }
- .form-input { border: 1px solid #e0e0e0; border-radius: 8px; padding: 12px; font-size: 14px; width: 100%; box-sizing: border-box; }
- .upload-area { border: 2px dashed #e0e0e0; border-radius: 12px; padding: 30px; text-align: center; }
- .upload-icon { font-size: 32px; display: block; margin-bottom: 8px; }
- .upload-text { font-size: 14px; color: #999; }
- .selected-images { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 15px; }
- .selected-image-item { position: relative; width: 80px; height: 80px; }
- .selected-image { width: 100%; height: 100%; border-radius: 8px; }
- .image-remove { position: absolute; top: -8px; right: -8px; width: 20px; height: 20px; background: #f56c6c; color: white; border-radius: 50%; font-size: 12px; display: flex; align-items: center; justify-content: center; }
- .audio-preview { background: #f5f5f5; padding: 15px; border-radius: 8px; }
- .audio-name { font-size: 14px; color: #333; display: block; margin-bottom: 10px; }
- .audio-remove { font-size: 14px; color: #409eff; margin-top: 10px; }
- .ratio-options { display: flex; gap: 10px; }
- .ratio-option { flex: 1; padding: 15px; border: 2px solid #e0e0e0; border-radius: 8px; text-align: center; }
- .ratio-option.active { border-color: #667eea; background: rgba(102, 126, 234, 0.1); }
- .ratio-label { font-size: 14px; color: #333; }
- .switch-item { display: flex; justify-content: space-between; align-items: center; padding: 10px 0; }
- .switch-label { font-size: 14px; color: #333; }
- .preview-card { background: #f5f5f5; padding: 20px; border-radius: 12px; margin-bottom: 20px; }
- .preview-title { font-size: 18px; font-weight: 600; color: #333; display: block; margin-bottom: 10px; }
- .meta-item { font-size: 14px; color: #666; display: block; margin-top: 5px; }
- .generate-section { text-align: center; }
- .generate-btn { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; padding: 15px 40px; border-radius: 25px; font-size: 16px; }
- .generating-section { text-align: center; padding: 30px; }
- .generating-text { font-size: 16px; color: #333; display: block; margin-bottom: 20px; }
- .progress-bar-large { height: 10px; background: #e0e0e0; border-radius: 5px; overflow: hidden; margin-bottom: 10px; }
- .progress-fill-large { height: 100%; background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); transition: width 0.3s; }
- .bottom-nav { display: flex; padding: 15px; gap: 10px; background: white; }
- .nav-btn { flex: 1; padding: 12px; border-radius: 8px; text-align: center; font-size: 14px; }
- .nav-btn.prev { background: #f5f5f5; color: #666; }
- .nav-btn.next { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; }
- </style>'''
- file_path = 'c:/Users/caoyg/ai/audio-tts/audio_codebuddy/my-uniapp-vue3/src/pages/video-generator/create.vue'
- os.makedirs(os.path.dirname(file_path), exist_ok=True)
- with open(file_path, 'w', encoding='utf-8') as f:
- f.write(content)
- print(f'文件已创建: {file_path}')
- print(f'文件大小: {os.path.getsize(file_path)} bytes')
|