Browse Source

feat(阅读功能): 添加批量订阅和阅读记录功能

- 在订阅弹窗中新增批量订阅功能,支持选择章节数量
- 添加获取用户阅读记录的API接口
- 根据阅读记录自动跳转到上次阅读章节
- 优化付费章节标识,区分已付费和未付费状态
- 修复书籍全选功能在阅读列表中的逻辑错误
- 调整本地开发环境的API基础地址
master
前端-胡立永 3 weeks ago
parent
commit
85f7b2ba6f
9 changed files with 393 additions and 14 deletions
  1. +5
    -0
      api/model/bookshelf.js
  2. +2
    -2
      config.js
  3. +2
    -2
      pages/index/bookshelf.vue
  4. +0
    -1
      pages/index/category.vue
  5. +1
    -1
      pages_order/author/createNovel.vue
  6. +16
    -1
      pages_order/components/novel/chapterPopup.vue
  7. +253
    -3
      pages_order/components/novel/subscriptionPopup.vue
  8. +32
    -3
      pages_order/novel/novelDetail.vue
  9. +82
    -1
      pages_order/novel/readnovels.vue

+ 5
- 0
api/model/bookshelf.js View File

@ -42,6 +42,11 @@ const api = {
method: 'POST', method: 'POST',
auth: true, auth: true,
}, },
// 根据书籍id获取当前阅读章节
getReadChapterByBookId: {
url: '/all_book/getReadChapterByBookId',
method: 'GET',
},
} }
export default api export default api

+ 2
- 2
config.js View File

@ -8,13 +8,13 @@ import uvUI from '@/uni_modules/uv-ui-tools'
Vue.use(uvUI); Vue.use(uvUI);
// 当前环境 // 当前环境
const type = 'prod'
const type = 'local'
// 环境配置 // 环境配置
const config = { const config = {
local : { local : {
baseUrl : 'http://127.0.0.1:8003/novel-admin',
baseUrl : 'http://127.0.0.1:8002/novel-admin',
}, },
dev : { dev : {
baseUrl : 'http://h5.xzaiyp.top/novel-admin', baseUrl : 'http://h5.xzaiyp.top/novel-admin',


+ 2
- 2
pages/index/bookshelf.vue View File

@ -332,11 +332,11 @@
selectAll() { selectAll() {
if (this.activeTab === 'read') { if (this.activeTab === 'read') {
// //
if (this.selectedItems.length === this.novels.length) {
if (this.selectedItems.length === this.list.length) {
this.selectedItems = []; this.selectedItems = [];
} else { } else {
// //
this.selectedItems = this.novels.map(novel => novel.id);
this.selectedItems = this.list.map(novel => novel.id);
} }
} else { } else {
// //


+ 0
- 1
pages/index/category.vue View File

@ -109,7 +109,6 @@
}, },
clickTabs({index}){ clickTabs({index}){
this.current = index this.current = index
this.currentChildren = 0
// //
this.getData() this.getData()
}, },


+ 1
- 1
pages_order/author/createNovel.vue View File

@ -192,7 +192,7 @@
}) })
return return
} }
if (!this.formData.shopClass) {
if (this.classList.length === 0) {
uni.showToast({ uni.showToast({
title: '请选择作品分类', title: '请选择作品分类',
icon: 'none' icon: 'none'


+ 16
- 1
pages_order/components/novel/chapterPopup.vue View File

@ -17,7 +17,8 @@
:class="['catalog-item theme-transition', {active: idx == currentIndex}]"> :class="['catalog-item theme-transition', {active: idx == currentIndex}]">
<view class="item-main"> <view class="item-main">
<text class="item-title theme-transition">{{ item.title }}</text> <text class="item-title theme-transition">{{ item.title }}</text>
<text v-if="item.isPay == 'Y'" class="vip-tag theme-transition">付费</text>
<text v-if="item.isPay == 'Y' && item.pay" class="paid-tag theme-transition">已付费</text>
<text v-else-if="item.isPay == 'Y'" class="vip-tag theme-transition">付费</text>
</view> </view>
</view> </view>
<uv-empty mode="list" v-if="chapterList.length == 0"></uv-empty> <uv-empty mode="list" v-if="chapterList.length == 0"></uv-empty>
@ -172,6 +173,15 @@
padding: 2rpx 18rpx; padding: 2rpx 18rpx;
margin-left: 16rpx; margin-left: 16rpx;
} }
.paid-tag {
background: #e8f5e8;
color: #4caf50;
border-radius: 20rpx;
font-size: 24rpx;
padding: 2rpx 18rpx;
margin-left: 16rpx;
}
} }
} }
@ -212,6 +222,11 @@
background: rgba(255, 153, 0, 0.2); background: rgba(255, 153, 0, 0.2);
color: #ff9900; color: #ff9900;
} }
.paid-tag {
background: rgba(76, 175, 80, 0.2);
color: #4caf50;
}
} }
} }
} }


