Browse Source

feat: 添加章节订阅功能及暗黑模式支持

- 新增订阅弹窗组件,支持单章订阅和批量订阅
- 实现章节付费内容部分预览功能
- 添加暗黑模式切换及状态管理
- 优化章节状态显示逻辑
- 修复图片引用路径问题
- 移除无用状态列显示
master
前端-胡立永 2 months ago
parent
commit
602d1f1385
13 changed files with 933 additions and 103 deletions
  1. BIN
      dist.zip
  2. +20
    -0
      src/api/home.js
  3. +0
    -0
      src/assets/images/Recommendationvote.png
  4. BIN
      src/assets/images/crown.png
  5. +457
    -0
      src/components/book/SubscriptionPopup.vue
  6. +1
    -1
      src/components/ranking/IntimacyRanking.vue
  7. +36
    -0
      src/utils/theme.js
  8. +135
    -28
      src/views/author/WorkEdit.vue
  9. +24
    -22
      src/views/author/components/ReadersManagement.vue
  10. +233
    -26
      src/views/book/chapter.vue
  11. +3
    -3
      src/views/home/Home.vue
  12. +2
    -2
      src/views/user/MoneyLog.vue
  13. +22
    -21
      src/views/user/TaskCenter.vue

BIN
dist.zip View File


+ 20
- 0
src/api/home.js View File

@ -135,4 +135,24 @@ export const homeApi = {
getBookAreaList() {
return api.get('/all_index/getBookAreaList');
},
/**
* 检查章节是否需要付费
* @param {Object} params 参数
* @param {string} params.bookId 书本ID
* @param {string} params.novelId 章节ID
*/
getMyShopNovel(params) {
return api.get('/my_book/getMyShopNovel', { params });
},
/**
* 购买章节
* @param {Object} params 参数
* @param {string} params.bookId 书本ID
* @param {string} params.novelId 章节ID可以是单个或多个多个用逗号分隔
*/
buyNovel(params) {
return api.post('/my_order/buyNovel', params);
},
}

src/assets/images/推荐票.png → src/assets/images/Recommendationvote.png View File


BIN
src/assets/images/crown.png View File

Before After
Width: 64  |  Height: 64  |  Size: 6.7 KiB

+ 457
- 0
src/components/book/SubscriptionPopup.vue View File

