11.21提交
This commit is contained in:
parent
6b37f0899d
commit
91726b87c5
@ -651,6 +651,9 @@ const api = {
|
||||
patientVideoByJingHuaNew(data){
|
||||
return request('/expertAPI/patientVideoByJingHuaNew', data, 'post', false);
|
||||
},
|
||||
useWelfareNum(data){
|
||||
return request('/expertAPI/useWelfareNum', data, 'post', false);
|
||||
},
|
||||
}
|
||||
|
||||
export default api
|
||||
21
pages.json
21
pages.json
@ -281,6 +281,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "reply/reply",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarRightButton":{ "hide": true},
|
||||
"navigationBarTitleText": "回复",
|
||||
"app": {
|
||||
"bounce": "none"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "video/video",
|
||||
"style": {
|
||||
@ -292,6 +303,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "downLoadVideo/downLoadVideo",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "uni-app分页",
|
||||
"app": {
|
||||
"bounce": "none"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "patientVideo/patientVideo",
|
||||
"style": {
|
||||
|
||||
898
pages_app/downLoadVideo/downLoadVideo.vue
Normal file
898
pages_app/downLoadVideo/downLoadVideo.vue
Normal file
@ -0,0 +1,898 @@
|
||||
<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>
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
backgroundColor="#eeeeee"
|
||||
>
|
||||
<template #right>
|
||||
<view class="nav-actions">
|
||||
<view class="nav-actions" v-if="from != 'download'">
|
||||
<view class="collect-img" @click="shareToggle">
|
||||
<image class="share-img-icon" :src="shareIcon" mode="aspectFill" />
|
||||
</view>
|
||||
@ -26,35 +26,42 @@
|
||||
</uni-nav-bar>
|
||||
</view>
|
||||
|
||||
<video
|
||||
<!-- <video
|
||||
v-if="showVideo"
|
||||
class="player-wrapper"
|
||||
:style="{width: videoWidth + 'px'}"
|
||||
:src="videoSrc"
|
||||
controls
|
||||
object-fit="contain"
|
||||
:autoplay="false"
|
||||
></video>
|
||||
<!-- 标签切换 -->
|
||||
<cover-view class="tabs">
|
||||
<cover-view
|
||||
class="tab"
|
||||
:class="{ active: activeTab === 'info' }"
|
||||
@click="switchTab('info')"
|
||||
>
|
||||
视频简介
|
||||
<view-cover class="tab-line" v-if="activeTab === 'info'"></view-cover>
|
||||
</cover-view>
|
||||
<cover-view
|
||||
class="tab"
|
||||
:class="{ active: activeTab === 'comment' }"
|
||||
@click="switchTab('comment')"
|
||||
>
|
||||
评论
|
||||
<view-cover class="tab-line" v-if="activeTab === 'comment'"></view-cover>
|
||||
</cover-view
|
||||
>
|
||||
</cover-view>
|
||||
<view class="video-detail-page">
|
||||
></video> -->
|
||||
<sunny-video
|
||||
v-if="showVideo"
|
||||
class="player-wrapper"
|
||||
:src="videoSrc"
|
||||
:videoHeight="220"
|
||||
:seekTime="0"
|
||||
/>
|
||||
<!-- 标签切换 -->
|
||||
<cover-view class="tabs" v-if="from != 'download'">
|
||||
<cover-view
|
||||
class="tab"
|
||||
:class="{ active: activeTab === 'info' }"
|
||||
@click="switchTab('info')"
|
||||
>
|
||||
视频简介
|
||||
<view-cover class="tab-line" v-if="activeTab === 'info'"></view-cover>
|
||||
</cover-view>
|
||||
<cover-view
|
||||
class="tab"
|
||||
:class="{ active: activeTab === 'comment' }"
|
||||
@click="switchTab('comment')"
|
||||
>
|
||||
评论
|
||||
<view-cover class="tab-line" v-if="activeTab === 'comment'"></view-cover>
|
||||
</cover-view>
|
||||
</cover-view>
|
||||
<view class="video-detail-page" v-if="from != 'download'">
|
||||
<!-- <scroll-view
|
||||
class="content-scroll"
|
||||
scroll-y
|
||||
@ -115,11 +122,20 @@
|
||||
</view>
|
||||
|
||||
<!-- 底部区域:信息页为下载条,评论页为上传图片+输入+发送 -->
|
||||
<view v-if="activeTab === 'info'" class="bottom-download" @click="onDownload">
|
||||
<text class="download-text" v-if="!hasDownload">点击下载<text v-if="!isVideoDownloadRecord">(限时<text class="discount">5</text>折,仅需{{videoInfo.point}}积分)</text></text>
|
||||
<text class="download-text" v-else>查看缓存</text>
|
||||
</view>
|
||||
<view v-if="activeTab !== 'info'" class="bottom-comment">
|
||||
<view v-if="activeTab === 'info' && from != 'download'" class="bottom-download" @click="onDownload">
|
||||
<text class="download-text" v-if="!hasDownload"
|
||||
>点击下载<text v-if="!isVideoDownloadRecord && welfareNum <= 0"
|
||||
>(限时<text class="discount">5</text>折,仅需{{
|
||||
videoInfo.point
|
||||
}}积分)</text
|
||||
>
|
||||
<text v-if="!isVideoDownloadRecord && welfareNum > 0">(剩余免费下载{{ welfareNum }}次数)</text>
|
||||
</text
|
||||
>
|
||||
<text class="download-text" v-else-if="downLoadStatus === 'loading'">视频缓存中</text>
|
||||
<text class="download-text" v-else-if="downLoadStatus === 'completed'">查看缓存</text>
|
||||
</view>
|
||||
<view v-if="activeTab !== 'info' && from != 'download'" class="bottom-comment">
|
||||
<input
|
||||
class="comment-input"
|
||||
v-model="commentText"
|
||||
@ -148,7 +164,7 @@
|
||||
@confirm="notEnoughConfirm"
|
||||
></unidialog>
|
||||
<!-- 分享弹窗 -->
|
||||
<uni-popup ref="shareRef" type="bottom" safeArea backgroundColor="#fff" >
|
||||
<uni-popup ref="shareRef" type="bottom" safeArea backgroundColor="#fff">
|
||||
<view class="share-popup">
|
||||
<view class="share-title">分享到</view>
|
||||
<view class="share-content">
|
||||
@ -179,7 +195,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref,nextTick } from "vue";
|
||||
import { ref, nextTick } from "vue";
|
||||
import uniVideo from "@/components/uniVideo/uniVideo.vue";
|
||||
import { onLoad, onShow } from "@dcloudio/uni-app";
|
||||
import unidialog from "@/components/dialog/dialog.vue";
|
||||
@ -207,10 +223,106 @@ const welfareNum = ref(0);
|
||||
const showVideo = ref(false);
|
||||
const videoWidth = ref(375);
|
||||
const hasDownload = ref(false);
|
||||
|
||||
let downList = uni.getStorageSync('downLoadVideo') || [];
|
||||
const downLoadStatus = ref('start');
|
||||
let downList = uni.getStorageSync("downLoadVideo") || [];
|
||||
const downLoadList = ref(downList);
|
||||
//import DomVideoPlayer from 'uniapp-video-player'
|
||||
import sunnyVideo from "@/uni_modules/sunny-video/components/sunny-video/sunny-video.vue";
|
||||
import downloadStore from "@/store/downloadStoreVideo.js";
|
||||
const downloadTasks = ref([]);
|
||||
// 添加下载任务
|
||||
const addDownloadTask = (item) => {
|
||||
// 使用store添加任务
|
||||
const taskIndex = downloadStore.addTask(item);
|
||||
// 同步任务列表
|
||||
syncTasksFromStore();
|
||||
// 开始下载
|
||||
startDownload(taskIndex);
|
||||
};
|
||||
// 开始下载
|
||||
const startDownload = (index) => {
|
||||
const taskItem = downloadStore.getTask(index);
|
||||
if (!taskItem) return;
|
||||
hasDownload.value = true;
|
||||
downLoadStatus.value = 'loading';
|
||||
const task = uni.downloadFile({
|
||||
url: taskItem.url,
|
||||
success: (res1) => {
|
||||
//console.log("res1:"+JSON.stringify(res1));
|
||||
if (res1.statusCode === 200) {
|
||||
downloadStore.updateTask(index, {
|
||||
status: "completed",
|
||||
filePath: res1.tempFilePath,
|
||||
});
|
||||
downLoadStatus.value = 'completed';
|
||||
uni.showToast({
|
||||
title: "下载成功",
|
||||
icon: "none",
|
||||
});
|
||||
downLoadStatus.value = 'completed';
|
||||
|
||||
uni.saveFile({
|
||||
tempFilePath: res1.tempFilePath,
|
||||
success: function (res2) {
|
||||
console.log("res2:"+JSON.stringify(res2));
|
||||
uni.getSavedFileInfo({
|
||||
filePath: res2.savedFilePath,
|
||||
success: function (res) {
|
||||
console.log("res:"+JSON.stringify(res));
|
||||
console.log("size:"+res.size);
|
||||
downloadStore.updateTask(index, {
|
||||
status: "completed",
|
||||
localPath: res2.savedFilePath,
|
||||
size: res.size,
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
} else {
|
||||
downloadStore.updateTask(index, {
|
||||
status: "failed",
|
||||
});
|
||||
uni.showToast({
|
||||
title: "下载失败",
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
downloadStore.updateTask(index, {
|
||||
status: "failed",
|
||||
});
|
||||
uni.showToast({
|
||||
title: "下载失败: " + (err.errMsg || "未知错误"),
|
||||
icon: "none",
|
||||
duration: 2000,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// 监听下载进度
|
||||
task.onProgressUpdate((update) => {
|
||||
const currentTask = downloadStore.getTask(index);
|
||||
if (currentTask && currentTask.status === "downloading") {
|
||||
downloadStore.updateTask(index, {
|
||||
progress: update.progress,
|
||||
downloadSize: update.totalBytesWritten,
|
||||
totalSize: update.totalBytesExpectedToWrite,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 保存task对象到store(用于暂停/继续)
|
||||
downloadStore.updateTask(index, {
|
||||
task: task,
|
||||
});
|
||||
};
|
||||
|
||||
const syncTasksFromStore = () => {
|
||||
downloadTasks.value = downloadStore.getTasks();
|
||||
};
|
||||
|
||||
const notEnoughConfirm = () => {
|
||||
notEnoughVisible.value = false;
|
||||
navTo({
|
||||
@ -231,7 +343,18 @@ const toCollection = () => {
|
||||
const networkConfirm = () => {
|
||||
networkVisible.value = false;
|
||||
pointVisible.value = true;
|
||||
downAndSave(videoSrc.value,video_uuid.value);
|
||||
if(welfareNum.value > 0) {
|
||||
useWelfareNum();
|
||||
}else{
|
||||
addDownloadTask({
|
||||
url: videoSrc.value,
|
||||
id: video_uuid.value,
|
||||
imgpath: videoInfo.value.imgpath,
|
||||
author: videoInfo.value.public_name,
|
||||
name: videoInfo.value.name,
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
const addVideoWatchRecord = async () => {
|
||||
const res = await api.addVideoWatchRecord({
|
||||
@ -241,25 +364,54 @@ const addVideoWatchRecord = async () => {
|
||||
}
|
||||
};
|
||||
const onLoadedmetadata = (e) => {
|
||||
un.showModal({
|
||||
content: '加载完成:'+JSON.stringify(e),
|
||||
showCancel: false
|
||||
})
|
||||
un.showModal({
|
||||
content: "加载完成:" + JSON.stringify(e),
|
||||
showCancel: false,
|
||||
});
|
||||
};
|
||||
const onVideoError = (e) => {
|
||||
uni.showModal({
|
||||
content: JSON.stringify(e),
|
||||
showCancel: false
|
||||
})
|
||||
content: JSON.stringify(e),
|
||||
showCancel: false,
|
||||
});
|
||||
};
|
||||
// 接收页面参数
|
||||
const pageParams = ref({});
|
||||
const useWelfareNum = async () => {
|
||||
const res = await api.useWelfareNum({
|
||||
type: 1,
|
||||
other_uuid:video_uuid.value,
|
||||
});
|
||||
if (res.code == 1) {
|
||||
uni.showToast({ title: "使用次数成功", icon: "none" });
|
||||
welfareNum.value--;
|
||||
addDownloadTask({
|
||||
url: videoSrc.value,
|
||||
id: video_uuid.value,
|
||||
imgpath: videoInfo.value.imgpath,
|
||||
author: videoInfo.value.public_name,
|
||||
name: videoInfo.value.name,
|
||||
});
|
||||
}
|
||||
};
|
||||
const videoDetail = async () => {
|
||||
const res = await api.videoDetail({ video_uuid: video_uuid.value });
|
||||
if (res.code == 200) {
|
||||
const userInfo = uni.getStorageSync("userInfo");
|
||||
videoInfo.value = res.video;
|
||||
hasDownload.value = downLoadList.value.includes(video_uuid.value);
|
||||
downloadTasks.value.forEach(item => {
|
||||
if (item.id == video_uuid.value) {
|
||||
uni.getSavedFileInfo({
|
||||
filePath: item.localPath, //仅做示例用,非真正的文件路径
|
||||
success: function (res) {
|
||||
hasDownload.value = true;
|
||||
downLoadStatus.value = 'completed';
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
});
|
||||
//hasDownload.value = downLoadList.value.includes(video_uuid.value);
|
||||
let vid = res.video.polyv_uuid;
|
||||
let uuid = vid.substring(0, 10);
|
||||
let index = vid.lastIndexOf("_");
|
||||
@ -300,27 +452,9 @@ const VideoDownloadRecord = async () => {
|
||||
const res = await api.isVideoDownloadRecord({ video_uuid: video_uuid.value });
|
||||
if (res.code == 200) {
|
||||
isVideoDownloadRecord.value = res.result == 0 ? false : true;
|
||||
console.log(isVideoDownloadRecord.value);
|
||||
}
|
||||
};
|
||||
const downAndSave = (url,uuid) => {
|
||||
|
||||
//let url='https://view.xdocin.com/view?src='+encodeURIComponent(pdfUrl);
|
||||
uni.downloadFile({
|
||||
url: url,
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
let index = downLoadList.value.find(cell => uuid == cell);
|
||||
if (!index) {
|
||||
downLoadList.value.push(item.uuid);
|
||||
uni.setStorageSync('downLoadVideo', downLoadList.value);
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
const getWelfareNum = async () => {
|
||||
const res = await api.getWelfareNum({
|
||||
type: 1,
|
||||
@ -347,13 +481,34 @@ const videoCommentListV2 = async () => {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
// 恢复正在下载的任务
|
||||
const resumeDownloadingTasks = () => {
|
||||
// 使用 nextTick 确保在页面渲染后再恢复下载任务
|
||||
nextTick(() => {
|
||||
downloadStore.resumeDownloadingTasks((index) => {
|
||||
// 重新开始下载(uni.downloadFile不支持断点续传,所以从0开始)
|
||||
console.log("恢复下载任务:", downloadStore.getTask(index)?.url);
|
||||
startDownload(index);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
onShow(() => {
|
||||
hasDownload.value = false;
|
||||
downLoadStatus.value = 'start';
|
||||
syncTasksFromStore();
|
||||
// 监听store变化
|
||||
downloadStore.addListener((tasks) => {
|
||||
downloadTasks.value = tasks;
|
||||
});
|
||||
// 恢复正在下载的任务
|
||||
resumeDownloadingTasks();
|
||||
if (activeTab.value == "comment") {
|
||||
videoCommentListV2();
|
||||
}
|
||||
videoDetail();
|
||||
uni.hideLoading();
|
||||
|
||||
|
||||
});
|
||||
|
||||
const shareRef = ref();
|
||||
@ -381,10 +536,10 @@ const shareToWechat = () => {
|
||||
href: shareLink.value,
|
||||
imageUrl: docUrl + videoInfo.value.imgpath,
|
||||
success: function (res) {
|
||||
console.log("success:" + JSON.stringify(res));
|
||||
//console.log("success:" + JSON.stringify(res));
|
||||
},
|
||||
fail: function (err) {
|
||||
console.log("fail:" + JSON.stringify(err));
|
||||
//console.log("fail:" + JSON.stringify(err));
|
||||
},
|
||||
});
|
||||
// #endif
|
||||
@ -449,10 +604,10 @@ const shareToMoments = () => {
|
||||
href: shareLink.value,
|
||||
imageUrl: docUrl + videoInfo.value.imgpath,
|
||||
success: function (res) {
|
||||
console.log("success:" + JSON.stringify(res));
|
||||
//console.log("success:" + JSON.stringify(res));
|
||||
},
|
||||
fail: function (err) {
|
||||
console.log("fail:" + JSON.stringify(err));
|
||||
//console.log("fail:" + JSON.stringify(err));
|
||||
},
|
||||
});
|
||||
// #endif
|
||||
@ -491,10 +646,10 @@ const shareToWeibo = () => {
|
||||
href: shareLink.value,
|
||||
imageUrl: logoImg,
|
||||
success: function (res) {
|
||||
console.log("分享成功");
|
||||
//console.log("分享成功");
|
||||
},
|
||||
fail: function (err) {
|
||||
console.log("fail:" + JSON.stringify(err));
|
||||
//console.log("fail:" + JSON.stringify(err));
|
||||
},
|
||||
});
|
||||
// plus.share.sendWithSystem({
|
||||
@ -529,19 +684,31 @@ const shareToWeibo = () => {
|
||||
|
||||
closeShare();
|
||||
};
|
||||
const from = ref('');
|
||||
// 使用uni-app的onLoad生命周期
|
||||
onLoad((options) => {
|
||||
console.log(options);
|
||||
uni.getSystemInfo({
|
||||
success: function (res) {
|
||||
videoWidth.value = res.windowWidth; // 窗口宽度
|
||||
|
||||
}
|
||||
});
|
||||
success: function (res) {
|
||||
videoWidth.value = res.windowWidth; // 窗口宽度
|
||||
},
|
||||
});
|
||||
video_uuid.value = options.id;
|
||||
from.value = options.from;
|
||||
addVideoWatchRecord();
|
||||
VideoDownloadRecord();
|
||||
getWelfareNum();
|
||||
// 从store同步任务列表
|
||||
syncTasksFromStore();
|
||||
// 监听store变化
|
||||
downloadStore.addListener((tasks) => {
|
||||
downloadTasks.value = tasks;
|
||||
|
||||
});
|
||||
// 恢复正在下载的任务
|
||||
resumeDownloadingTasks();
|
||||
// navTo({
|
||||
// url: "/pages_app/downLoadVideo/downLoadVideo",
|
||||
// });
|
||||
});
|
||||
|
||||
const videoSrc = ref("");
|
||||
@ -562,8 +729,12 @@ const switchTab = (tab) => {
|
||||
const payVideoDownload = async () => {
|
||||
const res = await api.payVideoDownload({ video_uuid: video_uuid.value });
|
||||
if (res.code == 200) {
|
||||
navTo({
|
||||
url: "/pages_app/myDownLoad/myDownLoad",
|
||||
addDownloadTask({
|
||||
url: videoSrc.value,
|
||||
id: video_uuid.value,
|
||||
imgpath: videoInfo.value.imgpath,
|
||||
author: videoInfo.value.public_name,
|
||||
name: videoInfo.value.name,
|
||||
});
|
||||
} else if (res.code == 106) {
|
||||
notEnoughVisible.value = true;
|
||||
@ -572,31 +743,47 @@ const payVideoDownload = async () => {
|
||||
};
|
||||
|
||||
const onDownload = () => {
|
||||
console.log(isVideoDownloadRecord.value);
|
||||
if (!isVideoDownloadRecord.value) {
|
||||
pointContent.value = `当前需要${videoInfo.value.point}积分兑换,若删除可以再次缓存`;
|
||||
uni.getNetworkType({
|
||||
success: function (res) {
|
||||
console.log(res);
|
||||
if (res.networkType != "none") {
|
||||
networkVisible.value = true;
|
||||
networkContent.value = `当前为${res.networkType}网络,确定下载?`;
|
||||
if(welfareNum.value <= 0) {
|
||||
pointContent.value = `当前需要${videoInfo.value.point}积分兑换,若删除可以再次缓存`;
|
||||
uni.getNetworkType({
|
||||
success: function (res) {
|
||||
if (res.networkType != "none") {
|
||||
networkVisible.value = true;
|
||||
networkContent.value = `当前为${res.networkType}网络,确定下载?`;
|
||||
} else {
|
||||
uni.showToast({ title: "当前未联网,请检查网络", icon: "none" });
|
||||
}
|
||||
},
|
||||
});
|
||||
}else{
|
||||
uni.getNetworkType({
|
||||
success: function (res) {
|
||||
if (res.networkType != "none") {
|
||||
networkVisible.value = true;
|
||||
networkContent.value = `当前为${res.networkType}网络,确定下载?`;
|
||||
} else {
|
||||
uni.showToast({ title: "当前未联网,请检查网络", icon: "none" });
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log(11111);
|
||||
} else {
|
||||
uni.showToast({ title: "当前未联网,请检查网络", icon: "none" });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
} else {
|
||||
if(hasDownload.value) {
|
||||
navTo({
|
||||
url: "/pages_app/myDownLoad/myDownLoad",
|
||||
if (hasDownload.value) {
|
||||
navTo({
|
||||
url: "/pages_app/downLoadVideo/downLoadVideo",
|
||||
});
|
||||
} else {
|
||||
downAndSave(videoSrc.value,video_uuid.value);
|
||||
addDownloadTask({
|
||||
url: videoSrc.value,
|
||||
id: video_uuid.value,
|
||||
imgpath: videoInfo.value.imgpath,
|
||||
author: videoInfo.value.public_name,
|
||||
name: videoInfo.value.name,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
@ -674,13 +861,13 @@ $theme-color: #8b2316;
|
||||
.collect-img {
|
||||
/* margin-top:50rpx; */
|
||||
}
|
||||
.navbox{
|
||||
:deep(.uni-navbar__header-btns-right){
|
||||
width: 100px!important;
|
||||
.navbox {
|
||||
:deep(.uni-navbar__header-btns-right) {
|
||||
width: 100px !important;
|
||||
}
|
||||
:deep(.uni-navbar__header-btns-left){
|
||||
min-width: 100px;
|
||||
width: 100px!important;
|
||||
:deep(.uni-navbar__header-btns-left) {
|
||||
min-width: 100px;
|
||||
width: 100px !important;
|
||||
}
|
||||
}
|
||||
.share-img-icon {
|
||||
@ -708,11 +895,10 @@ $theme-color: #8b2316;
|
||||
|
||||
.player-wrapper {
|
||||
background: #fff;
|
||||
position: fixed;
|
||||
top:calc(var(--status-bar-height) + 44px);
|
||||
left: 0;
|
||||
z-index:0;
|
||||
height: 220px!important;
|
||||
position: relative;
|
||||
top: calc(var(--status-bar-height) + 44px);
|
||||
z-index: 0;
|
||||
|
||||
overflow: hidden;
|
||||
}
|
||||
.share-img {
|
||||
@ -733,7 +919,7 @@ $theme-color: #8b2316;
|
||||
z-index: 99;
|
||||
|
||||
.tab {
|
||||
width:120rpx;
|
||||
width: 120rpx;
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@ -741,19 +927,20 @@ $theme-color: #8b2316;
|
||||
padding: 16rpx 0 24rpx;
|
||||
color: $text-secondary;
|
||||
font-size: 28rpx;
|
||||
.tab-line{
|
||||
.tab-line {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 64rpx;
|
||||
height: 6rpx;
|
||||
z-index: 2;
|
||||
border-radius: 6rpx;
|
||||
background: $theme-color;
|
||||
}
|
||||
&:nth-child(2){
|
||||
margin-left: 200rpx;
|
||||
}
|
||||
&:nth-child(2) {
|
||||
margin-left: 200rpx;
|
||||
}
|
||||
&.active {
|
||||
color: $theme-color;
|
||||
}
|
||||
@ -771,15 +958,15 @@ $theme-color: #8b2316;
|
||||
// }
|
||||
}
|
||||
}
|
||||
.video-detail-page{
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
// padding: 24rpx 28rpx 40rpx;
|
||||
top: calc(var(--status-bar-height) + 44px + 220px + 88rpx);
|
||||
height: calc(100vh - var(--status-bar-height) - 44px - 220px - 90rpx);
|
||||
overflow-y: scroll;
|
||||
// background: red;
|
||||
.video-detail-page {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
// padding: 24rpx 28rpx 40rpx;
|
||||
top: calc(var(--status-bar-height) + 44px + 220px + 88rpx);
|
||||
height: calc(100vh - var(--status-bar-height) - 44px - 220px - 90rpx);
|
||||
overflow-y: scroll;
|
||||
// background: red;
|
||||
}
|
||||
.intro {
|
||||
// background: #fff;
|
||||
|
||||
BIN
static/down_false.png
Normal file
BIN
static/down_false.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
BIN
static/down_true.png
Normal file
BIN
static/down_true.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
149
store/downloadStoreVideo.js
Normal file
149
store/downloadStoreVideo.js
Normal file
@ -0,0 +1,149 @@
|
||||
// 下载任务全局状态管理
|
||||
class DownloadStore {
|
||||
constructor() {
|
||||
let tasks = uni.getStorageSync('downloadVideoTasks');
|
||||
this.tasks = tasks || [] ; // 下载任务列表
|
||||
this.listeners = []; // 监听器列表
|
||||
}
|
||||
|
||||
// 添加任务
|
||||
addTask(data) {
|
||||
const taskIndex = this.tasks.length;
|
||||
const taskItem = {
|
||||
id: Date.now() + Math.random(), // 唯一ID
|
||||
url: '',
|
||||
status: 'downloading', // downloading, paused, completed, failed
|
||||
progress: 0,
|
||||
totalSize:0,
|
||||
downloadSize:0,
|
||||
task: null,
|
||||
filePath: '',
|
||||
imgpath:'',
|
||||
author:'',
|
||||
name:'',
|
||||
duration:'',
|
||||
size:'',
|
||||
createTime: Date.now(),
|
||||
localPath:''
|
||||
};
|
||||
Object.assign(taskItem, data);
|
||||
this.tasks.push(taskItem);
|
||||
this.notifyListeners();
|
||||
this.saveToStorage();
|
||||
|
||||
return taskIndex;
|
||||
}
|
||||
|
||||
// 更新任务
|
||||
updateTask(index, updates) {
|
||||
if (this.tasks[index]) {
|
||||
Object.assign(this.tasks[index], updates);
|
||||
this.notifyListeners();
|
||||
this.saveToStorage();
|
||||
}
|
||||
}
|
||||
|
||||
// 删除任务
|
||||
removeTask(index) {
|
||||
const taskItem = this.tasks[index];
|
||||
if (taskItem && taskItem.task && taskItem.status === 'downloading') {
|
||||
taskItem.task.abort();
|
||||
}
|
||||
this.tasks.splice(index, 1);
|
||||
this.notifyListeners();
|
||||
this.saveToStorage();
|
||||
}
|
||||
|
||||
// 获取所有任务
|
||||
getTasks() {
|
||||
return this.tasks;
|
||||
}
|
||||
|
||||
// 获取任务
|
||||
getTask(index) {
|
||||
return this.tasks[index];
|
||||
}
|
||||
|
||||
// 添加监听器
|
||||
addListener(callback) {
|
||||
this.listeners.push(callback);
|
||||
// 返回取消监听的函数
|
||||
return () => {
|
||||
const index = this.listeners.indexOf(callback);
|
||||
if (index > -1) {
|
||||
this.listeners.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 通知所有监听器
|
||||
notifyListeners() {
|
||||
// 创建新数组引用,确保Vue能检测到变化
|
||||
const tasksCopy = [...this.tasks];
|
||||
this.listeners.forEach(callback => {
|
||||
try {
|
||||
callback(tasksCopy);
|
||||
} catch (e) {
|
||||
console.error('DownloadStore listener error:', e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 保存到本地存储
|
||||
saveToStorage() {
|
||||
// 只保存基本信息,不保存task对象
|
||||
const tasksToSave = this.tasks.map(item => ({
|
||||
id: item.id,
|
||||
url: item.url,
|
||||
status: item.status,
|
||||
progress: item.progress,
|
||||
filePath: item.filePath,
|
||||
createTime: item.createTime,
|
||||
imgpath:item.imgpath,
|
||||
author:item.author,
|
||||
name:item.name,
|
||||
duration:item.duration,
|
||||
size:item.size,
|
||||
totalSize:item.totalSize,
|
||||
downloadSize:item.downloadSize,
|
||||
localPath:item.localPath,
|
||||
}));
|
||||
uni.setStorageSync('downloadVideoTasks', tasksToSave);
|
||||
}
|
||||
|
||||
// 从本地存储加载
|
||||
loadFromStorage() {
|
||||
const savedTasks = uni.getStorageSync('downloadVideoTasks');
|
||||
if (savedTasks && Array.isArray(savedTasks)) {
|
||||
this.tasks = savedTasks.map(item => ({
|
||||
...item,
|
||||
task: null // task对象无法序列化,需要重新创建
|
||||
}));
|
||||
this.notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// 恢复下载中的任务
|
||||
resumeDownloadingTasks(startDownloadCallback) {
|
||||
this.tasks.forEach((taskItem, index) => {
|
||||
// 如果任务状态是下载中,但task对象为null,说明需要重新创建下载任务
|
||||
if (taskItem.status === 'downloading' && !taskItem.task) {
|
||||
if (typeof startDownloadCallback === 'function') {
|
||||
startDownloadCallback(index);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 创建单例
|
||||
const downloadStore = new DownloadStore();
|
||||
|
||||
// 在应用启动时加载存储的任务
|
||||
// #ifndef VUE3
|
||||
if (typeof Vue !== 'undefined') {
|
||||
Vue.prototype.$downloadStore = downloadStore;
|
||||
}
|
||||
// #endif
|
||||
|
||||
export default downloadStore;
|
||||
@ -66,5 +66,6 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
27
uni_modules/sunny-video/changelog.md
Normal file
27
uni_modules/sunny-video/changelog.md
Normal file
@ -0,0 +1,27 @@
|
||||
## 2.0.0(2024-09-22)
|
||||
`重构`代码,使用了`composition-api`;`修复`解决个别情况下样式出现错乱问题
|
||||
## 1.2.1(2024-08-06)
|
||||
`修复` 多个视频同一页面首次点击会同步播放问题
|
||||
## 1.2.0(2024-03-16)
|
||||
`新增`自动跳转到历史位置及提示
|
||||
## 1.1.8(2024-03-05)
|
||||
`新增`多个实例方法
|
||||
## 1.1.7(2024-01-23)
|
||||
`修复`在VUE3使用APP-NVUE时,全屏状态字号过大问题;
|
||||
优化部分代码
|
||||
## 1.1.6(2024-01-18)
|
||||
修复在vue3中`handleBtn`会自动触发问题
|
||||
## 1.1.5(2024-01-18)
|
||||
修改vue3项目中的错误提示
|
||||
## 1.1.4(2023-12-14)
|
||||
优化精简代码,修复微信小程序全屏样式问题
|
||||
## 1.1.3(2023-11-08)
|
||||
可配置视频宽度,倍速盒子宽度、补充方法`changePause`,事件`fullscreenchange`
|
||||
## 1.1.2(2023-10-21)
|
||||
修改微信小程序试看样式问题
|
||||
## 1.1.1(2023-10-16)
|
||||
新增补充方法`seek`
|
||||
## 1.1.0(2023-10-09)
|
||||
增加视频试看功能
|
||||
## 1.0.0(2023-04-25)
|
||||
首次发布
|
||||
189
uni_modules/sunny-video/components/sunny-video/index.scss
Normal file
189
uni_modules/sunny-video/components/sunny-video/index.scss
Normal file
@ -0,0 +1,189 @@
|
||||
@use "sass:math";
|
||||
$primary: #5C91EF;
|
||||
|
||||
//css函数
|
||||
// /* #ifdef APP-NVUE */
|
||||
// @function tovmin($rpx) {
|
||||
// //$rpx为需要转换的字号
|
||||
// @return #{$rpx}rpx;
|
||||
|
||||
// }
|
||||
// /* #endif
|
||||
|
||||
// /* #ifndef APP-NVUE */
|
||||
// @function tovmin($rpx) {
|
||||
// //$rpx为需要转换的字号
|
||||
// /* #ifdef VUE2 */
|
||||
// @return #{math.div($rpx * 100, 750)}vmin;
|
||||
// /* #endif */
|
||||
// /* #ifdef VUE3 */
|
||||
// @return #{calc($rpx * 100 / 750)}vmin;
|
||||
// /* #endif */
|
||||
// }
|
||||
// /* #endif */
|
||||
.sunny-video {
|
||||
position: relative;
|
||||
.banner-view {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
.imgPal {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
.speedText {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
z-index: 5;
|
||||
padding: 5px;
|
||||
/* #ifndef APP-NVUE */
|
||||
box-sizing: border-box;
|
||||
/* #endif */
|
||||
.text {
|
||||
display: flex;
|
||||
width: 40px;
|
||||
font-size: 16px;
|
||||
color: $primary;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.vertical {
|
||||
position: relative;
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
&.vertical-full {
|
||||
.speed-box {
|
||||
padding: 25px 0;
|
||||
}
|
||||
}
|
||||
.vertical_left{
|
||||
position: absolute;
|
||||
left: 0;
|
||||
}
|
||||
.speed-box {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
/* #endif */
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
/* #ifdef APP-NVUE */
|
||||
// v1.1.3
|
||||
padding: 10px 0;
|
||||
/* #endif */
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
|
||||
.speed-text {
|
||||
width: 80px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
/* #ifdef APP-NVUE */
|
||||
padding: 5px 0px;
|
||||
/* #endif */
|
||||
/* #ifndef APP-NVUE */
|
||||
// v1.1.3
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
/* #endif */
|
||||
&.act {
|
||||
color: $primary;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 试看结束盒子
|
||||
.trialEndBox {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 999;
|
||||
// backdrop-filter: blur(14px);
|
||||
background-color: rgba(0, 0, 0, 0.9);
|
||||
|
||||
.tipText {
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.closeBtn {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 175px;
|
||||
height: 37.5px;
|
||||
margin: 16px 0 0;
|
||||
font-size: 15px;
|
||||
line-height: 37.5px;
|
||||
border-radius: 40px;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
background-color: $primary;
|
||||
}
|
||||
}
|
||||
|
||||
// 位置提示
|
||||
.mplayer-toast {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
bottom: 45px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
border-radius: 2px;
|
||||
opacity: 0;
|
||||
// transition: opacity .3s;
|
||||
padding: 6px 8px;
|
||||
&.mplayer-show {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
/* #ifdef APP-PLUS */
|
||||
margin: 5px 8px 8px;
|
||||
/* #endif */
|
||||
/* #ifdef APP-NVUE */
|
||||
margin: 0;
|
||||
|
||||
/* #endif */
|
||||
.btn_text {
|
||||
color: $primary;
|
||||
margin-left: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.kong {
|
||||
width: 8rpx;
|
||||
height: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
105
uni_modules/sunny-video/components/sunny-video/props.ts
Normal file
105
uni_modules/sunny-video/components/sunny-video/props.ts
Normal file
@ -0,0 +1,105 @@
|
||||
// @ts-nocheck
|
||||
import playIcon from '../../static/play.png'
|
||||
|
||||
export default {
|
||||
/** 视频地址 */
|
||||
src: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
/** 视频标题 */
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
/** 视频封面 */
|
||||
poster: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
/** 视频高度 */
|
||||
videoHeight: {
|
||||
type: [String, Number],
|
||||
default: '230px'
|
||||
},
|
||||
/** 视频宽度 v1.1.3 */
|
||||
videoWidth: {
|
||||
type: [String, Number],
|
||||
default: '750rpx'
|
||||
},
|
||||
/** v2.0.0 当视频大小与 video 容器大小不一致时,视频的表现形式。contain:包含,fill:填充,cover:覆盖 */
|
||||
objectFit:{
|
||||
type: String,
|
||||
default: 'contain'
|
||||
},
|
||||
/** 播放图片按钮宽高 */
|
||||
playImgHeight: {
|
||||
type: [String, Number],
|
||||
default: '70rpx'
|
||||
},
|
||||
/** 暂停按钮 */
|
||||
playImg: {
|
||||
type: String,
|
||||
default: () => playIcon
|
||||
},
|
||||
/** 是否显示静音按钮 */
|
||||
showMuteBtn: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/** 播放完毕是否退出全屏 */
|
||||
isExitFullScreen: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/** 主题色 */
|
||||
primaryColor: {
|
||||
type: String,
|
||||
default: '#5C91EF'
|
||||
},
|
||||
/** 试看提示的文字 */
|
||||
tipText: {
|
||||
type: String,
|
||||
default: '试看已结束,本片是会员专享内容'
|
||||
},
|
||||
/** 试看按钮的文字 */
|
||||
btnText: {
|
||||
type: String,
|
||||
default: '成为会员免费观看'
|
||||
},
|
||||
/** 视频试看时间(秒) */
|
||||
trialTime: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
/** 倍速盒子宽度 v1.1.3 */
|
||||
speedBoxWidth: {
|
||||
type: [String, Number],
|
||||
default: '160rpx'
|
||||
},
|
||||
/** 是否循环播放 v1.1.3 */
|
||||
loop: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/** 是否静音播放 v1.1.3 */
|
||||
muted: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/** 是否自动播放 */
|
||||
autoplay: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/** 历史观看位置 v1.2.0 */
|
||||
seekTime: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
/** 视频唯一ID v1.2.1 */
|
||||
videoId: {
|
||||
type: String,
|
||||
default: 'sunnyVideo'
|
||||
}
|
||||
}
|
||||
459
uni_modules/sunny-video/components/sunny-video/sunny-video.vue
Normal file
459
uni_modules/sunny-video/components/sunny-video/sunny-video.vue
Normal file
@ -0,0 +1,459 @@
|
||||
<template>
|
||||
<video class="sunny-video"
|
||||
:id="videoId"
|
||||
:title="title"
|
||||
:src="src"
|
||||
:show-center-play-btn="false"
|
||||
:controls="state.controls"
|
||||
:enable-play-gesture="state.enablePlayGesture"
|
||||
:muted="muted"
|
||||
:show-mute-btn="showMuteBtn"
|
||||
:autoplay="autoplay"
|
||||
:object-fit="objectFit"
|
||||
:style="{width: addUnit(videoWidth), height: addUnit(videoHeight)}"
|
||||
@play="play"
|
||||
@pause="emit('pause')"
|
||||
@ended="ended"
|
||||
@error="(e) => emit('playError', e)"
|
||||
@timeupdate="timeupdate"
|
||||
@fullscreenchange="fullscreenchange"
|
||||
>
|
||||
<cover-view v-if="!state.isPlay" class="banner-view">
|
||||
<cover-image
|
||||
class="banner"
|
||||
:style="{width: addUnit(videoWidth), height: addUnit(videoHeight)}"
|
||||
:src="poster"
|
||||
@click="changePlay"
|
||||
/>
|
||||
<cover-image
|
||||
class="imgPal"
|
||||
:style="[playImgStyle]"
|
||||
:src="playImg"
|
||||
@click="changePlay"
|
||||
/>
|
||||
</cover-view>
|
||||
<!-- 当前倍速标记 -->
|
||||
<cover-view
|
||||
v-if="state.isPlay&&!state.isShowRateBox"
|
||||
class="speedText"
|
||||
:style="[speedStyle]"
|
||||
@click="state.isShowRateBox = true"
|
||||
>
|
||||
<!-- #ifdef APP-NVUE -->
|
||||
<text class="text">{{state.rateText}}X</text>
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef APP-NVUE -->
|
||||
<cover-view class="text">{{state.rateText}}X</cover-view>
|
||||
<!-- #endif -->
|
||||
</cover-view>
|
||||
<!-- 倍速选择 -->
|
||||
<cover-view
|
||||
v-if="state.isShowRateBox"
|
||||
class="vertical"
|
||||
:class="{'vertical-full':state.isFullScreen}"
|
||||
:style="[speedBoxMaskStyle]"
|
||||
>
|
||||
<cover-view
|
||||
class="vertical_left"
|
||||
:style="[speedBoxLeftStyel]"
|
||||
@click="state.isShowRateBox=false"
|
||||
/>
|
||||
<!-- #ifdef APP-NVUE -->
|
||||
<view
|
||||
class="speed-box"
|
||||
:style="[speedBoxStyle]"
|
||||
>
|
||||
<text
|
||||
class="speed-text"
|
||||
:class="{act:item.isSelect}"
|
||||
v-for="(item,index) in rateList"
|
||||
:key="item.name"
|
||||
@click="changeRate(item,index)"
|
||||
>{{item.name}}X</text>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef APP-NVUE -->
|
||||
<cover-view
|
||||
class="speed-box"
|
||||
:style="[speedBoxStyle]"
|
||||
>
|
||||
<cover-view
|
||||
class="speed-text"
|
||||
:class="{act:item.isSelect}"
|
||||
v-for="(item,index) in rateList"
|
||||
:key="item.name"
|
||||
@click="changeRate(item,index)"
|
||||
>{{item.name}}X</cover-view>
|
||||
</cover-view>
|
||||
<!-- #endif -->
|
||||
</cover-view>
|
||||
<!-- 试看盒子 -->
|
||||
<cover-view v-if="state.visibleTrialEndBox" class="trialEndBox" :style="[trialEndBoxStyle]">
|
||||
<!-- #ifdef APP-NVUE -->
|
||||
<text class="tipText">{{tipText}}</text>
|
||||
<text class="closeBtn" @click.stop="handleClickTrialEnd">{{btnText}}</text>
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef APP-NVUE -->
|
||||
<cover-view class="tipText">{{tipText}}</cover-view>
|
||||
<cover-view class="closeBtn" @click.stop="handleClickTrialEnd">{{btnText}}</cover-view>
|
||||
<!-- #endif -->
|
||||
</cover-view>
|
||||
<!-- 进度记录跳转提示 -->
|
||||
<cover-view class="mplayer-toast" :class="{'mplayer-show':state.showMplayerToast}">
|
||||
<!-- #ifdef H5 -->
|
||||
<text class="text">
|
||||
记忆你上次看到{{convertSecondsToHMS(seekTime)}}<text class="btn_text" @click="handelSeek">跳转</text>
|
||||
</text>
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef H5 | APP-NVUE -->
|
||||
<cover-view v-if="state.showMplayerToast" class="text">
|
||||
{{'已为您定位至'+convertSecondsToHMS(seekTime)}}
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<cover-view class="kong" />
|
||||
<!-- #endif -->
|
||||
</cover-view>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef APP-NVUE -->
|
||||
<text class="text">已为您定位至{{convertSecondsToHMS(seekTime)}}</text>
|
||||
<!-- #endif -->
|
||||
</cover-view>
|
||||
</video>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
// @ts-nocheck
|
||||
import { defineComponent, ref, toRefs, reactive, computed, onMounted, getCurrentInstance, nextTick } from './vue-composition-api'
|
||||
import VideoProps from './props'
|
||||
import {getPx, addUnit, convertSecondsToHMS} from './utils'
|
||||
import type { StateData, rateListData } from './type'
|
||||
|
||||
const name = 'sunny-video'
|
||||
export default defineComponent({
|
||||
name,
|
||||
props: VideoProps,
|
||||
emits: ['play', 'pause', 'ended', 'timeupdate', 'trialEnd', 'playError', 'changeSeek', 'fullscreenchange', 'handleBtn'],
|
||||
setup(props, {emit}) {
|
||||
const { proxy } = getCurrentInstance()
|
||||
const { screenWidth, screenHeight } = uni.getSystemInfoSync()
|
||||
const state = reactive<StateData>({
|
||||
isPlay: false,
|
||||
controls: false,
|
||||
enablePlayGesture: false,
|
||||
countPlay: 0,
|
||||
isFullScreen: false,
|
||||
rateText: '1.0',
|
||||
isShowRateBox: false,
|
||||
visibleTrialEndBox: false,
|
||||
showMplayerToast: false
|
||||
})
|
||||
const rateList = ref<rateListData[]>([{
|
||||
name: '0.5',
|
||||
isSelect: false
|
||||
}, {
|
||||
name: '0.8',
|
||||
isSelect: false
|
||||
}, {
|
||||
name: '1.0',
|
||||
isSelect: true
|
||||
}, {
|
||||
name: '1.25',
|
||||
isSelect: false
|
||||
}, {
|
||||
name: '1.5',
|
||||
isSelect: false
|
||||
}
|
||||
// #ifdef MP-WEIXIN
|
||||
, {
|
||||
name: '2.0',
|
||||
isSelect: false
|
||||
}
|
||||
// #endif
|
||||
])
|
||||
const videoCtx = ref(null)
|
||||
const {isFullScreen} = toRefs(state)
|
||||
/** 播放按钮样式 */
|
||||
const playImgStyle = computed(()=>{
|
||||
const wh = getPx(props.playImgHeight)
|
||||
const style = {
|
||||
width: addUnit(wh),
|
||||
height: addUnit(wh),
|
||||
top: `${getPx(props.videoHeight)/2}px`,
|
||||
left: `${getPx(props.videoWidth)/2}px`,
|
||||
transform:`translate(-${getPx(wh)/2}px,-${getPx(wh)/2}px)`
|
||||
}
|
||||
return style
|
||||
})
|
||||
const speedStyle = ref({})
|
||||
const speedBoxMaskStyle = ref({})
|
||||
const speedBoxLeftStyel = ref({})
|
||||
const speedBoxStyle = ref({})
|
||||
const trialEndBoxStyle = ref({})
|
||||
|
||||
/** 设置样式 */
|
||||
const setStyles = () => {
|
||||
/** 当前倍速样式 */
|
||||
(function speedStyleFn (){
|
||||
let tops = getPx(props.videoHeight) / 2
|
||||
if(isFullScreen.value){
|
||||
tops = getPx(props.videoWidth) / 2
|
||||
}
|
||||
Object.assign(speedStyle.value, {top: tops - 15 + 'px'})
|
||||
})();
|
||||
/** 倍速盒子背景遮罩 */
|
||||
(function speedBoxMaskStyleFn(){
|
||||
let style = {
|
||||
width: addUnit(props.videoWidth),
|
||||
height: addUnit(props.videoHeight)
|
||||
}
|
||||
if(isFullScreen.value){
|
||||
style = {
|
||||
width: `${screenHeight}px`,
|
||||
height: `${screenWidth}px`
|
||||
}
|
||||
}
|
||||
Object.assign(speedBoxMaskStyle.value, style)
|
||||
})();
|
||||
/** 倍速盒子左侧遮罩 */
|
||||
(function speedBoxLeftStyelFn(){
|
||||
const w = getPx(props.speedBoxWidth)
|
||||
const pw = getPx(props.videoWidth)
|
||||
let style = {
|
||||
width: `${pw - w}px`,
|
||||
height: addUnit(props.videoHeight)
|
||||
}
|
||||
if(isFullScreen.value){
|
||||
style = {
|
||||
width: `${screenHeight - w}px`,
|
||||
height: `${screenWidth}px`
|
||||
}
|
||||
}
|
||||
Object.assign(speedBoxLeftStyel.value, style)
|
||||
proxy.$forceUpdate()
|
||||
})();
|
||||
|
||||
/** 倍速盒子样式 */
|
||||
(function speedBoxStyleFn(){
|
||||
let style = {
|
||||
width: addUnit(props.speedBoxWidth),
|
||||
height: addUnit(props.videoHeight)
|
||||
}
|
||||
if(isFullScreen.value){
|
||||
style = {
|
||||
width: addUnit(props.speedBoxWidth),
|
||||
height: `${screenWidth}px`
|
||||
}
|
||||
}
|
||||
Object.assign(speedBoxStyle.value, style)
|
||||
})();
|
||||
/** 试看样式 */
|
||||
(function trialEndBoxStyleFn() {
|
||||
let style = {
|
||||
width: addUnit(props.videoWidth),
|
||||
height: addUnit(props.videoHeight)
|
||||
}
|
||||
if(isFullScreen.value){
|
||||
style = {
|
||||
width: `${screenHeight}px`,
|
||||
height: `${screenWidth}px`
|
||||
}
|
||||
}
|
||||
Object.assign(trialEndBoxStyle.value, style)
|
||||
})();
|
||||
proxy.$forceUpdate()
|
||||
}
|
||||
|
||||
onMounted(()=>{
|
||||
videoCtx.value = uni.createVideoContext(props.videoId, proxy)
|
||||
setStyles()
|
||||
})
|
||||
|
||||
/**
|
||||
* 播放视频
|
||||
*/
|
||||
function changePlay() {
|
||||
nextTick(()=>{
|
||||
videoCtx.value.play()
|
||||
if (state.countPlay <= 0 && props.seekTime > 0) {
|
||||
// 没有播放过并且需要跳转视频
|
||||
// #ifndef H5
|
||||
seek(props.seekTime)
|
||||
// #endif
|
||||
state.showMplayerToast = true
|
||||
setTimeout(() => {
|
||||
state.showMplayerToast = false
|
||||
}, 3000)
|
||||
}
|
||||
state.countPlay++
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 跳转到指定位置,单位 s
|
||||
* @param {Object} position
|
||||
* @description V1.1.1
|
||||
*/
|
||||
function seek(position:number | string) {
|
||||
nextTick(() => {
|
||||
videoCtx.value.seek(position)
|
||||
emit('changeSeek', position)
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 切换倍速
|
||||
* @param {Object} item 当前点击项
|
||||
* @param {Object} index 倍速索引
|
||||
*/
|
||||
function changeRate(item:rateListData, index:number) {
|
||||
if (item.isSelect) return state.isShowRateBox = false
|
||||
rateList.value.forEach((v, i) => {
|
||||
i == index ? v.isSelect = true : v.isSelect = false
|
||||
})
|
||||
videoCtx.value.playbackRate(+item.name)
|
||||
state.rateText = item.name
|
||||
state.isShowRateBox = false
|
||||
}
|
||||
/**
|
||||
* 当开始/继续播放时触发play事件 - 会触发emit事件
|
||||
* @param {EventHandle} e
|
||||
*/
|
||||
function play(e) {
|
||||
state.isPlay = true
|
||||
state.controls = true
|
||||
state.enablePlayGesture = true
|
||||
emit('play', e)
|
||||
};
|
||||
/**
|
||||
* 视频播放进度变化 - 会触发emit事件
|
||||
* @param {Object} e event.detail = {currentTime, duration}
|
||||
*/
|
||||
function timeupdate(e) {
|
||||
emit('timeupdate', e)
|
||||
// 是否判断需要视频试看 V1.1.0
|
||||
if (props.trialTime <= 0) return
|
||||
if (e.detail.currentTime >= props.trialTime) {
|
||||
emit('trialEnd')
|
||||
showTrialEnd()
|
||||
state.enablePlayGesture = false
|
||||
state.controls = false
|
||||
}
|
||||
};
|
||||
/**
|
||||
* 控制试看模块显示 V1.1.0
|
||||
*/
|
||||
function showTrialEnd() {
|
||||
state.visibleTrialEndBox = true
|
||||
videoCtx.value.pause()
|
||||
}
|
||||
/**
|
||||
* 点击试看结束按钮 V1.1.6
|
||||
* @param {Event} e
|
||||
*/
|
||||
function handleClickTrialEnd(e) {
|
||||
if (e.hasOwnProperty('touches')) {
|
||||
emit('handleBtn')
|
||||
}
|
||||
}
|
||||
/**
|
||||
* H5用户手动跳转指定位置播放 v1.2.0
|
||||
*/
|
||||
function handelSeek() {
|
||||
seek(props.seekTime)
|
||||
videoCtx.value.play()
|
||||
state.showMplayerToast = false
|
||||
}
|
||||
/**
|
||||
* 全屏操作 - 会触发emit
|
||||
*/
|
||||
function fullscreenchange(e) {
|
||||
state.isFullScreen = e.detail.fullScreen
|
||||
setStyles()
|
||||
emit('fullscreenchange', e.detail)
|
||||
}
|
||||
/**
|
||||
* 监听视频结束 - 会触发emit事件
|
||||
*/
|
||||
function ended() {
|
||||
emit('ended')
|
||||
if (!props.isExitFullScreen) return
|
||||
videoCtx.value.exitFullScreen(); //退出全屏
|
||||
};
|
||||
/**
|
||||
* 控制试看模块隐藏 V1.1.0
|
||||
*/
|
||||
function closeTrialEnd() {
|
||||
state.visibleTrialEndBox = false
|
||||
changePlay()
|
||||
}
|
||||
/**
|
||||
* 手动全屏方法 v1.1.8
|
||||
* @param {Object} direction direction取为 vertical 或 horizontal
|
||||
* @description H5和抖音小程序不支持{direction}参数
|
||||
*/
|
||||
function requestFullScreen(direction: 'vertical' | 'horizontal' = 'horizontal') {
|
||||
videoCtx.value.requestFullScreen({
|
||||
direction
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 手动退出全屏 v1.1.8
|
||||
*/
|
||||
function exitFullScreen() {
|
||||
videoCtx.value.exitFullScreen()
|
||||
}
|
||||
/**
|
||||
* v1.1.8
|
||||
* 显示状态栏,仅在iOS全屏下有效
|
||||
* 平台差异:仅微信小程序、百度小程序、QQ小程序
|
||||
*/
|
||||
function showStatusBar() {
|
||||
videoCtx.value.showStatusBar()
|
||||
}
|
||||
/**
|
||||
* v1.1.8
|
||||
* 隐藏状态栏,仅在iOS全屏下有效
|
||||
* 平台差异:仅微信小程序、百度小程序、QQ小程序
|
||||
*/
|
||||
function hideStatusBar() {
|
||||
videoCtx.value.hideStatusBar()
|
||||
}
|
||||
/**
|
||||
* 停止视频 v1.1.8
|
||||
*/
|
||||
function handelStop() {
|
||||
videoCtx.value.stop()
|
||||
}
|
||||
|
||||
return {
|
||||
emit,
|
||||
addUnit,
|
||||
convertSecondsToHMS,
|
||||
state,
|
||||
play,
|
||||
ended,
|
||||
timeupdate,
|
||||
changePlay,
|
||||
speedStyle,
|
||||
playImgStyle,
|
||||
speedBoxStyle,
|
||||
speedBoxMaskStyle,
|
||||
speedBoxLeftStyel,
|
||||
rateList,
|
||||
trialEndBoxStyle,
|
||||
fullscreenchange,
|
||||
handleClickTrialEnd,
|
||||
handelSeek,
|
||||
changeRate,
|
||||
closeTrialEnd,
|
||||
requestFullScreen,
|
||||
exitFullScreen,
|
||||
showStatusBar,
|
||||
hideStatusBar,
|
||||
handelStop
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import './index.scss';
|
||||
</style>
|
||||
32
uni_modules/sunny-video/components/sunny-video/type.ts
Normal file
32
uni_modules/sunny-video/components/sunny-video/type.ts
Normal file
@ -0,0 +1,32 @@
|
||||
// @ts-nocheck
|
||||
export interface Props {
|
||||
|
||||
}
|
||||
|
||||
export type rateListData = {
|
||||
/** 名称 */
|
||||
name: string
|
||||
/** 是否选中 */
|
||||
isSelect: boolean
|
||||
}
|
||||
|
||||
export interface StateData {
|
||||
/** 视频是否播放过 */
|
||||
isPlay: boolean
|
||||
/** 是否显示原生控件 */
|
||||
controls: boolean
|
||||
/** 是否开启播放手势,即双击切换播放/暂停 */
|
||||
enablePlayGesture: boolean
|
||||
/** 视频播放次数 */
|
||||
countPlay: number | string
|
||||
/** 全屏状态 */
|
||||
isFullScreen: boolean
|
||||
/** 当前倍速 */
|
||||
rateText: string
|
||||
/** 是否显示倍速盒子 */
|
||||
isShowRateBox: boolean
|
||||
/** 控制试看结束内容显示隐藏 */
|
||||
visibleTrialEndBox: boolean
|
||||
/** 控制H5历史播放位置的显示隐藏 */
|
||||
showMplayerToast: boolean
|
||||
}
|
||||
61
uni_modules/sunny-video/components/sunny-video/utils.ts
Normal file
61
uni_modules/sunny-video/components/sunny-video/utils.ts
Normal file
@ -0,0 +1,61 @@
|
||||
// @ts-nocheck
|
||||
|
||||
/**
|
||||
* @description 用于获取用户传递值的px值 如果用户传递了"xxpx"或者"xxrpx",取出其数值部分,如果是"xxxrpx"还需要用过uni.upx2px进行转换
|
||||
* @param {number|string} value 用户传递值的px值
|
||||
* @param {boolean} unit
|
||||
* @returns {number|string}
|
||||
*/
|
||||
export const getPx = (value, unit = false)=> {
|
||||
if (testNumber(value)) {
|
||||
return unit ? `${value}px` : Number(value)
|
||||
}
|
||||
// 如果带有rpx,先取出其数值部分,再转为px值
|
||||
if (/(rpx|upx)$/.test(value)) {
|
||||
return unit ? `${uni.upx2px(parseInt(value))}px` : Number(uni.upx2px(parseInt(value)))
|
||||
}
|
||||
return unit ? `${parseInt(value)}px` : parseInt(value)
|
||||
}
|
||||
/**
|
||||
* @description 添加单位,如果有rpx,upx,%,px等单位结尾或者值为auto,直接返回,否则加上px单位结尾
|
||||
* @param {string|number} value 需要添加单位的值
|
||||
* @param {string} unit 添加的单位名 比如px
|
||||
*/
|
||||
export const addUnit =(value = 'auto', unit = 'px')=> {
|
||||
value = String(value)
|
||||
// number判断是否为数值
|
||||
return testNumber(value) ? `${value}${unit}` : value
|
||||
}
|
||||
/**
|
||||
* 验证十进制数字
|
||||
*/
|
||||
function testNumber(value) {
|
||||
return /^[\+-]?(\d+\.?\d*|\.\d+|\d\.\d+e\+\d+)$/.test(value)
|
||||
}
|
||||
/**
|
||||
* @description 获取系统信息同步接口
|
||||
* @link 获取系统信息同步接口 https://uniapp.dcloud.io/api/system/info?id=getsysteminfosync
|
||||
*/
|
||||
export const sys=()=> {
|
||||
return uni.getSystemInfoSync()
|
||||
}
|
||||
/**
|
||||
* 将秒转时分秒
|
||||
* @param {number} 数字
|
||||
*/
|
||||
export const convertSecondsToHMS =(seconds)=> {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
|
||||
// 在数字小于10时,在前面补零
|
||||
const minutesStr = minutes < 10 ? "0" + minutes : minutes;
|
||||
const secondsStr = remainingSeconds < 10 ? "0" + remainingSeconds : remainingSeconds;
|
||||
|
||||
if (hours === 0) {
|
||||
return minutesStr + ":" + secondsStr;
|
||||
} else {
|
||||
const hoursStr = hours < 10 ? "0" + hours : hours;
|
||||
return hoursStr + ":" + minutesStr + ":" + secondsStr;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
// @ts-nocheck
|
||||
|
||||
// #ifdef VUE3
|
||||
export * from 'vue';
|
||||
// #endif
|
||||
|
||||
// #ifndef VUE3
|
||||
export * from '@vue/composition-api';
|
||||
|
||||
// #ifdef APP-NVUE
|
||||
import Vue from 'vue'
|
||||
import VueCompositionAPI from '@vue/composition-api'
|
||||
Vue.use(VueCompositionAPI)
|
||||
// #endif
|
||||
|
||||
// #endif
|
||||
86
uni_modules/sunny-video/package.json
Normal file
86
uni_modules/sunny-video/package.json
Normal file
@ -0,0 +1,86 @@
|
||||
{
|
||||
"id": "sunny-video",
|
||||
"displayName": "sunny-video视频倍速试看组件",
|
||||
"version": "2.0.0",
|
||||
"description": "sunny-video简单的视频倍速播放插件,基于uni video,增加倍速播放、视频试看,兼容APP及微信小程序。",
|
||||
"keywords": [
|
||||
"sunny-video",
|
||||
"视频播放组件",
|
||||
"视频倍速",
|
||||
"video",
|
||||
"视频试看"
|
||||
],
|
||||
"repository": "",
|
||||
"engines": {
|
||||
"HBuilderX": "^4.0"
|
||||
},
|
||||
"dcloudext": {
|
||||
"type": "component-vue",
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "0.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "0.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": "2304493354"
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "无",
|
||||
"data": "无",
|
||||
"permissions": "无"
|
||||
},
|
||||
"npmurl": ""
|
||||
},
|
||||
"uni_modules": {
|
||||
"dependencies": [],
|
||||
"encrypt": [],
|
||||
"platforms": {
|
||||
"cloud": {
|
||||
"tcb": "y",
|
||||
"aliyun": "y",
|
||||
"alipay": "n"
|
||||
},
|
||||
"client": {
|
||||
"Vue": {
|
||||
"vue2": "y",
|
||||
"vue3": "y"
|
||||
},
|
||||
"App": {
|
||||
"app-vue": "y",
|
||||
"app-nvue": "y"
|
||||
},
|
||||
"H5-mobile": {
|
||||
"Safari": "u",
|
||||
"Android Browser": "u",
|
||||
"微信浏览器(Android)": "y",
|
||||
"QQ浏览器(Android)": "u"
|
||||
},
|
||||
"H5-pc": {
|
||||
"Chrome": "u",
|
||||
"IE": "u",
|
||||
"Edge": "u",
|
||||
"Firefox": "u",
|
||||
"Safari": "u"
|
||||
},
|
||||
"小程序": {
|
||||
"微信": "y",
|
||||
"阿里": "u",
|
||||
"百度": "u",
|
||||
"字节跳动": "u",
|
||||
"QQ": "u",
|
||||
"钉钉": "u",
|
||||
"快手": "u",
|
||||
"飞书": "u",
|
||||
"京东": "u"
|
||||
},
|
||||
"快应用": {
|
||||
"华为": "u",
|
||||
"联盟": "u"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
130
uni_modules/sunny-video/readme.md
Normal file
130
uni_modules/sunny-video/readme.md
Normal file
@ -0,0 +1,130 @@
|
||||
# sunny-video视频倍速播放器
|
||||
> **组件名:sunny-video**
|
||||
|
||||
### 平台差异说明
|
||||
- 目前已应用到APP(安卓、iOS)、微信(小程序、H5)
|
||||
- 其它平台未测试
|
||||
|
||||
### 安装方式
|
||||
|
||||
- 本组件符合[easycom](https://uniapp.dcloud.io/collocation/pages?id=easycom)规范,`HBuilderX 2.5.5`起,只需将本组件导入项目,在页面`template`中即可直接使用,无需在页面中`import`和注册`components`。
|
||||
- **uni-app插件市场链接** —— [https://ext.dcloud.net.cn/plugin?id=11982](https://ext.dcloud.net.cn/plugin?id=11982)
|
||||
|
||||
### <a id="jump1">基本用法</a>
|
||||
|
||||
- APP端需要配置`manifest.json>App模块配置`勾选`VideoPlay(视频播放)`
|
||||
- App端:3.6.14 以及 手机系统 iOS16 以上video全屏 需要配置应用支持横屏: 在`manifest.json` 文件内 `app-plus` 节点下新增 `screenOrientation` 配置,设置值为`["portrait-primary","portrait-secondary","landscape-primary","landscape-secondary"]`。如下:
|
||||
```json
|
||||
"app-plus" : {
|
||||
"screenOrientation" : [
|
||||
"portrait-primary",
|
||||
"portrait-secondary",
|
||||
"landscape-primary",
|
||||
"landscape-secondary"
|
||||
]
|
||||
}
|
||||
```
|
||||
#### <a id="jump2">vue2使用必看</a>
|
||||
- 自`2.0.0`开始,本组件使用`composition-api`, 如果你希望在vue2中使用,需要在`main.js`中的`vue2`部分中添加如下关键代码块
|
||||
```javascript
|
||||
// vue2
|
||||
import Vue from 'vue'
|
||||
import VueCompositionAPI from '@vue/composition-api'
|
||||
Vue.use(VueCompositionAPI)
|
||||
```
|
||||
- 更多说明请查看官方教程[vue-composition-api](https://uniapp.dcloud.net.cn/tutorial/vue-composition-api.html)
|
||||
|
||||
### 代码演示
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<view>
|
||||
<sunny-video
|
||||
ref="sunnyVideoRef"
|
||||
title="测试视频"
|
||||
src="https://qiniu-web-assets.dcloud.net.cn/unidoc/zh/2minute-demo.mp4"
|
||||
poster="https://ask.dcloud.net.cn/static/images/side/ask_right_unicloud_class.jpg"
|
||||
:trialTime="20"
|
||||
:seekTime="5"
|
||||
@timeupdate="timeupdate"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
timeupdate(e){
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
```
|
||||
|
||||
### Props
|
||||
|
||||
|属性名 | 类型 |默认值 | 可选值 | 说明 |
|
||||
|:-: | :-: |:-: | :-: | :-: |
|
||||
|src | String | '' | - | 视频播放地址 |
|
||||
|loop `1.1.3` | Boolean | false | true | 是否循环播放 |
|
||||
|muted `1.1.3` | Boolean | false | true | 是否静音播放 |
|
||||
|autoplay | Boolean | false | true | 是否自动播放 |
|
||||
|title | String | '' | - | 视频标题 |
|
||||
|poster | String | '' | - | 视频封面 |
|
||||
|videoHeight | String, Number| 230px | - | 视频高度 |
|
||||
|videoWidth `1.1.3` | String, Number| 750rpx | - | 视频宽度 |
|
||||
|playImgHeight | String, Number| 70rpx | - | 播放图标按钮宽高 |
|
||||
|playImg | String | base64 | - | 播放按钮图标 |
|
||||
|showMuteBtn | Boolean | false | true | 是否显示静音按钮 |
|
||||
|isExitFullScreen | Boolean | true | false | 播放完毕是否退出全屏 |
|
||||
|tipText `1.1.0` | String | '试看已结束,本片是会员专享内容' | - | 试看提示的文字 |
|
||||
|btnText `1.1.0` | String | '成为会员免费观看' | - | 试看按钮的文字 |
|
||||
|trialTime `1.1.0` | Number | 0 | - | 视频试看时间(秒), 不需要试看功能则默认为0 |
|
||||
|speedBoxWidth `1.1.3` | String, Number| 160rpx | - | 倍速盒子宽度 |
|
||||
|seekTime `1.2.0` | Number | 0 | - | 跳转到历史观看位置(秒), 不需要则默认为0,注:`H5`为被动 |
|
||||
|videoId `1.2.1` | String | sunnyVideo | - | 视频唯一ID |
|
||||
|objectFit `2.0.0` | String | contain | - | 当视频大小与 video 容器大小不一致时,视频的表现形式。contain:包含,fill:填充,cover:覆盖 |
|
||||
|
||||
### 事件 Events
|
||||
|
||||
| 事件名 | 说明 | 返回值 |
|
||||
|:-: | :-: |:-: |
|
||||
| play | 监听开始播放 | - |
|
||||
| pause | 监听视频暂停 | - |
|
||||
| playError | 视频播放出错时触发 | - |
|
||||
| videoEnded | 视频播放结束触发 | - |
|
||||
| timeupdate | 播放进度变化时触发 | event.detail={currentTime, duration}。触发频率 250ms 一次 |
|
||||
| fullscreenchange `1.1.3` | 当视频进入和退出全屏时触发 | event={fullScreen, direction},direction取为vertical或horizontal |
|
||||
| handleBtn `1.1.0` | 点击试看按钮时触发 | - |
|
||||
| trialEnd `1.1.0` | 试看结束时触发 | - |
|
||||
| changeSeek `1.1.7` | 视频跳转到指定位置时触发 | event= 播放位置单位 s |
|
||||
|
||||
### 方法 Methods
|
||||
|
||||
- 需要通过ref获取组件才能调用
|
||||
|
||||
| 名称 | 参数 | 说明 | 差异 |
|
||||
|:-: |:-: |:-: |:-: |
|
||||
| changePlay | | 开始播放视频 | - |
|
||||
| changePause `1.1.3` | | 暂停播放视频 | - |
|
||||
| showTrialEnd `1.1.0` | | 控制试看模块显示 | - |
|
||||
| closeTrialEnd `1.1.0` | | 控制试看模块隐藏 | - |
|
||||
| seek `1.1.1` | position | 跳转到指定位置,单位 s | - |
|
||||
| requestFullScreen `1.1.8` | |进入全屏 | - |
|
||||
| exitFullScreen `1.1.8` | |退出全屏 | - |
|
||||
| showStatusBar `1.1.8` | |显示状态栏,仅在iOS全屏下有效 |微信小程序、百度小程序、QQ小程序 |
|
||||
| hideStatusBar `1.1.8` | |隐藏状态栏,仅在iOS全屏下有效 |微信小程序、百度小程序、QQ小程序 |
|
||||
| handelStop `1.1.8` | |停止视频 |微信小程序 |
|
||||
|
||||
### 注意事项
|
||||
- APP全屏需要按照文档[基本用法](#jump1)进行配置,
|
||||
- APP端如果需要全屏倍速及试看,需要使用`.nvue`文件
|
||||
- `vue2`项目中使用,需要按照文档[vue2使用必看](#jump2)进行配置
|
||||
- 问题反馈交流群:[122150481](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=2_xYi389jXJRZwPseVEICL_9trE4RrPU&authKey=nsOJ%2BQd%2Fy3Irv4oKaNnxP6XUwTtHUbBVIy3Tw66WX%2FXgVTGWD%2FgBFGVajuQkWPru&noverify=0&group_code=122150481)
|
||||
BIN
uni_modules/sunny-video/static/play.png
Normal file
BIN
uni_modules/sunny-video/static/play.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
Loading…
x
Reference in New Issue
Block a user