ai-clone/frontend-ai/components/VideoSelectModal.vue

489 lines
9.8 KiB
Vue
Raw Normal View History

2026-03-05 14:29:21 +08:00
<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>