ai-clone/frontend-ai/components/VideoSelectModal.vue
2026-03-05 14:29:21 +08:00

489 lines
9.8 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view v-if="show" class="modal-mask" @click="handleClose">
<view class="modal-container" @click.stop>
<view class="modal-header">
<text class="modal-title">📹 选择复活视频</text>
<text class="modal-close" @click="handleClose">✕</text>
</view>
<view class="modal-content">
<!-- 加载状态 -->
<view v-if="loading" class="loading-box">
<text class="loading-text">⏳ 加载视频列表...</text>
</view>
<!-- 视频列表 -->
<scroll-view v-else-if="videos.length > 0" scroll-y class="video-list">
<view
v-for="video in videos"
:key="video.id"
:class="['video-card', selectedVideoId === video.id ? 'selected' : '']"
@click="selectVideo(video)"
>
<!-- 封面 -->
<view class="video-cover">
<image
v-if="video.photo_url"
:src="video.photo_url"
class="cover-image"
mode="aspectFill"
></image>
<view v-else class="cover-placeholder">
<text class="placeholder-icon">🎬</text>
</view>
<view v-if="selectedVideoId === video.id" class="selected-badge">
<text class="badge-icon">✓</text>
</view>
</view>
<!-- 信息 -->
<view class="video-info">
<text class="video-name">{{ video.name || '复活视频' }}</text>
<text class="video-desc">{{ video.text || '暂无描述' }}</text>
<text class="video-time">{{ formatTime(video.create_time) }}</text>
</view>
</view>
</scroll-view>
<!-- 空状态 -->
<view v-else class="empty-box">
<text class="empty-icon">🎬</text>
<text class="empty-text">暂无复活视频</text>
<text class="empty-hint">请先去复活照片生成视频</text>
<button class="go-revival-btn" @click="goToRevival">
🎬 去复活照片
</button>
</view>
</view>
<view class="modal-footer">
<button class="modal-btn cancel" @click="handleClose">取消</button>
<button
class="modal-btn confirm"
:disabled="!selectedVideoId"
@click="handleConfirm"
>
开始通话
</button>
</view>
</view>
</view>
</template>
<script>
import { API_BASE, API_TIMEOUT, API_CONFIG } from '@/config/api.js';
export default {
props: {
show: {
type: Boolean,
default: false
}
},
data() {
return {
loading: false,
videos: [],
selectedVideoId: '',
selectedVideo: null
};
},
watch: {
show(newVal) {
if (newVal) {
this.loadVideos();
}
}
},
methods: {
isVideoUsableForCall(video) {
if (!video) return false;
const videoUrl = video.edited_video_url || video.videoUrl || video.local_video_path || video.video_url || video.localVideoPath;
const voiceId = video.voice_id || video.voiceId;
return !!videoUrl && !!voiceId;
},
// 加载视频列表
async loadVideos() {
this.loading = true;
const userId = uni.getStorageSync('userId') || '';
const token = uni.getStorageSync('token') || '';
if (!API_BASE) {
uni.showToast({
title: '加载失败API地址未配置',
icon: 'none'
});
this.videos = [];
this.loading = false;
return;
}
const requestOnce = (baseUrl) => new Promise((resolve) => {
if (!baseUrl) {
resolve({ ok: false, baseUrl, err: { errMsg: 'invalid url' } });
return;
}
uni.request({
url: `${baseUrl}/api/photo-revival/videos`,
method: 'GET',
timeout: API_TIMEOUT || 30000,
header: {
'X-User-Id': userId,
'Authorization': token ? `Bearer ${token}` : ''
},
success: (res) => {
resolve({ ok: true, baseUrl, res });
},
fail: (err) => {
resolve({ ok: false, baseUrl, err });
}
});
});
const primary = await requestOnce(API_BASE);
let finalResult = primary;
if (!primary.ok && API_CONFIG && API_CONFIG.development && API_CONFIG.development.baseURL) {
const fallbackUrl = API_CONFIG.development.baseURL;
if (fallbackUrl && fallbackUrl !== API_BASE) {
finalResult = await requestOnce(fallbackUrl);
}
}
if (!finalResult.ok) {
console.error('[VideoSelectModal] 加载视频列表失败:', finalResult.err);
uni.showToast({
title: (finalResult.err && finalResult.err.errMsg) ? finalResult.err.errMsg : '加载失败',
icon: 'none'
});
this.videos = [];
this.loading = false;
return;
}
const res = finalResult.res;
if (res.statusCode === 200) {
let videoList = [];
if (Array.isArray(res.data)) {
videoList = res.data;
} else if (res.data && Array.isArray(res.data.data)) {
videoList = res.data.data;
} else if (res.data && res.data.videos && Array.isArray(res.data.videos)) {
videoList = res.data.videos;
}
this.videos = (videoList || []).filter(v => this.isVideoUsableForCall(v));
this.loading = false;
return;
}
uni.showToast({
title: `加载失败(HTTP ${res.statusCode})`,
icon: 'none'
});
this.videos = [];
this.loading = false;
},
// 选择视频
selectVideo(video) {
this.selectedVideoId = video.id;
this.selectedVideo = video;
},
// 确认选择
handleConfirm() {
if (!this.selectedVideo) {
uni.showToast({
title: '请选择视频',
icon: 'none'
});
return;
}
this.$emit('confirm', this.selectedVideo);
this.handleClose();
},
// 关闭弹窗
handleClose() {
this.selectedVideoId = '';
this.selectedVideo = null;
this.$emit('close');
},
// 跳转到复活照片
goToRevival() {
this.handleClose();
uni.navigateTo({
url: '/pages/revival/revival-original'
});
},
// 格式化时间
formatTime(timestamp) {
if (!timestamp) return '';
const date = new Date(timestamp);
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${month}-${day} ${hours}:${minutes}`;
}
}
};
</script>
<style lang="scss" scoped>
.modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
padding: 40rpx;
}
.modal-container {
width: 100%;
max-width: 600rpx;
height: 80vh;
background: white;
border-radius: 32rpx;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.3);
}
.modal-header {
padding: 40rpx 40rpx 30rpx;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1rpx solid #f0f0f0;
}
.modal-title {
font-size: 36rpx;
font-weight: bold;
color: #333;
}
.modal-close {
font-size: 40rpx;
color: #999;
font-weight: 300;
padding: 0 10rpx;
}
.modal-content {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
min-height: 0;
}
/* 加载状态 */
.loading-box {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
padding: 80rpx 0;
min-height: 0;
}
.loading-text {
font-size: 28rpx;
color: #999;
}
/* 视频列表 */
.video-list {
flex: 1;
padding: 20rpx;
height: 100%;
min-height: 200rpx;
}
.video-card {
background: white;
border-radius: 24rpx;
padding: 24rpx;
margin-bottom: 20rpx;
display: flex;
align-items: center;
border: 2rpx solid #f0f0f0;
transition: all 0.3s;
}
.video-card.selected {
background: linear-gradient(135deg, rgba(139, 115, 85, 0.08) 0%, rgba(109, 139, 139, 0.08) 100%);
border-color: #8B7355;
box-shadow: 0 8rpx 24rpx rgba(139, 115, 85, 0.15);
}
.video-cover {
position: relative;
width: 120rpx;
height: 120rpx;
border-radius: 16rpx;
overflow: hidden;
flex-shrink: 0;
margin-right: 24rpx;
background: #f5f5f5;
}
.cover-image {
width: 100%;
height: 100%;
}
.cover-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.placeholder-icon {
font-size: 50rpx;
color: #ccc;
}
.selected-badge {
position: absolute;
top: 8rpx;
right: 8rpx;
width: 36rpx;
height: 36rpx;
background: #8B7355;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.badge-icon {
font-size: 20rpx;
color: white;
font-weight: bold;
}
.video-info {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
}
.video-name {
font-size: 30rpx;
font-weight: bold;
color: #333;
margin-bottom: 8rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.video-desc {
font-size: 24rpx;
color: #666;
margin-bottom: 8rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.video-time {
font-size: 22rpx;
color: #999;
}
/* 空状态 */
.empty-box {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80rpx 40rpx;
min-height: 0;
overflow-y: auto;
}
.empty-icon {
font-size: 100rpx;
margin-bottom: 20rpx;
opacity: 0.3;
}
.empty-text {
font-size: 32rpx;
color: #666;
margin-bottom: 10rpx;
}
.empty-hint {
font-size: 26rpx;
color: #999;
margin-bottom: 40rpx;
}
.go-revival-btn {
background: linear-gradient(135deg, #8B7355 0%, #6D8B8B 100%);
color: white;
border: none;
border-radius: 50rpx;
padding: 20rpx 50rpx;
font-size: 28rpx;
font-weight: 600;
}
/* 底部按钮 */
.modal-footer {
padding: 30rpx 40rpx;
padding-bottom: calc(30rpx + env(safe-area-inset-bottom));
display: flex;
gap: 20rpx;
border-top: 1rpx solid #f0f0f0;
}
.modal-btn {
flex: 1;
height: 88rpx;
border-radius: 44rpx;
font-size: 30rpx;
font-weight: 600;
border: none;
display: flex;
align-items: center;
justify-content: center;
}
.modal-btn.cancel {
background: #f5f5f5;
color: #666;
}
.modal-btn.confirm {
background: linear-gradient(135deg, #8B7355 0%, #6D8B8B 100%);
color: white;
}
.modal-btn.confirm:disabled {
opacity: 0.5;
}
</style>