Files
test-subject/src/pages/video.vue

604 lines
14 KiB
Vue
Raw Normal View History

2025-10-14 18:04:20 +08:00
<template>
2025-11-03 10:53:55 +08:00
<main v-if="finishloading">
2025-10-27 15:03:40 +08:00
<!-- 视频容器 -->
<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">
2025-10-14 18:04:20 +08:00
2025-10-27 15:03:40 +08:00
<!-- 播放按钮 - 直接使用浏览器复制的样式 -->
<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>
2025-10-14 18:04:20 +08:00
</view>
2025-10-27 15:03:40 +08:00
<!-- 点赞按钮 -->
<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>
2025-10-14 18:04:20 +08:00
</view>
2025-10-27 15:03:40 +08:00
<!-- 评论按钮 -->
<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>
2025-10-14 18:04:20 +08:00
2025-10-27 15:03:40 +08:00
<!-- 分享按钮 -->
<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>
2025-10-14 18:04:20 +08:00
</view>
2025-10-27 15:03:40 +08:00
<!-- 视频信息 -->
<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 /> -->
2025-10-14 18:04:20 +08:00
2025-10-20 14:55:15 +08:00
2025-10-27 15:03:40 +08:00
</main>
<!-- Findmore -->
<!-- <Findmore /> -->
2025-10-14 18:04:20 +08:00
2025-11-03 10:53:55 +08:00
<view v-else class="loading-container">
2025-12-04 18:52:57 +08:00
<text>Loading...</text>
2025-11-03 10:53:55 +08:00
</view>
2025-10-14 18:04:20 +08:00
</template>
<script setup>
2025-10-27 15:03:40 +08:00
import { onLoad } from '@dcloudio/uni-app'
import { ref, reactive, getCurrentInstance, computed } from 'vue'
2025-10-14 18:04:20 +08:00
import { useCommonStore } from '@/stores/common.js'
2025-10-20 14:55:15 +08:00
import Head from '@/pages/head/head.vue'
import Comments from '@/pages/comments/comments.vue'
2025-10-27 15:03:40 +08:00
import Intereact from '@/pages/intereact/intereact.vue'
import { getPostList, getPostVideo, getUserImg } from '@/api/api.js'
2025-12-04 15:32:55 +08:00
// 导入翻译工具
2025-12-04 18:52:57 +08:00
import { translateZhToEn, translationCache } from '@/utils/translate.js'
2025-10-14 18:04:20 +08:00
const common = useCommonStore()
const formatCount = common.formatCount
const formatDate = common.formatDate
2025-10-27 15:03:40 +08:00
const videoData = reactive({})
2025-11-03 10:53:55 +08:00
// 资源加载状态
const finishloading = ref(false)
2025-10-14 18:04:20 +08:00
// 折叠展开状态
const isExpanded = ref(false)
// 静音状态
const isMuted = ref(false)
// 播放状态
const isPlaying = ref(true) // 默认播放状态
2025-10-27 15:03:40 +08:00
// 计算属性:是否需要显示展开按钮
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
2025-10-14 18:04:20 +08:00
}
2025-10-27 15:03:40 +08:00
}
// 如果截断后的文本和原文本相同,不需要加省略号
if (result === videoData.copywriting) {
return result
}
return result + '...'
2025-10-14 18:04:20 +08:00
})
// 切换展开状态
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
}
}
}
2025-10-16 14:14:26 +08:00
// 统一处理交互操作
const handleInteraction = () => {
2025-10-14 18:04:20 +08:00
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);
}
}
2025-10-27 15:03:40 +08:00
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
}
2025-12-04 15:32:55 +08:00
// 准备需要翻译的字段
const userName = data.user?.nickName || '匿名用户'
const copywriting = data.textContent || ''
// 并行翻译需要翻译的内容
Promise.all([
2025-12-04 18:52:57 +08:00
translateZhToEn(userName),
translateZhToEn(copywriting)
2025-12-04 15:32:55 +08:00
]).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)
})
2025-10-27 15:03:40 +08:00
}).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'
})
})
})
2025-10-14 18:04:20 +08:00
</script>
<style scoped>
2025-10-27 15:03:40 +08:00
main {
display: flex;
flex-direction: column;
2025-11-03 10:53:55 +08:00
/* position: relative; */
2025-10-27 15:03:40 +08:00
max-width: 430px;
2025-11-03 10:53:55 +08:00
height: 100vh;
/* max-height: 980px; */
2025-10-27 15:03:40 +08:00
margin: 0 auto;
}
.spacerview {
flex: 1;
pointer-events: none
}
/* 视频容器 */
.video-container {
position: relative;
2025-10-14 18:04:20 +08:00
width: 100%;
2025-10-20 14:55:15 +08:00
max-width: 430px;
2025-11-03 10:53:55 +08:00
height: 100%;
/* height: 100vh; */
/* max-height: 980px; */
2025-10-27 15:03:40 +08:00
flex-shrink: 0;
margin: 0 auto;
2025-10-14 18:04:20 +08:00
display: flex;
flex-direction: column;
}
2025-10-27 15:03:40 +08:00
video {
2025-10-14 18:04:20 +08:00
width: 100%;
2025-11-03 10:53:55 +08:00
/* height: 100%; */
2025-10-20 14:55:15 +08:00
min-height: 680px;
2025-11-03 10:53:55 +08:00
object-fit: contain;
2025-10-27 15:03:40 +08:00
display: flex;
flex-direction: column;
margin: 0 auto;
z-index: 0;
flex: 1;
2025-10-14 18:04:20 +08:00
}
::v-deep .uni-video-cover {
background-color: transparent;
2025-10-27 15:03:40 +08:00
cursor: pointer;
z-index: 1;
2025-10-14 18:04:20 +08:00
}
::v-deep .uni-video-cover-play-button {
width: 100% !important;
height: 100% !important;
display: flex;
align-items: center;
justify-content: center;
2025-10-27 15:03:40 +08:00
cursor: pointer;
z-index: 1;
2025-10-14 18:04:20 +08:00
}
/* 弹幕区域 */
::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;
2025-10-27 15:03:40 +08:00
pointer-events: auto;
2025-10-14 18:04:20 +08:00
}
/* 用户信息区域 */
.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;
2025-10-27 15:03:40 +08:00
text-align: center;
2025-10-14 18:04:20 +08:00
}
.action-icon {
width: 30px;
height: 30px;
}
.action-count {
2025-10-27 15:03:40 +08:00
width: 100%;
height: 17px;
display: flex;
align-items: center;
justify-content: center;
2025-10-14 18:04:20 +08:00
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;
2025-10-27 15:03:40 +08:00
flex-direction: column;
2025-10-14 18:04:20 +08:00
flex-wrap: wrap;
align-items: flex-start;
gap: 8px;
2025-10-27 15:03:40 +08:00
z-index: 2;
2025-10-14 18:04:20 +08:00
}
.vedioinfohead {
height: 24px;
display: flex;
gap: 8px;
}
2025-10-27 15:03:40 +08:00
.username,
.datetime {
display: flex;
align-items: center;
height: 100%;
}
2025-10-14 18:04:20 +08:00
.username {
font-size: 17px;
font-weight: 600;
2025-10-27 15:03:40 +08:00
/* font-family: 'SFPro'; */
2025-10-14 18:04:20 +08:00
color: #fff;
2025-10-27 15:03:40 +08:00
line-height: 1;
2025-10-14 18:04:20 +08:00
}
.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 {
2025-10-16 14:55:51 +08:00
width: 100% !important;
height: 100% !important;
2025-10-14 18:04:20 +08:00
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';
}
2025-11-03 10:53:55 +08:00
.loading-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
2025-10-14 18:04:20 +08:00
</style>