index.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. import React, { useState, useEffect, useRef } from 'react';
  2. import { View, Text, ScrollView } from '@tarojs/components';
  3. import { observer, inject } from 'mobx-react';
  4. import './index.less';
  5. import TopNavBar from '@src/components/TopNavBar/index';
  6. import Taro from '@tarojs/taro';
  7. import BookConfigPuP from '@src/components/bookConfigPup';
  8. import { BookStore } from '@src/store/book';
  9. import useApi from '@src/Hook/useApi';
  10. import { getReadLog, setReadLog } from '@src/utils/loginSto';
  11. interface Config {
  12. size: 'pre_15' | 'pre_17' | 'pre_19' | 'pre_21' | 'pre_23' | 'pre_25' | 'pre_27' | 'pre_29' | any;
  13. off_on: boolean;
  14. bg: 'bg_b' | 'bg_h' | 'bg_l' | 'bg_hh';
  15. lh: 'lh_72' | 'lh_84' | 'lh_96';
  16. }
  17. type PageStateProps = {
  18. store: {
  19. bookStore: BookStore,
  20. indexStore,
  21. appInfoStore
  22. }
  23. };
  24. interface BookArticleProps extends PageStateProps { }
  25. const BookArticle: React.FC<BookArticleProps> = ({ store }) => {
  26. const [isLoad, setIsLoad] = useState(false)
  27. const { bookStore, indexStore, appInfoStore } = store
  28. const { openBookData } = bookStore
  29. const [bookConfig, setBookConfig] = useState<Config>({ size: 'pre_21', off_on: true, bg: 'bg_b', lh: 'lh_72' });
  30. const [pup, setPup] = useState(false);
  31. const { getBookContent,readingBook } = useApi(appInfoStore)
  32. const [currentScrollId, setCurrentScrollId] = useState('');
  33. const newReadLogIdRef = useRef("");
  34. const readLogInterval = useRef<NodeJS.Timeout>()
  35. // 页面显示的操作请求
  36. useEffect(() => {
  37. // ComponentDidMount
  38. try {
  39. Taro.getStorage({
  40. key: 'bookConfig',
  41. success: (res) => {
  42. setBookConfig(JSON.parse(res.data));
  43. },
  44. fail: () => {
  45. setConfig({ size: 'pre_21', off_on: true, bg: 'bg_b', lh: 'lh_72' });
  46. }
  47. });
  48. } catch (e) {
  49. console.error('Error loading book config:', e);
  50. }
  51. //假如存在阅读记录获取对应章节的条数
  52. let readLogId = getReadLog(openBookData?.bookId)
  53. getBookContent({ pageNum: 1, pageSize: readLogId ? Number(readLogId?.split('-')[1]) : 2 }).then(res => {
  54. //继续阅读时跳转到上次阅读的段落
  55. console.log("上次阅读到readLogId", readLogId)
  56. setCurrentScrollId(readLogId)
  57. })
  58. }, []);
  59. // 页面卸载的操作
  60. useEffect(() => {
  61. reportRead()
  62. return () => {
  63. console.log("卸载", newReadLogIdRef.current);
  64. openBookData && setReadLog({ [openBookData.bookId]: newReadLogIdRef.current })//离开时存放当前位置
  65. clearInterval(readLogInterval.current)//清除上报心跳
  66. };
  67. }, [])
  68. // 这里可以填写上报逻辑
  69. const reportRead = () => {
  70. console.log("上报", newReadLogIdRef.current)
  71. openBookData && setReadLog({ [openBookData.bookId]: newReadLogIdRef.current })
  72. if(readLogInterval.current){
  73. clearInterval(readLogInterval.current)
  74. }
  75. // 定时上报心跳
  76. readLogInterval.current = setInterval(() => {
  77. readingBook(bookStore.readId)
  78. console.log("上报阅读心跳",newReadLogIdRef.current,bookStore.readId)
  79. }, 3000);
  80. };
  81. //设置配置
  82. const setConfig = (data: Config) => {
  83. if (data) {
  84. setBookConfig(data);
  85. Taro.setStorage({
  86. key: 'bookConfig',
  87. data: JSON.stringify(data),
  88. success: (res: any) => {
  89. console.log('添加bookConfig===>成功!', res);
  90. },
  91. fail: (err) => {
  92. console.log('添加bookConfig===>失败', err);
  93. }
  94. });
  95. // 控制自义定头部背景
  96. store.bookStore.setData({ bookConfig: data });
  97. }
  98. };
  99. /**计算字体 */
  100. const setSize = (parms: number) => {
  101. let num = bookConfig.size.split('_');
  102. if (num[1] && parms) {
  103. setConfig({ ...bookConfig, size: num[0] + '_' + (Number(num[1]) + 2) });
  104. } else {
  105. setConfig({ ...bookConfig, size: num[0] + '_' + (Number(num[1]) - 2) });
  106. }
  107. };
  108. /**设置背景 */
  109. const setBg = (bg: 'bg_b' | 'bg_h' | 'bg_l' | 'bg_hh') => {
  110. setConfig({ ...bookConfig, bg });
  111. if (!bookConfig.off_on) {
  112. setOff(bg);
  113. }
  114. };
  115. /**设置行高*/
  116. const setLh = (lh: 'lh_72' | 'lh_84' | 'lh_96') => {
  117. setConfig({ ...bookConfig, lh });
  118. };
  119. /**关灯 */
  120. const setOff = (bg?: 'bg_b' | 'bg_h' | 'bg_l' | 'bg_hh') => {
  121. const newOffOn = !bookConfig.off_on;
  122. const newConfig = bg ? { ...bookConfig, bg, off_on: newOffOn } : { ...bookConfig, off_on: newOffOn };
  123. setConfig(newConfig);
  124. Taro.setNavigationBarColor({
  125. frontColor: newOffOn ? '#000000' : '#ffffff',
  126. backgroundColor: newOffOn ? '#fff' : '#111111'
  127. });
  128. };
  129. /**菜单 */
  130. const togglePup = () => {
  131. setPup(!pup);
  132. };
  133. //设置setNavigationBarColor
  134. useEffect(() => {
  135. Taro.setNavigationBarColor({
  136. frontColor: bookConfig.off_on ? '#000000' : '#ffffff',
  137. backgroundColor: bookConfig.off_on ? '#fff' : '#111'
  138. });
  139. }, [bookConfig.off_on]);
  140. const onLoad = async () => {
  141. if (openBookData?.contentData) {
  142. let { size, current, total, records } = openBookData.contentData
  143. if (records.length < total) {
  144. console.log("加载")
  145. setIsLoad(true)
  146. await getBookContent({ pageNum: 1, pageSize: size + 2 }).then(res => {
  147. setIsLoad(false)
  148. })
  149. } else {
  150. Taro.showToast({
  151. title: '没有更多了~~',
  152. duration: 1000,
  153. icon: 'none'
  154. })
  155. }
  156. }
  157. }
  158. // 计算阅读的位置,存放以备下次阅读的时候跳转到对应位置
  159. const handleScroll = () => {
  160. const query = Taro.createSelectorQuery();
  161. query.selectAll('.shrot-text').boundingClientRect(); // 选择所有子元素
  162. query.selectViewport().scrollOffset(); // 获取视口的滚动位置
  163. query.exec(res => {
  164. const items = res[0]; // 子元素的位置信息
  165. const scrollTop = res[1].scrollTop + indexStore.navHeight; // 视口的滚动距离
  166. // 遍历所有子元素,判断哪个子元素在可视区域
  167. for (let item of items) {
  168. if (item.top <= scrollTop && item.bottom >= scrollTop) {
  169. if (openBookData?.bookId) {
  170. newReadLogIdRef.current = item.id//存放当前可视的段落ID
  171. }
  172. break;
  173. }
  174. }
  175. });
  176. };
  177. const { size, bg, off_on, lh } = bookConfig;
  178. return (
  179. <ScrollView
  180. lowerThreshold={indexStore.navHeight}
  181. // refresherTriggered={isLoad}//加载状态
  182. onScrollToLower={onLoad}
  183. style={{ height: `calc(100vh - ${indexStore.navHeight}px)` }}
  184. scrollY={true}
  185. refresherDefaultStyle='black'
  186. scrollWithAnimation={false}
  187. scrollIntoView={currentScrollId}
  188. onScroll={handleScroll}
  189. >
  190. <View className={`book_article ${off_on ? bg : 'bg_hh'} ${off_on ? 'cl_h' : 'cl_b'}`} >
  191. {/* 顶部设置 */}
  192. <View className='header'>
  193. <View className='btns'>
  194. <View className='left'>
  195. <Text
  196. onClick={() => { size !== 'pre_15' && setSize(0) }}
  197. className={!off_on ? size === 'pre_15' ? 'cl_e5' : '' : size === 'pre_15' ? 'cl_d6' : ''}
  198. >字小</Text>
  199. <Text
  200. onClick={() => { size !== 'pre_29' && setSize(1) }}
  201. className={!off_on ? size === 'pre_29' ? 'cl_e5' : '' : size === 'pre_29' ? 'cl_d6' : ''}
  202. >字大</Text>
  203. </View>
  204. <View className='right'>
  205. <Text onClick={() => { setOff() }}>{off_on ? '关灯' : '开灯'}</Text>
  206. {isLoad && <Text onClick={togglePup}>菜单</Text>}
  207. </View>
  208. </View>
  209. </View>
  210. {/* 正文内容 */}
  211. <View onClick={togglePup}>
  212. {
  213. openBookData?.contentData?.records?.map(item => {
  214. let arr = item.paragraphInfo.content?.match(/[^\n]*\n?/g);
  215. return <View
  216. id={'paragraphNo-' + item.paragraphInfo.paragraphNo}
  217. key={item.paragraphInfo.paragraphNo}
  218. className={`pre ${size} ${lh}`}
  219. >
  220. {
  221. arr?.map((str, index) => {
  222. // let strArr = str?.match(/[^。]*。/g)
  223. return <Text className='shrot-text' id={'paragraphNo-' + item.paragraphInfo.paragraphNo + '-' + index} key={index}>{
  224. str
  225. }</Text>
  226. })
  227. }
  228. </View>
  229. })
  230. }
  231. </View>
  232. {/* 设置弹窗 */}
  233. {pup && <BookConfigPuP {...bookConfig} off_on={off_on} setOff={setOff} setSize={setSize} setBg={setBg} setLh={setLh} />}
  234. </View>
  235. {/* 底部下来加载 */}
  236. {/* {
  237. isLoad && <UpLoading />
  238. } */}
  239. </ScrollView>
  240. );
  241. };
  242. export default inject('store')(observer(TopNavBar(BookArticle, { isToBack: true, isReloadBook: true })));