shopping-cart.uvue 8.3 KB

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