Browse Source

feat: 新增作家、书籍、评论、订单等相关接口和页面

- 新增作家相关接口和页面,包括保存或更新作家信息、获取作家信息等
- 新增书籍相关接口和页面,包括获取我的作品、添加或修改作品、增加或修改章节等
- 新增评论相关接口和页面,包括获取评论列表、删除评论、回复评论等
- 新增订单相关接口和页面,包括创建订单、支付订单、查询礼物详情等
- 优化部分页面样式和功能,修复已知问题
master
前端-胡立永 10 months ago
parent
commit
33dd0a57ac
48 changed files with 2777 additions and 2039 deletions
  1. +8
    -2
      api/api.js
  2. +6
    -21
      api/model/bookshelf.js
  3. +46
    -0
      api/model/comment.js
  4. +0
    -5
      api/model/index.js
  5. +33
    -0
      api/model/my_book.js
  6. +42
    -0
      api/model/order.js
  7. +52
    -0
      api/model/task.js
  8. +18
    -0
      api/model/writer.js
  9. +0
    -102
      components/novel/RankListItem.vue
  10. +1
    -1
      components/novel/newWorkItem.vue
  11. +23
    -5
      components/novel/novelItem.vue
  12. +60
    -12
      components/novel/workItem.vue
  13. +1
    -1
      config.js
  14. +10
    -7
      pages.json
  15. +43
    -37
      pages/index/bookshelf.vue
  16. +14
    -8
      pages/index/center.vue
  17. +1
    -1
      pages_order/auth/wxLogin.vue
  18. +231
    -0
      pages_order/author/chapterList.vue
  19. +18
    -2
      pages_order/author/createNovel.vue
  20. +43
    -20
      pages_order/author/creator.vue
  21. +13
    -4
      pages_order/author/editor.vue
  22. +246
    -0
      pages_order/comment/comments.vue
  23. +255
    -0
      pages_order/comment/respondComments.vue
  24. +197
    -0
      pages_order/comment/review.vue
  25. +29
    -2
      pages_order/components/comment/commentItem.vue
  26. +0
    -141
      pages_order/components/novel/README.md
  27. +83
    -0
      pages_order/components/novel/RankListItem.vue
  28. +28
    -99
      pages_order/components/novel/chapterPopup.vue
  29. +56
    -45
      pages_order/components/novel/novelVotePopup.vue
  30. +16
    -6
      pages_order/novel/Giftbox.vue
  31. +47
    -16
      pages_order/novel/ReaderAchievement.vue
  32. +0
    -212
      pages_order/novel/Respondcomments.vue
  33. +0
    -189
      pages_order/novel/Review.vue
  34. +234
    -271
      pages_order/novel/Tipping.vue
  35. +105
    -24
      pages_order/novel/Translation.vue
  36. +0
    -3
      pages_order/novel/Walletflow.vue
  37. +0
    -166
      pages_order/novel/chapterList.vue
  38. +0
    -250
      pages_order/novel/comments.vue
  39. +0
    -39
      pages_order/novel/components/AgreementCheck.vue
  40. +0
    -28
      pages_order/novel/components/BackArrow.vue
  41. +0
    -45
      pages_order/novel/components/LoginButton.vue
  42. +190
    -142
      pages_order/novel/novelDetail.vue
  43. +463
    -0
      pages_order/novel/readnovels - 副本.vue
  44. +160
    -128
      pages_order/novel/readnovels.vue
  45. BIN
      pages_order/static/top/4.png
  46. BIN
      pages_order/static/top/top1.png
  47. +1
    -1
      store/store.js
  48. +4
    -4
      uni_modules/uv-empty/components/uv-empty/props.js

+ 8
- 2
api/api.js View File

@ -5,7 +5,8 @@ import utils from '../utils/utils.js'
let limit = {}
let debounce = {}
const models = ['login', 'index', 'bookshelf']
const models = ['login', 'index', 'bookshelf', 'my_book', 'comment', 'task'
, 'order', 'writer']
const config = {
// 示例
@ -52,8 +53,13 @@ export function api(key, data, callback, loadingTitle) {
//必须登录
if (req.auth) {
if (!uni.getStorageSync('token')) {
// utils.toLogin()
// console.error('需要登录', req.url)
// store.commit('logout', '登录过期了,你可以停留在此页面或去重新登录')
console.error('登录过期');
utils.toLogin()
console.error('需要登录', req.url)
return Promise.reject()
}
}


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

@ -1,44 +1,29 @@
// 书架相关接口
const api = {
// 批量移除我阅读过的数据根据书籍标识
// 批量移除我书架过的数据根据书籍标识
batchRemoveReadBook: {
url: '/all_book/batchRemoveReadBook',
method: 'GET',
auth: true,
},
// 获取我的作品带分页
getMyBookPage: {
url: '/all_book/getMyBookPage',
method: 'GET',
auth: true,
},
// 获取我阅读过的书籍列表带分页
// 获取我书架过的书籍列表带分页
getReadBookPage: {
url: '/all_book/getReadBookPage',
method: 'GET',
auth: true,
},
// 移除我阅读过的书籍根据书籍标识
// 移除我书架过的书籍根据书籍标识
removeReadBook: {
url: '/all_book/removeReadBook',
method: 'GET',
auth: true,
},
// 添加作品或者修改作品
saveOrUpdateBook: {
url: '/all_book/saveOrUpdateBook',
method: 'POST',
auth: true,
limit: 800,
showLoading: true,
},
// 增加或修改作品章节
saveOrUpdateCatalog: {
url: '/all_book/saveOrUpdateCatalog',
// 增加书架记录
addReadBook: {
url: '/all_book/addReadBook',
method: 'POST',
auth: true,
limit: 800,
},
}

+ 46
- 0
api/model/comment.js View File

@ -0,0 +1,46 @@
const api = {
// 根据书本标识获取书本评论列表
getBookCommentList: {
url: '/my_comment/getCommentList',
method: 'GET',
},
// 删除评论信息
deleteComment: {
url: '/my_comment/deleteComment',
method: 'GET',
},
// 获取我的评论列表
getMyCommentList: {
url: '/my_comment/getMyCommentList',
method: 'GET',
},
// 获取我的评论数
getMyCommentNum: {
url: '/my_comment/getMyCommentNum',
method: 'GET',
},
// 回复评论信息
replyComment: {
url: '/my_comment/replyComment',
method: 'POST',
},
// 保存评论信息
saveComment: {
url: '/my_comment/saveComment',
method: 'POST',
},
// 更新评论已读状态
updateCommentRead: {
url: '/my_comment/updateCommentRead',
method: 'POST',
},
// 获取评论详情
getCommentDetail: {
url: '/my_comment/getCommentDetail',
method: 'POST',
},
}
export default api

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

@ -38,11 +38,6 @@ const api = {
url: '/all_index/getBookCatalogList',
method: 'GET',
},
// 根据书本标识获取书本评论列表
getBookCommentList: {
url: '/all_index/getBookCommentList',
method: 'GET',
},
// 根据书本标识获取书本详细信息
getBookDetail: {
url: '/all_index/getBookDetail',


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

@ -0,0 +1,33 @@
// 书架相关接口
const api = {
// 获取我的作品带分页
getMyBookPage: {
url: '/my_book/getMyShopPage',
method: 'GET',
auth: true,
},
// 添加作品或者修改作品
saveOrUpdateBook: {
url: '/my_book/saveOrUpdateShop',
method: 'POST',
auth: true,
limit: 800,
showLoading: true,
},
// 增加或修改作品章节
saveOrUpdateCatalog: {
url: '/my_book/saveOrUpdateShopNovel',
method: 'POST',
auth: true,
limit: 800,
},
// 获取我的小说章节列表带分页
getMyShopNovelPage : {
url: '/my_book/getMyShopNovelPage',
method: 'POST',
auth: true,
},
}
export default api

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

@ -0,0 +1,42 @@
const api = {
// 创建订单
createOrder : {
url: '/my_order/createOrder',
method: 'POST',
auth: true,
limit: 800,
},
// 查询礼物详情
getGiftDetail : {
url: '/my_order/getGiftDetail',
method: 'GET',
auth: true,
},
// 查询互动打赏礼物信息列表
getInteractionGiftList : {
url: '/my_order/getInteractionGiftList',
method: 'GET',
auth: true,
},
// 查询我的礼物包订单列表
getMyGiftList : {
url: '/my_order/getMyGiftList',
method: 'GET',
auth: true,
},
// 支付订单
payOrder : {
url: '/my_order/payOrder',
method: 'POST',
auth: true,
},
// 支付成功
paySuccess : {
url: '/my_order/paySuccess',
method: 'POST',
auth: true,
},
}
export default api

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

@ -0,0 +1,52 @@
const api = {
// 点击更多任务
clickMoreTask: {
url: '/my_task/clickMoreTask',
method: 'POST',
auth: true,
limit : 1000,
showLoading: true,
},
// 点击签到任务
clickSignTask: {
url: '/my_task/clickSignTask',
method: 'GET',
auth: true,
limit : 1000,
showLoading: true,
},
// 获取更多任务列表
getMoreTaskList: {
url: '/my_task/getMoreTaskList',
method: 'GET',
auth: true,
},
// 获取更多任务记录列表
getMoreTaskRecordPage: {
url: '/my_task/getMoreTaskRecordPage',
method: 'GET',
auth: true,
},
// 获取我的推荐票数
getMyRecommendTicketNum: {
url: '/my_task/getMyRecommendTicketNum',
method: 'GET',
auth: true,
},
// 获取我的推荐任务列表
getSignTaskList: {
url: '/my_task/getSignTaskList',
method: 'GET',
auth: true,
},
// 获取我的推荐任务记录列表
getSignTaskRecordPage: {
url: '/my_task/getSignTaskRecordPage',
method: 'GET',
auth: true,
},
}
export default api

+ 18
- 0
api/model/writer.js View File

@ -0,0 +1,18 @@
const api = {
// 填写或修改笔名以及简介成为作家
saveOrUpdateWriter : {
url: '/my_writer/saveOrUpdateWriter',
method: 'POST',
auth: true,
limit: 800,
},
// 查询我的笔名以及简介
getMyWriter : {
url: '/my_writer/getMyWriter',
method: 'GET',
auth: true,
},
}
export default api

+ 0
- 102
components/novel/RankListItem.vue View File

@ -1,102 +0,0 @@
<template>
<view class="rank-item">
<view class="rank-left">
<image v-if="rankIcon" :src="rankIcon" class="rank-icon" />
<image v-else-if="rankNumImg" :src="rankNumImg" class="rank-num-img" />
<slot name="rankNum" v-else />
<image v-if="medal" :src="medal" class="medal" />
<image class="avatar" :src="avatar" mode="aspectFill" />
<view class="name">{{ name }}</view>
</view>
<view class="rank-right">
<view class="score">{{ score }} 亲密值</view>
<view class="level">{{ level }}</view>
</view>
</view>
</template>
<script>
export default {
name: 'RankListItem',
props: {
rankIcon: String, // 3
rankNumImg: String, // 4-10
medal: String, //
avatar: String,
name: String,
score: [String, Number],
level: {
type: String,
default: '护书使者 五级'
}
}
}
</script>
<style lang="scss" scoped>
.rank-item {
display: flex;
align-items: center;
justify-content: space-between;
background: #fffbe6;
border-radius: 16rpx;
margin-bottom: 18rpx;
box-shadow: 0 2rpx 8rpx 0 rgba(184, 110, 59, 0.06);
padding: 0 24rpx;
height: 100rpx;
.rank-left {
display: flex;
align-items: center;
.rank-icon,
.rank-num-img {
width: 38rpx;
height: 38rpx;
margin-right: 10rpx;
}
.medal {
width: 44rpx;
height: 44rpx;
margin-right: 10rpx;
}
.avatar {
width: 44rpx;
height: 44rpx;
border-radius: 50%;
margin-right: 14rpx;
border: 2rpx solid #ffd700;
object-fit: cover;
}
.name {
font-size: 26rpx;
color: #222;
font-weight: 500;
}
}
.rank-right {
display: flex;
flex-direction: column;
align-items: flex-end;
.score {
font-size: 22rpx;
color: #b86e3b;
}
.level {
font-size: 20rpx;
color: #fff;
background: #e6b07c;
border-radius: 8rpx;
padding: 2rpx 10rpx;
margin-top: 6rpx;
font-weight: 500;
}
}
}
</style>

+ 1
- 1
components/novel/newWorkItem.vue View File

@ -20,7 +20,7 @@
methods: {
handleClick() {
uni.navigateTo({
url: '/pages_order/novel/createNovel'
url: '/pages_order/author/createNovel'
})
},
handleSettingClick() {


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

@ -16,7 +16,7 @@
<view class="content-row" v-if="!horizontal">
<view class="book-status">
<text>{{book.status}}</text>
<text>{{ statusText }}</text>
</view>
<view class="book-text">
{{ item.service || '大家都在读' }}
@ -39,6 +39,24 @@
default: false
}
},
computed: {
statusClass() {
const statusMap = {
'draft': 'new',
'0': 'ongoing',
'1': 'completed'
};
return statusMap[this.book.status] || 'ongoing';
},
statusText() {
const textMap = {
// '0': '',
'0': '连载中',
'1': '已完结'
};
return textMap[this.book.status] || '连载中';
},
},
data() {
return {}
},
@ -58,8 +76,8 @@
}
.book-cover {
width: 170rpx;
height: 230rpx;
width: 160rpx;
height: 210rpx;
border-radius: 8rpx;
margin-right: 20rpx;
box-shadow: 0 4rpx 8rpx rgba(0,0,0,0.1);
@ -139,13 +157,13 @@
/* 水平布局样式 - 用于网格展示 */
.book-item.horizontal {
flex-direction: column;
width: 200rpx;
width: 160rpx;
padding: 10rpx;
border: none;
.book-cover {
width: 100%;
height: 260rpx;
height: 200rpx;
margin-right: 0;
margin-bottom: 10rpx;
}


+ 60
- 12
components/novel/workItem.vue View File

@ -19,8 +19,17 @@
</view>
<!-- 发布状态标签 -->
<view class="publish-status" v-if="work.publishStatus">
<text>{{work.publishStatus || '发布审核中'}}</text>
<view class="publish-status"
:class="bookStatusClass"
>
<text>{{bookStatusText}}</text>
</view>
<!-- 设置状态标签 -->
<view class="publish-status"
:class="toolStatusClass"
>
<text>{{toolStatusText}}</text>
</view>
</view>
@ -41,7 +50,7 @@
isManaging: {
type: Boolean,
default: false
}
},
},
computed: {
statusClass() {
@ -59,7 +68,39 @@
'1': '已完结'
};
return textMap[this.work.status] || '连载中';
}
},
toolStatusClass() {
const toolStatusMap = {
'0': 'ongoing',
'1': 'completed',
'2': 'error',
};
return toolStatusMap[this.work.toolStatus] || '';
},
toolStatusText() {
const textMap = {
'0': '设置审核中',
'1': '设置审核通过',
'2': '设置审核不通过'
};
return textMap[this.work.toolStatus] || '连载中';
},
bookStatusClass() {
const bookStatusMap = {
'0': 'ongoing',
'1': 'completed',
'2': 'error',
};
return bookStatusMap[this.work.bookStatus] || '';
},
bookStatusText() {
const textMap = {
'0': '发布审核中',
'1': '发布审核通过',
'2': '发布审核不通过'
};
return textMap[this.work.bookStatus] || '连载中';
},
},
methods: {
handleClick() {
@ -69,7 +110,7 @@
}
//
uni.navigateTo({
url: '/pages_order/novel/chapterList?id=' + this.work.id
url: '/pages_order/author/chapterList?id=' + this.work.id
});
},
handleDelete() {
@ -148,6 +189,7 @@
display: flex;
align-items: center;
margin-top: 20rpx;
gap: 10rpx;
.status-tag {
font-size: 22rpx;
@ -173,14 +215,20 @@
}
.publish-status {
text {
font-size: 22rpx;
color: #666;
background-color: #f5f5f5;
padding: 4rpx 16rpx;
border-radius: 20rpx;
white-space: nowrap;
&.error{
color: #666;
background-color: #f5f5f5;
}
&.completed {
background-color: #67c23a;
color: #fff;
}
font-size: 22rpx;
color: #666;
background-color: #f5f5f5;
padding: 4rpx 16rpx;
border-radius: 20rpx;
white-space: nowrap;
}
}
}


+ 1
- 1
config.js View File

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


+ 10
- 7
pages.json View File

@ -62,10 +62,10 @@
"path": "announcement/announcements"
},
{
"path": "novel/createNovel"
"path": "author/createNovel"
},
{
"path": "novel/chapterList"
"path": "author/chapterList"
},
{
"path": "novel/ReaderAchievement"
@ -77,22 +77,25 @@
"path": "novel/SubscriptionInformation"
},
{
"path": "novel/Tipping"
"path": "novel/Tipping",
"style": {
"navigationBarTextStyle": "white"
}
},
{
"path": "novel/Review"
"path": "comment/review"
},
{
"path": "novel/comments"
"path": "comment/comments"
},
{
"path": "novel/Respondcomments"
"path": "comment/respondComments"
},
{
"path": "novel/Walletflow"
},
{
"path": "novel/creator"
"path": "author/creator"
},
{
"path": "novel/Giftbox"


+ 43
- 37
pages/index/bookshelf.vue View File

@ -31,6 +31,11 @@
</view>
</view>
</view>
<!-- 空状态提示 -->
<view class="empty-works" v-if="list.length === 0">
<text class="empty-text">你书架还没有书籍呢</text>
<text class="empty-tips">去首页看看吧</text>
</view>
</view>
<!-- 作品列表 - 作品模式 -->
@ -178,16 +183,16 @@
//
const activeTab = uni.getStorageSync('activeBookshelfTab')
if (activeTab === 'work') {
this.activeTab = 'work'
this.mixinsListApi = this.apiMap[tab]
uni.removeStorageSync('activeBookshelfTab')
}
// if (activeTab === 'work') {
// this.activeTab = 'work'
// this.mixinsListApi = this.apiMap[tab]
// uni.removeStorageSync('activeBookshelfTab')
// }
//
uni.$on('switchToWork', () => {
this.activeTab = 'work'
})
// uni.$on('switchToWork', () => {
// this.activeTab = 'work'
// })
},
onShow() {
@ -315,15 +320,22 @@
uni.showModal({
title: '提示',
content: content,
success: (res) => {
success: async (res) => {
if (res.confirm) {
if (this.activeTab === 'read') {
//
this.novels = this.novels.filter(novel => !this.selectedItems.includes(novel.id));
// this.novels = this.novels.filter(novel => !this.selectedItems.includes(novel.id));
await this.$fetch('batchRemoveReadBook', {
bookIds : this.selectedItems.join(',')
})
uni.showToast({
title: '移除成功',
icon: 'success'
});
this.getData()
} else {
//
this.list = this.list.filter(work => !this.selectedItems.includes(work.id));
@ -338,7 +350,7 @@
this.selectedItems = [];
// 退
if ((this.activeTab === 'read' && this.novels.length === 0) ||
if ((this.activeTab === 'read' && this.list.length === 0) ||
(this.activeTab === 'work' && this.list.length === 0)) {
this.exitEditMode();
}
@ -346,11 +358,6 @@
}
});
},
//
loadWorksList() {
const savedWorks = uni.getStorageSync('list') || []
this.list = savedWorks
}
}
}
</script>
@ -436,6 +443,26 @@
}
}
.empty-works {
width: 100%;
padding: 100rpx 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.empty-text {
font-size: 32rpx;
color: #666;
margin-bottom: 20rpx;
}
.empty-tips {
font-size: 28rpx;
color: #999;
}
}
.novel-grid {
padding: 20rpx;
padding-top: 30rpx;
@ -444,7 +471,6 @@
.novel-row {
display: flex;
justify-content: space-between;
margin-bottom: 40rpx;
.novel-item {
@ -494,26 +520,6 @@
position: relative;
margin-bottom: 20rpx;
}
.empty-works {
width: 100%;
padding: 100rpx 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.empty-text {
font-size: 32rpx;
color: #666;
margin-bottom: 20rpx;
}
.empty-tips {
font-size: 28rpx;
color: #999;
}
}
}
}


