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

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