uniapp-app/pages_app/downLoadVideo/downLoadVideo.vue
2025-11-21 17:49:39 +08:00

899 lines
21 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="navbox">
<view class="status_bar"></view>
<uni-nav-bar
left-icon="left"
title="视频缓存"
@clickLeft="goBack"
color="#8B2316"
:border="false"
backgroundColor="#eeeeee"
>
<template #right>
<view class="nav-actions">
<view class="nav-edit" @click="toggleEdit">
<text>{{ isEditMode ? "取消" : "编辑" }}</text>
</view>
</view>
</template>
</uni-nav-bar>
</view>
<view class="download-video-page">
<!-- 标签页 -->
<view class="tabs-container">
<view
class="tab-item"
:class="{ active: activeTab === 'completed' }"
@click="switchTab('completed')"
>
已完成
</view>
<view class="tab-divider"></view>
<view
class="tab-item"
:class="{ active: activeTab === 'downloading' }"
@click="switchTab('downloading')"
>
缓存中
</view>
</view>
<!-- 内容列表 -->
<scroll-view class="content-scroll" scroll-y>
<view v-if="currentList.length === 0" class="empty-state">
<text>暂无数据</text>
</view>
<view
v-for="(item, index) in currentList"
:key="item.id || index"
class="video-item"
@click="handleItemClick(item)"
>
<!-- 编辑模式下的多选框 -->
<view
v-if="isEditMode"
class="checkbox-wrapper"
@click.stop="toggleSelectItem(item)"
>
<view
class="checkbox"
:class="{ checked: isItemSelected(item) }"
>
<uni-icons
v-if="isItemSelected(item)"
type="checkmarkempty"
size="16"
color="#ffffff"
></uni-icons>
</view>
</view>
<!-- 缩略图 -->
<image
class="video-thumbnail"
:src="docUrl + item.imgpath"
mode="aspectFill"
/>
<!-- 内容信息 -->
<view class="video-info">
<view class="video-title">{{ item.name || "视频标题" }}</view>
<view class="video-author">{{ item.author || "作者" }}</view>
<!-- 时长和大小 -->
<view class="video-meta" v-if="item.status === 'completed'">
<uni-icons
type="clock"
size="12"
color="#999999"
class="meta-icon"
></uni-icons>
<text class="meta-text"><text v-if="item.duration">{{ formatDuration(item.duration) }}</text></text>
<text class="meta-size">{{ formatSize(item.size) }}</text>
</view>
<view class="video-meta video-status" v-if="item.status == 'downloading' || item.status == 'paused'">
<view class="status-icon">
<image :src="item.status == 'paused' ? playImg : pauseImg" mode="aspectFill"></image>
<text>{{ item.status == 'paused' ? '继续' : '暂停' }}</text>
</view>
<view class="box">
<!-- <view class="video-progress-bar">
<view class="video-progress-bar-inner" :style="{ width: item.progress + '%' }"></view>
</view> -->
<!-- <text>{{ item.progress + '%' }}</text> -->
<text>{{ formatSize(item.downloadSize) }} / {{ formatSize(item.totalSize) }}</text>
</view>
</view>
</view>
<!-- 编辑模式下的删除按钮 -->
<!-- <view
v-if="isEditMode"
class="delete-btn"
@click.stop="deleteItem(item, index)"
>
<text>删除</text>
</view> -->
</view>
</scroll-view>
<!-- 底部存储信息 -->
<view class="bottom-box">
<view class="storage-info">
<text
>手机存储:总空间{{ storageInfo.total }}G/剩余{{
storageInfo.available
}}G可用</text
>
</view>
<view class="footer-btn" v-if="isEditMode">
<view class="footer-btn-item select" @click="clearAll">
<text v-if="!isAllSelect">全选</text>
<text v-else>取消全选</text>
</view>
<view class="bar"></view>
<view class="footer-btn-item del" @click="delTask">
删除<text v-if="selectedItems.length > 0">({{ selectedItems.length }})</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted, nextTick } from "vue";
import downloadStore from "@/store/downloadStoreVideo.js";
import { onLoad, onShow, onUnload } from "@dcloudio/uni-app";
import playImg from "@/static/down_true.png";
import pauseImg from "@/static/down_false.png";
import navTo from "@/utils/navTo";
import docUrl from "@/utils/docUrl";
const downloadTasks = ref([]);
const unsubscribe = ref(null);
const isAllSelect = ref(false);
const selectedItems = ref([]);
// 响应式数据
const activeTab = ref("completed"); // completed: 已完成, downloading: 缓存中
const isEditMode = ref(false);
const storageInfo = ref({
total: "461.31",
available: "312.92",
});
const delTask=()=>{
if(selectedItems.value.length === 0){
uni.showToast({
title: "请选择要删除的缓存",
icon: "none",
});
return;
}
uni.showModal({
title: "提示",
content: "确定要删除选中的缓存吗?",
success: (res) => {
if (res.confirm) {
// 从后往前删除,避免索引变化问题
const idsToDelete = [...selectedItems.value];
idsToDelete.forEach(id => {
const taskIndex = downloadTasks.value.findIndex(t => t.id === id);
if (taskIndex !== -1) {
removeTask(taskIndex);
}
});
// 清空选择
selectedItems.value = [];
isAllSelect.value = false;
}
}
});
}
const clearAll=()=>{
if(!isAllSelect.value){
// 全选当前列表的所有项
selectedItems.value = currentList.value.map(item => item.id);
isAllSelect.value = true;
}else{
// 取消全选
selectedItems.value = [];
isAllSelect.value = false;
}
}
// 判断项是否被选中
const isItemSelected = (item) => {
return selectedItems.value.includes(item.id);
}
// 切换项的选中状态
const toggleSelectItem = (item) => {
const index = selectedItems.value.indexOf(item.id);
if (index > -1) {
selectedItems.value.splice(index, 1);
} else {
selectedItems.value.push(item.id);
}
// 更新全选状态
isAllSelect.value = selectedItems.value.length === currentList.value.length && currentList.value.length > 0;
}
// 计算当前显示的列表
const currentList = computed(() => {
let list = [];
if (activeTab.value === "completed") {
list = downloadTasks.value.filter((task) => task.status === "completed");
} else {
list = downloadTasks.value.filter(
(task) => task.status === "downloading" || task.status === "paused"
);
}
// 当列表变化时,更新全选状态
nextTick(() => {
if (isAllSelect.value && selectedItems.value.length !== list.length) {
isAllSelect.value = false;
}
});
return list;
});
// 方法
const goBack = () => {
uni.navigateBack({
fail() {
uni.redirectTo({
url: "/pages/index/index",
});
},
});
};
const switchTab = (tab) => {
activeTab.value = tab;
isEditMode.value = false;
// 切换标签时清空选择
selectedItems.value = [];
isAllSelect.value = false;
};
const toggleEdit = () => {
isEditMode.value = !isEditMode.value;
// 退出编辑模式时清空选择
if (!isEditMode.value) {
selectedItems.value = [];
isAllSelect.value = false;
}
};
const handleItemClick = (item) => {
if (isEditMode.value) {
// 编辑模式下,点击项切换选中状态
toggleSelectItem(item);
return;
}
// 跳转到视频详情或播放页面
if (item.status === "completed") {
uni.navigateTo({
url: `/pages_app/videoDetail/videoDetail?id=${item.id}&from=download`,
});
}else if(item.status === "downloading"){
let index=downloadTasks.value.findIndex(t => t.id === item.id);
if(index !== -1){
pauseTask(index);
}
}else if(item.status === "paused"){
let index=downloadTasks.value.findIndex(t => t.id === item.id);
if(index !== -1){
resumeTask(index);
}
}
};
const deleteItem = (item, index) => {
uni.showModal({
title: "提示",
content: "确定要删除这个缓存吗?",
success: (res) => {
if (res.confirm) {
// 从store中删除
const taskIndex = downloadTasks.value.findIndex((t) => t.id === item.id);
if (taskIndex !== -1) {
removeTask(taskIndex);
}
}
},
});
};
const formatDuration = (duration) => {
if (!duration) return "00:00:00";
// 如果duration是秒数
if (typeof duration === "number") {
const hours = Math.floor(duration / 3600);
const minutes = Math.floor((duration % 3600) / 60);
const seconds = duration % 60;
return `${String(hours).padStart(2, "0")}:${String(minutes).padStart(
2,
"0"
)}:${String(seconds).padStart(2, "0")}`;
}
// 如果已经是格式化字符串
return duration;
};
const formatSize = (size) => {
if (!size) return "0M";
// 如果size是字节数
const mb = (Number(size) / 1024 / 1024).toFixed(2);
return `${mb}M`;
// 如果已经是格式化字符串
return size;
};
onLoad(() => {
// 从store同步任务列表
syncTasksFromStore();
// 监听store变化
unsubscribe.value = downloadStore.addListener((tasks) => {
downloadTasks.value = tasks;
});
console.log(11111);
console.log(downloadTasks.value);
// 恢复正在下载的任务
resumeDownloadingTasks();
}),
onShow(() => {
// 页面显示时同步最新任务列表
syncTasksFromStore();
// 检查并恢复下载任务
resumeDownloadingTasks();
}),
onUnload(() => {
// 页面 卸载时取消监听
if (unsubscribe.value) {
unsubscribe.value();
}
});
const getStorageInfo = () => {
// 获取手机存储信息
// #ifdef APP-PLUS
if (uni.getSystemInfoSync().platform !== 'android') {
console.log('此方法仅适用于Android平台');
return;
}
// 1. 导入所需的Android类
const Environment = plus.android.importClass("android.os.Environment");
const StatFs = plus.android.importClass("android.os.StatFs");
try {
// 2. 获取内部存储的数据目录File对象
const dataDirectory = Environment.getDataDirectory(); // 这是一个Java File对象
// 3. 关键修正:使用 plus.android.invoke 调用 File 对象的 getPath 方法
const dataDirectoryPath = plus.android.invoke(dataDirectory, "getPath");
// 现在 dataDirectoryPath 是一个字符串,表示路径
// 4. 创建 StatFs 对象,传入路径字符串
const statFs = new StatFs(dataDirectoryPath);
// 5. 使用 invoke 调用 StatFs 的方法获取存储块信息
const blockSize = plus.android.invoke(statFs, "getBlockSize");
const availableBlocks = plus.android.invoke(statFs, "getAvailableBlocks");
const totalBlocks = plus.android.invoke(statFs, "getBlockCount");
// 6. 计算可用空间和总空间(单位:字节)
const availableSizeInBytes = blockSize * availableBlocks;
const totalSizeInBytes = blockSize * totalBlocks;
// 7. 转换为常用单位
const availableSizeInGB = (availableSizeInBytes / (1024 * 1024 * 1024)).toFixed(2);
const availableSizeInMB = (availableSizeInBytes / (1024 * 1024)).toFixed(2);
const totalSizeInGB = (totalSizeInBytes / (1024 * 1024 * 1024)).toFixed(2);
// console.log(`可用存储空间:${availableSizeInBytes} 字节`);
// console.log(`可用存储空间:${availableSizeInMB} MB`);
// console.log(`可用存储空间:${availableSizeInGB} GB`);
// 可以根据需要返回不同单位的值
storageInfo.value = {
total: totalSizeInGB,
available: availableSizeInGB,
};
} catch (error) {
console.error('获取存储信息时发生错误:', error);
return null;
}
// #endif
};
const syncTasksFromStore=()=> {
downloadTasks.value = downloadStore.getTasks();
};
// 添加下载任务
const addDownloadTask = (item) => {
// 使用store添加任务
const taskIndex = downloadStore.addTask(item);
// 同步任务列表
syncTasksFromStore();
// 开始下载
startDownload(taskIndex);
};
// 开始下载
const startDownload = (index) => {
const taskItem = downloadStore.getTask(index);
if (!taskItem) return;
const task = uni.downloadFile({
url: taskItem.url,
success: (res) => {
if (res.statusCode === 200) {
downloadStore.updateTask(index, {
status: "completed",
filePath: res.tempFilePath,
});
uni.showToast({
title: "下载成功",
icon: "success",
});
console.log(res);
uni.saveFile({
tempFilePath: res.tempFilePath,
success: function (res) {
uni.getSavedFileInfo({
filePath: res.savedFilePath,
success: function (res) {
downloadStore.updateTask(index, {
status: "completed",
realyPath: res.savedFilePath,
size: res.size,
});
},
});
},
});
} else {
downloadStore.updateTask(index, {
status: "failed",
});
uni.showToast({
title: "下载失败",
icon: "none",
});
}
},
fail: (err) => {
downloadStore.updateTask(index, {
status: "failed",
});
console.error("下载失败:", err);
uni.showToast({
title: "下载失败: " + (err.errMsg || "未知错误"),
icon: "none",
duration: 2000,
});
},
});
// 监听下载进度
task.onProgressUpdate((update) => {
const currentTask = downloadStore.getTask(index);
if (currentTask && currentTask.status === "downloading") {
console.log(update.progress);
downloadStore.updateTask(index, {
progress: update.progress,
downloadSize: update.totalBytesWritten,
totalSize: update.totalBytesExpectedToWrite,
});
}
});
// 保存task对象到store用于暂停/继续)
downloadStore.updateTask(index, {
task: task,
});
};
// 暂停任务
const pauseTask = (index) => {
const taskItem = downloadStore.getTask(index);
if (taskItem && taskItem.task) {
taskItem.task.abort();
downloadStore.updateTask(index, {
status: "paused",
task: null,
});
uni.showToast({
title: "已暂停",
icon: "none",
});
}
};
// 继续任务
const resumeTask = (index) => {
downloadStore.updateTask(index, {
status: "downloading",
progress: 0, // 重新开始uni.downloadFile不支持断点续传
});
startDownload(index);
};
// 删除任务
const removeTask = (index) => {
downloadStore.removeTask(index);
syncTasksFromStore();
};
// 打开文件
const openFile = (filePath) => {
// #ifdef APP-PLUS
plus.runtime.openFile(filePath);
// #endif
// #ifndef APP-PLUS
uni.showToast({
title: "请在APP中打开",
icon: "none",
});
// #endif
};
// 获取文件名
const getFileName = (url) => {
if (!url) return "未知文件";
try {
const urlObj = new URL(url);
const pathname = urlObj.pathname;
const fileName = pathname.split("/").pop();
return fileName || "下载文件";
} catch (e) {
// 如果不是完整URL尝试从路径中提取
const parts = url.split("/");
return parts[parts.length - 1] || "下载文件";
}
};
// 获取状态文本
const getStatusText = (status) => {
const statusMap = {
downloading: "下载中",
paused: "已暂停",
completed: "已完成",
failed: "下载失败",
};
return statusMap[status] || "未知";
};
// 恢复正在下载的任务
const resumeDownloadingTasks = () => {
// 使用 nextTick 确保在页面渲染后再恢复下载任务
nextTick(() => {
downloadStore.resumeDownloadingTasks((index) => {
// 重新开始下载uni.downloadFile不支持断点续传所以从0开始
console.log("恢复下载任务:", downloadStore.getTask(index)?.url);
startDownload(index);
});
});
};
onMounted(() => {
getStorageInfo();
});
</script>
<style lang="scss" scoped>
page{
background-color: #ffffff;
}
.box{
display: flex;
flex:1;
align-items: center;
margin-left: 30rpx;
text{
width: 40rpx;
font-size: 24rpx;
color:#8b2316
}
}
.download-video-page {
width: 100%;
margin-top: calc(var(--status-bar-height) + 44px);
height: calc(100vh - var(--status-bar-height) - 44px);
background-color: #ffffff;
display: flex;
flex-direction: column;
}
.footer-btn{
display: flex;
align-items: center;
justify-content: space-between;
height: 100rpx;
background-color: #ffffff;
border-top: 1px solid #f0f0f0;
display: flex;
justify-content: center;
.select{
flex:1;
height: 100rpx;
display: flex;
align-items: center;
justify-content: center;
color: #333;
}
.del{
height: 100rpx;
flex:1;
color:red;
display: flex;
align-items: center;
justify-content: center;
}
.bar{
width: 1px;
height: 40rpx;
background-color: #999;
}
}
.video-progress-bar {
flex:1;
height: 10rpx;
background-color: #f0f0f0;
border-radius: 5px;
}
.video-progress-bar-inner {
height: 10rpx;
background-color: #8b2316;
border-radius: 5px;
}
.nav-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 44px;
padding: 0 15px;
background-color: #ffffff;
position: relative;
}
.nav-back {
width: 40px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
}
.back-icon {
font-size: 28px;
color: #8b2316;
font-weight: bold;
line-height: 1;
}
.nav-title {
position: absolute;
left: 50%;
transform: translateX(-50%);
font-size: 18px;
font-weight: 500;
color: #8b2316;
}
.nav-edit {
width: 40px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
color: #333333;
}
.tabs-container {
display: flex;
align-items: center;
height: 50px;
background-color: #ffffff;
border-bottom: 1px solid #f0f0f0;
position: sticky;
top: calc(var(--status-bar-height) + 44px);
z-index: 100;
}
.tab-item {
flex: 1;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
color: #999999;
position: relative;
&.active {
color: #8b2316;
font-weight: 500;
}
}
.tab-divider {
width: 1px;
height: 20px;
background-color: #e0e0e0;
}
.content-scroll {
flex: 1;
overflow-y: auto;
padding-bottom: 200rpx;
background-color: #ffffff;
}
.empty-state {
display: flex;
align-items: center;
justify-content: center;
padding: 100px 0;
color: #999999;
font-size: 14px;
}
.video-item {
display: flex;
align-items: center;
padding: 15px;
border-bottom: 1px solid #f0f0f0;
position: relative;
}
.video-thumbnail {
width: 120px;
height: 80px;
border-radius: 4px;
background-color: #f5f5f5;
margin-right: 12px;
flex-shrink: 0;
}
.video-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
min-height: 80px;
}
.video-title {
font-size: 15px;
color: #333333;
line-height: 1.4;
margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
}
.video-author {
font-size: 13px;
color: #666666;
margin-bottom: 8px;
}
.video-meta {
display: flex;
align-items: center;
font-size: 12px;
color: #999999;
}
.video-status{
display: flex;
align-items: center;
margin-right: 20rpx;
justify-content: space-between;
.status-icon{
display: flex;
align-items: center;
white-space: nowrap;
justify-content: center;
image{
width:30rpx;
height: 29rpx;
margin-right: 10rpx;
}
text{
white-space: nowrap;
font-size: 24rpx;
color: #999999;
}
}
}
.meta-icon {
margin-right: 4px;
display: inline-flex;
align-items: center;
}
.meta-text {
margin-right: 12px;
}
.meta-size {
margin-left: auto;
}
.checkbox-wrapper {
margin-right: 12px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.checkbox {
width: 20px;
height: 20px;
border: 1px solid #d0d0d0;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background-color: #ffffff;
transition: all 0.2s;
&.checked {
background-color: #8b2316;
border-color: #8b2316;
}
}
.delete-btn {
position: absolute;
right: 15px;
top: 50%;
transform: translateY(-50%);
padding: 8px 16px;
background-color: #ff4444;
color: #ffffff;
border-radius: 4px;
font-size: 14px;
}
.bottom-box {
position: fixed;
bottom: 0;
left: 0;
right: 0;
width: 100%;
background-color: #ffffff;
z-index: 100;
}
.storage-info {
padding: 12px 15px;
background-color: #f8f8f8;
border-top: 1px solid #f0f0f0;
font-size: 12px;
color: #666666;
text-align: center;
}
</style>