s-select-sku.vue 16 KB

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