index.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615
  1. <template>
  2. <Theme>
  3. <view class="wrap">
  4. <Navbar fixed leftShow leftIconColor="var(--bg)">
  5. <template #center>
  6. <view class="nav_title">
  7. <trans _t="购物车" />
  8. </view>
  9. <view class="nav_title" v-if="cartNum">({{ cartNum }})</view>
  10. </template>
  11. <template #right>
  12. <view
  13. class="nav_right"
  14. @click.stop="delCart"
  15. v-if="selectedIds.length"
  16. >
  17. <trans _t="删除" />
  18. </view>
  19. </template>
  20. </Navbar>
  21. <view class="tab" id="tabs">
  22. <Tab :active="activeTab" :tabList="tabList" @confirm="tabClick" />
  23. </view>
  24. <view
  25. class="content"
  26. :style="{ '--height': footerHeight + tabbarHeight + tabHeight + 'px' }"
  27. >
  28. <view class="cont_wrap">
  29. <!-- 购物车为空 -->
  30. <view class="empty-cart" v-if="!cartList.length">
  31. <image src="/static/shop/empty_cart.png" class="empty-image" />
  32. <text class="empty-text">购物车空空如也</text>
  33. <view class="empty-btn" @click="goShopping">
  34. <trans _t="去逛逛" />
  35. </view>
  36. </view>
  37. <!-- 购物车列表 -->
  38. <view class="cart-list" v-else>
  39. <view class="cart-item" v-for="item in cartList" :key="item.id">
  40. <view class="item-checkbox">
  41. <up-checkbox
  42. :checked="item.checked"
  43. @change="toggleItem(item, $event)"
  44. activeColor="var(--black)"
  45. shape="circle"
  46. />
  47. </view>
  48. <view class="item-content" @click="toProductDetail(item)">
  49. <image :src="item.image" class="item-image" />
  50. <view class="item-info">
  51. <text class="item-name">{{ item.name }}</text>
  52. <text class="item-spec">{{ item.spec }}</text>
  53. <view class="item-price-row">
  54. <text class="item-price"
  55. >{{ symbol.symbol }}{{ item.price }}</text
  56. >
  57. <view class="quantity-control">
  58. <view
  59. class="quantity-btn minus"
  60. @click.stop="decreaseQuantity(item)"
  61. :class="{ disabled: item.quantity <= 1 }"
  62. >
  63. -
  64. </view>
  65. <input
  66. class="quantity-input"
  67. type="number"
  68. :value="item.quantity"
  69. @input="updateQuantity(item, $event)"
  70. @blur="validateQuantity(item)"
  71. />
  72. <view
  73. class="quantity-btn plus"
  74. @click.stop="increaseQuantity(item)"
  75. >
  76. +
  77. </view>
  78. </view>
  79. </view>
  80. </view>
  81. </view>
  82. </view>
  83. </view>
  84. </view>
  85. <view
  86. class="footer"
  87. id="footer"
  88. :style="{ '--tabbarHeight': tabbarHeight + 'px' }"
  89. v-if="cartList.length"
  90. >
  91. <view class="footer_left">
  92. <up-checkbox
  93. :label="t('全选')"
  94. :disabled="!cartList.length"
  95. activeColor="var(--black)"
  96. shape="circle"
  97. labelSize="14"
  98. labelColor="#676969"
  99. iconSize="16"
  100. name="selectAll"
  101. usedAlone
  102. v-model:checked="selectAllChecked"
  103. @change="selectAllChange"
  104. />
  105. </view>
  106. <view class="footer_right">
  107. <view class="total_info">
  108. <view class="total_price">
  109. <text>合计:{{ symbol.symbol }}{{ totalPrice }}</text>
  110. </view>
  111. <view class="total_desc">
  112. <trans _t="已选{{ selectedCount }}件商品" />
  113. </view>
  114. </view>
  115. <view
  116. class="total_btn"
  117. @click.stop="checkout"
  118. :style="{
  119. opacity: canCheckout ? 1 : 0.5,
  120. 'pointer-events': canCheckout ? 'auto' : 'none',
  121. }"
  122. >
  123. <trans _t="去结算" />
  124. </view>
  125. </view>
  126. </view>
  127. </view>
  128. </view>
  129. <Tabbar page="cart" @getTabbarHeight="getTabbarHeight" />
  130. </Theme>
  131. </template>
  132. <script setup>
  133. import { computed, ref, onMounted, nextTick, watch } from "vue";
  134. import Tabbar from "@/components/tabbar";
  135. import Navbar from "@/components/navbar";
  136. import Tab from "@/components/tabs";
  137. import { useUserStore, useShopStore, useSystemStore } from "@/store";
  138. import { t } from "@/locale";
  139. import { onShow } from "@dcloudio/uni-app";
  140. import { query } from "@/utils";
  141. const useUser = useUserStore();
  142. const useShop = useShopStore();
  143. const useSystem = useSystemStore();
  144. const symbol = computed(() => useSystem.getSymbol);
  145. // 购物车数据
  146. const cartList = ref([
  147. {
  148. id: 1,
  149. name: "时尚连衣裙",
  150. spec: "颜色:黑色 尺码:M",
  151. image: "/static/shop/product1.png",
  152. price: "299.00",
  153. quantity: 1,
  154. checked: true,
  155. stock: 99,
  156. },
  157. {
  158. id: 2,
  159. name: "护肤精华",
  160. spec: "容量:30ml",
  161. image: "/static/shop/product2.png",
  162. price: "199.00",
  163. quantity: 2,
  164. checked: true,
  165. stock: 50,
  166. },
  167. {
  168. id: 3,
  169. name: "无线耳机",
  170. spec: "颜色:白色",
  171. image: "/static/shop/product3.png",
  172. price: "599.00",
  173. quantity: 1,
  174. checked: false,
  175. stock: 20,
  176. },
  177. ]);
  178. const cartNum = computed(() => cartList.value.length);
  179. const selectAllChecked = ref(false);
  180. const footerHeight = ref(0);
  181. const tabbarHeight = ref(0);
  182. const activeTab = ref(0);
  183. const tabHeight = ref(0);
  184. const tabList = ["全部", "降价商品", "库存紧张"];
  185. const userInfo = computed(() => useUser.getuserInfo);
  186. const token = computed(() => useUser.getToken);
  187. // 选中的商品ID列表
  188. const selectedIds = computed(() => {
  189. return cartList.value.filter((item) => item.checked).map((item) => item.id);
  190. });
  191. // 选中商品数量
  192. const selectedCount = computed(() => {
  193. return cartList.value
  194. .filter((item) => item.checked)
  195. .reduce((sum, item) => sum + item.quantity, 0);
  196. });
  197. // 总价
  198. const totalPrice = computed(() => {
  199. return cartList.value
  200. .filter((item) => item.checked)
  201. .reduce((sum, item) => sum + parseFloat(item.price) * item.quantity, 0)
  202. .toFixed(2);
  203. });
  204. // 是否可以结算
  205. const canCheckout = computed(() => {
  206. return selectedIds.value.length > 0;
  207. });
  208. // 监听购物车变化,更新全选状态
  209. watch(
  210. () => cartList.value,
  211. (list) => {
  212. if (!list.length) {
  213. selectAllChecked.value = false;
  214. return;
  215. }
  216. const allChecked = list.every((item) => item.checked);
  217. selectAllChecked.value = allChecked;
  218. },
  219. { immediate: true, deep: true }
  220. );
  221. // 全选/取消全选
  222. const selectAllChange = (checked) => {
  223. cartList.value.forEach((item) => {
  224. item.checked = checked;
  225. });
  226. };
  227. // 切换单个商品选中状态
  228. const toggleItem = (item, checked) => {
  229. item.checked = checked;
  230. };
  231. // 增加数量
  232. const increaseQuantity = (item) => {
  233. if (item.quantity < item.stock) {
  234. item.quantity++;
  235. } else {
  236. uni.showToast({
  237. title: "库存不足",
  238. icon: "none",
  239. });
  240. }
  241. };
  242. // 减少数量
  243. const decreaseQuantity = (item) => {
  244. if (item.quantity > 1) {
  245. item.quantity--;
  246. }
  247. };
  248. // 更新数量
  249. const updateQuantity = (item, event) => {
  250. const value = parseInt(event.detail.value) || 1;
  251. if (value > item.stock) {
  252. item.quantity = item.stock;
  253. uni.showToast({
  254. title: "库存不足",
  255. icon: "none",
  256. });
  257. } else if (value < 1) {
  258. item.quantity = 1;
  259. } else {
  260. item.quantity = value;
  261. }
  262. };
  263. // 验证数量
  264. const validateQuantity = (item) => {
  265. if (item.quantity < 1) {
  266. item.quantity = 1;
  267. }
  268. };
  269. // 删除购物车
  270. const delCart = () => {
  271. if (selectedIds.value.length === 0) {
  272. uni.showToast({
  273. title: "请选择要删除的商品",
  274. icon: "none",
  275. });
  276. return;
  277. }
  278. uni.showModal({
  279. title: "确认删除",
  280. content: `确定要删除选中的${selectedIds.value.length}件商品吗?`,
  281. success: (res) => {
  282. if (res.confirm) {
  283. cartList.value = cartList.value.filter((item) => !item.checked);
  284. selectAllChecked.value = false;
  285. }
  286. },
  287. });
  288. };
  289. // 去结算
  290. const checkout = () => {
  291. if (!canCheckout.value) {
  292. uni.showToast({
  293. title: "请选择要结算的商品",
  294. icon: "none",
  295. });
  296. return;
  297. }
  298. const selectedItems = cartList.value.filter((item) => item.checked);
  299. uni.navigateTo({
  300. url: `/pagesBuyer/cart/checkout?items=${JSON.stringify(selectedItems)}`,
  301. });
  302. };
  303. // 去购物
  304. const goShopping = () => {
  305. uni.switchTab({
  306. url: "/pagesBuyer/home/index",
  307. });
  308. };
  309. // 查看商品详情
  310. const toProductDetail = (item) => {
  311. uni.navigateTo({
  312. url: `/pagesBuyer/home/product?id=${item.id}`,
  313. });
  314. };
  315. // 切换标签
  316. const tabClick = (item, index) => {
  317. if (activeTab.value === index) return;
  318. activeTab.value = index;
  319. // 根据标签筛选商品
  320. filterCartItems(index);
  321. };
  322. // 筛选购物车商品
  323. const filterCartItems = (tabIndex) => {
  324. // 这里可以根据不同标签筛选商品
  325. // 0: 全部, 1: 降价商品, 2: 库存紧张
  326. switch (tabIndex) {
  327. case 0:
  328. // 显示全部商品
  329. break;
  330. case 1:
  331. // 显示降价商品(这里简化处理)
  332. break;
  333. case 2:
  334. // 显示库存紧张商品(库存小于10)
  335. break;
  336. }
  337. };
  338. const getHeight = async () => {
  339. try {
  340. const res = await query("#footer");
  341. const resTab = await query("#tabs");
  342. nextTick(() => {
  343. tabHeight.value = resTab.height;
  344. footerHeight.value = res.height;
  345. });
  346. } catch (error) {}
  347. };
  348. const getTabbarHeight = (height) => {
  349. tabbarHeight.value = height;
  350. };
  351. onMounted(() => {
  352. getHeight();
  353. });
  354. onShow(() => {
  355. setTimeout(() => {
  356. getHeight();
  357. }, 0);
  358. });
  359. </script>
  360. <style lang="less" scoped>
  361. @import url("@/style.less");
  362. .wrap {
  363. min-height: 100vh;
  364. background: var(--bg);
  365. overflow: hidden;
  366. .flex();
  367. flex-direction: column;
  368. :deep(.u-navbar__content) {
  369. background-color: var(--black) !important;
  370. }
  371. .nav_title {
  372. color: var(--light);
  373. }
  374. .nav_right {
  375. color: var(--primary);
  376. .size(24rpx);
  377. font-weight: 500;
  378. }
  379. .content {
  380. flex-grow: 1;
  381. height: calc(100vh - 44px - var(--height));
  382. overflow-y: scroll;
  383. .flex();
  384. flex-direction: column;
  385. .cont_wrap {
  386. flex-grow: 1;
  387. overflow: hidden scroll;
  388. padding: 0 30rpx;
  389. }
  390. .empty-cart {
  391. display: flex;
  392. flex-direction: column;
  393. align-items: center;
  394. justify-content: center;
  395. height: 60vh;
  396. .empty-image {
  397. width: 200rpx;
  398. height: 200rpx;
  399. margin-bottom: 32rpx;
  400. }
  401. .empty-text {
  402. font-size: 32rpx;
  403. color: var(--text-01);
  404. margin-bottom: 48rpx;
  405. }
  406. .empty-btn {
  407. background: var(--black);
  408. color: var(--light);
  409. padding: 24rpx 48rpx;
  410. border-radius: 40rpx;
  411. font-size: 28rpx;
  412. }
  413. }
  414. .cart-list {
  415. .cart-item {
  416. display: flex;
  417. align-items: flex-start;
  418. background: var(--light);
  419. border-radius: 20rpx;
  420. padding: 24rpx;
  421. margin-bottom: 16rpx;
  422. .item-checkbox {
  423. margin-right: 20rpx;
  424. margin-top: 8rpx;
  425. }
  426. .item-content {
  427. flex: 1;
  428. display: flex;
  429. .item-image {
  430. width: 160rpx;
  431. height: 160rpx;
  432. border-radius: 16rpx;
  433. margin-right: 20rpx;
  434. }
  435. .item-info {
  436. flex: 1;
  437. display: flex;
  438. flex-direction: column;
  439. .item-name {
  440. font-size: 28rpx;
  441. color: var(--black);
  442. margin-bottom: 8rpx;
  443. display: -webkit-box;
  444. -webkit-line-clamp: 2;
  445. line-clamp: 2;
  446. -webkit-box-orient: vertical;
  447. overflow: hidden;
  448. }
  449. .item-spec {
  450. font-size: 24rpx;
  451. color: var(--text-01);
  452. margin-bottom: 16rpx;
  453. }
  454. .item-price-row {
  455. display: flex;
  456. justify-content: space-between;
  457. align-items: center;
  458. .item-price {
  459. font-size: 32rpx;
  460. font-weight: bold;
  461. color: var(--red);
  462. }
  463. .quantity-control {
  464. display: flex;
  465. align-items: center;
  466. border: 1rpx solid var(--border-color);
  467. border-radius: 8rpx;
  468. .quantity-btn {
  469. width: 60rpx;
  470. height: 60rpx;
  471. display: flex;
  472. align-items: center;
  473. justify-content: center;
  474. font-size: 32rpx;
  475. color: var(--text);
  476. background: var(--bg);
  477. &.disabled {
  478. color: var(--text-01);
  479. background: var(--inputBg);
  480. }
  481. &.minus {
  482. border-right: 1rpx solid var(--border-color);
  483. }
  484. &.plus {
  485. border-left: 1rpx solid var(--border-color);
  486. }
  487. }
  488. .quantity-input {
  489. width: 80rpx;
  490. height: 60rpx;
  491. text-align: center;
  492. font-size: 28rpx;
  493. border: none;
  494. background: transparent;
  495. }
  496. }
  497. }
  498. }
  499. }
  500. }
  501. }
  502. .footer {
  503. .flex_position(space-between);
  504. flex-wrap: wrap;
  505. padding: 8px 12px;
  506. background-color: var(--light);
  507. box-shadow: 0 -4px 6px #0000000d;
  508. position: fixed;
  509. bottom: var(--tabbarHeight);
  510. left: 0;
  511. right: 0;
  512. box-sizing: border-box;
  513. &_left {
  514. display: flex;
  515. align-items: center;
  516. }
  517. &_right {
  518. .ver();
  519. .total_info {
  520. .total_price {
  521. text-align: right;
  522. color: var(--red);
  523. .size();
  524. font-weight: 700;
  525. line-height: 48rpx;
  526. }
  527. .total_desc {
  528. color: var(--text-01);
  529. .size(24rpx);
  530. line-height: 40rpx;
  531. text-align: right;
  532. }
  533. }
  534. .total_btn {
  535. .size(28rpx);
  536. font-weight: 700;
  537. height: 38px;
  538. margin-left: 16rpx;
  539. min-width: 180rpx;
  540. background-color: var(--black);
  541. color: var(--light);
  542. border-radius: 16rpx;
  543. padding: 16rpx 30rpx;
  544. text-align: center;
  545. }
  546. }
  547. }
  548. }
  549. }
  550. </style>