s-select-seckill-sku.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  1. <template>
  2. <!-- 规格弹窗 -->
  3. <su-popup :show="show" round="10" @close="emits('close')">
  4. <view class="ss-modal-box bg-white ss-flex-col">
  5. <view class="modal-header ss-flex ss-col-center">
  6. <view class="header-left ss-m-r-30">
  7. <image
  8. class="sku-image"
  9. :src="sheep.$url.cdn(state.selectedSkuPrice.image || state.goodsInfo.image)"
  10. mode="aspectFill"
  11. >
  12. </image>
  13. </view>
  14. <view class="header-right ss-flex-col ss-row-between ss-flex-1">
  15. <view class="goods-title ss-line-2">{{ state.goodsInfo.title }}</view>
  16. <view class="header-right-bottom ss-flex ss-col-center ss-row-between">
  17. <view class="price-text">
  18. {{ state.selectedSkuPrice.groupon_price || formatPrice(state.goodsInfo.price) }}
  19. </view>
  20. <view class="tig ss-flex ss-col-center">
  21. <view class="tig-icon ss-flex ss-col-center ss-row-center">
  22. <text class="cicon-alarm"></text>
  23. </view>
  24. <view class="tig-title">秒杀价</view>
  25. </view>
  26. <view class="stock-text ss-m-l-20">
  27. 库存{{ state.selectedSkuPrice.stock || state.goodsInfo.stock }}件
  28. </view>
  29. </view>
  30. </view>
  31. </view>
  32. <view class="modal-content ss-flex-1">
  33. <scroll-view scroll-y="true" class="modal-content-scroll">
  34. <view class="sku-item ss-m-b-20" v-for="sku1 in state.goodsInfo.skus" :key="sku1.id">
  35. <view class="label-text ss-m-b-20">{{ sku1.name }}</view>
  36. <view class="ss-flex ss-col-center ss-flex-wrap">
  37. <button
  38. class="ss-reset-button spec-btn"
  39. v-for="sku2 in sku1.children"
  40. :class="[
  41. {
  42. 'checked-btn': state.currentSkuArray[sku2.parent_id] == sku2.id,
  43. },
  44. {
  45. 'disabled-btn': sku2.disabled == true,
  46. },
  47. ]"
  48. :key="sku2.id"
  49. :disabled="sku2.disabled == true"
  50. @tap="onSelectSku(sku2.parent_id, sku2.id)"
  51. >
  52. {{ sku2.name }}
  53. </button>
  54. </view>
  55. </view>
  56. <view class="buy-num-box ss-flex ss-col-center ss-row-between">
  57. <view class="label-text">购买数量</view>
  58. <su-number-box
  59. :min="1"
  60. :max="state.selectedSkuPrice.stock"
  61. :step="1"
  62. v-model="state.selectedSkuPrice.goods_num"
  63. @change="onNumberChange($event)"
  64. activity="seckill"
  65. ></su-number-box>
  66. </view>
  67. </scroll-view>
  68. </view>
  69. <view class="modal-footer">
  70. <view class="buy-box ss-flex ss-col-center ss-flex ss-col-center ss-row-center">
  71. <button class="ss-reset-button buy-btn" @tap="onBuy">确认</button>
  72. </view>
  73. </view>
  74. </view>
  75. </su-popup>
  76. </template>
  77. <script setup>
  78. /**
  79. * 1. 根据spus获得skus: 后台已算好
  80. * 2. 用路径字典,生成路径map
  81. * 3. 计算用户选择后,其他路径是否可用
  82. */
  83. // 按钮状态: active,nostock
  84. import { computed, reactive, watch } from 'vue';
  85. import sheep from '@/sheep';
  86. const emits = defineEmits(['change', 'addCart', 'buy', 'close']);
  87. const props = defineProps({
  88. modelValue: {
  89. type: Object,
  90. default() {},
  91. },
  92. show: {
  93. type: Boolean,
  94. default: false,
  95. },
  96. });
  97. const state = reactive({
  98. goodsInfo: computed(() => props.modelValue),
  99. skuPrices: computed(() => {
  100. let skuPrices = props.modelValue.sku_prices;
  101. if (props.modelValue.is_sku) {
  102. skuPrices.forEach((item) => {
  103. item.goods_sku_id_arr = item.goods_sku_ids.split(',');
  104. });
  105. }
  106. return skuPrices;
  107. }),
  108. skuList: props.modelValue.skus,
  109. selectedSkuPrice: {},
  110. currentSkuArray: [],
  111. });
  112. if (!state.goodsInfo.is_sku) {
  113. state.selectedSkuPrice = state.goodsInfo.sku_prices[0];
  114. }
  115. watch(
  116. () => state.selectedSkuPrice,
  117. (newVal) => {
  118. emits('change', newVal);
  119. },
  120. {
  121. immediate: true, // 立即执行
  122. deep: true, // 深度监听
  123. },
  124. );
  125. // 格式化价格
  126. const formatPrice = (e) => {
  127. return e.length === 1 ? e[0] : e.join('~');
  128. };
  129. const onAddCart = () => {
  130. if (state.selectedSkuPrice.goods_id) {
  131. if (state.selectedSkuPrice.stock <= 0) {
  132. sheep.$helper.toast('库存不足');
  133. } else {
  134. emits('addCart', state.selectedSkuPrice);
  135. }
  136. } else {
  137. sheep.$helper.toast('请选择规格');
  138. }
  139. };
  140. const onBuy = () => {
  141. if (state.selectedSkuPrice.goods_id) {
  142. if (state.selectedSkuPrice.stock <= 0) {
  143. sheep.$helper.toast('库存不足');
  144. } else {
  145. emits('buy', state.selectedSkuPrice);
  146. }
  147. } else {
  148. sheep.$helper.toast('请选择规格');
  149. }
  150. };
  151. //输入框改变数量
  152. function onNumberChange(e) {
  153. if (e === 0) return;
  154. if (state.selectedSkuPrice.goods_num === e) return;
  155. state.selectedSkuPrice.goods_num = e;
  156. }
  157. // 改变禁用状态
  158. const changeDisabled = (isChecked = false, pid = 0, skuId = 0) => {
  159. let newPrice = []; // 所有可以选择的 skuPrice
  160. if (isChecked) {
  161. // 选中规格
  162. // 当前点击选中规格下的 所有可用 skuPrice
  163. for (let price of state.skuPrices) {
  164. if (price.stock <= 0) {
  165. // this.goodsNum 不判断是否大于当前选择数量,在 uni-number-box 判断,并且 取 stock 和 goods_num 的小值
  166. continue;
  167. }
  168. if (price.goods_sku_id_arr.indexOf(skuId.toString()) >= 0) {
  169. newPrice.push(price);
  170. }
  171. }
  172. } else {
  173. // 取消选中
  174. // 当前所选规格下,所有可以选择的 skuPrice
  175. newPrice = getCanUseSkuPrice();
  176. }
  177. // 所有存在并且有库存未选择的规格项 的 子项 id
  178. let noChooseSkuIds = [];
  179. for (let price of newPrice) {
  180. noChooseSkuIds = noChooseSkuIds.concat(price.goods_sku_id_arr);
  181. }
  182. // 去重
  183. noChooseSkuIds = Array.from(new Set(noChooseSkuIds));
  184. if (isChecked) {
  185. // 去除当前选中的规格项
  186. let index = noChooseSkuIds.indexOf(skuId.toString());
  187. noChooseSkuIds.splice(index, 1);
  188. } else {
  189. // 循环去除当前已选择的规格项
  190. state.currentSkuArray.forEach((sku) => {
  191. if (sku.toString() != '') {
  192. // sku 为空是反选 填充的
  193. let index = noChooseSkuIds.indexOf(sku.toString());
  194. if (index >= 0) {
  195. // sku 存在于 noChooseSkuIds
  196. noChooseSkuIds.splice(index, 1);
  197. }
  198. }
  199. });
  200. }
  201. // 当前已选择的规格大类
  202. let chooseSkuKey = [];
  203. if (!isChecked) {
  204. // 当前已选择的规格大类
  205. state.currentSkuArray.forEach((sku, key) => {
  206. if (sku != '') {
  207. // sku 为空是反选 填充的
  208. chooseSkuKey.push(key);
  209. }
  210. });
  211. } else {
  212. // 当前点击选择的规格大类
  213. chooseSkuKey = [pid];
  214. }
  215. for (let i in state.skuList) {
  216. // 当前点击的规格,或者取消选择时候 已选中的规格 不进行处理
  217. if (chooseSkuKey.indexOf(state.skuList[i]['id']) >= 0) {
  218. continue;
  219. }
  220. for (let j in state.skuList[i]['children']) {
  221. // 如果当前规格项 id 不存在于有库存的规格项中,则禁用
  222. if (noChooseSkuIds.indexOf(state.skuList[i]['children'][j]['id'].toString()) >= 0) {
  223. state.skuList[i]['children'][j]['disabled'] = false;
  224. } else {
  225. state.skuList[i]['children'][j]['disabled'] = true;
  226. }
  227. }
  228. }
  229. };
  230. // 当前所选规格下,获取所有有库存的 skuPrice
  231. const getCanUseSkuPrice = () => {
  232. let newPrice = [];
  233. for (let price of state.skuPrices) {
  234. if (price.stock <= 0) {
  235. // || price.stock < this.goodsNum 不判断是否大于当前选择数量,在 uni-number-box 判断,并且 取 stock 和 goods_num 的小值
  236. continue;
  237. }
  238. var isOk = true;
  239. state.currentSkuArray.forEach((sku) => {
  240. // sku 不为空,并且,这个 条 skuPrice 没有被选中,则排除
  241. if (sku.toString() != '' && price.goods_sku_id_arr.indexOf(sku.toString()) < 0) {
  242. isOk = false;
  243. }
  244. });
  245. if (isOk) {
  246. newPrice.push(price);
  247. }
  248. }
  249. return newPrice;
  250. };
  251. // 选择规格
  252. const onSelectSku = (pid, skuId) => {
  253. // 清空已选择
  254. let isChecked = true; // 选中 or 取消选中
  255. if (state.currentSkuArray[pid] != undefined && state.currentSkuArray[pid] == skuId) {
  256. // 点击已被选中的,删除并填充 ''
  257. isChecked = false;
  258. state.currentSkuArray.splice(pid, 1, '');
  259. } else {
  260. // 选中
  261. state.currentSkuArray[pid] = skuId;
  262. }
  263. let chooseSkuId = []; // 选中的规格大类
  264. state.currentSkuArray.forEach((sku) => {
  265. if (sku != '') {
  266. // sku 为空是反选 填充的
  267. chooseSkuId.push(sku);
  268. }
  269. });
  270. // 当前所选规格下,所有可以选择的 skuPric
  271. let newPrice = getCanUseSkuPrice();
  272. // 判断所有规格大类是否选择完成
  273. if (chooseSkuId.length == state.skuList.length && newPrice.length) {
  274. newPrice[0].goods_num = state.selectedSkuPrice.goods_num || 1;
  275. state.selectedSkuPrice = newPrice[0];
  276. } else {
  277. state.selectedSkuPrice = {};
  278. }
  279. // 改变规格项禁用状态
  280. changeDisabled(isChecked, pid, skuId);
  281. };
  282. changeDisabled(false);
  283. </script>
  284. <style lang="scss" scoped>
  285. // 购买
  286. .buy-box {
  287. padding: 10rpx 20rpx;
  288. .buy-btn {
  289. width: 100%;
  290. height: 80rpx;
  291. border-radius: 40rpx;
  292. background: linear-gradient(90deg, #ff5854, #ff2621);
  293. color: #fff;
  294. }
  295. }
  296. .ss-modal-box {
  297. border-radius: 30rpx 30rpx 0 0;
  298. max-height: 1000rpx;
  299. .modal-header {
  300. position: relative;
  301. padding: 80rpx 20rpx 40rpx;
  302. .sku-image {
  303. width: 160rpx;
  304. height: 160rpx;
  305. border-radius: 10rpx;
  306. }
  307. .header-right {
  308. height: 160rpx;
  309. }
  310. .close-icon {
  311. position: absolute;
  312. top: 10rpx;
  313. right: 20rpx;
  314. font-size: 46rpx;
  315. opacity: 0.2;
  316. }
  317. .goods-title {
  318. font-size: 28rpx;
  319. font-weight: 500;
  320. line-height: 42rpx;
  321. }
  322. .price-text {
  323. font-size: 30rpx;
  324. font-weight: 500;
  325. color: $red;
  326. font-family: OPPOSANS;
  327. &::before {
  328. content: '¥';
  329. font-size: 24rpx;
  330. }
  331. }
  332. .stock-text {
  333. font-size: 26rpx;
  334. color: #999999;
  335. }
  336. }
  337. .modal-content {
  338. padding: 0 20rpx;
  339. .modal-content-scroll {
  340. max-height: 600rpx;
  341. .label-text {
  342. font-size: 26rpx;
  343. font-weight: 500;
  344. }
  345. .buy-num-box {
  346. height: 100rpx;
  347. }
  348. .spec-btn {
  349. height: 60rpx;
  350. min-width: 100rpx;
  351. padding: 0 30rpx;
  352. background: #f4f4f4;
  353. border-radius: 30rpx;
  354. color: #434343;
  355. font-size: 26rpx;
  356. margin-right: 10rpx;
  357. margin-bottom: 10rpx;
  358. }
  359. .checked-btn {
  360. background: linear-gradient(90deg, #ff5854, #ff2621);
  361. font-weight: 500;
  362. color: #ffffff;
  363. }
  364. .disabled-btn {
  365. font-weight: 400;
  366. color: #c6c6c6;
  367. background: #f8f8f8;
  368. }
  369. }
  370. }
  371. }
  372. .tig {
  373. border: 2rpx solid #ff5854;
  374. border-radius: 4rpx;
  375. width: 126rpx;
  376. height: 38rpx;
  377. .tig-icon {
  378. width: 40rpx;
  379. height: 40rpx;
  380. background: #ff5854;
  381. border-radius: 4rpx 0 0 4rpx;
  382. .cicon-alarm {
  383. font-size: 32rpx;
  384. color: #fff;
  385. }
  386. }
  387. .tig-title {
  388. font-size: 24rpx;
  389. font-weight: 500;
  390. line-height: normal;
  391. color: #ff6000;
  392. width: 86rpx;
  393. display: flex;
  394. justify-content: center;
  395. align-items: center;
  396. }
  397. }
  398. </style>