604 lines
14 KiB
Vue
604 lines
14 KiB
Vue
<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> |