@ -0,0 +1,457 @@
<template>
<el-dialog
v-model="visible"
:title="title"
width="500px"
:close-on-click-modal="false"
:close-on-press-escape="false"
class="subscription-popup"
:class="{ 'dark-mode': isDarkMode }"
>
<div class="content">
<!-- 默认订阅界面 -->
<div v-if="!showBatchDialog" class="popup-btns">
<el-button
type="primary"
class="popup-btn"
@click="handleSubscribe"
>
订阅本章
</el-button>
<el-button
class="popup-btn popup-btn-video"
@click="handleVideoUnlock"
>
观看视频解锁
</el-button>
<el-button
class="popup-btn popup-btn-batch"
@click="showBatchSubscribe"
>
批量订阅
</el-button>
</div>
<!-- 批量订阅界面 -->
<div v-else class="batch-subscribe-content">
<div class="batch-title">选择订阅章节数量</div>
<div class="batch-info">
<span>从当前章节开始可订阅 {{ availableChaptersCount }} </span>
</div>
<div v-if="availableChaptersCount === 0" class="no-chapters-tip">
<span>暂无需要付费的章节</span>
</div>
<div v-else>
<div class="batch-current-info">
<span>连续订阅 {{ batchCount }} </span>
</div>
<div class="batch-counter">
<el-button
:disabled="batchCount <= 1"
@click="decreaseBatchCount"
class="counter-btn"
>
-
</el-button>
<el-input
v-model="batchCount"
type="number"
class="counter-input"
:max="maxBatchCount"
@input="validateBatchCount"
/>
<el-button
:disabled="batchCount >= maxBatchCount"
@click="increaseBatchCount"
class="counter-btn"
>
+
</el-button>
</div>
</div>
<div class="batch-btns">
<el-button
class="batch-cancel-btn"
@click="cancelBatchSubscribe"
>
取消
</el-button>
<el-button
type="primary"
class="batch-confirm-btn"
:disabled="availableChaptersCount === 0"
@click="handleBatchSubscribe"
>
确认订阅
</el-button>
</div>
</div>
</div>
</el-dialog>
</template>
<script>
import { ref, computed, watch } from 'vue'
import { ElMessage } from 'element-plus'
export default {
name: 'SubscriptionPopup',
props: {
modelValue: {
type: Boolean,
default: false
},
title: {
type: String,
default: '这是付费章节 需要订阅后才能阅读'
},
chapterList: {
type: Array,
default: () => []
},
currentChapter: {
type: Object,
default: () => ({})
},
currentIndex: {
type: Number,
default: 0
},
bookId: {
type: [String, Number],
default: ''
},
isDarkMode: {
type: Boolean,
default: false
}
},
emits: ['update:modelValue', 'subscribe', 'batchSubscribe', 'videoUnlock'],
setup(props, { emit }) {
const visible = ref(false)
const showBatchDialog = ref(false)
const batchCount = ref(5)
// modelValue
watch(() => props.modelValue, (newVal) => {
visible.value = newVal
})
// visible
watch(visible, (newVal) => {
emit('update:modelValue', newVal)
})
//
const availableChaptersCount = computed(() => {
if (!props.chapterList.length || props.currentIndex < 0) return 0
let count = 0
for (let i = props.currentIndex; i < props.chapterList.length; i++) {
const chapter = props.chapterList[i]
// 使payisPay'Y'payfalse
if (chapter.isPay === 'Y' && !chapter.pay) {
count++
}
}
return count
})
//
const maxBatchCount = computed(() => {
return Math.max(1, availableChaptersCount.value)
})
//
const handleSubscribe = () => {
emit('subscribe')
visible.value = false
}
//
const handleVideoUnlock = () => {
emit('videoUnlock')
visible.value = false
}
//
const showBatchSubscribe = () => {
//
batchCount.value = Math.min(5, maxBatchCount.value)
showBatchDialog.value = true
}
//
const cancelBatchSubscribe = () => {
showBatchDialog.value = false
}
//
const handleBatchSubscribe = () => {
emit('batchSubscribe', batchCount.value)
showBatchDialog.value = false
}
//
const decreaseBatchCount = () => {
if (batchCount.value > 1) {
batchCount.value--
}
}
//
const increaseBatchCount = () => {
if (batchCount.value < maxBatchCount.value) {
batchCount.value++
}
}
//
const validateBatchCount = () => {
batchCount.value = Math.max(1, Math.min(maxBatchCount.value, parseInt(batchCount.value) || 1))
}
return {
visible,
showBatchDialog,
batchCount,
availableChaptersCount,
maxBatchCount,
handleSubscribe,
handleVideoUnlock,
showBatchSubscribe,
cancelBatchSubscribe,
handleBatchSubscribe,
decreaseBatchCount,
increaseBatchCount,
validateBatchCount
}
}
}
</script>
<style lang="scss" scoped>
.subscription-popup {
.content {
.popup-title {
font-size: 16px;
font-weight: bold;
color: #222;
margin-bottom: 12px;
word-wrap: break-word;
white-space: normal;
}
.popup-desc {
font-size: 13px;
color: #999;
margin-bottom: 20px;
word-wrap: break-word;
white-space: normal;
}
.popup-btns {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 12px;
.popup-btn {
background: #ff9800;
color: #fff;
border-radius: 16px;
font-size: 14px;
padding: 0 16px;
border: none;
margin-bottom: 8px;
word-break: keep-all;
&.popup-btn-video {
background: #fff3e0;
color: #ff9800;
border: 1px solid #ff9800;
}
&.popup-btn-batch {
background: #fff;
color: #ff9800;
border: 1px solid #ff9800;
}
}
}
.batch-subscribe-content {
.batch-title {
font-size: 14px;
color: #333;
margin-bottom: 8px;
text-align: center;
}
.batch-info {
font-size: 12px;
color: #666;
margin-bottom: 16px;
text-align: center;
}
.batch-current-info {
font-size: 12px;
color: #666;
text-align: center;
margin-bottom: 8px;
}
.batch-counter {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin-bottom: 16px;
.counter-btn {
width: 32px;
height: 32px;
border-radius: 50%;
background: #f5f5f5;
border: none;
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
color: #333;
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
.counter-input {
width: 60px;
height: 32px;
text-align: center;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
}
.batch-btns {
display: flex;
gap: 12px;
justify-content: center;
.batch-cancel-btn {
background: #f5f5f5;
color: #666;
border-radius: 16px;
font-size: 14px;
padding: 0 16px;
border: none;
}
.batch-confirm-btn {
background: #ff9800;
color: #fff;
border-radius: 16px;
font-size: 14px;
padding: 0 16px;
border: none;
&:disabled {
background: #ccc;
color: #666;
cursor: not-allowed;
}
}
}
.no-chapters-tip {
font-size: 12px;
color: #999;
text-align: center;
}
}
}
&.dark-mode {
.content {
.popup-title {
color: #eee;
}
.popup-desc {
color: #bbb;
}
.popup-btns {
.popup-btn {
background: #ff9800;
color: #fff;
&.popup-btn-video {
background: rgba(255, 152, 0, 0.1);
color: #ff9800;
border: 1px solid #ff9800;
}
&.popup-btn-batch {
background: #333;
color: #ff9800;
border: 1px solid #ff9800;
}
}
}
.batch-subscribe-content {
.batch-title {
color: #eee;
}
.batch-info {
color: #bbb;
}
.batch-current-info {
color: #eee;
}
.batch-counter {
.counter-btn {
background: #444;
color: #eee;
}
.counter-input {
background: #333;
border: 1px solid #555;
color: #eee;
}
}
.batch-btns {
.batch-cancel-btn {
background: #444;
color: #bbb;
}
.batch-confirm-btn {
background: #ff9800;
color: #fff;
&:disabled {
background: #444;
color: #999;
}
}
}
.no-chapters-tip {
color: #999;
}
}
}
}
}
</style>

+ 1
- 1
src/components/ranking/IntimacyRanking.vue View File

@ -2,7 +2,7 @@
<div class="intimacy-ranking-container">
<div class="ranking-header">
<div class="crown-wrapper">
<img src="@/assets/images/读者榜单.png" alt="皇冠" class="crown-icon">
<img src="@/assets/images/crown.png" alt="皇冠" class="crown-icon">
</div>
<div class="title-wrapper">
<span class="header-title">读者亲密值榜单</span>


+ 36
- 0
src/utils/theme.js View File

@ -0,0 +1,36 @@
import { ref } from 'vue'
// 主题状态
const isDarkMode = ref(false)
// 切换主题
export const toggleTheme = () => {
isDarkMode.value = !isDarkMode.value
if (isDarkMode.value) {
document.documentElement.classList.add('dark-theme')
} else {
document.documentElement.classList.remove('dark-theme')
}
}
// 获取主题状态
export const getThemeMode = () => {
return isDarkMode.value
}
// 设置主题状态
export const setThemeMode = (dark) => {
isDarkMode.value = dark
if (dark) {
document.documentElement.classList.add('dark-theme')
} else {
document.documentElement.classList.remove('dark-theme')
}
}
export default {
isDarkMode,
toggleTheme,
getThemeMode,
setThemeMode
}

+ 135
- 28
src/views/author/WorkEdit.vue View File

@ -64,7 +64,7 @@
</el-button>
</div>
<div class="chapter-status">
<span class="status-tag" :class="chapter.status">{{ getStatusText(chapter._status) }}</span>
<span class="status-tag" :class="getStatusClass(chapter._status, chapter.state)">{{ getStatusText(chapter._status, chapter.state) }}</span>
<span class="word-count">{{ chapter.num }}</span>
</div>
</div>
@ -76,28 +76,45 @@
</div>
<div class="editor-container">
<div class="chapter-header">
<el-input
v-model="chapterTitle"
placeholder="请输入章节标题"
clearable
maxlength="50"
show-word-limit
></el-input>
</div>
<div class="chapter-editor">
<el-input
v-model="chapterContent"
type="textarea"
:rows="20"
placeholder="请输入章节正文内容..."
resize="none"
></el-input>
<div class="editor-footer">
<span>字数统计{{ num }}</span>
<span>上次保存{{ lastSaved }}</span>
<!-- 当有章节时显示编辑器 -->
<template v-if="chapters.length > 0">
<div class="chapter-header">
<el-input
v-model="chapterTitle"
placeholder="请输入章节标题"
clearable
maxlength="50"
show-word-limit
></el-input>
</div>
</div>
<div class="chapter-editor">
<el-input
v-model="chapterContent"
type="textarea"
:rows="20"
placeholder="请输入章节正文内容..."
resize="none"
></el-input>
<div class="editor-footer">
<span>字数统计{{ num }}</span>
<span>上次保存{{ lastSaved }}</span>
</div>
</div>
</template>
<!-- 当没有章节时显示提示 -->
<template v-else>
<div class="empty-editor">
<el-empty
description="暂无章节,点击左侧新建章节开始创作"
:image-size="120"
>
<el-button type="primary" @click="createNewChapter">
新建章节
</el-button>
</el-empty>
</div>
</template>
</div>
</div>
</div>
@ -298,15 +315,51 @@ export default defineComponent({
};
//
const getStatusText = (status) => {
const getStatusText = (status, state) => {
//
if (status == '1' || status == 1) {
const stateMap = {
0: '审核中',
1: '已发布',
2: '已驳回'
};
// state
const stateValue = parseInt(state) || 0;
return stateMap[stateValue] || '审核中';
}
//
const statusMap = {
'new': '未保存',
'0': '草稿',
'1': '已发布'
0: '草稿'
};
return statusMap[status] || status;
};
//
const getStatusClass = (status, state) => {
//
if (status == '1' || status == 1) {
const stateClassMap = {
0: 'reviewing', //
1: 'published', //
2: 'rejected' //
};
// state
const stateValue = parseInt(state) || 0;
return stateClassMap[stateValue] || 'reviewing';
}
//
const statusClassMap = {
'new': 'new',
'0': 'draft',
0: 'draft'
};
return statusClassMap[status] || 'default';
};
//
const goToWorksList = async () => {
if (await checkSaveStatus()) {
@ -357,6 +410,7 @@ export default defineComponent({
chapters.value = chaptersResponse.result.records.map(chapter => ({
...chapter,
_status: chapter.status,
state: chapter.state || 0, // state
details: chapter.details || '',
title: chapter.title || '',
num: chapter.num || 0,
@ -372,11 +426,15 @@ export default defineComponent({
await selectChapter(savedChapters[savedChapters.length - 1]);
}
} else {
throw new Error(chaptersResponse.message || '获取章节列表失败');
//
chapters.value = [];
}
} catch (error) {
console.error('加载作品信息失败:', error);
ElMessage.error('加载作品信息失败');
//
chapters.value = [];
}
};
@ -579,6 +637,7 @@ export default defineComponent({
saveChapter,
publishChapter,
getStatusText,
getStatusClass,
goToWorksList,
moveChapterUp,
moveChapterDown,
@ -795,14 +854,37 @@ export default defineComponent({
border-color: #ffd591;
}
// 稿 (status = 0 "0")
&.draft {
background-color: #f6ffed;
color: #52c41a;
border-color: #b7eb8f;
}
&.published {
background-color: #e6f7ff;
color: #1890ff;
border-color: #91d5ff;
}
&.reviewing {
background-color: #fff7e6;
color: #fa8c16;
border-color: #ffd591;
}
&.rejected {
background-color: #fff2f0;
color: #ff4d4f;
border-color: #ffccc7;
}
//
&[class*="0"] {
background-color: #f6ffed;
color: #52c41a;
border-color: #b7eb8f;
}
// (status = 1 "1")
&[class*="1"] {
background-color: #e6f7ff;
color: #1890ff;
@ -810,7 +892,7 @@ export default defineComponent({
}
//
&:not(.new):not([class*="0"]):not([class*="1"]) {
&:not(.new):not(.draft):not(.published):not(.reviewing):not(.rejected):not([class*="0"]):not([class*="1"]) {
background-color: #fafafa;
color: #666;
border-color: #d9d9d9;
@ -875,6 +957,31 @@ export default defineComponent({
font-size: 14px;
}
}
.empty-editor {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
:deep(.el-empty) {
.el-empty__description {
color: #666;
font-size: 16px;
margin-bottom: 20px;
}
.el-button--primary {
background-color: #0A2463;
border-color: #0A2463;
&:hover, &:focus {
background-color: #1a3473;
border-color: #1a3473;
}
}
}
}
}
}
}

+ 24
- 22
src/views/author/components/ReadersManagement.vue View File

@ -42,7 +42,7 @@
</div>
<!-- 驳回原因显示 -->
<div v-if="form.status === 2 && form.rejectReason" class="reject-reason">
<div v-if="form.status == 2 && form.rejectReason" class="reject-reason">
<el-alert
title="驳回原因"
:description="form.rejectReason"
@ -71,7 +71,7 @@ export default defineComponent({
//
const canEdit = computed(() => {
return form.status === undefined || form.status === 2; //
return form.status == undefined || form.status == 2; //
});
//
@ -83,22 +83,18 @@ export default defineComponent({
//
const getStatusType = (status) => {
switch (status) {
case 0: return 'warning'; //
case 1: return 'success'; //
case 2: return 'danger'; //
default: return 'info';
}
if (status == 0) return 'warning'; //
if (status == 1) return 'success'; //
if (status == 2) return 'danger'; //
return 'info';
};
//
const getStatusText = (status) => {
switch (status) {
case 0: return '待审核';
case 1: return '已通过';
case 2: return '已驳回';
default: return '未设置';
}
if (status == 0) return '待审核';
if (status == 1) return '已通过';
if (status == 2) return '已驳回';
return '未设置';
};
//
@ -106,13 +102,13 @@ export default defineComponent({
if (isSubmitting.value) {
return '提交中...';
}
if (form.status === 0) {
if (form.status == 0) {
return '审核中';
}
if (form.status === 1) {
if (form.status == 1) {
return '已通过';
}
if (form.status === 2) {
if (form.status == 2) {
return '重新提交';
}
return '提交申请';
@ -136,8 +132,12 @@ export default defineComponent({
try {
const response = await achievementApi.getAchievement();
if (response.success && response.result) {
response.result.status = parseInt(response.result.status)
Object.assign(form, response.result);
//
const result = {
...response.result,
status: parseInt(response.result.status) || 0
};
Object.assign(form, result);
}
} catch (error) {
console.error('获取成就设置失败:', error);
@ -155,7 +155,7 @@ export default defineComponent({
}
//
if (form.status === 1) {
if (form.status == 1) {
ElMessage.warning('成就设置已通过,无需重复提交');
return;
}
@ -176,8 +176,10 @@ export default defineComponent({
const response = await achievementApi.setAchievementName(data);
if (response.success) {
ElMessage.success('提交成功,请等待审核');
//
await getAchievementDetail();
//
setTimeout(async () => {
await getAchievementDetail();
}, 500);
} else {
ElMessage.error(response.message || '提交失败');
}


+ 233
- 26
src/views/book/chapter.vue View File

@ -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,
});
// payResponsetruefalse
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];
// 使payisPay'Y'payfalse
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;


+ 3
- 3
src/views/home/Home.vue View File

@ -15,10 +15,10 @@
<el-carousel-item v-for="(banner, index) in banners" :key="index"
@click="goToDetail(banner.id)">
<div class="banner-item" :style="{ backgroundImage: `url(${banner.image})` }">
<div class="banner-content">
<!-- <div class="banner-content">
<h2>{{ banner.title }}</h2>
<p>{{ banner.details }}</p>
</div>
</div> -->
</div>
</el-carousel-item>
</el-carousel>
@ -241,7 +241,7 @@ export default {
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.4);
//background: rgba(0, 0, 0, 0.4);
border-radius: 8px;
}


+ 2
- 2
src/views/user/MoneyLog.vue View File

@ -49,13 +49,13 @@
</span>
</template>
</el-table-column>
<el-table-column prop="state" label="状态" width="100">
<!-- <el-table-column prop="state" label="状态" width="100">
<template #default="scope">
<el-tag :type="getStatusType(scope.row.state)">
{{ getStatusText(scope.row.state) }}
</el-tag>
</template>
</el-table-column>
</el-table-column> -->
</el-table>
<!-- 分页 -->


+ 22
- 21
src/views/user/TaskCenter.vue View File

@ -28,7 +28,7 @@
<el-icon v-if="day.commonSignLog" class="check-icon"><Check /></el-icon>
</div>
<div class="reward">
<img :src="day.image || '/src/assets/images/推荐票.png'" alt="推荐票" class="reward-icon">
<img :src="day.image || '@/assets/images/Recommendationvote.png'" alt="推荐票" class="reward-icon">
<span>推荐票 +{{ day.num }}</span>
</div>
</div>
@ -60,7 +60,7 @@
<h3 class="task-name">{{ task.title }}</h3>
<div class="task-reward">
<img
src="/src/assets/images/推荐票.png"
src="@/assets/images/Recommendationvote.png"
alt="推荐票"
class="reward-icon"
>
@ -143,7 +143,7 @@ export default {
try {
const res = await taskApi.getSignTaskToday();
// res.result == 0
canSignToday.value = res.result === 0;
canSignToday.value = res.result;
} catch (error) {
console.error('获取今日签到状态失败:', error);
//
@ -397,24 +397,25 @@ export default {
}
}
.sign-button-container {
text-align: center;
margin-top: 16px;
.sign-button {
width: 280px;
height: 44px;
border-radius: 22px;
font-weight: 600;
background-color: #0A2463;
border-color: #0A2463;
&.checked-btn {
background: linear-gradient(90deg, #e0e0e0, #bdbdbd) !important;
border-color: #bdbdbd !important;
color: #666 !important;
cursor: not-allowed;
}
}
.sign-button-container {
margin-top: 16px;
display: flex;
justify-content: center;
.sign-button {
width: 280px;
height: 44px;
border-radius: 22px;
font-weight: 600;
background-color: #0A2463;
border-color: #0A2463;
&.checked-btn {
background: linear-gradient(90deg, #e0e0e0, #bdbdbd) !important;
border-color: #bdbdbd !important;
color: #666 !important;
cursor: not-allowed;
}
}
}


Loading…
Cancel
Save