夜猫子的知识栈 夜猫子的知识栈
首页
  • 前端文章

    • JavaScript
  • 学习笔记

    • 《JavaScript教程》
    • 《Web Api》
    • 《ES6教程》
    • 《Vue》
    • 《React》
    • 《TypeScript》
    • 《Git》
    • 《Uniapp》
    • 小程序笔记
    • 《Electron》
    • JS设计模式总结
  • 《前端架构》

    • 《微前端》
    • 《权限控制》
    • monorepo
  • 全栈项目

    • 任务管理日历
    • 无代码平台
    • 图书管理系统
  • HTML
  • CSS
  • Nodejs
  • Midway
  • Nest
  • MySql
  • 其他
  • 技术文档
  • GitHub技巧
  • 博客搭建
  • Ajax
  • Vite
  • Vitest
  • Nuxt
  • UI库文章
  • Docker
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

夜猫子

前端练习生
首页
  • 前端文章

    • JavaScript
  • 学习笔记

    • 《JavaScript教程》
    • 《Web Api》
    • 《ES6教程》
    • 《Vue》
    • 《React》
    • 《TypeScript》
    • 《Git》
    • 《Uniapp》
    • 小程序笔记
    • 《Electron》
    • JS设计模式总结
  • 《前端架构》

    • 《微前端》
    • 《权限控制》
    • monorepo
  • 全栈项目

    • 任务管理日历
    • 无代码平台
    • 图书管理系统
  • HTML
  • CSS
  • Nodejs
  • Midway
  • Nest
  • MySql
  • 其他
  • 技术文档
  • GitHub技巧
  • 博客搭建
  • Ajax
  • Vite
  • Vitest
  • Nuxt
  • UI库文章
  • Docker
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • JavaScript文章

    • 33个非常实用的JavaScript一行代码
    • new命令原理
    • ES5面向对象
    • ES6面向对象
    • 多种数组去重性能对比
    • 获取ip地址
    • ES6扩展符运用
    • file、base64和blob互相转化
    • 图片转base64格式
    • 实现异步队列
    • websocket实现二维码扫描
    • js动画之requestAnimationFrame
    • JS随机打乱数组
    • 判断是否为移动端浏览器
    • 将一维数组按指定长度转为二维数组
    • 防抖与节流函数
    • 时间戳与倒计时
    • JS获取和修改url参数
    • 比typeof运算符更准确的类型判断
    • 前端下载excel文件
    • 商品sku算法
      • 商城sku控制
        • 数据结构
        • 矩阵图思路
        • 1.初始化顶点集和空邻接矩阵
        • 2.邻接矩阵赋值
        • 3.判断 attribute 是否可选
        • 源码
        • 实现页面
        • ts+hook化
    • 数组与对象的互相转化
    • Javascript原生事件监听及手动触发
    • JS编码与转码
    • 树结构与扁平化相互转换
    • 实现微信登录
    • 判断两个对象值是否相等
    • 常用表单验证
    • 在H5中实现OCR拍照识别身份证功能
    • 滚动条元素定位
    • 获取目标容器内的元素列表
    • 微信H5支付
    • 对象除空
    • 获取指定链接的参数
    • js中的函数注释
    • 大模型流式数据前端实现
    • 定高虚拟列表
    • 不定高虚拟列表
    • 虚拟表格
    • 移动端文件预览
    • 浏览器缓存机制
    • 前端从剪切板获取word图片
  • 前端架构

  • 学习笔记

  • 全栈项目

  • 前端
  • JavaScript文章
夜猫子
2022-09-15
目录

商品sku算法

# 商城sku控制

# 数据结构

export const properties = [
  {
    name: "容量",
    attributes: [
      { value: "1L", isActive: false, isDisabled: false },
      { value: "4L", isActive: false, isDisabled: false },
    ],
  },
  {
    name: "颜色",
    attributes: [
      { value: "红色", isActive: false, isDisabled: false },
      { value: "黑色", isActive: false, isDisabled: false },
    ],
  },
  {
    name: "优惠套餐",
    attributes: [
      { value: "套餐一", isActive: false, isDisabled: false },
      { value: "套餐二", isActive: false, isDisabled: false },
    ],
  },
];

export const skuList = [
  { id: "10", attributes: ["1L", "红色", "套餐一"] },
  { id: "20", attributes: ["1L", "黑色", "套餐二"] },
  { id: "30", attributes: ["4L", "红色", "套餐一"] },
  { id: "40", attributes: ["4L", "红色", "套餐二"] }]
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

# 矩阵图思路

# 1.初始化顶点集和空邻接矩阵

代码实现如下所示:

// 构造初始空邻接矩阵存储无向图
initEmptyAdjMatrix() {
  this.properties.forEach((prop) => {
    prop.attributes.forEach((attr) => {
      this.vertexList.push(attr.value);
    });
  });
  for (let i = 0; i < this.vertexList.length; i++) {
    this.matrix[i] = new Array(this.vertexList.length).fill(0);
  }
},
1
2
3
4
5
6
7
8
9
10
11

# 2.邻接矩阵赋值

setAdjMatrixValue 调用子函数 associateAttributes 时需要增加传参 skuId ,对于 properties 赋值的情况没有 skuId ,统一传一个能够确认与 skuList 中的每一个 skuId 不相同的值即可,这里传1:

// 根据 skuList 和 properties 设置邻接矩阵的值
setAdjMatrixValue() {
  this.skuList.forEach((sku) => {
    this.associateAttributes(sku.attributes, sku.id);
  });
  this.properties.forEach((prop) => {
    this.associateAttributes(prop.attributes, '1');
  });
},
1
2
3
4
5
6
7
8
9

在子函数 associateAttributes 中,赋值时需要修改逻辑,判断 this.matrix[index1][index2] 是否有值,若有值,则使用 add 方法在集合中增加当前传入的 skuId,否则赋值为新创建的 Set 对象,并在集合中增加当前传入的 skuId。

// 将 attributes 属性组中的属性在无向图中联系起来
associateAttributes(attributes, skuId) {
  attributes.forEach((attr1) => {
    attributes.forEach((attr2) => {
      // 因 properties 与 skuList 数据结构不一致,需作处理
      if (attr1 !== attr2 || attr1.value !== attr2.value) {
        if (attr1.value && attr2.value) {
          attr1 = attr1.value;
          attr2 = attr2.value;
        }
        const index1 = this.vertexList.indexOf(attr1);
        const index2 = this.vertexList.indexOf(attr2);
        if (index1 > -1 && index2 > -1) {
          if(this.matrix[index1][index2]) {
            this.matrix[index1][index2].add(skuId);
          }
          else {
            this.matrix[index1][index2] = new Set([skuId]);
          }
        }
      }
    });
  });
},
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

赋值后,邻接矩阵如下所示:

1L 4L 红色 黑色 套餐一 套餐二
1L 0 {1} {10} {20} {10} {20}
4L {1} 0 {30, 40} 0 {30} {40}
红色 {10} {30, 40} 0 {1} {10, 30} {40}
黑色 {20} 0 {1} 0 0 {20}
套餐一 {10} {30} {10, 30} 0 0 {1}
套餐二 {20} {40} {40} {20} {1} 0

# 3.判断 attribute 是否可选

我的思路是:此时 res 数组中存储的元素可能有两种类型,Number 类型的 0 或者 Object 类型的 Set 对象, Set 对象中存储的可能为 1 或者 skuList 中存在的 skuId, 1 和 skuId是 String 类型。我将结果区分为三种情况,注意三种情况有先后关系,后一种排除前一种存在的可能,即使用 if - else 的控制流:

  • 若 res 数组中存在值为 0 的元素,则返回 false ,表示需要置灰。

  • 排除上一种情况之后,此时 res 数组中存储的是 Set 对象, Set 对象存储的是 1 或者 skuId。若 res 数组中存在包含值为 1 的 Set 对象,则返回 true ,表示可选。

  • 排除以上两种情况之后,此时 res 数组中存储的是 Set 对象, Set 对象中存储的是 skuId。当且仅当每个 Set 对象中包含相同的一个 skuId 时,可选返回 true,否则不可选返回 false

  • 一、三情况比较容易理解,对于第二种情况举例说明:选择了 1L 和 红色 之后,4L 能不能选呢?此时对于 4L 这个 attribute, res 数组应为 [{'1'}, {'30', '40'}] ,数组中不包含 0 且包含 1,此时应该是可选的。

代码实现如下所示:

// 判断当前 attribute 是否可选,返回 true 表示可选,返回 false 表示不可选,选项置灰
canAttributeSelect(attribute) {
  if (!this.selected || !this.selected.length || attribute.isActive) {
    return true;
  }
  let res = [];
  this.selected.forEach((value) => {
    const index1 = this.vertexList.indexOf(value);
    const index2 = this.vertexList.indexOf(attribute.value);
    res.push(this.matrix[index1][index2]);
  });
  // console.log(attribute.value, '->', res);
  if(res.some((item)=> (item === 0))) {
    return false;
  }
  else if(res.some((item) => (item.has('1')))) {
    return true;
  }
  else {
    const first = res[0];
    const others = res.slice(1);
    return Array.from(first).some((skuId) => (others.every((item) => (item.has(skuId)))));
  }
},
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 源码

# 实现页面

<template>
  <div class="root">
    <p>商品多规格选择示例</p>
    <div
      v-for="(item, propertyIndex) in dataSource.properties"
      :key="propertyIndex"
      class="Specifications"
    >
      <div>{{ item.name }}</div>
      <div class="attrbute">
        <button
          v-for="(attribute, attributeIndex) in item.attributes"
          :key="attributeIndex"
          @click="handleClickAttribute(propertyIndex, attributeIndex)"
          :class="[
            'weight',
            attribute.isActive ? 'seletedSpecifications' : '',
            attribute.isDisabled ? 'disabledStyle' : 'unDisabledStyle',
          ]"
        >
          {{ attribute.value }}
        </button>
      </div>
    </div>
  </div>
</template>

<script setup>
import { reactive, ref, onMounted} from 'vue';    
const properties = [
  {
    name: "容量",
    attributes: [
      { value: "1L", isActive: false, isDisabled: false },
      { value: "4L", isActive: false, isDisabled: false },
    ],
  },
  {
    name: "颜色",
    attributes: [
      { value: "红色", isActive: false, isDisabled: false },
      { value: "黑色", isActive: false, isDisabled: false },
    ],
  },
  {
    name: "优惠套餐",
    attributes: [
      { value: "套餐一", isActive: false, isDisabled: false },
      { value: "套餐二", isActive: false, isDisabled: false },
    ],
  },
];

const skuList = [
  { id: "10", attributes: ["1L", "红色", "套餐一"] },
  { id: "20", attributes: ["1L", "黑色", "套餐二"] },
  { id: "30", attributes: ["4L", "红色", "套餐一"] },
  { id: "40", attributes: ["4L", "红色", "套餐二"] },
];

const dataSource = reactive({   
    properties: [], // property 列表
    skuList: [], // sku 列表
    matrix: [], // 邻接矩阵存储无向图
    vertexList: [], // 顶点数组
    selectedAttrs:[], // 当前已选的 attribute 列表
    selectedSkuInfo: [], // 当前已选中的sku选项信息
    skuId: '', // skuList组合中当前选中的的skuId,可以设置默认skuid(默认选中状态)
})

onMounted(()=>{
    dataSource.properties = properties;
    dataSource.skuList = skuList;    
    getInit()
})

const getInit=(()=>{
    initEmptyAdjMatrix();
    setAdjMatrixValue();  
    // 默认选中项
    dataSource.skuId&&selectedAttrsBySkuId(dataSource.skuId)
})

// 构造初始空邻接矩阵存储无向图
const initEmptyAdjMatrix = () => {
  // 获取顶点数组: ['2斤','4斤','豪华','精选']
  dataSource.properties.forEach((prop) => {
    prop.attributes.forEach((attr) => {
      dataSource.vertexList.push(attr.value);
    });
  });
  for (let i = 0; i < dataSource.vertexList.length; i++) {
    dataSource.matrix[i] = new Array(dataSource.vertexList.length).fill(0);
  }
};

// 根据 skuList 和 properties 设置邻接矩阵的值
const setAdjMatrixValue = () => {
  dataSource.skuList.forEach((sku) => {
    associateAttributes(sku.attributes, sku.id);
  });
  dataSource.properties.forEach((prop) => {
    associateAttributes(prop.attributes, '1');
  });
};

// 将 attributes 属性组中的属性在无向图中联系起来
const associateAttributes = (attributes, skuId) => {
  attributes.forEach((attr1) => {
    attributes.forEach((attr2) => {
      // 因 properties 与 skuList 数据结构不一致,需作处理
      if (attr1 !== attr2 || attr1.value !== attr2.value) {
        if (attr1.value && attr2.value) {
          attr1 = attr1.value;
          attr2 = attr2.value;
        }
        const value1 = attr1.value ? attr1.value : attr1;
        const value2 = attr2.value ? attr2.value : attr2;
        const index1 = dataSource.vertexList.indexOf(value1);
        const index2 = dataSource.vertexList.indexOf(value2);
        if (index1 > -1 && index2 > -1) {
          if (dataSource.matrix[index1][index2]) {
            dataSource.matrix[index1][index2].add(skuId);
          } else {
            dataSource.matrix[index1][index2] = new Set([skuId]);
          }
        }
      }
    });
  });
};

// 根据skuid选中标签
const selectedAttrsBySkuId = (skuid) => {
  dataSource.selectedSkuInfo = dataSource.skuList.find(
    (item) => item.id === skuid,
  );
  dataSource.selectedSkuInfo.attributes.forEach((item) => {
    dataSource.properties.forEach((property, propertyIndex) => {
      property.attributes.forEach((attr, attributeIndex) => {
        if (attr.value === item) {
          handleClickAttribute(propertyIndex, attributeIndex);
        }
      });
    });
  });
};    

// 判断当前 attribute 是否可选,返回 true 表示可选,返回 false 表示不可选,选项置灰
const canAttributeSelect = (attribute) => {
  if (
    !dataSource.selectedAttrs ||
    !dataSource.selectedAttrs.length ||
    attribute.isActive
  ) {
    return true;
  }

  let res = [];
  dataSource.selectedAttrs.forEach((value) => {
    const index1 = dataSource.vertexList.indexOf(value);
    const index2 = dataSource.vertexList.indexOf(attribute.value);
    res.push(dataSource.matrix[index1][index2]);
  });

  if (res.some((item) => item === 0)) {
    return false;
  } else if (res.some((item) => item.has('1'))) {
    return true;
  } else {
    const first = res[0];
    const others = res.slice(1);
    return Array.from(first).some((skuId) =>
      others.every((item) => item.has(skuId)),
    );
  }
};

// 当点击某个 attribute 时,如:黑色
const handleClickAttribute = (propertyIndex, attributeIndex) => {
  setAttributestate(propertyIndex, attributeIndex);
  getSelectSku();
};
    
// 重置 attribute 状态
const setAttributestate = (propertyIndex, attributeIndex) => {
  const attr = dataSource.properties[propertyIndex].attributes[attributeIndex];
  // 若选项置灰,直接返回,表现为点击无响应
  if (attr.isDisabled) {
    return;
  }

  // 重置每个 attribute 的 isActive 状态
  const isActive = !attr.isActive;

  // 当取消选中时,清空skuID
  if (isActive === false) {
    dataSource.selectedSkuInfo = {};
  }

  dataSource.properties[propertyIndex].attributes[attributeIndex].isActive =
    isActive;
  if (isActive) {
    dataSource.properties[propertyIndex].attributes.forEach((attr, index) => {
      if (index !== attributeIndex) {
        attr.isActive = false;
      }
    });
  }
  // 维护当前已选的 attribute 列表
  dataSource.selectedAttrs = [];
  dataSource.properties.forEach((prop) => {
    prop.attributes.forEach((attr) => {
      if (attr.isActive) {
        dataSource.selectedAttrs.push(attr.value);
      }
    });
  });
  // 重置每个 attribute 的 isDisabled 状态
  dataSource.properties.forEach((prop) => {
    prop.attributes.forEach((attr) => {
      attr.isDisabled = !canAttributeSelect(attr);
    });
  });
};

// 判断是否选中sku并获取
const getSelectSku = () => {
  // 判断是否含已有sku组合并获取
  dataSource.skuList.forEach((item) => {
    if (objectEqual(item.attributes, dataSource.selectedAttrs)) {
      dataSource.selectedSkuInfo = item;
    }
  });
  // 获取skuID
  if (dataSource.selectedSkuInfo?.id) {
    dataSource.skuId = dataSource.selectedSkuInfo.id;
  } else {
    dataSource.skuId = null;
  }
};    
</script>

<style  lang="less">

.Specifications {
  display: flex;
  flex-direction: row;
  align-items: center;
  border-bottom: 2px dashed red;
}

.attrbute {
  display: flex;
  flex-wrap: wrap;
  width: 100%;
}

.weight {
  width: 25%;
  height: 30px;
  line-height: 30px;
  text-align: center;
  margin: 10px 0;
  background: #ffffff;
  border: 1px solid #a6a6a6;
  font-size: 14px;
  color: rgba(0, 0, 0, 0.85);
  margin-right: 4px;
}

.disabledStyle {
  background-color: #f7f7f7;
}
.unDisabledStyle:hover {
  cursor: pointer;
  color: #fb6e23;
  border-color: #fb6e23;
}

.seletedSpecifications {
  border: 2px solid #fb6e23;
  position: relative;
  right: 0;
  bottom: 0;
}
.seletedSpecifications:before {
  content: '';
  position: absolute;
  right: -2px;
  bottom: -2px;
  border: 10px solid #fb6e23;
  border-top-color: transparent;
  border-left-color: transparent;
}

.seletedSpecifications:after {
  content: '';
  width: 4px;
  height: 8px;
  position: absolute;
  right: 2px;
  bottom: 2px;
  border: 1px solid #fff;
  border-top-color: transparent;
  border-left-color: transparent;
  transform: rotate(45deg);
}
</style>
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
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309

# ts+hook化

# 使用
<template>
  <div
    v-for="(item, propertyIndex) in dataSource.properties"
    :key="propertyIndex"
  >
    <div>{{item.name}}</div>
    <div class="attrbute">
      <div
        v-for="(attribute, attributeIndex) in item.attributes"
        :key="attributeIndex"
        @click="handleClickAttribute(propertyIndex, attributeIndex)"
        :class="[
          'weight',
          attribute.isActive ? 'seletedSpecifications' : '',
          attribute.isDisabled ? 'disabledStyle' : '',
        ]"
      >
        <div>{{ attribute.value }}</div>
      </div>
    </div>
  </div>
</template>

<script lang="ts" setup>
import { myUseSkuState } from "./skuHook";
import { skuInfoPropsType } from "@/type/goodsDetail/index";
import {properties,skuList} from './data'
    
interface propsType{
  properties: skuInfoPropsType.properties;
  skuList: skuInfoPropsType.skuList;
  defaultSkuId?: skuInfoPropsType.defaultSkuId;
}
const props:propsType = {
    properties,
    skuList
};

const [dataSource, handleClickAttribute] = myUseSkuState(props);
</script>

<style lang="scss" scoped>
.attrbute {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  flex: 1;
}

.weight {
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: center;
  padding: 5rpx 20rpx;
  text-align: center;
  margin: 10rpx 0;
  background: #ffffff;
  border-radius: 10rpx;
  border: 2rpx solid #a6a6a6;
  font-size: 14px;
  color: rgba(0, 0, 0, 0.85);
  margin-right: 20rpx;
}

.disabledStyle {
  background-color: #f7f7f7;
}

.seletedSpecifications {
  border: 2rpx solid #fb6e23;
}
</style>
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
# ts类型定义
// type/index.ts
export declare namespace skuInfoPropsType {
  export type propertiesAttribute = {
    value: string;
    isActive: boolean;
    isDisabled: boolean;
  };

  export type skuinfo = {
    id: string;
    attributes: string[];
    stock: number;
    price: number;
    originalPrice: number;
  };
  export type properties = {
    name: string;
    attributes: propertiesAttribute[];
  }[];

  export type skuList = skuinfo[];

  export type defaultSkuId = string;
}

export interface skuInfoSettingType {
  properties: skuInfoPropsType.properties;
  skuList: skuInfoPropsType.skuList;
  matrix: any[]; // 邻接矩阵存储无向图
  vertexList: any[]; // 顶点数组
  selectedAttrs: any[]; // 当前已选的 attribute 列表
  selectedSkuInfo: skuInfoPropsType.skuinfo|null; // 当前已选中的sku选项信息
  skuId: skuInfoPropsType.defaultSkuId; // skuList组合中当前选中的的skuId
}

export interface skuInfoPropsDefaultType{
  properties: skuInfoPropsType.properties;
  skuList: skuInfoPropsType.skuList;
  defaultSkuId?: skuInfoPropsType.defaultSkuId;
}

export type skuInfoEmit = Omit<
  skuInfoSettingType & {
    shopingNum: number;
  },
  "vertexList" | "matrix" | "properties" | "skuList"
>;
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
# hook源码
// skuHook.ts
import { reactive, ref, onMounted } from 'vue';
import {
  skuInfoPropsType,
  skuInfoSettingType,
  skuInfoPropsDefaultType,
} from '@/type/index';

function objectEqual(obj1: any, obj2: any): boolean {
  if (typeof obj1 !== "object" || typeof obj2 !== "object") {
    throw new TypeError("parameter must typeof object");
  }
  const obj1Constructor = obj1.constructor;
  const obj2Constructor = obj2.constructor;

  if (obj1Constructor !== obj2Constructor) {
    return false;
  }

  if (obj1 instanceof Array) {
    return ArrayEqual(obj1, obj2);
  } else {
    const entries1 = Object.entries(obj1);
    const entries2 = Object.entries(obj2);
    return ArrayEqual(entries1, entries2);
  }

  function ArrayEqual(arr1: any, arr2: any) {
    if (JSON.stringify(arr1.sort()) == JSON.stringify(arr2.sort())) {
      return true;
    } else {
      return false;
    }
  }
}

const dataSource: skuInfoSettingType = reactive({
  properties: [], // property 列表
  skuList: [], // sku 列表
  matrix: [], // 邻接矩阵存储无向图
  vertexList: [], // 顶点数组
  selectedAttrs: [], // 当前已选的 attribute 列表
  selectedSkuInfo: null, // 当前已选中的sku选项信息
  skuId: '', // skuList组合中当前选中的的skuId,可以设置默认skuid(默认选中状态)
});

const getInit = (props: skuInfoPropsDefaultType) => {
  dataSource.properties = props.properties;
  dataSource.skuList = props.skuList;
  dataSource.skuId = props.defaultSkuId || props.skuList[0].id;
  initEmptyAdjMatrix();
  setAdjMatrixValue();
  // 默认选中项
  dataSource.skuId && selectedAttrsBySkuId(dataSource.skuId);
};

// 构造初始空邻接矩阵存储无向图
const initEmptyAdjMatrix = () => {
  // 获取顶点数组: ['2斤','4斤','豪华','精选']
  dataSource.properties.forEach((prop) => {
    prop.attributes.forEach((attr) => {
      dataSource.vertexList.push(attr.value);
    });
  });
  for (let i = 0; i < dataSource.vertexList.length; i++) {
    dataSource.matrix[i] = new Array(dataSource.vertexList.length).fill(0);
  }
};

// 根据 skuList 和 properties 设置邻接矩阵的值
const setAdjMatrixValue = () => {
  dataSource.skuList.forEach((sku) => {
    associateAttributes(sku.attributes, sku.id);
  });
  dataSource.properties.forEach((prop) => {
    associateAttributes(prop.attributes, '1');
  });
};

// 将 attributes 属性组中的属性在无向图中联系起来
const associateAttributes = (
  attributes: string[] | Array<skuInfoPropsType.propertiesAttribute>,
  skuId: skuInfoPropsType.defaultSkuId,
) => {
  attributes.forEach((attr1) => {
    attributes.forEach((attr2) => {
      // 因 properties 与 skuList 数据结构不一致,需作处理
      const attr1Value = (attr1 as skuInfoPropsType.propertiesAttribute).value;
      const attr2Value = (attr2 as skuInfoPropsType.propertiesAttribute).value;
      if (attr1 !== attr2 || attr1Value !== attr2Value) {
        if (attr1Value && attr2Value) {
          attr1 = attr1Value;
          attr2 = attr2Value;
        }
        const index1 = dataSource.vertexList.indexOf(attr1);
        const index2 = dataSource.vertexList.indexOf(attr2);
        if (index1 > -1 && index2 > -1) {
          if (dataSource.matrix[index1][index2]) {
            dataSource.matrix[index1][index2].add(skuId);
          } else {
            dataSource.matrix[index1][index2] = new Set([skuId]);
          }
        }
      }
    });
  });
};

// 根据skuid选中标签
const selectedAttrsBySkuId = (skuid: skuInfoPropsType.defaultSkuId) => {
  dataSource.selectedSkuInfo =
    dataSource.skuList.find((item) => item.id === skuid) || null;
  dataSource.selectedSkuInfo?.attributes.forEach((item: string) => {
    dataSource.properties.forEach((property, propertyIndex) => {
      property.attributes.forEach((attr, attributeIndex) => {
        if (attr.value === item) {
          handleClickAttribute(propertyIndex, attributeIndex);
        }
      });
    });
  });
};

// 判断当前 attribute 是否可选,返回 true 表示可选,返回 false 表示不可选,选项置灰
const canAttributeSelect = (
  attribute: skuInfoPropsType.propertiesAttribute,
) => {
  if (
    !dataSource.selectedAttrs ||
    !dataSource.selectedAttrs.length ||
    attribute.isActive
  ) {
    return true;
  }

  let res: any[] = [];
  dataSource.selectedAttrs.forEach((value: any) => {
    const index1 = dataSource.vertexList.indexOf(value);
    const index2 = dataSource.vertexList.indexOf(attribute.value);
    res.push(dataSource.matrix[index1][index2]);
  });

  if (res.some((item) => item === 0)) {
    return false;
  } else if (res.some((item) => item.has('1'))) {
    return true;
  } else {
    const first = res[0];
    const others = res.slice(1);
    return Array.from(first).some((skuId) =>
      others.every((item) => item.has(skuId)),
    );
  }
};

// 当点击某个 attribute 时,如:黑色
const handleClickAttribute = (
  propertyIndex: number,
  attributeIndex: number,
) => {
  setAttributestate(propertyIndex, attributeIndex);
  getSelectSku();
};

// 重置 attribute 状态
const setAttributestate = (propertyIndex: number, attributeIndex: number) => {
  const attr = dataSource.properties[propertyIndex].attributes[attributeIndex];
  // 若选项置灰,直接返回,表现为点击无响应
  if (attr.isDisabled) {
    return;
  }

  // 重置每个 attribute 的 isActive 状态
  const isActive = !attr.isActive;

  // 当取消选中时,清空skuID
  if (isActive === false) {
    dataSource.selectedSkuInfo = null;
  }
  dataSource.properties[propertyIndex].attributes[attributeIndex].isActive =
    isActive;
  if (isActive) {
    dataSource.properties[propertyIndex].attributes.forEach((attr, index) => {
      if (index !== attributeIndex) {
        attr.isActive = false;
      }
    });
  }
  // 维护当前已选的 attribute 列表
  dataSource.selectedAttrs = [];
  dataSource.properties.forEach((prop) => {
    prop.attributes.forEach((attr) => {
      if (attr.isActive) {
        dataSource.selectedAttrs.push(attr.value);
      }
    });
  });
  // 重置每个 attribute 的 isDisabled 状态
  dataSource.properties.forEach((prop) => {
    prop.attributes.forEach((attr) => {
      attr.isDisabled = !canAttributeSelect(attr);
    });
  });
};

// 判断是否选中sku并获取
const getSelectSku = () => {
  // 判断是否含已有sku组合并获取
  dataSource.skuList.forEach((item) => {
    if (objectEqual(item.attributes, dataSource.selectedAttrs)) {
      dataSource.selectedSkuInfo = item;
    }
  });
  // 获取skuID
  if (dataSource.selectedSkuInfo?.id) {
    dataSource.skuId = dataSource.selectedSkuInfo.id;
  } else {
    dataSource.skuId = '';
  }
};

// 获取未选中标签
const getUnchooseLabel = () => {
  const unChooseLabel: string[] = [];
  dataSource.properties.forEach((prop) => {
    const hasLabel = prop.attributes.some((attr) => {
      return attr.isActive === true;
    });
    if (!hasLabel) {
      unChooseLabel.push(prop.name);
    }
  });
  return unChooseLabel;
};

const myUseSkuState = (
  initialValue: skuInfoPropsDefaultType,
): [
  skuInfoSettingType,
  (propertyIndex: number, attributeIndex: number) => void,
] => {
  if (dataSource.properties.length === 0) {
    console.log(initialValue);
    getInit(initialValue);
  }
  return [dataSource, handleClickAttribute];
};

export {
  getInit,
  dataSource,
  handleClickAttribute,
  getUnchooseLabel,
  myUseSkuState,
};

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
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
# 数据源
// data.ts
export const properties = [
  {
    name: "Size",
    attributes: [
      { value: "S", isActive: false, isDisabled: false },
      { value: "M", isActive: false, isDisabled: false },
      { value: "L", isActive: false, isDisabled: false },
      { value: "Y", isActive: false, isDisabled: false },
      { value: "B", isActive: false, isDisabled: false },
    ],
  },
  {
    name: "Color",
    attributes: [
      { value: "red", isActive: false, isDisabled: false },
      { value: "green", isActive: false, isDisabled: false },
    ],
  },
  {
    name: "Figure ",
    attributes: [
      { value: "stripe", isActive: false, isDisabled: false },
      { value: "wave", isActive: false, isDisabled: false },
    ],
  },
];

export const skuList = [
  { id: "10", attributes: ["S", "red", "stripe"], stock: 12, price: 100, originalPrice: 150 },
  { id: "20", attributes: ["S", "green", "wave"], stock: 30, price: 100, originalPrice: 110 },
  { id: "30", attributes: ["M", "red", "stripe"], stock: 20, price: 100, originalPrice: 130 },
  { id: "40", attributes: ["L", "red", "wave"], stock: 15, price: 100, originalPrice: 120 },
];
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
编辑 (opens new window)
上次更新: 2023/7/11 18:57:41
前端下载excel文件
数组与对象的互相转化

← 前端下载excel文件 数组与对象的互相转化→

最近更新
01
IoC 解决了什么痛点问题?
03-10
02
如何调试 Nest 项目
03-10
03
Provider注入对象
03-10
更多文章>
Copyright © 2019-2025 Study | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式