+ 253
- 3
pages_order/components/novel/subscriptionPopup.vue View File

@ -7,10 +7,40 @@
:customStyle="popupStyle"> :customStyle="popupStyle">
<view class="content theme-transition"> <view class="content theme-transition">
<view class="popup-title theme-transition">{{ title }}</view> <view class="popup-title theme-transition">{{ title }}</view>
<view class="popup-btns">
<!-- 默认订阅界面 -->
<view v-if="!showBatchDialog" class="popup-btns">
<button class="popup-btn theme-transition" @click="handleSubscribe">订阅本章</button> <button class="popup-btn theme-transition" @click="handleSubscribe">订阅本章</button>
<button class="popup-btn theme-transition">观看视频解锁</button>
<button class="popup-btn theme-transition">批量订阅</button>
<button class="popup-btn theme-transition" @click="handleVideoUnlock">观看视频解锁</button>
<button class="popup-btn theme-transition" @click="showBatchSubscribe">批量订阅</button>
</view>
<!-- 批量订阅界面 -->
<view v-else class="batch-subscribe-content">
<view class="batch-title">选择订阅章节数量</view>
<view class="batch-info">
<text>从当前章节开始可订阅 {{ availableChaptersCount }} </text>
</view>
<view v-if="availableChaptersCount === 0" class="no-chapters-tip">
<text>暂无需要付费的章节</text>
</view>
<view v-else>
<view class="batch-current-info">
<text>连续订阅 {{ batchCount }} </text>
</view>
<view class="batch-counter">
<button class="counter-btn" @click="decreaseBatchCount" :disabled="batchCount <= 1">-</button>
<input type="number" v-model="batchCount" class="counter-input"
:max="maxBatchCount" @input="validateBatchCount" />
<button class="counter-btn" @click="increaseBatchCount" :disabled="batchCount >= maxBatchCount">+</button>
</view>
</view>
<view class="batch-btns">
<button class="batch-cancel-btn theme-transition" @click="cancelBatchSubscribe">取消</button>
<button class="batch-confirm-btn theme-transition"
@click="handleBatchSubscribe"
:disabled="availableChaptersCount === 0">确认订阅</button>
</view>
</view> </view>
</view> </view>
</uv-popup> </uv-popup>
@ -28,9 +58,27 @@
type: String, type: String,
default: '这是付费章节 需要订阅后才能阅读' default: '这是付费章节 需要订阅后才能阅读'
}, },
chapterList: {
type: Array,
default: () => []
},
currentChapter: {
type: Object,
default: () => ({})
},
currentIndex: {
type: Number,
default: 0
},
bookId: {
type: [String, Number],
default: ''
}
}, },
data() { data() {
return { return {
showBatchDialog: false,
batchCount: 5
} }
}, },
computed: { computed: {
@ -42,6 +90,23 @@
'border-radius': '24rpx', 'border-radius': '24rpx',
'min-width': '500rpx' 'min-width': '500rpx'
} }
},
//
availableChaptersCount() {
if (!this.chapterList.length || this.currentIndex < 0) return 0;
let count = 0;
for (let i = this.currentIndex; i < this.chapterList.length; i++) {
const chapter = this.chapterList[i];
if (chapter.isPay === 'Y' && !chapter.pay) {
count++;
}
}
return count;
},
//
maxBatchCount() {
return Math.max(1, this.availableChaptersCount);
} }
}, },
methods: { methods: {
@ -59,6 +124,49 @@
handleSubscribe() { handleSubscribe() {
this.$emit('subscribe'); this.$emit('subscribe');
this.close(); this.close();
},
//
handleVideoUnlock() {
this.$emit('videoUnlock');
this.close();
},
//
showBatchSubscribe() {
//
this.batchCount = Math.min(5, this.maxBatchCount);
this.showBatchDialog = true;
},
//
cancelBatchSubscribe() {
this.showBatchDialog = false;
},
//
handleBatchSubscribe() {
this.$emit('batchSubscribe', this.batchCount);
this.showBatchDialog = false;
},
//
decreaseBatchCount() {
if (this.batchCount > 1) {
this.batchCount--;
}
},
//
increaseBatchCount() {
if (this.batchCount < this.maxBatchCount) {
this.batchCount++;
}
},
//
validateBatchCount() {
this.batchCount = Math.max(1, Math.min(this.maxBatchCount, parseInt(this.batchCount)));
} }
} }
} }
@ -113,6 +221,100 @@
} }
} }
} }
.batch-subscribe-content {
.batch-title {
font-size: 28rpx;
color: #333;
margin-bottom: 16rpx;
text-align: center;
}
.batch-info {
font-size: 24rpx;
color: #666;
margin-bottom: 32rpx;
text-align: center;
}
.batch-current-info {
font-size: 24rpx;
color: #666;
text-align: center;
margin-bottom: 16rpx;
}
.batch-counter {
display: flex;
align-items: center;
justify-content: center;
gap: 16rpx;
margin-bottom: 32rpx;
.counter-btn {
width: 64rpx;
height: 64rpx;
border-radius: 50%;
background: #f5f5f5;
border: none;
font-size: 32rpx;
display: flex;
align-items: center;
justify-content: center;
color: #333;
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
.counter-input {
width: 120rpx;
height: 64rpx;
text-align: center;
border: 1px solid #ddd;
border-radius: 8rpx;
font-size: 28rpx;
}
}
.batch-btns {
display: flex;
gap: 24rpx;
justify-content: center;
.batch-cancel-btn {
background: #f5f5f5;
color: #666;
border-radius: 32rpx;
font-size: 28rpx;
padding: 0 32rpx;
border: none;
}
.batch-confirm-btn {
background: #ff9800;
color: #fff;
border-radius: 32rpx;
font-size: 28rpx;
padding: 0 32rpx;
border: none;
&:disabled {
background: #ccc;
color: #666;
cursor: not-allowed;
}
}
}
.no-chapters-tip {
font-size: 24rpx;
color: #999;
text-align: center;
}
}
} }
&.dark-mode { &.dark-mode {
@ -143,6 +345,54 @@
} }
} }
} }
.batch-subscribe-content {
.batch-title {
color: $dark-text-color-primary;
}
.batch-info {
color: $dark-text-color-tertiary;
}
.batch-current-info {
color: $dark-text-color-primary;
}
.batch-counter {
.counter-btn {
background: $dark-bg-color-tertiary;
color: $dark-text-color-primary;
}
.counter-input {
background: $dark-bg-color-secondary;
border: 1px solid $dark-border-color;
color: $dark-text-color-primary;
}
}
.batch-btns {
.batch-cancel-btn {
background: $dark-bg-color-tertiary;
color: $dark-text-color-secondary;
}
.batch-confirm-btn {
background: #ff9800;
color: #fff;
&:disabled {
background: $dark-bg-color-tertiary;
color: $dark-text-color-tertiary;
}
}
}
.no-chapters-tip {
color: $dark-text-color-tertiary;
}
}
} }
} }
} }

