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

  1. <template>
  2. <!-- 书架页面 -->
  3. <view class="page">
  4. <!-- 头部标签切换 -->
  5. <view class="header" :style="{ paddingTop: `${statusBarHeight}px` }">
  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 row"
  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 worksList"
  43. :key="work.id"
  44. :work="work"
  45. @click="toWorkDetail(work.id)"
  46. @longpress="enterEditMode"
  47. />
  48. <!-- 空状态提示 -->
  49. <view class="empty-works" v-if="worksList.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 row"
  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 worksList"
  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 { mapGetters } from 'vuex'
  129. export default {
  130. components : {
  131. tabber,
  132. novelItem,
  133. workItem,
  134. newWorkItem
  135. },
  136. computed : {
  137. ...mapGetters(['userShop']),
  138. // 将小说列表分成每行3个的二维数组
  139. novelRows() {
  140. const rows = [];
  141. const itemsPerRow = 3;
  142. for (let i = 0; i < this.novels.length; i += itemsPerRow) {
  143. rows.push(this.novels.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. novels: [
  156. {
  157. id: '1',
  158. title: '我是半妖',
  159. cover: 'https://tse4-mm.cn.bing.net/th/id/OIP-C.iUyxJ_fxLjjX3kEBjteXWwAAAA?rs=1&pid=ImgDetMain',
  160. author: '炎兰',
  161. desc: '都市玄幻小说,主角获得半妖化能力,通过吸收妖气不断变强...',
  162. tags: ['玄幻', '都市', '热血'],
  163. status: '连载中'
  164. },
  165. {
  166. id: '2',
  167. title: '兽王进化:从被小萝莉召唤开始',
  168. cover: 'https://tse4-mm.cn.bing.net/th/id/OIP-C.iUyxJ_fxLjjX3kEBjteXWwAAAA?rs=1&pid=ImgDetMain',
  169. author: '九灵',
  170. desc: '一场意外让主角获得兽王血脉,开始了进化之路...',
  171. tags: ['奇幻', '冒险'],
  172. isOriginal: true,
  173. status: '连载中'
  174. },
  175. {
  176. id: '3',
  177. title: '魔法少女纯爷们',
  178. cover: 'https://tse4-mm.cn.bing.net/th/id/OIP-C.iUyxJ_fxLjjX3kEBjteXWwAAAA?rs=1&pid=ImgDetMain',
  179. author: '烟火',
  180. desc: '一个普通男孩意外获得魔法少女的力量,开始了奇妙冒险...',
  181. tags: ['搞笑', '奇幻'],
  182. status: '已完结'
  183. },
  184. {
  185. id: '4',
  186. title: '我是一条小青龙',
  187. cover: 'https://tse4-mm.cn.bing.net/th/id/OIP-C.iUyxJ_fxLjjX3kEBjteXWwAAAA?rs=1&pid=ImgDetMain',
  188. author: '东升',
  189. desc: '重生为一条小青龙,主角在修仙世界中成长的故事...',
  190. tags: ['仙侠', '修真'],
  191. tag: '独家',
  192. status: '连载中'
  193. },
  194. {
  195. id: '5',
  196. title: '女帝:别闹,朕怀孕了!',
  197. cover: 'https://tse4-mm.cn.bing.net/th/id/OIP-C.iUyxJ_fxLjjX3kEBjteXWwAAAA?rs=1&pid=ImgDetMain',
  198. author: '君临',
  199. desc: '一代女帝意外穿越成了皇帝,却发现自己怀孕了...',
  200. tags: ['宫廷', '穿越'],
  201. isOriginal: true,
  202. status: '连载中'
  203. },
  204. {
  205. id: '6',
  206. title: '中国式应酬——应酬是门技术活',
  207. cover: 'https://tse4-mm.cn.bing.net/th/id/OIP-C.iUyxJ_fxLjjX3kEBjteXWwAAAA?rs=1&pid=ImgDetMain',
  208. author: '商业顾问',
  209. desc: '一本教你如何在商业场合应对各种应酬的实用指南...',
  210. tags: ['商业', '实用'],
  211. status: '已完结'
  212. },
  213. {
  214. id: '7',
  215. title: '苏世民:我的经验与教训',
  216. cover: 'https://tse4-mm.cn.bing.net/th/id/OIP-C.iUyxJ_fxLjjX3kEBjteXWwAAAA?rs=1&pid=ImgDetMain',
  217. author: '苏世民',
  218. desc: '黑石集团创始人苏世民的商业回忆录...',
  219. tags: ['传记', '商业'],
  220. status: '已完结'
  221. },
  222. {
  223. id: '8',
  224. title: '认知觉醒:开启自我改变的原动力',
  225. cover: 'https://tse4-mm.cn.bing.net/th/id/OIP-C.iUyxJ_fxLjjX3kEBjteXWwAAAA?rs=1&pid=ImgDetMain',
  226. author: '周岭',
  227. desc: '帮助你打破思维局限,重塑认知结构的心理学著作...',
  228. tags: ['心理', '自助'],
  229. status: '已完结'
  230. },
  231. {
  232. id: '9',
  233. title: '纳瓦尔宝典',
  234. cover: 'https://tse4-mm.cn.bing.net/th/id/OIP-C.iUyxJ_fxLjjX3kEBjteXWwAAAA?rs=1&pid=ImgDetMain',
  235. author: 'Naval',
  236. desc: '硅谷天使投资人纳瓦尔·拉维坎特的人生智慧...',
  237. tags: ['哲学', '投资'],
  238. status: '已完结'
  239. }
  240. ],
  241. // 作品列表数据
  242. worksList: [
  243. {
  244. id: '9',
  245. title: '纳瓦尔宝典',
  246. cover: 'https://tse4-mm.cn.bing.net/th/id/OIP-C.iUyxJ_fxLjjX3kEBjteXWwAAAA?rs=1&pid=ImgDetMain',
  247. author: 'Naval',
  248. desc: '硅谷天使投资人纳瓦尔·拉维坎特的人生智慧...',
  249. tags: ['哲学', '投资'],
  250. status: '已完结'
  251. }
  252. ] // 清空初始数据,改为动态加载
  253. }
  254. },
  255. onLoad() {
  256. // 获取系统信息
  257. const systemInfo = uni.getSystemInfoSync();
  258. this.statusBarHeight = systemInfo.statusBarHeight;
  259. // 检查是否需要切换到作品标签
  260. const activeTab = uni.getStorageSync('activeBookshelfTab')
  261. if (activeTab === 'work') {
  262. this.activeTab = 'work'
  263. uni.removeStorageSync('activeBookshelfTab')
  264. }
  265. // 监听切换到作品标签的事件
  266. uni.$on('switchToWork', () => {
  267. this.activeTab = 'work'
  268. })
  269. // #ifdef MP-WEIXIN || MP-QQ || MP-TOUTIAO
  270. const menuButtonInfo = uni.getMenuButtonBoundingClientRect();
  271. const navBarHeight = (menuButtonInfo.top - systemInfo.statusBarHeight) * 2 + menuButtonInfo.height + systemInfo.statusBarHeight;
  272. this.navBarHeight = navBarHeight;
  273. // #endif
  274. },
  275. onShow() {
  276. // 检查是否需要弹窗
  277. const pages = getCurrentPages();
  278. const current = pages[pages.length - 1];
  279. if (current.options && current.options.fromPublish === '1') {
  280. this.activeTab = 'work';
  281. uni.showToast({
  282. title: '发布成功',
  283. icon: 'success'
  284. });
  285. // 移除参数,防止返回时重复弹窗
  286. delete current.options.fromPublish;
  287. }
  288. this.isEditMode = false;
  289. this.selectedItems = [];
  290. },
  291. onUnload() {
  292. // 移除事件监听
  293. uni.$off('switchToWork')
  294. },
  295. methods: {
  296. // 切换标签
  297. switchTab(tab) {
  298. this.activeTab = tab;
  299. // 退出编辑模式
  300. this.exitEditMode();
  301. },
  302. // 跳转到小说阅读页
  303. toNovelDetail(id) {
  304. uni.navigateTo({
  305. url: '/pages_order/novel/readnovels?id=' + id
  306. })
  307. },
  308. // 跳转到作品详情页
  309. toWorkDetail(id) {
  310. uni.navigateTo({
  311. url: '/pages/work/detail?id=' + id
  312. })
  313. },
  314. // 创建新作品
  315. createNewWork() {
  316. uni.navigateTo({
  317. url: '/pages_order/novel/createNovel'
  318. })
  319. },
  320. // 跳转到读者成就设置
  321. toReaderSettings() {
  322. uni.navigateTo({
  323. url: '/pages_order/novel/ReaderAchievement'
  324. })
  325. },
  326. // 进入编辑模式
  327. enterEditMode() {
  328. this.isEditMode = true;
  329. this.selectedItems = [];
  330. },
  331. // 退出编辑模式
  332. exitEditMode() {
  333. this.isEditMode = false;
  334. this.selectedItems = [];
  335. },
  336. // 切换选择状态
  337. toggleSelect(item, type) {
  338. const index = this.selectedItems.indexOf(item.id);
  339. if (index === -1) {
  340. this.selectedItems.push(item.id);
  341. } else {
  342. this.selectedItems.splice(index, 1);
  343. }
  344. },
  345. // 全选
  346. selectAll() {
  347. if (this.activeTab === 'read') {
  348. // 已经全选,则取消全选
  349. if (this.selectedItems.length === this.novels.length) {
  350. this.selectedItems = [];
  351. } else {
  352. // 全选所有小说
  353. this.selectedItems = this.novels.map(novel => novel.id);
  354. }
  355. } else {
  356. // 已经全选,则取消全选
  357. if (this.selectedItems.length === this.worksList.length) {
  358. this.selectedItems = [];
  359. } else {
  360. // 全选所有作品
  361. this.selectedItems = this.worksList.map(work => work.id);
  362. }
  363. }
  364. },
  365. // 移除选中的项目
  366. removeSelected() {
  367. if (this.selectedItems.length === 0) {
  368. uni.showToast({
  369. title: '请先选择项目',
  370. icon: 'none'
  371. });
  372. return;
  373. }
  374. const title = this.activeTab === 'read' ? '移出书架' : '删除作品';
  375. const content = this.activeTab === 'read'
  376. ? `确定要将选中的${this.selectedItems.length}本小说移出书架吗?`
  377. : `确定要删除选中的${this.selectedItems.length}部作品吗?`;
  378. uni.showModal({
  379. title: '提示',
  380. content: content,
  381. success: (res) => {
  382. if (res.confirm) {
  383. if (this.activeTab === 'read') {
  384. // 移除选中的小说
  385. this.novels = this.novels.filter(novel => !this.selectedItems.includes(novel.id));
  386. uni.showToast({
  387. title: '移除成功',
  388. icon: 'success'
  389. });
  390. } else {
  391. // 删除选中的作品
  392. this.worksList = this.worksList.filter(work => !this.selectedItems.includes(work.id));
  393. // 保存更新后的作品列表
  394. uni.setStorageSync('worksList', this.worksList)
  395. uni.showToast({
  396. title: '删除成功',
  397. icon: 'success'
  398. });
  399. }
  400. this.selectedItems = [];
  401. // 如果没有数据了,退出编辑模式
  402. if ((this.activeTab === 'read' && this.novels.length === 0) ||
  403. (this.activeTab === 'work' && this.worksList.length === 0)) {
  404. this.exitEditMode();
  405. }
  406. }
  407. }
  408. });
  409. },
  410. // 加载作品列表
  411. loadWorksList() {
  412. const savedWorks = uni.getStorageSync('worksList') || []
  413. this.worksList = savedWorks
  414. }
  415. }
  416. }
  417. </script>
  418. <style scoped lang="scss">
  419. .page {
  420. background-color: #ffffff;
  421. min-height: 100vh;
  422. position: relative;
  423. padding-bottom: calc(120rpx + env(safe-area-inset-bottom));
  424. box-sizing: border-box;
  425. }
  426. .header {
  427. display: flex;
  428. flex-direction: column;
  429. justify-content: flex-end;
  430. position: sticky;
  431. top: 0;
  432. z-index: 100;
  433. background-color: #ffffff;
  434. box-sizing: border-box;
  435. width: 100%;
  436. border-bottom: 1rpx solid #f5f5f5;
  437. padding-top: constant(safe-area-inset-top); /* iOS 11.0 */
  438. padding-top: env(safe-area-inset-top); /* iOS 11.2+ */
  439. .header-content {
  440. display: flex;
  441. justify-content: center;
  442. align-items: center;
  443. padding: 20rpx 30rpx;
  444. padding-bottom: 24rpx;
  445. width: 100%;
  446. }
  447. .tab-container {
  448. display: flex;
  449. align-items: center;
  450. font-size: 34rpx;
  451. .tab {
  452. margin-right: 40rpx;
  453. color: #999;
  454. position: relative;
  455. padding: 10rpx 0;
  456. &.active {
  457. color: #000;
  458. font-weight: bold;
  459. font-size: 36rpx;
  460. &::after {
  461. content: '';
  462. position: absolute;
  463. bottom: 0;
  464. left: 50%;
  465. transform: translateX(-50%);
  466. width: 40rpx;
  467. height: 6rpx;
  468. background-color: #000;
  469. border-radius: 3rpx;
  470. }
  471. }
  472. }
  473. }
  474. .header-right {
  475. display: flex;
  476. align-items: center;
  477. .header-icon {
  478. margin-left: 30rpx;
  479. height: 80rpx;
  480. display: flex;
  481. align-items: center;
  482. justify-content: center;
  483. text {
  484. font-size: 28rpx;
  485. color: #333;
  486. }
  487. }
  488. }
  489. }
  490. .novel-grid {
  491. padding: 20rpx;
  492. padding-top: 30rpx;
  493. padding-bottom: env(safe-area-inset-bottom);
  494. box-sizing: border-box;
  495. .novel-row {
  496. display: flex;
  497. justify-content: space-between;
  498. margin-bottom: 40rpx;
  499. .novel-item {
  500. width: 31%;
  501. position: relative;
  502. .novel-tag {
  503. position: absolute;
  504. top: 10rpx;
  505. right: 10rpx;
  506. background-color: rgba(0, 0, 0, 0.6);
  507. color: #fff;
  508. font-size: 20rpx;
  509. padding: 4rpx 10rpx;
  510. border-radius: 6rpx;
  511. z-index: 1;
  512. }
  513. .novel-original {
  514. position: absolute;
  515. top: 10rpx;
  516. right: 10rpx;
  517. background-color: #ff9900;
  518. color: #fff;
  519. font-size: 20rpx;
  520. padding: 4rpx 10rpx;
  521. border-radius: 6rpx;
  522. z-index: 1;
  523. }
  524. }
  525. }
  526. }
  527. /* 作品列表相关样式 */
  528. .works-container {
  529. padding: 30rpx;
  530. .works-header {
  531. margin-bottom: 30rpx;
  532. }
  533. .works-content {
  534. display: flex;
  535. flex-direction: column;
  536. .work-item-wrapper {
  537. position: relative;
  538. margin-bottom: 20rpx;
  539. }
  540. .empty-works {
  541. width: 100%;
  542. padding: 100rpx 0;
  543. display: flex;
  544. flex-direction: column;
  545. align-items: center;
  546. justify-content: center;
  547. .empty-text {
  548. font-size: 32rpx;
  549. color: #666;
  550. margin-bottom: 20rpx;
  551. }
  552. .empty-tips {
  553. font-size: 28rpx;
  554. color: #999;
  555. }
  556. }
  557. }
  558. }
  559. .bottom-action-bar {
  560. position: fixed;
  561. bottom: 0;
  562. left: 0;
  563. width: 100%;
  564. height: calc(120rpx + env(safe-area-inset-bottom));
  565. background-color: #fff;
  566. border-top: 1rpx solid #eee;
  567. display: flex;
  568. justify-content: space-around;
  569. align-items: center;
  570. z-index: 99;
  571. padding-bottom: env(safe-area-inset-bottom);
  572. .action-item {
  573. display: flex;
  574. flex-direction: column;
  575. align-items: center;
  576. justify-content: center;
  577. .action-icon {
  578. width: 80rpx;
  579. height: 80rpx;
  580. display: flex;
  581. align-items: center;
  582. justify-content: center;
  583. }
  584. text {
  585. font-size: 24rpx;
  586. color: #666;
  587. margin-top: 8rpx;
  588. }
  589. }
  590. }
  591. // 编辑模式公共样式
  592. .edit-mode {
  593. .item-checkbox {
  594. position: absolute;
  595. top: 10rpx;
  596. left: 10rpx;
  597. z-index: 10;
  598. background-color: rgba(255,255,255,0.8);
  599. border-radius: 50%;
  600. width: 40rpx;
  601. height: 40rpx;
  602. display: flex;
  603. align-items: center;
  604. justify-content: center;
  605. .checkbox-inner {
  606. width: 40rpx;
  607. height: 40rpx;
  608. background-color: #1989fa;
  609. border-radius: 50%;
  610. display: flex;
  611. align-items: center;
  612. justify-content: center;
  613. }
  614. .checkbox-inner-no {
  615. width: 40rpx;
  616. height: 40rpx;
  617. background-color: #fff;
  618. border-radius: 50%;
  619. display: flex;
  620. align-items: center;
  621. justify-content: center;
  622. }
  623. }
  624. }
  625. </style>