网易云桌面端--精选歌单布局思路记录

最近在学习electron想做一个自己喜欢的桌面端的软件,这边选择了网易云音乐,这边记录一下自己实现布局和功能的思路

查看图片可以发现这个页面内容包含了三个部分,左边箭头,右边箭头,中间的内容区域,这边开始将基本的布局框架搭建出来

html 复制代码
 <div class="scroll-warp group">
 <!-- 左箭头-->
    <div
      class="arrow left-arrow transition-opacity duration-300"
      :class="{ disabled: isAtStart }"
      @click="scroll('left')"
    >
      <Icon style="width: 100%; height: 100%" icon="tabler:chevron-left"></Icon>
    </div>
    <!--内容-->
    <div class="content" ref="contentRef" @scroll="handleScroll">
      <MusicItemCard v-for="(item, index) in 8" :key="index"></MusicItemCard>
    </div>
    <!-- 右箭头 -->
    <div
      class="arrow right-arrow transition-opacity duration-300"
      :class="{ disabled: isAtEnd }"
      @click="scroll('right')"
    >
      <Icon style="width: 100%; height: 100%" icon="tabler:chevron-right"></Icon>
    </div>
  </div>

有了基本的容器,我们就需要将样式完善出来

css 复制代码
.scroll-warp {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  background-color: pink;
  padding: 10px;

  // 箭头的通用样式
  .arrow {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 100%;
    min-height: 40px; // 防止高度为0
    cursor: pointer;
    z-index: 10;

    // 默认隐藏,父容器 hover 时显示
    opacity: 0;
    transition:
      opacity 0.3s ease,
      background-color 0.3s;

    // 禁用状态样式
    &.disabled {
      opacity: 0.5 !important; // 即使 hover 也保持半透明
      cursor: not-allowed;
      // background-color: #ccc; // 变灰
      pointer-events: none; // 禁止点击
    }
  }

  // 当鼠标移入 scroll-warp 时,显示箭头
  &:hover .arrow {
    opacity: 1;
  }

  .content {
    flex: 1;
    flex-shrink: 0;
    background-color: rgb(0, 255, 183);
    overflow-x: scroll;
    overflow-y: hidden;
    white-space: nowrap;
    display: flex;
    align-items: center;
    padding: 10px;
    gap: 10px;
    flex-wrap: nowrap;

    // 隐藏滚动条
    &::-webkit-scrollbar {
      display: none;
    }

    // 兼容其他浏览器隐藏滚动条
    -ms-overflow-style: none; /* IE and Edge */
    scrollbar-width: none; /* Firefox */

    margin: 0 10px;
  }
}

然后我们就可以得到一个这样的布局界面

接下来我们来实现一下js逻辑

js 复制代码
import { ref, onMounted, onUnmounted } from 'vue'
import MusicItemCard from './MusicItemCard.vue'

// 获取内容区域的 DOM 引用
const contentRef = ref(null)

// 定义状态变量
const isAtStart = ref(true) // 是否在最左侧
const isAtEnd = ref(false) // 是否在最右侧

// 滚动处理函数
const scroll = (direction) => {
  if (!contentRef.value) return

  // 每次滚动的距离,这里设置为容器宽度的 80%,也可以设置为固定像素如 300
  const scrollAmount = contentRef.value.clientWidth * 0.8

  if (direction === 'left') {
    contentRef.value.scrollBy({ left: -scrollAmount, behavior: 'smooth' })
  } else {
    contentRef.value.scrollBy({ left: scrollAmount, behavior: 'smooth' })
  }
}

// 监听滚动事件,更新按钮状态
const handleScroll = () => {
  if (!contentRef.value) return

  const { scrollLeft, scrollWidth, clientWidth } = contentRef.value

  // 判断是否在起点(允许 1px 的误差)
  isAtStart.value = scrollLeft <= 1

  // 判断是否在终点(scrollLeft + clientWidth >= scrollWidth)
  // 这里减去 1 是为了处理浮点数计算可能存在的微小误差,或者为了留一点边距
  isAtEnd.value = Math.ceil(scrollLeft + clientWidth) >= scrollWidth - 1
}

// 组件挂载和卸载时处理窗口大小变化(可选,为了更严谨)
const updateScrollState = () => handleScroll()

onMounted(() => {
  // 初始化时检查一次状态
  updateScrollState()
  // 监听窗口大小变化,因为窗口变化可能导致可滚动宽度变化
  window.addEventListener('resize', updateScrollState)
})

onUnmounted(() => {
  window.removeEventListener('resize', updateScrollState)
})

这样我们就可以实现这种的布局切换容器和界面了

如何需要MusicItemCard代码

vue 复制代码
<template>
  <div class="music-card">
    123
  </div>
</template>

<script setup>
import { ref,reactive,getCurrentInstance} from 'vue'
const { proxy } = getCurrentInstance()
</script>

<style scoped lang="scss">
.music-card {
  width: 140px;
  height: 190px;
  border-radius: 6px;
  background-color: #fff;
  flex-shrink: 0;
  margin: 0 5px;
}
</style>

完整代码

