Browse Source

feat: 新增钱包流水功能并优化多个页面功能

- 新增钱包流水功能,包括充值记录和支付记录展示
- 新增签到记录弹窗组件
- 优化小说详情页的订阅和阅读体验
- 修复修改个人信息页面签名字段不显示的问题
- 调整章节编辑页面的付费开关样式和大小
- 优化书架页面跳转逻辑,支持直接跳转到阅读记录章节
- 修复任务中心签到状态判断逻辑
- 新增书籍状态组件,统一展示连载/完结状态
- 优化评论列表和我的评论页面显示逻辑
- 调整个人中心页面布局和样式
- 新增充值功能入口页面
- 优化章节列表和章节内容的付费标识显示
- 修复成就等级设置页面数据加载问题
- 优化打赏排行榜数据显示和交互
- 调整环境配置为开发环境
master
前端-胡立永 4 months ago
parent
commit
e433ef1c9c
35 changed files with 2141 additions and 624 deletions
  1. +1
    -1
      api/api.js
  2. +2
    -2
      api/model/achievement.js
  3. +17
    -0
      api/model/all_money.js
  4. +6
    -0
      api/model/bookshelf.js
  5. +6
    -0
      api/model/my_book.js
  6. +24
    -0
      api/model/order.js
  7. +6
    -0
      api/model/task.js
  8. +51
    -0
      components/novel/bookStatus.vue
  9. +5
    -24
      components/novel/novelItem.vue
  10. +1
    -1
      config.js
  11. +6
    -2
      pages.json
  12. +44
    -6
      pages/index/bookshelf.vue
  13. +21
    -10
      pages/index/center.vue
  14. +6
    -1
      pages_order/auth/Modifyinformation.vue
  15. +1
    -0
      pages_order/auth/wxLogin.vue
  16. +14
    -1
      pages_order/author/chapterList.vue
  17. +1
    -1
      pages_order/author/createNovel.vue
  18. +14
    -1
      pages_order/author/editor.vue
  19. +30
    -24
      pages_order/comment/myComment.vue
  20. +5
    -0
      pages_order/components/comment/myCommentItem.vue
  21. +39
    -6
      pages_order/components/novel/RankListItem.vue
  22. +1
    -1
      pages_order/components/novel/chapterPopup.vue
  23. +408
    -0
      pages_order/components/novel/interactiveGiftPopup.vue
  24. +218
    -0
      pages_order/components/novel/signRecordPopup.vue
  25. +8
    -18
      pages_order/components/novel/subscriptionPopup.vue
  26. +364
    -0
      pages_order/mine/Walletflow.vue
  27. +530
    -0
      pages_order/mine/recharge.vue
  28. +61
    -32
      pages_order/novel/ReaderAchievement.vue
  29. +51
    -12
      pages_order/novel/Tipping.vue
  30. +74
    -54
      pages_order/novel/Translation.vue
  31. +0
    -388
      pages_order/novel/Walletflow.vue
  32. +4
    -3
      pages_order/novel/bookList.vue
  33. +41
    -15
      pages_order/novel/novelDetail.vue
  34. +80
    -20
      pages_order/novel/readnovels.vue
  35. +1
    -1
      uni_modules/uv-switch/components/uv-switch/props.js

+ 1
- 1
api/api.js View File

@ -6,7 +6,7 @@ let limit = {}
let debounce = {}
const models = ['login', 'index', 'bookshelf', 'my_book', 'comment', 'task'
, 'order', 'writer', 'achievement']
, 'order', 'writer', 'achievement', 'all_money']
const config = {
// 示例


+ 2
- 2
api/model/achievement.js View File

@ -3,7 +3,7 @@ const api = {
// 设置读者成就等级名称
setAchievementName: {
url: '/all_achievement/setAchievementName',
method: 'GET',
method: 'POST',
auth: true,
},
// 根据用户标识和书籍标识查询该用户的成就等级
@ -19,4 +19,4 @@ const api = {
},
}
export default api
export default api

+ 17
- 0
api/model/all_money.js View File

@ -0,0 +1,17 @@
const api = {
// 获取我的可用积分数
getMyMoneyNum : {
url: '/all_money/getMyMoneyNum',
method: 'GET',
auth: true,
},
// 获取我的流水列表带分页
getMyMoneyLogPage : {
url: '/all_money/getMyMoneyLogPage',
method: 'GET',
auth: true,
},
}
export default api

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

@ -25,6 +25,12 @@ const api = {
method: 'POST',
auth: true,
},
// 修改我的书籍信息
saveOrUpdateReadBook: {
url: '/all_book/saveOrUpdateReadBook',
method: 'POST',
auth: true,
},
}
export default api

+ 6
- 0
api/model/my_book.js View File

@ -40,6 +40,12 @@ const api = {
method: 'POST',
auth: true,
},
// 查询我是否购买了这个章节
getMyShopNovel: {
url: '/my_book/getMyShopNovel',
method: 'GET',
auth: true,
},
}
export default api

+ 24
- 0
api/model/order.js View File

@ -37,6 +37,30 @@ const api = {
method: 'POST',
auth: true,
},
// 创建支付套餐订单
createPayPackageOrder : {
url: '/my_order/createPayPackageOrder',
method: 'POST',
auth: true,
},
// 充值套餐列表
getPayPackageList : {
url: '/my_order/getPayPackageList',
method: 'GET',
auth: true,
},
// 根据书籍id,礼物id赠送礼物
giveGift : {
url: '/my_order/giveGift',
method: 'POST',
auth: true,
},
// 购买章节
buyNovel : {
url: '/my_order/buyNovel',
method: 'POST',
auth: true,
},
}
export default api

+ 6
- 0
api/model/task.js View File

@ -47,6 +47,12 @@ const api = {
method: 'GET',
auth: true,
},
// 获取我当天是否以及签到
getSignTaskToday: {
url: '/my_task/getSignTaskToday',
method: 'GET',
auth: true,
},
}
export default api

+ 51
- 0
components/novel/bookStatus.vue View File

@ -0,0 +1,51 @@
<template>
<view class="book-status"
:class="statusClass">
{{ statusText }}
</view>
</template>
<script>
export default {
props : ['status'],
computed: {
statusClass() {
const statusMap = {
'0': 'ongoing',
'1': 'completed'
};
return statusMap[this.status] || 'ongoing';
},
statusText() {
const textMap = {
// '0': '',
'0': '连载中',
'1': '已完结'
};
return textMap[this.status] || '连载中';
},
},
data() {
return {
}
},
methods: {
}
}
</script>
<style scoped lang="scss">
.book-status {
font-size: 20rpx;
color: #67C23A;
background-color: rgba(103, 194, 58, 0.1);
border-radius: 20rpx;
padding: 4rpx 12rpx;
&.ongoing{
color: #ffa502;
background-color: #ffa50223;
}
}
</style>

+ 5
- 24
components/novel/novelItem.vue View File

@ -14,10 +14,7 @@
</view> -->
<view class="content-row" v-if="!horizontal">
<view class="book-status">
<text>{{ statusText }}</text>
</view>
<bookStatus :status="book.status"/>
<view class="book-text">
{{ item.service || '大家都在读' }}
</view>
@ -27,7 +24,11 @@
</template>
<script>
import bookStatus from './bookStatus.vue';
export default {
components : {
bookStatus,
},
props: {
book: {
type: Object,
@ -136,18 +137,6 @@
margin-right: 10rpx;
}
}
.book-status {
flex-shrink: 0;
text {
font-size: 20rpx;
color: #67C23A;
background-color: rgba(103, 194, 58, 0.1);
border-radius: 20rpx;
padding: 4rpx 12rpx;
}
}
.book-text{
font-size: 20rpx;
}
@ -198,14 +187,6 @@
.book-tags {
display: none;
}
.book-status {
flex-shrink: 0;
text {
font-size: 18rpx;
}
}
}
}
</style>

+ 1
- 1
config.js View File

@ -8,7 +8,7 @@ import uvUI from '@/uni_modules/uv-ui-tools'
Vue.use(uvUI);
// 当前环境
const type = 'prod'
const type = 'dev'
// 环境配置


+ 6
- 2
pages.json View File

@ -3,7 +3,8 @@
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "",
"navigationBarTextStyle": "white"
"navigationBarTextStyle": "white",
"enablePullDownRefresh": true
}
},
{
@ -92,7 +93,7 @@
"path": "comment/respondComments"
},
{
"path": "novel/Walletflow"
"path": "mine/Walletflow"
},
{
"path": "author/creator"
@ -114,6 +115,9 @@
},
{
"path": "auth/Modifyinformation"
},
{
"path": "mine/recharge"
}
]
}],


+ 44
- 6
pages/index/bookshelf.vue View File

