ChapterContent.vue 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. <template>
  2. <div class="title" data-chapterpos="0" ref="titleRef">{{ title }}</div>
  3. <div
  4. v-for="(para, index) in contents"
  5. :key="index"
  6. ref="paragraphRef"
  7. :data-chapterpos="chapterPos[index]"
  8. >
  9. <img
  10. class="full"
  11. v-if="/^\s*<img[^>]*src[^>]+>$/.test(String(para))"
  12. :src="getImageSrc(para)"
  13. @error.once="proxyImage"
  14. loading="lazy"
  15. />
  16. <p v-else :style="{ fontFamily, fontSize }" v-html="para" />
  17. </div>
  18. </template>
  19. <script setup lang="ts">
  20. import { isLegadoUrl } from '@/utils/utils'
  21. import API from '@api'
  22. import jump from '@/plugins/jump'
  23. import type { webReadConfig } from '@/web'
  24. const store = useBookStore()
  25. const readWidth = computed(() => store.config.readWidth)
  26. const bookUrl = computed(() => store.readingBook.bookUrl)
  27. const props = defineProps<{
  28. chapterIndex: number
  29. contents: Array<string>
  30. title: string
  31. spacing: webReadConfig['spacing']
  32. fontFamily: string
  33. fontSize: string
  34. }>()
  35. const getImageSrc = (content: string) => {
  36. const imgPattern = /<img[^>]*src="([^"]*(?:"[^>]+\})?)"[^>]*>/
  37. const src = content.match(imgPattern)![1] //reg tested in template
  38. if (isLegadoUrl(src))
  39. return API.getProxyImageUrl(
  40. bookUrl.value,
  41. src,
  42. useBookStore().config.readWidth,
  43. )
  44. return src
  45. }
  46. const proxyImage = (event: Event) => {
  47. ;(event.target as HTMLImageElement).src = API.getProxyImageUrl(
  48. bookUrl.value,
  49. (event.target as HTMLImageElement).src,
  50. readWidth.value,
  51. )
  52. }
  53. const calculateWordCount = (paragraph: string) => {
  54. const imgPattern = /<img[^>]*src="[^"]*(?:"[^>]+\})?"[^>]*>/g
  55. //内嵌图片文字为1
  56. const imagePlaceHolder = ' '
  57. return paragraph.replaceAll(imgPattern, imagePlaceHolder).length
  58. }
  59. const chapterPos = computed(() => {
  60. let pos = -1
  61. return Array.from(props.contents, content => {
  62. pos += calculateWordCount(content) + 1 //计算上一段的换行符
  63. return pos
  64. })
  65. })
  66. const titleRef = ref<HTMLElement>()
  67. const paragraphRef = ref<HTMLParagraphElement[]>()
  68. const scrollToReadedLength = (length: number) => {
  69. if (length === 0) return
  70. const paragraphIndex = chapterPos.value.findIndex(
  71. wordCount => wordCount >= length,
  72. )
  73. if (paragraphIndex === -1) return
  74. nextTick(() => {
  75. jump(paragraphRef.value![paragraphIndex], {
  76. duration: 0,
  77. })
  78. })
  79. }
  80. defineExpose({
  81. scrollToReadedLength,
  82. })
  83. let intersectionObserver: IntersectionObserver | null = null
  84. const emit = defineEmits(['readedLengthChange'])
  85. onMounted(() => {
  86. intersectionObserver = new IntersectionObserver(
  87. entries => {
  88. for (const { target, isIntersecting } of entries) {
  89. if (isIntersecting) {
  90. emit(
  91. 'readedLengthChange',
  92. props.chapterIndex,
  93. parseInt((target as HTMLElement).dataset.chapterpos as string),
  94. )
  95. }
  96. }
  97. },
  98. {
  99. rootMargin: `0px 0px -${window.innerHeight - 24}px 0px`,
  100. },
  101. )
  102. intersectionObserver.observe(titleRef.value!)
  103. paragraphRef.value!.forEach(element => {
  104. intersectionObserver!.observe(element)
  105. })
  106. })
  107. onUnmounted(() => {
  108. intersectionObserver?.disconnect()
  109. intersectionObserver = null
  110. })
  111. </script>
  112. <style lang="scss" scoped>
  113. .title {
  114. margin-bottom: 57px;
  115. font:
  116. 24px / 32px PingFangSC-Regular,
  117. HelveticaNeue-Light,
  118. 'Helvetica Neue Light',
  119. 'Microsoft YaHei',
  120. sans-serif;
  121. }
  122. p {
  123. display: block;
  124. word-wrap: break-word;
  125. /* word-break: break-all; */
  126. letter-spacing: calc(v-bind('props.spacing.letter') * 1em);
  127. line-height: calc(1 + v-bind('props.spacing.line'));
  128. margin: calc(v-bind('props.spacing.paragraph') * 1em) 0;
  129. :deep(img) {
  130. height: 1em;
  131. }
  132. }
  133. .full {
  134. display: block;
  135. width: 100%;
  136. }
  137. </style>