ai-clone/frontend-ai/pages/video-gen/video-gen.vue

624 lines
13 KiB
Vue
Raw Normal View History

2026-03-05 14:29:21 +08:00
<template>
<view class="video-gen-page">
<view class="header">
<text class="title">🎬 AI图生视频</text>
<text class="subtitle">火山引擎 · doubao-seedance-1.0-pro</text>
</view>
<!-- 消息提示 -->
<view v-if="message.text" :class="['message', message.type]">
{{ message.text }}
</view>
<scroll-view scroll-y class="content">
<!-- 上传照片 -->
<view class="section">
<view class="section-title">📸 上传照片</view>
<view class="upload-area" @click="chooseImage">
<image v-if="imageUrl" :src="imageUrl" class="preview-image" mode="aspectFit"></image>
<view v-else class="upload-placeholder">
<text class="upload-icon">📷</text>
<text class="upload-text">点击上传照片</text>
<text class="upload-hint">支持 JPGPNG 格式</text>
</view>
</view>
</view>
<!-- 提示词 -->
<view class="section">
<view class="section-title"> 提示词可选</view>
<textarea v-model="prompt" class="prompt-input" :placeholder="promptPlaceholder" :maxlength="500"></textarea>
<view class="char-count">{{ prompt.length }}/500</view>
</view>
<!-- 视频时长 -->
<view class="section">
<view class="section-title"> 视频时长</view>
<view class="duration-selector">
<view
v-for="d in durations"
:key="d"
:class="['duration-item', duration === d ? 'active' : '']"
@click="duration = d"
>
{{ d }}
</view>
</view>
</view>
<!-- 视频名称 -->
<view class="section">
<view class="section-title">📝 视频名称可选</view>
<input
v-model="videoName"
class="name-input"
placeholder="给视频起个名字"
:maxlength="50"
/>
</view>
<!-- 生成按钮 -->
<view class="action-section">
<button
class="generate-btn"
:disabled="!imageUrl || generating"
@click="generateVideo"
>
<text v-if="generating"> 生成中...</text>
<text v-else>🎬 生成视频</text>
</button>
<text class="ai-disclaimer">本服务为AI生成内容结果仅供参考</text>
2026-03-05 14:29:21 +08:00
</view>
<!-- 进度提示 -->
<view v-if="generating" class="progress-section">
<view class="progress-text">{{ progressText }}</view>
<view class="progress-bar">
<view class="progress-fill" :style="{ width: progress + '%' }"></view>
</view>
</view>
<!-- 生成结果 -->
<view v-if="generatedVideo" class="result-section">
<view class="section-title"> 生成成功</view>
<view class="video-container">
<video
:src="generatedVideo"
class="result-video"
controls
autoplay
></video>
<!-- AI生成提示标签 -->
<view class="ai-tag">
<text class="ai-tag-text">AI生成</text>
</view>
</view>
2026-03-05 14:29:21 +08:00
<view class="result-actions">
<button class="action-btn" @click="saveToAlbum">💾 保存到相册</button>
<button class="action-btn secondary" @click="reset">🔄 重新生成</button>
</view>
</view>
<!-- 使用说明 -->
<view class="tips-section">
<view class="tips-title">💡 使用提示</view>
<view class="tip-item"> 支持 JPGPNG 格式的照片</view>
<view class="tip-item"> 提示词支持中文可描述动作表情场景等</view>
<view class="tip-item"> 视频时长支持 2-12 </view>
<view class="tip-item"> 生成时间约 1-3 分钟请耐心等待</view>
<view class="tip-item"> 使用火山引擎 doubao-seedance-1.0-pro 模型</view>
</view>
</scroll-view>
</view>
</template>
<script>
import { request, uploadFile, API_BASE } from '@/config/api.js';
export default {
data() {
return {
imageUrl: '',
imageFile: null,
prompt: '',
promptPlaceholder: '描述你想要的视频效果,支持中文,例如:一个人在微笑着说话,背景是温馨的客厅',
duration: 10,
durations: [2, 3, 4, 5, 6, 8, 10, 12],
videoName: '',
generating: false,
progress: 0,
progressText: '准备中...',
generatedVideo: '',
message: {
text: '',
type: 'info'
}
};
},
methods: {
// 选择图片
chooseImage() {
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
const selectedPath = res.tempFilePaths[0];
const selectedFile = res.tempFiles && res.tempFiles[0] ? res.tempFiles[0] : null;
const setSelected = (filePath) => {
this.imageUrl = filePath;
this.imageFile = selectedFile;
this.showMessage('照片已选择', 'success');
};
uni.getFileInfo({
filePath: selectedPath,
success: (info) => {
const maxBytes = 2 * 1024 * 1024;
if (info.size && info.size > maxBytes) {
this.showMessage('图片超过2MB正在自动压缩...', 'info');
const qualities = [60, 40, 20];
const tryCompress = (index, currentPath) => {
if (index >= qualities.length) {
this.showMessage('图片压缩后仍超过2MB请换一张更小的图片建议拍照/截图后再上传)', 'error');
return;
}
uni.compressImage({
src: currentPath,
quality: qualities[index],
success: (cRes) => {
uni.getFileInfo({
filePath: cRes.tempFilePath,
success: (cInfo) => {
if (cInfo.size && cInfo.size <= maxBytes) {
setSelected(cRes.tempFilePath);
} else {
tryCompress(index + 1, cRes.tempFilePath);
}
},
fail: (cInfoErr) => {
console.error('获取压缩后文件信息失败:', cInfoErr);
tryCompress(index + 1, cRes.tempFilePath);
}
});
},
fail: (cErr) => {
console.error('压缩图片失败:', cErr);
this.showMessage('图片过大且压缩失败,请换一张更小的图片', 'error');
}
});
};
tryCompress(0, selectedPath);
} else {
setSelected(selectedPath);
}
},
fail: (infoErr) => {
console.error('获取文件信息失败:', infoErr);
setSelected(selectedPath);
}
});
},
fail: (err) => {
console.error('选择图片失败:', err);
this.showMessage('选择图片失败', 'error');
}
});
},
// 生成视频
async generateVideo() {
if (!this.imageUrl) {
this.showMessage('请先上传照片', 'error');
return;
}
this.generating = true;
this.progress = 0;
this.progressText = '正在上传照片...';
try {
// 上传照片并生成视频
this.progress = 10;
this.progressText = '照片上传中...';
const result = await uploadFile({
url: '/api/photo-revival/volcengine-video',
filePath: this.imageUrl,
name: 'photo',
formData: {
text: '',
prompt: this.prompt || '',
duration: this.duration,
name: this.videoName || '火山引擎视频'
}
});
this.progress = 30;
this.progressText = '视频生成中,请稍候...';
// 模拟进度更新
const progressInterval = setInterval(() => {
if (this.progress < 90) {
this.progress += 2;
}
}, 2000);
// 等待一段时间后检查结果
setTimeout(() => {
clearInterval(progressInterval);
this.progress = 100;
this.progressText = '视频生成完成!';
if (result.status === 'success') {
this.generatedVideo = API_BASE + result.videoUrl;
this.showMessage('视频生成成功!', 'success');
} else {
throw new Error(result.message || '视频生成失败');
}
this.generating = false;
}, 3000);
} catch (error) {
console.error('生成视频失败:', error);
this.showMessage('生成失败: ' + (error.message || '未知错误'), 'error');
this.generating = false;
this.progress = 0;
}
},
// 保存到相册
saveToAlbum() {
if (!this.generatedVideo) {
this.showMessage('没有可保存的视频', 'error');
return;
}
uni.downloadFile({
url: this.generatedVideo,
success: (res) => {
if (res.statusCode === 200) {
uni.saveVideoToPhotosAlbum({
filePath: res.tempFilePath,
success: () => {
this.showMessage('已保存到相册', 'success');
},
fail: (err) => {
console.error('保存失败:', err);
this.showMessage('保存失败,请检查相册权限', 'error');
}
});
}
},
fail: (err) => {
console.error('下载失败:', err);
this.showMessage('下载失败', 'error');
}
});
},
// 重置
reset() {
this.imageUrl = '';
this.imageFile = null;
this.prompt = '';
this.duration = 10;
this.videoName = '';
this.generatedVideo = '';
this.progress = 0;
this.progressText = '准备中...';
this.message.text = '';
},
// 显示消息
showMessage(text, type = 'info') {
this.message = { text, type };
setTimeout(() => {
this.message.text = '';
}, 3000);
}
}
};
</script>
<style scoped>
.video-gen-page {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20rpx;
}
.header {
text-align: center;
padding: 40rpx 20rpx;
color: white;
}
.title {
font-size: 48rpx;
font-weight: bold;
display: block;
margin-bottom: 10rpx;
}
.subtitle {
font-size: 28rpx;
opacity: 0.9;
}
.message {
margin: 20rpx;
padding: 20rpx;
border-radius: 10rpx;
text-align: center;
font-size: 28rpx;
}
.message.success {
background: #d4edda;
color: #155724;
}
.message.error {
background: #f8d7da;
color: #721c24;
}
.message.info {
background: #d1ecf1;
color: #0c5460;
}
.content {
height: calc(100vh - 200rpx);
padding: 20rpx;
}
.section {
background: white;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 20rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
margin-bottom: 20rpx;
color: #333;
}
.upload-area {
border: 2rpx dashed #ddd;
border-radius: 15rpx;
min-height: 400rpx;
display: flex;
align-items: center;
justify-content: center;
background: #f8f9fa;
}
.upload-placeholder {
text-align: center;
}
.upload-icon {
font-size: 80rpx;
display: block;
margin-bottom: 20rpx;
}
.upload-text {
font-size: 32rpx;
color: #666;
display: block;
margin-bottom: 10rpx;
}
.upload-hint {
font-size: 24rpx;
color: #999;
}
.preview-image {
width: 100%;
max-height: 400rpx;
border-radius: 15rpx;
}
.prompt-input {
width: 100%;
min-height: 200rpx;
padding: 20rpx;
border: 1rpx solid #e0e0e0;
border-radius: 10rpx;
font-size: 28rpx;
background: #f8f9fa;
}
.char-count {
text-align: right;
font-size: 24rpx;
color: #999;
margin-top: 10rpx;
}
.duration-selector {
display: flex;
flex-wrap: wrap;
gap: 15rpx;
}
.duration-item {
flex: 0 0 calc(25% - 12rpx);
padding: 20rpx;
text-align: center;
border: 2rpx solid #e0e0e0;
border-radius: 10rpx;
font-size: 28rpx;
background: #f8f9fa;
}
.duration-item.active {
background: #667eea;
color: white;
border-color: #667eea;
}
.name-input {
width: 100%;
padding: 20rpx;
border: 1rpx solid #e0e0e0;
border-radius: 10rpx;
font-size: 28rpx;
background: #f8f9fa;
}
.action-section {
padding: 0 20rpx;
margin-bottom: 20rpx;
}
.generate-btn {
width: 100%;
padding: 30rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #ffffff !important;
2026-03-05 14:29:21 +08:00
border: none;
border-radius: 15rpx;
font-size: 32rpx;
font-weight: bold;
}
.generate-btn[disabled] {
opacity: 0.6;
color: #ffffff !important;
}
.ai-disclaimer {
display: block;
text-align: center;
font-size: 22rpx;
color: rgba(100, 100, 100, 0.6);
margin-top: 16rpx;
letter-spacing: 0.5rpx;
2026-03-05 14:29:21 +08:00
}
.progress-section {
background: white;
border-radius: 20rpx;
padding: 30rpx;
margin: 0 20rpx 20rpx;
}
.progress-text {
text-align: center;
font-size: 28rpx;
color: #666;
margin-bottom: 20rpx;
}
.progress-bar {
height: 20rpx;
background: #e0e0e0;
border-radius: 10rpx;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
transition: width 0.3s;
}
.result-section {
background: white;
border-radius: 20rpx;
padding: 30rpx;
margin: 0 20rpx 20rpx;
}
.result-section {
position: relative;
}
.video-container {
position: relative;
2026-03-05 14:29:21 +08:00
width: 100%;
height: 500rpx;
border-radius: 15rpx;
overflow: hidden;
2026-03-05 14:29:21 +08:00
margin: 20rpx 0;
}
.result-video {
width: 100%;
height: 100%;
border-radius: 15rpx;
}
/* AI生成提示标签 */
.ai-tag {
position: absolute;
top: 16rpx;
right: 16rpx;
z-index: 100;
background: rgba(0, 0, 0, 0.6);
padding: 8rpx 20rpx;
border-radius: 30rpx;
backdrop-filter: blur(10rpx);
}
.ai-tag-text {
font-size: 22rpx;
color: #fff;
font-weight: 500;
}
2026-03-05 14:29:21 +08:00
.result-actions {
display: flex;
gap: 20rpx;
}
.action-btn {
flex: 1;
padding: 25rpx;
background: #667eea;
color: white;
border: none;
border-radius: 10rpx;
font-size: 28rpx;
}
.action-btn.secondary {
background: #6c757d;
}
.tips-section {
background: rgba(255, 255, 255, 0.95);
border-radius: 20rpx;
padding: 30rpx;
margin: 0 20rpx 40rpx;
}
.tips-title {
font-size: 32rpx;
font-weight: bold;
margin-bottom: 20rpx;
color: #333;
}
.tip-item {
font-size: 26rpx;
color: #666;
line-height: 1.8;
margin-bottom: 10rpx;
}
</style>