记录:从零到一做小说阅读器能遇到多少坑(二)

2,400 阅读2分钟

前言

本以为不熟悉的nestjs会出的幺蛾子更大,没想到前端页面才更容易出幺蛾子

image.png

GitHub 链接

demo

五花八门的问题

继上文我们拿到小说内容后,目前最大的问题就是小说的展示,翻页等功能

小说排版问题

  • 使用 CSS 的 columns 属性实现内容分栏布局。保证容器高度占满屏幕,实现类似电子书的阅读效果。

    • column-width: 定义每列的宽度。
    • column-gap: 设置列间距。
  • 如果希望实现平滑翻页,可以加上transition: 0.4s ease-in

<template>
  <div ref="wrapperRef" class="book-wrapper" >
    <div ref="contentRef" class="book">
      <div v-text="content" />
      <div ref="endRef" class="book-end">
        读完啦!
      </div>
    </div>
    <div class="book-bottom flex items-center">
      <div class="flex-1 text-right font-size-12 color-gray-7">
        {{ current }}/{{ total }}
      </div>
    </div>
  </div>
</template>

<style lang="scss" scoped>
.book-wrapper {
  height: 100%;
  width: 100dvw;
  position: fixed;
  inset: 0;
  overflow: hidden;
}

.book {
  columnGap: 16px;
  columns: calc(100% - 32px) 1;
}
</style>

页数计算

  • 根据容器宽度和endRef的offsetLeft计算总页数。
  • 监听翻页操作(如点击或滑动事件),通过调整容器的 translateX 实现翻页。
  • 利用变量current来存储当前页,动态计算translateX

总页数是个很不稳定的因素,屏幕大小、字体、行高、间距等都会影响,没法作为进度存储,总体思路上还是通过current/total来存储进度的,在获取进度和提交进度是再根据当前计算的total*progress来换算,目前没有具体测试过这种方式的可行性,以及是否存在偏差

const width = wrapperRef.value?.clientWidth - configs.value.gap
let total = Math.ceil(endRef.value?.offsetLeft / width)

const translateX = computed(() => {
  return (current.value - 1) * width * -1
})

点击、滑动事件监听

  • 左右滑动翻页
  • 上下滑动添加书签
let startX, endX, startY, endY
function onTouchStart(e) {
  // 记录触摸开始的位置
  startX = e.touches[0].clientX
  startY = e.touches[0].clientY
}

function onTouchEnd(e) {
  endX = e.changedTouches[0].clientX // 记录触摸结束的位置
  endY = e.changedTouches[0].clientY
  const diffX = startX - endX // 计算水平滑动的距离
  const diffY = startY - endY // 计算水平滑动的距离

  // 左右滑动翻页,上下滑动添加书签
  if (Math.abs(diffX) >= Math.abs(diffY) && Math.abs(diffX) > 30) { // 设定滑动的最小距离(阈值)
    next(diffX > 0 ? 1 : -1)
  }
  else if (Math.abs(diffX) < Math.abs(diffY) && Math.abs(diffY) > 30) {
    onToggleMark()
  }
}
  • 点击左侧右侧翻页
  • 点击中间展示底部设置栏顶部navbar
const bottomPopRef = ref()
const topPopRef = ref()
function onClick(e) {
  const clickX = e.clientX // 获取点击的X坐标
  const middle = wrapperRef.value.clientWidth / 2 // 页面中间的X坐标

  if (clickX < middle / 2) {
    next(-1)
  }
  else if (clickX > middle + middle / 2) {
    // 点击在页面的右侧
    next(1)
  }
  else {
    // 点击在页面的中间
    bottomPopRef.value.onToggle()
    topPopRef.value.onToggle()
  }
}

段落缩进问题

下载的小说,格式千奇百怪,为了符合自身的阅读习惯,开始思考如何让页面尽量整齐一些

  • 为每段加上p标签,利用css属性text-indent: 2em;来实现首行缩进
 content.value = content
      .replace('<', '&lt;')
      .replace('>', '&gt;')
      .replace('script', '')
      .split('\n')
      .map(line => `<p>${line.replace(/^\s*/g, '')}</p>`)
      .join('')
  • 段落分割了,是不是章节也可以这么分割,顺便存储章节,方便后续快速选择
 content.value = content
      .replace('<', '&lt;')
      .replace('>', '&gt;')
      .replace('script', '')
      .replace(/^(第\s*[一二三四五六七八九十百千万\d]+\s*[章回].*)$/gm, (match) => {
        const chapter = { title: match, id: `chapter${chapters.value.length}` }
        chapters.value.push(chapter)
        return `<div class="chapter" id="${chapter.id}">${match}</div>`
      })
      .split('\n')
      .map(line => `<p>${line.replace(/^\s*/g, '')}</p>`)
      .join('')