1142 lines
28 KiB
Vue
1142 lines
28 KiB
Vue
<template>
|
||
<view class="history-container">
|
||
<!-- 头部 -->
|
||
<view class="header">
|
||
<text class="title">我的视频</text>
|
||
<text class="subtitle">{{ videos.length }} 个作品</text>
|
||
</view>
|
||
|
||
<!-- 视频列表 -->
|
||
<scroll-view scroll-y class="video-list">
|
||
<view v-if="loading" class="loading-box">
|
||
<text class="loading-text">⏳ 加载中...</text>
|
||
</view>
|
||
|
||
<view v-else-if="videos.length === 0" class="empty-box">
|
||
<text class="empty-icon">📹</text>
|
||
<text class="empty-text">暂无复活视频</text>
|
||
<text class="empty-hint">快去复活你的第一张照片吧</text>
|
||
</view>
|
||
|
||
<view v-else class="video-grid">
|
||
<view
|
||
v-for="video in videos"
|
||
:key="video.id"
|
||
class="video-card"
|
||
@click="viewVideo(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 class="play-overlay">
|
||
<text class="play-icon">▶</text>
|
||
</view>
|
||
<!-- 缓存状态标识 -->
|
||
<view v-if="video.isCached" class="cache-badge">
|
||
<text class="cache-text">已缓存</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 信息 -->
|
||
<view class="video-info">
|
||
<text class="video-title">{{ video.name || '复活视频' }}</text>
|
||
<text class="video-text" v-if="video.text">{{ video.text }}</text>
|
||
<view class="video-meta">
|
||
<text class="video-time">{{ formatTime(video.create_time) }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 操作按钮 -->
|
||
<view class="video-actions">
|
||
<view class="action-btn" @click.stop="viewVideo(video)">
|
||
<text class="action-icon">▶️</text>
|
||
<text class="action-text">播放</text>
|
||
</view>
|
||
<!-- #ifdef MP-WEIXIN -->
|
||
<view class="action-btn" @click.stop="saveVideoToAlbum(video)">
|
||
<text class="action-icon">💾</text>
|
||
<text class="action-text">保存</text>
|
||
</view>
|
||
<!-- #endif -->
|
||
<!-- #ifdef MP-WEIXIN -->
|
||
<button class="action-btn share" open-type="share" @click.stop="prepareShare(video)">
|
||
<text class="action-icon">🔗</text>
|
||
<text class="action-text">分享</text>
|
||
</button>
|
||
<!-- #endif -->
|
||
<!-- #ifndef MP-WEIXIN -->
|
||
<view class="action-btn share" @click.stop="shareVideo(video)">
|
||
<text class="action-icon">🔗</text>
|
||
<text class="action-text">分享</text>
|
||
</view>
|
||
<!-- #endif -->
|
||
<view v-if="isVideoPublishable(video)" class="action-btn publish" @click.stop="togglePublish(video)">
|
||
<text class="action-icon">{{ video.published ? '↩️' : '📢' }}</text>
|
||
<text class="action-text">{{ video.published ? '下架' : '发布' }}</text>
|
||
</view>
|
||
<view class="action-btn delete" @click.stop="deleteVideo(video)">
|
||
<text class="action-icon">🗑️</text>
|
||
<text class="action-text">删除</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<view v-if="publishDialogVisible" class="publish-mask" @click="closePublishDialog">
|
||
<view class="publish-dialog" @click.stop>
|
||
<view class="publish-title">发布作品</view>
|
||
<input
|
||
class="publish-input"
|
||
v-model="publishTitle"
|
||
placeholder="请输入标题(最多20字)"
|
||
maxlength="20"
|
||
/>
|
||
<view class="publish-actions">
|
||
<view class="publish-btn cancel" @click="closePublishDialog">取消</view>
|
||
<view class="publish-btn confirm" @click="confirmPublish">发布</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import { API_BASE, API_ENDPOINTS, buildURL } from '@/config/api.js';
|
||
|
||
export default {
|
||
data() {
|
||
return {
|
||
API_BASE,
|
||
videos: [],
|
||
loading: false,
|
||
enablePrecache: false,
|
||
precacheQueue: [],
|
||
precaching: false,
|
||
precacheInFlight: {},
|
||
publishDialogVisible: false,
|
||
publishTitle: '',
|
||
publishTargetVideo: null,
|
||
shareTargetVideo: null
|
||
};
|
||
},
|
||
|
||
onLoad() {
|
||
this.loadVideos();
|
||
},
|
||
|
||
onShareAppMessage() {
|
||
const v = this.shareTargetVideo || {};
|
||
const title = (v && (v.name || '').trim()) ? v.name : '我的视频';
|
||
const imageUrl = v && v.photo_url ? v.photo_url : undefined;
|
||
const videoUrl = v && (v.edited_video_url || v.local_video_path || v.video_url) ? String(v.edited_video_url || v.local_video_path || v.video_url) : '';
|
||
const path = '/pages/video-player/video-player?audioInVideo=1&url=' + encodeURIComponent(videoUrl) + '&title=' + encodeURIComponent(title);
|
||
return {
|
||
title,
|
||
path,
|
||
imageUrl
|
||
};
|
||
},
|
||
|
||
onShareTimeline() {
|
||
const v = this.shareTargetVideo || {};
|
||
const title = (v && (v.name || '').trim()) ? v.name : '我的视频';
|
||
const imageUrl = v && v.photo_url ? v.photo_url : undefined;
|
||
const videoUrl = v && (v.edited_video_url || v.local_video_path || v.video_url) ? String(v.edited_video_url || v.local_video_path || v.video_url) : '';
|
||
const query = 'url=' + encodeURIComponent(videoUrl) + '&title=' + encodeURIComponent(title);
|
||
return {
|
||
title,
|
||
query,
|
||
imageUrl
|
||
};
|
||
},
|
||
|
||
methods: {
|
||
normalizeMediaUrl(url) {
|
||
if (!url) return url;
|
||
if (url.startsWith('/')) {
|
||
return `${this.API_BASE}${url}`;
|
||
}
|
||
if (url.startsWith('http://115.190.167.176:20002')) {
|
||
return url.replace('http://115.190.167.176:20002', this.API_BASE);
|
||
}
|
||
return url;
|
||
},
|
||
|
||
ensureLoginOrRedirect() {
|
||
const token = uni.getStorageSync('token') || '';
|
||
const userId = uni.getStorageSync('userId') || '';
|
||
if (token && userId) {
|
||
return true;
|
||
}
|
||
uni.showToast({
|
||
title: '请先登录',
|
||
icon: 'none'
|
||
});
|
||
setTimeout(() => {
|
||
uni.navigateTo({
|
||
url: '/pages/login/login'
|
||
});
|
||
}, 800);
|
||
return false;
|
||
},
|
||
|
||
isVideoPublishable(video) {
|
||
if (!video) return false;
|
||
const videoUrl = video.edited_video_url || video.local_video_path || video.video_url || video.videoUrl || video.localVideoPath;
|
||
return !!videoUrl;
|
||
},
|
||
|
||
openPublishDialog(video) {
|
||
this.publishTargetVideo = video;
|
||
this.publishTitle = (video && (video.name || '') ? video.name : '复活视频');
|
||
this.publishDialogVisible = true;
|
||
},
|
||
|
||
closePublishDialog() {
|
||
this.publishDialogVisible = false;
|
||
this.publishTitle = '';
|
||
this.publishTargetVideo = null;
|
||
},
|
||
|
||
confirmPublish() {
|
||
const video = this.publishTargetVideo;
|
||
if (!video) {
|
||
this.closePublishDialog();
|
||
return;
|
||
}
|
||
const title = (this.publishTitle || '').trim();
|
||
if (!title) {
|
||
uni.showToast({ title: '请输入标题', icon: 'none' });
|
||
return;
|
||
}
|
||
if (title.length > 20) {
|
||
uni.showToast({ title: '标题不能超过20字', icon: 'none' });
|
||
return;
|
||
}
|
||
this.closePublishDialog();
|
||
this.publishVideo(video, title);
|
||
},
|
||
|
||
publishVideo(video, title) {
|
||
const token = uni.getStorageSync('token') || '';
|
||
const userId = uni.getStorageSync('userId') || '';
|
||
if (!token || !userId) {
|
||
uni.showToast({ title: '请先登录', icon: 'none' });
|
||
return;
|
||
}
|
||
uni.showLoading({ title: '发布中...' });
|
||
uni.request({
|
||
url: `${this.API_BASE}/api/works/publish/revival-video`,
|
||
method: 'POST',
|
||
header: {
|
||
'Content-Type': 'application/json',
|
||
'X-User-Id': userId,
|
||
'Authorization': token ? `Bearer ${token}` : ''
|
||
},
|
||
data: {
|
||
userId: Number(userId),
|
||
revivalVideoId: video.id,
|
||
title
|
||
},
|
||
success: (res) => {
|
||
if (res.statusCode === 200 && res.data && res.data.success) {
|
||
video.published = true;
|
||
video.workId = res.data.data && res.data.data.id;
|
||
uni.showToast({ title: '发布成功', icon: 'success' });
|
||
} else {
|
||
uni.showToast({ title: res.data?.message || '发布失败', icon: 'none' });
|
||
}
|
||
},
|
||
fail: () => {
|
||
uni.showToast({ title: '发布失败', icon: 'none' });
|
||
},
|
||
complete: () => {
|
||
uni.hideLoading();
|
||
}
|
||
});
|
||
},
|
||
loadVideos() {
|
||
if (!this.ensureLoginOrRedirect()) {
|
||
this.loading = false;
|
||
this.videos = [];
|
||
return;
|
||
}
|
||
|
||
this.loading = true;
|
||
|
||
// 获取用户ID和Token
|
||
const userId = uni.getStorageSync('userId') || '';
|
||
const token = uni.getStorageSync('token') || '';
|
||
|
||
uni.request({
|
||
url: `${this.API_BASE}/api/photo-revival/videos`,
|
||
method: 'GET',
|
||
header: {
|
||
'X-User-Id': userId,
|
||
'Authorization': token ? `Bearer ${token}` : ''
|
||
},
|
||
success: (res) => {
|
||
console.log('[RevivalHistory] API响应:', res.data);
|
||
console.log('[RevivalHistory] 用户ID:', userId);
|
||
if (res.data.success) {
|
||
this.videos = (res.data.videos || []).map(v => ({
|
||
...v,
|
||
photo_url: this.normalizeMediaUrl(v && v.photo_url ? String(v.photo_url) : ''),
|
||
video_url: this.normalizeMediaUrl(v && v.video_url ? String(v.video_url) : ''),
|
||
edited_video_url: this.normalizeMediaUrl(v && v.edited_video_url ? String(v.edited_video_url) : ''),
|
||
local_video_path: this.normalizeMediaUrl(v && v.local_video_path ? String(v.local_video_path) : ''),
|
||
audio_url: this.normalizeMediaUrl(v && v.audio_url ? String(v.audio_url) : ''),
|
||
published: false,
|
||
workId: null
|
||
}));
|
||
console.log('[RevivalHistory] 加载视频数量:', this.videos.length);
|
||
// 检查每个视频的缓存状态
|
||
this.checkVideosCache();
|
||
// 查询发布状态
|
||
this.loadPublishStatus();
|
||
}
|
||
},
|
||
fail: (err) => {
|
||
console.error('[RevivalHistory] 加载失败:', err);
|
||
uni.showToast({
|
||
title: '加载失败',
|
||
icon: 'none'
|
||
});
|
||
},
|
||
complete: () => {
|
||
this.loading = false;
|
||
}
|
||
});
|
||
},
|
||
|
||
loadPublishStatus() {
|
||
const token = uni.getStorageSync('token') || '';
|
||
const userId = uni.getStorageSync('userId') || '';
|
||
this.videos.forEach(video => {
|
||
uni.request({
|
||
url: `${this.API_BASE}/api/works/source?sourceType=REVIVAL_VIDEO&sourceId=${video.id}`,
|
||
method: 'GET',
|
||
header: {
|
||
'X-User-Id': userId,
|
||
'Authorization': token ? `Bearer ${token}` : ''
|
||
},
|
||
success: (res) => {
|
||
if (res.statusCode === 200 && res.data) {
|
||
video.published = res.data.published === 1;
|
||
video.workId = res.data.id;
|
||
}
|
||
}
|
||
});
|
||
});
|
||
},
|
||
|
||
togglePublish(video) {
|
||
const token = uni.getStorageSync('token') || '';
|
||
const userId = uni.getStorageSync('userId') || '';
|
||
if (!token || !userId) {
|
||
uni.showToast({ title: '请先登录', icon: 'none' });
|
||
return;
|
||
}
|
||
|
||
if (!video.published) {
|
||
this.openPublishDialog(video);
|
||
return;
|
||
}
|
||
|
||
// 已发布:取消发布
|
||
uni.showModal({
|
||
title: '取消发布',
|
||
content: '确定要从作品广场下架该作品吗?',
|
||
success: (m) => {
|
||
if (!m.confirm) return;
|
||
uni.showLoading({ title: '处理中...' });
|
||
uni.request({
|
||
url: `${this.API_BASE}/api/works/unpublish?workId=${video.workId}&userId=${userId}`,
|
||
method: 'POST',
|
||
header: {
|
||
'X-User-Id': userId,
|
||
'Authorization': token ? `Bearer ${token}` : ''
|
||
},
|
||
success: (res) => {
|
||
if (res.statusCode === 200 && res.data && res.data.success) {
|
||
video.published = false;
|
||
uni.showToast({ title: '已取消发布', icon: 'success' });
|
||
} else {
|
||
uni.showToast({ title: res.data?.message || '操作失败', icon: 'none' });
|
||
}
|
||
},
|
||
fail: () => {
|
||
uni.showToast({ title: '操作失败', icon: 'none' });
|
||
},
|
||
complete: () => {
|
||
uni.hideLoading();
|
||
}
|
||
});
|
||
}
|
||
});
|
||
},
|
||
|
||
checkVideosCache() {
|
||
let scheduled = 0;
|
||
this.videos.forEach(video => {
|
||
const videoUrl = video.edited_video_url || video.local_video_path || video.video_url;
|
||
if (videoUrl) {
|
||
const normalizedUrl = this.normalizeMediaUrl(videoUrl);
|
||
const cacheKey = this.generateCacheKey(normalizedUrl);
|
||
const cachedPath = uni.getStorageSync(cacheKey);
|
||
video.isCached = !!cachedPath;
|
||
if (this.enablePrecache && !video.isCached && typeof plus !== 'undefined' && scheduled < 3) {
|
||
scheduled++;
|
||
this.enqueuePrecache(normalizedUrl);
|
||
}
|
||
}
|
||
});
|
||
},
|
||
|
||
enqueuePrecache(videoUrl) {
|
||
if (!videoUrl) return;
|
||
if (this.precacheInFlight[videoUrl]) return;
|
||
if (this.precacheQueue.indexOf(videoUrl) !== -1) return;
|
||
this.precacheQueue.push(videoUrl);
|
||
this.processPrecacheQueue();
|
||
},
|
||
|
||
async processPrecacheQueue() {
|
||
if (this.precaching) return;
|
||
if (!this.precacheQueue.length) return;
|
||
this.precaching = true;
|
||
try {
|
||
while (this.precacheQueue.length) {
|
||
const url = this.precacheQueue.shift();
|
||
if (!url) continue;
|
||
if (this.precacheInFlight[url]) continue;
|
||
this.precacheInFlight[url] = true;
|
||
try {
|
||
await this.precacheVideoApp(url);
|
||
} catch (e) {
|
||
} finally {
|
||
delete this.precacheInFlight[url];
|
||
}
|
||
}
|
||
} finally {
|
||
this.precaching = false;
|
||
}
|
||
},
|
||
|
||
precacheVideoApp(videoUrl) {
|
||
return new Promise((resolve, reject) => {
|
||
if (typeof plus === 'undefined') {
|
||
resolve();
|
||
return;
|
||
}
|
||
const cacheKey = this.generateCacheKey(videoUrl);
|
||
const cachedPath = uni.getStorageSync(cacheKey);
|
||
if (cachedPath) {
|
||
resolve();
|
||
return;
|
||
}
|
||
uni.downloadFile({
|
||
url: videoUrl,
|
||
success: (res) => {
|
||
if (res.statusCode !== 200) {
|
||
reject(new Error('download failed'));
|
||
return;
|
||
}
|
||
uni.saveFile({
|
||
tempFilePath: res.tempFilePath,
|
||
success: (saveRes) => {
|
||
const savedPath = saveRes.savedFilePath;
|
||
uni.setStorageSync(cacheKey, savedPath);
|
||
const cacheInfo = uni.getStorageSync('video_cache_info') || {};
|
||
cacheInfo[cacheKey] = { path: savedPath, url: videoUrl, time: Date.now() };
|
||
uni.setStorageSync('video_cache_info', cacheInfo);
|
||
resolve(savedPath);
|
||
},
|
||
fail: (err) => {
|
||
reject(err);
|
||
}
|
||
});
|
||
},
|
||
fail: (err) => {
|
||
reject(err);
|
||
}
|
||
});
|
||
});
|
||
},
|
||
|
||
generateCacheKey(url) {
|
||
let hash = 0;
|
||
for (let i = 0; i < url.length; i++) {
|
||
const char = url.charCodeAt(i);
|
||
hash = ((hash << 5) - hash) + char;
|
||
hash = hash & hash;
|
||
}
|
||
return 'video_' + Math.abs(hash);
|
||
},
|
||
|
||
viewVideo(video) {
|
||
console.log('[RevivalHistory] 点击视频:', video);
|
||
|
||
// 使用本地保存的视频路径,如果没有则使用远程URL
|
||
const localPathStr = video && video.local_video_path ? String(video.local_video_path) : '';
|
||
const videoUrl = video.edited_video_url || video.local_video_path || video.video_url;
|
||
const videoUrlStr = videoUrl ? String(videoUrl) : '';
|
||
const usedEdited = !!video.edited_video_url
|
||
|| (localPathStr.indexOf('/static/videos/revival_') !== -1)
|
||
|| (videoUrlStr.indexOf('/static/videos/revival_') !== -1);
|
||
console.log('[RevivalHistory] 视频URL:', videoUrl);
|
||
console.log('[RevivalHistory] usedEdited判定:', usedEdited, 'localPathStr=', localPathStr, 'videoUrlStr=', videoUrlStr);
|
||
|
||
if (!videoUrl) {
|
||
uni.showToast({
|
||
title: '视频地址不存在',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
const normalizedUrl = this.normalizeMediaUrl(videoUrl);
|
||
if (this.enablePrecache && typeof plus !== 'undefined') {
|
||
this.enqueuePrecache(normalizedUrl);
|
||
}
|
||
let targetUrl = `/pages/video-player/video-player?audioInVideo=1&id=${encodeURIComponent(video.id)}&url=${encodeURIComponent(videoUrl)}&title=${encodeURIComponent(video.name || '复活视频')}`;
|
||
// 不传入外部音频参数,播放器只播放视频本身,不做额外音频处理
|
||
console.log('[RevivalHistory] 跳转URL:', targetUrl);
|
||
|
||
// 跳转到视频播放页面
|
||
uni.navigateTo({
|
||
url: targetUrl,
|
||
success: () => {
|
||
console.log('[RevivalHistory] 跳转成功');
|
||
},
|
||
fail: (err) => {
|
||
console.error('[RevivalHistory] 跳转失败:', err);
|
||
uni.showToast({
|
||
title: '跳转失败: ' + err.errMsg,
|
||
icon: 'none'
|
||
});
|
||
}
|
||
});
|
||
},
|
||
|
||
playVideo(video) {
|
||
const localPathStr = video && video.local_video_path ? String(video.local_video_path) : '';
|
||
const videoUrl = video.edited_video_url || video.local_video_path || video.video_url;
|
||
const videoUrlStr = videoUrl ? String(videoUrl) : '';
|
||
const usedEdited = !!video.edited_video_url
|
||
|| (localPathStr.indexOf('/static/videos/revival_') !== -1)
|
||
|| (videoUrlStr.indexOf('/static/videos/revival_') !== -1);
|
||
if (!videoUrl) {
|
||
uni.showToast({
|
||
title: '视频地址不存在',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
let targetUrl = `/pages/video-player/video-player?audioInVideo=1&url=${encodeURIComponent(videoUrl)}&title=${encodeURIComponent(video.name || '复活视频')}`;
|
||
// 不传入外部音频参数,播放器只播放视频本身,不做额外音频处理
|
||
uni.navigateTo({
|
||
url: targetUrl,
|
||
fail: (err) => {
|
||
console.error('[RevivalHistory] 跳转失败:', err);
|
||
uni.showToast({
|
||
title: '播放失败,请稍后重试',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
});
|
||
},
|
||
|
||
|
||
prepareShare(video) {
|
||
this.shareTargetVideo = video || null;
|
||
},
|
||
|
||
shareVideo(video) {
|
||
console.log('[RevivalHistory] 分享视频:', video);
|
||
|
||
const videoUrl = video.edited_video_url || video.local_video_path || video.video_url;
|
||
if (!videoUrl) {
|
||
uni.showToast({
|
||
title: '视频地址不存在',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
uni.showActionSheet({
|
||
itemList: ['保存到相册', '复制链接'],
|
||
success: (res) => {
|
||
if (res.tapIndex === 0) {
|
||
// 保存到相册 - 传入完整video对象以获取音频URL
|
||
this.saveVideoToAlbum(video);
|
||
} else if (res.tapIndex === 1) {
|
||
// 复制链接 - 如果有音频,先合成再复制
|
||
this.copyVideoLink(video);
|
||
}
|
||
}
|
||
});
|
||
},
|
||
|
||
// 复制视频链接(如果有音频则先合成)
|
||
async copyVideoLink(video) {
|
||
const videoUrl = video.edited_video_url || video.local_video_path || video.video_url;
|
||
if (!videoUrl) {
|
||
uni.showToast({ title: '视频地址不存在', icon: 'none' });
|
||
return;
|
||
}
|
||
uni.setClipboardData({
|
||
data: videoUrl,
|
||
success: () => {
|
||
uni.showToast({
|
||
title: '链接已复制',
|
||
icon: 'success'
|
||
});
|
||
}
|
||
});
|
||
},
|
||
|
||
async saveVideoToAlbum(video) {
|
||
const videoUrl = video.edited_video_url || video.local_video_path || video.video_url;
|
||
|
||
console.log('[RevivalHistory] 保存视频:', videoUrl);
|
||
this.saveVideoOnly(videoUrl);
|
||
},
|
||
|
||
// 仅保存视频(不含音频)
|
||
saveVideoOnly(videoUrl) {
|
||
uni.showLoading({
|
||
title: '保存中...'
|
||
});
|
||
|
||
// 如果是本地路径,直接保存
|
||
if (videoUrl.startsWith('file://') || videoUrl.startsWith('wxfile://')) {
|
||
uni.saveVideoToPhotosAlbum({
|
||
filePath: videoUrl,
|
||
success: () => {
|
||
uni.hideLoading();
|
||
uni.showToast({
|
||
title: '保存成功',
|
||
icon: 'success'
|
||
});
|
||
},
|
||
fail: (err) => {
|
||
uni.hideLoading();
|
||
console.error('[RevivalHistory] 保存失败:', err);
|
||
if (err.errMsg.includes('auth')) {
|
||
uni.showModal({
|
||
title: '需要授权',
|
||
content: '请授权保存到相册',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
uni.openSetting();
|
||
}
|
||
}
|
||
});
|
||
} else {
|
||
uni.showToast({
|
||
title: '保存失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
}
|
||
});
|
||
} else {
|
||
// 远程URL,先下载再保存
|
||
uni.downloadFile({
|
||
url: videoUrl,
|
||
success: (res) => {
|
||
if (res.statusCode === 200) {
|
||
uni.saveVideoToPhotosAlbum({
|
||
filePath: res.tempFilePath,
|
||
success: () => {
|
||
uni.hideLoading();
|
||
uni.showToast({
|
||
title: '保存成功',
|
||
icon: 'success'
|
||
});
|
||
},
|
||
fail: (err) => {
|
||
uni.hideLoading();
|
||
console.error('[RevivalHistory] 保存失败:', err);
|
||
if (err.errMsg.includes('auth')) {
|
||
uni.showModal({
|
||
title: '需要授权',
|
||
content: '请授权保存到相册',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
uni.openSetting();
|
||
}
|
||
}
|
||
});
|
||
} else {
|
||
uni.showToast({
|
||
title: '保存失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
}
|
||
});
|
||
} else {
|
||
uni.hideLoading();
|
||
uni.showToast({
|
||
title: '下载失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
},
|
||
fail: (err) => {
|
||
uni.hideLoading();
|
||
console.error('[RevivalHistory] 下载失败:', err);
|
||
uni.showToast({
|
||
title: '下载失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
});
|
||
}
|
||
},
|
||
|
||
async deleteVideo(video) {
|
||
uni.showModal({
|
||
title: '确认删除',
|
||
content: `确定要删除视频"${video.name || '复活视频'}"吗?`,
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
const userId = uni.getStorageSync('userId') || '';
|
||
const token = uni.getStorageSync('token') || '';
|
||
|
||
uni.request({
|
||
url: `${API_BASE}/api/photo-revival/videos/${video.id}`,
|
||
method: 'DELETE',
|
||
header: {
|
||
'X-User-Id': userId,
|
||
'Authorization': token ? `Bearer ${token}` : ''
|
||
},
|
||
success: (res) => {
|
||
if (res.statusCode === 200 && res.data && res.data.success) {
|
||
uni.showToast({
|
||
title: '删除成功',
|
||
icon: 'success'
|
||
});
|
||
// 刷新列表
|
||
this.loadVideos();
|
||
} else {
|
||
uni.showToast({
|
||
title: res.data?.message || '删除失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
},
|
||
fail: (err) => {
|
||
uni.showToast({
|
||
title: '删除失败,请重试',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
});
|
||
}
|
||
}
|
||
});
|
||
},
|
||
|
||
formatTime(timestamp) {
|
||
if (!timestamp) return '';
|
||
const date = new Date(timestamp);
|
||
const year = date.getFullYear();
|
||
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 `${year}-${month}-${day} ${hours}:${minutes}`;
|
||
},
|
||
|
||
formatDuration(seconds) {
|
||
if (!seconds) return '';
|
||
const mins = Math.floor(seconds / 60);
|
||
const secs = seconds % 60;
|
||
return `${mins}:${String(secs).padStart(2, '0')}`;
|
||
}
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.history-container {
|
||
min-height: 100vh;
|
||
background: linear-gradient(180deg, #FDF8F2 0%, #F5EDE3 100%);
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
// 头部
|
||
.header {
|
||
padding: 48upx 32upx 32upx 40upx;
|
||
background: transparent;
|
||
|
||
.title {
|
||
font-size: 52upx;
|
||
font-weight: 700;
|
||
color: #8B7355;
|
||
display: block;
|
||
margin-bottom: 12upx;
|
||
letter-spacing: 3upx;
|
||
text-shadow: 0 2upx 4upx rgba(139, 115, 85, 0.1);
|
||
}
|
||
|
||
.subtitle {
|
||
font-size: 26upx;
|
||
color: #B8A898;
|
||
display: inline-block;
|
||
background: rgba(139, 115, 85, 0.08);
|
||
padding: 6upx 16upx;
|
||
border-radius: 20upx;
|
||
}
|
||
}
|
||
|
||
.video-list {
|
||
flex: 1;
|
||
padding: 0 24upx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.loading-box {
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
align-items: center;
|
||
padding: 160rpx 0;
|
||
}
|
||
|
||
.loading-text {
|
||
font-size: 28rpx;
|
||
color: #8B7355;
|
||
margin-top: 16rpx;
|
||
}
|
||
|
||
.empty-box {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
padding: 180upx 40upx;
|
||
}
|
||
|
||
.empty-icon {
|
||
font-size: 140upx;
|
||
margin-bottom: 40upx;
|
||
filter: grayscale(20%);
|
||
}
|
||
|
||
.empty-text {
|
||
font-size: 34upx;
|
||
color: #8B7355;
|
||
margin-bottom: 16upx;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.empty-hint {
|
||
font-size: 26upx;
|
||
color: #B8A898;
|
||
text-align: center;
|
||
line-height: 1.6;
|
||
background: rgba(139, 115, 85, 0.06);
|
||
padding: 16upx 32upx;
|
||
border-radius: 24upx;
|
||
}
|
||
|
||
.video-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 20upx;
|
||
padding: 0 0 40upx 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.video-card {
|
||
background: white;
|
||
border-radius: 28upx;
|
||
overflow: hidden;
|
||
box-shadow: 0 8upx 32upx rgba(139, 115, 85, 0.12);
|
||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
border: 1upx solid rgba(139, 115, 85, 0.08);
|
||
|
||
&:active {
|
||
transform: translateY(-6upx) scale(1.01);
|
||
box-shadow: 0 16upx 40upx rgba(139, 115, 85, 0.2);
|
||
}
|
||
}
|
||
|
||
.video-cover {
|
||
position: relative;
|
||
width: 100%;
|
||
padding-top: 150%;
|
||
overflow: hidden;
|
||
background: linear-gradient(135deg, #F8F4F0 0%, #EDE6DD 100%);
|
||
}
|
||
|
||
.cover-image {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
transition: transform 0.3s ease;
|
||
}
|
||
|
||
.video-card:active .cover-image {
|
||
transform: scale(1.02);
|
||
}
|
||
|
||
.play-overlay {
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
width: 88upx;
|
||
height: 88upx;
|
||
background: rgba(255, 255, 255, 0.95);
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
box-shadow: 0 8upx 24upx rgba(0, 0, 0, 0.2);
|
||
}
|
||
|
||
.play-icon {
|
||
font-size: 36upx;
|
||
color: #8B7355;
|
||
padding-left: 6upx;
|
||
}
|
||
|
||
.cover-placeholder {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.placeholder-icon {
|
||
font-size: 60upx;
|
||
color: #ccc;
|
||
}
|
||
|
||
.video-duration {
|
||
position: absolute;
|
||
bottom: 8upx;
|
||
right: 8upx;
|
||
background: rgba(0, 0, 0, 0.7);
|
||
color: white;
|
||
padding: 4upx 10upx;
|
||
border-radius: 6upx;
|
||
font-size: 20upx;
|
||
}
|
||
|
||
.cache-badge {
|
||
position: absolute;
|
||
top: 12upx;
|
||
right: 12upx;
|
||
background: linear-gradient(135deg, #8B7355 0%, #A68B5B 100%);
|
||
padding: 6upx 12upx;
|
||
border-radius: 20upx;
|
||
box-shadow: 0 2upx 8upx rgba(139, 115, 85, 0.4);
|
||
}
|
||
|
||
.cache-text {
|
||
color: white;
|
||
font-size: 18upx;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.video-info {
|
||
padding: 20upx 16upx 16upx;
|
||
background: linear-gradient(180deg, #FFFFFF 0%, #FDFBF9 100%);
|
||
}
|
||
|
||
.video-title {
|
||
font-size: 28upx;
|
||
font-weight: 600;
|
||
color: #5A4A3A;
|
||
display: -webkit-box;
|
||
-webkit-box-orient: vertical;
|
||
-webkit-line-clamp: 2;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
margin-bottom: 8upx;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.video-text {
|
||
font-size: 22upx;
|
||
color: #999;
|
||
display: -webkit-box;
|
||
-webkit-box-orient: vertical;
|
||
-webkit-line-clamp: 1;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
margin-bottom: 8upx;
|
||
}
|
||
|
||
.video-meta {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
gap: 8upx;
|
||
}
|
||
|
||
.video-time {
|
||
font-size: 20upx;
|
||
color: #B8A898;
|
||
background: rgba(139, 115, 85, 0.06);
|
||
padding: 4upx 10upx;
|
||
border-radius: 8upx;
|
||
}
|
||
|
||
.video-status {
|
||
font-size: 20upx;
|
||
color: #8B7355;
|
||
background: rgba(139, 115, 85, 0.1);
|
||
padding: 2upx 8upx;
|
||
border-radius: 6upx;
|
||
}
|
||
|
||
.video-actions {
|
||
display: flex;
|
||
border-top: 1upx solid rgba(139, 115, 85, 0.1);
|
||
background: #FDFBF9;
|
||
}
|
||
|
||
.action-btn {
|
||
flex: 1;
|
||
padding: 14upx 4upx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 6upx;
|
||
border-right: 1upx solid rgba(139, 115, 85, 0.1);
|
||
transition: all 0.2s ease;
|
||
background: transparent;
|
||
border: none;
|
||
border-radius: 0;
|
||
line-height: normal;
|
||
|
||
&::after {
|
||
border: none;
|
||
}
|
||
|
||
&:last-child {
|
||
border-right: none;
|
||
}
|
||
|
||
&:active {
|
||
background: rgba(139, 115, 85, 0.08);
|
||
transform: scale(0.96);
|
||
}
|
||
|
||
&.delete:active {
|
||
background: rgba(220, 80, 80, 0.08);
|
||
}
|
||
|
||
&.share:active {
|
||
background: rgba(139, 115, 85, 0.1);
|
||
}
|
||
|
||
&.publish:active {
|
||
background: rgba(100, 180, 100, 0.1);
|
||
}
|
||
}
|
||
|
||
.action-icon {
|
||
font-size: 26upx;
|
||
}
|
||
|
||
.action-text {
|
||
font-size: 18upx;
|
||
color: #8B7355;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.publish-mask {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.45);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 60upx;
|
||
z-index: 9999;
|
||
}
|
||
|
||
.publish-dialog {
|
||
width: 100%;
|
||
max-width: 650upx;
|
||
background: #fff;
|
||
border-radius: 28upx;
|
||
padding: 36upx;
|
||
box-shadow: 0 20upx 60upx rgba(0, 0, 0, 0.2);
|
||
}
|
||
|
||
.publish-title {
|
||
font-size: 32upx;
|
||
font-weight: 700;
|
||
color: #333;
|
||
margin-bottom: 24upx;
|
||
}
|
||
|
||
.publish-input {
|
||
width: 100%;
|
||
background: #f7f2ea;
|
||
border-radius: 20upx;
|
||
min-height: 96upx;
|
||
padding: 28upx 28upx;
|
||
font-size: 30upx;
|
||
color: #333;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.publish-actions {
|
||
display: flex;
|
||
gap: 20upx;
|
||
margin-top: 28upx;
|
||
}
|
||
|
||
.publish-btn {
|
||
flex: 1;
|
||
text-align: center;
|
||
padding: 22upx 0;
|
||
border-radius: 22upx;
|
||
font-size: 28upx;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.publish-btn.cancel {
|
||
background: #f0f0f0;
|
||
color: #333;
|
||
}
|
||
|
||
.publish-btn.confirm {
|
||
background: linear-gradient(135deg, #8B7355 0%, #6D8B8B 100%);
|
||
color: #fff;
|
||
}
|
||
</style>
|