u-goods-sku.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432
  1. <template>
  2. <view class="up-goods-sku">
  3. <view @click="open">
  4. <slot name="trigger"></slot>
  5. </view>
  6. <up-popup
  7. v-model:show="show"
  8. mode="bottom"
  9. :closeable="pageInline ? false : closeable"
  10. :pageInline="pageInline"
  11. :border-radius="20"
  12. @close="close"
  13. >
  14. <view class="up-goods-sku-container" :style="{padding: pageInline ? '0px' : ''}">
  15. <view class="up-goods-sku__header">
  16. <slot name="header">
  17. <view class="up-goods-sku__header__image">
  18. <image :src="goodsInfo.image || goodsInfo.picture" mode="aspectFill"></image>
  19. </view>
  20. <view class="up-goods-sku__header__info">
  21. <view class="up-goods-sku__header__info__price">
  22. <text class="up-goods-sku__header__info__price__symbol">¥</text>
  23. <text class="up-goods-sku__header__info__price__value">{{ price }}</text>
  24. </view>
  25. <view class="up-goods-sku__header__info__stock">库存 {{ stock }} 件</view>
  26. <view class="up-goods-sku__header__info__selected">已选: {{ selectedSkuText }}</view>
  27. </view>
  28. </slot>
  29. </view>
  30. <scroll-view class="up-goods-sku__content" scroll-y>
  31. <view v-for="(treeItem, index) in skuTree" :key="index" class="up-goods-sku__content__item">
  32. <view class="up-goods-sku__content__item__title">{{ treeItem.label }}</view>
  33. <view class="up-goods-sku__content__item__list">
  34. <view
  35. v-for="(leafItem, leafIndex) in treeItem.children"
  36. :key="leafIndex"
  37. class="up-goods-sku__content__item__list__item"
  38. :class="{
  39. 'up-goods-sku__content__item__list__item--active': isSelected(treeItem.name, leafItem.id),
  40. 'up-goods-sku__content__item__list__item--disabled': isDisabled(treeItem.name, leafItem.id)
  41. }"
  42. @click="onSkuClick(treeItem.name, leafItem)"
  43. >
  44. <text>{{ leafItem.name }}</text>
  45. </view>
  46. </view>
  47. </view>
  48. <view class="up-goods-sku__content__count">
  49. <view class="up-goods-sku__content__count__title">购买数量</view>
  50. <view class="up-goods-sku__content__count__control">
  51. <up-number-box
  52. v-model="buyNum"
  53. :min="1"
  54. :max="maxBuyNum"
  55. :disabled="!canBuy"
  56. @change="onNumChange"
  57. ></up-number-box>
  58. </view>
  59. </view>
  60. </scroll-view>
  61. <view class="up-goods-sku__footer">
  62. <up-button
  63. type="primary"
  64. :disabled="!canBuy"
  65. @click="onConfirm"
  66. >
  67. {{ confirmText }}
  68. </up-button>
  69. </view>
  70. </view>
  71. </up-popup>
  72. </view>
  73. </template>
  74. <script>
  75. export default {
  76. name: 'up-goods-sku',
  77. props: {
  78. // 商品信息
  79. goodsInfo: {
  80. type: Object,
  81. default: () => ({})
  82. },
  83. // SKU树形结构
  84. skuTree: {
  85. type: Array,
  86. default: () => []
  87. },
  88. // SKU列表
  89. skuList: {
  90. type: Array,
  91. default: () => []
  92. },
  93. // 最大购买数量
  94. maxBuy: {
  95. type: Number,
  96. default: 999
  97. },
  98. // 确认按钮文字
  99. confirmText: {
  100. type: String,
  101. default: '确定'
  102. },
  103. // 是否显示关闭弹窗按钮
  104. closeable: {
  105. type: Boolean,
  106. default: true
  107. },
  108. // 是否页面内联模式
  109. pageInline: {
  110. type: Boolean,
  111. default: false
  112. }
  113. },
  114. data() {
  115. return {
  116. show: false,
  117. // 已选择的SKU
  118. selectedSku: {},
  119. // 购买数量
  120. buyNum: 1
  121. }
  122. },
  123. computed: {
  124. // 当前价格
  125. price() {
  126. const selectedSkuComb = this.getSelectedSkuComb()
  127. if (selectedSkuComb) {
  128. return selectedSkuComb.price || selectedSkuComb.price_fee
  129. }
  130. return this.goodsInfo.price || this.goodsInfo.price_fee || 0
  131. },
  132. // 当前库存
  133. stock() {
  134. const selectedSkuComb = this.getSelectedSkuComb()
  135. if (selectedSkuComb) {
  136. return selectedSkuComb.stock || selectedSkuComb.quantity
  137. }
  138. return this.goodsInfo.stock || this.goodsInfo.quantity || 0
  139. },
  140. // 最大购买数量
  141. maxBuyNum() {
  142. const stock = this.stock
  143. return stock > this.maxBuy ? this.maxBuy : stock
  144. },
  145. // 是否可以购买
  146. canBuy() {
  147. const selectedSkuCount = Object.keys(this.selectedSku).length
  148. const skuTreeCount = this.skuTree.length
  149. return selectedSkuCount === skuTreeCount && this.buyNum > 0 && this.stock > 0
  150. },
  151. // 已选SKU文字描述
  152. selectedSkuText() {
  153. const selected = []
  154. Object.keys(this.selectedSku).forEach(key => {
  155. const value = this.selectedSku[key]
  156. if (value) {
  157. this.skuTree.forEach(treeItem => {
  158. if (treeItem.name === key) {
  159. treeItem.children.forEach(leafItem => {
  160. if (leafItem.id === value) {
  161. selected.push(leafItem.name)
  162. }
  163. })
  164. }
  165. })
  166. }
  167. })
  168. return selected.join(', ')
  169. }
  170. },
  171. watch: {
  172. },
  173. emits: ['open', 'confirm', 'close'],
  174. created() {
  175. if (this.pageInline) {
  176. this.show = true;
  177. }
  178. },
  179. methods: {
  180. // 判断SKU是否被选中
  181. isSelected(skuKey, skuValueId) {
  182. return this.selectedSku[skuKey] === skuValueId
  183. },
  184. // 判断SKU是否禁用
  185. isDisabled(skuKey, skuValueId) {
  186. // 构造一个临时的已选中SKU对象
  187. const tempSelected = { ...this.selectedSku, [skuKey]: skuValueId }
  188. // 检查是否还有未选择的SKU维度
  189. const selectedCount = Object.keys(tempSelected).filter(key => tempSelected[key]).length
  190. const totalSkuCount = this.skuTree.length
  191. // 如果所有SKU都已选择,则检查组合是否存在
  192. if (selectedCount === totalSkuCount) {
  193. return !this.getSkuComb(tempSelected)
  194. }
  195. // 检查当前选择的SKU是否会导致无法组成有效组合
  196. for (let i = 0; i < this.skuList.length; i++) {
  197. const sku = this.skuList[i]
  198. let match = true
  199. // 检查已选中的SKU是否匹配
  200. for (const key in tempSelected) {
  201. if (tempSelected[key] && sku[key] !== tempSelected[key]) {
  202. match = false
  203. break
  204. }
  205. }
  206. if (match) {
  207. return false
  208. }
  209. }
  210. return true
  211. },
  212. // SKU点击事件
  213. onSkuClick(skuKey, skuValue) {
  214. // 如果是禁用状态,直接返回
  215. if (this.isDisabled(skuKey, skuValue.id)) {
  216. return
  217. }
  218. // 如果已选中,则取消选中
  219. if (this.selectedSku[skuKey] === skuValue.id) {
  220. this.$set(this.selectedSku, skuKey, '')
  221. } else {
  222. this.$set(this.selectedSku, skuKey, skuValue.id)
  223. }
  224. },
  225. // 数量改变事件
  226. onNumChange(e) {
  227. this.buyNum = e.value
  228. },
  229. // 获取选中的SKU组合
  230. getSelectedSkuComb() {
  231. return this.getSkuComb(this.selectedSku)
  232. },
  233. // 根据已选SKU获取组合信息
  234. getSkuComb(selectedSku) {
  235. const selected = { ...selectedSku }
  236. // 过滤掉空值
  237. Object.keys(selected).forEach(key => {
  238. if (!selected[key]) {
  239. delete selected[key]
  240. }
  241. })
  242. // 检查是否所有SKU都已选择
  243. if (Object.keys(selected).length !== this.skuTree.length) {
  244. return null
  245. }
  246. // 查找匹配的SKU组合
  247. for (let i = 0; i < this.skuList.length; i++) {
  248. const sku = this.skuList[i]
  249. let match = true
  250. for (const key in selected) {
  251. if (sku[key] !== selected[key]) {
  252. match = false
  253. break
  254. }
  255. }
  256. if (match) {
  257. return sku
  258. }
  259. }
  260. return null
  261. },
  262. // 重置选择
  263. reset() {
  264. this.selectedSku = {}
  265. this.buyNum = 1
  266. },
  267. open() {
  268. this.show = true;
  269. this.$emit('open')
  270. },
  271. // 关闭弹窗
  272. close() {
  273. this.false = true;
  274. this.$emit('close')
  275. },
  276. // 确认选择
  277. onConfirm() {
  278. if (!this.canBuy) {
  279. return
  280. }
  281. const selectedSkuComb = this.getSelectedSkuComb()
  282. this.$emit('confirm', {
  283. sku: selectedSkuComb,
  284. goodsInfo: this.goodsInfo,
  285. num: this.buyNum,
  286. selectedText: this.selectedSkuText
  287. })
  288. }
  289. }
  290. }
  291. </script>
  292. <style lang="scss" scoped>
  293. .up-goods-sku {
  294. background-color: #fff;
  295. overflow: hidden;
  296. .up-goods-sku-container {
  297. padding: 4rpx 30rpx;
  298. }
  299. &__header {
  300. display: flex;
  301. flex-direction: row;
  302. padding: 30rpx 0;
  303. position: relative;
  304. &__image {
  305. width: 180rpx;
  306. height: 180rpx;
  307. border-radius: 10rpx;
  308. overflow: hidden;
  309. margin-right: 20rpx;
  310. image {
  311. width: 100%;
  312. height: 100%;
  313. }
  314. }
  315. &__info {
  316. flex: 1;
  317. &__price {
  318. display: flex;
  319. flex-direction: row;
  320. align-items: baseline;
  321. margin-bottom: 20rpx;
  322. &__symbol {
  323. font-size: 24rpx;
  324. color: #fa3534;
  325. margin-right: 4rpx;
  326. }
  327. &__value {
  328. font-size: 36rpx;
  329. color: #fa3534;
  330. font-weight: bold;
  331. }
  332. }
  333. &__stock {
  334. font-size: 26rpx;
  335. color: #999;
  336. margin-bottom: 20rpx;
  337. }
  338. &__selected {
  339. font-size: 26rpx;
  340. color: #333;
  341. }
  342. }
  343. }
  344. &__content {
  345. max-height: 600rpx;
  346. padding: 0 30rpx 30rpx 0;
  347. &__item {
  348. margin-bottom: 30rpx;
  349. &__title {
  350. font-size: 28rpx;
  351. color: #333;
  352. margin-bottom: 20rpx;
  353. }
  354. &__list {
  355. display: flex;
  356. flex-direction: row;
  357. flex-wrap: wrap;
  358. &__item {
  359. padding: 10rpx 20rpx;
  360. border: 2rpx solid #eee;
  361. border-radius: 10rpx;
  362. margin-right: 20rpx;
  363. margin-bottom: 20rpx;
  364. font-size: 26rpx;
  365. color: #333;
  366. &--active {
  367. border-color: #fa3534;
  368. color: #fa3534;
  369. }
  370. &--disabled {
  371. color: #ccc;
  372. border-color: #eee;
  373. }
  374. }
  375. }
  376. }
  377. &__count {
  378. display: flex;
  379. flex-direction: row;
  380. align-items: center;
  381. justify-content: space-between;
  382. margin-top: 20rpx;
  383. &__title {
  384. font-size: 28rpx;
  385. color: #333;
  386. }
  387. }
  388. }
  389. &__footer {
  390. padding: 20rpx 0rpx 40rpx 0;
  391. }
  392. }
  393. </style>