电商图片显示组件和SKU组件的封装

一、电商图片显示组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
<script setup>
import { ref, watch } from 'vue'
import { useMouseInElement } from '@vueuse/core'

// 图片列表
const imageList = [
'https://yanxuan-item.nosdn.127.net/d917c92e663c5ed0bb577c7ded73e4ec.png',
'https://yanxuan-item.nosdn.127.net/e801b9572f0b0c02a52952b01adab967.jpg',
'https://yanxuan-item.nosdn.127.net/b52c447ad472d51adbdde1a83f550ac2.jpg',
'https://yanxuan-item.nosdn.127.net/f93243224dc37674dfca5874fe089c60.jpg',
'https://yanxuan-item.nosdn.127.net/f881cfe7de9a576aaeea6ee0d1d24823.jpg',
]

// 1.小图切换大图显示
const activeIndex = ref(0)

const enterhandler = (i) => {
activeIndex.value = i
}

// 2. 获取鼠标相对位置
const target = ref(null)
const { elementX, elementY, isOutside } = useMouseInElement(target)

// 3. 控制滑块跟随鼠标移动(监听elementX/Y变化,一旦变化 重新设置left/top)
const left = ref(0)
const top = ref(0)

const positionX = ref(0)
const positionY = ref(0)
watch([elementX, elementY, isOutside], () => {
// 如果鼠标没有移入到盒子里面 直接不执行后面的逻辑
if (isOutside.value) return

// 有效范围内控制滑块距离
// 横向
if (elementX.value > 100 && elementX.value < 300) {
left.value = elementX.value - 100
}
// 纵向
if (elementY.value > 100 && elementY.value < 300) {
top.value = elementY.value - 100
}

// 处理边界
if (elementX.value > 300) {
left.value = 200
}
if (elementX.value < 100) {
left.value = 0
}

if (elementY.value > 300) {
top.value = 200
}
if (elementY.value < 100) {
top.value = 0
}

// 控制大图的显示
positionX.value = -left.value * 2
positionY.value = -top.value * 2
})
</script>

<template>
<div class="goods-image">
<!-- 左侧大图-->
<div class="middle" ref="target">
<img :src="imageList[activeIndex]" alt="" />
<!-- 蒙层小滑块 -->
<div
class="layer"
v-show="!isOutside"
:style="{ left: `${left}px`, top: `${top}px` }"
></div>
</div>
<!-- 小图列表 -->
<ul class="small">
<li
v-for="(img, i) in imageList"
:key="i"
@mouseenter="enterhandler(i)"
:class="{ active: i === activeIndex }"
>
<img :src="img" alt="" />
</li>
</ul>
<!-- 放大镜大图 -->
<div
class="large"
:style="[
{
backgroundImage: `url(${imageList[0]})`,
backgroundPositionX: `${positionX}px`,
backgroundPositionY: `${positionY}px`,
},
]"
v-show="!isOutside"
></div>
</div>
</template>

<style scoped lang="scss">
.goods-image {
width: 480px;
height: 400px;
position: relative;
display: flex;

.middle {
width: 400px;
height: 400px;
background: #f5f5f5;
}

.large {
position: absolute;
top: 0;
left: 412px;
width: 400px;
height: 400px;
z-index: 500;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
background-repeat: no-repeat;
// 背景图:盒子的大小 = 2:1 将来控制背景图的移动来实现放大的效果查看 background-position
background-size: 800px 800px;
background-color: #f8f8f8;
}

.layer {
width: 200px;
height: 200px;
background: rgba(0, 0, 0, 0.2);
// 绝对定位 然后跟随咱们鼠标控制left和top属性就可以让滑块移动起来
left: 0;
top: 0;
position: absolute;
}

.small {
width: 80px;

li {
width: 68px;
height: 68px;
margin-left: 12px;
margin-bottom: 15px;
cursor: pointer;

&:hover,
&.active {
border: 2px solid $xtxColor;
}
}
}
}
</style>

二、SKU 组件

此组件需要用到计算数组子集的算法

