create_vue.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. #!/usr/bin/env python3
  2. import os
  3. content = '''<template>
  4. <view class="page">
  5. <view class="nav-bar">
  6. <view class="nav-content">
  7. <view class="nav-left" @click="goBack"><text class="back-icon">←</text></view>
  8. <text class="page-title">创建视频</text>
  9. <view class="nav-right"><text class="save-btn" @click="saveProject">保存</text></view>
  10. </view>
  11. </view>
  12. <view class="step-nav">
  13. <view v-for="(step, index) in steps" :key="index" :class="['step-item', { active: currentStep === index }]">
  14. <view class="step-number">{{ index + 1 }}</view>
  15. <text class="step-text">{{ step }}</text>
  16. </view>
  17. </view>
  18. <view class="step-content">
  19. <view v-show="currentStep === 0" class="step-panel">
  20. <view class="form-section">
  21. <text class="section-title">基本信息</text>
  22. <view class="form-item">
  23. <text class="form-label">视频标题 *</text>
  24. <input v-model="formData.title" class="form-input" placeholder="给视频起个标题" />
  25. </view>
  26. </view>
  27. <view class="form-section">
  28. <text class="section-title">选择配图</text>
  29. <view v-if="formData.config.images.length > 0" class="selected-images">
  30. <view v-for="(img, index) in formData.config.images" :key="index" class="selected-image-item">
  31. <image :src="img.url" mode="aspectFill" class="selected-image" />
  32. <view class="image-remove" @click="removeImage(index)">×</view>
  33. </view>
  34. </view>
  35. <view class="upload-area" @click="chooseImage">
  36. <text class="upload-icon">+</text>
  37. <text class="upload-text">添加图片</text>
  38. </view>
  39. </view>
  40. <view class="form-section">
  41. <text class="section-title">选择音频</text>
  42. <view v-if="formData.config.audio.url" class="audio-preview">
  43. <text class="audio-name">已选择音频</text>
  44. <view class="audio-remove" @click="removeAudio">更换音频</view>
  45. </view>
  46. <view v-else class="upload-area" @click="chooseAudio">
  47. <text class="upload-icon">+</text>
  48. <text class="upload-text">添加音频</text>
  49. </view>
  50. </view>
  51. </view>
  52. <view v-show="currentStep === 1" class="step-panel">
  53. <view class="form-section">
  54. <text class="section-title">视频尺寸</text>
  55. <view class="ratio-options">
  56. <view v-for="ratio in ratios" :key="ratio.value" :class="['ratio-option', { active: formData.config.video.width === ratio.width }]" @click="selectRatio(ratio)">
  57. <text class="ratio-label">{{ ratio.label }}</text>
  58. </view>
  59. </view>
  60. </view>
  61. <view class="form-section">
  62. <text class="section-title">Ken Burns 效果</text>
  63. <view class="switch-item">
  64. <text class="switch-label">启用效果</text>
  65. <switch :checked="formData.config.kenburns.enabled" @change="toggleKenBurns" color="#667eea" />
  66. </view>
  67. </view>
  68. </view>
  69. <view v-show="currentStep === 2" class="step-panel">
  70. <view class="preview-card">
  71. <text class="preview-title">{{ formData.title || '未命名视频' }}</text>
  72. <text class="meta-item">尺寸: {{ formData.config.video.width }}×{{ formData.config.video.height }}</text>
  73. <text class="meta-item">图片: {{ formData.config.images.length }}张</text>
  74. </view>
  75. <view v-if="!isGenerating" class="generate-section">
  76. <button class="generate-btn" @click="startGenerate">开始生成视频</button>
  77. </view>
  78. <view v-else class="generating-section">
  79. <text class="generating-text">正在生成视频... {{ generateProgress }}%</text>
  80. <view class="progress-bar-large">
  81. <view class="progress-fill-large" :style="{ width: generateProgress + '%' }"></view>
  82. </view>
  83. </view>
  84. </view>
  85. </view>
  86. <view class="bottom-nav">
  87. <view v-if="currentStep > 0" class="nav-btn prev" @click="currentStep--">← 上一步</view>
  88. <view v-if="currentStep < steps.length - 1" class="nav-btn next" @click="nextStep">下一步 →</view>
  89. </view>
  90. </view>
  91. </template>
  92. <script setup lang="ts">
  93. import { ref } from 'vue';
  94. import { onLoad } from '@dcloudio/uni-app';
  95. import { createVideoProject, generateVideo, getGenerateProgress } from '@/utils/video-generator-api';
  96. import { PRESET_VIDEO_CONFIGS, type VideoConfig } from '@/types/video-generator';
  97. const steps = ['选择素材', '配置效果', '生成视频'];
  98. const currentStep = ref(0);
  99. const projectId = ref<number | null>(null);
  100. const isGenerating = ref(false);
  101. const generateProgress = ref(0);
  102. const ratios = [
  103. { value: 'portrait', label: '竖屏', width: 720, height: 1280 },
  104. { value: 'landscape', label: '横屏', width: 1920, height: 1080 },
  105. { value: 'square', label: '方形', width: 1080, height: 1080 },
  106. ];
  107. const formData = ref({
  108. title: '',
  109. config: { ...PRESET_VIDEO_CONFIGS.portrait, audio: { url: '', volume: 1 } } as VideoConfig,
  110. });
  111. function goBack() { uni.navigateBack(); }
  112. function nextStep() {
  113. if (currentStep.value === 0) {
  114. if (!formData.value.title) { uni.showToast({ title: '请输入标题', icon: 'none' }); return; }
  115. if (formData.value.config.images.length === 0) { uni.showToast({ title: '请添加图片', icon: 'none' }); return; }
  116. }
  117. if (currentStep.value < steps.length - 1) currentStep.value++;
  118. }
  119. function selectRatio(ratio: any) {
  120. const preset = PRESET_VIDEO_CONFIGS[ratio.value];
  121. formData.value.config.video = { ...preset.video };
  122. }
  123. function toggleKenBurns(e: any) { formData.value.config.kenburns.enabled = e.detail.value; }
  124. function chooseImage() {
  125. uni.chooseImage({ count: 9, success: (res) => {
  126. res.tempFilePaths.forEach((path) => {
  127. formData.value.config.images.push({ url: path, duration: 5, transition: 'fade' });
  128. });
  129. }});
  130. }
  131. function removeImage(index: number) { formData.value.config.images.splice(index, 1); }
  132. function chooseAudio() {
  133. uni.chooseMessageFile({ count: 1, type: 'audio', success: (res) => {
  134. formData.value.config.audio.url = res.tempFiles[0].path;
  135. }});
  136. }
  137. function removeAudio() { formData.value.config.audio.url = ''; }
  138. async function saveProject() {
  139. try {
  140. uni.showLoading({ title: '保存中...' });
  141. const res = await createVideoProject({ title: formData.value.title, config: formData.value.config });
  142. projectId.value = res.data.data.id;
  143. uni.hideLoading();
  144. uni.showToast({ title: '保存成功', icon: 'success' });
  145. } catch (error) {
  146. uni.hideLoading();
  147. uni.showToast({ title: '保存失败', icon: 'none' });
  148. }
  149. }
  150. async function startGenerate() {
  151. if (!projectId.value) {
  152. await saveProject();
  153. if (!projectId.value) { uni.showToast({ title: '请先保存', icon: 'none' }); return; }
  154. }
  155. try {
  156. isGenerating.value = true;
  157. generateProgress.value = 0;
  158. await generateVideo(projectId.value);
  159. const pollInterval = setInterval(async () => {
  160. try {
  161. const res = await getGenerateProgress(projectId.value!);
  162. const data = res.data.data;
  163. generateProgress.value = data.progress;
  164. if (data.status === 'completed') {
  165. clearInterval(pollInterval);
  166. uni.showToast({ title: '生成成功', icon: 'success' });
  167. setTimeout(() => { uni.navigateTo({ url: '/pages/video-generator/preview?id=' + projectId.value }); }, 1500);
  168. } else if (data.status === 'failed') {
  169. clearInterval(pollInterval);
  170. isGenerating.value = false;
  171. uni.showToast({ title: '生成失败', icon: 'none' });
  172. }
  173. } catch (error) { console.error('获取进度失败:', error); }
  174. }, 2000);
  175. } catch (error) {
  176. isGenerating.value = false;
  177. uni.showToast({ title: '启动生成失败', icon: 'none' });
  178. }
  179. }
  180. onLoad((query: any) => { if (query?.id) console.log('编辑项目:', query.id); });
  181. </script>
  182. <style scoped>
  183. .page { min-height: 100vh; background: #f5f5f5; }
  184. .nav-bar { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 20px 15px; }
  185. .nav-content { display: flex; align-items: center; justify-content: space-between; }
  186. .nav-left, .nav-right { width: 40px; }
  187. .back-icon, .save-btn { font-size: 18px; color: white; }
  188. .page-title { font-size: 18px; font-weight: 600; color: white; }
  189. .step-nav { display: flex; background: white; padding: 15px; gap: 10px; }
  190. .step-item { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 5px; }
  191. .step-number { width: 28px; height: 28px; border-radius: 50%; background: #e0e0e0; color: #999; display: flex; align-items: center; justify-content: center; font-size: 14px; }
  192. .step-item.active .step-number { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; }
  193. .step-text { font-size: 12px; color: #666; }
  194. .step-content { padding: 15px; }
  195. .step-panel { background: white; border-radius: 12px; padding: 20px; }
  196. .form-section { margin-bottom: 25px; }
  197. .section-title { font-size: 16px; font-weight: 600; color: #333; display: block; margin-bottom: 15px; }
  198. .form-item { margin-bottom: 15px; }
  199. .form-label { font-size: 14px; color: #666; display: block; margin-bottom: 8px; }
  200. .form-input { border: 1px solid #e0e0e0; border-radius: 8px; padding: 12px; font-size: 14px; width: 100%; box-sizing: border-box; }
  201. .upload-area { border: 2px dashed #e0e0e0; border-radius: 12px; padding: 30px; text-align: center; }
  202. .upload-icon { font-size: 32px; display: block; margin-bottom: 8px; }
  203. .upload-text { font-size: 14px; color: #999; }
  204. .selected-images { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 15px; }
  205. .selected-image-item { position: relative; width: 80px; height: 80px; }
  206. .selected-image { width: 100%; height: 100%; border-radius: 8px; }
  207. .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; }
  208. .audio-preview { background: #f5f5f5; padding: 15px; border-radius: 8px; }
  209. .audio-name { font-size: 14px; color: #333; display: block; margin-bottom: 10px; }
  210. .audio-remove { font-size: 14px; color: #409eff; margin-top: 10px; }
  211. .ratio-options { display: flex; gap: 10px; }
  212. .ratio-option { flex: 1; padding: 15px; border: 2px solid #e0e0e0; border-radius: 8px; text-align: center; }
  213. .ratio-option.active { border-color: #667eea; background: rgba(102, 126, 234, 0.1); }
  214. .ratio-label { font-size: 14px; color: #333; }
  215. .switch-item { display: flex; justify-content: space-between; align-items: center; padding: 10px 0; }
  216. .switch-label { font-size: 14px; color: #333; }
  217. .preview-card { background: #f5f5f5; padding: 20px; border-radius: 12px; margin-bottom: 20px; }
  218. .preview-title { font-size: 18px; font-weight: 600; color: #333; display: block; margin-bottom: 10px; }
  219. .meta-item { font-size: 14px; color: #666; display: block; margin-top: 5px; }
  220. .generate-section { text-align: center; }
  221. .generate-btn { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; padding: 15px 40px; border-radius: 25px; font-size: 16px; }
  222. .generating-section { text-align: center; padding: 30px; }
  223. .generating-text { font-size: 16px; color: #333; display: block; margin-bottom: 20px; }
  224. .progress-bar-large { height: 10px; background: #e0e0e0; border-radius: 5px; overflow: hidden; margin-bottom: 10px; }
  225. .progress-fill-large { height: 100%; background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); transition: width 0.3s; }
  226. .bottom-nav { display: flex; padding: 15px; gap: 10px; background: white; }
  227. .nav-btn { flex: 1; padding: 12px; border-radius: 8px; text-align: center; font-size: 14px; }
  228. .nav-btn.prev { background: #f5f5f5; color: #666; }
  229. .nav-btn.next { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; }
  230. </style>'''
  231. file_path = 'c:/Users/caoyg/ai/audio-tts/audio_codebuddy/my-uniapp-vue3/src/pages/video-generator/create.vue'
  232. os.makedirs(os.path.dirname(file_path), exist_ok=True)
  233. with open(file_path, 'w', encoding='utf-8') as f:
  234. f.write(content)
  235. print(f'文件已创建: {file_path}')
  236. print(f'文件大小: {os.path.getsize(file_path)} bytes')