s-select-sku.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394
  1. <template>
  2. <!-- SKU 信息 -->
  3. <div>
  4. <!-- 属性选择 -->
  5. <div class="d-flex mb-5" v-for="property in propertyList" :key="property.id">
  6. <span class="parameterColor mb-1">{{ property.name }}</span>:
  7. <span style="flex: 1;">
  8. <v-chip
  9. class="spec-btn mb-1 mr-3"
  10. :class="[
  11. { 'ui-BG-Main-Gradient': state.currentPropertyArray[property.id] === value.id, },
  12. { 'disabled-btn': value.disabled === true, },
  13. ]"
  14. v-for="value in property.values"
  15. :key="value.id"
  16. size="small"
  17. label
  18. density="comfortable"
  19. color="primary"
  20. variant="outlined"
  21. :disabled="value.disabled === true"
  22. @click="onSelectSku(property.id, value.id)"
  23. >
  24. {{ value.name }}
  25. </v-chip>
  26. </span>
  27. </div>
  28. <!-- 购买数量 -->
  29. <div class="modal-content">
  30. <div>
  31. <div class="buyCount mb-10">
  32. <span class="parameterColor"><span class="l-s-10">数量</span>:</span>
  33. <su-number-box
  34. :min="1"
  35. :max="state.selectedSku.stock"
  36. :totalStock="totalStock"
  37. :step="1"
  38. ref="selectSkuRef"
  39. v-model="state.selectedSku.goods_num"
  40. @change="onNumberChange($event)"
  41. />
  42. </div>
  43. </div>
  44. </div>
  45. <!-- 操作区 -->
  46. <div>
  47. <v-btn class="buttons" color="primary" @click="onBuy">立即购买</v-btn>
  48. <v-btn class="ml-3 px-8" color="warning" @click="onAddCart">加入购物车</v-btn>
  49. </div>
  50. </div>
  51. </template>
  52. <script setup>
  53. defineOptions({name: 'wares-s-select-sku'})
  54. import Snackbar from '@/plugins/snackbar'
  55. import suNumberBox from '@/components/FormUI/su-number-box/su-number-box.vue'
  56. import { computed, reactive, watch, ref } from 'vue'
  57. import { convertProductPropertyList } from '@/views/mall/utils'
  58. const emits = defineEmits(['change', 'addCart', 'buy', 'close']);
  59. const props = defineProps({
  60. goodsInfo: {
  61. type: Object,
  62. default() {},
  63. },
  64. });
  65. const totalStock = ref(props.goodsInfo?.stock-0 || 0)
  66. const state = reactive({
  67. selectedSku: {}, // 选中的 SKU
  68. currentPropertyArray: [], // 当前选中的属性,实际是个 Map。key 是 property 编号,value 是 value 编号
  69. });
  70. const propertyList = convertProductPropertyList(props.goodsInfo.skus);
  71. // SKU 列表
  72. const skuList = computed(() => {
  73. let skuPrices = props.goodsInfo.skus;
  74. for (let price of skuPrices) {
  75. price.value_id_array = price.properties.map((item) => item.valueId);
  76. }
  77. return skuPrices;
  78. });
  79. watch(
  80. () => state.selectedSku,
  81. (newVal) => {
  82. if (newVal?.stock) totalStock.value = newVal.stock
  83. emits('change', newVal);
  84. },
  85. {
  86. immediate: true, // 立即执行
  87. deep: true, // 深度监听
  88. },
  89. );
  90. // 输入框改变数量
  91. function onNumberChange(e) {
  92. if (e === 0) return;
  93. if (state.selectedSku.goods_num === e) return;
  94. state.selectedSku.goods_num = e;
  95. }
  96. // 加入购物车
  97. function onAddCart() {
  98. if (state.selectedSku.id <= 0) {
  99. Snackbar.warning('请选择商品规格')
  100. return;
  101. }
  102. if (state.selectedSku.stock <= 0) {
  103. Snackbar.warning('库存不足')
  104. return;
  105. }
  106. emits('addCart', state.selectedSku);
  107. }
  108. // 立即购买
  109. function onBuy() {
  110. if (!state?.selectedSku?.id || state.selectedSku.id <= 0) {
  111. Snackbar.warning('请选择商品规格')
  112. return;
  113. }
  114. if (!state?.selectedSku?.stock || state.selectedSku.stock <= 0) {
  115. Snackbar.warning('库存不足')
  116. return;
  117. }
  118. emits('buy', state.selectedSku);
  119. }
  120. // 改变禁用状态:计算每个 property 属性值的按钮,是否禁用
  121. function changeDisabled(isChecked = false, propertyId = 0, valueId = 0) {
  122. let newSkus = []; // 所有可以选择的 sku 数组
  123. if (isChecked) {
  124. // 情况一:选中 property
  125. // 获得当前点击选中 property 的、所有可用 SKU
  126. for (let price of skuList.value) {
  127. if (price.stock <= 0) {
  128. continue;
  129. }
  130. if (price.value_id_array.indexOf(valueId) >= 0) {
  131. newSkus.push(price);
  132. }
  133. }
  134. } else {
  135. // 情况二:取消选中 property
  136. // 当前所选 property 下,所有可以选择的 SKU
  137. newSkus = getCanUseSkuList();
  138. }
  139. // 所有存在并且有库存未选择的 SKU 的 value 属性值 id
  140. let noChooseValueIds = [];
  141. for (let price of newSkus) {
  142. noChooseValueIds = noChooseValueIds.concat(price.value_id_array);
  143. }
  144. noChooseValueIds = Array.from(new Set(noChooseValueIds)); // 去重
  145. if (isChecked) {
  146. // 去除当前选中的 value 属性值 id
  147. let index = noChooseValueIds.indexOf(valueId);
  148. noChooseValueIds.splice(index, 1);
  149. } else {
  150. // 循环去除当前已选择的 value 属性值 id
  151. state.currentPropertyArray.forEach((currentPropertyId) => {
  152. if (currentPropertyId.toString() !== '') {
  153. return;
  154. }
  155. // currentPropertyId 为空是反选 填充的
  156. let index = noChooseValueIds.indexOf(currentPropertyId);
  157. if (index >= 0) {
  158. // currentPropertyId 存在于 noChooseValueIds
  159. noChooseValueIds.splice(index, 1);
  160. }
  161. });
  162. }
  163. // 当前已选择的 property 数组
  164. let choosePropertyIds = [];
  165. if (!isChecked) {
  166. // 当前已选择的 property
  167. state.currentPropertyArray.forEach((currentPropertyId, currentValueId) => {
  168. if (currentPropertyId !== '') {
  169. // currentPropertyId 为空是反选 填充的
  170. choosePropertyIds.push(currentValueId);
  171. }
  172. });
  173. } else {
  174. // 当前点击选择的 property
  175. choosePropertyIds = [propertyId];
  176. }
  177. for (let propertyIndex in propertyList) {
  178. // 当前点击的 property、或者取消选择时候,已选中的 property 不进行处理
  179. if (choosePropertyIds.indexOf(propertyList[propertyIndex]['id']) >= 0) {
  180. continue;
  181. }
  182. // 如果当前 property id 不存在于有库存的 SKU 中,则禁用
  183. for (let valueIndex in propertyList[propertyIndex]['values']) {
  184. propertyList[propertyIndex]['values'][valueIndex]['disabled'] =
  185. noChooseValueIds.indexOf(propertyList[propertyIndex]['values'][valueIndex]['id']) < 0; // true 禁用 or false 不禁用
  186. }
  187. }
  188. }
  189. // 当前所选属性下,获取所有有库存的 SKU 们
  190. function getCanUseSkuList() {
  191. let newSkus = [];
  192. for (let sku of skuList.value) {
  193. if (sku.stock <= 0) {
  194. continue;
  195. }
  196. let isOk = true;
  197. state.currentPropertyArray.forEach((propertyId) => {
  198. // propertyId 不为空,并且,这个 条 sku 没有被选中,则排除
  199. if (propertyId.toString() !== '' && sku.value_id_array.indexOf(propertyId) < 0) {
  200. isOk = false;
  201. }
  202. });
  203. if (isOk) {
  204. newSkus.push(sku);
  205. }
  206. }
  207. return newSkus;
  208. }
  209. // 选择规格
  210. function onSelectSku(propertyId, valueId) {
  211. // 清空已选择
  212. let isChecked = true; // 选中 or 取消选中
  213. if (
  214. state.currentPropertyArray[propertyId] !== undefined &&
  215. state.currentPropertyArray[propertyId] === valueId
  216. ) {
  217. // 点击已被选中的,删除并填充 ''
  218. isChecked = false;
  219. state.currentPropertyArray.splice(propertyId, 1, '');
  220. } else {
  221. // 选中
  222. state.currentPropertyArray[propertyId] = valueId;
  223. }
  224. // 选中的 property 大类
  225. let choosePropertyId = [];
  226. state.currentPropertyArray.forEach((currentPropertyId) => {
  227. if (currentPropertyId !== '') {
  228. // currentPropertyId 为空是反选 填充的
  229. choosePropertyId.push(currentPropertyId);
  230. }
  231. });
  232. // 当前所选 property 下,所有可以选择的 SKU 们
  233. let newSkuList = getCanUseSkuList();
  234. // 判断所有 property 大类是否选择完成
  235. if (choosePropertyId.length === propertyList.length && newSkuList.length) {
  236. newSkuList[0].goods_num = state.selectedSku.goods_num || 1;
  237. state.selectedSku = newSkuList[0];
  238. } else {
  239. state.selectedSku = {};
  240. }
  241. // 改变 property 禁用状态
  242. changeDisabled(isChecked, propertyId, valueId);
  243. }
  244. changeDisabled(false);
  245. // TODO 芋艿:待讨论的优化点:1)单规格,要不要默认选中;2)默认要不要选中第一个规格
  246. </script>
  247. <style lang="scss" scoped>
  248. // 主题色渐变,横向
  249. .ui-BG-Main-Gradient {
  250. // background: linear-gradient(90deg, #ff3000, #ff300099);
  251. background: var(--v-primary-base);
  252. color: #fff !important;
  253. }
  254. .disabled-btn {
  255. color: #c6c6c6;
  256. background: #f8f8f8;
  257. }
  258. .parameterColor {
  259. color: #7a7a7a;
  260. }
  261. input::-webkit-outer-spin-button, input::-webkit-inner-spin-button{
  262. -webkit-appearance: none !important;
  263. margin: 0;
  264. }
  265. .inputItem {
  266. width: 70px; border: 1px solid #eee; padding: 0 5px; text-align: center;
  267. }
  268. .buyCount {
  269. display: flex;
  270. align-items: center;
  271. }
  272. .ss-modal-box {
  273. border-radius: 30px 30px 0 0;
  274. max-height: 1000px;
  275. .modal-header {
  276. position: relative;
  277. padding: 80px 20px 40px;
  278. .sku-image {
  279. width: 160px;
  280. height: 160px;
  281. border-radius: 10px;
  282. }
  283. .header-right {
  284. height: 160px;
  285. }
  286. .close-icon {
  287. position: absolute;
  288. top: 10px;
  289. right: 20px;
  290. font-size: 46px;
  291. opacity: 0.2;
  292. }
  293. .goods-title {
  294. font-size: 28px;
  295. font-weight: 500;
  296. line-height: 42px;
  297. }
  298. .score-img {
  299. width: 36px;
  300. height: 36px;
  301. margin: 0 4px;
  302. }
  303. .stock-text {
  304. font-size: 26px;
  305. color: #999999;
  306. }
  307. }
  308. .modal-content {
  309. padding: 0 20px;
  310. .modal-content-scroll {
  311. max-height: 600px;
  312. .label-text {
  313. font-size: 26px;
  314. font-weight: 500;
  315. }
  316. .buy-num-box {
  317. height: 100px;
  318. }
  319. .spec-btn {
  320. height: 60px;
  321. min-width: 100px;
  322. padding: 0 30px;
  323. background: #f4f4f4;
  324. border-radius: 30px;
  325. color: #434343;
  326. font-size: 26px;
  327. margin-right: 10px;
  328. margin-bottom: 10px;
  329. }
  330. .disabled-btn {
  331. // font-weight: 400;
  332. color: #c6c6c6;
  333. background: #f8f8f8;
  334. }
  335. }
  336. }
  337. }
  338. .iconBox {
  339. width: fit-content;
  340. height: fit-content;
  341. padding: 2px 10px;
  342. background-color: rgb(255, 242, 241);
  343. color: #ff2621;
  344. font-size: 24px;
  345. margin-left: 5px;
  346. }
  347. .origin-price-text {
  348. font-size: 26px;
  349. font-weight: 400;
  350. text-decoration: line-through;
  351. color: gray;
  352. font-family: OPPOSANS;
  353. &::before {
  354. content: '¥';
  355. }
  356. }
  357. </style>