Files
test-subject/src/pages/video.vue
2025-12-04 18:52:57 +08:00

604 lines
14 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>
<main v-if="finishloading">
<!-- 视频容器 -->
<section class="video-container">
<Head></Head>
<video id="videoid" :src="videoData.videoUrl" :danmu-list="videoData.danmuList" enable-danmu loop :muted="isMuted"
:show-mute-btn="true" :controls="false" object-fit="contain" @tap="pausevideo">
<!-- 播放按钮 - 直接使用浏览器复制的样式 -->
<section v-if="!isPlaying" class="uni-video-cover" @tap="pausevideo">
<view class="uni-video-cover-play-button uni-video-icon"></view>
</section>
</video>
<!-- 右侧交互按钮 -->
<view class="interaction-panel">
<!-- 头像+关注 -->
<view class="user-section">
<image :src="videoData.userImg" class="user-avatar" mode="aspectFill"></image>
<view class="follow-btn-container" @tap="() => handleInteraction('follow')">
<image src="/static/imgs/followbtn/btn@3x.webp" mode="aspectFit" style="height: 24px;"></image>
</view>
</view>
<!-- 点赞按钮 -->
<view class="action-btn" @tap="() => handleInteraction('like')">
<image src="/static/imgs/likeicon2/icon-2@3x.webp" class="action-icon" mode="aspectFit"></image>
<text class="action-count">{{ formatCount(videoData.likesum) }}</text>
</view>
<!-- 评论按钮 -->
<view class="action-btn" @tap="() => handleInteraction('comment')">
<image src="/static/imgs/commenticon/icon-2@3x.webp" class="action-icon" mode="aspectFit"></image>
<text class="action-count">{{ formatCount(videoData.commentsum) }}</text>
</view>
<!-- 分享按钮 -->
<view class="action-btn" @tap="() => handleInteraction('share')">
<image src="/static/imgs/shareicon/icon-2@3x.webp" class="action-icon" mode="aspectFit"></image>
<text class="action-count">{{ formatCount(videoData.sharesum) }}</text>
</view>
<!-- 静音按钮 -->
<image :src="isMuted ? '/static/imgs/mutebtn/mutebtn@3x.webp' : '/static/imgs/unmutebtn/unmutebtn@3x.webp'"
class="mutebtn" mode="aspectFit" @tap="toggleMute"></image>
</view>
<!-- 视频信息 -->
<section class="videoinfo">
<view class="vedioinfohead" @tap="() => handleInteraction('user')">
<text class="username">{{ '@' + videoData.userName }}</text>
<time class="datetime">{{ formatDate(videoData.date) }}</time>
</view>
<view class="content-container">
<text class="content" :class="{ 'expanded': isExpanded }">{{ isExpanded ? videoData.copywriting :
truncatedText }}</text>
<view class="flodbtncontainer">
<image v-if="showExpandButton" class="expand-btn" :class="{ 'rotated': isExpanded }"
:src="isExpanded ? '/static/imgs/foldicon/flodicon@3x.webp' : '/static/imgs/expandicon/expandicon@3x.webp'"
mode="aspectFit" @tap="toggleExpand"></image>
</view>
</view>
</section>
</section>
<!-- 评论区域 + 互动区域 -->
<Comments :postid="videoData.id" />
<view class="spacerview"></view>
<!-- 互动区域 -->
<!-- <Intereact /> -->
</main>
<!-- Findmore -->
<!-- <Findmore /> -->
<view v-else class="loading-container">
<text>Loading...</text>
</view>
</template>
<script setup>
import { onLoad } from '@dcloudio/uni-app'
import { ref, reactive, getCurrentInstance, computed } from 'vue'
import { useCommonStore } from '@/stores/common.js'
import Head from '@/pages/head/head.vue'
import Comments from '@/pages/comments/comments.vue'
import Intereact from '@/pages/intereact/intereact.vue'
import { getPostList, getPostVideo, getUserImg } from '@/api/api.js'
// 导入翻译工具
import { translateZhToEn, translationCache } from '@/utils/translate.js'
const common = useCommonStore()
const formatCount = common.formatCount
const formatDate = common.formatDate
const videoData = reactive({})
// 资源加载状态
const finishloading = ref(false)
// 折叠展开状态
const isExpanded = ref(false)
// 静音状态
const isMuted = ref(false)
// 播放状态
const isPlaying = ref(true) // 默认播放状态
// 计算属性:是否需要显示展开按钮
const showExpandButton = computed(() => {
if (!videoData.copywriting) return false
// 考虑中文字符每个中文字符算2个字符宽度
const textLength = videoData.copywriting.replace(/[^\x00-\xff]/g, '**').length
return textLength > 30
})
// 计算属性:截断后的文本
const truncatedText = computed(() => {
if (!videoData.copywriting) return ''
if (!showExpandButton.value) return videoData.copywriting
// 智能截断,保留完整的中文字符
let result = ''
let charCount = 0
for (let i = 0; i < videoData.copywriting.length; i++) {
const char = videoData.copywriting[i]
// 中文字符算2个字符其他算1个
charCount += /[^\x00-\xff]/.test(char) ? 2 : 1
if (charCount <= 30) {
result += char
} else {
break
}
}
// 如果截断后的文本和原文本相同,不需要加省略号
if (result === videoData.copywriting) {
return result
}
return result + '...'
})
// 切换展开状态
const toggleExpand = () => {
isExpanded.value = !isExpanded.value
}
//暂停/播放
const pausevideo = () => {
// 获取VideoContext实例
const videoCtx = uni.createVideoContext('videoid', getCurrentInstance());
// 检查视频状态并切换播放/暂停
if (videoCtx) {
// 使用一个状态变量来跟踪播放状态
if (isPlaying.value) {
videoCtx.pause()
isPlaying.value = false
} else {
videoCtx.play()
isPlaying.value = true
}
}
}
// 统一处理交互操作
const handleInteraction = () => {
common.openapp();
}
// 切换静音
const toggleMute = () => {
isMuted.value = !isMuted.value;
// 获取VideoContext实例
const videoCtx = uni.createVideoContext('videoid', getCurrentInstance());
if (videoCtx && typeof videoCtx.volume === 'function') {
// H5平台使用volume方法控制音量
videoCtx.volume(isMuted.value ? 0 : 1);
}
}
onLoad(() => {
const params = 'video_only'
getPostList(params).then(res => {
try {
// 检查响应状态
if (res.statusCode === 200 && res.data) {
const data = res.data.data
console.log('视频数据:', data)
// 创建Promise数组来处理异步资源
const resourcePromises = []
// 处理视频资源 - 直接取第一个视频
if (data.videos && Array.isArray(data.videos) && data.videos.length > 0) {
const video = data.videos[0] // 直接取第一个视频
resourcePromises.push(getPostVideo(video.original_url).then(videoRes => {
if (videoRes.statusCode === 200 && videoRes.data) {
// 使用uni.arrayBufferToBase64方法参考post.vue的实现
const base64 = uni.arrayBufferToBase64(videoRes.data)
const videoUrl = 'data:video/mp4;base64,' + base64
return {
type: 'video',
src: videoUrl,
original_url: video.original_url,
loading: true,
error: false
}
}
return null
}).catch(error => {
console.warn('获取视频失败:', error)
uni.showToast({
title: '视频加载失败',
icon: 'error'
})
return null
}))
}
// 处理用户头像
if (data.user && data.user.avatar) {
resourcePromises.push(getUserImg(data.user.avatar).then(avatarRes => {
if (avatarRes.statusCode === 200 && avatarRes.data) {
// 将arrayBuffer转换为base64
const base64 = uni.arrayBufferToBase64(avatarRes.data)
const userImgUrl = 'data:image/webp;base64,' + base64
return {
type: 'avatar',
url: userImgUrl
}
}
return null
}).catch(error => {
console.warn('获取用户头像失败:', error)
return null
}))
}
// 等待所有资源加载完成
Promise.all(resourcePromises).then(results => {
// 处理视频结果
const videoResults = results.filter(result => result && result.type === 'video')
if (videoResults.length > 0) {
// 使用第一个视频作为主视频
const mainVideo = videoResults[0]
videoData.videoUrl = mainVideo.src
}
// 处理头像结果
const avatarResult = results.find(result => result && result.type === 'avatar')
if (avatarResult) {
videoData.userImg = avatarResult.url
}
// 准备需要翻译的字段
const userName = data.user?.nickName || '匿名用户'
const copywriting = data.textContent || ''
// 并行翻译需要翻译的内容
Promise.all([
translateZhToEn(userName),
translateZhToEn(copywriting)
]).then(([translatedUserName, translatedCopywriting]) => {
// 更新视频数据 - 使用翻译后的内容
data.userName = translatedUserName
data.date = data.time || data.date || ''
data.copywriting = translatedCopywriting
data.likesum = data.likeCount || 0
data.commentsum = data.commentCount || 0
data.sharesum = data.shareCount || 0
// 标记资源加载完成
finishloading.value = true
// 最后用data覆盖整个videoData
Object.assign(videoData, data)
})
}).catch(error => {
console.error('资源加载失败:', error)
uni.showToast({
title: '资源加载失败',
icon: 'error'
})
})
} else {
throw new Error(`请求失败: ${res.statusCode}`)
}
} catch (error) {
console.error('获取视频数据失败:', error)
uni.showToast({
title: '加载视频失败',
icon: 'error'
})
}
}).catch(error => {
console.error('获取视频列表失败:', error)
uni.showToast({
title: '网络连接异常',
icon: 'error'
})
})
})
</script>
<style scoped>
main {
display: flex;
flex-direction: column;
/* position: relative; */
max-width: 430px;
height: 100vh;
/* max-height: 980px; */
margin: 0 auto;
}
.spacerview {
flex: 1;
pointer-events: none
}
/* 视频容器 */
.video-container {
position: relative;
width: 100%;
max-width: 430px;
height: 100%;
/* height: 100vh; */
/* max-height: 980px; */
flex-shrink: 0;
margin: 0 auto;
display: flex;
flex-direction: column;
}
video {
width: 100%;
/* height: 100%; */
min-height: 680px;
object-fit: contain;
display: flex;
flex-direction: column;
margin: 0 auto;
z-index: 0;
flex: 1;
}
::v-deep .uni-video-cover {
background-color: transparent;
cursor: pointer;
z-index: 1;
}
::v-deep .uni-video-cover-play-button {
width: 100% !important;
height: 100% !important;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
z-index: 1;
}
/* 弹幕区域 */
::v-deep .uni-video-danmu {
height: 15% !important;
margin: 32px 0 0;
}
/* 右侧交互面板 */
.interaction-panel {
position: absolute;
right: 16px;
bottom: 16px;
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
z-index: 2;
pointer-events: auto;
}
/* 用户信息区域 */
.user-section,
.action-btn,
.mutebtn {
cursor: pointer;
}
.user-section {
width: 48px;
height: 60px;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
}
.user-avatar {
width: 48px;
height: 48px;
border-radius: 50%;
border: 2px solid rgba(255, 255, 255, 0.8);
}
.follow-btn-container {
position: absolute;
bottom: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(10px);
border-radius: 50%;
}
/* 交互按钮 */
.action-btn {
width: 30px;
height: 47px;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.action-icon {
width: 30px;
height: 30px;
}
.action-count {
width: 100%;
height: 17px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
color: #fff;
font-weight: 600;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
}
.mutebtn {
width: 40px;
height: 40px;
}
/* 视频信息区域 */
.videoinfo {
width: 294px;
height: auto;
margin: 0 0 17px 16px;
position: absolute;
left: 0;
bottom: 0;
display: flex;
flex-direction: column;
flex-wrap: wrap;
align-items: flex-start;
gap: 8px;
z-index: 2;
}
.vedioinfohead {
height: 24px;
display: flex;
gap: 8px;
}
.username,
.datetime {
display: flex;
align-items: center;
height: 100%;
}
.username {
font-size: 17px;
font-weight: 600;
/* font-family: 'SFPro'; */
color: #fff;
line-height: 1;
}
.datetime {
font-size: 11px;
font-weight: normal;
color: rgba(255, 255, 255, 0.6);
}
.content {
font-size: 13px;
font-weight: normal;
font-stretch: normal;
font-style: normal;
line-height: 1.69;
letter-spacing: normal;
color: rgba(255, 255, 255, 0.8);
max-width: 250px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
height: 22px;
flex-shrink: 0;
transition: all 0.3s ease;
}
.content.expanded {
white-space: normal;
overflow: visible;
text-overflow: clip;
height: auto;
max-height: none;
}
.content-container {
display: flex;
align-items: center;
gap: 4px;
max-width: 273px;
}
.flodbtncontainer {
height: 22px;
width: 16px;
display: flex;
align-self: flex-start;
flex-shrink: 0;
}
.expand-btn {
width: 16px;
height: 16px;
object-fit: contain;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.3s ease;
}
.expand-btn.rotated {
transform: rotate(180deg);
}
.uni-video-cover {
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.uni-video-icon {
font-family: 'uni-video-icon';
text-align: center;
}
.uni-video-cover-play-button {
width: 100% !important;
height: 100% !important;
display: flex;
align-items: center;
justify-content: center;
line-height: 75px;
font-size: 56px;
color: rgba(255, 255, 255, 0.5);
cursor: pointer;
}
.uni-video-cover-play-button::after {
content: '\ea24';
}
.loading-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
</style>