s-select-seckill-sku.vue 13 KB

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