index.vue 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669
  1. <template>
  2. <view class="cart-page">
  3. <page-scroll
  4. ref="pageRef"
  5. url="/token/shop/cart/list"
  6. method="post"
  7. :page-size="10"
  8. :immediate="false"
  9. slotEmpty
  10. emptyText="购物车空空如也"
  11. height="100vh"
  12. bgColor="#f5f5f5"
  13. @updateList="handleUpdateList"
  14. >
  15. <view class="cart-list" v-if="cartList.length > 0">
  16. <uni-swipe-action>
  17. <uni-swipe-action-item v-for="(item, index) in cartList" :key="item.id" :right-options="uniActionOptions" @click="uniClickAction($event, index)" @change="uniSwipeChange($event, index)" :show="item.show">
  18. <cart-item :item="item" @check="handleCheck" @changeNum="handleChangeNum" @reduce="handleReduce"
  19. @selectCondition="onSelectCondition"></cart-item>
  20. </uni-swipe-action-item>
  21. </uni-swipe-action>
  22. </view>
  23. <!-- 为你推荐 -->
  24. <view class="recommend-section" v-if="recommendList.length > 0">
  25. <view class="section-title">
  26. <text class="line"></text>
  27. <text class="text">为你推荐</text>
  28. <text class="line"></text>
  29. </view>
  30. <view class="recommend-grid">
  31. <view class="grid-item" v-for="(item, index) in recommendList" :key="index">
  32. <image :src="item.cover" mode="aspectFill" class="cover"></image>
  33. <view class="title">{{ item.title }}</view>
  34. </view>
  35. </view>
  36. </view>
  37. <!-- 底部占位,避免被结算栏遮挡 -->
  38. <view style="height: 120rpx;"></view>
  39. </page-scroll>
  40. <!-- 品相切换弹窗 -->
  41. <condition-popup ref="conditionPopup" @select="handleConditionUpdate"></condition-popup>
  42. <!-- 悬浮清空按钮 -->
  43. <!-- <view class="floating-clear-btn" v-if="cartList.length > 0" @click="handleClearCart">
  44. <u-icon name="trash" size="36" color="#fa3534"></u-icon>
  45. </view> -->
  46. <!-- 底部结算栏 -->
  47. <view class="bottom-fixed" v-if="cartList.length > 0">
  48. <view class="left-part">
  49. <view class="checkbox-wrap" @click="toggleSelectAll" style="display: flex; align-items: center; height: 100%; padding-right: 20rpx;">
  50. <u-checkbox v-model="isAllSelected" shape="circle" active-color="#38C148" :disabled="false"
  51. @change="onAllCheckChange" style="pointer-events: none;">全选</u-checkbox>
  52. </view>
  53. </view>
  54. <view class="right-part">
  55. <view class="info-wrapper">
  56. <view class="total-info">
  57. <view class="total-price">
  58. <text class="label">合计:</text>
  59. <text class="price">¥{{ totalPrice }}</text>
  60. </view>
  61. <view class="reduced-tip" v-if="totalReduced > 0">已降 ¥{{ totalReduced }}</view>
  62. </view>
  63. <view class="freight-template" @click="handleGetFreightTemplate" v-if="shippingText">
  64. {{ shippingText }}
  65. </view>
  66. </view>
  67. <view class="checkout-btn" @click="checkout">
  68. 结算({{ selectedCount }})
  69. </view>
  70. </view>
  71. </view>
  72. <!-- 减钱弹窗 -->
  73. <price-reduction-popup ref="reducePopup" @share="handleShare" @scan="handleScan"></price-reduction-popup>
  74. <reduce-qrcode ref="reduceQrcode"></reduce-qrcode>
  75. <common-dialog ref="reduceDialog" title="温馨提示" @confirm="onNext">
  76. <text>购物车中有可减钱的商品,如您提交订单,则失去该资格哦~</text>
  77. </common-dialog>
  78. </view>
  79. </template>
  80. <script>
  81. import CartItem from '../components/cart-item.vue';
  82. import PriceReductionPopup from '@/components/price-reduction-popup/price-reduction-popup.vue';
  83. import ReduceQrcode from '@/components/reduce-qrcode/reduce-qrcode.vue';
  84. import CommonDialog from '@/components/common-dialog.vue';
  85. import ConditionPopup from '../components/condition-popup.vue';
  86. import pageScroll from '@/components/pageScroll/index.vue';
  87. import { eventBus } from '@/utils/event-bus.js';
  88. export default {
  89. components: {
  90. CartItem,
  91. PriceReductionPopup,
  92. ReduceQrcode,
  93. CommonDialog,
  94. ConditionPopup,
  95. pageScroll
  96. },
  97. data() {
  98. return {
  99. currentItem: null,
  100. isAllSelected: false,
  101. cartList: [],
  102. actionOptions: [
  103. {
  104. text: '删除',
  105. style: {
  106. backgroundColor: '#fa3534'
  107. }
  108. }
  109. ],
  110. uniActionOptions: [
  111. {
  112. text: '删除',
  113. style: {
  114. backgroundColor: '#fa3534'
  115. }
  116. }
  117. ],
  118. recommendList: [],
  119. shippingText: ''
  120. };
  121. },
  122. onShow() {
  123. this.refreshCartPage();
  124. },
  125. created() {
  126. // Tab 页通过事件触发刷新,避免父页面访问 $refs 时占位组件未就绪报错
  127. this.cartPageRefreshHandler = () => {
  128. this.refreshCartPage();
  129. };
  130. eventBus.on('cartPageRefresh', this.cartPageRefreshHandler);
  131. },
  132. beforeDestroy() {
  133. if (this.cartPageRefreshHandler) {
  134. eventBus.off('cartPageRefresh', this.cartPageRefreshHandler);
  135. }
  136. },
  137. mounted() {
  138. this.refreshCartPage();
  139. },
  140. // 分享配置
  141. onShareAppMessage(res) {
  142. console.log(res, '分享');
  143. if (res.from === "button" && res.target && res.target.dataset && res.target.dataset.shareType === "reduce") {
  144. let reduceCode = uni.getStorageSync("reduceCodeShare");
  145. // 调用分享接口
  146. this.$u.api.goToReduceShareAjax({ reduceCode: reduceCode });
  147. return {
  148. title: "快来帮我减钱买书吧!",
  149. path: "/pages/sell/index?reduceCode=" + reduceCode,
  150. imageUrl: "https://shuhi.oss-cn-qingdao.aliyuncs.com/mini/share.jpg",
  151. desc: "书嗨,专注于书籍交易的平台,提供新书和二手书的买卖服务",
  152. };
  153. } else {
  154. return {
  155. title: "书嗨",
  156. page: "/pages/home/index",
  157. };
  158. }
  159. },
  160. // 分享到朋友圈
  161. onShareTimeline(res) {
  162. if (res.from === "button") {
  163. let reduceCode = uni.getStorageSync("reduceCodeShare");
  164. // 调用分享接口
  165. this.$u.api.goToReduceShareAjax({ reduceCode: reduceCode });
  166. return {
  167. title: "快来帮我减钱买书吧!",
  168. path: "/pages/sell/index?reduceCode=" + reduceCode,
  169. imageUrl: "https://shuhi.oss-cn-qingdao.aliyuncs.com/mini/share.jpg",
  170. desc: "书嗨,专注于书籍交易的平台,提供新书和二手书的买卖服务",
  171. };
  172. } else {
  173. return {
  174. title: "书嗨",
  175. page: "/pages/home/index",
  176. };
  177. }
  178. },
  179. computed: {
  180. selectedItems() {
  181. return this.cartList.filter(item => item.checked && item.stockStatus !== 3 && item.availableStock > 0);
  182. },
  183. totalPrice() {
  184. const sumPrice = this.selectedItems.reduce((sum, item) => sum + item.price * item.quantity, 0);
  185. const sumReduce = this.selectedItems.reduce((sum, item) => sum + (item.currReduceMoney || 0), 0);
  186. const result = sumPrice - sumReduce;
  187. return result > 0 ? result.toFixed(2) : "0.00";
  188. },
  189. totalReduced() {
  190. return this.selectedItems.reduce((sum, item) => sum + (item.currReduceMoney || 0), 0).toFixed(2);
  191. },
  192. selectedCount() {
  193. return this.selectedItems.reduce((sum, item) => sum + item.quantity, 0);
  194. }
  195. },
  196. watch: {
  197. cartList: {
  198. handler(val) {
  199. const validItems = val.filter(item => item.stockStatus !== 3 && item.availableStock > 0);
  200. if (validItems.length === 0) {
  201. this.isAllSelected = false;
  202. return;
  203. }
  204. this.isAllSelected = validItems.every(item => item.checked);
  205. },
  206. deep: true
  207. }
  208. },
  209. methods: {
  210. refreshCartPage() {
  211. this.$nextTick(() => {
  212. this.$refs.pageRef?.loadData(true);
  213. });
  214. this.getShippingText();
  215. this.$updateCartBadge();
  216. },
  217. handleUpdateList(list) {
  218. const storageCheckedIds = uni.getStorageSync('cartCheckedIds') || [];
  219. const prevCheckedIds = this.cartList.filter(i => i.checked).map(i => i.id);
  220. const checkedIds = [...new Set([...storageCheckedIds, ...prevCheckedIds])];
  221. this.cartList = (list || []).map(item => ({
  222. ...item,
  223. checked: checkedIds.includes(item.id),
  224. show: 'none'
  225. }));
  226. const finalCheckedIds = this.cartList.filter(i => i.checked).map(i => i.id);
  227. uni.setStorageSync('cartCheckedIds', finalCheckedIds);
  228. eventBus.emit('cartPageLoadDone');
  229. },
  230. getShippingText() {
  231. this.$u.api.getShippingTempleteAjax().then(res => {
  232. if (res.code === 200) {
  233. this.shippingText = res.data.templeteName;
  234. }
  235. });
  236. },
  237. handleGetFreightTemplate() {
  238. console.log('获取运费模版');
  239. },
  240. swipeStart(index) {
  241. // this.cartList.forEach((item, idx) => {
  242. // if (index !== idx) {
  243. // item.show = 'none';
  244. // }
  245. // });
  246. },
  247. uniClickAction(e, index) {
  248. if (e.content.text === '删除') {
  249. const item = this.cartList[index];
  250. this.handleDelete(item);
  251. }
  252. },
  253. uniSwipeChange(e, index) {
  254. // uni-swipe-action-item 改变状态时
  255. if (e === 'right' || e === 'left') {
  256. // 当滑动某一项时,手动将其他项的 show 设置为 'none'
  257. this.cartList.forEach((val, idx) => {
  258. if (index !== idx) {
  259. val.show = 'none';
  260. } else {
  261. val.show = e;
  262. }
  263. });
  264. } else {
  265. this.cartList.forEach((val, idx) => {
  266. if (index === idx) {
  267. val.show = 'none';
  268. }
  269. });
  270. }
  271. },
  272. clickAction(index, index1) {
  273. if (this.actionOptions[index1].text == '删除') {
  274. const item = this.cartList[index];
  275. this.handleDelete(item);
  276. }
  277. },
  278. openAction(index) {
  279. this.cartList[index].show = true;
  280. this.cartList.map((val, idx) => {
  281. if (index != idx) this.cartList[idx].show = false;
  282. })
  283. },
  284. handleCheck({ id, checked }) {
  285. const item = this.cartList.find(i => i.id === id);
  286. if (item) {
  287. item.checked = checked;
  288. // 更新本地缓存
  289. const finalCheckedIds = this.cartList.filter(i => i.checked).map(i => i.id);
  290. uni.setStorageSync('cartCheckedIds', finalCheckedIds);
  291. }
  292. },
  293. handleChangeNum({ id, num }) {
  294. const item = this.cartList.find(i => i.id === id);
  295. if (item) {
  296. // 乐观更新还是等待服务器?
  297. // 通常是服务器优先。
  298. this.$u.api.updateCartNumAjax({
  299. id: id,
  300. quantity: num
  301. }).then(() => {
  302. item.quantity = num;
  303. // 通过 computed 重新计算总额
  304. this.$updateCartBadge();
  305. }).catch(() => {
  306. });
  307. }
  308. },
  309. handleReduce(item) {
  310. console.log('handleReduce item:', item);
  311. this.$nextTick(() => {
  312. if (this.$refs.reducePopup) {
  313. this.$refs.reducePopup.open({
  314. isbn: item.isbn,
  315. conditionType: item.conditionType,
  316. orderId: item.id
  317. });
  318. } else {
  319. console.error('reducePopup ref not found');
  320. }
  321. });
  322. },
  323. onAllCheckChange(e) {
  324. const checked = e.value;
  325. this.cartList.forEach(item => {
  326. if (item.stockStatus !== 3 && item.availableStock > 0) {
  327. item.checked = checked;
  328. }
  329. });
  330. // 保存选中状态到本地缓存
  331. const finalCheckedIds = this.cartList.filter(i => i.checked).map(i => i.id);
  332. uni.setStorageSync('cartCheckedIds', finalCheckedIds);
  333. },
  334. toggleSelectAll() {
  335. this.isAllSelected = !this.isAllSelected;
  336. this.onAllCheckChange({ value: this.isAllSelected });
  337. },
  338. handleClearCart() {
  339. uni.showModal({
  340. title: '提示',
  341. content: '确定要清空购物车吗?',
  342. success: (res) => {
  343. if (res.confirm) {
  344. this.$u.api.clearCartAjax().then(() => {
  345. this.$u.toast('清空成功');
  346. this.cartList = [];
  347. this.$updateCartBadge();
  348. });
  349. }
  350. }
  351. });
  352. },
  353. checkout() {
  354. if (this.selectedCount === 0) {
  355. uni.showToast({ title: '请选择商品', icon: 'none' });
  356. return;
  357. }
  358. // 如果需要,检查减价商品逻辑
  359. // 如果有选中的商品可以减价,显示对话框?
  360. const hasReduceItem = this.selectedItems.some(item => (item.reduceNum < item.maxReduceNum) && (item.reduceMoney > 0));
  361. if (hasReduceItem) {
  362. this.$refs.reduceDialog.openPopup();
  363. } else {
  364. this.onNext();
  365. }
  366. },
  367. onNext() {
  368. // 提交订单 - 传递选中的 ID
  369. if (this.selectedItems.length === 0) {
  370. this.$u.toast('请选择商品');
  371. return;
  372. }
  373. const ids = this.selectedItems.map(item => item.id);
  374. // 调用预提交接口检查
  375. uni.showLoading({ title: '处理中' });
  376. this.$u.api.preSubmitOrderAjax({ cartIdList: ids }).then(res => {
  377. uni.hideLoading();
  378. if (res.code === 200) {
  379. if (res.data.code === 2) {
  380. return this.$u.toast('库存不足');
  381. } else if (res.data.code === 3) {
  382. return this.$u.toast('图书已下架');
  383. }
  384. uni.navigateTo({
  385. url: `/pages-car/pages/confirm-order`
  386. });
  387. //存储提交的数据
  388. res.data.cartIdList = ids;
  389. uni.setStorageSync('preSubmitOrderData', res.data);
  390. } else {
  391. // 失败,显示错误信息
  392. this.$u.toast(res.msg || '无法提交订单');
  393. }
  394. }).catch(() => {
  395. uni.hideLoading();
  396. });
  397. },
  398. handleShare() {
  399. console.log('share');
  400. },
  401. handleScan(bookInfo) {
  402. this.$refs.reduceQrcode.open(bookInfo);
  403. },
  404. handleDelete(item) {
  405. uni.showModal({
  406. title: '提示',
  407. content: '确定要删除该商品吗?',
  408. success: (res) => {
  409. if (res.confirm) {
  410. this.$u.api.deleteCartItemAjax(item.id).then(async () => {
  411. await this.$updateCartBadge();
  412. this.$u.toast('删除成功');
  413. // 从本地列表中移除
  414. const index = this.cartList.findIndex(i => i.id === item.id);
  415. if (index > -1) {
  416. this.cartList.splice(index, 1);
  417. }
  418. });
  419. } else {
  420. item.show = 'none';
  421. }
  422. }
  423. });
  424. },
  425. onSelectCondition(item) {
  426. this.currentItem = item;
  427. // 获取商品详情中的 SKU 列表
  428. // 注意:这里使用 isbn,假设 item 中包含 isbn
  429. if (!item.isbn) {
  430. this.$u.toast('无法获取商品信息');
  431. return;
  432. }
  433. uni.showLoading({ title: '加载中' });
  434. this.$u.http.get('/token/shop/bookDetail', { isbn: item.isbn }).then(res => {
  435. uni.hideLoading();
  436. if (res.code === 200 && res.data && res.data.skuList) {
  437. this.$refs.conditionPopup.open(res.data.skuList, item.conditionType);
  438. } else {
  439. this.$u.toast('获取品相信息失败');
  440. }
  441. }).catch(() => {
  442. uni.hideLoading();
  443. this.$u.toast('网络请求失败');
  444. });
  445. },
  446. handleConditionUpdate(sku) {
  447. if (!this.currentItem) return;
  448. uni.showLoading({
  449. title: '更新中'
  450. });
  451. this.$u.api.updateCartConditionAjax({
  452. id: this.currentItem.id,
  453. conditionType: sku.conditionType
  454. }).then(() => {
  455. uni.hideLoading();
  456. this.$u.toast('更新成功');
  457. this.$refs.pageRef?.loadData(true);
  458. }).catch(() => {
  459. uni.hideLoading();
  460. });
  461. }
  462. }
  463. }
  464. </script>
  465. <style lang="scss" scoped>
  466. .cart-page {
  467. min-height: 100vh;
  468. background-color: #f5f5f5;
  469. padding-bottom: 120rpx; // 底部栏的留白
  470. }
  471. .ad-banner {
  472. background-color: #d1f2d6; // 浅绿色
  473. padding: 20rpx 30rpx;
  474. display: flex;
  475. justify-content: space-between;
  476. align-items: center;
  477. color: #666;
  478. font-size: 26rpx;
  479. }
  480. .cart-list {
  481. padding: 20rpx;
  482. }
  483. .recommend-section {
  484. padding: 0 20rpx;
  485. .section-title {
  486. display: flex;
  487. align-items: center;
  488. justify-content: center;
  489. margin: 30rpx 0;
  490. .line {
  491. width: 60rpx;
  492. height: 2rpx;
  493. background-color: #ccc;
  494. }
  495. .text {
  496. margin: 0 20rpx;
  497. font-size: 28rpx;
  498. color: #333;
  499. font-weight: bold;
  500. }
  501. }
  502. .recommend-grid {
  503. display: flex;
  504. flex-wrap: wrap;
  505. justify-content: space-between;
  506. .grid-item {
  507. width: 32%; // 3 列
  508. background-color: #fff;
  509. border-radius: 12rpx;
  510. margin-bottom: 20rpx;
  511. padding: 20rpx;
  512. box-sizing: border-box;
  513. display: flex;
  514. flex-direction: column;
  515. align-items: center;
  516. .cover {
  517. width: 100%;
  518. height: 220rpx;
  519. border-radius: 8rpx;
  520. background-color: #eee;
  521. }
  522. .title {
  523. margin-top: 10rpx;
  524. font-size: 24rpx;
  525. color: #333;
  526. line-height: 1.4;
  527. display: -webkit-box;
  528. -webkit-box-orient: vertical;
  529. -webkit-line-clamp: 2;
  530. overflow: hidden;
  531. width: 100%;
  532. }
  533. }
  534. }
  535. }
  536. .floating-clear-btn {
  537. position: fixed;
  538. right: 30rpx;
  539. bottom: 160rpx; /* 留出底部结算栏的空间 */
  540. /* #ifdef H5 */
  541. bottom: calc(160rpx + 50px);
  542. /* #endif */
  543. width: 80rpx;
  544. height: 80rpx;
  545. background-color: #fff;
  546. border: 2rpx solid #f56c6c;
  547. border-radius: 50%;
  548. display: flex;
  549. justify-content: center;
  550. align-items: center;
  551. z-index: 98;
  552. box-shadow: 0 4rpx 10rpx rgba(250, 53, 52, 0.2);
  553. &:active {
  554. background-color: #fef0f0;
  555. }
  556. }
  557. .bottom-fixed {
  558. position: fixed;
  559. bottom: 0;
  560. /* #ifdef H5 */
  561. bottom: 50px;
  562. /* #endif */
  563. left: 0;
  564. width: 100%;
  565. background-color: #fff;
  566. display: flex;
  567. align-items: center;
  568. justify-content: space-between;
  569. padding: 24rpx 30rpx;
  570. box-sizing: border-box;
  571. box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
  572. z-index: 99;
  573. .left-part {
  574. display: flex;
  575. align-items: center;
  576. }
  577. .right-part {
  578. display: flex;
  579. align-items: center;
  580. .info-wrapper {
  581. display: flex;
  582. flex-direction: column;
  583. align-items: flex-end;
  584. margin-right: 20rpx;
  585. .total-info {
  586. display: flex;
  587. align-items: center;
  588. .total-price {
  589. display: flex;
  590. align-items: center;
  591. .label {
  592. font-size: 28rpx;
  593. color: #333;
  594. }
  595. .price {
  596. font-size: 36rpx;
  597. color: #e02020;
  598. font-weight: bold;
  599. }
  600. }
  601. .reduced-tip {
  602. font-size: 22rpx;
  603. color: #38C148;
  604. border: 1rpx solid #38C148;
  605. border-radius: 4rpx;
  606. padding: 0 10rpx;
  607. margin-left: 10rpx;
  608. }
  609. }
  610. .freight-template {
  611. font-size: 24rpx;
  612. color: #d79f5f;
  613. margin-top: 6rpx;
  614. }
  615. }
  616. .checkout-btn {
  617. background-color: #e02020;
  618. color: #fff;
  619. font-size: 30rpx;
  620. padding: 16rpx 40rpx;
  621. border-radius: 40rpx;
  622. }
  623. }
  624. }
  625. </style>