shopping-cart.uvue 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416
  1. <template>
  2. <cl-page>
  3. <cl-sticky>
  4. <cl-topbar fixed safe-area-top :title="`${$t('购物车 ({num})', { num: list.length })}`">
  5. <template #prepend>
  6. <!-- #ifdef MP -->
  7. <cl-text
  8. :pt="{
  9. className: 'ml-1'
  10. }"
  11. @tap="isDel = !isDel"
  12. >
  13. {{ isDel ? t("完成") : t("管理") }}
  14. </cl-text>
  15. <!-- #endif -->
  16. </template>
  17. <template #append>
  18. <!-- #ifndef MP -->
  19. <cl-text
  20. :pt="{
  21. className: 'mr-3'
  22. }"
  23. @tap="isDel = !isDel"
  24. >
  25. {{ isDel ? t("完成") : t("管理") }}
  26. </cl-text>
  27. <!-- #endif -->
  28. </template>
  29. </cl-topbar>
  30. </cl-sticky>
  31. <view class="p-3">
  32. <view class="p-3 rounded-xl bg-white dark:!bg-surface-800 mb-3">
  33. <cl-text
  34. :pt="{
  35. className: 'text-sm'
  36. }"
  37. >🔥 最新降价商品,限时优惠,抓紧抢购!</cl-text
  38. >
  39. </view>
  40. <cl-list-item
  41. v-for="(item, index) in list"
  42. :key="item.id"
  43. :pt="{
  44. className: parseClass([
  45. 'rounded-2xl ',
  46. [index == list.length - 1, 'mb-0', 'mb-3']
  47. ]),
  48. inner: {
  49. className: '!p-4'
  50. }
  51. }"
  52. swipeable
  53. >
  54. <view class="flex flex-row flex-1">
  55. <view class="flex flex-col mr-4 pt-[55rpx]" @tap="selectItem(item)">
  56. <cl-icon
  57. name="checkbox-circle-line"
  58. color="primary"
  59. :size="40"
  60. v-if="item.checked"
  61. ></cl-icon>
  62. <cl-icon name="checkbox-blank-circle-line" :size="40" v-else></cl-icon>
  63. </view>
  64. <cl-image :width="150" :height="150" :src="item.cover"></cl-image>
  65. <view class="flex flex-col ml-3 flex-1">
  66. <cl-text
  67. :pt="{
  68. className: 'mb-2 font-bold'
  69. }"
  70. >{{ item.name }}</cl-text
  71. >
  72. <view class="flex flex-row mb-2">
  73. <view
  74. class="bg-surface-100 dark:!bg-surface-700 rounded-md py-1 px-2 flex flex-row items-center"
  75. >
  76. <cl-text
  77. :pt="{
  78. className: 'text-xs'
  79. }"
  80. >{{ item.skuName }}</cl-text
  81. >
  82. <cl-icon name="arrow-down-s-line"></cl-icon>
  83. </view>
  84. </view>
  85. <view class="flex flex-row items-center mb-2">
  86. <cl-text
  87. :pt="{
  88. className: 'text-[22rpx] text-red-500 mr-[1rpx]'
  89. }"
  90. >¥</cl-text
  91. >
  92. <cl-text
  93. :pt="{
  94. className: 'text-red-500 text-[32rpx] mr-auto'
  95. }"
  96. >{{ item.price }}</cl-text
  97. >
  98. <view
  99. v-if="isDel"
  100. class="p-[8rpx] bg-red-500 rounded-lg"
  101. @tap="delItem(index)"
  102. >
  103. <cl-icon name="delete-bin-line" color="white" :size="24"></cl-icon>
  104. </view>
  105. <view class="flex" v-else>
  106. <cl-input-number
  107. v-model="item.count"
  108. :size="40"
  109. :min="1"
  110. :pt="{
  111. op: {
  112. plus: {
  113. className: '!rounded-full'
  114. },
  115. minus: {
  116. className: '!rounded-full'
  117. },
  118. icon: {
  119. size: 28
  120. }
  121. },
  122. value: {
  123. className: '!w-[60rpx] rounded-full !px-0',
  124. input: {
  125. className: 'text-[24rpx]'
  126. }
  127. }
  128. }"
  129. ></cl-input-number>
  130. </view>
  131. </view>
  132. <cl-text :size="22" color="error">比加入时降¥100</cl-text>
  133. <cl-text :size="22">满1件可换购0.5元商品</cl-text>
  134. </view>
  135. </view>
  136. <template #swipe-right>
  137. <cl-button
  138. type="error"
  139. :pt="{
  140. className: '!rounded-none h-full w-[160rpx]'
  141. }"
  142. @tap="delItem(index)"
  143. >{{ t("删除") }}</cl-button
  144. >
  145. </template>
  146. </cl-list-item>
  147. <cl-empty v-if="list.length == 0"></cl-empty>
  148. </view>
  149. <cl-footer>
  150. <view class="flex flex-row items-center h-[70rpx]">
  151. <cl-checkbox
  152. active-icon="checkbox-circle-line"
  153. inactive-icon="checkbox-blank-circle-line"
  154. :pt="{
  155. className: 'mr-auto'
  156. }"
  157. :size="28"
  158. v-model="selectAll"
  159. @change="onSelectAllChange"
  160. >{{ t("全选") }}</cl-checkbox
  161. >
  162. <template v-if="isDel">
  163. <cl-button
  164. type="error"
  165. :pt="{
  166. className: '!px-5'
  167. }"
  168. @tap="delAll"
  169. >
  170. {{ t("删除") }}
  171. </cl-button>
  172. </template>
  173. <template v-else>
  174. <view class="flex flex-col mr-3 items-end pt-1">
  175. <cl-text
  176. color="info"
  177. :pt="{
  178. className: 'text-xs'
  179. }"
  180. >{{ t("合计") }}</cl-text
  181. >
  182. <cl-text color="error" :value="totalPrice" type="amount"></cl-text>
  183. </view>
  184. <cl-button
  185. type="error"
  186. :pt="{
  187. className: '!px-8'
  188. }"
  189. @tap="toSettle"
  190. >
  191. {{ t("去结算") }}
  192. </cl-button>
  193. </template>
  194. </view>
  195. </cl-footer>
  196. </cl-page>
  197. </template>
  198. <script lang="ts" setup>
  199. import { parseClass, isEmpty } from "@/cool";
  200. import { $t, t } from "@/locale";
  201. import { useUi } from "@/uni_modules/cool-ui";
  202. import { computed, ref } from "vue";
  203. const ui = useUi();
  204. // 商品类型
  205. type Goods = {
  206. id: number;
  207. name: string;
  208. count: number;
  209. price: number;
  210. cover: string;
  211. skuName: string;
  212. checked: boolean;
  213. };
  214. // 商品列表
  215. const list = ref<Goods[]>([
  216. {
  217. id: 1,
  218. name: "Apple iPad",
  219. count: 1,
  220. price: 450,
  221. cover: "https://img.yzcdn.cn/vant/ipad.png",
  222. skuName: "深空灰色 128GB WLAN版",
  223. checked: true
  224. },
  225. {
  226. id: 2,
  227. name: "Samsung Galaxy S24",
  228. count: 2,
  229. price: 699,
  230. cover: "https://img.yzcdn.cn/vant/samsung.png",
  231. skuName: "曜石黑 12GB+256GB 官方标配",
  232. checked: false
  233. },
  234. {
  235. id: 3,
  236. name: "Sony WH-1000XM5",
  237. count: 1,
  238. price: 299,
  239. cover: "https://img.yzcdn.cn/vant/sony.png",
  240. skuName: "黑色 无线蓝牙 官方标配",
  241. checked: false
  242. },
  243. {
  244. id: 4,
  245. name: "小米手环8",
  246. count: 3,
  247. price: 49,
  248. cover: "https://img.yzcdn.cn/vant/xiaomi.png",
  249. skuName: "曜石黑 标准版 硅胶表带",
  250. checked: false
  251. },
  252. {
  253. id: 5,
  254. name: "Kindle Paperwhite",
  255. count: 1,
  256. price: 120,
  257. cover: "https://img.yzcdn.cn/vant/kindle.png",
  258. skuName: "黑色 8GB 官方标配",
  259. checked: false
  260. }
  261. ]);
  262. // 是否全选
  263. const selectAll = ref(false);
  264. /**
  265. * 选择/取消选择单个商品
  266. * @param item 需要操作的商品
  267. */
  268. function selectItem(item: Goods) {
  269. // 切换选中状态
  270. item.checked = !item.checked;
  271. // 判断是否所有商品都被选中,更新全选状态
  272. selectAll.value = list.value.every((item) => item.checked);
  273. }
  274. /**
  275. * 全选/取消全选
  276. * @param val 是否全选
  277. */
  278. function onSelectAllChange(val: boolean) {
  279. list.value.forEach((item, index, arr) => {
  280. // item.checked = val; // 这样写,在 android 上无效
  281. arr[index].checked = val;
  282. });
  283. }
  284. // 是否处于删除模式
  285. const isDel = ref(false);
  286. /**
  287. * 删除单个商品
  288. * @param index 商品索引
  289. */
  290. function delItem(index: number) {
  291. ui.showConfirm({
  292. title: t("温馨提示"),
  293. message: t("确定删除该商品吗?"),
  294. callback(action) {
  295. if (action === "confirm") {
  296. // 删除指定索引的商品
  297. list.value.splice(index, 1);
  298. ui.showToast({
  299. message: t("删除成功")
  300. });
  301. }
  302. }
  303. });
  304. }
  305. /**
  306. * 删除所有已选中的商品
  307. */
  308. function delAll() {
  309. const checked = list.value.filter((item) => item.checked);
  310. // 如果没有选中商品,提示用户
  311. if (isEmpty(checked)) {
  312. return ui.showToast({
  313. message: t("请先选择商品")
  314. });
  315. }
  316. ui.showConfirm({
  317. title: t("温馨提示"),
  318. message: t("确定删除选中的商品吗?"),
  319. callback(action) {
  320. if (action == "confirm") {
  321. // 只保留未选中的商品
  322. list.value = list.value.filter((item) => !item.checked);
  323. // 如果之前是全选,删除后取消全选状态
  324. if (selectAll.value) {
  325. selectAll.value = false;
  326. }
  327. ui.showToast({
  328. message: t("删除成功")
  329. });
  330. }
  331. }
  332. });
  333. }
  334. /**
  335. * 清空购物车
  336. */
  337. function clear() {
  338. list.value = [];
  339. selectAll.value = false;
  340. isDel.value = false;
  341. }
  342. /**
  343. * 计算已选中商品的总价
  344. */
  345. const totalPrice = computed(() => {
  346. return list.value
  347. .filter((item) => item.checked)
  348. .reduce((acc, item) => acc + item.price * item.count, 0);
  349. });
  350. /**
  351. * 结算操作
  352. */
  353. function toSettle() {
  354. // 如果没有选中商品,提示用户
  355. if (totalPrice.value <= 0) {
  356. return ui.showToast({
  357. message: t("请先选择商品")
  358. });
  359. }
  360. ui.showConfirm({
  361. title: t("温馨提示"),
  362. message: $t("您需支付 {price} 元,请确认支付", { price: totalPrice.value }),
  363. beforeClose(action, { showLoading, close }) {
  364. if (action == "confirm") {
  365. showLoading();
  366. setTimeout(() => {
  367. ui.showToast({
  368. message: t("支付成功")
  369. });
  370. close();
  371. }, 1000);
  372. } else {
  373. close();
  374. }
  375. }
  376. });
  377. }
  378. </script>