2026-03-05 14:29:21 +08:00
|
|
|
|
<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>
|
2026-03-06 18:05:51 +08:00
|
|
|
|
<!-- AI生成提示标签 -->
|
|
|
|
|
|
<view class="ai-tag">
|
|
|
|
|
|
<text class="ai-tag-text">AI生成</text>
|
|
|
|
|
|
</view>
|
2026-03-05 14:29:21 +08:00
|
|
|
|
</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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-06 18:05:51 +08:00
|
|
|
|
/* AI生成提示标签 */
|
|
|
|
|
|
.ai-tag {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 12upx;
|
|
|
|
|
|
left: 12upx;
|
|
|
|
|
|
padding: 6upx 16upx;
|
|
|
|
|
|
background: rgba(255, 165, 0, 0.85);
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
font-size: 20upx;
|
|
|
|
|
|
border-radius: 20upx;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
z-index: 10;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ai-tag-text {
|
|
|
|
|
|
font-size: 20upx;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-05 14:29:21 +08:00
|
|
|
|
.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>
|