1205 lines
27 KiB
Plaintext
1205 lines
27 KiB
Plaintext
<!-- 视频播放页面 - 支持弹幕、评论、收藏、转发、点赞等功能 -->
|
||
<template>
|
||
<scroll-view direction="vertical" class="video-page" :scroll-y="true">
|
||
<!-- 视频播放器 -->
|
||
<view class="video-container">
|
||
<video
|
||
ref="videoPlayer"
|
||
id="videoPlayer"
|
||
class="video-player"
|
||
:src="videoData?.video_url || ''"
|
||
:poster="videoData?.video_poster || ''"
|
||
:autoplay="false"
|
||
:controls="true"
|
||
:enable-danmu="danmuConfig.enabled"
|
||
:danmu-btn="true"
|
||
:danmu-list="currentDanmuList"
|
||
:show-fullscreen-btn="true"
|
||
:show-play-btn="true"
|
||
:show-center-play-btn="true"
|
||
:object-fit="'contain'"
|
||
@play="onVideoPlay"
|
||
@pause="onVideoPause"
|
||
@ended="onVideoEnded"
|
||
@timeupdate="onVideoTimeUpdate"
|
||
@fullscreenchange="onFullscreenChange"
|
||
@error="onVideoError">
|
||
|
||
<!-- 自定义弹幕输入框(全屏时显示) -->
|
||
<view v-if="playerState.fullscreen && danmuConfig.enabled" class="danmu-input-container">
|
||
<input
|
||
id="danmu-input"
|
||
class="danmu-input"
|
||
v-model="danmuInputText"
|
||
:placeholder="tt('mt.video.danmu.placeholder')"
|
||
:maxlength="100"
|
||
@confirm="sendDanmu"
|
||
@focus="onDanmuInputFocus"
|
||
@blur="onDanmuInputBlur">
|
||
</input>
|
||
<view class="danmu-send-btn" @click="sendDanmu">
|
||
<text class="send-btn-text">{{ tt('mt.video.danmu.send') }}</text>
|
||
</view>
|
||
</view>
|
||
</video>
|
||
|
||
<!-- 加载状态 -->
|
||
<view v-if="playerState.loading" class="video-loading">
|
||
<text class="loading-text">{{ tt('mt.status.loading') }}</text>
|
||
</view>
|
||
|
||
<!-- 错误状态 -->
|
||
<view v-if="playerState.error" class="video-error">
|
||
<text class="error-text">{{ playerState.error }}</text>
|
||
<view class="retry-btn" @click="retryVideoLoad">
|
||
<text class="retry-text">{{ tt('mt.action.retry') }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 视频信息 -->
|
||
<view class="video-info" v-if="videoData">
|
||
<view class="video-header">
|
||
<text class="video-title">{{ videoData.title }}</text>
|
||
<view class="video-meta">
|
||
<text class="meta-item">{{ formatViewCount(videoData.view_count) }}{{ tt('mt.video.unit.views') }}</text>
|
||
<text class="meta-item">{{ formatRelativeTimeKey(videoData.published_at) }}</text>
|
||
<text class="meta-item">{{ formatVideoDuration(videoData.video_duration) }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 操作按钮 -->
|
||
<view class="action-buttons">
|
||
<view class="action-btn" :class="{ active: videoData.is_liked }" @click="toggleLike">
|
||
<text class="action-icon">{{ videoData.is_liked ? '❤️' : '🤍' }}</text>
|
||
<text class="action-text">{{ formatViewCount(videoData.like_count) }}</text>
|
||
</view>
|
||
|
||
<view class="action-btn" :class="{ active: videoData.is_favorited }" @click="toggleFavorite">
|
||
<text class="action-icon">{{ videoData.is_favorited ? '⭐' : '☆' }}</text>
|
||
<text class="action-text">{{ tt('mt.video.action.favorite') }}</text>
|
||
</view>
|
||
|
||
<view class="action-btn" @click="showShareModal">
|
||
<text class="action-icon">📤</text>
|
||
<text class="action-text">{{ formatViewCount(videoData.share_count) }}</text>
|
||
</view>
|
||
|
||
<view class="action-btn" @click="toggleDanmu">
|
||
<text class="action-icon">💬</text>
|
||
<text class="action-text">{{ danmuConfig.enabled ? tt('mt.video.danmu.hide') : tt('mt.video.danmu.show') }}</text>
|
||
</view>
|
||
|
||
<view class="action-btn" @click="showSettingsModal">
|
||
<text class="action-icon">⚙️</text>
|
||
<text class="action-text">{{ tt('mt.video.settings') }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 视频描述 -->
|
||
<view class="video-description" v-if="videoData">
|
||
<view class="description-header">
|
||
<text class="author-name">{{ videoData.author }}</text>
|
||
<view class="description-toggle" @click="toggleDescription">
|
||
<text class="toggle-text">{{ showFullDescription ? tt('mt.video.description.collapse') : tt('mt.video.description.expand') }}</text>
|
||
</view>
|
||
</view>
|
||
<text class="description-content" :class="{ expanded: showFullDescription }">{{ videoData.summary || videoData.content }}</text>
|
||
</view>
|
||
|
||
<!-- 弹幕区域(非全屏时) -->
|
||
<view class="danmu-section" v-if="!playerState.fullscreen">
|
||
<view class="section-header">
|
||
<text class="section-title">{{ tt('mt.video.danmu.title') }} ({{ videoData?.danmu_count || 0 }})</text>
|
||
<view class="danmu-toggle" @click="toggleDanmu">
|
||
<text class="toggle-text">{{ danmuConfig.enabled ? tt('mt.video.danmu.hide') : tt('mt.video.danmu.show') }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 弹幕输入 -->
|
||
<view class="danmu-input-section" v-if="danmuConfig.enabled">
|
||
<input
|
||
class="danmu-input-field"
|
||
v-model="danmuInputText"
|
||
:placeholder="tt('mt.video.danmu.placeholder')"
|
||
:maxlength="100">
|
||
</input>
|
||
<view class="danmu-color-picker">
|
||
<view
|
||
v-for="color in DANMU_COLOR_OPTIONS"
|
||
:key="color"
|
||
class="color-option"
|
||
:class="{ active: selectedDanmuColor === color }"
|
||
:style="{ backgroundColor: color }"
|
||
@click="selectedDanmuColor = color">
|
||
</view>
|
||
</view>
|
||
<view class="danmu-send-button" @click="sendDanmu" :disabled="pageState.sending_danmu">
|
||
<text class="send-button-text">{{ pageState.sending_danmu ? tt('mt.status.loading') : tt('mt.video.danmu.send') }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 弹幕列表 -->
|
||
<view class="danmu-list" v-if="danmuConfig.enabled">
|
||
<view
|
||
v-for="danmu in danmuList"
|
||
:key="danmu.id"
|
||
class="danmu-item">
|
||
<text class="danmu-time">{{ formatVideoDuration(danmu.time_point) }}</text>
|
||
<text class="danmu-text" :style="{ color: danmu.color }">{{ danmu.text }}</text>
|
||
<text class="danmu-user">{{ danmu.user_name }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 评论区域 -->
|
||
<view class="comments-section">
|
||
<view class="section-header">
|
||
<text class="section-title">{{ tt('mt.comment.title') }} ({{ videoData?.comment_count || 0 }})</text>
|
||
</view>
|
||
|
||
<!-- 评论输入 -->
|
||
<view class="comment-input-section">
|
||
<textarea
|
||
class="comment-input-field"
|
||
v-model="commentInputText"
|
||
:placeholder="tt('mt.comment.placeholder')"
|
||
:maxlength="500"
|
||
:auto-height="true">
|
||
</textarea>
|
||
<view class="comment-send-button" @click="postComment" :disabled="pageState.posting_comment">
|
||
<text class="send-button-text">{{ pageState.posting_comment ? tt('mt.status.loading') : tt('mt.comment.submit') }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 评论列表 -->
|
||
<Comments
|
||
:targetType="'video'"
|
||
:targetId="videoData?.id || ''"
|
||
:userId="currentUserId"
|
||
:userName="currentUserName">
|
||
</Comments>
|
||
</view>
|
||
|
||
<!-- 分享模态框 -->
|
||
<view v-if="showShareModalVisible" class="modal-overlay" @click="hideShareModal">
|
||
<view class="share-modal" @click.stop>
|
||
<view class="modal-header">
|
||
<text class="modal-title">{{ tt('mt.share.title') }}</text>
|
||
<text class="modal-close" @click="hideShareModal">✕</text>
|
||
</view>
|
||
<view class="share-options">
|
||
<view
|
||
v-for="option in SHARE_PLATFORM_OPTIONS"
|
||
:key="option.platform"
|
||
class="share-option"
|
||
@click="shareToplatform(option.platform)">
|
||
<text class="share-icon" :style="{ color: option.color }">{{ option.icon }}</text>
|
||
<text class="share-name">{{ tt(option.name) }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 设置模态框 -->
|
||
<view v-if="showSettingsModalVisible" class="modal-overlay" @click="hideSettingsModal">
|
||
<view class="settings-modal" @click.stop>
|
||
<view class="modal-header">
|
||
<text class="modal-title">{{ tt('mt.video.settings') }}</text>
|
||
<text class="modal-close" @click="hideSettingsModal">✕</text>
|
||
</view>
|
||
<view class="settings-content">
|
||
<!-- 视频质量设置 -->
|
||
<view class="setting-group">
|
||
<text class="setting-label">{{ tt('mt.video.quality.title') }}</text>
|
||
<view class="setting-options">
|
||
<view
|
||
v-for="quality in VIDEO_QUALITY_OPTIONS"
|
||
:key="quality.value"
|
||
class="setting-option"
|
||
:class="{ active: currentQuality === quality.value }"
|
||
@click="changeVideoQuality(quality.value)">
|
||
<text class="option-text">{{ tt(quality.text) }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 播放速度设置 -->
|
||
<view class="setting-group">
|
||
<text class="setting-label">{{ tt('mt.video.speed.title') }}</text>
|
||
<view class="setting-options">
|
||
<view
|
||
v-for="rate in PLAYBACK_RATE_OPTIONS"
|
||
:key="rate.value"
|
||
class="setting-option"
|
||
:class="{ active: playerState.playback_rate === rate.value }"
|
||
@click="changePlaybackRate(rate.value)">
|
||
<text class="option-text">{{ rate.text.startsWith('mt.') ? tt(rate.text) : rate.text }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 弹幕设置 -->
|
||
<view class="setting-group">
|
||
<text class="setting-label">{{ tt('mt.video.danmu.settings') }}</text>
|
||
<view class="setting-item">
|
||
<text class="item-label">{{ tt('mt.video.danmu.opacity') }}</text>
|
||
<slider class="setting-slider" :value="danmuConfig.opacity" :max="100" @change="onDanmuOpacityChange" />
|
||
</view>
|
||
<view class="setting-item">
|
||
<text class="item-label">{{ tt('mt.video.danmu.fontSize') }}</text>
|
||
<slider class="setting-slider" :value="danmuConfig.font_size" :min="16" :max="36" @change="onDanmuFontSizeChange" />
|
||
</view>
|
||
<view class="setting-item">
|
||
<text class="item-label">{{ tt('mt.video.danmu.speed') }}</text>
|
||
<slider class="setting-slider" :value="danmuConfig.speed" :min="1" :max="5" @change="onDanmuSpeedChange" />
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import Comments from './comments.uvue'
|
||
import {
|
||
VideoContent,
|
||
DanmuData,
|
||
DanmuSendData,
|
||
VideoPageState,
|
||
VideoPlayerState,
|
||
DanmuConfig,
|
||
formatVideoDuration,
|
||
formatViewCount,
|
||
validateDanmuText,
|
||
VIDEO_QUALITY_OPTIONS,
|
||
PLAYBACK_RATE_OPTIONS,
|
||
DANMU_COLOR_OPTIONS,
|
||
SHARE_PLATFORM_OPTIONS
|
||
} from './video-types.uts'
|
||
import { formatRelativeTimeKey } from './types.uts'
|
||
import supa from '@/components/supadb/aksupainstance.uts'
|
||
import { tt } from '@/utils/i18nfun.uts'
|
||
import i18n from '@/i18n/index.uts'
|
||
|
||
// 页面参数
|
||
const videoId = ref<string>('')
|
||
|
||
// 视频数据
|
||
const videoData = ref<VideoContent | null>(null)
|
||
|
||
// 页面状态
|
||
const pageState = ref<VideoPageState>({
|
||
loading: false,
|
||
error: null,
|
||
danmu_loading: false,
|
||
comment_loading: false,
|
||
sending_danmu: false,
|
||
posting_comment: false
|
||
})
|
||
|
||
// 播放器状态
|
||
const playerState = ref<VideoPlayerState>({
|
||
playing: false,
|
||
current_time: 0,
|
||
duration: 0,
|
||
volume: 100,
|
||
playback_rate: 1.0,
|
||
fullscreen: false,
|
||
quality: '720p',
|
||
loading: false,
|
||
error: null
|
||
})
|
||
|
||
// 弹幕配置
|
||
const danmuConfig = ref<DanmuConfig>({
|
||
enabled: true,
|
||
opacity: 80,
|
||
font_size: 25,
|
||
speed: 1,
|
||
show_area: 50,
|
||
max_count: 50,
|
||
filter_enabled: false,
|
||
filter_keywords: []
|
||
})
|
||
|
||
// UI 状态
|
||
const showFullDescription = ref<boolean>(false)
|
||
const showShareModalVisible = ref<boolean>(false)
|
||
const showSettingsModalVisible = ref<boolean>(false)
|
||
|
||
// 输入状态
|
||
const danmuInputText = ref<string>('')
|
||
const commentInputText = ref<string>('')
|
||
const selectedDanmuColor = ref<string>('#FFFFFF')
|
||
|
||
// 当前播放质量
|
||
const currentQuality = ref<string>('720p')
|
||
|
||
// 用户信息
|
||
const currentUserId = ref<string>('00000000-0000-0000-0000-000000000000')
|
||
const currentUserName = ref<string>('匿名用户')
|
||
|
||
// 弹幕数据
|
||
const danmuList = ref<Array<DanmuData>>([])
|
||
const currentDanmuList = ref<Array<any>>([])
|
||
|
||
// 视频上下文
|
||
let videoContext: VideoContext | null = null
|
||
|
||
|
||
|
||
onReady(() => {
|
||
videoContext = uni.createVideoContext('videoPlayer')
|
||
})
|
||
|
||
// 加载视频数据
|
||
const loadVideoData = async () => {
|
||
pageState.value.loading = true
|
||
pageState.value.error = null
|
||
|
||
try {
|
||
const result = await supa.from('vw_video_content_detail')
|
||
.select('*')
|
||
.eq('id', videoId.value)
|
||
.executeAs<VideoContent>()
|
||
|
||
if (result.error) {
|
||
pageState.value.error = tt('mt.error.loadFailed')
|
||
return
|
||
}
|
||
|
||
if (result.data) {
|
||
const res =result.data as Array<VideoContent>
|
||
videoData.value = res[0]
|
||
currentQuality.value = videoData.value.video_quality || '720p'
|
||
// 记录观看
|
||
recordView()
|
||
}
|
||
} catch (error) {
|
||
console.error('加载视频数据失败:', error)
|
||
pageState.value.error = tt('mt.error.loadFailed')
|
||
} finally {
|
||
pageState.value.loading = false
|
||
}
|
||
}
|
||
|
||
// 加载弹幕列表
|
||
const loadDanmuList = async () => {
|
||
pageState.value.danmu_loading = true
|
||
|
||
try {
|
||
const result = await supa.from('vw_video_danmakus')
|
||
.select('*')
|
||
.eq('content_id', videoId.value)
|
||
.order('time_point', { ascending: true })
|
||
.executeAs<DanmuData>()
|
||
|
||
if (result.error) {
|
||
console.error('加载弹幕失败:', result.error)
|
||
return
|
||
}
|
||
|
||
if (result.data) {
|
||
danmuList.value = result.data as Array<DanmuData>
|
||
updateCurrentDanmuList()
|
||
}
|
||
} catch (error) {
|
||
console.error('加载弹幕异常:', error)
|
||
} finally {
|
||
pageState.value.danmu_loading = false
|
||
}
|
||
}
|
||
|
||
// 更新当前显示的弹幕列表
|
||
const updateCurrentDanmuList = () => {
|
||
if (!danmuConfig.value.enabled) {
|
||
currentDanmuList.value = []
|
||
return
|
||
}
|
||
|
||
// 转换为video组件需要的格式
|
||
currentDanmuList.value = danmuList.value.map(danmu => ({
|
||
text: danmu.text,
|
||
color: danmu.color,
|
||
time: danmu.time_point
|
||
}))
|
||
}
|
||
|
||
// 视频播放事件
|
||
const onVideoPlay = () => {
|
||
playerState.value.playing = true
|
||
}
|
||
|
||
const onVideoPause = () => {
|
||
playerState.value.playing = false
|
||
}
|
||
|
||
const onVideoEnded = () => {
|
||
playerState.value.playing = false
|
||
// 记录播放完成
|
||
recordPlayCompletion()
|
||
}
|
||
|
||
const onVideoTimeUpdate = (e: any) => {
|
||
playerState.value.current_time = e.detail.currentTime
|
||
playerState.value.duration = e.detail.duration
|
||
|
||
// 更新播放记录
|
||
updatePlayRecord()
|
||
}
|
||
|
||
const onFullscreenChange = (e: any) => {
|
||
playerState.value.fullscreen = e.detail.fullScreen
|
||
}
|
||
|
||
const onVideoError = (e: UniVideoErrorEvent) => {
|
||
// playerState.value.error = tt('mt.video.error.playFailed')
|
||
console.error('视频播放错误:', e)
|
||
}
|
||
|
||
// 发送弹幕
|
||
const sendDanmu = async () => {
|
||
const validation = validateDanmuText(danmuInputText.value)
|
||
if (!validation.valid) {
|
||
uni.showToast({
|
||
title: validation.error || '',
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
|
||
pageState.value.sending_danmu = true
|
||
|
||
try {
|
||
const danmuData: DanmuSendData = {
|
||
text: danmuInputText.value,
|
||
time_point: playerState.value.current_time,
|
||
color: selectedDanmuColor.value,
|
||
font_size: danmuConfig.value.font_size,
|
||
position_type: 'scroll',
|
||
speed: danmuConfig.value.speed
|
||
}
|
||
|
||
// 保存到数据库
|
||
const result = await supa.from('ak_video_danmakus')
|
||
.insert({
|
||
content_id: videoId.value,
|
||
user_id: currentUserId.value??null,
|
||
user_name: currentUserName.value,
|
||
...danmuData
|
||
})
|
||
.execute()
|
||
|
||
if (result.error) {
|
||
uni.showToast({
|
||
title: tt('mt.video.danmu.error.sendFailed'),
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
|
||
// 立即发送到播放器
|
||
videoContext?.sendDanmu({
|
||
text: danmuData.text,
|
||
color: danmuData.color
|
||
})
|
||
|
||
// 清空输入
|
||
danmuInputText.value = ''
|
||
|
||
// 重新加载弹幕列表
|
||
loadDanmuList()
|
||
|
||
uni.showToast({
|
||
title: tt('mt.video.danmu.sendSuccess'),
|
||
icon: 'success'
|
||
})
|
||
} catch (error) {
|
||
console.error('发送弹幕失败:', error)
|
||
uni.showToast({
|
||
title: tt('mt.video.danmu.error.sendFailed'),
|
||
icon: 'none'
|
||
})
|
||
} finally {
|
||
pageState.value.sending_danmu = false
|
||
}
|
||
}
|
||
|
||
// 切换点赞
|
||
const toggleLike = async () => {
|
||
if (!videoData.value) return
|
||
|
||
try {
|
||
if (videoData.value.is_liked) {
|
||
// 取消点赞
|
||
await supa.from('ak_user_interactions')
|
||
.delete()
|
||
.eq('user_id', currentUserId.value)
|
||
.eq('content_id', videoId.value)
|
||
.eq('interaction_type', 'like')
|
||
.execute()
|
||
|
||
videoData.value.is_liked = false
|
||
videoData.value.like_count = Math.max(0, videoData.value.like_count - 1)
|
||
} else {
|
||
// 点赞
|
||
await supa.from('ak_user_interactions')
|
||
.insert({
|
||
user_id: currentUserId.value,
|
||
content_id: videoId.value,
|
||
interaction_type: 'like'
|
||
})
|
||
.execute()
|
||
|
||
videoData.value.is_liked = true
|
||
videoData.value.like_count += 1
|
||
}
|
||
} catch (error) {
|
||
console.error('点赞操作失败:', error)
|
||
}
|
||
}
|
||
|
||
// 切换收藏
|
||
const toggleFavorite = async () => {
|
||
if (!videoData.value) return
|
||
|
||
try {
|
||
if (videoData.value.is_favorited) {
|
||
// 取消收藏
|
||
await supa.from('ak_user_interactions')
|
||
.delete()
|
||
.eq('user_id', currentUserId.value)
|
||
.eq('content_id', videoId.value)
|
||
.eq('interaction_type', 'favorite')
|
||
.execute()
|
||
|
||
videoData.value.is_favorited = false
|
||
videoData.value.favorite_count = Math.max(0, videoData.value.favorite_count - 1)
|
||
} else {
|
||
// 收藏
|
||
await supa.from('ak_user_interactions')
|
||
.insert({
|
||
user_id: currentUserId.value,
|
||
content_id: videoId.value,
|
||
interaction_type: 'favorite'
|
||
})
|
||
.execute()
|
||
|
||
videoData.value.is_favorited = true
|
||
videoData.value.favorite_count += 1
|
||
}
|
||
} catch (error) {
|
||
console.error('收藏操作失败:', error)
|
||
}
|
||
}
|
||
|
||
// 记录观看
|
||
const recordView = async () => {
|
||
try {
|
||
await supa.from('ak_user_interactions')
|
||
.insert({
|
||
user_id: currentUserId.value??null,
|
||
content_id: videoId.value,
|
||
interaction_type: 'view'
|
||
})
|
||
.execute()
|
||
} catch (error) {
|
||
console.error('记录观看失败:', error)
|
||
}
|
||
}
|
||
|
||
// 更新播放记录
|
||
const updatePlayRecord = async () => {
|
||
// 节流更新,每5秒更新一次
|
||
// 实际实现中可以使用防抖或节流
|
||
}
|
||
|
||
// 记录播放完成
|
||
const recordPlayCompletion = async () => {
|
||
try {
|
||
await supa.from('ak_video_play_records')
|
||
.insert({
|
||
content_id: videoId.value,
|
||
user_id: currentUserId.value,
|
||
play_position: playerState.value.current_time,
|
||
play_duration: playerState.value.current_time,
|
||
play_percentage: Math.round((playerState.value.current_time / playerState.value.duration) * 100),
|
||
is_completed: true,
|
||
device_type: 'mobile',
|
||
resolution: currentQuality.value,
|
||
play_speed: playerState.value.playback_rate
|
||
})
|
||
.execute()
|
||
} catch (error) {
|
||
console.error('记录播放完成失败:', error)
|
||
}
|
||
}
|
||
|
||
// UI交互函数
|
||
const toggleDescription = () => {
|
||
showFullDescription.value = !showFullDescription.value
|
||
}
|
||
|
||
const toggleDanmu = () => {
|
||
danmuConfig.value.enabled = !danmuConfig.value.enabled
|
||
updateCurrentDanmuList()
|
||
}
|
||
|
||
const showShareModal = () => {
|
||
showShareModalVisible.value = true
|
||
}
|
||
|
||
const hideShareModal = () => {
|
||
showShareModalVisible.value = false
|
||
}
|
||
|
||
const showSettingsModal = () => {
|
||
showSettingsModalVisible.value = true
|
||
}
|
||
|
||
const hideSettingsModal = () => {
|
||
showSettingsModalVisible.value = false
|
||
}
|
||
|
||
const shareToplatform = async (platform: string) => {
|
||
// 记录分享
|
||
try {
|
||
await supa.from('ak_user_interactions')
|
||
.insert({
|
||
user_id: currentUserId.value??null,
|
||
content_id: videoId.value,
|
||
interaction_type: 'share',
|
||
interaction_data: { platform }
|
||
})
|
||
.execute()
|
||
|
||
if (videoData.value) {
|
||
videoData.value.share_count += 1
|
||
}
|
||
} catch (error) {
|
||
console.error('记录分享失败:', error)
|
||
}
|
||
|
||
// 执行分享逻辑
|
||
if (platform === 'link') {
|
||
// 复制链接
|
||
uni.setClipboardData({
|
||
data: `https://example.com/video/${videoId.value}`,
|
||
success: () => {
|
||
uni.showToast({
|
||
title: tt('mt.share.success'),
|
||
icon: 'success'
|
||
})
|
||
}
|
||
})
|
||
} else {
|
||
// 其他平台分享
|
||
uni.showToast({
|
||
title: tt('mt.share.processing'),
|
||
icon: 'none'
|
||
})
|
||
}
|
||
|
||
hideShareModal()
|
||
}
|
||
|
||
const postComment = async () => {
|
||
if (!commentInputText.value.trim()) {
|
||
uni.showToast({
|
||
title: tt('mt.comment.error.empty'),
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
|
||
pageState.value.posting_comment = true
|
||
|
||
try {
|
||
const result = await supa.from('ak_content_comments')
|
||
.insert({
|
||
content_id: videoId.value,
|
||
user_id: currentUserId.value,
|
||
user_name: currentUserName.value,
|
||
content: commentInputText.value
|
||
})
|
||
.execute()
|
||
|
||
if (result.error) {
|
||
uni.showToast({
|
||
title: tt('mt.comment.error.postFailed'),
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
|
||
// 清空输入
|
||
commentInputText.value = ''
|
||
|
||
// 更新评论数量
|
||
if (videoData.value) {
|
||
videoData.value.comment_count += 1
|
||
}
|
||
|
||
uni.showToast({
|
||
title: tt('mt.comment.postSuccess'),
|
||
icon: 'success'
|
||
})
|
||
} catch (error) {
|
||
console.error('发表评论失败:', error)
|
||
uni.showToast({
|
||
title: tt('mt.comment.error.postFailed'),
|
||
icon: 'none'
|
||
})
|
||
} finally {
|
||
pageState.value.posting_comment = false
|
||
}
|
||
}
|
||
|
||
// 设置相关函数
|
||
const changeVideoQuality = (quality: string) => {
|
||
currentQuality.value = quality
|
||
// 实际实现中需要切换视频源
|
||
hideSettingsModal()
|
||
}
|
||
|
||
const changePlaybackRate = (rate: number) => {
|
||
playerState.value.playback_rate = rate
|
||
videoContext?.playbackRate(rate)
|
||
}
|
||
|
||
const onDanmuOpacityChange = (e: any) => {
|
||
danmuConfig.value.opacity = e.detail.value
|
||
}
|
||
|
||
const onDanmuFontSizeChange = (e: any) => {
|
||
danmuConfig.value.font_size = e.detail.value
|
||
}
|
||
|
||
const onDanmuSpeedChange = (e: any) => {
|
||
danmuConfig.value.speed = e.detail.value
|
||
}
|
||
|
||
const onDanmuInputFocus = () => {
|
||
// 弹幕输入框获得焦点
|
||
}
|
||
|
||
const onDanmuInputBlur = () => {
|
||
// 弹幕输入框失去焦点
|
||
}
|
||
|
||
const retryVideoLoad = () => {
|
||
playerState.value.error = null
|
||
loadVideoData()
|
||
}
|
||
|
||
// 生命周期
|
||
onLoad((options: OnLoadOptions) => {
|
||
if (options.id !== undefined) {
|
||
videoId.value = options.id as string
|
||
}
|
||
if (videoId.value !== '') {
|
||
loadVideoData()
|
||
loadDanmuList()
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<style>
|
||
.video-page {
|
||
flex: 1;
|
||
background-color: #000000;
|
||
}
|
||
|
||
.video-container {
|
||
position: relative;
|
||
width: 100%;
|
||
height: 240px;
|
||
}
|
||
|
||
.video-player {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.danmu-input-container {
|
||
position: absolute;
|
||
bottom: 80px;
|
||
left: 20px;
|
||
right: 20px;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
}
|
||
|
||
.danmu-input {
|
||
flex: 1;
|
||
height: 40px;
|
||
background-color: rgba(0, 0, 0, 0.6);
|
||
color: #ffffff;
|
||
border-radius: 20px;
|
||
padding: 0 15px;
|
||
margin-right: 10px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.danmu-send-btn {
|
||
background-color: #ff6b35;
|
||
border-radius: 20px;
|
||
padding: 8px 16px;
|
||
}
|
||
|
||
.send-btn-text {
|
||
color: #ffffff;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.video-loading, .video-error {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
justify-content: center;
|
||
align-items: center;
|
||
background-color: rgba(0, 0, 0, 0.8);
|
||
}
|
||
|
||
.loading-text, .error-text {
|
||
color: #ffffff;
|
||
font-size: 16px;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.retry-btn {
|
||
background-color: #ff6b35;
|
||
border-radius: 20px;
|
||
padding: 8px 16px;
|
||
}
|
||
|
||
.retry-text {
|
||
color: #ffffff;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.video-info {
|
||
background-color: #ffffff;
|
||
padding: 16px;
|
||
}
|
||
|
||
.video-header {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.video-title {
|
||
font-size: 18px;
|
||
font-weight: bold;
|
||
color: #1f2937;
|
||
line-height: 24px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.video-meta {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
}
|
||
|
||
.meta-item {
|
||
font-size: 12px;
|
||
color: #6b7280;
|
||
margin-right: 16px;
|
||
}
|
||
|
||
.action-buttons {
|
||
flex-direction: row;
|
||
justify-content: space-around;
|
||
align-items: center;
|
||
padding: 16px 0;
|
||
border-top-width: 1px;
|
||
border-top-color: #e5e7eb;
|
||
}
|
||
|
||
.action-btn {
|
||
align-items: center;
|
||
padding: 8px;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.action-btn.active {
|
||
background-color: #fef3f2;
|
||
}
|
||
|
||
.action-icon {
|
||
font-size: 20px;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.action-text {
|
||
font-size: 12px;
|
||
color: #6b7280;
|
||
}
|
||
|
||
.video-description {
|
||
background-color: #ffffff;
|
||
margin-top: 8px;
|
||
padding: 16px;
|
||
}
|
||
|
||
.description-header {
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.author-name {
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
color: #1f2937;
|
||
}
|
||
|
||
.description-toggle, .danmu-toggle {
|
||
padding: 4px 8px;
|
||
}
|
||
|
||
.toggle-text {
|
||
font-size: 12px;
|
||
color: #3b82f6;
|
||
}
|
||
|
||
.description-content {
|
||
font-size: 14px;
|
||
color: #4b5563;
|
||
line-height: 20px;
|
||
max-height: 60px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.description-content.expanded {
|
||
max-height: none;
|
||
}
|
||
|
||
.danmu-section, .comments-section {
|
||
background-color: #ffffff;
|
||
margin-top: 8px;
|
||
padding: 16px;
|
||
}
|
||
|
||
.section-header {
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
color: #1f2937;
|
||
}
|
||
|
||
.danmu-input-section, .comment-input-section {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.danmu-input-field, .comment-input-field {
|
||
width: 100%;
|
||
min-height: 40px;
|
||
background-color: #f9fafb;
|
||
border-radius: 8px;
|
||
padding: 8px 12px;
|
||
font-size: 14px;
|
||
border-width: 1px;
|
||
border-color: #e5e7eb;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.danmu-color-picker {
|
||
flex-direction: row;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.color-option {
|
||
width: 30px;
|
||
height: 30px;
|
||
border-radius: 15px;
|
||
margin-right: 8px;
|
||
border-width: 2px;
|
||
border-color: transparent;
|
||
}
|
||
|
||
.color-option.active {
|
||
border-color: #3b82f6;
|
||
}
|
||
|
||
.danmu-send-button, .comment-send-button {
|
||
background-color: #3b82f6;
|
||
border-radius: 6px;
|
||
padding: 8px 16px;
|
||
align-self: flex-end;
|
||
}
|
||
|
||
.send-button-text {
|
||
color: #ffffff;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.danmu-list {
|
||
max-height: 200px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.danmu-item {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
padding: 8px 0;
|
||
border-bottom-width: 1px;
|
||
border-bottom-color: #f3f4f6;
|
||
}
|
||
|
||
.danmu-time {
|
||
font-size: 12px;
|
||
color: #6b7280;
|
||
width: 60px;
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.danmu-text {
|
||
flex: 1;
|
||
font-size: 14px;
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.danmu-user {
|
||
font-size: 12px;
|
||
color: #9ca3af;
|
||
width: 80px;
|
||
text-align: right;
|
||
}
|
||
|
||
.modal-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background-color: rgba(0, 0, 0, 0.5);
|
||
z-index: 1000;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
|
||
.share-modal, .settings-modal {
|
||
background-color: #ffffff;
|
||
border-radius: 12px;
|
||
margin: 20px;
|
||
max-width: 400px;
|
||
width: 90%;
|
||
}
|
||
|
||
.modal-header {
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 16px;
|
||
border-bottom-width: 1px;
|
||
border-bottom-color: #e5e7eb;
|
||
}
|
||
|
||
.modal-title {
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
color: #1f2937;
|
||
}
|
||
|
||
.modal-close {
|
||
font-size: 18px;
|
||
color: #6b7280;
|
||
padding: 4px;
|
||
}
|
||
|
||
.share-options {
|
||
padding: 16px;
|
||
flex-direction: row;
|
||
flex-wrap: wrap;
|
||
justify-content: space-around;
|
||
}
|
||
|
||
.share-option {
|
||
align-items: center;
|
||
padding: 16px;
|
||
width: 25%;
|
||
}
|
||
|
||
.share-icon {
|
||
font-size: 24px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.share-name {
|
||
font-size: 12px;
|
||
color: #4b5563;
|
||
text-align: center;
|
||
}
|
||
|
||
.settings-content {
|
||
padding: 16px;
|
||
}
|
||
|
||
.setting-group {
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.setting-label {
|
||
font-size: 14px;
|
||
font-weight: bold;
|
||
color: #1f2937;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.setting-options {
|
||
flex-direction: row;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.setting-option {
|
||
background-color: #f3f4f6;
|
||
border-radius: 6px;
|
||
padding: 8px 12px;
|
||
margin-right: 8px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.setting-option.active {
|
||
background-color: #3b82f6;
|
||
}
|
||
|
||
.option-text {
|
||
font-size: 12px;
|
||
color: #4b5563;
|
||
}
|
||
|
||
.setting-option.active .option-text {
|
||
color: #ffffff;
|
||
}
|
||
|
||
.setting-item {
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.item-label {
|
||
font-size: 14px;
|
||
color: #4b5563;
|
||
width: 120px;
|
||
}
|
||
|
||
.setting-slider {
|
||
flex: 1;
|
||
margin-left: 16px;
|
||
}
|
||
</style>
|