|
|
|
@ -42,8 +42,27 @@ |
|
|
|
@selectstart.prevent |
|
|
|
@dragstart.prevent |
|
|
|
> |
|
|
|
<!-- 参考readnovels.vue的段落显示方式 --> |
|
|
|
<div v-if="chapter.paragraphs && chapter.paragraphs.length" class="paragraph-content"> |
|
|
|
<!-- 付费章节只显示部分内容 --> |
|
|
|
<div v-if="isPay && chapter.paragraphs && chapter.paragraphs.length" class="paragraph-content"> |
|
|
|
<p v-for="(paragraph, index) in chapter.paragraphs.slice(0, 3)" :key="index" class="paragraph"> |
|
|
|
{{ paragraph }} |
|
|
|
</p> |
|
|
|
<div class="pay-content-mask"> |
|
|
|
<div class="mask-content"> |
|
|
|
<div class="mask-icon"> |
|
|
|
<el-icon size="48px" color="#ff9800"> |
|
|
|
<Lock /> |
|
|
|
</el-icon> |
|
|
|
</div> |
|
|
|
<div class="mask-text"> |
|
|
|
<p class="mask-title">这是付费章节</p> |
|
|
|
<p class="mask-desc">订阅后可阅读完整内容</p> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
<!-- 免费章节显示全部内容 --> |
|
|
|
<div v-else-if="!isPay && chapter.paragraphs && chapter.paragraphs.length" class="paragraph-content"> |
|
|
|
<p v-for="(paragraph, index) in chapter.paragraphs" :key="index" class="paragraph"> |
|
|
|
{{ paragraph }} |
|
|
|
</p> |
|
|
|
@ -87,7 +106,7 @@ |
|
|
|
</el-icon> |
|
|
|
<span>目录</span> |
|
|
|
</div> |
|
|
|
<div class="chapter-menu-item" @click="toggleTheme"> |
|
|
|
<div class="chapter-menu-item" @click="toggleThemeMode"> |
|
|
|
<el-icon size="20px"> |
|
|
|
<MoonNight /> |
|
|
|
</el-icon> |
|
|
|
@ -107,6 +126,19 @@ |
|
|
|
:current-chapter-id="chapterId" |
|
|
|
@chapter-select="handleChapterSelect" |
|
|
|
/> |
|
|
|
|
|
|
|
<!-- 订阅弹窗 --> |
|
|
|
<SubscriptionPopup |
|
|
|
v-model="subscriptionVisible" |
|
|
|
:chapter-list="chapterList" |
|
|
|
:current-chapter="currentChapterInfo" |
|
|
|
:current-index="currentChapterIndex" |
|
|
|
:book-id="bookId" |
|
|
|
:is-dark-mode="isDarkMode" |
|
|
|
@subscribe="handleSubscribe" |
|
|
|
@batch-subscribe="handleBatchSubscribe" |
|
|
|
@video-unlock="handleVideoUnlock" |
|
|
|
/> |
|
|
|
</div> |
|
|
|
</template> |
|
|
|
|
|
|
|
@ -114,10 +146,12 @@ |
|
|
|
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'; |
|
|
|
import { useRoute, useRouter } from 'vue-router'; |
|
|
|
import { ElMessage } from 'element-plus'; |
|
|
|
import { ArrowLeft, ArrowRight, ZoomIn, ZoomOut, MoonNight, ArrowUp, ArrowDown, Reading, Grid } from '@element-plus/icons-vue'; |
|
|
|
import { ArrowLeft, ArrowRight, ZoomIn, ZoomOut, MoonNight, ArrowUp, ArrowDown, Reading, Grid, Lock } from '@element-plus/icons-vue'; |
|
|
|
import { homeApi } from '@/api/home.js'; |
|
|
|
import { bookshelfApi } from '@/api/bookshelf.js'; |
|
|
|
import CatalogDialog from '@/components/book/CatalogDialog.vue'; |
|
|
|
import SubscriptionPopup from '@/components/book/SubscriptionPopup.vue'; |
|
|
|
import { toggleTheme, getThemeMode, setThemeMode } from '@/utils/theme.js'; |
|
|
|
|
|
|
|
export default { |
|
|
|
name: 'ChapterDetail', |
|
|
|
@ -131,7 +165,9 @@ export default { |
|
|
|
ArrowDown, |
|
|
|
Reading, |
|
|
|
Grid, |
|
|
|
CatalogDialog |
|
|
|
Lock, |
|
|
|
CatalogDialog, |
|
|
|
SubscriptionPopup |
|
|
|
}, |
|
|
|
setup() { |
|
|
|
const route = useRoute(); |
|
|
|
@ -147,8 +183,10 @@ export default { |
|
|
|
const bookDetail = ref(null); |
|
|
|
const isInBookshelf = ref(false); |
|
|
|
const fontSize = ref(18); |
|
|
|
const isDarkMode = ref(false); |
|
|
|
const isDarkMode = ref(getThemeMode()); |
|
|
|
const catalogDialogVisible = ref(false); |
|
|
|
const subscriptionVisible = ref(false); |
|
|
|
const isPay = ref(false); |
|
|
|
|
|
|
|
// 计算上一章和下一章 |
|
|
|
const currentChapterIndex = computed(() => { |
|
|
|
@ -166,6 +204,11 @@ export default { |
|
|
|
return index >= 0 && index < chapterList.value.length - 1 ? chapterList.value[index + 1] : null; |
|
|
|
}); |
|
|
|
|
|
|
|
// 当前章节信息 |
|
|
|
const currentChapterInfo = computed(() => { |
|
|
|
return chapterList.value.find(ch => ch.id === chapterId.value) || {}; |
|
|
|
}); |
|
|
|
|
|
|
|
// 获取书籍详情 |
|
|
|
const getBookDetail = async () => { |
|
|
|
try { |
|
|
|
@ -188,6 +231,22 @@ export default { |
|
|
|
throw new Error('章节ID不存在'); |
|
|
|
} |
|
|
|
|
|
|
|
// 只有登录后才检查是否已购买 |
|
|
|
const token = localStorage.getItem('token'); |
|
|
|
if (token) { |
|
|
|
try { |
|
|
|
const payResponse = await homeApi.getMyShopNovel({ |
|
|
|
bookId: bookId.value, |
|
|
|
novelId: chapterId.value, |
|
|
|
}); |
|
|
|
// payResponse为true表示已购买,为false表示未购买 |
|
|
|
isPay.value = !payResponse.result; |
|
|
|
} catch (err) { |
|
|
|
console.error('检查付费状态失败:', err); |
|
|
|
isPay.value = true; // 默认需要付费 |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 获取章节详情 |
|
|
|
const response = await homeApi.getBookCatalogDetail(chapterId.value); |
|
|
|
if (response && response.success) { |
|
|
|
@ -198,6 +257,11 @@ export default { |
|
|
|
// 按换行符分割成段落数组 |
|
|
|
chapter.value.paragraphs = chapter.value.details.split('\n') |
|
|
|
} |
|
|
|
|
|
|
|
// 如果章节标记为付费章节,设置付费状态 |
|
|
|
if (chapter.value.isPay != 'Y') { |
|
|
|
isPay.value = false; |
|
|
|
} |
|
|
|
} else { |
|
|
|
throw new Error(response?.message || '获取章节详情失败'); |
|
|
|
} |
|
|
|
@ -205,6 +269,9 @@ export default { |
|
|
|
// 更新阅读进度到书架 |
|
|
|
await updateReadProgress(); |
|
|
|
|
|
|
|
// 更新订阅弹窗状态 |
|
|
|
updateSubscriptionStatus(); |
|
|
|
|
|
|
|
} catch (err) { |
|
|
|
console.error('获取章节详情失败:', err); |
|
|
|
throw err; |
|
|
|
@ -346,12 +413,95 @@ export default { |
|
|
|
}; |
|
|
|
|
|
|
|
// 切换暗黑模式 |
|
|
|
const toggleTheme = () => { |
|
|
|
const toggleThemeMode = () => { |
|
|
|
isDarkMode.value = !isDarkMode.value; |
|
|
|
if (isDarkMode.value) { |
|
|
|
document.documentElement.classList.add('dark-theme'); |
|
|
|
setThemeMode(isDarkMode.value); |
|
|
|
}; |
|
|
|
|
|
|
|
// 更新订阅弹窗状态 |
|
|
|
const updateSubscriptionStatus = () => { |
|
|
|
if (isPay.value) { |
|
|
|
subscriptionVisible.value = true; |
|
|
|
} else { |
|
|
|
document.documentElement.classList.remove('dark-theme'); |
|
|
|
subscriptionVisible.value = false; |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
// 处理订阅 |
|
|
|
const handleSubscribe = async () => { |
|
|
|
try { |
|
|
|
await homeApi.buyNovel({ |
|
|
|
bookId: bookId.value, |
|
|
|
novelId: chapterId.value, |
|
|
|
}); |
|
|
|
|
|
|
|
isPay.value = false; |
|
|
|
updateSubscriptionStatus(); |
|
|
|
ElMessage.success('订阅成功'); |
|
|
|
|
|
|
|
// 重新获取章节详情 |
|
|
|
await getBookCatalogDetail(); |
|
|
|
} catch (err) { |
|
|
|
console.error('订阅失败:', err); |
|
|
|
ElMessage.error('订阅失败,请重试'); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
// 处理批量订阅 |
|
|
|
const handleBatchSubscribe = async (batchCount) => { |
|
|
|
try { |
|
|
|
// 获取从当前章节开始的连续章节ID |
|
|
|
const chapterIds = []; |
|
|
|
const startIndex = currentChapterIndex.value; |
|
|
|
|
|
|
|
for (let i = 0; i < batchCount && (startIndex + i) < chapterList.value.length; i++) { |
|
|
|
const chapter = chapterList.value[startIndex + i]; |
|
|
|
// 使用pay字段判断是否已购买,isPay为'Y'且pay为false表示需要购买 |
|
|
|
if (chapter.isPay === 'Y' && !chapter.pay) { |
|
|
|
chapterIds.push(chapter.id); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (chapterIds.length === 0) { |
|
|
|
ElMessage.warning('没有需要购买的章节'); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// 调用批量订阅接口 |
|
|
|
await homeApi.buyNovel({ |
|
|
|
bookId: bookId.value, |
|
|
|
novelId: chapterIds.join(',') |
|
|
|
}); |
|
|
|
|
|
|
|
ElMessage.success(`成功订阅${chapterIds.length}章`); |
|
|
|
|
|
|
|
// 刷新章节列表状态 |
|
|
|
await getBookCatalogList(); |
|
|
|
isPay.value = false; |
|
|
|
updateSubscriptionStatus(); |
|
|
|
|
|
|
|
} catch (error) { |
|
|
|
console.error('批量订阅失败:', error); |
|
|
|
ElMessage.error('订阅失败,请重试'); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
// 处理视频解锁 |
|
|
|
const handleVideoUnlock = async () => { |
|
|
|
try { |
|
|
|
ElMessage.info('暂未开放'); |
|
|
|
// 这里可以添加视频解锁的逻辑 |
|
|
|
// await homeApi.openBookCatalog({ |
|
|
|
// bookId: bookId.value, |
|
|
|
// catalogId: chapterId.value |
|
|
|
// }); |
|
|
|
|
|
|
|
// isPay.value = false; |
|
|
|
// updateSubscriptionStatus(); |
|
|
|
|
|
|
|
} catch (error) { |
|
|
|
console.error('视频解锁失败:', error); |
|
|
|
ElMessage.error('解锁失败,请重试'); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
@ -380,6 +530,10 @@ export default { |
|
|
|
nextChapter, |
|
|
|
isInBookshelf, |
|
|
|
isDarkMode, |
|
|
|
subscriptionVisible, |
|
|
|
isPay, |
|
|
|
currentChapterInfo, |
|
|
|
currentChapterIndex, |
|
|
|
fetchChapterDetail, |
|
|
|
goToPrevChapter, |
|
|
|
goToNextChapter, |
|
|
|
@ -387,9 +541,12 @@ export default { |
|
|
|
goToChapterList, |
|
|
|
toggleBookshelf, |
|
|
|
toggleFontSize, |
|
|
|
toggleTheme, |
|
|
|
toggleThemeMode, |
|
|
|
catalogDialogVisible, |
|
|
|
handleChapterSelect, |
|
|
|
handleSubscribe, |
|
|
|
handleBatchSubscribe, |
|
|
|
handleVideoUnlock, |
|
|
|
chapterList, |
|
|
|
chapterId, |
|
|
|
}; |
|
|
|
@ -432,6 +589,20 @@ export default { |
|
|
|
.paragraph { |
|
|
|
color: var(--reading-text-color) !important; |
|
|
|
} |
|
|
|
|
|
|
|
.pay-content-mask { |
|
|
|
background: linear-gradient(180deg, rgba(26, 26, 26, 0) 0%, rgba(26, 26, 26, 0.9) 50%, rgba(26, 26, 26, 1) 100%) !important; |
|
|
|
|
|
|
|
.mask-text { |
|
|
|
.mask-title { |
|
|
|
color: #ff9800 !important; |
|
|
|
} |
|
|
|
|
|
|
|
.mask-desc { |
|
|
|
color: #999 !important; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
.el-button { |
|
|
|
@ -512,23 +683,59 @@ export default { |
|
|
|
.paragraph-content { |
|
|
|
width: 100%; |
|
|
|
|
|
|
|
.paragraph { |
|
|
|
text-indent: 2em; |
|
|
|
margin-bottom: 30px; |
|
|
|
line-height: 1.8; |
|
|
|
font-size: var(--reading-font-size); |
|
|
|
color: #333; |
|
|
|
word-wrap: break-word; |
|
|
|
word-break: normal; |
|
|
|
white-space: normal; |
|
|
|
transition: color 0.3s ease; |
|
|
|
/* 禁止选中文字 */ |
|
|
|
user-select: none; |
|
|
|
-webkit-user-select: none; |
|
|
|
-moz-user-select: none; |
|
|
|
-ms-user-select: none; |
|
|
|
.paragraph { |
|
|
|
text-indent: 2em; |
|
|
|
margin-bottom: 30px; |
|
|
|
line-height: 1.8; |
|
|
|
font-size: var(--reading-font-size); |
|
|
|
color: #333; |
|
|
|
word-wrap: break-word; |
|
|
|
word-break: normal; |
|
|
|
white-space: normal; |
|
|
|
transition: color 0.3s ease; |
|
|
|
/* 禁止选中文字 */ |
|
|
|
user-select: none; |
|
|
|
-webkit-user-select: none; |
|
|
|
-moz-user-select: none; |
|
|
|
-ms-user-select: none; |
|
|
|
} |
|
|
|
|
|
|
|
.pay-content-mask { |
|
|
|
position: relative; |
|
|
|
margin-top: 40px; |
|
|
|
padding: 60px 20px; |
|
|
|
background: linear-gradient(180deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 50%, rgba(255, 255, 255, 1) 100%); |
|
|
|
border-radius: 8px; |
|
|
|
text-align: center; |
|
|
|
|
|
|
|
.mask-content { |
|
|
|
display: flex; |
|
|
|
flex-direction: column; |
|
|
|
align-items: center; |
|
|
|
justify-content: center; |
|
|
|
gap: 20px; |
|
|
|
|
|
|
|
.mask-icon { |
|
|
|
margin-bottom: 10px; |
|
|
|
} |
|
|
|
|
|
|
|
.mask-text { |
|
|
|
.mask-title { |
|
|
|
font-size: 18px; |
|
|
|
font-weight: bold; |
|
|
|
color: #ff9800; |
|
|
|
margin: 0 0 8px 0; |
|
|
|
} |
|
|
|
|
|
|
|
.mask-desc { |
|
|
|
font-size: 14px; |
|
|
|
color: #666; |
|
|
|
margin: 0; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
.paragraph { |
|
|
|
margin-bottom: 20px; |
|
|
|
|