@ -6,7 +6,10 @@
<view class="header-content">
<view class="tab-container">
<view class="tab" :class="{'active': activeTab === 'read'}" @click="switchTab('read')">阅读</view>
<view class="tab" :class="{'active': activeTab === 'work'}" @click="switchTab('work')">作品</view>
<view class="tab" :class="{'active': activeTab === 'work'}"
v-if="userInfo.isUser == 'Y'"
@click="switchTab('work')">作品</view>
</view>
</view>
</view>
@ -17,7 +20,7 @@
<view class="novel-item"
v-for="(novel, index) in list"
:key="novel.id"
@click="toNovelDetail(novel.id)"
@click="toNovelDetail(novel)"
@longpress="enterEditMode">
<novel-item
:book="novel"
@ -209,6 +212,7 @@
if(this.isLogin){
this.mixinsListApi = this.apiMap[this.activeTab]
this.$store.commit('getUserInfo')
}
},
onShow() {
@ -240,10 +244,44 @@
},
//
toNovelDetail(id) {
uni.navigateTo({
url: '/pages_order/novel/readnovels?id=' + id
})
toNovelDetail(novel) {
// novelId
if (novel.novelId) {
uni.navigateTo({
url: `/pages_order/novel/readnovels?id=${novel.shopId}&cid=${novel.novelId}`
});
} else {
//
this.getFirstChapterAndRead(novel.shopId);
}
},
//
getFirstChapterAndRead(bookId) {
this.$fetch('getBookCatalogList', {
bookId: bookId,
pageNo: 1,
pageSize: 1,
reverse: 0, //
}).then(res => {
if (res.records && res.records.length > 0) {
const firstChapter = res.records[0];
uni.navigateTo({
url: `/pages_order/novel/readnovels?id=${bookId}&cid=${firstChapter.id}`
});
} else {
uni.showToast({
title: '暂无章节内容',
icon: 'none'
});
}
}).catch(err => {
console.error('获取章节列表失败:', err);
uni.showToast({
title: '获取章节失败',
icon: 'none'
});
});
},
//


+ 21
- 10
pages/index/center.vue View File

@ -2,6 +2,12 @@
<view>
<view class="page" v-if="isLogin && !showLogoutModal">
<view class="nologin-top-bg"
style="margin-top: calc(var(--status-bar-height) + 20rpx);
position: absolute;z-index: 1;">
</view>
<view class="user">
<view class=""
@ -12,33 +18,35 @@
<view class="user-info">
<image class="avatar" :src="userInfo.headImage" mode="aspectFill"></image>
<view class="info">
<view class="name">{{ userInfo.nickName }} <text class="id"> (ID: {{ userInfo.id }})</text></view>
<view class="desc">世界达人小说控</view>
<view class="name">{{ userInfo.nickName }}
<!-- <text class="id"> (ID: {{ userInfo.id }})</text> -->
</view>
<view class="desc">{{ userInfo.details || '' }}</view>
<view class="phone">手机号{{ userInfo.phone }}</view>
</view>
<view class="more">
<!-- <view class="more">
<uv-icon name="more-dot-fill" size="46rpx" color="#999"></uv-icon>
</view>
</view> -->
</view>
<!-- 图片区域 -->
<view class="section">
<view class="section-title">账户</view>
<view class="section-list">
<view class="section-item" @click="$utils.navigateTo('/pages_order/novel/Walletflow')">
<view class="section-item" @click="$utils.navigateTo('/pages_order/mine/Walletflow')">
<view class="section-item-left">
<uv-icon name="photo" size="40rpx" color="#333"></uv-icon>
<text>钱包流水</text>
</view>
<uv-icon name="arrow-right" size="36rpx" color="#999"></uv-icon>
</view>
<view class="section-item" @click="$utils.navigateTo('/pages_order/novel/Giftbox')">
<!-- <view class="section-item" @click="$utils.navigateTo('/pages_order/novel/Giftbox')">
<view class="section-item-left">
<uv-icon name="gift" size="40rpx" color="#333"></uv-icon>
<text>礼物盒</text>
</view>
<uv-icon name="arrow-right" size="36rpx" color="#999"></uv-icon>
</view>
</view> -->
</view>
</view>
@ -72,7 +80,8 @@
<view class="section-item" @click="$utils.navigateTo('/pages_order/author/creator')">
<view class="section-item-left">
<uv-icon name="star" size="40rpx" color="#333"></uv-icon>
<text>申请成为作者</text>
<text v-if="userInfo.isUser == 'Y'">修改作者信息</text>
<text v-else>申请成为作者</text>
</view>
<uv-icon name="arrow-right" size="36rpx" color="#999"></uv-icon>
</view>
@ -162,12 +171,13 @@
.user {
padding: 20rpx 30rpx;
position: relative;
z-index: 2;
.user-info {
height: 270rpx;
display: flex;
align-items: center;
padding: 30rpx 20rpx;
background: linear-gradient(135deg, #e8e6fa 0%, #fbeff3 100%);
// background: linear-gradient(135deg, #e8e6fa 0%, #fbeff3 100%);
border-radius: 20rpx;
margin-bottom: 20rpx;
@ -271,6 +281,7 @@
height: 580rpx;
background: linear-gradient(180deg, #f3eafe 0%, #f5f5f5 100%);
position: relative;
z-index: 2;
display: flex;
flex-direction: row;
align-items: flex-start;


+ 6
- 1
pages_order/auth/Modifyinformation.vue View File

@ -27,7 +27,7 @@
<view class="form-item">
<view class="form-label"><text class="star">*</text> 个性签名</view>
<view class="form-value">
<input placeholder="请输入个性签名" v-model="userInfoForm.signature" />
<input placeholder="请输入个性签名" v-model="userInfoForm.details" />
</view>
</view>
<view class="divider"></view>
@ -72,6 +72,7 @@
this.userInfoForm.phone = this.userInfo.phone || ''
this.userInfoForm.nickName = this.userInfo.nickName || ''
this.userInfoForm.headImage = this.userInfo.headImage || ''
this.userInfoForm.details = this.userInfo.details || ''
},
methods: {
onChooseAvatar(res) {
@ -112,6 +113,7 @@
headImage: '请选择头像',
nickName: '请填写昵称',
phone: '请填写手机号',
details : '请填写个性签名',
})) {
return
}
@ -120,12 +122,15 @@
avatarUrl: self.userInfoForm.headImage,
nickName: self.userInfoForm.nickName,
phone: self.userInfoForm.phone,
details: self.userInfoForm.details,
}, res => {
if (res.code == 200) {
// uni.reLaunch({
// url: '/pages/index/index'
// })
this.$store.commit('getUserInfo')
uni.showToast({
title: '修改成功',
icon: 'none',


+ 1
- 0
pages_order/auth/wxLogin.vue View File

@ -105,6 +105,7 @@
background-color: #e5eaf9;
min-height: 100vh;
.logo{
margin-top: 120rpx;
height: 140rpx;
width: 140rpx;
image{


+ 14
- 1
pages_order/author/chapterList.vue View File

@ -36,7 +36,12 @@
>
<view class="chapter-info">
<text class="chapter-title">章节名</text>
<text class="chapter-number">{{ chapter.title }}</text>
<text class="chapter-number">
{{ chapter.title }}
<text v-if="chapter.isPay == 'Y'" class="vip-tag theme-transition">付费</text>
</text>
</view>
<uv-icon name="arrow-right" color="#999" size="28"></uv-icon>
</view>
@ -243,6 +248,14 @@
color: #333;
display: block;
}
.vip-tag {
background: #ffe1b2;
color: #ff9900;
border-radius: 20rpx;
font-size: 24rpx;
padding: 2rpx 18rpx;
margin-left: 16rpx;
}
}
.icon-arrow {


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

@ -103,7 +103,7 @@
</view>
<view class="form-item">
<text class="label">总字数</text>
<text class="value">{{ formData.num || '_' }}</text>
<text class="value">{{ formData.bookSum || '_' }}</text>
</view>
</view>


+ 14
- 1
pages_order/author/editor.vue View File

@ -10,6 +10,14 @@
placeholder='请输入章节号与章节名。例如:"第十章天降奇缘"'
v-model="form.title" />
</view>
<view class="form-item"
style="justify-content: space-between;align-items: center;flex-direction: row;">
<view class="">
<text class="required">*</text>
<text class="label">是否付费</text>
</view>
<uv-switch v-model="value" size="70rpx"></uv-switch>
</view>
<view class="form-item">
<text class="required">*</text>
<text class="label">章节内容</text>
@ -31,7 +39,7 @@
</view>
</view>
<view class="footer-btns">
<button class="btn save-btn" @click="onPublish(0)">保存</button>
<button class="btn save-btn" @click="onPublish(0)">保存成草稿</button>
<button class="btn publish-btn" @click="onPublish(1)">发布</button>
</view>
</view>
@ -47,6 +55,7 @@
},
id : 0,
cid : 0,
value : false,
}
},
onLoad({id, cid}) {
@ -63,6 +72,8 @@
}).then(res => {
this.form.title = res.title
this.form.details = res.details
this.value = res.isPay == 'Y'
})
},
async onPublish(status) {
@ -72,6 +83,7 @@
num : this.form.details.length,
title : this.form.title,
details : this.form.details,
isPay : this.value ? 'Y' : 'N'
}
if(this.cid){
@ -145,6 +157,7 @@
.textarea-container {
position: relative;
line-height: 50rpx;
padding-bottom: 200rpx;
/deep/ .uv-textarea{
min-height: 70vh;
border: none !important;


+ 30
- 24
pages_order/comment/myComment.vue View File

@ -3,7 +3,8 @@
<navbar title="我的评论" :leftClick="true" @leftClick="goBack" />
<view class="comment-section">
<view class="section-title">未读评论·{{ unreadComments.length }}</view>
<myCommentItem :item="item" v-for="(item, idx) in list" :key="idx"/>
<myCommentItem :item="item" v-for="(item, idx) in unreadComments" :key="idx"/>
<uv-empty mode="list" v-if="unreadComments.length == 0"></uv-empty>
</view>
<view class="comment-section history-section">
<view class="section-title">历史评论</view>
@ -24,6 +25,8 @@
</view>
</view> -->
<uv-empty mode="list" v-if="list.length == 0"></uv-empty>
<myCommentItem :item="item" v-for="(item, idx) in list" :key="idx"/>
</view>
</view>
@ -40,32 +43,35 @@
data() {
return {
mixinsListApi : 'getMyCommentList',
unreadComments: [
{
avatar: 'https://tse4-mm.cn.bing.net/th/id/OIP-C.iUyxJ_fxLjjX3kEBjteXWwAAAA?rs=1&pid=ImgDetMain',
username: '方香橙',
bookTitle: '重生之财富滚滚',
content: '我是本书的作者方香橙,这是一本甜文爽文哒!请放心入坑,五星好评!女主又美有个性可爱,绝对不圣母,不傻白!男主身心干净深情独宠媳妇儿一个人...',
time: '2024.07.09'
},
{
avatar: 'https://tse4-mm.cn.bing.net/th/id/OIP-C.iUyxJ_fxLjjX3kEBjteXWwAAAA?rs=1&pid=ImgDetMain',
username: '战斗世界',
bookTitle: '重生之财富滚滚',
content: '这本书打破了我看甜文套路之前的观念女主真的太可爱太有趣和以往看过的一个NPC介绍有很大不同',
time: '2024.06.09'
},
{
avatar: 'https://tse4-mm.cn.bing.net/th/id/OIP-C.iUyxJ_fxLjjX3kEBjteXWwAAAA?rs=1&pid=ImgDetMain',
username: '战斗世界',
bookTitle: '重生之财富滚滚',
content: '这本书打破了我看甜文套路之前的观念女主真的太可爱太有趣和以往看过的一个NPC介绍有很大不同',
time: '2024.06.09'
}
],
unreadComments: [],
}
},
onLoad() {
this.queryParams.type = 'Y'
},
onShow() {
this.getList()
},
methods: {
//
getList(){
this.$fetch('getMyCommentList', {
type : 'N',
pageNo: 1,
pageSize: 100000
}).then(res => {
this.unreadComments = res.records
this.unreadComments.forEach(n => {
this.updateCommentRead(n.id)
})
})
},
updateCommentRead(commentId){
this.$fetch('updateCommentRead', {
commentId
})
},
}
}
</script>


+ 5
- 0
pages_order/components/comment/myCommentItem.vue View File

@ -27,6 +27,11 @@
},
methods: {
goToReply(item) {
// this.$fetch('updateCommentRead', {
// commentId : item.id,
// })
uni.navigateTo({
url: '/pages_order/comment/respondComments?id=' + item.id
})


+ 39
- 6
pages_order/components/novel/RankListItem.vue View File

@ -1,14 +1,36 @@
<template>
<view class="rank-item">
<view class="rank-left">
<image src="@/pages_order/static/top/4.png" class="rank-icon" />
<!-- <image src="@/pages_order/static/top/4.png" class="rank-icon" /> -->
<view class="rank-icon">
{{ index }}
</view>
<image src="@/pages_order/static/book/dj.png" class="rank-num-img" />
<image class="avatar" src="@/pages_order/static/book/bd.png" mode="aspectFill" />
<view class="name">发源于</view>
<image class="avatar" :src="item.hanHaiMember
&& item.hanHaiMember.headImage" mode="aspectFill" />
<view class="name">{{
item.hanHaiMember
&& item.hanHaiMember.nickName
}}</view>
</view>
<view class="rank-right">
<view class="score">200亲密值</view>
<view class="level">护书使者 五级</view>
<view class="score">
{{
item.hanHaiMember
&& item.num
}}
亲密值</view>
<view class="level">
{{
item.commonBookAchievement
&& (item.commonBookAchievement.oldName || item.commonBookAchievement.title)
}}
</view>
</view>
</view>
</template>
@ -17,7 +39,12 @@
export default {
name: 'RankListItem',
props: {
}
index : {
},
item : {
default : {}
}
},
}
</script>
@ -41,6 +68,12 @@
width: 60rpx;
height: 60rpx;
margin-right: 10rpx;
display: flex;
justify-content: center;
align-items: center;
font-size: 34rpx;
font-weight: 900;
color: #684427;
}
.avatar {


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

@ -15,7 +15,7 @@
:class="['catalog-item theme-transition', {active: idx == currentIndex}]">
<view class="item-main">
<text class="item-title theme-transition">{{ item.title }}</text>
<text v-if="item.vip == 'Y'" class="vip-tag theme-transition">付费</text>
<text v-if="item.isPay == 'Y'" class="vip-tag theme-transition">付费</text>
</view>
</view>
<uv-empty mode="list" v-if="chapterList.length == 0"></uv-empty>


+ 408
- 0
pages_order/components/novel/interactiveGiftPopup.vue View File

@ -0,0 +1,408 @@
<template>
<uv-popup ref="popup" mode="bottom" :round="20" :safeAreaInsetBottom="true" @close="handleClose">
<view class="interactive-gift-popup">
<!-- 标题栏 -->
<view class="popup-header">
<text class="popup-title">互动打赏</text>
<uv-icon name="close" size="40rpx" color="#999" @click="close"></uv-icon>
</view>
<!-- 当前选中礼物信息 -->
<view class="selected-gift-info" v-if="selectedGift">
<view class="selected-gift-left">
<view class="selected-gift-img">
<image :src="selectedGift.image" mode="aspectFill"></image>
</view>
<view class="selected-gift-details">
<text class="selected-gift-name">{{ selectedGift.title }}</text>
<text class="selected-gift-price">{{ selectedGift.integerPrice }}豆豆</text>
</view>
</view>
<view class="selected-gift-right">
<uv-number-box
v-model="giftCount"
:min="1"
:max="99"
size="small"
bgColor="#f5f5f5"
color="#223a7a"
/>
</view>
</view>
<!-- 用户余额信息 -->
<view class="balance-info">
<view class="balance-text">
账户余额<text class="bean-amount">{{ userInfo.integerPrice }} 豆豆</text>
</view>
<view v-if="selectedGift && totalPrice > userInfo.integerPrice" class="insufficient-tip">
余额不足请先充值
</view>
</view>
<view class="gift-grid">
<view
v-for="(gift, idx) in giftList"
:key="gift.id"
:class="['gift-item', { selected: selectedIndex === idx }]"
@click="selectGift(idx)"
>
<view class="gift-img">
<image :src="gift.image" mode="aspectFill"></image>
</view>
<view class="gift-name">{{ gift.title }}</view>
<view class="gift-price">{{ gift.integerPrice }}豆豆</view>
</view>
</view>
<!-- 底部操作栏 -->
<view class="popup-bottom">
<view class="total-info">
<text class="total-text">总计</text>
<text class="total-price">{{ totalPrice }}豆豆</text>
</view>
<button class="gift-btn" @click="sendGift" :disabled="!selectedGift || loading">
{{ loading ? '赠送中...' : '赠送' }}
</button>
</view>
</view>
</uv-popup>
</template>
<script>
export default {
name: 'InteractiveGiftPopup',
props: {
bookId: {
type: [String, Number],
required: true
}
},
data() {
return {
giftList: [],
selectedIndex: 0,
selectedGift: null,
giftCount: 1,
loading: false,
}
},
computed: {
totalPrice() {
if (!this.selectedGift) return 0;
return this.selectedGift.integerPrice * this.giftCount;
}
},
methods: {
//
open() {
this.$refs.popup.open();
this.getGiftList();
this.getUserBalance();
},
//
close() {
this.$refs.popup.close();
},
//
handleClose() {
this.resetData();
},
//
resetData() {
this.selectedIndex = 0;
this.selectedGift = null;
this.giftCount = 1;
this.loading = false;
},
//
getGiftList() {
this.$fetch('getInteractionGiftList').then(res => {
this.giftList = res.records || [];
if (this.giftList.length > 0) {
this.selectGift(0);
}
}).catch(err => {
console.error('获取礼物列表失败:', err);
uni.showToast({
title: '获取礼物列表失败',
icon: 'none'
});
});
},
//
selectGift(index) {
this.selectedIndex = index;
this.selectedGift = this.giftList[index];
this.giftCount = 1;
},
//
getUserBalance() {
// store
this.$store.commit('getUserInfo');
},
//
sendGift() {
if (!this.selectedGift) {
uni.showToast({
title: '请选择礼物',
icon: 'none'
});
return;
}
//
if (this.totalPrice > this.userInfo.integerPrice) {
uni.showToast({
title: '余额不足,请先充值',
icon: 'none'
});
setTimeout(() => {
uni.navigateTo({
url: '/pages_order/mine/recharge'
})
}, 600)
return;
}
this.loading = true;
//
this.$fetch('giveGift', {
giftId: this.selectedGift.id,
num: this.giftCount,
bookId: this.bookId,
}).then(res => {
//
uni.showToast({
title: '赠送成功',
icon: 'success'
});
//
this.$emit('giftSent', {
gift: this.selectedGift,
count: this.giftCount,
totalPrice: this.totalPrice
});
this.getUserBalance()
this.close();
}).catch(err => {
console.error('赠送失败:', err);
uni.showToast({
title: err.message || '赠送失败',
icon: 'none'
});
}).finally(() => {
this.loading = false;
});
}
}
}
</script>
<style lang="scss" scoped>
.interactive-gift-popup {
background: #fff;
border-radius: 20rpx 20rpx 0 0;
max-height: 80vh;
display: flex;
flex-direction: column;
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 32rpx 32rpx 24rpx 32rpx;
border-bottom: 1rpx solid #f5f5f5;
.popup-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
}
.selected-gift-info {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx 32rpx;
background: #f8f9ff;
margin: 0 32rpx 24rpx 32rpx;
border-radius: 16rpx;
.selected-gift-left {
display: flex;
align-items: center;
gap: 16rpx;
.selected-gift-img {
width: 60rpx;
height: 60rpx;
border-radius: 8rpx;
overflow: hidden;
image {
width: 100%;
height: 100%;
}
}
.selected-gift-details {
display: flex;
flex-direction: column;
gap: 4rpx;
.selected-gift-name {
font-size: 28rpx;
color: #333;
font-weight: 500;
}
.selected-gift-price {
font-size: 24rpx;
color: #223a7a;
}
}
}
}
.balance-info {
padding: 0 32rpx 24rpx 32rpx;
.balance-text {
font-size: 28rpx;
color: #333;
margin-bottom: 8rpx;
.bean-amount {
color: #223a7a;
font-weight: 600;
}
}
.insufficient-tip {
font-size: 24rpx;
color: #e94f7a;
background: rgba(233, 79, 122, 0.1);
padding: 8rpx 16rpx;
border-radius: 8rpx;
text-align: center;
}
}
.gift-grid {
flex: 1;
display: flex;
flex-wrap: wrap;
gap: 20rpx;
padding: 0 32rpx;
max-height: 400rpx;
overflow-y: auto;
}
.gift-item {
width: calc(25% - 18rpx);
background: #fff;
border-radius: 12rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
display: flex;
flex-direction: column;
align-items: center;
padding: 20rpx 4rpx;
border: 2rpx solid transparent;
transition: all 0.2s;
box-sizing: border-box;
&.selected {
border-color: #223a7a;
background: #f8f9ff;
}
.gift-img {
width: 70rpx;
height: 70rpx;
border-radius: 8rpx;
overflow: hidden;
margin-bottom: 8rpx;
image {
width: 100%;
height: 100%;
}
}
.gift-name {
font-size: 22rpx;
color: #333;
text-align: center;
margin-bottom: 4rpx;
line-height: 1.2;
}
.gift-price {
font-size: 20rpx;
color: #223a7a;
text-align: center;
}
}
.popup-bottom {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx 32rpx;
border-top: 1rpx solid #f5f5f5;
background: #fff;
width: 100%;
box-sizing: border-box;
.total-info {
display: flex;
align-items: center;
gap: 8rpx;
.total-text {
font-size: 28rpx;
color: #666;
}
.total-price {
font-size: 32rpx;
color: #223a7a;
font-weight: bold;
}
}
.gift-btn {
background: #223a7a;
color: #fff;
font-size: 28rpx;
border-radius: 32rpx;
padding: 0 48rpx;
height: 68rpx;
line-height: 68rpx;
border: none;
min-width: 140rpx;
margin-left: auto;
margin-right: 20rpx;
&[disabled] {
background: #ccc;
color: #999;
}
}
}
</style>

+ 218
- 0
pages_order/components/novel/signRecordPopup.vue View File

@ -0,0 +1,218 @@
<template>
<view class="sign-record-popup" :class="{'dark-mode': isDarkMode}">
<uv-popup ref="popup" mode="center" :closeOnClickOverlay="true" :customStyle="popupStyle">
<view class="content theme-transition">
<view class="popup-header">
<view class="popup-title theme-transition">签到记录</view>
<view class="close-btn" @click="close">
<uv-icon name="close" :color="isDarkMode ? '#ccc' : '#666'" size="32rpx"></uv-icon>
</view>
</view>
<view class="record-list">
<scroll-view scroll-y class="record-scroll">
<view class="record-item" v-for="(record, index) in recordList" :key="index">
<view class="record-left">
<view class="record-date">{{ formatDate(record.createTime) }}</view>
<view class="record-task">{{ record.taskName || '每日签到' }}</view>
</view>
<view class="record-right">
<text class="record-reward">+{{ record.num || 1 }}推荐票</text>
<text class="record-status success">已签到</text>
</view>
</view>
<view class="no-record" v-if="recordList.length === 0 && !loading">暂无签到记录</view>
<view class="loading" v-if="loading">加载中...</view>
</scroll-view>
</view>
</view>
</uv-popup>
</view>
</template>
<script>
import themeMixin from '@/mixins/themeMode.js'
export default {
name: 'signRecordPopup',
mixins: [themeMixin],
data() {
return {
recordList: [],
loading: false,
pageNo: 1,
pageSize: 20
}
},
computed: {
popupStyle() {
return {
'background': this.isDarkMode ? '#232323' : '#fff',
'border-radius': '24rpx',
'width': '600rpx',
'max-height': '700rpx'
}
}
},
methods: {
//
open() {
this.$refs.popup.open();
this.getRecordList();
},
//
close() {
this.$refs.popup.close();
},
//
getRecordList() {
this.loading = true;
this.$fetch('getSignTaskRecordPage', {
pageNo: this.pageNo,
pageSize: this.pageSize
}).then(res => {
this.recordList = res.records || [];
this.loading = false;
}).catch(err => {
console.error('获取签到记录失败:', err);
this.loading = false;
});
},
//
formatDate(dateString) {
if (!dateString) return '';
const date = new Date(dateString);
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
return `${month}-${day}`;
}
}
}
</script>
<style lang="scss" scoped>
.sign-record-popup {
.content {
padding: 0;
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 32rpx 32rpx 24rpx;
border-bottom: 1rpx solid #f5f5f5;
.popup-title {
font-size: 32rpx;
font-weight: bold;
color: #222;
}
.close-btn {
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
}
}
.record-list {
.record-scroll {
max-height: 560rpx;
.record-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 32rpx;
border-bottom: 1rpx solid #f8f8f8;
.record-left {
display: flex;
flex-direction: column;
align-items: flex-start;
.record-date {
font-size: 28rpx;
color: #333;
font-weight: 500;
margin-bottom: 8rpx;
}
.record-task {
font-size: 24rpx;
color: #999;
}
}
.record-right {
display: flex;
flex-direction: column;
align-items: flex-end;
.record-reward {
font-size: 26rpx;
color: #ff6b35;
margin-bottom: 4rpx;
font-weight: 500;
}
.record-status {
font-size: 22rpx;
&.success {
color: #4caf50;
}
}
}
}
.no-record, .loading {
text-align: center;
padding: 80rpx 32rpx;
color: #999;
font-size: 26rpx;
}
}
}
}
&.dark-mode {
.content {
.popup-header {
border-bottom-color: #333;
.popup-title {
color: #eee;
}
}
.record-list {
.record-scroll {
.record-item {
border-bottom-color: #333;
.record-left {
.record-date {
color: #ccc;
}
.record-task {
color: #888;
}
}
}
.no-record, .loading {
color: #666;
}
}
}
}
}
}
</style>

+ 8
- 18
pages_order/components/novel/subscriptionPopup.vue View File

@ -1,13 +1,16 @@
<template>
<view class="subscription-popup" :class="{'dark-mode': isDarkMode}">
<uv-popup ref="popup" mode="center" :closeOnClickOverlay="true" :customStyle="popupStyle">
<uv-popup ref="popup"
mode="bottom"
:closeOnClickOverlay="false"
@maskClick="$emit('maskClick')"
:customStyle="popupStyle">
<view class="content theme-transition">
<view class="popup-title theme-transition">{{ title }}</view>
<view class="popup-desc theme-transition">{{ description }}</view>
<view class="popup-btns">
<button class="popup-btn theme-transition" @click="handleSubscribe">订阅本章</button>
<button class="popup-btn popup-btn-video theme-transition">观看视频解锁</button>
<button class="popup-btn popup-btn-batch theme-transition">批量订阅</button>
<button class="popup-btn theme-transition">观看视频解锁</button>
<button class="popup-btn theme-transition">批量订阅</button>
</view>
</view>
</uv-popup>
@ -25,14 +28,9 @@
type: String,
default: '这是付费章节 需要订阅后才能阅读'
},
description: {
type: String,
default: '订阅后可继续阅读本章内容'
}
},
data() {
return {
popupShown: false, //
}
},
computed: {
@ -49,10 +47,7 @@
methods: {
//
open() {
if (!this.popupShown) {
this.$refs.popup.open();
this.popupShown = true;
}
this.$refs.popup.open();
},
//
@ -60,11 +55,6 @@
this.$refs.popup.close();
},
//
reset() {
this.popupShown = false;
},
//
handleSubscribe() {
this.$emit('subscribe');


+ 364
- 0
pages_order/mine/Walletflow.vue View File

@ -0,0 +1,364 @@
<!-- 钱包流水页面 -->
<template>
<view class="walletflow-page">
<!-- 顶部导航栏 -->
<navbar title="钱包流水" leftClick @leftClick="$utils.navigateBack" />
<!-- 账户余额卡片 -->
<view class="balance-card">
<view class="balance-label">账户</view>
<view class="balance-row">
<text class="balance-amount">{{ accountBalance }}</text>
<button class="recharge-btn" @click="goRecharge">充值</button>
</view>
</view>
<!-- tab和流水列表卡片 -->
<view class="flow-card">
<uv-tabs
:list="tabList"
:current="activeTab"
@change="switchTab"
:scrollable="false"
activeColor="#223a7a"
inactiveColor="#888"
lineColor="#223a7a"
lineWidth="44rpx"
lineHeight="4rpx"
:itemStyle="{
height: '88rpx',
fontSize: '30rpx',
fontWeight: 'bold'
}"
></uv-tabs>
<scroll-view scroll-y class="flow-list" @scrolltolower="loadMore">
<view v-if="activeTab === 0">
<view class="flow-item" v-for="(item, idx) in rechargeList" :key="idx">
<view class="flow-item-row">
<view class="flow-item-left">
<view class="flow-title">{{ item.title || item.type || '充值' }}</view>
<view class="flow-date">{{ formatDate(item.createTime) }}</view>
</view>
<view class="flow-amount plus">+{{ item.num }}</view>
</view>
</view>
<view v-if="rechargeList.length === 0 && !loading" class="empty-tip">暂无充值记录</view>
</view>
<view v-else>
<view class="flow-item" v-for="(item, idx) in payList" :key="idx">
<view class="flow-item-row">
<view class="flow-item-left">
<view class="flow-title">{{ item.title || item.type || '支付' }}</view>
<view class="flow-date">{{ formatDate(item.createTime) }}</view>
</view>
<view class="flow-amount minus">-{{ item.num }}</view>
</view>
</view>
<view v-if="payList.length === 0 && !loading" class="empty-tip">暂无支付记录</view>
</view>
<view v-if="loading" class="loading-tip">加载中...</view>
<view v-if="noMore && (rechargeList.length > 0 || payList.length > 0)" class="no-more-tip">没有更多了</view>
</scroll-view>
</view>
</view>
</template>
<script>
import mixinsList from '@/mixins/list.js'
export default {
mixins: [mixinsList],
components: {
},
data() {
return {
accountBalance: 0,
activeTab: 0,
rechargeList: [],
payList: [],
loading: false,
noMore: false,
pageNo: 1,
pageSize: 20,
tabList: [
{
name: '充值',
value: 0
},
{
name: '支付',
value: 1
}
]
}
},
onLoad() {
this.getAccountBalance();
this.getFlowList();
},
methods: {
//
getAccountBalance() {
this.$fetch('getMyMoneyNum').then(res => {
this.accountBalance = res || 0;
}).catch(err => {
console.error('获取账户余额失败:', err);
this.accountBalance = 0;
});
},
// tab
switchTab(tab) {
// uv-tabsindex
const tabIndex = typeof tab === 'object' ? tab.index : tab;
if (this.activeTab === tabIndex) return;
this.activeTab = tabIndex;
this.pageNo = 1;
this.noMore = false;
//
if (tabIndex === 0) {
this.rechargeList = [];
} else {
this.payList = [];
}
this.getFlowList();
},
//
getFlowList() {
if (this.loading || this.noMore) return;
this.loading = true;
// tab0-1-
const logType = this.activeTab === 0 ? 0 : 1;
this.$fetch('getMyMoneyLogPage', {
pageNo: this.pageNo,
pageSize: this.pageSize,
status: logType // 0-1-
}).then(res => {
const records = res.records || [];
if (records.length === 0) {
this.noMore = true;
} else {
if (this.activeTab === 0) {
//
if (this.pageNo === 1) {
this.rechargeList = records;
} else {
this.rechargeList = [...this.rechargeList, ...records];
}
} else {
//
if (this.pageNo === 1) {
this.payList = records;
} else {
this.payList = [...this.payList, ...records];
}
}
// pageSize
if (records.length < this.pageSize) {
this.noMore = true;
}
}
this.loading = false;
}).catch(err => {
console.error('获取流水列表失败:', err);
this.loading = false;
uni.showToast({
title: '获取流水失败',
icon: 'none'
});
});
},
//
loadMore() {
if (this.loading || this.noMore) return;
this.pageNo++;
this.getFlowList();
},
//
formatDate(dateString) {
if (!dateString) return '';
const date = new Date(dateString);
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
return `${year}.${month}.${day}`;
},
goRecharge() {
uni.navigateTo({
url: '/pages_order/mine/recharge'
})
}
}
}
</script>
<style lang="scss" scoped>
.walletflow-page {
min-height: 100vh;
background: linear-gradient(180deg, #f8f8fc 0%, #fff 100%);
padding-bottom: 30rpx;
}
.balance-card {
background: linear-gradient(90deg, #f7f2fa 0%, #fbeaf2 100%);
border-radius: 18rpx;
margin: 24rpx 12rpx 0 12rpx;
padding: 18rpx 24rpx 14rpx 24rpx;
box-shadow: none;
border: 1rpx solid #ede7ef;
position: relative;
display: flex;
flex-direction: column;
min-height: 130rpx;
justify-content: center;
.balance-label {
color: #bbb;
font-size: 26rpx;
margin-bottom: 8rpx;
}
.balance-row {
display: flex;
align-items: center;
margin-top: 0;
position: relative;
.balance-amount {
color: #e94f7a;
font-size: 48rpx;
font-weight: bold;
}
.recharge-btn {
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
background: linear-gradient(90deg, #ffb6c1 0%, #fa5a99 100%);
color: #fff;
font-size: 28rpx;
border-radius: 32rpx;
padding: 0 40rpx;
height: 56rpx;
line-height: 56rpx;
font-weight: 500;
border: none;
box-shadow: none;
display: flex;
align-items: center;
justify-content: center;
}
}
}
.flow-card {
background: #fff;
border-radius: 20rpx;
margin: 32rpx 16rpx 0 16rpx;
box-shadow: 0 4rpx 24rpx 0 rgba(0, 0, 0, 0.06);
padding-bottom: 8rpx;
overflow: hidden;
// uv-tabs
:deep(.uv-tabs) {
background: #fff;
border-top-left-radius: 20rpx;
border-top-right-radius: 20rpx;
}
:deep(.uv-tabs__wrapper__nav__line) {
border-radius: 2rpx !important;
}
}
.flow-list {
margin: 0;
padding: 0 16rpx;
max-height: calc(75vh - 88rpx);
background: #fff;
}
.flow-item {
border-bottom: 1px solid #f5f5f5;
padding: 18rpx 0 8rpx 0;
&:last-child {
border-bottom: none;
}
.flow-item-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding-right: 45rpx;
padding-left: 15rpx;
}
.flow-item-left {
display: flex;
flex-direction: column;
align-items: flex-start;
.flow-title {
font-size: 28rpx;
color: #222;
font-weight: 500;
margin-bottom: 2rpx;
}
.flow-date {
color: #bbb;
font-size: 22rpx;
margin-top: 0;
}
}
.flow-amount {
font-size: 26rpx;
font-weight: 500;
margin-left: 24rpx;
margin-top: 2rpx;
&.plus {
color: #223a7a;
}
&.minus {
color: #e94f7a;
}
}
}
//
.empty-tip,
.loading-tip,
.no-more-tip {
text-align: center;
padding: 40rpx 0;
color: #999;
font-size: 26rpx;
}
.loading-tip {
color: #666;
}
</style>

+ 530
- 0
pages_order/mine/recharge.vue View File

@ -0,0 +1,530 @@
<template>
<!-- 豆豆充值页面 -->
<view class="recharge-page">
<!-- 顶部导航栏 -->
<navbar title="豆豆充值" leftClick @leftClick="$utils.navigateBack" />
<!-- 当前余额卡片 -->
<view class="card balance-card">
<view class="card-title">当前余额</view>
<view class="current-balance">
<text class="balance-amount">{{ userInfo.integerPrice || 0 }}</text>
<text class="balance-unit">豆豆</text>
</view>
</view>
<!-- 充值套餐选择 -->
<view class="card package-card">
<view class="card-title">选择充值套餐</view>
<view class="package-grid">
<view
v-for="(item, index) in rechargePackages"
:key="index"
:class="['package-item', { selected: selectedPackage === index }]"
@click="selectPackage(index)"
>
<view class="package-beans">{{ item.num }}豆豆</view>
<view class="package-price">¥{{ item.money }}</view>
<view v-if="item.giveNum" class="package-bonus">{{ item.giveNum }}豆豆</view>
</view>
</view>
</view>
<!-- 自定义充值 -->
<!-- <view class="card custom-card">
<view class="card-title">自定义充值</view>
<view class="form-row">
<view class="form-label required">
<text class="star">*</text> 充值金额
</view>
<input
class="form-input"
placeholder="请输入充值金额"
v-model="customAmount"
type="number"
:placeholder-style="'color:#bbb;'"
:style="customAmount ? 'color:#222;' : ''"
@input="onCustomAmountChange"
/>
</view>
<view class="divider"></view>
<view class="form-row">
<view class="form-label">可获得豆豆</view>
<view class="form-value">{{ customBeans }}豆豆</view>
</view>
</view> -->
<!-- 订单信息卡片 -->
<view class="card order-card" v-if="totalPrice > 0">
<view class="order-title">订单信息</view>
<view class="order-item">
<text class="order-label">充值金额</text>
<text class="order-value">¥{{ totalPrice.toFixed(2) }}</text>
</view>
<view class="order-item">
<text class="order-label">获得豆豆</text>
<text class="order-value">{{ totalBeans }}豆豆</text>
</view>
<view class="order-divider"></view>
<view class="order-item total-row">
<view class="order-total-label">合计</view>
<view class="order-total">
<text class="order-total-highlight">¥{{ totalPrice.toFixed(2) }}</text>
</view>
</view>
</view>
<!-- 支付方式 -->
<!-- <view class="card payment-card">
<view class="card-title">支付方式</view>
<view class="payment-methods">
<view
v-for="(method, index) in paymentMethods"
:key="index"
:class="['payment-item', { selected: selectedPayment === index }]"
@click="selectPayment(index)"
>
<view class="payment-icon">
<uv-icon :name="method.icon" size="40rpx" color="#223a7a" />
</view>
<text class="payment-name">{{ method.name }}</text>
<view class="payment-radio">
<view v-if="selectedPayment === index" class="radio-checked"></view>
</view>
</view>
</view>
</view> -->
<!-- 提示信息 -->
<view class="tip-text">
请仔细核查并确认相关信息因用户个人疏忽导致的充值错误需由用户自行承担一旦完成充值概不退换
</view>
<!-- 底部充值按钮 -->
<view class="footer-bar">
<button class="recharge-btn" @click="handleRecharge" :disabled="totalPrice <= 0">
立即充值 ¥{{ totalPrice.toFixed(2) }}
</button>
</view>
</view>
</template>
<script>
export default {
data() {
return {
selectedPackage: null,
customAmount: '',
selectedPayment: 0,
rechargePackages: [
// { beans: 100, price: 10, bonus: 0 },
// { beans: 300, price: 30, bonus: 20 },
// { beans: 680, price: 68, bonus: 50 },
// { beans: 1280, price: 128, bonus: 120 },
// { beans: 3280, price: 328, bonus: 320 },
// { beans: 6480, price: 648, bonus: 680 }
],
paymentMethods: [
{ name: '微信支付', icon: 'weixin-fill' },
]
}
},
computed: {
customBeans() {
const amount = parseFloat(this.customAmount)
return isNaN(amount) ? 0 : Math.floor(amount * 10)
},
totalPrice() {
if (this.selectedPackage !== null) {
return this.rechargePackages[this.selectedPackage].money
} else if (this.customAmount) {
return parseFloat(this.customAmount) || 0
}
return 0
},
totalBeans() {
if (this.selectedPackage !== null) {
const pkg = this.rechargePackages[this.selectedPackage]
return pkg.num + (pkg.giveNum || 0)
} else if (this.customAmount) {
return this.customBeans
}
return 0
}
},
onLoad(query) {
//
if (query.amount) {
this.customAmount = query.amount
}
},
onShow() {
this.$store.commit('getUserInfo')
this.getPayPackageList()
},
methods: {
getPayPackageList(){
this.$api('getPayPackageList')
.then(res => {
if(res.code == 200){
this.rechargePackages = res.result
}
})
},
//
selectPackage(index) {
this.selectedPackage = index
this.customAmount = ''
},
//
selectPayment(index) {
this.selectedPayment = index
},
//
onCustomAmountChange() {
if (this.customAmount) {
this.selectedPackage = null
}
},
//
async handleRecharge() {
if (this.totalPrice <= 0) {
uni.showToast({
title: '请选择充值套餐或输入充值金额',
icon: 'none'
})
return
}
if (this.selectedPayment === null) {
uni.showToast({
title: '请选择支付方式',
icon: 'none'
})
return
}
try {
//
const result = await this.$fetch('createPayPackageOrder', {
// amount: this.totalPrice,
// beans: this.totalBeans,
// paymentMethod: this.paymentMethods[this.selectedPayment].name
packageId : this.rechargePackages[this.selectedPackage].id
})
await uni.requestPaymentWxPay({result})
uni.showToast({
title: `充值成功,获得${this.totalBeans}豆豆`,
icon: 'success'
})
//
this.$store.commit('getUserInfo')
setTimeout(() => {
uni.navigateBack()
}, 1500)
} catch (error) {
uni.showToast({
title: '充值失败,请重试',
icon: 'none'
})
}
}
}
}
</script>
<style scoped lang="scss">
.recharge-page {
min-height: 100vh;
background: #f8f8f8;
padding-bottom: 120rpx;
}
.card {
background: #fff;
border-radius: 20rpx;
margin: 24rpx 16rpx 0 16rpx;
padding: 24rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.03);
}
.card-title {
font-size: 32rpx;
font-weight: bold;
color: #222;
margin-bottom: 24rpx;
}
//
.balance-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
text-align: center;
.card-title {
color: #fff;
opacity: 0.9;
}
.current-balance {
display: flex;
align-items: baseline;
justify-content: center;
gap: 8rpx;
margin-top: 16rpx;
.balance-amount {
font-size: 56rpx;
font-weight: bold;
}
.balance-unit {
font-size: 28rpx;
opacity: 0.8;
}
}
}
//
.package-grid {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.package-item {
width: calc(50% - 8rpx);
background: #f8f9ff;
border: 2rpx solid #f0f0f0;
border-radius: 16rpx;
padding: 24rpx 16rpx;
text-align: center;
transition: all 0.2s;
position: relative;
box-sizing: border-box;
&.selected {
border-color: #223a7a;
background: rgba(34, 58, 122, 0.05);
}
.package-beans {
font-size: 32rpx;
font-weight: bold;
color: #222;
margin-bottom: 8rpx;
}
.package-price {
font-size: 28rpx;
color: #223a7a;
font-weight: 500;
margin-bottom: 4rpx;
}
.package-bonus {
font-size: 20rpx;
color: #e94f7a;
background: rgba(233, 79, 122, 0.1);
padding: 2rpx 8rpx;
border-radius: 8rpx;
display: inline-block;
}
}
//
.form-row {
display: flex;
flex-direction: column;
align-items: flex-start;
margin-bottom: 24rpx;
}
.form-label {
color: #888;
font-size: 28rpx;
margin-bottom: 12rpx;
font-weight: 400;
&.required {
color: #222;
font-weight: 500;
display: flex;
align-items: center;
}
}
.star {
color: #e94f7a;
margin-right: 4rpx;
font-size: 28rpx;
}
.form-value {
color: #222;
font-size: 28rpx;
font-weight: 600;
}
.form-input {
width: 100%;
border: 1rpx solid #e5e5e5;
border-radius: 12rpx;
font-size: 28rpx;
padding: 16rpx;
background: #fafafa;
color: #222;
box-sizing: border-box;
}
.divider {
height: 1rpx;
background: #f2f2f2;
margin: 16rpx 0;
width: 100%;
}
//
.order-card {
.order-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
.order-label {
font-size: 28rpx;
color: #666;
}
.order-value {
font-size: 28rpx;
color: #222;
font-weight: 500;
}
&.total-row {
margin-bottom: 0;
margin-top: 16rpx;
.order-total-label {
font-size: 32rpx;
color: #222;
font-weight: 600;
}
.order-total-highlight {
font-size: 32rpx;
color: #223a7a;
font-weight: bold;
}
}
}
.order-divider {
height: 1rpx;
background: #f2f2f2;
margin: 16rpx 0;
}
}
//
.payment-methods {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.payment-item {
display: flex;
align-items: center;
padding: 20rpx;
border: 2rpx solid #f0f0f0;
border-radius: 12rpx;
transition: all 0.2s;
&.selected {
border-color: #223a7a;
background: rgba(34, 58, 122, 0.05);
}
.payment-icon {
width: 48rpx;
height: 48rpx;
margin-right: 16rpx;
image {
width: 100%;
height: 100%;
}
}
.payment-name {
flex: 1;
font-size: 28rpx;
color: #222;
}
.payment-radio {
width: 32rpx;
height: 32rpx;
border: 2rpx solid #ddd;
border-radius: 50%;
position: relative;
.radio-checked {
width: 16rpx;
height: 16rpx;
background: #223a7a;
border-radius: 50%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
}
.tip-text {
color: #bbb;
font-size: 22rpx;
margin: 32rpx 32rpx 0 32rpx;
line-height: 1.6;
}
.footer-bar {
// position: fixed;
// left: 0;
// right: 0;
// bottom: 90rpx;
background: #fff;
padding: 24rpx 32rpx 32rpx 32rpx;
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
z-index: 10;
}
.recharge-btn {
width: 100%;
background: #223a7a;
color: #fff;
font-size: 32rpx;
border-radius: 32rpx;
height: 88rpx;
line-height: 88rpx;
border: none;
font-weight: 500;
&[disabled] {
background: #ccc;
color: #999;
}
}
</style>

+ 61
- 32
pages_order/novel/ReaderAchievement.vue View File

@ -13,15 +13,25 @@
<image class="badge-img"
:src="item.image"
mode="aspectFill" />
<view class="input-area">
<view class="label-row">
<text class="required">*</text>
<text class="label">{{ item.title }}</text>
<view class="">
<view class="input-area">
<view class="label-row">
<text class="required">*</text>
<text class="label">{{ item.title }}</text>
</view>
<input class="input"
v-model="form[keys[index] + 'Name']"
placeholder="请输入"
placeholder-class="input-placeholder" />
</view>
<view class="input-area" v-if="form[keys[index] + 'Num']">
<view class="label-row">
<text class="label">达成人数</text>
</view>
<view class="Num">
{{ form[keys[index] + 'Num'] || 0 }}
</view>
</view>
<input class="input"
v-model="item.levelName"
placeholder="请输入"
placeholder-class="input-placeholder" />
</view>
</view>
<!-- <view class="divider"></view>
@ -67,39 +77,55 @@
mixins: [mixinsList],
data() {
return {
level1: '',
level2: '',
level3: '',
mixinsListApi : 'getAchievementList',
isPending: false
isPending: false,
keys : ['one', 'two', 'three'],
form : {},
}
},
onLoad() {
this.getDetail()
},
methods: {
async getDetail(){
this.form = await this.$fetch('getAchievement')
this.isPending = this.form.status == 0
},
async submit() {
for (var index = 0; index < this.list.length; index++) {
var element = this.list[index];
if (!element.levelName) {
uni.showToast({
title: '请填写所有成就名称',
icon: 'none'
})
return
}
}
// for (var index = 0; index < this.list.length; index++) {
// var element = this.list[index];
// if (!element.levelName) {
// uni.showToast({
// title: '',
// icon: 'none'
// })
// return
// }
// }
// this.isPending = true
let arr = []
// let arr = []
for (var index = 0; index < this.list.length; index++) {
var element = this.list[index];
// for (var index = 0; index < this.list.length; index++) {
// var element = this.list[index];
arr.push(this.$fetch('setAchievementName', {
name : element.levelName
}))
// arr.push(this.$fetch('setAchievementName', {
// name : element.levelName
// }))
// }
// await Promise.all(arr)
if(this.$utils.verificationAll(this.form, {
oneName : '请填写所有成就名称',
twoName : '请填写所有成就名称',
threeName : '请填写所有成就名称',
})){
return
}
await Promise.all(arr)
await this.$fetch('setAchievementName', this.form)
uni.showToast({
title: '提交成功',
@ -185,10 +211,13 @@
}
.label {
font-size: 28rpx;
color: #222;
font-size: 24rpx;
color: #999;
font-weight: bold;
}
.Num{
}
.input {
width: 100%;
@ -196,7 +225,7 @@
border-bottom: 1.5rpx solid #ececec;
font-size: 28rpx;
background: transparent;
padding: 8rpx 0 6rpx 0;
padding: 16rpx 0;
margin-bottom: 2rpx;
}


+ 51
- 12
pages_order/novel/Tipping.vue View File

@ -8,40 +8,58 @@
<view class="top-item second" v-if="topList[1]">
<view class="avatar-frame">
<image class="frame-img" src="/pages_order/static/top/top1.png" mode="aspectFit"></image>
<image class="avatar" :src="topList[1].avatar" mode="aspectFill"></image>
<image class="avatar" :src="topList[1].hanHaiMember
&& topList[1].hanHaiMember.headImage" mode="aspectFill"></image>
</view>
<view class="badge">
<image class="badge-img" src="/pages_order/static/book/dj.png" mode="aspectFit"></image>
</view>
<view class="name">{{ topList[1].name }}</view>
<view class="name">{{
topList[1].hanHaiMember
&& topList[1].hanHaiMember.nickName
}}</view>
<view class="score">{{ topList[1].num }} 亲密值</view>
<view class="level">护书使者 四级</view>
<view class="level">{{
topList[1].commonBookAchievement
&& (topList[1].commonBookAchievement.oldName || topList[1].commonBookAchievement.title)
}}</view>
</view>
<view class="top-item first" v-if="topList[0]">
<view class="avatar-frame">
<image class="frame-img" src="/pages_order/static/top/top1.png" mode="aspectFit"></image>
<image class="avatar" :src="topList[0].avatar" mode="aspectFill"></image>
<image class="avatar" :src="topList[0].hanHaiMember
&& topList[0].hanHaiMember.headImage" mode="aspectFill"></image>
</view>
<view class="badge">
<image class="badge-img" :src="topList[0].icon" mode="aspectFit"></image>
</view>
<view class="name">{{ topList[0].name }}</view>
<view class="name">{{
topList[0].hanHaiMember
&& topList[0].hanHaiMember.nickName
}}</view>
<view class="score">{{ topList[0].num }} 亲密值</view>
<view class="level">护书使者 五级</view>
<view class="level">{{
topList[0].commonBookAchievement
&& (topList[0].commonBookAchievement.oldName || topList[0].commonBookAchievement.title)
}}</view>
</view>
<view class="top-item third" v-if="topList[2]">
<view class="avatar-frame">
<image class="frame-img" src="/pages_order/static/top/top1.png" mode="aspectFit"></image>
<image class="avatar" :src="topList[2].avatar" mode="aspectFill"></image>
<image class="avatar" :src="topList[2].hanHaiMember
&& topList[2].hanHaiMember.headImage" mode="aspectFill"></image>
</view>
<view class="badge">
<image class="badge-img" src="/pages_order/static/book/dj.png" mode="aspectFit"></image>
</view>
<view class="name">{{ topList[2].name }}</view>
<view class="score">{{ topList[2].num }} 亲密值</view>
<view class="level">护书使者 三级</view>
<view class="level">{{
topList[2].commonBookAchievement
&& (topList[2].commonBookAchievement.oldName || topList[2].commonBookAchievement.title)
}}</view>
</view>
</view>
@ -50,23 +68,29 @@
v-if="topList.length > 3">
<RankListItem v-for="(item, idx) in topList"
v-if="idx > 2"
:index="idx + 1"
:item="item"
:key="item.id" />
</view>
<!-- 底部按钮 -->
<view class="bottom-btn-area">
<button class="tipping-btn"
@click="$utils.navigateTo('pages_order/novel/Giftbox?id=' + bookId)"
@click="$refs.interactiveGiftPopup.open"
>互动打赏</button>
</view>
<interactiveGiftPopup ref="interactiveGiftPopup" :bookId="bookId" @giftSent="handleGiftSent"/>
</view>
</template>
<script>
import RankListItem from '../components/novel/RankListItem.vue'
import interactiveGiftPopup from '../components/novel/interactiveGiftPopup.vue'
export default {
components: {
RankListItem
RankListItem,
interactiveGiftPopup,
},
data() {
return {
@ -90,7 +114,18 @@ export default {
}).then(res => {
this.topList = res.records;
});
}
},
handleGiftSent(giftData) {
//
console.log('礼物发送成功:', giftData);
uni.showToast({
title: `成功赠送${giftData.gift.title} x${giftData.count}`,
icon: 'success'
});
//
this.getTopList();
},
}
}
</script>
@ -182,8 +217,12 @@ export default {
}
.name {
width: 200rpx;
overflow:hidden; //
text-overflow:ellipsis; //
white-space:nowrap; //
margin-top: 10rpx;
font-size: 36rpx;
font-size: 30rpx;
color: #FFFFFF;
font-weight: bold;
}


+ 74
- 54
pages_order/novel/Translation.vue View File

@ -1,32 +1,28 @@
<template>
<view class="page-container">
<navbar title="任务中心" leftClick @leftClick="$utils.navigateBack" />
<navbar title="任务中心" leftClick @leftClick="$utils.navigateBack" />
<view class="task-center">
<!-- 账户剩余 -->
<view class="account-balance">
<view class="balance-label">账户剩余</view>
<view class="balance-value"><text class="num">{{ maxVote }}</text> <text class="unit"> 推荐票</text></view>
<view class="balance-value"><text class="num">{{ maxVote }}</text> <text class="unit"> 推荐票</text>
</view>
</view>
<!-- 打卡得奖励 -->
<view class="checkin-section">
<view class="section-header">
<text>打卡得奖励</text>
<view class="record-btn">打卡记录</view>
<view class="record-btn" @click="$refs.signRecordPopup.open()">打卡记录</view>
</view>
<view class="checkin-grid">
<view v-for="day in clockList"
:key="day"
class="checkin-day"
:class="{ active: day.commonSignLog }">
<view class="day-label"
:class="{ bold: day.commonSignLog }">
<view v-for="day in clockList" :key="day" class="checkin-day"
:class="{ active: day.commonSignLog }">
<view class="day-label" :class="{ bold: day.commonSignLog }">
{{ day.title }}
</view>
<image class="ticket-img"
:src="day.image"
mode="aspectFit" />
<image class="ticket-img" :src="day.image" mode="aspectFit" />
<view class="ticket-num">+ {{ day.num }}</view>
</view>
</view>
@ -45,23 +41,28 @@
<view class="task-title">{{ task.title }}</view>
<view class="task-desc">推荐票 +{{ task.num }}</view>
</view>
<button class="get-btn" :class="{ 'received-btn': task.commonTaskLog }"
:disabled="task.commonTaskLog"
@click="clickMoreTask(task.id)">
<button class="get-btn" :class="{ 'received-btn': task.commonTaskLog }"
:disabled="task.commonTaskLog" @click="clickMoreTask(task.id)">
{{ task.commonTaskLog ? '已领取' : '去领取' }}
</button>
</view>
</view>
</view>
</view>
<!-- 签到记录弹窗组件 -->
<signRecordPopup ref="signRecordPopup" />
</view>
</template>
<script>
import mixinsList from '@/mixins/list.js'
import mixinsList from '@/mixins/list.js'
import signRecordPopup from '../components/novel/signRecordPopup.vue'
export default {
mixins: [mixinsList],
mixins: [mixinsList],
components: {
signRecordPopup,
},
data() {
return {
@ -79,70 +80,75 @@
received: false
},
],
clockList : [],
clockList: [],
isChecked: false, //
maxVote : 0,
mixinsListApi : '',
maxVote: 0,
mixinsListApi: '',
canSignToday: false, //
}
},
computed : {
isSign(){
let taskId = 0
this.clockList.forEach(n => {
if(!n.commonSignLog){
taskId = n.id
}
})
return !taskId
computed: {
isSign() {
//
return !this.canSignToday;
},
},
onShow() {
this.getMyRecommendTicketNum()
this.getSignTaskList()
this.getMoreTaskList()
this.checkTodaySignStatus()
},
methods: {
getMyRecommendTicketNum(){
getMyRecommendTicketNum() {
this.$fetch('getMyRecommendTicketNum')
.then(res => {
this.maxVote = res
})
.then(res => {
this.maxVote = res
})
},
getSignTaskList(){
getSignTaskList() {
this.$fetch('getSignTaskList', {
token : uni.getStorageSync('token')
})
.then(res => {
this.clockList = res
})
token: uni.getStorageSync('token')
})
.then(res => {
this.clockList = res
})
},
getMoreTaskList(){
getMoreTaskList() {
this.$fetch('getMoreTaskList', {
token : uni.getStorageSync('token')
})
.then(res => {
this.list = res
})
token: uni.getStorageSync('token')
})
.then(res => {
this.list = res
})
},
async clickSignTask(){
async clickSignTask() {
if (!this.canSignToday) {
uni.showToast({
title: '今日已签到',
icon: 'none'
})
return
}
let taskId = 0
for (var index = 0; index < this.clockList.length; index++) {
var element = this.clockList[index];
if(!element.commonSignLog){
if (!element.commonSignLog) {
taskId = element.id
break
}
}
if(!taskId){
if (!taskId) {
uni.showToast({
title: '已全部签到',
icon: 'none'
})
return
}
await this.$fetch('clickSignTask', {
taskId,
})
@ -150,10 +156,13 @@
title: '签到成功',
icon: 'none'
});
//
this.canSignToday = false;
this.getSignTaskList()
this.getMyRecommendTicketNum()
},
async clickMoreTask(taskId){
async clickMoreTask(taskId) {
await this.$fetch('clickMoreTask', {
taskId,
})
@ -187,6 +196,18 @@
icon: 'success'
});
},
checkTodaySignStatus() {
this.$fetch('getSignTaskToday')
.then(res => {
// res.result == 0
this.canSignToday = res.result;
})
.catch(err => {
console.error('获取今日签到状态失败:', err);
//
this.canSignToday = false;
});
},
},
}
</script>
@ -226,7 +247,6 @@
.balance-value {
font-weight: bold;
color: #bfa100;
.num {
font-size: 24rpx;


+ 0
- 388
pages_order/novel/Walletflow.vue View File

@ -1,388 +0,0 @@
<!-- 钱包流水页面 -->
<template>
<view class="walletflow-page">
<!-- 顶部导航栏 -->
<navbar title="钱包流水" leftClick @leftClick="$utils.navigateBack" />
<!-- 账户余额卡片 -->
<view class="balance-card">
<view class="balance-label">账户</view>
<view class="balance-row">
<text class="balance-amount">{{ balance }}</text>
<button class="recharge-btn" @click="goRecharge">充值</button>
</view>
</view>
<!-- tab和流水列表卡片 -->
<view class="flow-card">
<view class="tab-header">
<view :class="['tab-item', {active: activeTab === 0}]" @click="activeTab = 0">
充值
<view v-if="activeTab === 0" class="tab-underline"></view>
</view>
<view :class="['tab-item', {active: activeTab === 1}]" @click="activeTab = 1">
支付
<view v-if="activeTab === 1" class="tab-underline"></view>
</view>
</view>
<scroll-view scroll-y class="flow-list">
<view v-if="activeTab === 0">
<view class="flow-item" v-for="(item, idx) in rechargeList" :key="idx">
<view class="flow-item-row">
<view class="flow-item-left">
<view class="flow-title">{{ item.title }}</view>
<view class="flow-date">{{ item.date }}</view>
</view>
<view class="flow-amount plus">+{{ item.amount }}</view>
</view>
</view>
</view>
<view v-else>
<view class="flow-item" v-for="(item, idx) in payList" :key="idx">
<view class="flow-item-row">
<view class="flow-item-left">
<view class="flow-title">{{ item.title }}</view>
<view class="flow-date">{{ item.date }}</view>
</view>
<view class="flow-amount minus">-{{ item.amount }}</view>
</view>
</view>
</view>
</scroll-view>
</view>
</view>
</template>
<script>
export default {
components: {
},
data() {
return {
balance: 34532,
activeTab: 0,
rechargeList: [{
title: '豆豆充值',
date: '2025.03.18',
amount: 55
},
{
title: '豆豆充值',
date: '2025.03.18',
amount: 55
},
{
title: '豆豆充值',
date: '2025.03.18',
amount: 55
},
{
title: '推荐票',
date: '2025.03.18',
amount: 5
},
{
title: '豆豆充值',
date: '2025.03.18',
amount: 55
},
{
title: '豆豆充值',
date: '2025.03.18',
amount: 55
},
{
title: '豆豆充值',
date: '2025.03.18',
amount: 55
},
{
title: '豆豆充值',
date: '2025.03.18',
amount: 55
},
{
title: '豆豆充值',
date: '2025.03.18',
amount: 55
},
{
title: '豆豆充值',
date: '2025.03.18',
amount: 55
},
{
title: '豆豆充值',
date: '2025.03.18',
amount: 55
},
{
title: '豆豆充值',
date: '2025.03.18',
amount: 55
},
{
title: '豆豆充值',
date: '2025.03.18',
amount: 55
},
{
title: '豆豆充值',
date: '2025.03.18',
amount: 55
},
],
payList: [{
title: '章节支付',
date: '2025.03.18',
amount: 10
},
{
title: '章节支付',
date: '2025.03.18',
amount: 10
},
{
title: '章节支付',
date: '2025.03.18',
amount: 10
},
{
title: '章节支付',
date: '2025.03.18',
amount: 10
},
{
title: '章节支付',
date: '2025.03.18',
amount: 10
},
{
title: '章节支付',
date: '2025.03.18',
amount: 10
},
{
title: '章节支付',
date: '2025.03.18',
amount: 10
},
{
title: '章节支付',
date: '2025.03.18',
amount: 10
},
{
title: '章节支付',
date: '2025.03.18',
amount: 10
},
{
title: '章节支付',
date: '2025.03.18',
amount: 10
},
{
title: '章节支付',
date: '2025.03.18',
amount: 10
},
{
title: '章节支付',
date: '2025.03.18',
amount: 10
},
{
title: '章节支付',
date: '2025.03.18',
amount: 10
},
{
title: '章节支付',
date: '2025.03.18',
amount: 10
},
{
title: '章节支付',
date: '2025.03.18',
amount: 10
},
]
}
},
methods: {
goRecharge() {
uni.showToast({
title: '充值功能开发中',
icon: 'none'
})
}
}
}
</script>
<style lang="scss" scoped>
.walletflow-page {
min-height: 100vh;
background: linear-gradient(180deg, #f8f8fc 0%, #fff 100%);
padding-bottom: 30rpx;
}
.balance-card {
background: linear-gradient(90deg, #f7f2fa 0%, #fbeaf2 100%);
border-radius: 18rpx;
margin: 24rpx 12rpx 0 12rpx;
padding: 18rpx 24rpx 14rpx 24rpx;
box-shadow: none;
border: 1rpx solid #ede7ef;
position: relative;
display: flex;
flex-direction: column;
min-height: 130rpx;
justify-content: center;
.balance-label {
color: #bbb;
font-size: 26rpx;
margin-bottom: 8rpx;
}
.balance-row {
display: flex;
align-items: center;
margin-top: 0;
position: relative;
.balance-amount {
color: #e94f7a;
font-size: 48rpx;
font-weight: bold;
}
.recharge-btn {
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
background: linear-gradient(90deg, #ffb6c1 0%, #fa5a99 100%);
color: #fff;
font-size: 28rpx;
border-radius: 32rpx;
padding: 0 40rpx;
height: 56rpx;
line-height: 56rpx;
font-weight: 500;
border: none;
box-shadow: none;
display: flex;
align-items: center;
justify-content: center;
}
}
}
.flow-card {
background: #fff;
border-radius: 20rpx;
margin: 32rpx 16rpx 0 16rpx;
box-shadow: 0 4rpx 24rpx 0 rgba(0, 0, 0, 0.06);
padding-bottom: 8rpx;
overflow: hidden;
}
.tab-header {
display: flex;
margin: 0;
background: #fff;
border-top-left-radius: 20rpx;
border-top-right-radius: 20rpx;
overflow: hidden;
.tab-item {
flex: 1;
text-align: center;
font-size: 30rpx;
color: #888;
padding: 0 0 18rpx 0;
font-weight: bold;
background: transparent;
position: relative;
&.active {
color: #223a7a;
font-weight: bold;
}
.tab-underline {
position: absolute;
left: 50%;
bottom: 0;
transform: translateX(-50%);
width: 44rpx;
height: 4rpx;
background: #223a7a;
border-radius: 2rpx;
margin-top: 4rpx;
}
}
}
.flow-list {
margin: 0;
padding: 0 16rpx;
max-height: 75vh;
background: #fff;
}
.flow-item {
border-bottom: 1px solid #f5f5f5;
padding: 18rpx 0 8rpx 0;
&:last-child {
border-bottom: none;
}
.flow-item-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding-right: 45rpx;
padding-left: 15rpx;
}
.flow-item-left {
display: flex;
flex-direction: column;
align-items: flex-start;
.flow-title {
font-size: 28rpx;
color: #222;
font-weight: 500;
margin-bottom: 2rpx;
}
.flow-date {
color: #bbb;
font-size: 22rpx;
margin-top: 0;
}
}
.flow-amount {
font-size: 26rpx;
font-weight: 500;
margin-left: 24rpx;
margin-top: 2rpx;
&.plus {
color: #223a7a;
}
&.minus {
color: #e94f7a;
}
}
}
</style>

+ 4
- 3
pages_order/novel/bookList.vue View File

@ -6,9 +6,10 @@
<uv-search bgColor="#ffffff"
placeholder="搜索"
inputAlign="left"
@search="handleSearch"
v-model="keyword"
@change="searchChange"
@search="getData"
@clear="getData"
@custom="getData"
v-model="queryParams.name"
showAction="false"></uv-search>
</view>


+ 41
- 15
pages_order/novel/novelDetail.vue View File

@ -1,7 +1,7 @@
<template>
<!-- 小说详情页面 -->
<view class="novel-detail">
<navbar title="小说详情" leftClick @leftClick="$utils.navigateBack" />
<navbar leftClick @leftClick="$utils.navigateBack" />
<!-- 小说基本信息 -->
<view class="novel-info">
@ -13,7 +13,7 @@
<text class="title">{{ novelData.name }}</text>
<view class="author-line">
<text class="label">作者</text>
<text class="author">{{ novelData.author }}</text>
<text class="author">{{ novelData.author || '暂无显示' }}</text>
</view>
<!-- <view class="status-line">
<text class="label">完结</text>
@ -21,9 +21,12 @@
</view> -->
<view class="content-row">
<view class="book-status">
<!-- <view class="book-status">
<text>{{novelData.status}}</text>
</view>
</view> -->
<bookStatus :status="novelData.status"/>
<view class="book-text">
{{ novelData.service }}
</view>
@ -114,7 +117,7 @@
</view>
<view class="chapter-nav">
<text class="current-chapter">
{{ catalog ? catalog.title : '暂无章节' }}
{{ catalog ? catalog.title || '暂无章节' : '暂无章节' }}
</text>
<text class="nav-arrow">></text>
</view>
@ -152,7 +155,7 @@
<text class="btn-icon">🎁</text>
<text>礼物盒</text>
</view> -->
<view class="action-btn" @click="$utils.navigateTo(`/pages_order/novel/Giftbox?id=${novelData.id}`)">
<view class="action-btn" @click="$refs.interactiveGiftPopup.open()">
<view class="btn-icon">
<uv-icon name="gift" color="#999" size="60rpx"></uv-icon>
</view>
@ -167,6 +170,8 @@
<novelVotePopup ref="novelVotePopup" @updateVote="updateVote"/>
<chapterPopup ref="chapterPopup" :bookId="id" :chapterList="chapterList" @selectChapter="selectChapter"/>
<interactiveGiftPopup ref="interactiveGiftPopup" :bookId="id" @giftSent="handleGiftSent"/>
</view>
</template>
@ -175,7 +180,9 @@
import chapterPopup from '../components/novel/chapterPopup.vue'
import commentItem from '../components/comment/commentItem.vue'
import novelVotePopup from '../components/novel/novelVotePopup.vue'
import interactiveGiftPopup from '../components/novel/interactiveGiftPopup.vue'
import mixinsList from '@/mixins/list.js'
import bookStatus from '@/components/novel/bookStatus.vue'
export default {
mixins: [mixinsList],
components: {
@ -183,6 +190,8 @@
chapterPopup,
commentItem,
novelVotePopup,
interactiveGiftPopup,
bookStatus,
},
data() {
return {
@ -281,6 +290,7 @@
shopId: this.id,
name: this.novelData.name,
image: this.novelData.image,
novelId : this.fastCatalog && this.fastCatalog.id
}).then(res => {
uni.showToast({
title: '已加入书架',
@ -316,6 +326,22 @@
url: `/pages_order/novel/readnovels?cid=${item.id}&id=${this.id}`
})
},
handleGiftSent(giftData) {
//
console.log('礼物发送成功:', giftData);
uni.showToast({
title: `成功赠送${giftData.gift.title} x${giftData.count}`,
icon: 'success'
});
//
this.getDateil();
//
if(this.isLogin){
this.getAchievement();
}
},
}
}
</script>
@ -324,7 +350,7 @@
.novel-detail {
min-height: 100vh;
background-color: #f5f5f5;
padding-bottom: calc(env(safe-area-inset-bottom) + 30rpx);
padding-bottom: calc(env(safe-area-inset-bottom) + 100rpx);
.nav-header {
display: flex;
@ -340,19 +366,19 @@
}
.novel-info {
padding: 20rpx;
padding: 40rpx;
display: flex;
background: #fff;
.novel-cover {
width: 200rpx;
height: 280rpx;
width: 160rpx;
height: 200rpx;
margin-right: 20rpx;
image {
width: 100%;
height: 100%;
border-radius: 8rpx;
border-radius: 20rpx;
}
}
@ -373,7 +399,7 @@
display: flex;
align-items: center;
margin-bottom: 12rpx;
font-size: 28rpx;
font-size: 26rpx;
color: #666;
}
@ -433,11 +459,11 @@
.rec-left {
display: flex;
flex-direction: column;
align-items: flex-start;
align-items: center;
margin-left: 70rpx;
.rec-count {
font-size: 44rpx;
font-size: 34rpx;
font-weight: 500;
color: #333;
line-height: 1.2;
@ -621,7 +647,7 @@
.check-text {
font-size: 22rpx;
color: #999;
color: #33e;
}
}
}


+ 80
- 20
pages_order/novel/readnovels.vue View File

@ -18,10 +18,13 @@
<view class="progress-inner" :style="{width: readProgress + '%'}"></view>
</view> -->
</view>
<scroll-view scroll-y class="chapter-content" :class="{'full-content': isFullScreen}" @scroll="handleScroll"
@tap="handleContentClick">
<scroll-view :scroll-y="!isPay" :key="'scroll-' + cid" id="chapter-scroll" class="chapter-content" :class="{'full-content': isFullScreen}"
:scroll-top="scrollTop" @scroll="handleScroll" @tap="handleContentClick">
<view class="chapter-content-item">
<view class="chapter-title">{{ currentChapter }}</view>
<view class="paragraph-content">
@ -62,7 +65,9 @@
</view>
<!-- 使用封装的订阅弹窗组件 -->
<subscriptionPopup ref="subscriptionPopup" @subscribe="goToSubscription" />
<subscriptionPopup ref="subscriptionPopup"
@maskClick="toggleFullScreen"
@subscribe="goToSubscription" />
<novelVotePopup ref="novelVotePopup" />
@ -95,6 +100,10 @@
cid: 0,
novelData: {},
chapterList: [],
scrollTop: 0, //
//
isPay : false,
}
},
computed: {
@ -131,12 +140,35 @@
this.novelData = res
})
},
getBookCatalogDetail() {
async getBookCatalogDetail() {
this.isPay = await this.$fetch('getMyShopNovel', {
bookId : this.id,
novelId : this.cid,
})
this.isPay = !this.isPay
this.$fetch('getBookCatalogDetail', {
id: this.cid
}).then(res => {
this.paragraphs = res.details && res.details.split('\n')
this.currentChapter = res.title
if(res.isPay != 'Y'){
this.isPay = false
}
this.updateSub()
//
this.updateReadProgress()
//
this.$nextTick(() => {
this.scrollTop = 0; // scroll-view
})
})
},
getBookCatalogList() {
@ -147,8 +179,6 @@
reverse: 0,
}).then(res => {
this.chapterList = res.records
this.catalog = res.records[res.records.length - 1]
this.fastCatalog = res.records[0]
})
},
handleContentClick() {
@ -157,6 +187,7 @@
handleScroll(e) {
//
const scrollTop = e.detail.scrollTop;
this.scrollTop = scrollTop; //
//
if (scrollTop > 50 && !this.popupShown) {
@ -167,10 +198,14 @@
toggleFullScreen() {
this.isFullScreen = !this.isFullScreen
},
goToSubscription() {
uni.navigateTo({
url: '/pages_order/novel/SubscriptionInformation'
async goToSubscription() {
await this.$fetch('buyNovel', {
bookId : this.id,
novelId : this.cid,
})
this.isPay = false
this.updateSub()
},
selectChapter({
item,
@ -193,6 +228,29 @@
this.isFullScreen = true
this.getBookCatalogDetail()
},
//
updateReadProgress() {
if (!this.id || !this.cid) return;
this.$fetch('saveOrUpdateReadBook', {
shopId: this.id, // id
novelId: this.cid, // id
name: this.novelData.name,
image: this.novelData.image
}).then(res => {
console.log('阅读进度已更新');
}).catch(err => {
console.error('更新阅读进度失败:', err);
});
},
updateSub(){
if(this.isPay){
this.$refs.subscriptionPopup.open()
}else{
this.$refs.subscriptionPopup.close()
}
},
},
}
</script>
@ -265,12 +323,12 @@
.bottom-right {
.outline-btn {
background: #222;
color: #4a90e2;
border: 2rpx solid #4a90e2;
color: #999;
border: 2rpx solid #999;
.btn-text {
color: #4a90e2;
border-bottom: 2rpx solid #4a90e2;
color: #999;
border-bottom: 2rpx solid #999;
}
}
}
@ -284,7 +342,7 @@
right: 0;
background: rgba(255, 255, 255, 0.98);
padding-top: calc(var(--status-bar-height) + 10rpx);
z-index: 100;
z-index: 100000;
transform: translateY(0);
transition: transform 0.3s ease-in-out, opacity 0.3s ease-in-out, background-color 0.3s ease;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
@ -418,7 +476,7 @@
align-items: center;
height: 180rpx;
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
z-index: 10;
z-index: 100000;
padding: 0 40rpx 10rpx 40rpx;
transform: translateY(0);
transition: transform 0.3s ease-in-out, background-color 0.3s ease;
@ -460,19 +518,21 @@
.bottom-right {
display: flex;
align-items: flex-end;
gap: 32rpx;
gap: 22rpx;
margin-left: 40rpx;
text-overflow: ellipsis;
.outline-btn {
flex-shrink: 0;
min-width: 110rpx;
padding: 0 28rpx;
padding: 0 26rpx;
height: 60rpx;
line-height: 60rpx;
background: #fff;
color: #223a7a;
border: 2rpx solid #223a7a;
border-radius: 32rpx;
font-size: 28rpx;
font-size: 26rpx;
font-weight: bold;
margin: 0;
display: flex;
@ -483,7 +543,7 @@
.btn-text {
font-weight: bold;
color: #223a7a;
font-size: 28rpx;
font-size: 26rpx;
border-bottom: 2rpx solid #223a7a;
padding-bottom: 2rpx;
transition: color 0.3s ease, border-color 0.3s ease;


+ 1
- 1
uni_modules/uv-switch/components/uv-switch/props.js View File

@ -21,7 +21,7 @@ export default {
// 开关尺寸,单位px
size: {
type: [String, Number],
default: 25
default: 50
},
// 打开时的背景颜色
activeColor: {


Loading…
Cancel
Save