Files
akmon/pages/info/video-player.uvue
2026-01-20 08:04:15 +08:00

1205 lines
27 KiB
Plaintext
Raw Permalink 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>
<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>