ai-clone/frontend-ai/pages/revival/revival-history.vue
2026-03-06 18:05:51 +08:00

1165 lines
28 KiB
Vue
Raw 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 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>
<!-- AI生成提示标签 -->
<view class="ai-tag">
<text class="ai-tag-text">AI生成</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;
}
/* 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;
}
.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>