| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428 |
- <template>
- <view class="reader-container">
- <!-- 顶部操作栏 -->
- <view class="reader-header">
- <view class="left-actions">
- <button class="back-btn" @click="goBack">
- <uni-icons type="arrowleft" size="24" color="#333"></uni-icons>
- </button>
- <text class="chapter-title">{{ chapterTitle }}</text>
- </view>
- <view class="right-actions">
- <button class="action-btn" @click="toggleTheme">
- <uni-icons type="color" size="20"></uni-icons>
- </button>
- <button class="action-btn" @click="toggleFontSize">
- <uni-icons type="font" size="20"></uni-icons>
- </button>
- <button class="action-btn" @click="toggleMode">
- <uni-icons :type="readingMode === 'scroll' ? 'list' : 'grid'" size="20"></uni-icons>
- </button>
- </view>
- </view>
-
- <!-- 阅读模式选择器 -->
- <view v-if="showModeSelector" class="mode-selector">
- <button
- v-for="mode in readingModes"
- :key="mode.value"
- :class="['mode-btn', { active: readingMode === mode.value }]"
- @click="changeMode(mode.value)"
- >
- {{ mode.label }}
- </button>
- </view>
-
- <!-- 字体大小选择器 -->
- <view v-if="showFontSizeSelector" class="font-size-selector">
- <button class="size-btn" @click="decreaseFontSize">A-</button>
- <slider
- :value="fontSize"
- min="14"
- max="24"
- step="2"
- @change="changeFontSize"
- class="font-slider"
- />
- <button class="size-btn" @click="increaseFontSize">A+</button>
- </view>
-
- <!-- 滚动阅读模式 -->
- <scroll-view
- v-if="readingMode === 'scroll'"
- scroll-y
- class="scroll-reader"
- :style="{ fontSize: fontSize + 'px' }"
- :scroll-top="scrollTop"
- @scroll="onScroll"
- @scrolltolower="loadNextChapter"
- >
- <rich-text :nodes="formatContent(chapterContent)" />
- </scroll-view>
-
- <!-- 翻页阅读模式 -->
- <swiper
- v-else
- :current="currentPage"
- circular
- class="page-reader"
- @change="onPageChange"
- >
- <swiper-item v-for="(page, index) in paginatedContent" :key="index">
- <view :style="{ fontSize: fontSize + 'px', padding: '20px' }">
- <rich-text :nodes="page" />
- </view>
- </swiper-item>
- </swiper>
-
- <!-- 底部操作栏 -->
- <view class="reader-footer">
- <button class="footer-btn" @click="prevChapter">上一章</button>
- <button class="footer-btn" @click="showChapterList">目录</button>
- <button class="footer-btn" @click="nextChapter">下一章</button>
- </view>
-
- <!-- 底部广告 -->
- <view v-if="showBottomAd && !isVIP" class="bottom-ad">
- <ad :unit-id="bottomAdUnitId" ad-type="feed" />
- </view>
- </view>
- </template>
-
- <script setup>
- import { ref, computed, onMounted, watch } from 'vue'
- import { useAdManager } from '@/utils/adManager'
- import { useUserStore } from '@/stores/user'
- import { useThemeStore } from '@/stores/theme'
- import { useReadingProgress } from '@/composables/useReadingProgress'
- import { cleanNovelContent, paginateContent } from '@/utils/contentUtils'
-
- const props = defineProps({
- chapterId: {
- type: Number,
- required: true
- },
- novelId: {
- type: Number,
- required: true
- },
- chapterTitle: {
- type: String,
- default: '加载中...'
- }
- })
-
- const emit = defineEmits(['back', 'prev', 'next', 'show-chapters'])
-
- const userStore = useUserStore()
- const themeStore = useThemeStore()
- const { showRewardAd } = useAdManager()
- const { saveProgress, loadProgress } = useReadingProgress()
-
- // 阅读状态
- const readingMode = ref('scroll') // 'scroll' | 'page'
- const readingModes = ref([
- { label: '滚动模式', value: 'scroll' },
- { label: '翻页模式', value: 'page' }
- ])
- const showModeSelector = ref(false)
- const showFontSizeSelector = ref(false)
- const fontSize = ref(18)
- const chapterContent = ref('')
- const paginatedContent = ref([])
- const currentPage = ref(0)
- const scrollTop = ref(0)
- const lastScrollPosition = ref(0)
- const showBottomAd = ref(true)
- const bottomAdUnitId = ref('')
- const isVIP = computed(() => userStore.isVIP)
-
- // 获取章节内容
- const fetchChapterContent = async () => {
- try {
- const res = await uni.request({
- url: `https://php-backend.aiyadianzi.ltd/chapter/${props.chapterId}`,
- method: 'GET'
- })
-
- if (res.statusCode === 200) {
- // 清洗内容并移除原始网站信息
- chapterContent.value = cleanNovelContent(res.data.content)
-
- // 分页处理
- paginatedContent.value = paginateContent(chapterContent.value, 800)
-
- // 恢复阅读位置
- restoreReadingPosition()
- } else {
- throw new Error('章节加载失败')
- }
- } catch (error) {
- uni.showToast({
- title: error.message || '加载章节失败',
- icon: 'none'
- })
- }
- }
-
- // 恢复阅读位置
- const restoreReadingPosition = async () => {
- const progress = await loadProgress(props.novelId, props.chapterId)
-
- if (progress) {
- if (readingMode.value === 'scroll') {
- scrollTop.value = progress.scrollTop
- } else {
- currentPage.value = progress.page
- }
- }
- }
-
- // 翻页事件处理
- const onPageChange = (e) => {
- currentPage.value = e.detail.current
- saveReadingPosition()
-
- // 每5页触发广告
- if (currentPage.value % 5 === 0) {
- showRewardAd(props.chapterId)
- }
- }
-
- // 滚动事件处理
- const onScroll = (e) => {
- lastScrollPosition.value = e.detail.scrollTop
- saveReadingPosition()
- }
-
- // 保存阅读位置
- const saveReadingPosition = () => {
- saveProgress(props.novelId, props.chapterId, {
- page: currentPage.value,
- scrollTop: lastScrollPosition.value
- })
- }
-
- // 切换阅读模式
- const toggleMode = () => {
- showModeSelector.value = !showModeSelector.value
- }
-
- // 改变阅读模式
- const changeMode = (mode) => {
- readingMode.value = mode
- showModeSelector.value = false
- saveReadingPosition()
- }
-
- // 切换字体大小选择器
- const toggleFontSize = () => {
- showFontSizeSelector.value = !showFontSizeSelector.value
- }
-
- // 改变字体大小
- const changeFontSize = (e) => {
- fontSize.value = e.detail.value
- }
-
- // 增大字体
- const increaseFontSize = () => {
- if (fontSize.value < 24) fontSize.value += 2
- }
-
- // 减小字体
- const decreaseFontSize = () => {
- if (fontSize.value > 14) fontSize.value -= 2
- }
-
- // 切换主题
- const toggleTheme = () => {
- const themes = Object.keys(themeStore.themes)
- const currentIndex = themes.indexOf(themeStore.currentTheme)
- const nextIndex = (currentIndex + 1) % themes.length
- themeStore.setTheme(themes[nextIndex])
- }
-
- // 加载下一章
- const loadNextChapter = () => {
- emit('next')
- }
-
- // 返回
- const goBack = () => {
- emit('back')
- }
-
- // 上一章
- const prevChapter = () => {
- emit('prev')
- }
-
- // 下一章
- const nextChapter = () => {
- emit('next')
- }
-
- // 显示章节列表
- const showChapterList = () => {
- emit('show-chapters')
- }
-
- // 初始化
- onMounted(() => {
- fetchChapterContent()
-
- // 设置广告单元ID
- // #ifdef MP-WEIXIN
- bottomAdUnitId.value = 'wechat_feed_ad_123456'
- // #endif
- // #ifdef MP-TOUTIAO
- bottomAdUnitId.value = 'douyin_feed_ad_654321'
- // #endif
- // #ifdef H5
- bottomAdUnitId.value = 'h5_feed_ad_789012'
- // #endif
-
- // 监听VIP状态变化
- watch(() => userStore.isVIP, (isVip) => {
- showBottomAd.value = !isVip
- })
- })
- </script>
-
- <style scoped>
- .reader-container {
- display: flex;
- flex-direction: column;
- height: 100vh;
- background-color: var(--reader-bg-color);
- color: var(--reader-text-color);
- }
-
- .reader-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 10px 15px;
- background-color: var(--header-bg-color);
- border-bottom: 1px solid var(--border-color);
- height: 50px;
- box-sizing: border-box;
- }
-
- .left-actions {
- display: flex;
- align-items: center;
- flex: 1;
- }
-
- .back-btn {
- margin-right: 15px;
- background: none;
- border: none;
- padding: 0;
- }
-
- .chapter-title {
- font-size: 16px;
- font-weight: bold;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- max-width: 60vw;
- }
-
- .right-actions {
- display: flex;
- }
-
- .action-btn {
- background: none;
- border: none;
- padding: 5px 10px;
- margin-left: 10px;
- }
-
- .mode-selector {
- display: flex;
- padding: 10px;
- background-color: var(--header-bg-color);
- justify-content: center;
- border-bottom: 1px solid var(--border-color);
- }
-
- .mode-btn {
- margin: 0 10px;
- padding: 5px 15px;
- border-radius: 15px;
- background-color: var(--button-bg);
- color: var(--button-color);
- border: none;
- font-size: 14px;
- }
-
- .mode-btn.active {
- background-color: var(--primary-color);
- color: white;
- }
-
- .font-size-selector {
- display: flex;
- align-items: center;
- padding: 10px 15px;
- background-color: var(--header-bg-color);
- border-bottom: 1px solid var(--border-color);
- }
-
- .size-btn {
- background: none;
- border: 1px solid var(--border-color);
- border-radius: 4px;
- width: 40px;
- height: 40px;
- display: flex;
- align-items: center;
- justify-content: center;
- font-weight: bold;
- }
-
- .font-slider {
- flex: 1;
- margin: 0 15px;
- }
-
- .scroll-reader {
- flex: 1;
- padding: 20px;
- overflow-y: auto;
- line-height: 1.8;
- }
-
- .page-reader {
- flex: 1;
- }
-
- .reader-footer {
- display: flex;
- justify-content: space-between;
- padding: 10px 15px;
- background-color: var(--header-bg-color);
- border-top: 1px solid var(--border-color);
- }
-
- .footer-btn {
- flex: 1;
- margin: 0 5px;
- padding: 8px 0;
- border-radius: 4px;
- background-color: var(--button-bg);
- color: var(--button-color);
- border: none;
- font-size: 14px;
- }
-
- .bottom-ad {
- height: 100px;
- background-color: #f5f5f5;
- }
- </style>
|