+ 32
- 3
pages_order/novel/novelDetail.vue View File

@ -228,6 +228,7 @@
chapterList : [],// chapterList : [],//
isBooshelf : false, isBooshelf : false,
readingRecord: {}, //
} }
}, },
computed: {}, computed: {},
@ -243,6 +244,7 @@
this.isAddBook() this.isAddBook()
if(this.isLogin){ if(this.isLogin){
this.getAchievement() this.getAchievement()
this.getReadingRecord()
} }
}, },
methods: { methods: {
@ -338,7 +340,26 @@
bookId: this.id bookId: this.id
}) })
}, },
//
async getReadingRecord() {
try {
const res = await this.$fetch('getReadChapterByBookId', {
bookId : this.id
});
this.readingRecord = res || {};
} catch (error) {
console.error('获取阅读记录失败:', error);
this.readingRecord = {};
}
},
toRead() { toRead() {
//
if (!uni.getStorageSync('token')) {
this.$utils.toLogin();
return;
}
if (!this.fastCatalog) { if (!this.fastCatalog) {
uni.showToast({ uni.showToast({
title: '暂无章节', title: '暂无章节',
@ -347,9 +368,17 @@
return return
} }
uni.navigateTo({
url: `/pages_order/novel/readnovels?cid=${this.fastCatalog.id}&id=${this.id}`
})
// novelId
if (this.readingRecord && this.readingRecord.id) {
uni.navigateTo({
url: `/pages_order/novel/readnovels?cid=${this.readingRecord.id}&id=${this.id}`
});
} else {
//
uni.navigateTo({
url: `/pages_order/novel/readnovels?cid=${this.fastCatalog.id}&id=${this.id}`
});
}
}, },
selectChapter({item, index}){ selectChapter({item, index}){
uni.navigateTo({ uni.navigateTo({


+ 82
- 1
pages_order/novel/readnovels.vue View File

@ -66,8 +66,14 @@
<!-- 使用封装的订阅弹窗组件 --> <!-- 使用封装的订阅弹窗组件 -->
<subscriptionPopup ref="subscriptionPopup" <subscriptionPopup ref="subscriptionPopup"
:chapterList="chapterList"
:currentChapter="currentChapterInfo"
:currentIndex="currentIndex"
:bookId="id"
@maskClick="toggleFullScreen" @maskClick="toggleFullScreen"
@subscribe="goToSubscription" />
@subscribe="goToSubscription"
@batchSubscribe="handleBatchSubscribe"
@videoUnlock="handleVideoUnlock" />
<novelVotePopup ref="novelVotePopup" /> <novelVotePopup ref="novelVotePopup" />
@ -116,6 +122,9 @@
return -1 return -1
}, },
currentChapterInfo() {
return this.chapterList.find(chapter => chapter.id == this.cid) || {}
}
}, },
onLoad({ onLoad({
id, id,
@ -269,6 +278,78 @@
this.$refs.subscriptionPopup.close() this.$refs.subscriptionPopup.close()
} }
}, },
//
async handleBatchSubscribe(batchCount) {
try {
// ID
const chapterIds = [];
const startIndex = this.currentIndex;
for (let i = 0; i < batchCount && (startIndex + i) < this.chapterList.length; i++) {
const chapter = this.chapterList[startIndex + i];
if (chapter.isPay === 'Y' && !chapter.pay) {
chapterIds.push(chapter.id);
}
}
if (chapterIds.length === 0) {
uni.showToast({
title: '没有需要购买的章节',
icon: 'none'
});
return;
}
//
await this.$fetch('buyNovel', {
bookId: this.id,
novelId: chapterIds.join(',')
});
uni.showToast({
title: `成功订阅${chapterIds.length}`,
icon: 'success'
});
//
this.getBookCatalogList();
this.isPay = false;
this.updateSub();
} catch (error) {
console.error('批量订阅失败:', error);
uni.showToast({
title: '订阅失败,请重试',
icon: 'none'
});
}
},
//
async handleVideoUnlock() {
try {
await this.$fetch('openBookCatalog', {
bookId: this.id,
catalogId: this.cid
});
uni.showToast({
title: '视频解锁成功',
icon: 'success'
});
this.isPay = false;
this.updateSub();
} catch (error) {
console.error('视频解锁失败:', error);
uni.showToast({
title: '解锁失败,请重试',
icon: 'none'
});
}
},
}, },
} }
</script> </script>


Loading…
Cancel
Save