s-select-sku.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573
  1. <template>
  2. <!-- 规格弹窗 -->
  3. <su-popup :show="show" round="10" @close="emits('close')">
  4. <!-- SKU 信息 -->
  5. <view class="ss-modal-box bg-white ss-flex-col">
  6. <view class="modal-header ss-flex ss-col-center">
  7. <view class="header-left ss-m-r-30">
  8. <image
  9. class="sku-image"
  10. :src="state.selectedSku.picUrl || goodsInfo.picUrl"
  11. mode="aspectFill"
  12. />
  13. </view>
  14. <view class="header-right ss-flex-col ss-row-between ss-flex-1">
  15. <view class="goods-title ss-line-2">{{ goodsInfo.name }}</view>
  16. <view class="header-right-bottom ss-flex ss-col-center ss-row-between">
  17. <view class="ss-flex">
  18. <view class="price-text">
  19. <image
  20. src="@/static/icon/points.png"
  21. v-if="goodsInfo.spuPayType == 2"
  22. style="width: 30rpx; height: 30rpx"
  23. ></image>
  24. <text v-else>¥</text>
  25. {{ fen2yuan(state.selectedSku.price || goodsInfo.price) }}
  26. </view>
  27. <view
  28. class="origin-price-text ss-m-l-10"
  29. v-if="
  30. state.selectedSku.promotionFee >= 0 ||
  31. goodsInfo.promotionFee >= 0
  32. "
  33. >
  34. 数字权益:{{
  35. fen2yuan(
  36. state.selectedSku.promotionFee || goodsInfo.promotionFee,
  37. )
  38. }}
  39. </view>
  40. </view>
  41. <view class="stock-text ss-m-l-20">
  42. {{ formatStock('exact', state.selectedSku.stock || goodsInfo.stock) }}
  43. </view>
  44. </view>
  45. </view>
  46. </view>
  47. <!-- 属性选择 -->
  48. <view class="modal-content ss-flex-1">
  49. <scroll-view scroll-y="true" class="modal-content-scroll" @touchmove.stop>
  50. <view
  51. class="sku-item ss-m-b-20"
  52. v-for="property in propertyList"
  53. :key="property.id"
  54. >
  55. <view class="label-text ss-m-b-20">{{ property.name }}</view>
  56. <view class="ss-flex ss-col-center ss-flex-wrap">
  57. <button
  58. class="ss-reset-button spec-btn"
  59. v-for="value in property.values"
  60. :class="[
  61. {
  62. 'ui-BG-Main-Gradient':
  63. state.currentPropertyArray[property.id] === value.id,
  64. },
  65. {
  66. 'disabled-btn': value.disabled === true,
  67. },
  68. ]"
  69. :key="value.id"
  70. :disabled="value.disabled === true"
  71. @tap="onSelectSku(property.id, value.id)"
  72. >
  73. {{ value.name }}
  74. </button>
  75. </view>
  76. </view>
  77. <view
  78. class="buy-num-box ss-flex ss-col-center ss-row-between ss-m-b-40"
  79. v-if="goodsInfo.spuPayType == 2"
  80. >
  81. <view class="label-text"
  82. >购买金额 (当前可用数字权益:{{
  83. points2point(userWallet.integralDO.currentQuota)
  84. }})</view
  85. >
  86. <view class="ss-flex ss-col-center">
  87. <image
  88. src="@/static/icon/points.png"
  89. v-if="goodsInfo.spuPayType == 2"
  90. style="width: 30rpx; height: 30rpx"
  91. ></image>
  92. <input
  93. v-model="state.selectedSku.use_points"
  94. class="uni-input input-points"
  95. style="width: 100rpx; text-align: center"
  96. type="number"
  97. placeholder="0"
  98. @input="inputPoints"
  99. :disabled="!state.selectedSku.id"
  100. />
  101. </view>
  102. </view>
  103. <view class="buy-num-box ss-flex ss-col-center ss-row-between ss-m-b-40" v-else>
  104. <view class="label-text">购买数量</view>
  105. <su-number-box
  106. :min="1"
  107. :max="state.selectedSku.stock"
  108. :step="1"
  109. v-model="state.selectedSku.goods_num"
  110. @change="onNumberChange($event)"
  111. />
  112. </view>
  113. </scroll-view>
  114. </view>
  115. <!-- 操作区 -->
  116. <view class="modal-footer border-top">
  117. <view
  118. class="buy-box ss-flex ss-col-center ss-flex ss-col-center ss-row-center"
  119. v-if="goodsInfo.spuType && goodsInfo.spuPayType == 1"
  120. >
  121. <button class="ss-reset-button add-btn ui-Shadow-Main" @tap="onAddCart"
  122. >加入购物车</button
  123. >
  124. <button class="ss-reset-button buy-btn ui-Shadow-Main" @tap="onBuy"
  125. >立即购买</button
  126. >
  127. </view>
  128. <view
  129. class="buy-box ss-flex ss-col-center ss-flex ss-col-center ss-row-center"
  130. v-else
  131. >
  132. <button class="ss-reset-button one-buy-btn ui-Shadow-Main" @tap="onBuy"
  133. >立即购买</button
  134. >
  135. </view>
  136. </view>
  137. </view>
  138. </su-popup>
  139. </template>
  140. <script setup>
  141. import { computed, reactive, watch, onMounted,nextTick } from 'vue';
  142. import sheep from '@/sheep';
  143. import {
  144. formatStock,
  145. convertProductPropertyList,
  146. fen2yuan,
  147. points2point,
  148. } from '@/sheep/hooks/useGoods';
  149. const emits = defineEmits(['change', 'addCart', 'buy', 'close']);
  150. const props = defineProps({
  151. goodsInfo: {
  152. type: Object,
  153. default() {},
  154. },
  155. show: {
  156. type: Boolean,
  157. default: false,
  158. },
  159. });
  160. const userWallet = computed(() => sheep.$store('user').userWallet);
  161. const state = reactive({
  162. selectedSku: {}, // 选中的 SKU
  163. currentPropertyArray: [], // 当前选中的属性,实际是个 Map。key 是 property 编号,value 是 value 编号
  164. });
  165. const propertyList = convertProductPropertyList(props.goodsInfo.skus);
  166. // SKU 列表
  167. const skuList = computed(() => {
  168. let skuPrices = props.goodsInfo.skus;
  169. for (let price of skuPrices) {
  170. price.value_id_array = price.properties.map((item) => item.valueId);
  171. }
  172. return skuPrices;
  173. });
  174. function inputPoints(e) {
  175. const points = e.detail.value;
  176. const userCanUsePoints = parseFloat(points2point(userWallet.value.integralDO.currentQuota));
  177. state.selectedSku.goods_num = parseInt(state.selectedSku.use_points / fen2yuan(state.selectedSku.price));
  178. if (points > userCanUsePoints) {
  179. sheep.$helper.toast('可用数字权益不足');
  180. console.log('输入的数字权益大于可用数字权益了', points, userCanUsePoints);
  181. nextTick(() => {
  182. state.selectedSku.use_points = userCanUsePoints;
  183. state.selectedSku.goods_num = parseInt(state.selectedSku.use_points / fen2yuan(state.selectedSku.price));
  184. });
  185. }
  186. // 如果输入的金额转换的商品数大于选中的sku的库存数,提示
  187. if(state.selectedSku.use_points / fen2yuan(state.selectedSku.price) > state.selectedSku.stock){
  188. sheep.$helper.toast('库存不足,已为您转换到可购买最多数量');
  189. nextTick(() => {
  190. state.selectedSku.use_points = (state.selectedSku.stock * fen2yuan(state.selectedSku.price) ).toFixed(2)
  191. console.log(state.selectedSku.use_points )
  192. // console.log(state.selectedSku.stock,fen2yuan( state.selectedSku.price))
  193. });
  194. }
  195. console.log("购买的数量",state.selectedSku.goods_num)
  196. console.log(state.selectedSku.use_points )
  197. }
  198. watch(
  199. () => state.selectedSku,
  200. (newVal) => {
  201. emits('change', newVal);
  202. },
  203. {
  204. immediate: true, // 立即执行
  205. deep: true, // 深度监听
  206. },
  207. );
  208. // 输入框改变数量
  209. function onNumberChange(e) {
  210. if (e === 0) return;
  211. if (state.selectedSku.goods_num === e) return;
  212. state.selectedSku.goods_num = e;
  213. }
  214. // 加入购物车
  215. function onAddCart() {
  216. if (state.selectedSku.id <= 0) {
  217. sheep.$helper.toast('请选择规格');
  218. return;
  219. }
  220. if (state.selectedSku.stock <= 0) {
  221. sheep.$helper.toast('库存不足');
  222. return;
  223. }
  224. emits('addCart', state.selectedSku);
  225. }
  226. // 立即购买
  227. function onBuy() {
  228. if (props.goodsInfo.spuPayType == 2) {
  229. if (state.selectedSku.goods_num < 1 || !state.selectedSku.use_points) {
  230. sheep.$helper.toast('输入金额少于可购买数量');
  231. return;
  232. }
  233. // 下单时检测如果
  234. if(state.selectedSku.goods_num > state.selectedSku.stock){
  235. state.selectedSku.use_points = (state.selectedSku.stock * fen2yuan(state.selectedSku.price) ).toFixed(2)
  236. state.selectedSku.goods_num = state.selectedSku.stock
  237. }
  238. }
  239. if (state.selectedSku.id <= 0) {
  240. sheep.$helper.toast('请选择规格');
  241. return;
  242. }
  243. if (state.selectedSku.stock <= 0) {
  244. sheep.$helper.toast('库存不足');
  245. return;
  246. }
  247. emits('buy', state.selectedSku);
  248. }
  249. // 改变禁用状态:计算每个 property 属性值的按钮,是否禁用
  250. function changeDisabled(isChecked = false, propertyId = 0, valueId = 0) {
  251. let newSkus = []; // 所有可以选择的 sku 数组
  252. if (isChecked) {
  253. // 情况一:选中 property
  254. // 获得当前点击选中 property 的、所有可用 SKU
  255. for (let price of skuList.value) {
  256. if (price.stock <= 0) {
  257. continue;
  258. }
  259. if (price.value_id_array.indexOf(valueId) >= 0) {
  260. newSkus.push(price);
  261. }
  262. }
  263. } else {
  264. // 情况二:取消选中 property
  265. // 当前所选 property 下,所有可以选择的 SKU
  266. newSkus = getCanUseSkuList();
  267. }
  268. // 所有存在并且有库存未选择的 SKU 的 value 属性值 id
  269. let noChooseValueIds = [];
  270. for (let price of newSkus) {
  271. noChooseValueIds = noChooseValueIds.concat(price.value_id_array);
  272. }
  273. noChooseValueIds = Array.from(new Set(noChooseValueIds)); // 去重
  274. if (isChecked) {
  275. // 去除当前选中的 value 属性值 id
  276. let index = noChooseValueIds.indexOf(valueId);
  277. noChooseValueIds.splice(index, 1);
  278. } else {
  279. // 循环去除当前已选择的 value 属性值 id
  280. state.currentPropertyArray.forEach((currentPropertyId) => {
  281. if (currentPropertyId.toString() !== '') {
  282. return;
  283. }
  284. // currentPropertyId 为空是反选 填充的
  285. let index = noChooseValueIds.indexOf(currentPropertyId);
  286. if (index >= 0) {
  287. // currentPropertyId 存在于 noChooseValueIds
  288. noChooseValueIds.splice(index, 1);
  289. }
  290. });
  291. }
  292. // 当前已选择的 property 数组
  293. let choosePropertyIds = [];
  294. if (!isChecked) {
  295. // 当前已选择的 property
  296. state.currentPropertyArray.forEach((currentPropertyId, currentValueId) => {
  297. if (currentPropertyId !== '') {
  298. // currentPropertyId 为空是反选 填充的
  299. choosePropertyIds.push(currentValueId);
  300. }
  301. });
  302. } else {
  303. // 当前点击选择的 property
  304. choosePropertyIds = [propertyId];
  305. }
  306. for (let propertyIndex in propertyList) {
  307. // 当前点击的 property、或者取消选择时候,已选中的 property 不进行处理
  308. if (choosePropertyIds.indexOf(propertyList[propertyIndex]['id']) >= 0) {
  309. continue;
  310. }
  311. // 如果当前 property id 不存在于有库存的 SKU 中,则禁用
  312. for (let valueIndex in propertyList[propertyIndex]['values']) {
  313. propertyList[propertyIndex]['values'][valueIndex]['disabled'] =
  314. noChooseValueIds.indexOf(
  315. propertyList[propertyIndex]['values'][valueIndex]['id'],
  316. ) < 0; // true 禁用 or false 不禁用
  317. }
  318. }
  319. }
  320. // 当前所选属性下,获取所有有库存的 SKU 们
  321. function getCanUseSkuList() {
  322. let newSkus = [];
  323. for (let sku of skuList.value) {
  324. if (sku.stock <= 0) {
  325. continue;
  326. }
  327. let isOk = true;
  328. state.currentPropertyArray.forEach((propertyId) => {
  329. // propertyId 不为空,并且,这个 条 sku 没有被选中,则排除
  330. if (propertyId.toString() !== '' && sku.value_id_array.indexOf(propertyId) < 0) {
  331. isOk = false;
  332. }
  333. });
  334. if (isOk) {
  335. newSkus.push(sku);
  336. }
  337. }
  338. return newSkus;
  339. }
  340. // 选择规格
  341. function onSelectSku(propertyId, valueId) {
  342. // console.log(propertyId,valueId)
  343. // 清空已输入金额
  344. state.selectedSku.use_points = 0;
  345. // 清空已选择
  346. let isChecked = true; // 选中 or 取消选中
  347. if (
  348. state.currentPropertyArray[propertyId] !== undefined &&
  349. state.currentPropertyArray[propertyId] === valueId
  350. ) {
  351. // 点击已被选中的,删除并填充 ''
  352. isChecked = false;
  353. state.currentPropertyArray.splice(propertyId, 1, '');
  354. } else {
  355. // 选中
  356. state.currentPropertyArray[propertyId] = valueId;
  357. }
  358. // 选中的 property 大类
  359. let choosePropertyId = [];
  360. state.currentPropertyArray.forEach((currentPropertyId) => {
  361. if (currentPropertyId !== '') {
  362. // currentPropertyId 为空是反选 填充的
  363. choosePropertyId.push(currentPropertyId);
  364. }
  365. });
  366. // 当前所选 property 下,所有可以选择的 SKU 们
  367. let newSkuList = getCanUseSkuList();
  368. // 判断所有 property 大类是否选择完成
  369. if (choosePropertyId.length === propertyList.length && newSkuList.length) {
  370. newSkuList[0].goods_num = state.selectedSku.goods_num || 1;
  371. state.selectedSku = newSkuList[0];
  372. } else {
  373. state.selectedSku = {};
  374. }
  375. // 改变 property 禁用状态
  376. changeDisabled(isChecked, propertyId, valueId);
  377. }
  378. changeDisabled(false);
  379. onMounted(() => {
  380. // 如果商品的属性只有一条,则默认选中
  381. if (propertyList.length == 1 && propertyList[0].values.length == 1) {
  382. onSelectSku(0, 0);
  383. }
  384. // 这里可以处理其他初始化逻辑,但请注意这不会接收到 onLoad 的 options 参数
  385. });
  386. // TODO 非繁人:待讨论的优化点:1)单规格,要不要默认选中;2)默认要不要选中第一个规格
  387. </script>
  388. <style lang="scss" scoped>
  389. .origin-price-text {
  390. font-size: 22rpx;
  391. font-weight: 400;
  392. color: $gray-c;
  393. font-family: OPPOSANS;
  394. background: #ffca3e;
  395. padding: 2px 8px;
  396. border-radius: 4px;
  397. display: inline-block;
  398. color: #597533;
  399. }
  400. // 购买
  401. .buy-box {
  402. padding: 10rpx 0;
  403. .add-btn {
  404. width: 356rpx;
  405. height: 80rpx;
  406. border-radius: 40rpx 0 0 40rpx;
  407. background-color: var(--ui-BG-Main-light);
  408. color: var(--ui-BG-Main);
  409. }
  410. .buy-btn {
  411. width: 356rpx;
  412. height: 80rpx;
  413. border-radius: 0 40rpx 40rpx 0;
  414. background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
  415. color: #fff;
  416. }
  417. .one-buy-btn {
  418. width: 98%;
  419. height: 80rpx;
  420. border-radius: 40rpx;
  421. background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
  422. color: #fff;
  423. }
  424. .score-btn {
  425. width: 100%;
  426. margin: 0 20rpx;
  427. height: 80rpx;
  428. border-radius: 40rpx;
  429. background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
  430. color: #fff;
  431. }
  432. }
  433. .ss-modal-box {
  434. border-radius: 30rpx 30rpx 0 0;
  435. max-height: 1000rpx;
  436. .modal-header {
  437. position: relative;
  438. padding: 80rpx 20rpx 40rpx;
  439. .sku-image {
  440. width: 160rpx;
  441. height: 160rpx;
  442. border-radius: 10rpx;
  443. }
  444. .header-right {
  445. height: 160rpx;
  446. }
  447. .close-icon {
  448. position: absolute;
  449. top: 10rpx;
  450. right: 20rpx;
  451. font-size: 46rpx;
  452. opacity: 0.2;
  453. }
  454. .goods-title {
  455. font-size: 28rpx;
  456. font-weight: 500;
  457. line-height: 42rpx;
  458. }
  459. .score-img {
  460. width: 36rpx;
  461. height: 36rpx;
  462. margin: 0 4rpx;
  463. }
  464. .score-text {
  465. font-size: 30rpx;
  466. font-weight: 500;
  467. color: $red;
  468. font-family: OPPOSANS;
  469. }
  470. .price-text {
  471. font-size: 30rpx;
  472. font-weight: 500;
  473. color: $red;
  474. font-family: OPPOSANS;
  475. &::before {
  476. // content: '¥';
  477. // font-size: 30rpx;
  478. // font-weight: 500;
  479. // color: $red;
  480. }
  481. }
  482. .stock-text {
  483. font-size: 26rpx;
  484. color: #999999;
  485. }
  486. }
  487. .modal-content {
  488. padding: 0 20rpx;
  489. .modal-content-scroll {
  490. max-height: 600rpx;
  491. .label-text {
  492. font-size: 26rpx;
  493. font-weight: 500;
  494. }
  495. .buy-num-box {
  496. height: 100rpx;
  497. }
  498. .spec-btn {
  499. height: 60rpx;
  500. min-width: 100rpx;
  501. padding: 0 30rpx;
  502. background: #f4f4f4;
  503. border-radius: 30rpx;
  504. color: #434343;
  505. font-size: 26rpx;
  506. margin-right: 10rpx;
  507. margin-bottom: 10rpx;
  508. }
  509. .disabled-btn {
  510. font-weight: 400;
  511. color: #c6c6c6;
  512. background: #f8f8f8;
  513. }
  514. }
  515. }
  516. }
  517. </style>