+ 14
- 8
pages/index/center.vue View File

@ -51,7 +51,7 @@
<uv-icon name="chat" size="40rpx" color="#333"></uv-icon>
<text>我的评论</text>
</view>
<view class="badge">294</view>
<view class="badge" v-if="commentNum">{{ commentNum }}</view>
<uv-icon name="arrow-right" size="36rpx" color="#999"></uv-icon>
</view>
<view class="section-item" @click="$utils.navigateTo('/pages_order/novel/Translation')">
@ -59,7 +59,7 @@
<uv-icon name="list" size="40rpx" color="#333"></uv-icon>
<text>任务中心</text>
</view>
<view class="badge">5</view>
<!-- <view class="badge">5</view> -->
<uv-icon name="arrow-right" size="36rpx" color="#999"></uv-icon>
</view>
</view>
@ -69,7 +69,7 @@
<view class="section">
<view class="section-title">设置</view>
<view class="section-list">
<view class="section-item" @click="$utils.navigateTo('/pages_order/novel/creator')">
<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>
@ -107,15 +107,15 @@
<view v-else-if="!isLogin" class="nologin-page">
<view class="nologin-top-bg">
<view class="nologin-header">
<image class="nologin-avatar" mode="aspectFill" @click="toLogin" />
<view class="nologin-text" @click="toLogin">点击登录</view>
<image class="nologin-avatar" mode="aspectFill" @click="$utils.toLogin" />
<view class="nologin-text" @click="$utils.toLogin">点击登录</view>
</view>
<view class="nologin-header-right">
<uv-icon name="more-dot-fill" size="40rpx" color="#bbb"></uv-icon>
</view>
</view>
<view class="nologin-content-center">
<button class="nologin-btn" @click="toLogin">立即登录</button>
<button class="nologin-btn" @click="$utils.toLogin">立即登录</button>
</view>
</view>
@ -133,14 +133,19 @@
},
data() {
return {
commentNum : 0,
}
},
onLoad() {
onShow() {
if(this.isLogin){
this.$store.commit('getUserInfo')
this.getMyCommentNum()
}
},
methods: {
async getMyCommentNum(){
this.commentNum = await this.$fetch('getMyCommentNum')
},
}
}
</script>
@ -243,7 +248,8 @@
font-size: 22rpx;
padding: 2rpx 12rpx;
border-radius: 20rpx;
margin-right: 10rpx;
margin-right: auto;
margin-left: 20rpx;
}
}
}


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

@ -14,7 +14,7 @@
<image src="../static/auth/wx.png" mode=""></image>
</view> -->
<view class="">
手机号授权登录
授权登录
</view>
</view>


+ 231
- 0
pages_order/author/chapterList.vue View File

@ -0,0 +1,231 @@
<template>
<view class="chapter-container">
<navbar title="章节列表" leftClick @leftClick="$utils.navigateBack"/>
<view class="tabs">
<uv-tabs :list="tabs"
:activeStyle="{color : '#0A2463', fontWeight : 600}"
lineColor="#0A2463"
:inactiveStyle="{color: '#0A2463'}"
lineHeight="8rpx"
lineWidth="50rpx"
:scrollable="false"
:current="activeTab"
@click="clickTabs"></uv-tabs>
</view>
<view class="box">
<view class="chapter-list" >
<view class="draft-header">
<text class="draft-title">{{ activeTab === 0 ? '草稿箱章节' : '已发布章节' }}</text>
<text class="delete-btn" @click="reverseList">{{ queryParams.reverse ? '正序' : '倒序' }}</text>
</view>
<view
class="chapter-item"
v-for="(chapter, index) in list"
:key="chapter.id"
@click="editChapter(chapter)">
<view class="chapter-info">
<text class="chapter-title">章节名</text>
<text class="chapter-number">{{ chapter.title }}</text>
</view>
<uv-icon name="arrow-right" color="#999" size="28"></uv-icon>
</view>
</view>
</view>
<view class="bottom-actions">
<button class="btn-settings" @click="handleSettings">设置作品</button>
<button class="btn-new" @click="addNewChapter">新建章节</button>
</view>
</view>
</template>
<script>
import mixinsList from '@/mixins/list.js'
export default {
mixins: [mixinsList],
components: {
},
data() {
return {
tabs : [
{
name : '草稿箱',
index : 1
},
{
name : '已发布',
index : 0
}
],
activeTab: 0,
mixinsListApi : 'getMyShopNovelPage',
id : 0,
}
},
onLoad(options) {
this.queryParams.reverse = 0
// tabtab
if (options.activeTab) {
this.activeTab = options.activeTab;
}
if(options.id){
this.queryParams.bookId = options.id
this.id = options.id
this.queryParams.status = this.activeTab
}
},
methods: {
clickTabs(tab) {
this.activeTab = tab.index;
this.queryParams.status = this.activeTab
this.list = []
this.getData()
},
reverseList() {
this.queryParams.reverse = [1, 0][this.queryParams.reverse]
this.getData()
},
addNewChapter() {
uni.navigateTo({
url: '/pages_order/author/editor?id=' + this.id
})
},
editChapter(chapter) {
uni.navigateTo({
url: '/pages_order/author/editor?cid=' + chapter.id + '&id=' + this.id
})
},
handleSettings() {
uni.navigateTo({
url: '/pages_order/novel/createNovel?id=' + this.id
})
},
deleteAll() {
uni.showModal({
title: '提示',
content: '确定要删除所有草稿章节吗?',
success: (res) => {
if (res.confirm) {
// 稿API
this.$api.deleteAllDrafts({bookId: this.id}).then(res => {
if (res.code === 200) {
this.$utils.showToast('删除成功')
this.getData()
}
})
}
}
})
}
}
}
</script>
<style lang="scss" scoped>
.chapter-container {
min-height: 100vh;
background-color: #f7f7f7;
padding-bottom: 70px;
.tabs{
background-color: #fff;
}
.box{
padding: 20rpx 40rpx;
background-color: #fff;
margin: 50rpx 40rpx;
border-radius: 20rpx;
.draft-header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 20rpx;
border-bottom: 1px dashed #eee;
margin-bottom: 10rpx;
.draft-title {
font-size: 30rpx;
color: #000;
font-weight: 900;
}
.delete-btn {
font-size: 28rpx;
color: #999;
}
}
.chapter-list {
.chapter-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 0;
border-bottom: 1px solid #eee;
.chapter-info {
.chapter-title {
font-size: 14px;
color: #999;
margin-bottom: 5px;
display: block;
}
.chapter-number {
font-size: 16px;
color: #333;
display: block;
}
}
.icon-arrow {
color: #999;
font-size: 16px;
}
}
}
}
.bottom-actions {
padding-bottom: calc(env(safe-area-inset-bottom) + 30rpx);
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
padding: 10px 15px;
background: #fff;
box-shadow: 0 -2px 6px rgba(0, 0, 0, 0.05);
button {
flex: 1;
height: 40px;
border-radius: 20px;
font-size: 16px;
margin: 0 5px;
&.btn-settings {
background: #fff;
border: 1px solid #ddd;
color: #333;
}
&.btn-new {
background: #2b4acb;
color: #fff;
border: none;
}
}
}
}
</style>

pages_order/novel/createNovel.vue → pages_order/author/createNovel.vue View File

