小说小程序前端代码仓库(小程序)
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.

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