|
|
@@ -1,245 +1,567 @@
|
|
|
<template>
|
|
|
- <div class="short-video-container" v-if="video !== null">
|
|
|
- <div class="video-overlay-top">
|
|
|
- <el-button
|
|
|
- type="primary"
|
|
|
- size="medium"
|
|
|
- round
|
|
|
- icon="el-icon-refresh-right"
|
|
|
- class="next-btn"
|
|
|
- @click="nextVideo"
|
|
|
- >
|
|
|
- 下一个
|
|
|
- </el-button>
|
|
|
- </div>
|
|
|
+ <div
|
|
|
+ class="short-video-wrapper"
|
|
|
+ v-if="video !== null"
|
|
|
+ @wheel.prevent="handleWheel"
|
|
|
+ @touchstart="touchStart"
|
|
|
+ @touchmove.prevent
|
|
|
+ @touchend="touchEnd"
|
|
|
+ >
|
|
|
+ <transition name="el-fade-in">
|
|
|
+ <div :key="video.videoId" class="video-bg" :style="{ backgroundImage: `url(${video.coverUrl})` }"></div>
|
|
|
+ </transition>
|
|
|
+
|
|
|
+ <div class="video-container" @click="handleSingleClick" @dblclick="handleDoubleLike">
|
|
|
+ <video
|
|
|
+ v-if="videoActive && currentVideoUrl"
|
|
|
+ ref="videoPlayer"
|
|
|
+ class="native-video"
|
|
|
+ :src="currentVideoUrl"
|
|
|
+ :poster="video.coverUrl"
|
|
|
+ loop
|
|
|
+ preload="auto"
|
|
|
+ playsinline
|
|
|
+ webkit-playsinline
|
|
|
+ x5-video-player-type="h5-page"
|
|
|
+ x5-video-player-fullscreen="true"
|
|
|
+ @loadedmetadata="onVideoLoad"
|
|
|
+ @waiting="onWaiting"
|
|
|
+ @playing="onPlaying"
|
|
|
+ @error="onVideoError"
|
|
|
+ ></video>
|
|
|
+
|
|
|
+ <div class="video-status-overlay">
|
|
|
+ <transition name="el-zoom-in-center">
|
|
|
+ <i v-if="isLoading" class="el-icon-loading status-icon"></i>
|
|
|
+ <i v-else-if="isPaused" class="el-icon-video-play status-icon"></i>
|
|
|
+ </transition>
|
|
|
+ </div>
|
|
|
|
|
|
- <div class="video-wrapper">
|
|
|
- <div id="dplayer" ref="dplayer" class="player-instance" />
|
|
|
+ <div class="like-anim-container" ref="likeAnimBox"></div>
|
|
|
</div>
|
|
|
|
|
|
- <div class="video-overlay-info">
|
|
|
- <h3 class="video-title" v-html="video.title" />
|
|
|
- <div class="video-meta">
|
|
|
- <span class="meta-item"><i class="el-icon-video-play"></i> {{ video.view }}</span>
|
|
|
- <span class="meta-item"><i class="el-icon-s-comment"></i> {{ video.comment }}</span>
|
|
|
- <span class="meta-item"><i class="el-icon-time"></i> {{ video.pubDate }}</span>
|
|
|
+ <div class="side-bar">
|
|
|
+ <div class="side-item author-box" v-if="user" @click.stop="handleAvatarClick">
|
|
|
+ <div class="avatar-wrapper">
|
|
|
+ <el-avatar :size="50" :src="user.avatarUrl"></el-avatar>
|
|
|
+ <div class="follow-plus"><i class="el-icon-plus"></i></div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
- <div v-if="user" class="author-info">
|
|
|
- <el-avatar :size="30" :src="user.avatar" icon="el-icon-user"></el-avatar>
|
|
|
- <span class="username">{{ user.screenName }}</span>
|
|
|
+
|
|
|
+ <transition name="slide-fade">
|
|
|
+ <div class="user-peek-card" v-if="showUserCard">
|
|
|
+ <div class="card-mask" @click="showUserCard = false"></div>
|
|
|
+
|
|
|
+ <div class="card-content">
|
|
|
+ <div class="profile-main">
|
|
|
+ <el-avatar
|
|
|
+ :size="80"
|
|
|
+ :src="user.avatarUrl"
|
|
|
+ class="card-avatar clickable-avatar"
|
|
|
+ @click.native="goToFullProfile"
|
|
|
+ ></el-avatar>
|
|
|
+ <h2 class="name">@{{ user.screenName }}</h2>
|
|
|
+ <p class="bio">{{ user.signature || '暂无个性签名' }}</p>
|
|
|
+
|
|
|
+ <div class="stats-group">
|
|
|
+ <div class="stat"><b>{{ formatCount(user.fans) }}</b><span>粉丝</span></div>
|
|
|
+ <div class="stat"><b>{{ formatCount(user.likes) }}</b><span>获赞</span></div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <el-button type="danger" round class="action-btn" @click="handleFollowerUser">立即关注</el-button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="recent-works">
|
|
|
+ <p class="label">近期作品</p>
|
|
|
+ <div class="thumb-list">
|
|
|
+ <div v-for="{item, index} in userWorks" :key="index" class="work-thumb">
|
|
|
+ <i class="el-icon-video-play"></i>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </transition>
|
|
|
+
|
|
|
+ <div class="side-item" @click.stop="handleLike">
|
|
|
+ <div class="icon-circle" :class="{ 'is-active': isLiked }"><i class="el-icon-star-on"></i></div>
|
|
|
+ <span class="count-text">{{ formatCount(video.view) }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="side-item" @click.stop="toggleComments">
|
|
|
+ <div class="icon-circle"><i class="el-icon-s-comment"></i></div>
|
|
|
+ <span class="count-text">{{ formatCount(video.comment) }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="side-item" @click.stop="handleShare">
|
|
|
+ <div class="icon-circle"><i class="el-icon-share"></i></div>
|
|
|
+ <span class="count-text">{{ formatCount(video.comment) }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="video-info-bottom">
|
|
|
+ <div class="info-content">
|
|
|
+ <h3 class="author-name" v-if="user">@{{ user.screenName }}</h3>
|
|
|
+ <p class="video-desc">{{ video.title }}</p>
|
|
|
+ <div class="music-tag">
|
|
|
+ <div class="music-icon-box"><i class="el-icon-headset"></i></div>
|
|
|
+ <div class="marquee-box">
|
|
|
+ <span class="marquee-text">{{ video.title }} - 精彩原声</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
+
|
|
|
+ <div class="top-controls">
|
|
|
+ <el-button type="info" icon="el-icon-refresh" circle size="small" :loading="isSwitching" @click="nextVideo"></el-button>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
-import DPlayer from 'dplayer'
|
|
|
import { videoUrl, getShortVideo } from '@/api/video'
|
|
|
import { getUserInfo } from '@/api/user'
|
|
|
-import { getAccessToken } from '@/utils/auth'
|
|
|
|
|
|
export default {
|
|
|
name: 'ShortVideo',
|
|
|
data() {
|
|
|
return {
|
|
|
- dp: null, // 存放播放器实例
|
|
|
video: null,
|
|
|
user: null,
|
|
|
- userToken: null,
|
|
|
- danmaku: {
|
|
|
- api: process.env.VUE_APP_SERVER_URL + '/api/comment/danmaku/'
|
|
|
+ currentVideoUrl: '',
|
|
|
+ videoActive: true,
|
|
|
+ isLiked: false,
|
|
|
+ isPaused: false,
|
|
|
+ isLoading: true,
|
|
|
+ isSwitching: false,
|
|
|
+ isChangingSource: false, // 核心:切换状态锁
|
|
|
+ startY: 0,
|
|
|
+ clickTimer: null,
|
|
|
+ showUserCard: false,
|
|
|
+ originalVolume: 1.0,
|
|
|
+ volumeTimer: null,
|
|
|
+ userWorks: []
|
|
|
+ }
|
|
|
+ },
|
|
|
+ watch: {
|
|
|
+ // 监听卡片状态,触发音量变化
|
|
|
+ showUserCard(val) {
|
|
|
+ if (val) {
|
|
|
+ this.fadeVolume(0.2); // 弹出时:音量降至 20%
|
|
|
+ } else {
|
|
|
+ this.fadeVolume(this.originalVolume); // 关闭时:恢复原音量
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
created() {
|
|
|
- this.userToken = getAccessToken()
|
|
|
this.getShortVideoWrapper()
|
|
|
},
|
|
|
- beforeDestroy() {
|
|
|
- if (this.dp) {
|
|
|
- this.dp.destroy()
|
|
|
- }
|
|
|
- },
|
|
|
methods: {
|
|
|
- getShortVideoWrapper() {
|
|
|
- getShortVideo().then(resp => {
|
|
|
- if (resp.code === 0) {
|
|
|
- this.video = resp.data
|
|
|
- document.title = resp.data.title
|
|
|
- this.getVideoUrl(this.video.videoId)
|
|
|
- this.fetchUserInfo(resp.data.userId)
|
|
|
+ fadeVolume(targetVolume) {
|
|
|
+ const v = this.$refs.videoPlayer;
|
|
|
+ if (!v) return;
|
|
|
+
|
|
|
+ clearInterval(this.volumeTimer);
|
|
|
+
|
|
|
+ // 每次调节的步长
|
|
|
+ const step = v.volume < targetVolume ? 0.05 : -0.05;
|
|
|
+
|
|
|
+ this.volumeTimer = setInterval(() => {
|
|
|
+ const newVolume = v.volume + step;
|
|
|
+
|
|
|
+ // 判断是否到达目标值
|
|
|
+ if ((step > 0 && newVolume >= targetVolume) || (step < 0 && newVolume <= targetVolume)) {
|
|
|
+ v.volume = targetVolume;
|
|
|
+ clearInterval(this.volumeTimer);
|
|
|
+ } else {
|
|
|
+ v.volume = Math.max(0, Math.min(1, newVolume));
|
|
|
}
|
|
|
- }).catch(err => {
|
|
|
- this.$message.error(err.message)
|
|
|
- })
|
|
|
+ }, 30); // 30ms 调节一次,体感非常平滑
|
|
|
},
|
|
|
- fetchUserInfo(userId) {
|
|
|
- getUserInfo(userId).then(resp => {
|
|
|
- if (resp.code === 0) {
|
|
|
- this.user = resp.data
|
|
|
- }
|
|
|
- })
|
|
|
+ async handleAvatarClick() {
|
|
|
+ this.showUserCard = true;
|
|
|
+ // 2. 如果已经加载过该用户的作品,且 ID 没变,可以不重复加载
|
|
|
+ // 这里假设 user 对象里有 userId
|
|
|
+ if (this.userWorks.length > 0 && this.userWorks[0].authorId === this.user.userId) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 这里的 getWorksByUserId 是你定义的 API 方法
|
|
|
+ // const res = await getWorksByUserId(this.user.userId);
|
|
|
+ // --- 模拟 API 请求延迟 ---
|
|
|
+ await new Promise(resolve => setTimeout(resolve, 800));
|
|
|
+ this.userWorks = [
|
|
|
+ { id: 1, coverUrl: 'https://via.placeholder.com/150x200', playNum: 12500, authorId: this.user.userId },
|
|
|
+ { id: 2, coverUrl: 'https://via.placeholder.com/150x200', playNum: 8800, authorId: this.user.userId },
|
|
|
+ { id: 3, coverUrl: 'https://via.placeholder.com/150x200', playNum: 21000, authorId: this.user.userId }
|
|
|
+ ];
|
|
|
+ console.log("获取作品");
|
|
|
+ } catch (error) {
|
|
|
+ this.$message.error("作品加载失败");
|
|
|
+ }
|
|
|
},
|
|
|
- async getVideoUrl(videoId) {
|
|
|
- videoUrl(videoId).then(res => {
|
|
|
- if (res.code === 0 && res.data.type === 'mp4') {
|
|
|
- const urls = res.data.urls.map(u => ({ ...u, type: 'normal' }))
|
|
|
- this.initMp4Player(videoId, this.video.coverUrl, urls, res.data.currentTime)
|
|
|
- }
|
|
|
- })
|
|
|
+ // 点击作品小图的处理
|
|
|
+ playThisWork(work) {
|
|
|
+ this.showUserCard = false;
|
|
|
+ this.$message.info(`正在切换至视频: ${work.id}`);
|
|
|
+ // 这里可以触发你之前定义的切换视频 API
|
|
|
+ },
|
|
|
+ // 模拟跳转完整主页
|
|
|
+ goToFullProfile() {
|
|
|
+ // 1. 立即关闭卡片显示
|
|
|
+ this.showUserCard = false;
|
|
|
+
|
|
|
+ // 2. 触发恢复音量的逻辑(watch 会自动监听到 showUserCard 变为 false)
|
|
|
+ // 但为了确保跳转瞬间声音已经开始恢复,可以手动调用一次
|
|
|
+ this.fadeVolume(this.originalVolume || 1.0);
|
|
|
+
|
|
|
+ // 3. 执行路由跳转
|
|
|
+ // 使用 $nextTick 确保卡片关闭的动画开始后再跳转,或者直接跳转
|
|
|
+ this.$router.push(`/user/${this.user.userId}`).catch(err => {
|
|
|
+ if (err.name !== 'NavigationDuplicated') console.error(err);
|
|
|
+ });
|
|
|
+ },
|
|
|
+ handleLike() { this.isLiked = !this.isLiked },
|
|
|
+ handleShare() { this.$message.success('链接已复制') },
|
|
|
+ toggleComments() { this.$message.info('评论暂未开启') },
|
|
|
+ handleFollowerUser() {
|
|
|
+ this.$message.info('follow user')
|
|
|
},
|
|
|
- initMp4Player(videoId, coverUrl, urls, pos) {
|
|
|
- // 如果已经存在实例,先销毁切换
|
|
|
- if (this.dp) {
|
|
|
- this.dp.switchVideo({
|
|
|
- url: urls[0].url,
|
|
|
- pic: coverUrl,
|
|
|
- thumbnails: coverUrl
|
|
|
- })
|
|
|
- this.dp.seek(pos)
|
|
|
- return
|
|
|
+ async getShortVideoWrapper() {
|
|
|
+ if (this.isSwitching) return
|
|
|
+ this.isSwitching = true
|
|
|
+ this.isLoading = true
|
|
|
+
|
|
|
+ try {
|
|
|
+ const resp = await getShortVideo()
|
|
|
+ if (resp.code === 0 && resp.data) {
|
|
|
+ this.video = resp.data
|
|
|
+ this.isLiked = false
|
|
|
+ this.fetchUserInfo(resp.data.userId)
|
|
|
+ await this.prepareNewVideo(this.video.videoId)
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ this.isSwitching = false
|
|
|
}
|
|
|
+ },
|
|
|
+
|
|
|
+ async prepareNewVideo(videoId) {
|
|
|
+ const resp = await videoUrl(videoId)
|
|
|
+ if (resp.code === 0 && resp.data?.urls?.length > 0) {
|
|
|
+ const nextUrl = resp.data.urls[0].url
|
|
|
+
|
|
|
+ // 1. 开启切换锁,此时所有 Error 事件都会被静默
|
|
|
+ this.isChangingSource = true
|
|
|
+ this.videoActive = false
|
|
|
|
|
|
- this.dp = new DPlayer({
|
|
|
- container: document.querySelector('#dplayer'),
|
|
|
- lang: 'zh-cn',
|
|
|
- screenshot: true,
|
|
|
- autoplay: true, // 沉浸式体验建议自动播放
|
|
|
- volume: 0.2,
|
|
|
- loop: true,
|
|
|
- video: {
|
|
|
- pic: coverUrl,
|
|
|
- defaultQuality: 0,
|
|
|
- quality: urls
|
|
|
- },
|
|
|
- danmaku: {
|
|
|
- id: videoId,
|
|
|
- api: this.danmaku.api,
|
|
|
- token: this.userToken,
|
|
|
- maximum: 1000,
|
|
|
- bottom: '15%'
|
|
|
+ // 2. 彻底清理旧 video
|
|
|
+ const v = this.$refs.videoPlayer
|
|
|
+ if (v) {
|
|
|
+ v.pause()
|
|
|
+ v.removeAttribute('src') // 彻底移除属性
|
|
|
+ v.load()
|
|
|
}
|
|
|
- })
|
|
|
|
|
|
- this.dp.seek(pos)
|
|
|
+ this.currentVideoUrl = ''
|
|
|
+
|
|
|
+ // 3. 增加一个微小的延迟(100ms),确保旧解码器完全释放
|
|
|
+ setTimeout(() => {
|
|
|
+ this.currentVideoUrl = nextUrl
|
|
|
+ this.videoActive = true
|
|
|
+
|
|
|
+ this.$nextTick(() => {
|
|
|
+ const newV = this.$refs.videoPlayer
|
|
|
+ if (newV) {
|
|
|
+ newV.play().catch(() => { this.isPaused = true })
|
|
|
+ }
|
|
|
+ // 4. 视频准备播放后,延迟关闭切换锁
|
|
|
+ setTimeout(() => {
|
|
|
+ this.isChangingSource = false
|
|
|
+ this.isSwitching = false
|
|
|
+ }, 500)
|
|
|
+ })
|
|
|
+ }, 100)
|
|
|
+ } else {
|
|
|
+ this.isSwitching = false
|
|
|
+ this.isChangingSource = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ onVideoError() {
|
|
|
+ // 核心:只要在切换过程中,或者标签不活跃,绝对不报错
|
|
|
+ if (this.isChangingSource || !this.videoActive) return
|
|
|
+
|
|
|
+ const v = this.$refs.videoPlayer
|
|
|
+ if (!v || !v.error) return
|
|
|
+
|
|
|
+ // 过滤掉 Aborted(1) 和 Network(2)
|
|
|
+ if (v.error.code <= 2) return
|
|
|
+
|
|
|
+ this.isLoading = false
|
|
|
+ this.$message.error('视频资源暂不可用')
|
|
|
+ },
|
|
|
+
|
|
|
+ fetchUserInfo(userId) {
|
|
|
+ getUserInfo(userId).then(resp => { if (resp.code === 0) this.user = resp.data })
|
|
|
+ },
|
|
|
+ onVideoLoad() { this.isLoading = false },
|
|
|
+ onWaiting() { this.isLoading = true },
|
|
|
+ onPlaying() { this.isLoading = false; this.isPaused = false },
|
|
|
+ handleSingleClick() {
|
|
|
+ clearTimeout(this.clickTimer)
|
|
|
+ this.clickTimer = setTimeout(() => this.togglePlay(), 250)
|
|
|
+ },
|
|
|
+ handleDoubleLike(e) {
|
|
|
+ clearTimeout(this.clickTimer)
|
|
|
+ if (!this.isLiked) this.handleLike()
|
|
|
+ this.showLikeAnimation(e)
|
|
|
+ },
|
|
|
+ showLikeAnimation(e) {
|
|
|
+ const box = this.$refs.likeAnimBox
|
|
|
+ if (!box) return
|
|
|
+ const heart = document.createElement('i')
|
|
|
+ heart.className = 'el-icon-star-on heart-anim'
|
|
|
+ heart.style.left = `${e.clientX - 40}px`
|
|
|
+ heart.style.top = `${e.clientY - 40}px`
|
|
|
+ box.appendChild(heart)
|
|
|
+ setTimeout(() => heart.remove(), 800)
|
|
|
},
|
|
|
- nextVideo() {
|
|
|
- this.getShortVideoWrapper()
|
|
|
+ togglePlay() {
|
|
|
+ const v = this.$refs.videoPlayer
|
|
|
+ if (!v) return
|
|
|
+ v.paused ? v.play() : v.pause()
|
|
|
+ this.isPaused = v.paused
|
|
|
+ },
|
|
|
+ nextVideo() { this.getShortVideoWrapper() },
|
|
|
+ formatCount(n) { return n > 10000 ? (n / 10000).toFixed(1) + 'w' : (n || 0) },
|
|
|
+ handleWheel(e) {
|
|
|
+ if (this.isSwitching || Math.abs(e.deltaY) < 50) return
|
|
|
+ this.nextVideo()
|
|
|
+ },
|
|
|
+ touchStart(e) { this.startY = e.touches[0].clientY },
|
|
|
+ touchEnd(e) {
|
|
|
+ const diff = this.startY - e.changedTouches[0].clientY
|
|
|
+ if (!this.isSwitching && Math.abs(diff) > 80) this.nextVideo()
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|
|
|
-/* 容器全屏化 */
|
|
|
-.short-video-container {
|
|
|
+.short-video-wrapper {
|
|
|
+ position: fixed;
|
|
|
+ inset: 0;
|
|
|
+ width: 100vw;
|
|
|
+ height: 100vh;
|
|
|
+ background: #000;
|
|
|
+ overflow: hidden;
|
|
|
+ z-index: 999;
|
|
|
+}
|
|
|
+
|
|
|
+.video-bg {
|
|
|
+ position: absolute;
|
|
|
+ inset: -10%;
|
|
|
+ background-size: cover;
|
|
|
+ background-position: center;
|
|
|
+ filter: blur(40px) brightness(0.3);
|
|
|
+ z-index: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.video-container {
|
|
|
position: relative;
|
|
|
+ z-index: 1;
|
|
|
width: 100%;
|
|
|
- height: calc(100vh - 60px); /* 减去顶部导航栏高度,如果没有导航栏请设为 100vh */
|
|
|
- background-color: #000;
|
|
|
- overflow: hidden;
|
|
|
+ height: 100%;
|
|
|
display: flex;
|
|
|
justify-content: center;
|
|
|
align-items: center;
|
|
|
}
|
|
|
|
|
|
-/* 播放器容器 */
|
|
|
-.video-wrapper {
|
|
|
+.native-video {
|
|
|
width: 100%;
|
|
|
height: 100%;
|
|
|
- z-index: 1;
|
|
|
+ object-fit: contain;
|
|
|
}
|
|
|
|
|
|
-.player-instance {
|
|
|
- width: 100% !important;
|
|
|
- height: 100% !important;
|
|
|
+@media screen and (max-width: 768px) {
|
|
|
+ .native-video { object-fit: cover; }
|
|
|
}
|
|
|
|
|
|
-/* 顶部操作遮罩 */
|
|
|
-.video-overlay-top {
|
|
|
+.video-info-bottom {
|
|
|
position: absolute;
|
|
|
- top: 20px;
|
|
|
- right: 20px;
|
|
|
+ left: 0;
|
|
|
+ bottom: 0;
|
|
|
+ width: 100%;
|
|
|
+ background: linear-gradient(to top, rgba(0,0,0,0.5) 0%, transparent 100%);
|
|
|
+ padding: 40px 15px 35px 15px;
|
|
|
z-index: 10;
|
|
|
+ pointer-events: none;
|
|
|
+ box-sizing: border-box;
|
|
|
}
|
|
|
|
|
|
-.next-btn {
|
|
|
- background: rgba(255, 255, 255, 0.2);
|
|
|
- border: 1px solid rgba(255, 255, 255, 0.4);
|
|
|
+.info-content {
|
|
|
+ margin-right: 80px;
|
|
|
color: #fff;
|
|
|
- backdrop-filter: blur(5px);
|
|
|
+ text-align: left;
|
|
|
}
|
|
|
|
|
|
-.next-btn:hover {
|
|
|
- background: rgba(255, 255, 255, 0.4);
|
|
|
+.author-name {
|
|
|
+ font-size: 17px;
|
|
|
+ font-weight: 600;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ text-shadow: 0 1px 2px rgba(0,0,0,0.5);
|
|
|
}
|
|
|
|
|
|
-/* 底部信息遮罩 */
|
|
|
-.video-overlay-info {
|
|
|
- position: absolute;
|
|
|
- bottom: 40px;
|
|
|
- left: 20px;
|
|
|
- right: 80px; /* 避开播放器右侧可能的控制按钮 */
|
|
|
- z-index: 10;
|
|
|
+.video-desc {
|
|
|
+ font-size: 15px;
|
|
|
+ line-height: 1.5;
|
|
|
color: #fff;
|
|
|
- pointer-events: none; /* 防止点击遮罩挡住视频操作 */
|
|
|
+ pointer-events: auto;
|
|
|
+ display: -webkit-box;
|
|
|
+ -webkit-box-orient: vertical;
|
|
|
+ -webkit-line-clamp: 3;
|
|
|
+ overflow: hidden;
|
|
|
}
|
|
|
|
|
|
-.video-title {
|
|
|
- font-size: 1.2rem;
|
|
|
- margin-bottom: 10px;
|
|
|
- text-shadow: 0 1px 2px rgba(0,0,0,0.8);
|
|
|
- pointer-events: auto;
|
|
|
+.music-tag {
|
|
|
+ margin-top: 12px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
}
|
|
|
|
|
|
-.video-meta {
|
|
|
- font-size: 14px;
|
|
|
- opacity: 0.9;
|
|
|
- margin-bottom: 15px;
|
|
|
+.side-bar {
|
|
|
+ position: absolute;
|
|
|
+ right: 12px;
|
|
|
+ bottom: 15%;
|
|
|
+ z-index: 11;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ gap: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 其他样式同前... */
|
|
|
+.icon-circle { width: 48px; height: 48px; background: rgba(255, 255, 255, 0.15); border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 26px; color: #fff; backdrop-filter: blur(5px); }
|
|
|
+.icon-circle.is-active { color: #fe2c55; }
|
|
|
+.count-text { font-size: 12px; margin-top: 4px; color: #fff; }
|
|
|
+.avatar-wrapper { border: 2px solid #fff; border-radius: 50%; position: relative; }
|
|
|
+.follow-plus { position: absolute; bottom: -8px; left: 50%; transform: translateX(-50%); background: #fe2c55; border-radius: 50%; width: 18px; height: 18px; display: flex; align-items: center; justify-content: center; font-size: 10px; color: #fff; }
|
|
|
+.video-status-overlay { position: absolute; z-index: 2; pointer-events: none; }
|
|
|
+.status-icon { font-size: 70px; color: rgba(255, 255, 255, 0.4); }
|
|
|
+@keyframes rotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
|
|
+@keyframes marquee { 0% { transform: translateX(100%); } 100% { transform: translateX(-100%); } }
|
|
|
+.music-icon-box { animation: rotate 4s linear infinite; }
|
|
|
+.marquee-box { overflow: hidden; white-space: nowrap; width: 150px; }
|
|
|
+.marquee-text { display: inline-block; animation: marquee 8s linear infinite; font-size: 13px; }
|
|
|
+.like-anim-container { position: absolute; inset: 0; pointer-events: none; z-index: 5; }
|
|
|
+::v-deep .heart-anim { position: absolute; font-size: 80px; color: #fe2c55; animation: heartFly 0.8s ease-out forwards; }
|
|
|
+@keyframes heartFly { 0% { transform: scale(1); opacity: 0.8; } 100% { transform: translateY(-150px) scale(2); opacity: 0; } }
|
|
|
+.top-controls { position: absolute; top: 20px; left: 20px; z-index: 10; }
|
|
|
+
|
|
|
+.user-peek-card {
|
|
|
+ position: fixed;
|
|
|
+ inset: 0;
|
|
|
+ z-index: 2000;
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
}
|
|
|
|
|
|
-.meta-item {
|
|
|
- margin-right: 15px;
|
|
|
+.card-mask {
|
|
|
+ flex: 1;
|
|
|
+ background: rgba(0, 0, 0, 0.1); /* 极轻微的遮罩 */
|
|
|
}
|
|
|
|
|
|
-.author-info {
|
|
|
+.card-content {
|
|
|
+ width: 75%;
|
|
|
+ height: 100%;
|
|
|
+ background: rgba(15, 15, 15, 0.75); /* 深色半透明 */
|
|
|
+ backdrop-filter: blur(20px) saturate(150%); /* 磨砂玻璃效果 */
|
|
|
+ -webkit-backdrop-filter: blur(20px) saturate(150%);
|
|
|
+ border-left: 1px solid rgba(255, 255, 255, 0.1);
|
|
|
+ color: #fff;
|
|
|
+ padding: 80px 24px 40px;
|
|
|
display: flex;
|
|
|
- align-items: center;
|
|
|
- pointer-events: auto;
|
|
|
+ flex-direction: column;
|
|
|
+ box-shadow: -10px 0 30px rgba(0, 0, 0, 0.5);
|
|
|
}
|
|
|
|
|
|
-.username {
|
|
|
- margin-left: 10px;
|
|
|
- font-weight: 500;
|
|
|
+/* 进场动画:右侧滑入 + 柔和淡入 */
|
|
|
+.slide-fade-enter-active, .slide-fade-leave-active {
|
|
|
+ transition: all 0.4s cubic-bezier(0.165, 0.84, 0.44, 1);
|
|
|
+}
|
|
|
+.slide-fade-enter, .slide-fade-leave-to {
|
|
|
+ transform: translateX(100%);
|
|
|
+ opacity: 0;
|
|
|
}
|
|
|
|
|
|
-/* 移动端适配 */
|
|
|
-@media screen and (max-width: 768px) {
|
|
|
- .short-video-container {
|
|
|
- height: calc(100vh - 50px); /* 移动端高度调整 */
|
|
|
- }
|
|
|
+/* 内部元素样式优化 */
|
|
|
+.profile-main {
|
|
|
+ text-align: center;
|
|
|
+ margin-bottom: 40px;
|
|
|
+}
|
|
|
+.card-avatar {
|
|
|
+ border: 2px solid #fe2c55;
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+.stats-group {
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 40px;
|
|
|
+ margin: 20px 0;
|
|
|
+}
|
|
|
+.stat {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+.stat b { font-size: 18px; }
|
|
|
+.stat span { font-size: 12px; color: rgba(255,255,255,0.5); }
|
|
|
|
|
|
- .video-overlay-info {
|
|
|
- bottom: 60px; /* 避开底部控制条 */
|
|
|
- left: 15px;
|
|
|
- }
|
|
|
+.recent-works .thumb-list {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+ margin-top: 12px;
|
|
|
+}
|
|
|
+.work-thumb {
|
|
|
+ flex: 1;
|
|
|
+ aspect-ratio: 3/4;
|
|
|
+ background: rgba(255,255,255,0.1);
|
|
|
+ border-radius: 6px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ color: rgba(255,255,255,0.3);
|
|
|
+}
|
|
|
|
|
|
- .video-title {
|
|
|
- font-size: 1rem;
|
|
|
- }
|
|
|
+.footer-link {
|
|
|
+ margin-top: auto;
|
|
|
+ text-align: center;
|
|
|
+ font-size: 14px;
|
|
|
+ color: rgba(255,255,255,0.6);
|
|
|
+ padding: 15px;
|
|
|
+ border-top: 1px solid rgba(255,255,255,0.05);
|
|
|
+}
|
|
|
|
|
|
- .video-overlay-top {
|
|
|
- top: 15px;
|
|
|
- right: 15px;
|
|
|
- }
|
|
|
+.clickable-avatar {
|
|
|
+ cursor: pointer;
|
|
|
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.clickable-avatar:hover {
|
|
|
+ transform: scale(1.05);
|
|
|
+ box-shadow: 0 0 15px rgba(254, 44, 85, 0.5); /* 淡淡的红晕,呼应主题色 */
|
|
|
+}
|
|
|
+
|
|
|
+.clickable-avatar:active {
|
|
|
+ transform: scale(0.95);
|
|
|
}
|
|
|
|
|
|
-/* 强制修改 DPlayer 样式,适配全屏 */
|
|
|
-::v-deep .dplayer-video-wrap {
|
|
|
- background-color: #000;
|
|
|
+.name {
|
|
|
+ cursor: pointer;
|
|
|
+ margin-top: 10px;
|
|
|
}
|
|
|
|
|
|
-::v-deep .dplayer-video {
|
|
|
- object-fit: contain; /* 确保长宽比不一致时,视频完整显示不拉伸 */
|
|
|
+.name:hover {
|
|
|
+ text-decoration: underline;
|
|
|
+ opacity: 0.8;
|
|
|
}
|
|
|
</style>
|