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

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