Sfoglia il codice sorgente

使用 gemini 优化 ShortVideo.vue 的 UI, 实现类似抖音的播放页面

reghao 15 ore fa
parent
commit
dc801a37f5
2 ha cambiato i file con 486 aggiunte e 164 eliminazioni
  1. 6 6
      src/router/index.js
  2. 480 158
      src/views/home/ShortVideo.vue

+ 6 - 6
src/router/index.js

@@ -64,12 +64,6 @@ export const constantRoutes = [
         component: TimelineIndex,
         meta: { needAuth: true }
       },
-      {
-        path: '/shortvideo',
-        name: 'ShortVideoIndex',
-        component: ShortVideoIndex,
-        meta: { needAuth: false }
-      },
       {
         path: '/region',
         name: 'RegionIndex',
@@ -126,6 +120,12 @@ export const constantRoutes = [
       }
     ]
   },
+  {
+    path: '/shortvideo',
+    name: 'ShortVideoIndex',
+    component: ShortVideoIndex,
+    meta: { needAuth: false }
+  },
   {
     path: '/bg',
     name: 'BackgroundIndex',

+ 480 - 158
src/views/home/ShortVideo.vue

@@ -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>