vue 复制代码
<template>
  <div class="scroll-warp group">
    <!-- 左箭头 -->
    <!-- 
      1. 添加 @click 事件
      2. 动态绑定 class,当 isAtStart 为 true 时添加 disabled 样式
      3. 添加 opacity-0 和 group-hover:opacity-100 类实现鼠标移入显示
    -->
    <div
      class="arrow left-arrow transition-opacity duration-300"
      :class="{ disabled: isAtStart }"
      @click="scroll('left')"
    >
      <Icon style="width: 100%; height: 100%" icon="tabler:chevron-left"></Icon>
    </div>

    <!-- 内容区域 -->
    <!-- 
      1. 绑定 ref 以便在 JS 中获取 DOM 元素
      2. 监听 scroll 事件以更新状态
    -->
    <div class="content" ref="contentRef" @scroll="handleScroll">
      <!-- 这里的 item 只是演示,实际使用请传入你的数据 -->
      <MusicItemCard v-for="(item, index) in 8" :key="index"></MusicItemCard>
    </div>

    <!-- 右箭头 -->
    <div
      class="arrow right-arrow transition-opacity duration-300"
      :class="{ disabled: isAtEnd }"
      @click="scroll('right')"
    >
      <Icon style="width: 100%; height: 100%" icon="tabler:chevron-right"></Icon>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import MusicItemCard from './MusicItemCard.vue'

// 获取内容区域的 DOM 引用
const contentRef = ref(null)

// 定义状态变量
const isAtStart = ref(true) // 是否在最左侧
const isAtEnd = ref(false) // 是否在最右侧

// 滚动处理函数
const scroll = (direction) => {
  if (!contentRef.value) return

  // 每次滚动的距离,这里设置为容器宽度的 80%,也可以设置为固定像素如 300
  const scrollAmount = contentRef.value.clientWidth * 0.8

  if (direction === 'left') {
    contentRef.value.scrollBy({ left: -scrollAmount, behavior: 'smooth' })
  } else {
    contentRef.value.scrollBy({ left: scrollAmount, behavior: 'smooth' })
  }
}

// 监听滚动事件,更新按钮状态
const handleScroll = () => {
  if (!contentRef.value) return

  const { scrollLeft, scrollWidth, clientWidth } = contentRef.value

  // 判断是否在起点(允许 1px 的误差)
  isAtStart.value = scrollLeft <= 1

  // 判断是否在终点(scrollLeft + clientWidth >= scrollWidth)
  // 这里减去 1 是为了处理浮点数计算可能存在的微小误差,或者为了留一点边距
  isAtEnd.value = Math.ceil(scrollLeft + clientWidth) >= scrollWidth - 1
}

// 组件挂载和卸载时处理窗口大小变化(可选,为了更严谨)
const updateScrollState = () => handleScroll()

onMounted(() => {
  // 初始化时检查一次状态
  updateScrollState()
  // 监听窗口大小变化,因为窗口变化可能导致可滚动宽度变化
  window.addEventListener('resize', updateScrollState)
})

onUnmounted(() => {
  window.removeEventListener('resize', updateScrollState)
})
</script>

<style scoped lang="scss">
.scroll-warp {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  background-color: pink;
  padding: 10px;
  position: relative; // 如果箭头需要绝对定位可以开启

  // 箭头的通用样式
  .arrow {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 100%;
    min-height: 40px; // 防止高度为0
    cursor: pointer;
    z-index: 10;

    // 默认隐藏,父容器 hover 时显示 (Tailwind CSS 写法: opacity-0 group-hover:opacity-100)
    opacity: 0;
    transition:
      opacity 0.3s ease,
      background-color 0.3s;

    &:hover {
      // background-color: darkorange;
    }

    // 禁用状态样式
    &.disabled {
      opacity: 0.5 !important; // 即使 hover 也保持半透明
      cursor: not-allowed;
      // background-color: #ccc; // 变灰
      pointer-events: none; // 禁止点击
    }
  }

  // 当鼠标移入 scroll-warp 时,显示箭头
  &:hover .arrow {
    opacity: 1;
  }

  .content {
    flex: 1;
    flex-shrink: 0;
    background-color: rgb(0, 255, 183);
    overflow-x: scroll;
    overflow-y: hidden;
    white-space: nowrap;
    display: flex;
    align-items: center;
    padding: 10px;
    gap: 10px; // 使用 gap 代替 margin 控制间距
    flex-wrap: nowrap;

    // 隐藏滚动条
    &::-webkit-scrollbar {
      display: none;
    }

    // 兼容其他浏览器隐藏滚动条
    -ms-overflow-style: none; /* IE and Edge */
    scrollbar-width: none; /* Firefox */

    margin: 0 10px;
  }
}
</style>
相关推荐
anew___23 分钟前
《从零手写操作系统 (20):ELF加载器——让OS读懂现代编译器》
java·服务器·前端
ss27324 分钟前
AI全栈实战 | 2.3-01 组件库实战:Element Plus 怎么搭出专业界面,过度封装为什么成维护负担
前端·javascript·elementui·vue
对空六课30 分钟前
动态按钮ID变化时的稳定埋点标识设计
前端·数据分析
福兮说34 分钟前
URL 编码的七个坑:c++ 传到后端变空格、%25 套娃、截断 emoji 直接报错
开发语言·前端·javascript·node.js·url
吴声子夜歌39 分钟前
Nginx应用与运维——Nginx Web服务应用实战(Python网站的搭建)
运维·前端·nginx
小的时候可菜了1 小时前
JS 异步的本质:从单线程到 thenable(一条进化链)
开发语言·前端·javascript
liangshanbo12151 小时前
Webpack vs Vite 底层原理高频追问的问题
前端·webpack·node.js
可乐鸡翅yeah_1 小时前
hls.js 配置 loadStartPosition,点播起始播放位置踩坑
开发语言·javascript·网络协议·ecmascript·m3u8在线
web打印社区1 小时前
Windows 网页静默打印设置步骤:客户端、防火墙与联调清单
开发语言·前端·javascript·chrome·pdf·ecmascript
恋猫de小郭2 小时前
KMP 又改了编译流程, Separate Compilation 禁止了 `commonMain` 的依赖穿透
android·前端·flutter