ChapterContent.vue 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  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(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>
  20. import { getImageFromLegado, isLegadoUrl } from "@/plugins/utils";
  21. import jump from "@/plugins/jump";
  22. const props = defineProps({
  23. chapterIndex: { type: Number, required: true },
  24. contents: { type: Array, required: true },
  25. title: { type: String, required: true },
  26. spacing: { type: Object, required: true },
  27. fontFamily: { type: String, required: true },
  28. fontSize: { type: String, required: true },
  29. });
  30. const getImageSrc = (content) => {
  31. const imgPattern = /<img[^>]*src="([^"]*(?:"[^>]+\})?)"[^>]*>/;
  32. const src = content.match(imgPattern)[1];
  33. if (isLegadoUrl(src)) return getImageFromLegado(src);
  34. return src;
  35. };
  36. const proxyImage = (event) => {
  37. event.target.src = getImageFromLegado(event.target.src);
  38. };
  39. const calculateWordCount = (paragraph) => {
  40. const imgPattern = /<img[^>]*src="[^"]*(?:"[^>]+\})?"[^>]*>/g;
  41. //内嵌图片文字为1
  42. const imagePlaceHolder = " ";
  43. return paragraph.replaceAll(imgPattern, imagePlaceHolder).length;
  44. };
  45. const chapterPos = computed(() => {
  46. let pos = -1;
  47. return Array.from(props.contents, (content) => {
  48. pos += calculateWordCount(content) + 1; //计算上一段的换行符
  49. return pos;
  50. });
  51. });
  52. const titleRef = ref();
  53. const paragraphRef = ref();
  54. const scrollToReadedLength = (length) => {
  55. if (length === 0) return;
  56. let paragraphIndex = chapterPos.value.findIndex(
  57. (wordCount) => wordCount >= length
  58. );
  59. if (paragraphIndex === -1) return;
  60. nextTick(() => {
  61. jump(paragraphRef.value[paragraphIndex], {
  62. duration: 0,
  63. });
  64. });
  65. };
  66. defineExpose({
  67. scrollToReadedLength,
  68. });
  69. const observer = ref(null);
  70. const emit = defineEmits(["readedLengthChange"]);
  71. onMounted(() => {
  72. observer.value = new IntersectionObserver(
  73. (entries) => {
  74. for (let { target, isIntersecting } of entries) {
  75. if (isIntersecting) {
  76. emit(
  77. "readedLengthChange",
  78. props.chapterIndex,
  79. parseInt(target.dataset.chapterpos)
  80. );
  81. }
  82. }
  83. },
  84. {
  85. rootMargin: `0px 0px -${window.innerHeight - 24}px 0px`,
  86. }
  87. );
  88. observer.value.observe(titleRef.value);
  89. paragraphRef.value.forEach((element) => {
  90. observer.value.observe(element);
  91. });
  92. });
  93. onUnmounted(() => {
  94. observer.value?.disconnect();
  95. observer.value = null;
  96. });
  97. </script>
  98. <style lang="scss" scoped>
  99. .title {
  100. margin-bottom: 57px;
  101. font: 24px / 32px PingFangSC-Regular, HelveticaNeue-Light,
  102. "Helvetica Neue Light", "Microsoft YaHei", sans-serif;
  103. }
  104. p {
  105. display: block;
  106. word-wrap: break-word;
  107. word-break: break-all;
  108. letter-spacing: calc(v-bind("props.spacing.letter") * 1em);
  109. line-height: calc(1 + v-bind("props.spacing.line"));
  110. margin: calc(v-bind("props.spacing.paragraph") * 1em) 0;
  111. :deep(img) {
  112. height: 1em;
  113. }
  114. }
  115. .full {
  116. display: block;
  117. width: 100%;
  118. }
  119. </style>