power-set.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
export default function bwPowerSet(originalSet) {
const subSets = []

// We will have 2^n possible combinations (where n is a length of original set).
// It is because for every element of original set we will decide whether to include
// it or not (2 options for each set element).
const numberOfCombinations = 2 ** originalSet.length

// Each number in binary representation in a range from 0 to 2^n does exactly what we need:
// it shows by its bits (0 or 1) whether to include related element from the set or not.
// For example, for the set {1, 2, 3} the binary number of 0b010 would mean that we need to
// include only "2" to the current set.
for (
let combinationIndex = 0;
combinationIndex < numberOfCombinations;
combinationIndex += 1
) {
const subSet = []

for (
let setElementIndex = 0;
setElementIndex < originalSet.length;
setElementIndex += 1
) {
// Decide whether we need to include current element into the subset or not.
if (combinationIndex & (1 << setElementIndex)) {
subSet.push(originalSet[setElementIndex])
}
}

// Add current subset to the list of all subsets.
subSets.push(subSet)
}

return subSets
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
<template>
<div class="goods-sku">
<dl v-for="item in goods.specs" :key="item.id">
<dt>{{ item.name }}</dt>
<dd>
<template v-for="val in item.values" :key="val.name">
<img
:class="{ selected: val.selected, disabled: val.disabled }"
@click="clickSpecs(item, val)"
v-if="val.picture"
:src="val.picture"
/>
<span
:class="{ selected: val.selected, disabled: val.disabled }"
@click="clickSpecs(item, val)"
v-else
>{{ val.name }}</span
>
</template>
</dd>
</dl>
</div>
</template>
<script>
import { watchEffect } from 'vue'
import getPowerSet from './power-set'
const spliter = '★'
// 根据skus数据得到路径字典对象
const getPathMap = (skus) => {
const pathMap = {}
if (skus && skus.length > 0) {
skus.forEach((sku) => {
// 1. 过滤出有库存有效的sku
if (sku.inventory) {
// 2. 得到sku属性值数组
const specs = sku.specs.map((spec) => spec.valueName)
// 3. 得到sku属性值数组的子集
const powerSet = getPowerSet(specs)
// 4. 设置给路径字典对象
powerSet.forEach((set) => {
const key = set.join(spliter)
// 如果没有就先初始化一个空数组
if (!pathMap[key]) {
pathMap[key] = []
}
pathMap[key].push(sku.id)
})
}
})
}
return pathMap
}

// 初始化禁用状态
function initDisabledStatus(specs, pathMap) {
if (specs && specs.length > 0) {
specs.forEach((spec) => {
spec.values.forEach((val) => {
// 设置禁用状态
val.disabled = !pathMap[val.name]
})
})
}
}

// 得到当前选中规格集合
const getSelectedArr = (specs) => {
const selectedArr = []
specs.forEach((spec, index) => {
const selectedVal = spec.values.find((val) => val.selected)
if (selectedVal) {
selectedArr[index] = selectedVal.name
} else {
selectedArr[index] = undefined
}
})
return selectedArr
}

// 更新按钮的禁用状态
const updateDisabledStatus = (specs, pathMap) => {
// 遍历每一种规格
specs.forEach((item, i) => {
// 拿到当前选择的项目
const selectedArr = getSelectedArr(specs)
// 遍历每一个按钮
item.values.forEach((val) => {
if (!val.selected) {
selectedArr[i] = val.name
// 去掉undefined之后组合成key
const key = selectedArr.filter((value) => value).join(spliter)
val.disabled = !pathMap[key]
}
})
})
}

export default {
name: 'XtxGoodSku',
props: {
// specs:所有的规格信息 skus:所有的sku组合
goods: {
type: Object,
default: () => ({ specs: [], skus: [] }),
},
},
emits: ['change'],
setup(props, { emit }) {
let pathMap = {}
watchEffect(() => {
// 得到所有字典集合
pathMap = getPathMap(props.goods.skus)
// 组件初始化的时候更新禁用状态
initDisabledStatus(props.goods.specs, pathMap)
})

const clickSpecs = (item, val) => {
if (val.disabled) return false
// 选中与取消选中逻辑
if (val.selected) {
val.selected = false
} else {
item.values.forEach((bv) => {
bv.selected = false
})
val.selected = true
}
// 点击之后再次更新选中状态
updateDisabledStatus(props.goods.specs, pathMap)
// 把选择的sku信息传出去给父组件
// 触发change事件将sku数据传递出去
const selectedArr = getSelectedArr(props.goods.specs).filter(
(value) => value
)
// 如果选中得规格数量和传入得规格总数相等则传出完整信息(都选择了)
// 否则传出空对象
if (selectedArr.length === props.goods.specs.length) {
// 从路径字典中得到skuId
const skuId = pathMap[selectedArr.join(spliter)][0]
const sku = props.goods.skus.find((sku) => sku.id === skuId)
// 传递数据给父组件
emit('change', {
skuId: sku.id,
price: sku.price,
oldPrice: sku.oldPrice,
inventory: sku.inventory,
specsText: sku.specs
.reduce((p, n) => `${p} ${n.name}:${n.valueName}`, '')
.trim(),
})
} else {
emit('change', {})
}
}
return { clickSpecs }
},
}
</script>

<style scoped lang="scss">
@mixin sku-state-mixin {
border: 1px solid #e4e4e4;
margin-right: 10px;
cursor: pointer;

&.selected {
border-color: $xtxColor;
}

&.disabled {
opacity: 0.6;
border-style: dashed;
cursor: not-allowed;
}
}

.goods-sku {
padding-left: 10px;
padding-top: 20px;

dl {
display: flex;
padding-bottom: 20px;
align-items: center;

dt {
width: 50px;
color: #999;
}

dd {
flex: 1;
color: #666;

> img {
width: 50px;
height: 50px;
margin-bottom: 4px;
@include sku-state-mixin;
}

> span {
display: inline-block;
height: 30px;
line-height: 28px;
padding: 0 20px;
margin-bottom: 4px;
@include sku-state-mixin;
}
}
}
}
</style>
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2023-2025 congtianfeng
  • 访问人数: | 浏览次数: