小说小程序前端代码仓库(小程序)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

686 lines
16 KiB

  1. <template>
  2. <!-- 书架页面 -->
  3. <view class="page">
  4. <!-- 头部标签切换 -->
  5. <view class="header">
  6. <view class="header-content">
  7. <view class="tab-container">
  8. <view class="tab" :class="{'active': activeTab === 'read'}" @click="switchTab('read')">阅读</view>
  9. <view class="tab" :class="{'active': activeTab === 'work'}"
  10. v-if="userInfo.isUser == 'Y'"
  11. @click="switchTab('work')">作品</view>
  12. </view>
  13. </view>
  14. </view>
  15. <!-- 书籍列表 - 阅读模式 -->
  16. <view class="novel-grid" v-if="activeTab === 'read' && !isEditMode">
  17. <view class="novel-row" v-for="(row, rowIndex) in novelRows" :key="rowIndex">
  18. <view class="novel-item"
  19. v-for="(novel, index) in list"
  20. :key="novel.id"
  21. @click="toNovelDetail(novel)"
  22. @longpress="enterEditMode">
  23. <novel-item
  24. :book="novel"
  25. horizontal="true"
  26. :style="{ width: '220rpx' }">
  27. </novel-item>
  28. <view class="novel-tag" v-if="novel.tag">{{novel.tag}}</view>
  29. <view class="novel-original" v-if="novel.isOriginal">
  30. <text>原创</text>
  31. </view>
  32. </view>
  33. </view>
  34. <!-- 空状态提示 -->
  35. <view class="empty-works" v-if="list.length === 0">
  36. <text class="empty-text">你书架还没有书籍呢</text>
  37. <text class="empty-tips">去首页看看吧</text>
  38. </view>
  39. </view>
  40. <!-- 作品列表 - 作品模式 -->
  41. <view class="works-container" v-if="activeTab === 'work' && !isEditMode">
  42. <!-- 顶部创建区域 -->
  43. <view class="works-header">
  44. <new-work-item @click="createNewWork" @settings="toReaderSettings" />
  45. </view>
  46. <!-- 作品列表 -->
  47. <view class="works-content">
  48. <work-item
  49. v-for="work in list"
  50. :key="work.id"
  51. :work="work"
  52. :isManaging="isEditMode"
  53. @click="toggleSelect(work, 'work')"
  54. @longpress="enterEditMode"
  55. />
  56. <!-- 空状态提示 -->
  57. <view class="empty-works" v-if="list.length === 0">
  58. <text class="empty-text">你还没有创建作品</text>
  59. <text class="empty-tips">点击左上角"+"创建你的第一部作品吧</text>
  60. </view>
  61. </view>
  62. </view>
  63. <!-- 编辑模式 - 阅读 -->
  64. <view class="novel-grid edit-mode" v-if="activeTab === 'read' && isEditMode">
  65. <view class="novel-row" v-for="(row, rowIndex) in novelRows" :key="rowIndex">
  66. <view class="novel-item"
  67. v-for="(novel, index) in list"
  68. :key="novel.id"
  69. @click="toggleSelect(novel, 'novel')">
  70. <view class="item-checkbox" v-if="selectedItems.includes(novel.id)">
  71. <view class="checkbox-inner">
  72. <uv-icon name="checkmark" size="28" color="#ffffff"></uv-icon>
  73. </view>
  74. </view>
  75. <view class="item-checkbox" v-else>
  76. <view class="checkbox-inner-no">
  77. </view>
  78. </view>
  79. <novel-item
  80. :book="novel"
  81. horizontal="true"
  82. :style="{ width: '220rpx', opacity: selectedItems.includes(novel.id) ? '0.8' : '1' }">
  83. </novel-item>
  84. <view class="novel-tag" v-if="novel.tag">{{novel.tag}}</view>
  85. <view class="novel-original" v-if="novel.isOriginal">
  86. <text>原创</text>
  87. </view>
  88. </view>
  89. </view>
  90. </view>
  91. <!-- 编辑模式 - 作品 -->
  92. <view class="works-container edit-mode" v-if="activeTab === 'work' && isEditMode">
  93. <view class="works-content">
  94. <view
  95. class="work-item-wrapper"
  96. v-for="work in list"
  97. :key="work.id"
  98. >
  99. <!-- 选中框 -->
  100. <view class="item-checkbox-work" v-if="selectedItems.includes(work.id)">
  101. <view class="checkbox-inner">
  102. <uv-icon name="checkmark" size="28" color="#ffffff"></uv-icon>
  103. </view>
  104. </view>
  105. <view class="item-checkbox-work" v-else>
  106. <view class="checkbox-inner-no">
  107. </view>
  108. </view>
  109. <work-item
  110. :work="work"
  111. :isManaging="true"
  112. @click="toggleSelect(work)"
  113. :style="{ opacity: selectedItems.includes(work.id) ? '0.8' : '1' }"
  114. />
  115. </view>
  116. </view>
  117. </view>
  118. <!-- 底部操作栏 -->
  119. <view class="bottom-action-bar" v-if="isEditMode">
  120. <view class="action-item" @click="exitEditMode">
  121. <view class="action-icon">
  122. <uv-icon name="reload" size="40" color="#666"></uv-icon>
  123. </view>
  124. <text>取消</text>
  125. </view>
  126. <view class="action-item" @click="selectAll">
  127. <view class="action-icon">
  128. <uv-icon name="grid-fill" size="40" color="#ff9900"></uv-icon>
  129. </view>
  130. <text>全选</text>
  131. </view>
  132. <view class="action-item" @click="removeSelected">
  133. <view class="action-icon">
  134. <uv-icon name="trash-fill" size="40" color="#f56c6c"></uv-icon>
  135. </view>
  136. <text>{{activeTab === 'read' ? '移出书架' : '删除'}}</text>
  137. </view>
  138. </view>
  139. <tabber select="bookshelf" v-if="!isEditMode"/>
  140. </view>
  141. </template>
  142. <script>
  143. import tabber from '@/components/base/tabbar.vue'
  144. import novelItem from '@/components/novel/bookshelfItem.vue'
  145. import workItem from '@/components/novel/workItem.vue'
  146. import newWorkItem from '@/components/novel/newWorkItem.vue'
  147. import mixinsList from '@/mixins/list.js'
  148. export default {
  149. mixins: [mixinsList],
  150. components : {
  151. tabber,
  152. novelItem,
  153. workItem,
  154. newWorkItem
  155. },
  156. computed : {
  157. // 将小说列表分成每行3个的二维数组
  158. novelRows() {
  159. const rows = [];
  160. const itemsPerRow = 3;
  161. for (let i = 0; i < this.list.length; i += itemsPerRow) {
  162. rows.push(this.list.slice(i, i + itemsPerRow));
  163. }
  164. return rows;
  165. }
  166. },
  167. data() {
  168. return {
  169. statusBarHeight: 0, // 状态栏高度
  170. navBarHeight: 0, // 导航栏高度
  171. activeTab: 'read',
  172. isEditMode: false,
  173. selectedItems: [], // 统一选中项
  174. mixinsListApi : '',
  175. apiMap : {
  176. read : 'getReadBookPage',
  177. work : 'getMyBookPage',
  178. // work : 'getMyBookPage',
  179. },
  180. }
  181. },
  182. onLoad() {
  183. // 检查是否需要切换到作品标签
  184. const activeTab = uni.getStorageSync('activeBookshelfTab')
  185. // if (activeTab === 'work') {
  186. // this.activeTab = 'work'
  187. // this.mixinsListApi = this.apiMap[tab]
  188. // uni.removeStorageSync('activeBookshelfTab')
  189. // }
  190. // 监听切换到作品标签的事件
  191. // uni.$on('switchToWork', () => {
  192. // this.activeTab = 'work'
  193. // })
  194. if(this.isLogin){
  195. this.mixinsListApi = this.apiMap[this.activeTab]
  196. this.$store.commit('getUserInfo')
  197. }
  198. },
  199. onShow() {
  200. // this.isEditMode = false;
  201. // this.selectedItems = [];
  202. if(this.isLogin){
  203. this.mixinsListApi = this.apiMap[this.activeTab]
  204. }
  205. },
  206. onUnload() {
  207. // 移除事件监听
  208. uni.$off('switchToWork')
  209. },
  210. methods: {
  211. // 切换标签
  212. switchTab(tab) {
  213. this.activeTab = tab;
  214. if(this.isLogin){
  215. this.mixinsListApi = this.apiMap[tab]
  216. }
  217. this.list = []
  218. this.getData()
  219. // 退出编辑模式
  220. this.exitEditMode();
  221. },
  222. // 跳转到小说阅读页
  223. toNovelDetail(novel) {
  224. // 如果有阅读记录(novelId存在),直接跳转到该章节
  225. if (novel.novelId) {
  226. uni.navigateTo({
  227. url: `/pages_order/novel/readnovels?id=${novel.shopId}&cid=${novel.novelId}`
  228. });
  229. } else {
  230. // 没有阅读记录,获取第一章然后跳转
  231. this.getFirstChapterAndRead(novel.shopId);
  232. }
  233. },
  234. // 获取第一章并跳转阅读
  235. getFirstChapterAndRead(bookId) {
  236. this.$fetch('getBookCatalogList', {
  237. bookId: bookId,
  238. pageNo: 1,
  239. pageSize: 1,
  240. reverse: 0, // 正序获取第一章
  241. }).then(res => {
  242. if (res.records && res.records.length > 0) {
  243. const firstChapter = res.records[0];
  244. uni.navigateTo({
  245. url: `/pages_order/novel/readnovels?id=${bookId}&cid=${firstChapter.id}`
  246. });
  247. } else {
  248. uni.showToast({
  249. title: '暂无章节内容',
  250. icon: 'none'
  251. });
  252. }
  253. }).catch(err => {
  254. console.error('获取章节列表失败:', err);
  255. uni.showToast({
  256. title: '获取章节失败',
  257. icon: 'none'
  258. });
  259. });
  260. },
  261. // 跳转到作品详情页
  262. toWorkDetail(id) {
  263. console.log(id);
  264. uni.navigateTo({
  265. url: '/pages/work/detail?id=' + id
  266. })
  267. },
  268. // 创建新作品
  269. createNewWork() {
  270. uni.navigateTo({
  271. url: '/pages_order/novel/createNovel'
  272. })
  273. },
  274. // 跳转到读者成就设置
  275. toReaderSettings() {
  276. uni.navigateTo({
  277. url: '/pages_order/novel/ReaderAchievement'
  278. })
  279. },
  280. // 进入编辑模式
  281. enterEditMode() {
  282. this.isEditMode = true;
  283. this.selectedItems = [];
  284. },
  285. // 退出编辑模式
  286. exitEditMode() {
  287. this.isEditMode = false;
  288. this.selectedItems = [];
  289. },
  290. // 切换选择状态
  291. toggleSelect(item) {
  292. const index = this.selectedItems.indexOf(item.id);
  293. if (index === -1) {
  294. this.selectedItems.push(item.id);
  295. } else {
  296. this.selectedItems.splice(index, 1);
  297. }
  298. },
  299. // 全选
  300. selectAll() {
  301. if (this.activeTab === 'read') {
  302. // 已经全选,则取消全选
  303. if (this.selectedItems.length === this.novels.length) {
  304. this.selectedItems = [];
  305. } else {
  306. // 全选所有小说
  307. this.selectedItems = this.novels.map(novel => novel.id);
  308. }
  309. } else {
  310. // 已经全选,则取消全选
  311. if (this.selectedItems.length === this.list.length) {
  312. this.selectedItems = [];
  313. } else {
  314. // 全选所有作品
  315. this.selectedItems = this.list.map(work => work.id);
  316. }
  317. }
  318. },
  319. // 移除选中的项目
  320. removeSelected() {
  321. if (this.selectedItems.length === 0) {
  322. uni.showToast({
  323. title: '请先选择项目',
  324. icon: 'none'
  325. });
  326. return;
  327. }
  328. const title = this.activeTab === 'read' ? '移出书架' : '删除作品';
  329. const content = this.activeTab === 'read'
  330. ? `确定要将选中的${this.selectedItems.length}本小说移出书架吗?`
  331. : `确定要删除选中的${this.selectedItems.length}部作品吗?`;
  332. uni.showModal({
  333. title: '提示',
  334. content: content,
  335. success: async (res) => {
  336. if (res.confirm) {
  337. if (this.activeTab === 'read') {
  338. // 移除选中的小说
  339. await this.$fetch('batchRemoveReadBook', {
  340. bookIds : this.selectedItems.join(',')
  341. })
  342. uni.showToast({
  343. title: '移除成功',
  344. icon: 'success'
  345. });
  346. this.getData()
  347. } else {
  348. // 删除选中的作品
  349. await this.$fetch('deleteMyShopList', {
  350. bookIds: this.selectedItems.join(',')
  351. })
  352. uni.showToast({
  353. title: '删除成功',
  354. icon: 'success'
  355. });
  356. // 重新获取列表数据
  357. this.getData()
  358. }
  359. this.selectedItems = [];
  360. // 如果没有数据了,退出编辑模式
  361. if ((this.activeTab === 'read' && this.list.length === 0) ||
  362. (this.activeTab === 'work' && this.list.length === 0)) {
  363. this.exitEditMode();
  364. }
  365. }
  366. }
  367. });
  368. },
  369. }
  370. }
  371. </script>
  372. <style scoped lang="scss">
  373. .page {
  374. background-color: #ffffff;
  375. min-height: 100vh;
  376. position: relative;
  377. padding-bottom: calc(120rpx + env(safe-area-inset-bottom));
  378. box-sizing: border-box;
  379. }
  380. .header {
  381. display: flex;
  382. flex-direction: column;
  383. justify-content: flex-end;
  384. position: sticky;
  385. top: 0;
  386. z-index: 100;
  387. background-color: #ffffff;
  388. box-sizing: border-box;
  389. width: 100%;
  390. border-bottom: 1rpx solid #f5f5f5;
  391. padding-top: calc(var(--status-bar-height) + 20rpx);
  392. .header-content {
  393. display: flex;
  394. justify-content: center;
  395. align-items: center;
  396. padding: 20rpx 30rpx;
  397. padding-bottom: 24rpx;
  398. width: 100%;
  399. }
  400. .tab-container {
  401. display: flex;
  402. align-items: center;
  403. font-size: 34rpx;
  404. .tab {
  405. margin-right: 40rpx;
  406. color: #999;
  407. position: relative;
  408. padding: 10rpx 0;
  409. &.active {
  410. color: #000;
  411. font-weight: bold;
  412. font-size: 36rpx;
  413. &::after {
  414. content: '';
  415. position: absolute;
  416. bottom: 0;
  417. left: 50%;
  418. transform: translateX(-50%);
  419. width: 40rpx;
  420. height: 6rpx;
  421. background-color: #000;
  422. border-radius: 3rpx;
  423. }
  424. }
  425. }
  426. }
  427. .header-right {
  428. display: flex;
  429. align-items: center;
  430. .header-icon {
  431. margin-left: 30rpx;
  432. height: 80rpx;
  433. display: flex;
  434. align-items: center;
  435. justify-content: center;
  436. text {
  437. font-size: 28rpx;
  438. color: #333;
  439. }
  440. }
  441. }
  442. }
  443. .empty-works {
  444. width: 100%;
  445. padding: 100rpx 0;
  446. display: flex;
  447. flex-direction: column;
  448. align-items: center;
  449. justify-content: center;
  450. .empty-text {
  451. font-size: 32rpx;
  452. color: #666;
  453. margin-bottom: 20rpx;
  454. }
  455. .empty-tips {
  456. font-size: 28rpx;
  457. color: #999;
  458. }
  459. }
  460. .novel-grid {
  461. padding: 20rpx;
  462. padding-top: 30rpx;
  463. padding-bottom: env(safe-area-inset-bottom);
  464. box-sizing: border-box;
  465. .novel-row {
  466. display: flex;
  467. margin-bottom: 40rpx;
  468. .novel-item {
  469. width: 33%;
  470. position: relative;
  471. .novel-tag {
  472. position: absolute;
  473. top: 10rpx;
  474. right: 10rpx;
  475. background-color: rgba(0, 0, 0, 0.6);
  476. color: #fff;
  477. font-size: 20rpx;
  478. padding: 4rpx 10rpx;
  479. border-radius: 6rpx;
  480. z-index: 1;
  481. }
  482. .novel-original {
  483. position: absolute;
  484. top: 10rpx;
  485. right: 10rpx;
  486. background-color: #ff9900;
  487. color: #fff;
  488. font-size: 20rpx;
  489. padding: 4rpx 10rpx;
  490. border-radius: 6rpx;
  491. z-index: 1;
  492. }
  493. }
  494. }
  495. }
  496. /* 作品列表相关样式 */
  497. .works-container {
  498. padding: 30rpx;
  499. .works-header {
  500. margin-bottom: 30rpx;
  501. }
  502. .works-content {
  503. display: flex;
  504. flex-direction: column;
  505. .work-item-wrapper {
  506. position: relative;
  507. margin-bottom: 20rpx;
  508. }
  509. }
  510. }
  511. .bottom-action-bar {
  512. position: fixed;
  513. bottom: 0;
  514. left: 0;
  515. width: 100%;
  516. height: calc(120rpx + env(safe-area-inset-bottom));
  517. background-color: #fff;
  518. border-top: 1rpx solid #eee;
  519. display: flex;
  520. justify-content: space-around;
  521. align-items: center;
  522. z-index: 99;
  523. padding-bottom: env(safe-area-inset-bottom);
  524. .action-item {
  525. display: flex;
  526. flex-direction: column;
  527. align-items: center;
  528. justify-content: center;
  529. .action-icon {
  530. width: 80rpx;
  531. height: 80rpx;
  532. display: flex;
  533. align-items: center;
  534. justify-content: center;
  535. }
  536. text {
  537. font-size: 24rpx;
  538. color: #666;
  539. margin-top: 8rpx;
  540. }
  541. }
  542. }
  543. // 编辑模式公共样式
  544. .edit-mode {
  545. .item-checkbox {
  546. position: absolute;
  547. right: 20rpx;
  548. top: 230rpx;
  549. z-index: 10;
  550. background-color: rgba(255,255,255,0.8);
  551. border-radius: 50%;
  552. width: 40rpx;
  553. height: 40rpx;
  554. display: flex;
  555. align-items: center;
  556. justify-content: center;
  557. .checkbox-inner {
  558. width: 40rpx;
  559. height: 40rpx;
  560. background-color: $uni-color;
  561. border-radius: 50%;
  562. display: flex;
  563. align-items: center;
  564. justify-content: center;
  565. }
  566. .checkbox-inner-no {
  567. width: 40rpx;
  568. height: 40rpx;
  569. background-color: #fff;
  570. border-radius: 50%;
  571. display: flex;
  572. align-items: center;
  573. justify-content: center;
  574. }
  575. }
  576. .item-checkbox-work {
  577. position: absolute;
  578. right: 20rpx;
  579. top: 20rpx;
  580. z-index: 10;
  581. background-color: rgba(255,255,255,0.8);
  582. border-radius: 50%;
  583. width: 40rpx;
  584. height: 40rpx;
  585. display: flex;
  586. align-items: center;
  587. justify-content: center;
  588. .checkbox-inner {
  589. width: 40rpx;
  590. height: 40rpx;
  591. background-color: $uni-color;
  592. border-radius: 50%;
  593. display: flex;
  594. align-items: center;
  595. justify-content: center;
  596. }
  597. .checkbox-inner-no {
  598. width: 40rpx;
  599. height: 40rpx;
  600. background-color: #fff;
  601. border: 2rpx solid #ddd;
  602. border-radius: 50%;
  603. display: flex;
  604. align-items: center;
  605. justify-content: center;
  606. }
  607. }
  608. .work-item-wrapper {
  609. position: relative;
  610. }
  611. }
  612. </style>