@ -100,10 +100,26 @@
type: '1',
details: '',
status: '0' //
}
},
id: 0
}
},
onLoad(options) {
if (options.id) {
this.id = options.id
this.getBookInfo()
}
},
methods: {
getBookInfo() {
this.$fetch('getBookDetail', {
id : this.id
}).then(res => {
this.formData = res
})
},
//
chooseCover() {
uni.chooseImage({
@ -149,7 +165,7 @@
Image: this.formData.Image,
type: this.formData.type,
details: this.formData.details,
status: this.formData.status,
status: this.formData.status || 0,
}
await this.$fetch('saveOrUpdateBook', workData)

pages_order/novel/creator.vue → pages_order/author/creator.vue View File

@ -5,14 +5,12 @@
<view class="form-card">
<view class="form-item">
<text class="required">*</text>
<text class="label">笔名</text>
<input v-model="penName" placeholder="请输入" class="input" placeholder-class="input-placeholder" />
<input v-model="form.name" placeholder="请输入" class="input" placeholder-class="input-placeholder" />
</view>
<view class="form-item">
<text class="required">*</text>
<text class="label">简介</text>
<textarea v-model="intro" placeholder="请输入" class="textarea" placeholder-class="input-placeholder" />
<textarea v-model="form.details" placeholder="请输入" class="textarea" placeholder-class="input-placeholder" />
</view>
</view>
<view class="footer-btn-area">
@ -28,26 +26,52 @@ export default {
},
data() {
return {
penName: '',
intro: ''
form : {
name : '',
details : '',
},
}
},
onLoad() {
this.getMyWriter()
},
methods: {
submit() {
if (!this.penName) {
if (!this.form.name) {
uni.showToast({ title: '请输入笔名', icon: 'none' })
return
}
if (!this.intro) {
if (!this.form.details) {
uni.showToast({ title: '请输入简介', icon: 'none' })
return
}
//
uni.showToast({ title: '申请成功', icon: 'success' })
setTimeout(() => {
uni.navigateBack()
}, 800)
}
let data = {
penName : this.form.name,
details : this.form.details,
}
if(this.form.id){
data.id = this.form.id
}
this.$api('saveOrUpdateWriter', data, res => {
if(res.code == 200){
uni.showToast({ title: '申请成功', icon: 'success' })
setTimeout(() => {
uni.navigateBack()
}, 800)
}
})
},
getMyWriter(){
this.$api('getMyWriter')
.then(res => {
if(res.result){
this.form = res.result
}
})
},
}
}
</script>
@ -75,17 +99,16 @@ export default {
position: relative;
}
.required {
color: #e23d3d;
font-size: 28rpx;
margin-right: 4rpx;
}
.label {
font-size: 28rpx;
color: #222;
font-weight: bold;
margin-bottom: 12rpx;
&::before{
content: '*';
color: #e23d3d;
font-size: 28rpx;
}
}
.input,

+ 13
- 4
pages_order/author/editor.vue View File

@ -52,8 +52,19 @@
onLoad({id, cid}) {
this.id = id
this.cid = cid || 0
if(this.cid){
this.getChapterDetail()
}
},
methods: {
getChapterDetail(){
this.$fetch('getBookCatalogDetail', {
id: this.cid
}).then(res => {
this.form.title = res.title
this.form.details = res.details
})
},
async onPublish(status) {
let data = {
@ -64,12 +75,10 @@
}
if(this.cid){
data.id = this.id
data.id = this.cid
}
if(status == 1){
data.status = status
}
data.status = status
await this.$fetch('saveOrUpdateCatalog', data)


+ 246
- 0
pages_order/comment/comments.vue View File

@ -0,0 +1,246 @@
<template>
<view class="comments-page">
<navbar title="评论详情" leftClick @leftClick="$utils.navigateBack" />
<!-- 书本名称 -->
<view class="book-title-area">
<view class="book-title-label">书本名称</view>
<view class="book-title">{{ bookTitle }}</view>
</view>
<!-- 主评论卡片 -->
<view class="comment-card" v-if="comment.hanHaiMember">
<view class="comment-header">
<image class="avatar" :src="comment.hanHaiMember.headImage" mode="aspectFill" />
<view class="user-info">
<text class="username">{{ comment.hanHaiMember.nickName }}</text>
</view>
</view>
<view class="comment-content">{{ comment.comment }}</view>
<view class="comment-footer">
<text class="comment-time">{{ comment.createTime }}</text>
</view>
</view>
<!-- 全部评论列表 -->
<view class="all-reply-area">
<view class="all-reply-header">回复 · {{ comment.children.length }}</view>
<view class="reply-list">
<view class="reply-item" v-for="(item, idx) in comment.children" :key="idx">
<image class="reply-avatar" :src="item.hanHaiMember.headImage" mode="aspectFill" />
<view class="reply-main">
<view class="reply-username">{{ item.hanHaiMember.nickName }}</view>
<view class="reply-content">{{ item.comment }}</view>
<view class="reply-time">{{ item.createTime }}</view>
</view>
</view>
<uv-empty mode="list" v-if="comment.children.length == 0"></uv-empty>
</view>
</view>
<!-- 底部回复按钮 -->
<view class="reply-footer">
<button class="submit-btn" @click="goToRespond">回复评论</button>
</view>
</view>
</template>
<script>
import navbar from '@/components/base/navbar.vue'
export default {
components: {
navbar
},
data() {
return {
bookTitle: '',
comment: {},
id : 0,
}
},
onLoad({id}) {
this.id = id
},
onShow() {
this.getData()
},
methods: {
async getData(){
this.comment = await this.$fetch('getCommentDetail', {
commentId : this.id,
})
this.bookTitle = this.comment.commonShop.name
},
goToRespond() {
uni.navigateTo({
url: '/pages_order/comment/respondComments?id=' + this.id
})
},
}
}
</script>
<style scoped lang="scss">
.comments-page {
min-height: 100vh;
background: #f8f8f8;
display: flex;
flex-direction: column;
}
.book-title-area {
background: #fff;
margin: 24rpx 24rpx 0 24rpx;
border-radius: 16rpx;
padding: 24rpx 24rpx 0 24rpx;
}
.book-title-label {
color: #bdbdbd;
font-size: 24rpx;
margin-bottom: 4rpx;
}
.book-title {
font-size: 28rpx;
color: #222;
margin-bottom: 16rpx;
border-bottom: 1px solid #ededed;
padding-bottom: 8rpx;
}
.comment-card {
background: #fff;
margin: 24rpx;
border-radius: 16rpx;
padding: 24rpx 24rpx 0 24rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.03);
margin-bottom: 0;
}
.comment-header {
display: flex;
align-items: center;
margin-bottom: 8rpx;
}
.avatar {
width: 56rpx;
height: 56rpx;
border-radius: 50%;
margin-right: 16rpx;
}
.user-info {
display: flex;
flex-direction: column;
}
.username {
font-size: 26rpx;
color: #222;
font-weight: 500;
}
.comment-content {
font-size: 26rpx;
color: #333;
margin-bottom: 12rpx;
}
.comment-footer {
display: flex;
align-items: center;
font-size: 22rpx;
color: #bdbdbd;
padding-bottom: 20rpx;
}
.comment-time {
color: #bdbdbd;
margin-top: 18rpx;
}
.all-reply-area {
background: #fff;
margin: 0 24rpx 0 24rpx;
border-radius: 16rpx;
padding: 24rpx 24rpx 16rpx 24rpx;
margin-top: 24rpx;
}
.all-reply-header {
color: #222;
font-size: 28rpx;
font-weight: 500;
margin-bottom: 16rpx;
}
.reply-list {
margin-top: 40rpx;
display: flex;
flex-direction: column;
gap: 50rpx;
}
.reply-item {
display: flex;
align-items: flex-start;
}
.reply-avatar {
width: 44rpx;
height: 44rpx;
border-radius: 50%;
margin-right: 16rpx;
flex-shrink: 0;
}
.reply-main {
flex: 1;
display: flex;
flex-direction: column;
}
.reply-username {
font-size: 24rpx;
color: #222;
font-weight: 500;
margin-bottom: 4rpx;
}
.reply-content {
font-size: 24rpx;
color: #333;
margin-bottom: 6rpx;
word-break: break-all;
}
.reply-time {
font-size: 20rpx;
color: #bdbdbd;
}
.reply-footer {
position: fixed;
left: 0;
right: 0;
bottom: 90rpx;
background: #fff;
padding: 24rpx 32rpx 32rpx 32rpx;
box-shadow: 0 -2rpx 12rpx rgba(0, 0, 0, 0.03);
z-index: 10;
}
.submit-btn {
width: 100%;
height: 80rpx;
background: #0a225f;
color: #fff;
font-size: 30rpx;
border-radius: 40rpx;
font-weight: 500;
letter-spacing: 2rpx;
}
</style>

+ 255
- 0
pages_order/comment/respondComments.vue View File

@ -0,0 +1,255 @@
<template>
<view class="respond-comments-page">
<!-- 顶部导航栏 -->
<navbar title="回复评论" leftClick @leftClick="$utils.navigateBack" />
<!-- 原评论展示 -->
<view class="origin-comment-card">
<!-- <view class="comment-header">
<image class="avatar" :src="comment.avatar" mode="aspectFill" />
<view class="user-info">
<text class="username">{{ comment.username }}</text>
</view>
</view>
<view class="comment-content">{{ comment.content }}</view>
<view class="comment-footer">
<text class="comment-time">{{ comment.time }}</text>
<text class="comment-reply-count">
<text class="emoji-icon">💬</text>
{{ comment.replyCount }}
</text>
</view> -->
<commentItem :item="comment" noClick/>
</view>
<!-- 回复输入区 -->
<view class="reply-area">
<view class="form-label-row">
<text class="required-star">*</text>
<text class="form-label">回复内容</text>
</view>
<textarea v-model="replyContent" class="review-textarea custom-placeholder full-textarea"
placeholder="请输入书评内容" />
</view>
<!-- 底部提交按钮 -->
<view class="reply-footer">
<button class="submit-btn" :disabled="!replyContent.trim()" @click="submitReview">发送</button>
</view>
</view>
</template>
<script>
import commentItem from '../components/comment/commentItem.vue'
export default {
components: {
commentItem,
},
data() {
return {
comment: {},
replyContent: '',
id : 0,
}
},
onLoad({id}) {
this.id = id
this.getData()
},
methods: {
async getData(){
this.comment = await this.$fetch('getCommentDetail', {
commentId : this.id,
})
},
async submitReview() {
if (!this.replyContent.trim()) {
uni.showToast({
title: '请输入回复内容',
icon: 'none'
})
return
}
await this.$fetch('replyComment', {
commentId : this.id,
content : this.replyContent,
})
uni.showToast({
title: '回复成功',
icon: 'none'
})
setTimeout(() => {
uni.navigateBack()
}, 1000)
}
}
}
</script>
<style scoped lang="scss">
.respond-comments-page {
min-height: 100vh;
background: #f8f8f8;
display: flex;
flex-direction: column;
}
.origin-comment-card {
background: #fff;
margin: 24rpx 24rpx 0 24rpx;
padding: 24rpx 24rpx 0 24rpx;
margin-bottom: 0;
border-radius: 0;
box-shadow: none;
padding-bottom: 0;
}
.comment-header {
display: flex;
align-items: center;
margin-bottom: 8rpx;
}
.avatar {
width: 56rpx;
height: 56rpx;
border-radius: 50%;
margin-right: 16rpx;
}
.user-info {
display: flex;
flex-direction: column;
}
.username {
font-size: 26rpx;
color: #222;
font-weight: 500;
}
.comment-content {
font-size: 26rpx;
color: #333;
margin-bottom: 12rpx;
}
.comment-footer {
display: flex;
align-items: center;
font-size: 22rpx;
color: #bdbdbd;
justify-content: space-between;
}
.comment-time {
color: #bdbdbd;
margin-top: 18rpx;
}
.comment-reply-count {
display: flex;
align-items: center;
font-size: 22rpx;
color: #bdbdbd;
line-height: 1;
}
.emoji-icon {
margin-right: 4rpx;
font-size: 22rpx;
line-height: 1;
display: inline-block;
vertical-align: middle;
}
.reply-area {
background: #fff;
margin: 0 24rpx 0 24rpx;
padding: 0 24rpx 24rpx 24rpx;
display: flex;
flex-direction: column;
border-radius: 0;
box-shadow: none;
margin-top: 0;
.review-textarea {
width: 100%;
min-height: 320rpx;
border: none;
background: transparent;
font-size: 28rpx;
color: #333;
resize: none;
outline: none;
margin-top: 20rpx;
}
.review-textarea.custom-placeholder::placeholder {
color: #d2d2d2;
font-size: 26rpx;
}
}
.form-label-row {
display: flex;
align-items: center;
margin-bottom: 8rpx;
margin-top: 50rpx;
}
.required-star {
color: #e23d3d;
font-size: 22rpx;
margin-right: 4rpx;
line-height: 1;
}
.form-label {
color: #222;
font-size: 26rpx;
font-weight: 400;
}
.reply-input {
margin-top: 12rpx;
border: none !important;
box-shadow: none !important;
}
.reply-footer {
position: fixed;
left: 0;
right: 0;
bottom: 90rpx;
background: #fff;
padding: 24rpx 32rpx 32rpx 32rpx;
box-shadow: 0 -2rpx 12rpx rgba(0, 0, 0, 0.03);
z-index: 10;
}
.submit-btn {
width: 100%;
height: 80rpx;
background: #0a225f !important;
color: #fff !important;
font-size: 30rpx;
border-radius: 40rpx;
font-weight: 500;
letter-spacing: 2rpx;
border: none;
box-shadow: none;
margin: 0 auto;
display: block;
text-align: center;
line-height: 80rpx;
}
.submit-btn:disabled {
background: #bdbdbd;
color: #fff;
}
</style>

+ 197
- 0
pages_order/comment/review.vue View File

@ -0,0 +1,197 @@
<template>
<view class="review-page">
<!-- 顶部导航栏 -->
<navbar title="写书评" leftClick @leftClick="$utils.navigateBack" />
<view class="review-content">
<view class="book-title-label">书本名称</view>
<view class="book-title">{{ bookTitle }}</view>
<view class="form-area flex-grow">
<view class="form-label-row">
<text class="required-star">*</text>
<text class="form-label">书评内容</text>
</view>
<textarea v-model="form.content" class="review-textarea custom-placeholder full-textarea"
placeholder="请输入书评内容" />
</view>
</view>
<view class="review-footer">
<view class="footer-divider"></view>
<button class="submit-btn" @click="submitReview">发布</button>
</view>
</view>
</template>
<script>
import navbar from '@/components/base/navbar.vue'
export default {
components: {
navbar
},
data() {
return {
bookTitle: '',
form: {
content: ''
},
id: 0
}
},
onLoad(options) {
//
this.bookTitle = options.title || '未知书名'
this.id = options.id || 0
this.getDateil()
},
methods: {
getDateil(){
this.$fetch('getBookDetail', {
id : this.id
}).then(res => {
this.bookTitle = res.name
})
},
async submitReview() {
if(this.$utils.verificationAll(this.form, {
content : '请输入书评内容'
})){
return
}
await this.$fetch('saveComment', {
bookId : this.id,
content : this.form.content,
})
uni.showToast({
title: '发布成功',
icon: 'none'
})
setTimeout(() => {
uni.navigateBack()
}, 1000)
}
}
}
</script>
<style scoped lang="scss">
.review-page {
min-height: 100vh;
background: #f7f7f7;
display: flex;
flex-direction: column;
}
.review-content {
background: #fff;
margin: 0 24rpx;
margin-top: 24rpx;
border-radius: 16rpx;
padding: 32rpx 24rpx 24rpx 24rpx;
display: flex;
flex-direction: column;
flex: 1;
padding-bottom: 140rpx;
.flex-grow {
flex: 1 1 0;
display: flex;
flex-direction: column;
min-height: 0;
}
.book-title-label {
color: #bdbdbd;
font-size: 24rpx;
margin-bottom: 4rpx;
}
.book-title {
font-size: 28rpx;
color: #222;
margin-bottom: 36rpx;
border-bottom: 1px solid #ededed;
padding-bottom: 8rpx;
}
.form-area {
margin-top: 18rpx;
}
.form-label-row {
display: flex;
align-items: center;
margin-bottom: 8rpx;
}
.required-star {
color: #e23d3d;
font-size: 22rpx;
margin-right: 4rpx;
line-height: 1;
}
.form-label {
color: #222;
font-size: 26rpx;
font-weight: 400;
}
.review-textarea {
width: 100%;
min-height: 320rpx;
border: none;
background: transparent;
font-size: 28rpx;
color: #333;
resize: none;
margin-top: 0;
outline: none;
}
.review-textarea.custom-placeholder::placeholder {
color: #d2d2d2;
font-size: 26rpx;
}
.full-textarea {
flex: 1;
min-height: 0;
max-height: none;
box-sizing: border-box;
margin-bottom: 0;
}
}
.review-footer {
position: fixed;
left: 0;
right: 0;
bottom: 90rpx;
background: #fff;
padding: 24rpx 32rpx 32rpx 32rpx;
box-shadow: 0 -2rpx 12rpx rgba(0, 0, 0, 0.03);
z-index: 10;
.footer-divider {
width: 100%;
height: 2rpx;
background: #f2f2f2;
margin-bottom: 24rpx;
}
.submit-btn {
width: 100%;
height: 80rpx;
background: #0a225f;
color: #fff;
font-size: 30rpx;
border-radius: 40rpx;
font-weight: 500;
letter-spacing: 2rpx;
}
}
</style>

+ 29
- 2
pages_order/components/comment/commentItem.vue View File

@ -1,5 +1,5 @@
<template>
<view class="comment-item">
<!-- <view class="comment-item">
<view class="comment-header">
<image class="avatar"
src="https://tse4-mm.cn.bing.net/th/id/OIP-C.iUyxJ_fxLjjX3kEBjteXWwAAAA?rs=1&pid=ImgDetMain"
@ -16,6 +16,25 @@
</view>
</view>
</view>
</view> -->
<view class="comment-item" @click="goToCommentReply">
<view class="comment-header">
<image class="avatar"
:src="item.hanHaiMember.headImage"
mode="aspectFill"></image>
<text class="username">{{ item.hanHaiMember.nickName }}</text>
</view>
<view class="comment-body">
<text class="content-text">{{ item.comment }}</text>
<view class="comment-footer">
<text class="comment-time">{{ item.createTime }}</text>
<view class="comment-likes">
<text class="like-icon">💬</text>
<text class="like-count">{{ item.children.length }}</text>
</view>
</view>
</view>
</view>
<!--
@ -37,13 +56,21 @@
<script>
export default {
props : ['item', 'noClick'],
data() {
return {
}
},
methods: {
goToCommentReply(){
if(this.noClick){
return
}
uni.navigateTo({
url: '/pages_order/comment/comments?id=' + this.item.id
})
},
}
}
</script>


+ 0
- 141
pages_order/components/novel/README.md View File

@ -1,141 +0,0 @@
# 小说阅读相关组件
本目录包含小说阅读页面使用的各种公共组件,所有组件都支持暗色主题模式。
## 组件列表
### 1. 订阅弹窗 (subscriptionPopup.vue)
中心弹出的订阅章节提示弹窗。
#### 使用方法
```html
<template>
<subscriptionPopup ref="subscriptionPopup" @subscribe="handleSubscribe"/>
</template>
<script>
import subscriptionPopup from 'pages_order/components/novel/subscriptionPopup.vue'
export default {
components: {
subscriptionPopup
},
methods: {
showSubscriptionPopup() {
this.$refs.subscriptionPopup.open()
},
handleSubscribe() {
// 处理订阅事件
}
}
}
</script>
```
### 2. 支付弹窗 (paymentPopup.vue)
从底部弹出的支付方式选择弹窗。
#### 使用方法
```html
<template>
<paymentPopup ref="paymentPopup" @subscribe="handleSubscribe" @close="handleClose"/>
</template>
<script>
import paymentPopup from 'pages_order/components/novel/paymentPopup.vue'
export default {
components: {
paymentPopup
},
methods: {
showPaymentPopup() {
this.$refs.paymentPopup.open()
},
handleSubscribe() {
// 处理订阅事件
},
handleClose() {
// 处理关闭事件
}
}
}
</script>
```
### 3. 章节目录弹窗 (chapterPopup.vue)
显示小说章节列表的弹窗。
#### 使用方法
```html
<template>
<chapterPopup ref="chapterPopup"/>
</template>
<script>
import chapterPopup from 'pages_order/components/novel/chapterPopup.vue'
export default {
components: {
chapterPopup
},
methods: {
showChapterList() {
this.$refs.chapterPopup.open()
}
}
}
</script>
```
### 4. 评分弹窗 (novelVotePopup.vue)
用于评价小说的弹窗组件。
## 暗色主题支持
所有组件都支持暗色主题模式,当系统切换到暗色模式或用户手动切换时,组件会自动应用暗色样式。
### 主题切换
主题的状态保存在Vuex中,可以通过以下方式切换:
```js
// 切换主题模式
this.$store.commit('toggleThemeMode')
// 设置为特定模式
this.$store.commit('setThemeMode', 'dark') // 或 'light'
// 获取当前主题模式
const isDarkMode = this.$store.getters.isDarkMode
```
### 使用主题混入器
为了方便在组件中使用主题相关功能,可以引入主题混入器:
```js
import themeMixin from '@/mixins/themeMode.js'
export default {
mixins: [themeMixin],
// 然后可以直接使用以下属性和方法
created() {
console.log(this.isDarkMode) // 是否为暗色模式
console.log(this.currentTheme) // 'light' 或 'dark'
// 切换主题
this.toggleThemeMode()
}
}
```
详细的文档请参考 `doc/dark-mode-guide.md`

+ 83
- 0
pages_order/components/novel/RankListItem.vue View File

@ -0,0 +1,83 @@
<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/book/dj.png" class="rank-num-img" />
<image class="avatar" src="@/pages_order/static/book/bd.png" mode="aspectFill" />
<view class="name">发源于</view>
</view>
<view class="rank-right">
<view class="score">200亲密值</view>
<view class="level">护书使者 五级</view>
</view>
</view>
</template>
<script>
export default {
name: 'RankListItem',
props: {
}
}
</script>
<style lang="scss" scoped>
.rank-item {
display: flex;
align-items: center;
justify-content: space-between;
background: linear-gradient(to bottom, #FBEBC5, #FDF9DD);
border-radius: 16rpx;
margin-bottom: 18rpx;
box-shadow: 0 2rpx 8rpx 0 rgba(184, 110, 59, 0.06);
padding: 24rpx;
.rank-left {
display: flex;
align-items: center;
.rank-icon,
.rank-num-img {
width: 60rpx;
height: 60rpx;
margin-right: 10rpx;
}
.avatar {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
margin-right: 14rpx;
object-fit: cover;
}
.name {
font-size: 26rpx;
color: #222;
font-weight: 500;
}
}
.rank-right {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 10rpx;
.score {
font-size: 22rpx;
color: #684427;
}
.level {
font-size: 20rpx;
color: #C28E00;
background: #FFE8A8;
border-radius: 8rpx;
padding: 2rpx 10rpx;
margin-top: 6rpx;
font-weight: 500;
}
}
}
</style>

+ 28
- 99
pages_order/components/novel/chapterPopup.vue View File

@ -11,13 +11,14 @@
</view>
<scroll-view scroll-y class="catalog-list">
<view v-for="(item, idx) in (orderAsc ? chapterList : [...chapterList].reverse())" :key="item.id"
@click="selectChapter(orderAsc ? idx : chapterList.length - 1 - idx)"
:class="['catalog-item theme-transition', {active: (orderAsc ? idx : chapterList.length - 1 - idx) === currentIndex}]">
@click="selectChapter(item, idx)"
: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" class="vip-tag theme-transition">付费</text>
<text v-if="item.vip == 'Y'" class="vip-tag theme-transition">付费</text>
</view>
</view>
<uv-empty mode="list" v-if="chapterList.length == 0"></uv-empty>
</scroll-view>
</view>
</uv-popup>
@ -28,103 +29,23 @@
import themeMixin from '@/mixins/themeMode.js'
export default {
props : {
'bookId' : {
default : 0
},
'currentIndex' : {
// default : 0
},
'chapterList' : {
default : []
}
},
mixins: [themeMixin],
data() {
return {
chapterCount: 2814,
orderAsc : true,
currentIndex : 0,
chapterList: [{
id: 1,
title: '第一章 重回2004',
vip: false
},
{
id: 2,
title: '第二章 陈年旧恨',
vip: false
},
{
id: 3,
title: '第三章 再相见',
vip: false
},
{
id: 4,
title: '第四章 李东的邀请',
vip: false
},
{
id: 5,
title: '第五章 小气的男',
vip: false
},
{
id: 6,
title: '第六章 先送谁?',
vip: false
},
{
id: 7,
title: '第七章 打听行情',
vip: false
},
{
id: 8,
title: '第八章 省城探路',
vip: false
},
{
id: 9,
title: '第九章 订货',
vip: false
},
{
id: 10,
title: '第十章 第一桶金',
vip: true
},
{
id: 11,
title: '第十一章 高富帅来袭',
vip: true
},
{
id: 12,
title: '第十二章 故学后,挥场见!',
vip: true
},
{
id: 13,
title: '第十三章 你来我往',
vip: true
},
{
id: 14,
title: '第十四章 你来我往',
vip: true
},
{
id: 15,
title: '第十五章 你来我往',
vip: true
},
{
id: 16,
title: '第十六章 你来我往',
vip: true
},
{
id: 17,
title: '第十七章 你来我往',
vip: true
},
{
id: 18,
title: '第十八章 你来我往',
vip: true
}
]
// chapterList: [],
}
},
computed: {
@ -138,14 +59,22 @@
methods: {
open() {
this.$refs.popup.open('bottom')
//
// this.$fetch('getBookCatalogList', {
// bookId : this.bookId,
// pageNo : 1,
// pageSize : 9999999,
// reverse : this.orderAsc ? 1 : 0,
// }).then(res => {
// this.chapterList = res.records
// })
},
close() {
this.$refs.popup.close()
},
selectChapter(idx) {
this.currentIndex = idx
this.showCatalog = false
// TODO:
selectChapter(item, index) {
this.close()
this.$emit('selectChapter', {item, index})
},
}
}


+ 56
- 45
pages_order/components/novel/novelVotePopup.vue View File

@ -8,18 +8,23 @@
<view class="vote-options">
<text class="option-label">推荐投票</text>
<view class="quick-options">
<view class="option-btn" @click="setVotes(1)">1</view>
<view class="option-btn" @click="setVotes(5)">5</view>
<view class="option-btn" @click="setVotes(10)">10</view>
<view class="option-btn"
v-for="item in voteList"
:key="item"
:class="{'active': voteCount == item}"
@click="setVotes(item)">{{ item }}</view>
</view>
<text class="option-label">手动设置</text>
<view class="manual-input">
<view class="minus-btn" @click="decreaseVotes">-</view>
<view class="vote-count">
<text>x{{voteCount}}</text>
</view>
<view class="plus-btn" @click="increaseVotes">+</view>
<uv-number-box
v-model="voteCount"
:min="1"
:max="maxVote"
integer
inputWidth="200rpx"
buttonSize="80rpx"
placeholder="请输入投票数" />
</view>
</view>
@ -32,16 +37,36 @@
export default {
data() {
return {
voteCount: 1,
id: 0,
voteList: [1, 5, 10],
maxVote : 0,
}
},
methods: {
open() {
setVotes(count) {
this.voteCount = Math.min(count, this.maxVote)
},
open(id) {
this.id = id
this.$refs.popup.open('bottom')
this.$fetch('getMyRecommendTicketNum')
.then(res => {
this.maxVote = res
})
},
close() {
this.$refs.popup.close()
},
submitVote() {
this.$fetch('vote', {
bookId: this.id,
num: this.voteCount
}).then(res => {
this.close()
this.$emit('updateVote')
})
}
}
}
</script>
@ -79,17 +104,20 @@
gap: 20rpx;
margin-top: 30rpx;
margin-bottom: 40rpx;
}
.option-btn {
flex: 1;
height: 80rpx;
background-color: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8rpx;
font-size: 28rpx;
.option-btn {
flex: 1;
height: 80rpx;
background-color: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8rpx;
font-size: 28rpx;
&.active{
background-color: $uni-color;
color: #fff;
}
}
}
.manual-input {
@ -98,35 +126,18 @@
align-items: center;
justify-content: center;
gap: 20rpx;
}
.minus-btn,
.plus-btn {
width: 220rpx;
height: 80rpx;
background-color: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8rpx;
font-size: 32rpx;
}
.vote-count {
width: 220rpx;
height: 82rpx;
background-color: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8rpx;
font-size: 28rpx;
::v-deep .uv-number-box__plus{
height: 200rpx !important;
}
::v-deep .uv-number-box__minus{
height: 200rpx !important;
}
}
.submit-btn {
width: 100%;
height: 88rpx;
background-color: #000033;
background-color: $uni-color;
color: #fff;
border-radius: 8rpx;
font-size: 32rpx;


+ 16
- 6
pages_order/novel/Giftbox.vue View File

@ -5,7 +5,19 @@
<!-- 礼物列表 -->
<view class="gift-list">
<view v-for="(gift, idx) in gifts" :key="gift.id"
<!-- <view v-for="(gift, idx) in gifts" :key="gift.id"
:class="['gift-item', { selected: idx === selectedIndex }]" @click="selectGift(idx)">
<view class="gift-img">
<text class="gift-emoji">{{ gift.emoji }}</text>
</view>
<view class="gift-name">{{ gift.name }}</view>
<view class="gift-info">
<text class="gift-count">x{{ gift.count }}</text>
<text class="gift-bean">{{ gift.price }}豆豆</text>
</view>
</view> -->
<view v-for="(gift, idx) in list" :key="gift.id"
:class="['gift-item', { selected: idx === selectedIndex }]" @click="selectGift(idx)">
<view class="gift-img">
<text class="gift-emoji">{{ gift.emoji }}</text>
@ -30,16 +42,14 @@
</template>
<script>
import BackArrow from './components/BackArrow.vue';
import mixinsList from '@/mixins/list.js'
export default {
mixins: [mixinsList],
components: {
'uv-navbar': () => import('@/uni_modules/uv-navbar/components/uv-navbar/uv-navbar.vue'),
'uv-number-box': () => import('@/uni_modules/uv-number-box/components/uv-number-box/uv-number-box.vue'),
BackArrow
},
data() {
return {
mixinsListApi : 'getInteractionGiftList',
gifts: [{
id: 1,
name: '小星星',


+ 47
- 16
pages_order/novel/ReaderAchievement.vue View File

@ -7,19 +7,24 @@
<view v-if="isPending" class="pending-tag">设置审核中</view>
</view>
<view class="achievement-list">
<view class="achievement-item">
<view class="achievement-item"
v-for="(item, index) in list"
:key="index">
<image class="badge-img"
src="https://tse3-mm.cn.bing.net/th/id/OIP-C.wUsFZgl70iE4tI7b_HKaKgHaHa?w=166&h=180&c=7&r=0&o=5&dpr=1.1&pid=1.7"
:src="item.image"
mode="aspectFill" />
<view class="input-area">
<view class="label-row">
<text class="required">*</text>
<text class="label">一级成就名称</text>
<text class="label">{{ item.title }}</text>
</view>
<input class="input" v-model="level1" placeholder="请输入" placeholder-class="input-placeholder" />
<input class="input"
v-model="item.levelName"
placeholder="请输入"
placeholder-class="input-placeholder" />
</view>
</view>
<view class="divider"></view>
<!-- <view class="divider"></view>
<view class="achievement-item">
<image class="badge-img"
src="https://tse3-mm.cn.bing.net/th/id/OIP-C.wUsFZgl70iE4tI7b_HKaKgHaHa?w=166&h=180&c=7&r=0&o=5&dpr=1.1&pid=1.7"
@ -44,7 +49,7 @@
</view>
<input class="input" v-model="level3" placeholder="请输入" placeholder-class="input-placeholder" />
</view>
</view>
</view> -->
</view>
</view>
<view class="bottom-btn-area">
@ -57,27 +62,53 @@
</template>
<script>
import mixinsList from '@/mixins/list.js'
export default {
mixins: [mixinsList],
data() {
return {
level1: '',
level2: '',
level3: '',
mixinsListApi : 'getAchievementList',
isPending: false
}
},
methods: {
submit() {
if (!this.level1 || !this.level2 || !this.level3) {
uni.showToast({
title: '请填写所有成就名称',
icon: 'none'
})
return
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
}
}
this.isPending = true
//
}
// this.isPending = true
let arr = []
for (var index = 0; index < this.list.length; index++) {
var element = this.list[index];
arr.push(this.$fetch('setAchievementName', {
name : element.levelName
}))
}
await Promise.all(arr)
uni.showToast({
title: '提交成功',
icon: 'none'
})
setTimeout(() => {
uni.navigateBack()
}, 1000)
},
}
}
</script>


+ 0
- 212
pages_order/novel/Respondcomments.vue View File

@ -1,212 +0,0 @@
<template>
<view class="respond-comments-page">
<!-- 顶部导航栏 -->
<navbar title="回复评论" leftClick @leftClick="$utils.navigateBack" />
<!-- 原评论展示 -->
<view class="origin-comment-card">
<view class="comment-header">
<image class="avatar" :src="comment.avatar" mode="aspectFill" />
<view class="user-info">
<text class="username">{{ comment.username }}</text>
</view>
</view>
<view class="comment-content">{{ comment.content }}</view>
<view class="comment-footer">
<text class="comment-time">{{ comment.time }}</text>
<text class="comment-reply-count">
<text class="emoji-icon">💬</text>
{{ comment.replyCount }}
</text>
</view>
</view>
<!-- 回复输入区 -->
<view class="reply-area">
<view class="form-label-row">
<text class="required-star">*</text>
<text class="form-label">回复内容</text>
</view>
<uv-input
v-model="replyContent"
type="text"
:maxlength="200"
placeholder="请输入回复内容"
border="surround"
clearable
class="reply-input"
/>
</view>
<!-- 底部提交按钮 -->
<view class="reply-footer">
<button class="submit-btn" :disabled="!replyContent.trim()" @click="submitReply">发送</button>
</view>
</view>
</template>
<script>
import navbar from '@/components/base/navbar.vue'
export default {
components: { navbar },
data() {
return {
comment: {
avatar: 'https://tse4-mm.cn.bing.net/th/id/OIP-C.iUyxJ_fxLjjX3kEBjteXWwAAAA?rs=1&pid=ImgDetMain',
username: '方香橙',
content: '我是本书的作者方香橙,这是一本甜文爽文哒!请放心入坑,五星好评!女主又美有个性可爱,绝对不圣母,不傻白!男主身心干净深情独宠媳妇儿一个人...',
time: '2024.07.09',
replyCount: 17
},
replyContent: ''
}
},
methods: {
goBack() {
uni.navigateBack()
},
submitReply() {
if (!this.replyContent.trim()) {
uni.showToast({ title: '请输入回复内容', icon: 'none' })
return
}
// API
uni.showToast({ title: '回复成功', icon: 'success' })
this.replyContent = ''
setTimeout(() => {
uni.navigateBack()
}, 1000)
}
}
}
</script>
<style scoped lang="scss">
.respond-comments-page {
min-height: 100vh;
background: #f8f8f8;
display: flex;
flex-direction: column;
}
.origin-comment-card {
background: #fff;
margin: 24rpx 24rpx 0 24rpx;
padding: 24rpx 24rpx 0 24rpx;
margin-bottom: 0;
border-radius: 0;
box-shadow: none;
padding-bottom: 0;
}
.comment-header {
display: flex;
align-items: center;
margin-bottom: 8rpx;
}
.avatar {
width: 56rpx;
height: 56rpx;
border-radius: 50%;
margin-right: 16rpx;
}
.user-info {
display: flex;
flex-direction: column;
}
.username {
font-size: 26rpx;
color: #222;
font-weight: 500;
}
.comment-content {
font-size: 26rpx;
color: #333;
margin-bottom: 12rpx;
}
.comment-footer {
display: flex;
align-items: center;
font-size: 22rpx;
color: #bdbdbd;
justify-content: space-between;
}
.comment-time {
color: #bdbdbd;
margin-top: 18rpx;
}
.comment-reply-count {
display: flex;
align-items: center;
font-size: 22rpx;
color: #bdbdbd;
line-height: 1;
}
.emoji-icon {
margin-right: 4rpx;
font-size: 22rpx;
line-height: 1;
display: inline-block;
vertical-align: middle;
}
.reply-area {
background: #fff;
margin: 0 24rpx 0 24rpx;
padding: 0 24rpx 24rpx 24rpx;
display: flex;
flex-direction: column;
border-radius: 0;
box-shadow: none;
margin-top: 0;
}
.form-label-row {
display: flex;
align-items: center;
margin-bottom: 8rpx;
margin-top: 50rpx;
}
.required-star {
color: #e23d3d;
font-size: 22rpx;
margin-right: 4rpx;
line-height: 1;
}
.form-label {
color: #222;
font-size: 26rpx;
font-weight: 400;
}
.reply-input {
margin-top: 12rpx;
border: none !important;
box-shadow: none !important;
}
.reply-footer {
position: fixed;
left: 0;
right: 0;
bottom: 90rpx;
background: #fff;
padding: 24rpx 32rpx 32rpx 32rpx;
box-shadow: 0 -2rpx 12rpx rgba(0,0,0,0.03);
z-index: 10;
}
.submit-btn {
width: 100%;
height: 80rpx;
background: #0a225f !important;
color: #fff !important;
font-size: 30rpx;
border-radius: 40rpx;
font-weight: 500;
letter-spacing: 2rpx;
border: none;
box-shadow: none;
margin: 0 auto;
display: block;
text-align: center;
line-height: 80rpx;
}
.submit-btn:disabled {
background: #bdbdbd;
color: #fff;
}
</style>

+ 0
- 189
pages_order/novel/Review.vue View File

@ -1,189 +0,0 @@
<template>
<view class="review-page">
<!-- 顶部导航栏 -->
<navbar title="写书评" leftClick @leftClick="$utils.navigateBack" />
<view class="review-content">
<view class="book-title-label">书本名称</view>
<view class="book-title">{{ bookTitle }}</view>
<view class="form-area flex-grow">
<view class="form-label-row">
<text class="required-star">*</text>
<text class="form-label">书评内容</text>
</view>
<textarea v-model="form.content" class="review-textarea custom-placeholder full-textarea"
placeholder="请输入书评内容" />
</view>
</view>
<view class="review-footer">
<view class="footer-divider"></view>
<button class="submit-btn" @click="submitReview">发布</button>
</view>
</view>
</template>
<script>
import navbar from '@/components/base/navbar.vue'
export default {
components: {
navbar
},
data() {
return {
bookTitle: '',
form: {
content: ''
},
rules: {
content: [{
required: true,
message: '请输入书评内容',
trigger: ['blur', 'change']
}]
}
}
},
onLoad(options) {
//
this.bookTitle = options.title || '未知书名'
},
methods: {
goBack() {
uni.navigateBack()
},
submitReview() {
this.$refs.reviewForm.validate().then(() => {
// API
uni.showToast({
title: '发布成功',
icon: 'success'
})
setTimeout(() => {
uni.navigateBack()
}, 1000)
}).catch(() => {})
}
}
}
</script>
<style scoped lang="scss">
.review-page {
min-height: 100vh;
background: #f8f8f8;
display: flex;
flex-direction: column;
margin-top: -50rpx;
}
.review-content {
background: #fff;
margin: 24rpx 24rpx 0 24rpx;
border-radius: 16rpx;
padding: 32rpx 24rpx 24rpx 24rpx;
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
padding-bottom: 140rpx;
margin-top: calc(var(--status-bar-height, 0px) + 100rpx);
}
.flex-grow {
flex: 1 1 0;
display: flex;
flex-direction: column;
min-height: 0;
}
.book-title-label {
color: #bdbdbd;
font-size: 24rpx;
margin-bottom: 4rpx;
}
.book-title {
font-size: 28rpx;
color: #222;
margin-bottom: 36rpx;
border-bottom: 1px solid #ededed;
padding-bottom: 8rpx;
}
.form-area {
margin-top: 18rpx;
}
.form-label-row {
display: flex;
align-items: center;
margin-bottom: 8rpx;
}
.required-star {
color: #e23d3d;
font-size: 22rpx;
margin-right: 4rpx;
line-height: 1;
}
.form-label {
color: #222;
font-size: 26rpx;
font-weight: 400;
}
.review-textarea {
width: 100%;
min-height: 320rpx;
border: none;
background: transparent;
font-size: 28rpx;
color: #333;
resize: none;
margin-top: 0;
outline: none;
}
.review-textarea.custom-placeholder::placeholder {
color: #d2d2d2;
font-size: 26rpx;
}
.full-textarea {
flex: 1;
min-height: 0;
max-height: none;
box-sizing: border-box;
margin-bottom: 0;
}
.review-footer {
position: fixed;
left: 0;
right: 0;
bottom: 90rpx;
background: #fff;
padding: 24rpx 32rpx 32rpx 32rpx;
box-shadow: 0 -2rpx 12rpx rgba(0, 0, 0, 0.03);
z-index: 10;
}
.footer-divider {
width: 100%;
height: 2rpx;
background: #f2f2f2;
margin-bottom: 24rpx;
}
.submit-btn {
width: 100%;
height: 80rpx;
background: #0a225f;
color: #fff;
font-size: 30rpx;
border-radius: 40rpx;
font-weight: 500;
letter-spacing: 2rpx;
}
</style>

+ 234
- 271
pages_order/novel/Tipping.vue View File

@ -1,279 +1,242 @@
<template>
<view class="tipping-page">
<!-- 顶部导航栏 -->
<navbar title="读者亲密值榜单" leftClick @leftClick="$utils.navigateBack" />
<!-- 榜单前三名 -->
<view class="top-three">
<view class="top-item second">
<image class="avatar" :src="topList[1].avatar" mode="aspectFill" />
<view class="name">{{ topList[1].name }}</view>
<view class="score">{{ topList[1].score }} 亲密值</view>
<view class="level">护书使者 五级</view>
</view>
<view class="top-item first">
<image class="avatar" :src="topList[0].avatar" mode="aspectFill" />
<view class="name">{{ topList[0].name }}</view>
<view class="score">{{ topList[0].score }} 亲密值</view>
<view class="level">护书使者 五级</view>
</view>
<view class="top-item third">
<image class="avatar" :src="topList[2].avatar" mode="aspectFill" />
<view class="name">{{ topList[2].name }}</view>
<view class="score">{{ topList[2].score }} 亲密值</view>
<view class="level">护书使者 五级</view>
</view>
</view>
<!-- 榜单列表 -->
<view class="rank-list">
<RankListItem v-for="(item, idx) in rankList" :key="item.id" :rankIcon="idx < 3 ? rankIcons[idx] : ''"
:rankNumImg="idx >= 3 ? ('/static/rank-num-' + (idx+1) + '.png') : ''" medal="/static/medal.png"
:avatar="item.avatar" :name="item.name" :score="item.score" level="护书使者 五级" />
</view>
<!-- 底部按钮 -->
<view class="bottom-btn-area">
<button class="tipping-btn">互动打赏</button>
</view>
</view>
<view class="tipping-page">
<!-- 顶部导航栏 -->
<navbar title="读者亲密值榜单" bgColor="#ac4b2c" color="#fff" leftClick @leftClick="$utils.navigateBack" />
<!-- 榜单前三名 -->
<view class="top-three">
<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>
</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="score">{{ topList[1].num }} 亲密值</view>
<view class="level">护书使者 四级</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>
</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="score">{{ topList[0].num }} 亲密值</view>
<view class="level">护书使者 五级</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>
</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>
</view>
<!-- 榜单列表 -->
<view class="rank-list"
v-if="topList.length > 3">
<RankListItem v-for="(item, idx) in topList"
v-if="idx > 2"
:key="item.id" />
</view>
<!-- 底部按钮 -->
<view class="bottom-btn-area">
<button class="tipping-btn"
@click="$utils.navigateTo('pages_order/novel/Giftbox?id=' + bookId)"
>互动打赏</button>
</view>
</view>
</template>
<script>
import RankListItem from '@/components/novel/RankListItem.vue'
export default {
components: {
RankListItem
},
data() {
return {
topList: [{
avatar: 'https://tse4-mm.cn.bing.net/th/id/OIP-C.iUyxJ_fxLjjX3kEBjteXWwAAAA?rs=1&pid=ImgDetMain',
name: '周海',
score: 6785452
},
{
avatar: 'https://tse4-mm.cn.bing.net/th/id/OIP-C.iUyxJ_fxLjjX3kEBjteXWwAAAA?rs=1&pid=ImgDetMain',
name: '冯冉冉',
score: 6785452
},
{
avatar: 'https://tse4-mm.cn.bing.net/th/id/OIP-C.iUyxJ_fxLjjX3kEBjteXWwAAAA?rs=1&pid=ImgDetMain',
name: '南静',
score: 6785452
}
],
rankList: [{
id: 4,
avatar: 'https://tse4-mm.cn.bing.net/th/id/OIP-C.iUyxJ_fxLjjX3kEBjteXWwAAAA?rs=1&pid=ImgDetMain',
name: '钱胡胡',
score: 5325324
},
{
id: 5,
avatar: 'https://tse4-mm.cn.bing.net/th/id/OIP-C.iUyxJ_fxLjjX3kEBjteXWwAAAA?rs=1&pid=ImgDetMain',
name: '冯艺瑄',
score: 4819704
},
{
id: 6,
avatar: 'https://tse4-mm.cn.bing.net/th/id/OIP-C.iUyxJ_fxLjjX3kEBjteXWwAAAA?rs=1&pid=ImgDetMain',
name: '王凡宏',
score: 4696874
},
{
id: 7,
avatar: 'https://tse4-mm.cn.bing.net/th/id/OIP-C.iUyxJ_fxLjjX3kEBjteXWwAAAA?rs=1&pid=ImgDetMain',
name: '辛书萍',
score: 3722953
},
{
id: 8,
avatar: 'https://tse4-mm.cn.bing.net/th/id/OIP-C.iUyxJ_fxLjjX3kEBjteXWwAAAA?rs=1&pid=ImgDetMain',
name: '李婷',
score: 2872476
},
{
id: 9,
avatar: 'https://tse4-mm.cn.bing.net/th/id/OIP-C.iUyxJ_fxLjjX3kEBjteXWwAAAA?rs=1&pid=ImgDetMain',
name: '郑盈',
score: 2464869
},
{
id: 10,
avatar: 'https://tse4-mm.cn.bing.net/th/id/OIP-C.iUyxJ_fxLjjX3kEBjteXWwAAAA?rs=1&pid=ImgDetMain',
name: '吴承联',
score: 990238
}
],
rankIcons: [
'https://img.yzcdn.cn/vant/rank-1.png',
'https://img.yzcdn.cn/vant/rank-2.png',
'https://img.yzcdn.cn/vant/rank-3.png'
]
}
}
}
import RankListItem from '../components/novel/RankListItem.vue'
export default {
components: {
RankListItem
},
data() {
return {
topList: [],
rankList: [],
bookId: 0,
}
},
onLoad(options) {
this.bookId = options.id;
},
onShow() {
this.getTopList();
},
methods: {
getTopList() {
this.$fetch('getIntimacyRankList', {
bookId: this.bookId,
pageNo: 1,
pageSize: 10
}).then(res => {
this.topList = res.records;
});
}
}
}
</script>
<style lang="scss" scoped>
.tipping-page {
min-height: 100vh;
background: linear-gradient(180deg, #b86e3b 0%, #e6b07c 100%);
padding-bottom: 40rpx;
}
.top-three {
display: flex;
justify-content: center;
align-items: flex-end;
margin: 40rpx 0 20rpx 0;
.top-item {
display: flex;
flex-direction: column;
align-items: center;
background: #fff7e0;
border-radius: 20rpx;
margin: 0 16rpx;
padding: 24rpx 18rpx 18rpx 18rpx;
box-shadow: 0 4rpx 16rpx 0 rgba(0, 0, 0, 0.08);
position: relative;
width: 180rpx;
.avatar {
width: 90rpx;
height: 90rpx;
border-radius: 50%;
border: 4rpx solid #ffd700;
margin-bottom: 10rpx;
}
.name {
font-size: 28rpx;
font-weight: bold;
color: #b86e3b;
margin-bottom: 6rpx;
}
.score {
font-size: 24rpx;
color: #e6b07c;
margin-bottom: 4rpx;
}
.level {
font-size: 22rpx;
color: #b86e3b;
background: #ffe7b2;
border-radius: 10rpx;
padding: 2rpx 12rpx;
}
}
.first {
transform: scale(1.15);
z-index: 2;
background: #fffbe6;
box-shadow: 0 8rpx 24rpx 0 rgba(255, 215, 0, 0.18);
}
.second,
.third {
z-index: 1;
opacity: 0.95;
}
}
.rank-list {
background: transparent;
margin: 0 24rpx;
margin-top: 20rpx;
.rank-item {
display: flex;
align-items: center;
justify-content: space-between;
background: #fffbe6;
border-radius: 16rpx;
margin-bottom: 18rpx;
box-shadow: 0 2rpx 8rpx 0 rgba(184, 110, 59, 0.06);
padding: 0 24rpx;
height: 100rpx;
.rank-left {
display: flex;
align-items: center;
.rank-icon,
.rank-num-img {
width: 38rpx;
height: 38rpx;
margin-right: 10rpx;
}
.medal {
width: 44rpx;
height: 44rpx;
margin-right: 10rpx;
}
.avatar {
width: 44rpx;
height: 44rpx;
border-radius: 50%;
margin-right: 14rpx;
border: 2rpx solid #ffd700;
object-fit: cover;
}
.name {
font-size: 26rpx;
color: #222;
font-weight: 500;
}
}
.rank-right {
display: flex;
flex-direction: column;
align-items: flex-end;
.score {
font-size: 22rpx;
color: #b86e3b;
}
.level {
font-size: 20rpx;
color: #fff;
background: #e6b07c;
border-radius: 8rpx;
padding: 2rpx 10rpx;
margin-top: 6rpx;
font-weight: 500;
}
}
}
}
.bottom-btn-area {
margin: 40rpx 24rpx 90rpx 24rpx;
display: flex;
flex-direction: column;
gap: 24rpx;
.tipping-btn {
width: 100%;
height: 80rpx;
background: #fffbe6;
color: #b86e3b;
font-size: 32rpx;
border-radius: 40rpx;
font-weight: bold;
letter-spacing: 2rpx;
box-shadow: 0 4rpx 16rpx 0 rgba(184, 110, 59, 0.12);
border: none;
}
}
.tipping-page {
min-height: 100vh;
// background: linear-gradient(180deg, #b86e3b 0%, #e6b07c 100%);
background-color: #ac4b2c;
padding-bottom: 40rpx;
}
.top-three {
display: flex;
justify-content: center;
align-items: flex-end;
margin: 80rpx 0 200rpx 0;
padding: 0 30rpx;
gap: 20rpx;
.top-item {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
border-top-left-radius: 110rpx;
border-top-right-radius: 110rpx;
gap: 10rpx;
padding-bottom: 20rpx;
&.first {
z-index: 3;
margin-top: -50rpx;
background: linear-gradient(to bottom, #F8DA87, #F8DA8700);
border: 1px solid #fff;
}
&.second {
z-index: 2;
background: linear-gradient(to bottom, #C2C9CD, #C2C9CD00);
border: 1px solid #fff;
top: 80rpx;
}
&.third {
z-index: 2;
background: linear-gradient(to bottom, #834941, #83494100);
border: 1px solid #fff;
top: 80rpx;
}
.avatar-frame {
position: relative;
width: 220rpx;
height: 220rpx;
margin-top: -50rpx;
.frame-img {
position: absolute;
width: 120%;
height: 120%;
z-index: 2;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.avatar {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 80%;
height: 80%;
border-radius: 50%;
z-index: 1;
}
}
.badge {
width: 100rpx;
height: 100rpx;
z-index: 3;
.badge-img {
width: 100%;
height: 100%;
}
}
.name {
margin-top: 10rpx;
font-size: 36rpx;
color: #FFFFFF;
font-weight: bold;
}
.score {
font-size: 28rpx;
color: #FFFFFF;
margin-top: 6rpx;
}
.level {
position: absolute;
bottom: -40rpx;
width: 102%;
text-align: center;
padding: 6rpx 20rpx;
background-color: #FFCC33;
border-radius: 6rpx;
font-size: 26rpx;
color: #8B4513;
font-weight: bold;
box-sizing: border-box;
}
}
}
.rank-list {
background: #FAE5BE;
margin: 0 24rpx;
padding: 20rpx;
border-radius: 20rpx;
display: flex;
flex-direction: column;
gap: 20rpx;
}
.bottom-btn-area {
margin: 40rpx 24rpx 90rpx 24rpx;
display: flex;
flex-direction: column;
gap: 24rpx;
.tipping-btn {
width: 100%;
height: 80rpx;
background: #fffbe6;
color: #b86e3b;
font-size: 32rpx;
border-radius: 40rpx;
font-weight: bold;
letter-spacing: 2rpx;
box-shadow: 0 4rpx 16rpx 0 rgba(184, 110, 59, 0.12);
border: none;
}
}
</style>

+ 105
- 24
pages_order/novel/Translation.vue View File

@ -1,13 +1,13 @@
<template>
<view class="page-container">
<navbar title="任务中心" leftClick @leftClick="$utils.navigateBack" />
<view class="task-center">
<navbar title="回复评论" leftClick @leftClick="$utils.navigateBack" />
<view class="navbar-placeholder"></view>
<!-- 账户剩余 -->
<view class="account-balance">
<view class="balance-label">账户剩余</view>
<view class="balance-value"><text class="num">9</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">
@ -16,32 +16,39 @@
<view class="record-btn">打卡记录</view>
</view>
<view class="checkin-grid">
<view v-for="day in 8" :key="day" class="checkin-day" :class="{ active: day <= checkedDays }">
<view class="day-label" :class="{ bold: day <= checkedDays }">{{ day }}</view>
<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="https://tse1-mm.cn.bing.net/th/id/OIP-C.pca_tFb6ZjyDNdQYgFvi0wHaE7?w=219&h=180&c=7&r=0&o=5&dpr=1.1&pid=1.7"
:src="day.image"
mode="aspectFit" />
<view class="ticket-num">+1</view>
<view class="ticket-num">+ {{ day.num }}</view>
</view>
</view>
<button class="checkin-btn" :class="{ 'checked-btn': isChecked }" :disabled="isChecked"
@click="handleCheckin">
{{ isChecked ? '已签到' : '签到得奖励' }}
<button class="checkin-btn" :class="{ 'checked-btn': isSign }" :disabled="isSign"
@click="clickSignTask">
{{ isSign ? '已签到' : '签到得奖励' }}
</button>
</view>
<!-- 更多任务 -->
<view class="more-tasks">
<view class="more-header">更多任务</view>
<view class="task-list">
<view class="task-item" v-for="(task, idx) in tasks" :key="idx"
<view class="task-item" v-for="(task, idx) in list" :key="idx"
:class="{ 'no-border': idx === tasks.length - 1 }">
<view class="task-info">
<view class="task-title">{{ task.title }}</view>
<view class="task-desc">推荐票 +1</view>
<view class="task-desc">推荐票 +{{ task.num }}</view>
</view>
<button class="get-btn" :class="{ 'received-btn': task.received }" :disabled="task.received"
@click="handleReceive(idx)">
{{ task.received ? '已领取' : '去领取' }}
<button class="get-btn" :class="{ 'received-btn': task.commonTaskLog }"
:disabled="task.commonTaskLog"
@click="clickMoreTask(task.id)">
{{ task.commonTaskLog ? '已领取' : '去领取' }}
</button>
</view>
</view>
@ -51,14 +58,10 @@
</template>
<script>
import uvNavbar from '@/uni_modules/uv-navbar/components/uv-navbar/uv-navbar.vue'
import uvIcon from '@/uni_modules/uv-icon/components/uv-icon/uv-icon.vue'
import BackArrow from './components/BackArrow.vue'
import mixinsList from '@/mixins/list.js'
export default {
mixins: [mixinsList],
components: {
uvNavbar,
uvIcon,
BackArrow
},
data() {
return {
@ -76,12 +79,90 @@
received: false
},
],
clockList : [],
isChecked: false, //
maxVote : 0,
mixinsListApi : '',
}
},
computed : {
isSign(){
let taskId = 0
this.clockList.forEach(n => {
if(!n.commonSignLog){
taskId = n.id
}
})
return !taskId
},
},
onShow() {
this.getMyRecommendTicketNum()
this.getSignTaskList()
this.getMoreTaskList()
},
methods: {
goBack() {
uni.navigateBack();
getMyRecommendTicketNum(){
this.$fetch('getMyRecommendTicketNum')
.then(res => {
this.maxVote = res
})
},
getSignTaskList(){
this.$fetch('getSignTaskList', {
token : uni.getStorageSync('token')
})
.then(res => {
this.clockList = res
})
},
getMoreTaskList(){
this.$fetch('getMoreTaskList', {
token : uni.getStorageSync('token')
})
.then(res => {
this.list = res
})
},
async clickSignTask(){
let taskId = 0
for (var index = 0; index < this.clockList.length; index++) {
var element = this.clockList[index];
if(!element.commonSignLog){
taskId = element.id
break
}
}
if(!taskId){
uni.showToast({
title: '已全部签到',
icon: 'none'
})
return
}
await this.$fetch('clickSignTask', {
taskId,
})
uni.showToast({
title: '签到成功',
icon: 'none'
});
this.getSignTaskList()
this.getMyRecommendTicketNum()
},
async clickMoreTask(taskId){
await this.$fetch('clickMoreTask', {
taskId,
})
uni.showToast({
title: '领取成功',
icon: 'none'
});
this.getMoreTaskList()
this.getMyRecommendTicketNum()
},
handleCheckin() {
if (this.checkedDays < 8) {
@ -89,7 +170,7 @@
this.isChecked = true;
uni.showToast({
title: '签到成功',
icon: 'success'
icon: 'none'
});
} else {
this.isChecked = true;


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

@ -54,11 +54,8 @@
</template>
<script>
import BackArrow from './components/BackArrow.vue';
export default {
components: {
BackArrow,
},
data() {
return {


+ 0
- 166
pages_order/novel/chapterList.vue View File

@ -1,166 +0,0 @@
<template>
<view class="chapter-container">
<navbar title="章节列表" leftClick @leftClick="$utils.navigateBack"/>
<view class="tabs">
<uv-tabs :list="tabs"
:activeStyle="{color : '#0A2463', fontWeight : 600}"
lineColor="#0A2463"
:inactiveStyle="{color: '#0A2463'}"
lineHeight="8rpx"
lineWidth="50rpx"
:scrollable="false"
:current="activeTab"
@click="clickTabs"></uv-tabs>
</view>
<view class="chapter-list">
<view class="chapter-item" v-for="(chapter, index) in chapters" :key="index" @click="editChapter(chapter)">
<view class="chapter-info">
<text class="chapter-title">章节名</text>
<text class="chapter-number">{{index + 1}}</text>
</view>
<uv-icon name="arrow-right" color="#999" size="28"></uv-icon>
</view>
</view>
<view class="bottom-actions">
<button class="btn-settings" @click="handleSettings">设置作品</button>
<button class="btn-new" @click="addNewChapter">新建章节</button>
</view>
</view>
</template>
<script>
import mixinsList from '@/mixins/list.js'
export default {
mixins: [mixinsList],
components: {
},
data() {
return {
tabs : [
{
name : '已发布',
},
{
name : '草稿箱',
}
],
activeTab: 0,
mixinsListApi : 'getBookCatalogList',
id : 0,
}
},
onLoad(options) {
// tabtab
if (options.activeTab) {
this.activeTab = options.activeTab;
}
if(options.id){
this.queryParams.bookId = options.id
this.id = options.id
this.queryParams.status = this.activeTab
}
},
methods: {
clickTabs(tab) {
this.activeTab = tab.index;
this.queryParams.status = this.activeTab
this.getData()
},
addNewChapter() {
uni.navigateTo({
url: '/pages_order/author/editor?id=' + this.id
})
},
editChapter(chapter) {
uni.navigateTo({
url: '/pages_order/author/editor?cid=' + chapter.id + '&id=' + this.id
})
},
handleSettings() {
uni.navigateTo({
url: '/pages_order/novel/createNovel?type=edit'
})
}
}
}
</script>
<style lang="scss" scoped>
.chapter-container {
min-height: 100vh;
background-color: #fff;
padding-bottom: 70px;
.chapter-list {
padding: 20rpx 40rpx;
background-color: rgb(255, 254, 254);
margin: 50rpx 40rpx;
border-radius: 3%;
.chapter-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 0;
border-bottom: 1px solid #eee;
.chapter-info {
.chapter-title {
font-size: 14px;
color: #999;
margin-bottom: 5px;
display: block;
}
.chapter-number {
font-size: 16px;
color: #333;
display: block;
}
}
.icon-arrow {
color: #999;
font-size: 16px;
}
}
}
.bottom-actions {
padding-bottom: calc(env(safe-area-inset-bottom) + 30rpx);
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
padding: 10px 15px;
background: #fff;
box-shadow: 0 -2px 6px rgba(0, 0, 0, 0.05);
button {
flex: 1;
height: 40px;
border-radius: 20px;
font-size: 16px;
margin: 0 5px;
&.btn-settings {
background: #fff;
border: 1px solid #ddd;
color: #333;
}
&.btn-new {
background: #2b4acb;
color: #fff;
border: none;
}
}
}
}
</style>

+ 0
- 250
pages_order/novel/comments.vue View File

@ -1,250 +0,0 @@
<template>
<view class="comments-page">
<navbar title="评论详情" leftClick @leftClick="$utils.navigateBack" />
<!-- 书本名称 -->
<view class="book-title-area">
<view class="book-title-label">书本名称</view>
<view class="book-title">{{ bookTitle }}</view>
</view>
<!-- 主评论卡片 -->
<view class="comment-card">
<view class="comment-header">
<image class="avatar" :src="comment.avatar" mode="aspectFill" />
<view class="user-info">
<text class="username">{{ comment.username }}</text>
</view>
</view>
<view class="comment-content">{{ comment.content }}</view>
<view class="comment-footer">
<text class="comment-time">{{ comment.time }}</text>
</view>
</view>
<!-- 全部评论列表 -->
<view class="all-reply-area">
<view class="all-reply-header">全部评论·{{ comment.replyCount }}</view>
<view class="reply-list">
<view class="reply-item" v-for="(item, idx) in replies" :key="idx">
<image class="reply-avatar" :src="item.avatar" mode="aspectFill" />
<view class="reply-main">
<view class="reply-username">{{ item.username }}</view>
<view class="reply-content">{{ item.content }}</view>
<view class="reply-time">{{ item.time }}</view>
</view>
</view>
</view>
</view>
<!-- 底部回复按钮 -->
<view class="reply-footer">
<button class="submit-btn" @click="goToRespond">回复评论</button>
</view>
</view>
</template>
<script>
import navbar from '@/components/base/navbar.vue'
export default {
components: { navbar },
data() {
return {
bookTitle: '这游戏也太真实了',
comment: {
avatar: 'https://tse4-mm.cn.bing.net/th/id/OIP-C.iUyxJ_fxLjjX3kEBjteXWwAAAA?rs=1&pid=ImgDetMain',
username: '方香橙',
content: '我是本书的作者方香橙,这是一本甜文爽文哒!请放心入坑,五星好评!女主又美有个性可爱,绝对不圣母,不傻白!男主身心干净深情独宠媳妇儿一个人...',
time: '2024.07.09',
replyCount: 17
},
replies: [
{
avatar: 'https://tse4-mm.cn.bing.net/th/id/OIP-C.iUyxJ_fxLjjX3kEBjteXWwAAAA?rs=1&pid=ImgDetMain',
username: '方香橙',
content: '我是本书的作者方香橙,这是一本甜文爽文哒!请放心入坑,五星好评!女主又美有个性可爱,绝对不圣母,不傻白!男主身心干净深情独宠媳妇儿一个人...',
time: '2024.07.09'
},
{
avatar: 'https://tse4-mm.cn.bing.net/th/id/OIP-C.iUyxJ_fxLjjX3kEBjteXWwAAAA?rs=1&pid=ImgDetMain',
username: '战斗世界',
content: '这本书打破了我看甜文套路之前的观念女主真的太可爱太有趣和以往看过的一个NPC介绍有很大不同',
time: '2024.07.09'
}
]
}
},
methods: {
goToRespond() {
uni.navigateTo({ url: '/pages_order/novel/Respondcomments' })
},
submitReply() {
uni.showToast({ title: '功能开发中', icon: 'none' })
}
}
}
</script>
<style scoped lang="scss">
.comments-page {
min-height: 100vh;
background: #f8f8f8;
display: flex;
flex-direction: column;
}
.book-title-area {
background: #fff;
margin: 24rpx 24rpx 0 24rpx;
border-radius: 16rpx;
padding: 24rpx 24rpx 0 24rpx;
}
.book-title-label {
color: #bdbdbd;
font-size: 24rpx;
margin-bottom: 4rpx;
}
.book-title {
font-size: 28rpx;
color: #222;
margin-bottom: 16rpx;
border-bottom: 1px solid #ededed;
padding-bottom: 8rpx;
}
.comment-card {
background: #fff;
margin: 0 24rpx 0 24rpx;
border-radius: 16rpx;
padding: 24rpx 24rpx 0 24rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.03);
margin-bottom: 0;
}
.comment-header {
display: flex;
align-items: center;
margin-bottom: 8rpx;
}
.avatar {
width: 56rpx;
height: 56rpx;
border-radius: 50%;
margin-right: 16rpx;
}
.user-info {
display: flex;
flex-direction: column;
}
.username {
font-size: 26rpx;
color: #222;
font-weight: 500;
}
.comment-content {
font-size: 26rpx;
color: #333;
margin-bottom: 12rpx;
}
.comment-footer {
display: flex;
align-items: center;
font-size: 22rpx;
color: #bdbdbd;
}
.comment-time {
color: #bdbdbd;
margin-top: 18rpx;
}
.all-reply-area {
background: #fff;
margin: 0 24rpx 0 24rpx;
border-radius: 16rpx;
padding: 24rpx 24rpx 16rpx 24rpx;
margin-top: 24rpx;
}
.all-reply-header {
color: #222;
font-size: 28rpx;
font-weight: 500;
margin-bottom: 16rpx;
}
.reply-list {
margin-top: 40rpx;
display: flex;
flex-direction: column;
gap: 50rpx;
}
.reply-item {
display: flex;
align-items: flex-start;
}
.reply-avatar {
width: 44rpx;
height: 44rpx;
border-radius: 50%;
margin-right: 16rpx;
flex-shrink: 0;
}
.reply-main {
flex: 1;
display: flex;
flex-direction: column;
}
.reply-username {
font-size: 24rpx;
color: #222;
font-weight: 500;
margin-bottom: 4rpx;
}
.reply-content {
font-size: 24rpx;
color: #333;
margin-bottom: 6rpx;
word-break: break-all;
}
.reply-time {
font-size: 20rpx;
color: #bdbdbd;
}
.reply-footer {
position: fixed;
left: 0;
right: 0;
bottom: 90rpx;
background: #fff;
padding: 24rpx 32rpx 32rpx 32rpx;
box-shadow: 0 -2rpx 12rpx rgba(0, 0, 0, 0.03);
z-index: 10;
}
.submit-btn {
width: 100%;
height: 80rpx;
background: #0a225f;
color: #fff;
font-size: 30rpx;
border-radius: 40rpx;
font-weight: 500;
letter-spacing: 2rpx;
}
</style>

+ 0
- 39
pages_order/novel/components/AgreementCheck.vue View File

@ -1,39 +0,0 @@
<template>
<div class="agreement-check">
<input type="checkbox" v-model="checked" @change="$emit('update:checked', checked)" id="agreement" />
<label for="agreement">
<slot />
</label>
</div>
</template>
<script>
export default {
name: 'AgreementCheck',
props: {
checked: {
type: Boolean,
default: false
}
}
}
</script>
<style scoped>
.agreement-check {
display: flex;
align-items: center;
font-size: 14px;
color: #b3b3b3;
margin-top: 16px;
}
.agreement-check input[type='checkbox'] {
margin-right: 6px;
accent-color: #183b6b;
}
.agreement-check a {
color: #183b6b;
text-decoration: underline;
margin: 0 2px;
}
</style>

+ 0
- 28
pages_order/novel/components/BackArrow.vue View File

@ -1,28 +0,0 @@
<template>
<uv-icon name="arrow-left" :size="44" :color="color" @click="goBack" />
</template>
<script>
export default {
name: 'BackArrow',
props: {
size: {
type: [String, Number],
default: 56
},
color: {
type: String,
default: '#333'
}
},
methods: {
goBack() {
this.$emit('back');
if (this.$listeners.back) return;
if (typeof uni !== 'undefined' && uni.navigateBack) {
uni.navigateBack();
}
}
}
}
</script>

+ 0
- 45
pages_order/novel/components/LoginButton.vue View File

@ -1,45 +0,0 @@
<template>
<button :class="['login-btn', type]" @click="$emit('click')">
<slot />
</button>
</template>
<script>
export default {
name: 'LoginButton',
props: {
type: {
type: String,
default: ''
}
}
}
</script>
<style scoped>
.login-btn {
width: 80%;
height: 44px;
border-radius: 22px;
font-size: 18px;
font-weight: 500;
margin: 12px 0;
outline: none;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s;
}
.login-btn.primary {
background: #183b6b;
color: #fff;
}
.login-btn.secondary {
background: #fff;
color: #183b6b;
border: 2px solid #183b6b;
position: relative;
}
</style>

+ 190
- 142
pages_order/novel/novelDetail.vue View File

@ -19,16 +19,16 @@
<text class="label">完结</text>
<text class="status">{{ novelData.status }}</text>
</view> -->
<view class="content-row">
<view class="book-status">
<text>{{novelData.status}}</text>
</view>
<view class="book-text">
{{ novelData.service }}
</view>
<view class="book-status">
<text>{{novelData.status}}</text>
</view>
<view class="book-text">
{{ novelData.service }}
</view>
</view>
<view class="score-line">
<text class="score">{{ novelData.qmNum || 0}}</text>
<text class="score-label">作者累计亲密度值</text>
@ -39,12 +39,12 @@
<!-- 推荐票数显示 -->
<view class="recommendation-section">
<view class="rec-left">
<text class="rec-count">{{ novelData.tuiNum }}</text>
<text class="rec-count">{{ novelData.tuiNum || 0 }}</text>
<text class="rec-label">推荐票数</text>
</view>
<view class="rec-divider"></view>
<view class="rec-right">
<button class="recommend-btn" @click="$refs.novelVotePopup.open()">
<button class="recommend-btn" @click="$refs.novelVotePopup.open(id)">
<text class="btn-icon">📑</text>
投推荐票
</button>
@ -66,20 +66,18 @@
<text>我的等级</text>
</view>
<view class="level-info">
<image class="user-avatar"
src="/pages_order/static/book/dj.png"
mode="aspectFill"></image>
<image class="user-avatar" src="/pages_order/static/book/dj.png" mode="aspectFill"></image>
<view class="user-details">
<text class="username">
<image
src="https://tse4-mm.cn.bing.net/th/id/OIP-C.iUyxJ_fxLjjX3kEBjteXWwAAAA?rs=1&pid=ImgDetMain"
mode="aspectFill"></image>
src="https://tse4-mm.cn.bing.net/th/id/OIP-C.iUyxJ_fxLjjX3kEBjteXWwAAAA?rs=1&pid=ImgDetMain"
mode="aspectFill"></image>
<view class="name">
周海
</view>
</text>
<view class="user-score">
<text class="score-value">6785452</text>
@ -90,7 +88,7 @@
</view>
</view>
<view class="level-right">
<view class="rank-btn">
<view class="rank-btn" @click="toggleInteractive">
<image class="rank-icon" src="/pages_order/static/book/bd.png" mode="aspectFit"></image>
<text class="check-text">点击查看</text>
</view>
@ -115,7 +113,9 @@
<text>目录</text>
</view>
<view class="chapter-nav">
<text class="current-chapter">第九集 - 高去与归来</text>
<text class="current-chapter">
{{ catalog ? catalog.title : '暂无章节' }}
</text>
<text class="nav-arrow">></text>
</view>
</view>
@ -134,7 +134,8 @@
</view>
</view>
<view class="comment-list">
<commentItem v-for="(item, index) in 10" :key="index" />
<commentItem v-for="(item, index) in list" :item="item" :key="index" />
<uv-empty mode="list" v-if="list.length == 0"></uv-empty>
</view>
</view>
@ -143,11 +144,7 @@
<view class="bottom-left">
<view class="action-btn" @click="addToBookshelf">
<view class="btn-icon">
<uv-icon
name="grid"
color="#999"
size="60rpx"
></uv-icon>
<uv-icon name="grid" color="#999" size="60rpx"></uv-icon>
</view>
<text>加入书架</text>
</view>
@ -155,26 +152,21 @@
<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="$utils.navigateTo(`/pages_order/novel/Giftbox?id=${novelData.id}`)">
<view class="btn-icon">
<uv-icon
name="gift"
color="#999"
size="60rpx"
></uv-icon>
<uv-icon name="gift" color="#999" size="60rpx"></uv-icon>
</view>
<text>互动打赏</text>
</view>
</view>
<view class="bottom-right">
<button class="read-now-btn" @click="">立即阅读</button>
<button class="read-now-btn" @click="toRead">立即阅读</button>
</view>
</view>
<novelVotePopup ref="novelVotePopup"/>
<chapterPopup ref="chapterPopup" />
<novelVotePopup ref="novelVotePopup" @updateVote="updateVote"/>
<chapterPopup ref="chapterPopup" :bookId="id" :chapterList="chapterList" @selectChapter="selectChapter"/>
</view>
</template>
@ -183,9 +175,9 @@
import chapterPopup from '../components/novel/chapterPopup.vue'
import commentItem from '../components/comment/commentItem.vue'
import novelVotePopup from '../components/novel/novelVotePopup.vue'
import mixinsList from '@/mixins/list.js'
import mixinsList from '@/mixins/list.js'
export default {
mixins: [mixinsList],
mixins: [mixinsList],
components: {
catalogpopup,
chapterPopup,
@ -198,76 +190,128 @@
isCollected: false,
comments: [],
currentIndex: 0,
id : 0,
bookLevel : {},
mixinsListApi : 'getBookCommentList',
id: 0,
bookLevel: {},
mixinsListApi: 'getBookCommentList',
catalog: {}, //
fastCatalog: {}, //
chapterList : [],//
}
},
computed: {
userInfo() {
return {
avatar: '/static/images/avatar.jpg',
level: '67级达人'
}
}
},
onLoad({id}) {
computed: {},
onLoad({
id
}) {
this.id = id
this.queryParams.id = id
this.queryParams.bookId = id
this.getDateil()
this.getAchievement()
this.getBookCatalogList()
},
methods: {
getDateil(){
this.$fetch('getBookDetail', {
id : this.id
}).then(res => {
updateVote() {
this.getDateil()
this.getAchievement()
},
getDateil() {
let data = {
id: this.id
}
if(uni.getStorageSync('data')){
data.token = uni.getStorageSync('token')
}
this.$fetch('getBookDetail', data).then(res => {
this.novelData = res
})
},
getAchievement(){
getAchievement() {
this.$fetch('getAchievement', {
id : this.id
id: this.id
}).then(res => {
this.bookLevel = res
})
},
getBookCatalogList(){
getBookCatalogList() {
this.$fetch('getBookCatalogList', {
id : this.id
bookId : this.id,
pageNo : 1,
pageSize : 9999999,
reverse : 0,
}).then(res => {
this.chapterList = res.records
this.catalog = res.records[res.records.length - 1]
this.fastCatalog = res.records[0]
})
//
// this.$fetch('getBookCatalogList', {
// bookId: this.id,
// pageNo: 1,
// pageSize: 1,
// reverse: 1,
// }).then(res => {
// this.catalog = res.records[0]
// })
// //
// this.$fetch('getBookCatalogList', {
// bookId: this.id,
// pageNo: 1,
// pageSize: 1,
// reverse: 0,
// }).then(res => {
// this.fastCatalog = res.records[0]
// })
},
toggleCollect() {
this.isCollected = !this.isCollected
},
goToWriteReview() {
uni.navigateTo({
url: `/pages_order/novel/Review?title=${encodeURIComponent(this.novelData.title)}`
url: `/pages_order/comment/review?id=${this.id}`
})
},
addToBookshelf() {
// TODO:
uni.showToast({
title: '已加入书架',
icon: 'success'
this.$fetch('addReadBook', {
shopId: this.id,
name: this.novelData.name,
image: this.novelData.image,
}).then(res => {
uni.showToast({
title: '已加入书架',
icon: 'success'
})
})
},
toggleInteractive() {
uni.navigateTo({
url: `/pages_order/novel/Tipping?id=${this.novelData.id}`
url: `/pages_order/novel/Tipping?id=${this.id}`
})
},
goToGiftbox() {
uni.navigateTo({
url: `/pages_order/novel/Giftbox?id=${this.novelData.id}`
url: `/pages_order/novel/Giftbox?id=${this.id}`
})
},
goToCommentReply() {
toRead() {
if (!this.fastCatalog) {
uni.showToast({
title: '暂无章节',
icon: 'none'
})
return
}
uni.navigateTo({
url: '/pages_order/novel/comments'
url: `/pages_order/novel/readnovels?cid=${this.fastCatalog.id}&id=${this.id}`
})
}
},
selectChapter({item, index}){
uni.navigateTo({
url: `/pages_order/novel/readnovels?cid=${item.id}&id=${this.id}`
})
},
}
}
</script>
@ -277,7 +321,7 @@
min-height: 100vh;
background-color: #f5f5f5;
padding-bottom: calc(env(safe-area-inset-bottom) + 30rpx);
.nav-header {
display: flex;
justify-content: space-between;
@ -290,36 +334,36 @@
right: 0;
z-index: 100;
}
.novel-info {
padding: 20rpx;
display: flex;
background: #fff;
.novel-cover {
width: 200rpx;
height: 280rpx;
margin-right: 20rpx;
image {
width: 100%;
height: 100%;
border-radius: 8rpx;
}
}
.novel-basic {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
.title {
font-size: 36rpx;
font-weight: bold;
margin-bottom: 16rpx;
}
.author-line,
.status-line {
display: flex;
@ -328,14 +372,15 @@
font-size: 28rpx;
color: #666;
}
.content-row {
display: flex;
align-items: center;
margin-bottom: 10rpx;
.book-status {
flex-shrink: 0;
text {
font-size: 20rpx;
color: #67C23A;
@ -344,25 +389,26 @@
padding: 4rpx 12rpx;
}
}
.book-text{
font-size: 20rpx;
.book-text {
font-size: 20rpx;
}
}
.label {
color: #999;
margin-right: 8rpx;
}
.score-line {
margin-top: 16rpx;
.score {
font-size: 32rpx;
color: #333;
font-weight: bold;
}
.score-label {
font-size: 24rpx;
color: #999;
@ -371,7 +417,7 @@
}
}
}
.recommendation-section {
padding: 24rpx 32rpx;
background: #fff;
@ -379,27 +425,27 @@
justify-content: space-between;
align-items: center;
position: relative;
.rec-left {
display: flex;
flex-direction: column;
align-items: flex-start;
margin-left: 70rpx;
.rec-count {
font-size: 44rpx;
font-weight: 500;
color: #333;
line-height: 1.2;
}
.rec-label {
font-size: 26rpx;
color: #999;
margin-top: 4rpx;
}
}
.rec-divider {
position: absolute;
right: 160rpx;
@ -408,10 +454,10 @@
width: 2rpx;
background: #eee;
}
.rec-right {
flex-shrink: 0;
.recommend-btn {
background: #fff;
color: #4a90e2;
@ -423,7 +469,7 @@
align-items: center;
line-height: 1;
height: 64rpx;
.btn-icon {
margin-right: 8rpx;
font-size: 32rpx;
@ -431,12 +477,12 @@
}
}
}
.action-buttons {
display: flex;
padding: 30rpx;
gap: 20rpx;
button {
flex: 1;
height: 80rpx;
@ -446,18 +492,18 @@
align-items: center;
justify-content: center;
}
.read-btn {
background-color: #4a90e2;
color: #fff;
}
.collect-btn {
background-color: #f0f0f0;
color: #666;
}
}
.user-level {
margin: 20rpx 30rpx;
background-color: #fff;
@ -466,74 +512,74 @@
display: flex;
justify-content: space-between;
align-items: stretch;
.level-left {
flex: 1;
.level-title {
display: flex;
align-items: center;
gap: 8rpx;
margin-bottom: 20rpx;
margin-left: 20rpx;
.title-icon {
font-size: 36rpx;
color: #FFB800;
}
text {
font-size: 32rpx;
font-weight: 500;
color: #333;
}
}
.level-info {
display: flex;
align-items: flex-start;
gap: 20rpx;
.user-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
border: 2rpx solid #f0f0f0;
}
.user-details {
display: flex;
flex-direction: column;
gap: 8rpx;
.username {
display: flex;
font-size: 28rpx;
color: #333;
font-weight: 500;
image {
width: 60rpx;
height: 60rpx;
}
}
.user-score {
display: flex;
align-items: center;
gap: 8rpx;
.score-value {
font-size: 28rpx;
color: #333;
}
.score-label {
font-size: 24rpx;
color: #999;
}
}
.user-role {
font-size: 24rpx;
color: #666;
@ -545,30 +591,30 @@
}
}
}
.level-right {
display: flex;
align-items: center;
.rank-btn {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0 20rpx;
.rank-icon {
width: 200rpx;
height: 60rpx;
margin-bottom: 8rpx;
}
text {
font-size: 26rpx;
color: #333;
line-height: 1.4;
}
.check-text {
font-size: 22rpx;
color: #999;
@ -576,20 +622,20 @@
}
}
}
.novel-intro {
margin: 20rpx 30rpx;
background-color: #fff;
border-radius: 12rpx;
padding: 24rpx;
.intro-title {
font-size: 32rpx;
font-weight: 500;
color: #333;
margin-bottom: 16rpx;
}
.intro-content {
font-size: 28rpx;
color: #666;
@ -597,19 +643,19 @@
display: flex;
flex-direction: column;
gap: 16rpx;
text {
display: block;
}
}
}
.comments-section {
margin: 20rpx 30rpx;
background-color: #fff;
border-radius: 12rpx;
padding: 24rpx;
.comments-header {
display: flex;
align-items: center;
@ -617,16 +663,16 @@
border-bottom: 2rpx solid #f5f5f5;
padding-bottom: 24rpx;
justify-content: flex-start;
.header-left {
display: flex;
align-items: center;
gap: 8rpx;
.title-icon {
font-size: 32rpx;
}
text {
display: flex;
align-items: center;
@ -636,67 +682,67 @@
white-space: nowrap;
}
}
.header-right {
margin-left: auto;
}
}
.comment-list {
display: flex;
flex-direction: column;
gap: 32rpx;
}
.like-icon {
font-size: 24rpx;
color: #999;
}
.like-count {
font-size: 24rpx;
color: #999;
}
}
.novel-catalog {
margin: 20rpx 30rpx;
background-color: #fff;
border-radius: 12rpx;
padding: 24rpx;
.catalog-header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 2rpx solid #f5f5f5;
.catalog-title {
display: flex;
align-items: center;
gap: 8rpx;
.title-icon {
font-size: 32rpx;
}
text {
font-size: 32rpx;
font-weight: 500;
color: #333;
}
}
.chapter-nav {
display: flex;
align-items: center;
gap: 8rpx;
.current-chapter {
font-size: 28rpx;
color: #666;
}
.nav-arrow {
font-size: 28rpx;
color: #999;
@ -704,7 +750,7 @@
}
}
}
.novel-bottom {
position: fixed;
bottom: 0;
@ -719,31 +765,33 @@
padding-bottom: env(safe-area-inset-bottom);
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
gap: 40rpx;
.bottom-left {
display: flex;
gap: 40rpx;
.action-btn {
display: flex;
flex-direction: column;
align-items: center;
gap: 4rpx;
.btn-icon {
font-size: 40rpx;
line-height: 1;
}
text {
font-size: 24rpx;
color: #666;
}
}
}
.bottom-right {
flex: 1;
display: flex;
.read-now-btn {
flex: 1;
background: #1a237e;


+ 463
- 0
pages_order/novel/readnovels - 副本.vue View File

@ -0,0 +1,463 @@
<template>
<!-- 小说文本页面 -->
<view class="reader-container" :class="{'dark-mode': isDarkMode}">
<view class="top-controls" :class="{'top-controls-hidden': isFullScreen}">
<view class="controls-inner">
<view class="left">
<uv-icon name="arrow-left" @click="$utils.navigateBack" :color="isDarkMode ? '#ccc' : '#333'" size="46rpx"></uv-icon>
</view>
<view class="center">
<text class="title">{{ novelTitle }}</text>
<text class="chapter">{{ currentChapter }}</text>
</view>
<!-- <view class="right">
<uv-icon name="more-dot-fill" color="#333" size="46rpx"></uv-icon>
</view> -->
</view>
<view class="progress-bar">
<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">
<view class="chapter-content-item">
<view class="chapter-title">第1章 重回2004</view>
<view class="paragraph-content">
<view class="paragraph" v-for="(paragraph, index) in paragraphs" :key="index">
{{ paragraph }}
</view>
</view>
</view>
</scroll-view>
<view class="bottom-bar" :class="{'bottom-bar-hidden': isFullScreen}">
<view class="bottom-left">
<view class="bar-item">
<view class="bar-icon"> <uv-icon name="plus"></uv-icon> </view>
<text class="bar-label">加入书架</text>
</view>
<view class="bar-item" @click="toggleThemeMode">
<view class="bar-icon">
<uv-icon :name="isDarkMode ? 'eye' : 'eye-fill'"></uv-icon>
</view>
<text class="bar-label">{{ isDarkMode ? '白天' : '夜间' }}</text>
</view>
</view>
<view class="bottom-right">
<button class="outline-btn"><text class="btn-text">上一章</text></button>
<button class="outline-btn" @click="$refs.chapterPopup.open()"><text class="btn-text">目录</text></button>
<button class="outline-btn"><text class="btn-text">下一章</text></button>
</view>
</view>
<!-- 使用封装的订阅弹窗组件 -->
<subscriptionPopup ref="subscriptionPopup" @subscribe="goToSubscription"/>
<novelVotePopup ref="novelVotePopup"/>
<chapterPopup ref="chapterPopup" />
</view>
</template>
<script>
import chapterPopup from '../components/novel/chapterPopup.vue'
import novelVotePopup from '../components/novel/novelVotePopup.vue'
import subscriptionPopup from '../components/novel/subscriptionPopup.vue'
import themeMixin from '@/mixins/themeMode.js' //
export default {
components: {
chapterPopup,
novelVotePopup,
subscriptionPopup,
},
mixins: [themeMixin], // 使
data() {
return {
isFullScreen: false,
popupShown: false, //
novelTitle: "这游戏也太真实了",
currentChapter: "第1章 重回2004",
readProgress: 15, //
paragraphs: [
"华东地区某个不知名街区,2004年冬。",
"天还没有亮,王明就起床了。他要去赶早市,今天是进货的日子,错过了就要等下周。他轻轻地穿好衣服,不想吵醒还在熟睡的妻子。",
"天气比想象中冷,他裹紧了身上那件略显破旧的棉袄。出门前,他看了眼床头那个旧闹钟,四点半,还算准时。",
"街上几乎没有人,只有零星的几辆三轮车和面包车正往市场方向驶去。王明加快了脚步,他知道好位置都是先到先得。",
"这一年,互联网刚刚开始在中国普及,但对于像王明这样的小摊贩来说,生活并没有什么变化。每天起早贪黑,挣扎在温饱线上。",
"然而就在今天,他的生活将迎来一场他从未预料到的变化。",
"市场入口处,一个陌生人递给他一张名片,上面写着:\"电子产品批发,价格优惠\"。王明随手接过,塞进了口袋,继续往里走。",
"他不知道的是,这张小小的名片,将成为改变他命运的第一步。",
"几个小时后,当他收摊准备回家时,他偶然摸到了那张名片。出于好奇,他决定去看看。",
"名片上的地址在城市的另一边,是一个他从未去过的工业区。坐了将近一个小时的公交车,他终于找到了那个地方。",
"那是一个不起眼的仓库,门口停着几辆货车。王明犹豫了一下,还是推门走了进去。",
"里面的景象让他震惊。货架上整齐地摆放着各种电子产品:MP3播放器、数码相机、U盘……这些在当时都是新奇而昂贵的物品。",
"\"您是新顾客吧?\"一个中年男人走过来,热情地招呼道。",
"\"是的,我看到了你的名片。\"王明有些拘谨地回答。",
"\"那您来得正是时候,我们刚收到一批新货,价格特别优惠。\"",
"王明被带到一个展示台前,上面摆着几个小巧的设备。\"这是最新款的MP3,容量大,音质好,在市场上很受欢迎。\"",
"王明拿起一个仔细端详。他虽然没什么文化,但做生意的直觉告诉他,这东西可能有市场。",
"\"多少钱一个?\"他问道。",
"\"批发价150元,零售价可以卖到300元以上。\"",
"王明心里快速计算着。他今天的存款只有3000元,如果全买这个,可以拿20个。要是真能卖出去,就是3000元的利润。",
"但风险也很大。万一卖不出去,这可是他半年的积蓄啊。",
"就在他犹豫的时候,旁边传来一个熟悉的声音:\"老王,你也来这进货啊?\"",
"是他在市场上认识的李东。李东比他年轻,做生意也比他精明。",
"\"你觉得这东西怎么样?\"王明问道。",
"\"那必须相当不错啊。我上周进了一批,三天就卖光了。现在是过节嘛,年轻人喜欢这些新鲜玩意儿。\"李东拍了拍他的肩膀,\"要我说,你应该赶紧进一批,过了这个村可就没这个店了。\"",
"看到李东的信心,王明心里的天平开始倾斜。",
"\"那...好吧,给我来20个。\"他终于下定决心,从口袋里掏出了钱。",
"这一决定,将彻底改变他的人生轨迹...",
]
}
},
methods: {
handleContentClick() {
this.toggleFullScreen();
},
handleScroll(e) {
//
const scrollTop = e.detail.scrollTop;
//
if (scrollTop > 50 && !this.popupShown) {
this.$refs.subscriptionPopup.open();
this.popupShown = true;
}
},
toggleFullScreen() {
this.isFullScreen = !this.isFullScreen
},
goToSubscription() {
uni.navigateTo({
url: '/pages_order/novel/SubscriptionInformation'
})
},
},
mounted() {
//
this.isFullScreen = true;
// #ifdef H5
if (typeof window !== 'undefined') {
window.onscroll = () => {
const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
if (scrollTop > 50 && !this.popupShown) {
this.$refs.subscriptionPopup.open();
this.popupShown = true;
}
};
}
// #endif
},
beforeDestroy() {
// #ifdef H5
if (typeof window !== 'undefined') {
window.onscroll = null;
}
// #endif
},
onLoad() {
// idid
}
}
</script>
<style lang="scss" scoped>
.reader-container {
min-height: 100vh;
background: #fff;
display: flex;
flex-direction: column;
position: relative;
overflow: hidden;
&.dark-mode {
background: #1a1a1a;
.top-controls {
background: rgba(34, 34, 34, 0.98);
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.2);
.controls-inner {
.center {
.title {
color: #eee;
}
.chapter {
color: #bbb;
}
}
}
.progress-bar {
background: #333;
.progress-inner {
background: #4a90e2;
}
}
}
.chapter-content {
color: #ccc;
.chapter-content-item {
.chapter-title {
color: #eee;
}
.paragraph-content {
.paragraph {
color: #bbb;
}
}
}
}
.bottom-bar {
background: #222;
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.2);
.bottom-left {
.bar-item {
.bar-label {
color: #999;
}
}
}
.bottom-right {
.outline-btn {
background: #222;
color: #4a90e2;
border: 2rpx solid #4a90e2;
.btn-text {
color: #4a90e2;
border-bottom: 2rpx solid #4a90e2;
}
}
}
}
}
.top-controls {
position: fixed;
top: 0;
left: 0;
right: 0;
background: rgba(255, 255, 255, 0.98);
padding-top: calc(var(--status-bar-height) + 10rpx);
z-index: 100;
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);
.controls-inner {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 32rpx;
position: relative;
.left {
width: 100rpx;
display: flex;
justify-content: flex-start;
align-items: center;
}
.center {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.title {
font-size: 32rpx;
font-weight: 500;
color: #333;
margin-bottom: 4rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 320rpx;
}
.chapter {
font-size: 24rpx;
color: #666;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 320rpx;
}
}
.right {
width: 100rpx;
display: flex;
justify-content: flex-end;
align-items: center;
}
}
.progress-bar {
height: 4rpx;
background: #f0f0f0;
width: 100%;
position: relative;
.progress-inner {
height: 100%;
background: #4a90e2;
transition: width 0.3s;
}
}
&.top-controls-hidden {
transform: translateY(-100%);
opacity: 0;
}
}
.chapter-content {
flex: 1;
padding: 0 32rpx;
font-size: 28rpx;
color: #222;
line-height: 2.2;
padding-top: 160rpx; /* 为导航栏预留空间,不随状态变化 */
padding-bottom: 180rpx;
width: 100%;
box-sizing: border-box;
overflow-x: hidden;
transition: color 0.3s ease, background-color 0.3s ease;
.chapter-content-item {
width: 100%;
.chapter-title {
font-size: 36rpx;
font-weight: bold;
margin: 20rpx 0 40rpx 0;
text-align: center;
word-break: break-word;
white-space: normal;
transition: color 0.3s ease;
}
.paragraph-content {
width: 100%;
.paragraph {
text-indent: 2em;
margin-bottom: 30rpx;
line-height: 1.8;
font-size: 30rpx;
color: #333;
word-wrap: break-word;
word-break: normal;
white-space: normal;
transition: color 0.3s ease;
}
}
}
&.full-content {
/* 不再修改顶部padding,保持内容位置不变 */
}
}
.bottom-bar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
background: #fff;
display: flex;
justify-content: center;
align-items: center;
height: 180rpx;
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
z-index: 10;
padding: 0 40rpx 10rpx 40rpx;
transform: translateY(0);
transition: transform 0.3s ease-in-out, background-color 0.3s ease;
&.bottom-bar-hidden {
transform: translateY(100%);
}
.bottom-left {
display: flex;
align-items: flex-end;
gap: 48rpx;
.bar-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
.bar-icon {
width: 48rpx;
height: 48rpx;
margin-bottom: 4rpx;
margin-right: 1rpx;
display: flex;
align-items: center;
justify-content: center;
}
.bar-label {
font-size: 22rpx;
color: #b3b3b3;
margin-top: 2rpx;
transition: color 0.3s ease;
}
}
}
.bottom-right {
display: flex;
align-items: flex-end;
gap: 32rpx;
margin-left: 40rpx;
.outline-btn {
min-width: 110rpx;
padding: 0 28rpx;
height: 60rpx;
line-height: 60rpx;
background: #fff;
color: #223a7a;
border: 2rpx solid #223a7a;
border-radius: 32rpx;
font-size: 28rpx;
font-weight: bold;
margin: 0;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
.btn-text {
font-weight: bold;
color: #223a7a;
font-size: 28rpx;
border-bottom: 2rpx solid #223a7a;
padding-bottom: 2rpx;
transition: color 0.3s ease, border-color 0.3s ease;
}
}
}
}
}
</style>

+ 160
- 128
pages_order/novel/readnovels.vue View File

@ -3,36 +3,36 @@
<view class="reader-container" :class="{'dark-mode': isDarkMode}">
<view class="top-controls" :class="{'top-controls-hidden': isFullScreen}">
<view class="controls-inner">
<view class="left">
<uv-icon name="arrow-left" @click="$utils.navigateBack" :color="isDarkMode ? '#ccc' : '#333'" size="46rpx"></uv-icon>
<view class="left" @click="$utils.navigateBack">
<uv-icon name="arrow-left" :color="isDarkMode ? '#ccc' : '#333'" size="46rpx"></uv-icon>
</view>
<view class="center">
<text class="title">{{ novelTitle }}</text>
<text class="title">{{ novelData.name }}</text>
<text class="chapter">{{ currentChapter }}</text>
</view>
<!-- <view class="right">
<uv-icon name="more-dot-fill" color="#333" size="46rpx"></uv-icon>
</view> -->
</view>
<view class="progress-bar">
<!-- <view class="progress-bar">
<view class="progress-inner" :style="{width: readProgress + '%'}"></view>
</view>
</view> -->
</view>
<scroll-view scroll-y class="chapter-content" :class="{'full-content': isFullScreen}"
@scroll="handleScroll" @tap="handleContentClick">
<scroll-view scroll-y class="chapter-content" :class="{'full-content': isFullScreen}" @scroll="handleScroll"
@tap="handleContentClick">
<view class="chapter-content-item">
<view class="chapter-title">第1章 重回2004</view>
<view class="paragraph-content">
<view class="paragraph" v-for="(paragraph, index) in paragraphs" :key="index">
{{ paragraph }}
</view>
</view>
<view class="chapter-title">{{ currentChapter }}</view>
<view class="paragraph-content">
<view class="paragraph" v-for="(paragraph, index) in paragraphs" :key="index">
{{ paragraph }}
</view>
</view>
</view>
</scroll-view>
<view class="bottom-bar" :class="{'bottom-bar-hidden': isFullScreen}">
<view class="bottom-left">
<view class="bar-item">
@ -40,25 +40,34 @@
<text class="bar-label">加入书架</text>
</view>
<view class="bar-item" @click="toggleThemeMode">
<view class="bar-icon">
<uv-icon :name="isDarkMode ? 'eye' : 'eye-fill'"></uv-icon>
<view class="bar-icon">
<uv-icon :name="isDarkMode ? 'eye' : 'eye-fill'"></uv-icon>
</view>
<text class="bar-label">{{ isDarkMode ? '白天' : '夜间' }}</text>
</view>
</view>
<view class="bottom-right">
<button class="outline-btn"><text class="btn-text">上一章</text></button>
<button class="outline-btn" @click="$refs.chapterPopup.open()"><text class="btn-text">目录</text></button>
<button class="outline-btn"><text class="btn-text">下一章</text></button>
<button class="outline-btn"
@click="nextChapter(-1)">
<text class="btn-text">上一章</text>
</button>
<button class="outline-btn" @click="$refs.chapterPopup.open()">
<text class="btn-text">目录</text>
</button>
<button class="outline-btn"
@click="nextChapter(1)">
<text class="btn-text">下一章</text>
</button>
</view>
</view>
<!-- 使用封装的订阅弹窗组件 -->
<subscriptionPopup ref="subscriptionPopup" @subscribe="goToSubscription"/>
<novelVotePopup ref="novelVotePopup"/>
<chapterPopup ref="chapterPopup" />
<subscriptionPopup ref="subscriptionPopup" @subscribe="goToSubscription" />
<novelVotePopup ref="novelVotePopup" />
<chapterPopup ref="chapterPopup" :chapterList="chapterList" :currentIndex="currentIndex"
@selectChapter="selectChapter" />
</view>
</template>
@ -67,7 +76,7 @@
import novelVotePopup from '../components/novel/novelVotePopup.vue'
import subscriptionPopup from '../components/novel/subscriptionPopup.vue'
import themeMixin from '@/mixins/themeMode.js' //
export default {
components: {
chapterPopup,
@ -79,42 +88,69 @@
return {
isFullScreen: false,
popupShown: false, //
novelTitle: "这游戏也太真实了",
currentChapter: "第1章 重回2004",
currentChapter: "",
readProgress: 15, //
paragraphs: [
"华东地区某个不知名街区,2004年冬。",
"天还没有亮,王明就起床了。他要去赶早市,今天是进货的日子,错过了就要等下周。他轻轻地穿好衣服,不想吵醒还在熟睡的妻子。",
"天气比想象中冷,他裹紧了身上那件略显破旧的棉袄。出门前,他看了眼床头那个旧闹钟,四点半,还算准时。",
"街上几乎没有人,只有零星的几辆三轮车和面包车正往市场方向驶去。王明加快了脚步,他知道好位置都是先到先得。",
"这一年,互联网刚刚开始在中国普及,但对于像王明这样的小摊贩来说,生活并没有什么变化。每天起早贪黑,挣扎在温饱线上。",
"然而就在今天,他的生活将迎来一场他从未预料到的变化。",
"市场入口处,一个陌生人递给他一张名片,上面写着:\"电子产品批发,价格优惠\"。王明随手接过,塞进了口袋,继续往里走。",
"他不知道的是,这张小小的名片,将成为改变他命运的第一步。",
"几个小时后,当他收摊准备回家时,他偶然摸到了那张名片。出于好奇,他决定去看看。",
"名片上的地址在城市的另一边,是一个他从未去过的工业区。坐了将近一个小时的公交车,他终于找到了那个地方。",
"那是一个不起眼的仓库,门口停着几辆货车。王明犹豫了一下,还是推门走了进去。",
"里面的景象让他震惊。货架上整齐地摆放着各种电子产品:MP3播放器、数码相机、U盘……这些在当时都是新奇而昂贵的物品。",
"\"您是新顾客吧?\"一个中年男人走过来,热情地招呼道。",
"\"是的,我看到了你的名片。\"王明有些拘谨地回答。",
"\"那您来得正是时候,我们刚收到一批新货,价格特别优惠。\"",
"王明被带到一个展示台前,上面摆着几个小巧的设备。\"这是最新款的MP3,容量大,音质好,在市场上很受欢迎。\"",
"王明拿起一个仔细端详。他虽然没什么文化,但做生意的直觉告诉他,这东西可能有市场。",
"\"多少钱一个?\"他问道。",
"\"批发价150元,零售价可以卖到300元以上。\"",
"王明心里快速计算着。他今天的存款只有3000元,如果全买这个,可以拿20个。要是真能卖出去,就是3000元的利润。",
"但风险也很大。万一卖不出去,这可是他半年的积蓄啊。",
"就在他犹豫的时候,旁边传来一个熟悉的声音:\"老王,你也来这进货啊?\"",
"是他在市场上认识的李东。李东比他年轻,做生意也比他精明。",
"\"你觉得这东西怎么样?\"王明问道。",
"\"那必须相当不错啊。我上周进了一批,三天就卖光了。现在是过节嘛,年轻人喜欢这些新鲜玩意儿。\"李东拍了拍他的肩膀,\"要我说,你应该赶紧进一批,过了这个村可就没这个店了。\"",
"看到李东的信心,王明心里的天平开始倾斜。",
"\"那...好吧,给我来20个。\"他终于下定决心,从口袋里掏出了钱。",
"这一决定,将彻底改变他的人生轨迹...",
]
paragraphs: [],
id: 0,
cid: 0,
novelData: {},
chapterList: [],
}
},
computed: {
currentIndex() {
for (var index = 0; index < this.chapterList.length; index++) {
var element = this.chapterList[index];
if (element.id == this.cid) return index
}
return -1
},
},
onLoad({
id,
cid
}) {
this.id = id
this.cid = cid
this.getDateil()
this.getBookCatalogDetail()
},
onShow() {
this.getBookCatalogList()
},
mounted() {
//
this.isFullScreen = true;
},
methods: {
getDateil() {
this.$fetch('getBookDetail', {
id: this.id
}).then(res => {
this.novelData = res
})
},
getBookCatalogDetail() {
this.$fetch('getBookCatalogDetail', {
id: this.cid
}).then(res => {
this.paragraphs = res.details && res.details.split('\n')
this.currentChapter = res.title
})
},
getBookCatalogList() {
this.$fetch('getBookCatalogList', {
bookId: this.id,
pageNo: 1,
pageSize: 9999999,
reverse: 0,
}).then(res => {
this.chapterList = res.records
this.catalog = res.records[res.records.length - 1]
this.fastCatalog = res.records[0]
})
},
handleContentClick() {
this.toggleFullScreen();
},
@ -136,33 +172,28 @@
url: '/pages_order/novel/SubscriptionInformation'
})
},
selectChapter({
item,
index
}) {
this.cid = item.id
this.isFullScreen = true
this.getBookCatalogDetail()
},
nextChapter(next) {
let index = this.currentIndex + next
if(index < 0 || index >= this.chapterList.length){
uni.showToast({
title: '到底了',
icon: 'none'
})
return
}
this.cid = this.chapterList[index].id
this.isFullScreen = true
this.getBookCatalogDetail()
},
},
mounted() {
//
this.isFullScreen = true;
// #ifdef H5
if (typeof window !== 'undefined') {
window.onscroll = () => {
const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
if (scrollTop > 50 && !this.popupShown) {
this.$refs.subscriptionPopup.open();
this.popupShown = true;
}
};
}
// #endif
},
beforeDestroy() {
// #ifdef H5
if (typeof window !== 'undefined') {
window.onscroll = null;
}
// #endif
},
onLoad() {
// idid
}
}
</script>
@ -174,43 +205,43 @@
flex-direction: column;
position: relative;
overflow: hidden;
&.dark-mode {
background: #1a1a1a;
.top-controls {
background: rgba(34, 34, 34, 0.98);
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.2);
.controls-inner {
.center {
.title {
color: #eee;
}
.chapter {
color: #bbb;
}
}
}
.progress-bar {
background: #333;
.progress-inner {
background: #4a90e2;
}
}
}
.chapter-content {
color: #ccc;
.chapter-content-item {
.chapter-title {
color: #eee;
}
.paragraph-content {
.paragraph {
color: #bbb;
@ -218,11 +249,11 @@
}
}
}
.bottom-bar {
background: #222;
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.2);
.bottom-left {
.bar-item {
.bar-label {
@ -230,13 +261,13 @@
}
}
}
.bottom-right {
.outline-btn {
background: #222;
color: #4a90e2;
border: 2rpx solid #4a90e2;
.btn-text {
color: #4a90e2;
border-bottom: 2rpx solid #4a90e2;
@ -245,7 +276,7 @@
}
}
}
.top-controls {
position: fixed;
top: 0;
@ -257,28 +288,28 @@
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);
.controls-inner {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 32rpx;
position: relative;
.left {
width: 100rpx;
display: flex;
justify-content: flex-start;
align-items: center;
}
.center {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.title {
font-size: 32rpx;
font-weight: 500;
@ -289,7 +320,7 @@
text-overflow: ellipsis;
max-width: 320rpx;
}
.chapter {
font-size: 24rpx;
color: #666;
@ -299,7 +330,7 @@
max-width: 320rpx;
}
}
.right {
width: 100rpx;
display: flex;
@ -307,42 +338,43 @@
align-items: center;
}
}
.progress-bar {
height: 4rpx;
background: #f0f0f0;
width: 100%;
position: relative;
.progress-inner {
height: 100%;
background: #4a90e2;
transition: width 0.3s;
}
}
&.top-controls-hidden {
transform: translateY(-100%);
opacity: 0;
}
}
.chapter-content {
flex: 1;
padding: 0 32rpx;
font-size: 28rpx;
color: #222;
line-height: 2.2;
padding-top: 160rpx; /* 为导航栏预留空间,不随状态变化 */
padding-top: 160rpx;
/* 为导航栏预留空间,不随状态变化 */
padding-bottom: 180rpx;
width: 100%;
box-sizing: border-box;
overflow-x: hidden;
transition: color 0.3s ease, background-color 0.3s ease;
.chapter-content-item {
width: 100%;
.chapter-title {
font-size: 36rpx;
font-weight: bold;
@ -352,10 +384,10 @@
white-space: normal;
transition: color 0.3s ease;
}
.paragraph-content {
width: 100%;
.paragraph {
text-indent: 2em;
margin-bottom: 30rpx;
@ -369,12 +401,12 @@
}
}
}
&.full-content {
/* 不再修改顶部padding,保持内容位置不变 */
}
}
.bottom-bar {
position: fixed;
left: 0;
@ -390,32 +422,32 @@
padding: 0 40rpx 10rpx 40rpx;
transform: translateY(0);
transition: transform 0.3s ease-in-out, background-color 0.3s ease;
&.bottom-bar-hidden {
transform: translateY(100%);
}
.bottom-left {
display: flex;
align-items: flex-end;
gap: 48rpx;
.bar-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
.bar-icon {
width: 48rpx;
height: 48rpx;
margin-bottom: 4rpx;
margin-right: 1rpx;
display: flex;
align-items: center;
justify-content: center;
display: flex;
align-items: center;
justify-content: center;
}
.bar-label {
font-size: 22rpx;
color: #b3b3b3;
@ -424,13 +456,13 @@
}
}
}
.bottom-right {
display: flex;
align-items: flex-end;
gap: 32rpx;
margin-left: 40rpx;
.outline-btn {
min-width: 110rpx;
padding: 0 28rpx;
@ -447,7 +479,7 @@
align-items: center;
justify-content: center;
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
.btn-text {
font-weight: bold;
color: #223a7a;


BIN
pages_order/static/top/4.png View File

Before After
Width: 96  |  Height: 97  |  Size: 5.1 KiB

BIN
pages_order/static/top/top1.png View File

Before After
Width: 355  |  Height: 354  |  Size: 130 KiB

+ 1
- 1
store/store.js View File

@ -11,7 +11,7 @@ const store = new Vuex.Store({
configList: {}, //配置列表
userInfo: {}, //用户信息
themeMode: uni.getStorageSync('themeMode') || 'light', // 主题模式:light(白天)或dark(黑夜)
isLogin : !!uni.getStorageSync('token')
isLogin : !!uni.getStorageSync('token'),
},
getters: {
// 当前主题模式


+ 4
- 4
uni_modules/uv-empty/components/uv-empty/props.js View File

@ -18,7 +18,7 @@ export default {
// 文字大小
textSize: {
type: [String, Number],
default: 14
default: 28
},
// 图标的颜色
iconColor: {
@ -28,7 +28,7 @@ export default {
// 图标的大小
iconSize: {
type: [String, Number],
default: 90
default: 180
},
// 选择预置的图标类型
mode: {
@ -38,12 +38,12 @@ export default {
// 图标宽度,单位px
width: {
type: [String, Number],
default: 160
default: 320
},
// 图标高度,单位px
height: {
type: [String, Number],
default: 160
default: 320
},
// 是否显示组件
show: {


Loading…
Cancel
Save