ソースを参照

feat: sku 商品规格组件 #64 (#827)

* feat: sku

* feat: 可配置颜色提出

* feat: prop 调整
yangxiaolu1993 4 年 前
コミット
34a2d20808

+ 11 - 0
src/config.json

@@ -946,6 +946,17 @@
           "show": false,
           "exportEmpty": true,
           "author": "szg2008"
+        },
+        {
+          "version": "3.0.0",
+          "taro": true,
+          "name": "Sku",
+          "type": "component",
+          "cName": "商品规格选择",
+          "desc": "商品规格选择",
+          "sort": 1,
+          "show": true,
+          "author": "yangxiaolu3"
         }
       ]
     }

+ 42 - 0
src/packages/__VUE/sku/components/SkuHeader.vue

@@ -0,0 +1,42 @@
+<template>
+  <view class="nut-sku-header">
+    <img :src="goods.imagePath" />
+    <view class="nut-sku-header-right">
+      <template v-if="getSlots('sku-header-price')">
+        <slot name="sku-header-price"></slot>
+      </template>
+      <nut-price v-else :price="goods.price" :needSymbol="true" :thousands="false"> </nut-price>
+
+      <template v-if="getSlots('sku-header-extra')">
+        <slot name="sku-header-extra"></slot>
+      </template>
+      <view class="nut-sku-header-right-extra" v-if="goods.skuId && !getSlots('sku-header-extra')"
+        >商品编号:{{ goods.skuId }}</view
+      >
+    </view>
+  </view>
+</template>
+<script lang="ts">
+import { ref, watch, onMounted } from 'vue';
+
+import { createComponent } from '../../../utils/create';
+const { componentName, create } = createComponent('sku-header');
+
+export default create({
+  props: {
+    goods: {
+      type: Object,
+      default: {}
+    }
+  },
+  emits: [],
+
+  setup(props: any, { emit, slots }) {
+    const getSlots = (name: string) => slots[name];
+
+    return {
+      getSlots
+    };
+  }
+});
+</script>

+ 82 - 0
src/packages/__VUE/sku/components/SkuOperate.vue

@@ -0,0 +1,82 @@
+<template>
+  <view class="nut-sku-operate" v-if="btnOptions.length > 0">
+    <view class="nut-sku-operate-desc" v-if="btnExtraText" v-html="btnExtraText"></view>
+
+    <slot name="operate-btn"></slot>
+
+    <view class="nut-sku-operate-btn" v-if="!getSlots('operate-btn')">
+      <view
+        :class="[`nut-sku-operate-btn-${btn}`, 'nut-sku-operate-btn-item']"
+        v-for="(btn, i) in btnOptions"
+        :key="i"
+        @click="clickBtnOperate(btn)"
+        >{{ getBtnDesc(btn) }}</view
+      >
+    </view>
+  </view>
+</template>
+<script lang="ts">
+import { ref, onMounted } from 'vue';
+import { createComponent } from '../../../utils/create';
+const { componentName, create } = createComponent('sku-operate');
+
+export default create({
+  props: {
+    // 底部按钮配置  confirm cart  buy
+    btnOptions: {
+      type: Array,
+      default: () => ['confirm']
+    },
+
+    btnExtraText: {
+      type: String,
+      default: ''
+    },
+
+    // 立即购买文案
+    buyText: {
+      type: String,
+      default: '立即购买'
+    },
+    // 加入购物车文案
+    addCartText: {
+      type: String,
+      default: '加入购物车'
+    },
+
+    confirmText: {
+      type: String,
+      default: '确定'
+    }
+  },
+  emits: ['click', 'changeSku', 'changeBuyCount', 'clickBtnOperate'],
+
+  setup(props: any, { emit, slots }) {
+    const getBtnDesc = (type: string) => {
+      let mapD: { [props: string]: string } = {
+        confirm: props.confirmText,
+        cart: props.addCartText,
+        buy: props.buyText
+      };
+
+      return mapD[type];
+    };
+
+    onMounted(() => {
+      console.log(slots);
+    });
+
+    const getSlots = (name: string) => slots[name];
+
+    const clickBtnOperate = (btn: string) => {
+      emit('clickBtnOperate', btn);
+    };
+
+    return {
+      getBtnDesc,
+      clickBtnOperate,
+      getSlots
+    };
+  }
+});
+</script>

+ 71 - 0
src/packages/__VUE/sku/components/SkuSelect.vue

@@ -0,0 +1,71 @@
+<template>
+  <view class="nut-sku-select">
+    <view class="nut-sku-select-item" :key="item.id" v-for="(item, index) in skuInfo">
+      <view class="nut-sku-select-item-title">{{ item.name }}</view>
+      <view class="nut-sku-select-item-skus">
+        <view
+          class="nut-sku-select-item-skus-sku"
+          @click="changeSaleChild(itemAttr, itemAttrIndex, item, index)"
+          :class="[{ active: !itemAttr.disable && itemAttr.active }, { disable: itemAttr.disable }]"
+          :key="itemAttr.name"
+          v-for="(itemAttr, itemAttrIndex) in item.list"
+        >
+          {{ itemAttr.name }}
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+<script lang="ts">
+import { ref, watch, onMounted } from 'vue';
+import { createComponent } from '../../../utils/create';
+const { componentName, create } = createComponent('sku-select');
+
+export default create({
+  props: {
+    sku: {
+      type: Array,
+      default: () => []
+    }
+  },
+  emits: ['selectSku'],
+
+  setup(props: any, { emit }) {
+    const skuInfo = ref([]);
+
+    watch(
+      () => props.sku,
+      (value) => {
+        // console.log('发生变化');
+        skuInfo.value = [].slice.call(value);
+      },
+      { deep: true }
+    );
+
+    onMounted(() => {
+      if (props.sku.length > 0) {
+        skuInfo.value = [].slice.call(props.sku);
+      }
+    });
+
+    // 切换商品 Sku
+    const changeSaleChild = (attrItem: any, index: any, parentItem: any, parentIndex: any) => {
+      if (attrItem.checkFlag || attrItem.disable) {
+        return;
+      }
+
+      emit('selectSku', {
+        sku: attrItem,
+        skuIndex: index,
+        parentSku: parentItem,
+        parentIndex: parentIndex
+      });
+    };
+
+    return {
+      skuInfo,
+      changeSaleChild
+    };
+  }
+});
+</script>

+ 101 - 0
src/packages/__VUE/sku/components/SkuStepper.vue

@@ -0,0 +1,101 @@
+<template>
+  <view class="nut-sku-stepper">
+    <view class="nut-sku-stepper-title">{{ stepperTitle }}</view>
+    <view class="nut-sku-stepper-limit" v-html="getExtraText()"></view>
+    <view class="nut-sku-stepper-count">
+      <nut-inputnumber
+        v-model="goodsCount"
+        :min="stepperMin"
+        :max="stepperMax"
+        @add="add"
+        @reduce="reduce"
+        @overlimit="overlimit"
+        @change="changeStepper"
+      />
+    </view>
+  </view>
+</template>
+<script lang="ts">
+import { ref, onMounted } from 'vue';
+import { TypeOfFun } from '../../../utils/util';
+import { createComponent } from '../../../utils/create';
+const { componentName, create } = createComponent('sku-stepper');
+
+export default create({
+  props: {
+    // 购买数量最大值
+    stepperMax: {
+      type: [Number, String],
+      default: 99999
+    },
+
+    stepperMin: {
+      type: [Number, String],
+      default: 1
+    },
+
+    // stepper 前文案提示
+    stepperExtraText: {
+      type: [Function, Boolean],
+      default: false
+    },
+
+    // 数量选择左侧文案
+    stepperTitle: {
+      type: String,
+      default: '购买数量'
+    }
+  },
+  emits: ['click', 'changeSku', 'changeStepper', 'clickBtnOptions', 'overLimit', 'reduce', 'add'],
+
+  setup(props: any, { emit }) {
+    const goodsCount = ref(props.stepperMin);
+
+    onMounted(() => {
+      goodsCount.value = props.stepperMin;
+    });
+
+    const getExtraText = () => {
+      const { stepperExtraText } = props;
+
+      if (stepperExtraText && TypeOfFun(stepperExtraText) == 'function') {
+        return stepperExtraText();
+      } else {
+        return '';
+      }
+    };
+
+    // 修改购买数量 add 加  reduce 减
+    const add = (value: number) => {
+      emit('add', value);
+    };
+
+    const reduce = (value: number) => {
+      emit('reduce', value);
+    };
+
+    // stepper 极限值
+    const overlimit = (e: Event, action: string) => {
+      emit('overLimit', {
+        action,
+        value: parseInt(goodsCount.value + '')
+      });
+    };
+    // stepper 发生了改变
+    const changeStepper = (value: number) => {
+      goodsCount.value = value;
+
+      emit('changeStepper', value);
+    };
+
+    return {
+      goodsCount,
+      add,
+      reduce,
+      overlimit,
+      getExtraText,
+      changeStepper
+    };
+  }
+});
+</script>

+ 102 - 0
src/packages/__VUE/sku/data.js

@@ -0,0 +1,102 @@
+export const Sku = [
+  {
+    id: 1,
+    name: '颜色',
+    list: [
+      {
+        name: '亮黑色',
+        id: 100016015112,
+        active: true,
+        disable: false
+      },
+      {
+        name: '釉白色',
+        id: 100016015142,
+        active: false,
+        disable: false
+      },
+      {
+        name: '秘银色',
+        id: 100016015078,
+        active: false,
+        disable: false
+      },
+      {
+        name: '夏日胡杨',
+        id: 100009064831,
+        active: false,
+        disable: false
+      },
+      {
+        name: '秋日胡杨',
+        id: 100009064830,
+        active: false,
+        disable: false
+      }
+    ]
+  },
+  {
+    id: 2,
+    name: '版本',
+    list: [
+      {
+        name: '8GB+128GB',
+        id: 100016015102,
+        active: true,
+        disable: false
+      },
+      {
+        name: '8GB+256GB',
+        id: 100016015122,
+        active: false,
+        disable: false
+      }
+    ]
+  },
+  {
+    id: 3,
+    name: '版本',
+    list: [
+      {
+        name: '4G(有充版)',
+        id: 100016015103,
+        active: true,
+        disable: false
+      },
+      {
+        name: '5G(有充版)',
+        id: 100016015123,
+        active: false,
+        disable: false
+      },
+      {
+        name: '5G(无充版)',
+        id: 100016015104,
+        active: true,
+        disable: true
+      },
+      {
+        name: '5G(无充)质保换新版',
+        id: 100016015125,
+        active: false,
+        disable: false
+      }
+    ]
+  }
+];
+
+export const Goods = {
+  skuId: '100016015112',
+  price: '4599.00',
+  imagePath:
+    '//m.360buyimg.com/mobilecms/s750x750_jfs/t1/210630/17/8651/208682/618a5bd6Eddc8ea0e/b5e55e1a03bc0126.jpg!q80.dpg.webp'
+};
+
+export const imagePathMap = {
+  100016015112:
+    '//m.360buyimg.com/mobilecms/s750x750_jfs/t1/210630/17/8651/208682/618a5bd6Eddc8ea0e/b5e55e1a03bc0126.jpg!q80.dpg.web',
+  100016015142: '//img14.360buyimg.com/n4/jfs/t1/216079/14/3895/201095/618a5c0cEe0b9e2ba/cf5b98fb6128a09e.jpg',
+  100016015078: '//img14.360buyimg.com/n4/jfs/t1/215845/12/3788/221990/618a5c4dEc71cb4c7/7bd6eb8d17830991.jpg',
+  100009064831: '//img14.360buyimg.com/n4/jfs/t1/203247/8/14659/237368/618a5c87Ecc968774/b0bb25331e5e2d1a.jpg',
+  100009064830: '//img14.360buyimg.com/n4/jfs/t1/160950/40/25098/234168/618a5cb9E65ba975e/7f8f93ea7767a51b.jpg'
+};

+ 320 - 0
src/packages/__VUE/sku/demo.vue

@@ -0,0 +1,320 @@
+<template>
+  <div class="demo">
+    <h2>基本用法</h2>
+    <nut-cell :title="`基本用法`" desc="" @click="base = true"></nut-cell>
+
+    <h2>不可售</h2>
+    <nut-cell title="不可售" desc="" @click="notSell = true"></nut-cell>
+
+    <h2>自定义计步器</h2>
+    <nut-cell title="自定义计步器" desc="" @click="customStepper = true"></nut-cell>
+
+    <h2>自定义插槽</h2>
+    <nut-cell title="通过插槽自定义设置" desc="" @click="customBySlot = true"></nut-cell>
+
+    <nut-sku
+      v-model:visible="base"
+      :sku="skuData"
+      :goods="goodsInfo"
+      @selectSku="selectSku"
+      @clickBtnOperate="clickBtnOperate"
+      @close="close"
+    ></nut-sku>
+
+    <nut-sku
+      v-model:visible="notSell"
+      :sku="skuData"
+      :goods="goodsInfo"
+      :btnExtraText="btnExtraText"
+      @changeStepper="changeStepper"
+      :btnOptions="['buy', 'cart']"
+      @selectSku="selectSku"
+      @close="close"
+    >
+      <template #sku-operate>
+        <div class="sku-operate-box">
+          <nut-button class="sku-operate-box-dis" type="warning">查看相似商品</nut-button>
+          <nut-button class="sku-operate-box-dis" type="info">到货通知</nut-button>
+        </div>
+      </template>
+    </nut-sku>
+
+    <nut-sku
+      v-model:visible="customStepper"
+      :sku="skuData"
+      :goods="goodsInfo"
+      :stepperMax="7"
+      :stepperMin="2"
+      :stepperExtraText="stepperExtraText"
+      @changeStepper="changeStepper"
+      @overLimit="overLimit"
+      :btnOptions="['buy', 'cart']"
+      @selectSku="selectSku"
+      @clickBtnOperate="clickBtnOperate"
+      @close="close"
+    ></nut-sku>
+
+    <nut-sku
+      v-model:visible="customBySlot"
+      :sku="skuData"
+      :goods="goodsInfo"
+      :btnOptions="['buy', 'cart']"
+      @selectSku="selectSku"
+      @clickBtnOperate="clickBtnOperate"
+      @close="close()"
+    >
+      <template #sku-header-price>
+        <div>
+          <nut-price :price="goodsInfo.price" :needSymbol="true" :thousands="false"> </nut-price>
+          <span class="tag"></span>
+        </div>
+      </template>
+
+      <template #sku-header-extra>
+        <span class="nut-sku-header-right-extra">重量:0.1kg 编号:{{ goodsInfo.skuId }} </span>
+      </template>
+
+      <template #sku-operate>
+        <div class="sku-operate-box">
+          <nut-button class="sku-operate-item" shape="square" type="warning">加入购物车</nut-button>
+          <nut-button class="sku-operate-item" shape="square" type="primary">立即购买</nut-button>
+        </div>
+      </template>
+
+      <template #sku-select-top>
+        <div class="address">
+          <nut-cell
+            style="box-shadow: none; padding: 13px 0"
+            title="送至"
+            :desc="addressDesc"
+            @click="showAddressPopup = true"
+          ></nut-cell>
+        </div>
+      </template>
+    </nut-sku>
+
+    <nut-address
+      v-model:visible="showAddressPopup"
+      type="exist"
+      :exist-address="existAddress"
+      @close="close"
+      :is-show-custom-address="false"
+      @selected="selectedAddress"
+      exist-address-title="配送至"
+    ></nut-address>
+  </div>
+</template>
+
+<script lang="ts">
+import { reactive, ref, toRefs, onMounted } from 'vue';
+import { Sku, Goods, imagePathMap } from './data';
+
+import { createComponent } from '../../utils/create';
+import { Toast } from '@/packages/nutui.vue';
+const { createDemo } = createComponent('sku');
+
+interface Skus {
+  id: number;
+  name: string;
+  list: SkuItem[];
+  [key: string]: any;
+}
+
+interface SkuItem {
+  id: number;
+  name: string;
+  imagePath: string;
+  [key: string]: any;
+}
+
+interface GoodsProps {
+  skuId: string | number;
+  price: string; // 商品信息展示区,商品价格
+  imagePath?: string;
+  [key: string]: any;
+}
+
+interface Data {
+  skuData: Skus[];
+  goodsInfo: GoodsProps;
+}
+
+export default createDemo({
+  props: {},
+  setup() {
+    const popup = reactive({
+      base: false,
+      notSell: false,
+      customStepper: false,
+      customBySlot: false,
+
+      showAddressPopup: false
+    });
+
+    const data = reactive<Data>({
+      skuData: [],
+      goodsInfo: {}
+    });
+
+    const stepperExtraText = () => {
+      return `<div style="width:100%;text-align:right;color:#F00">2 件起售</div>`;
+    };
+
+    const btnExtraText = ref('抱歉,此商品在所选区域暂无存货');
+    const addressDesc = ref('(配送地会影响库存,请先确认)');
+    const existAddress = ref([
+      {
+        id: 1,
+        addressDetail: 'th ',
+        cityName: '石景山区',
+        countyName: '城区',
+        provinceName: '北京',
+        selectedAddress: true,
+        townName: ''
+      },
+      {
+        id: 2,
+        addressDetail: '12_ ',
+        cityName: '电饭锅',
+        countyName: '扶绥县',
+        provinceName: '北京',
+        selectedAddress: false,
+        townName: ''
+      },
+      {
+        id: 3,
+        addressDetail: '发大水比 ',
+        cityName: '放到',
+        countyName: '广宁街道',
+        provinceName: '钓鱼岛全区',
+        selectedAddress: false,
+        townName: ''
+      },
+      {
+        id: 4,
+        addressDetail: '还是想吧百度吧 ',
+        cityName: '研发',
+        countyName: '八里庄街道',
+        provinceName: '北京',
+        selectedAddress: false,
+        townName: ''
+      }
+    ]);
+
+    onMounted(() => {
+      getData();
+    });
+
+    const getData = () => {
+      setTimeout(() => {
+        data.skuData = Sku;
+        data.goodsInfo = Goods;
+      }, 500);
+    };
+    const selectSku = (s: any) => {
+      const { sku, parentIndex } = s;
+
+      if (sku.disable) return false;
+
+      data.skuData[parentIndex].list.forEach((s) => {
+        s.active = s.id == sku.id;
+      });
+
+      data.goodsInfo = {
+        skuId: sku.id,
+        price: '4599.00' // 商品信息展示区,商品价格
+      };
+
+      data.skuData[0].list.forEach((el) => {
+        if (el.active && !el.disable) {
+          data.goodsInfo.imagePath = imagePathMap[el.id];
+        }
+      });
+    };
+
+    // stepper 更改
+    const changeStepper = (count: number) => {
+      console.log('购买数量', count);
+    };
+
+    // stepper 极限值
+    const overLimit = (val: any) => {
+      if (val.action == 'reduce') {
+        Toast.text(`至少买${val.value}件哦`);
+      } else {
+        Toast.text(`最多买${val.value}件哦`);
+      }
+    };
+
+    const clickBtnOperate = (op: string) => {
+      console.log('点击了操作按钮', op);
+    };
+    // 关闭弹框
+    const close = () => {
+      console.log('选择弹框关闭');
+    };
+
+    const selectedAddress = (prevExistAdd: any, nowExistAdd: any) => {
+      const { provinceName, countyName, cityName } = nowExistAdd;
+      addressDesc.value = `${provinceName}${countyName}${cityName}`;
+    };
+
+    return {
+      selectSku,
+      changeStepper,
+      Goods,
+      clickBtnOperate,
+      close,
+      existAddress,
+      selectedAddress,
+      addressDesc,
+      stepperExtraText,
+      btnExtraText,
+      overLimit,
+      ...toRefs(popup),
+      ...toRefs(data)
+    };
+  }
+});
+</script>
+
+<style lang="scss" scoped>
+.tag {
+  display: inline-block;
+  width: 50px;
+  height: 15px;
+  font-size: 12px;
+  margin-left: 10px;
+  background: url('//storage.360buyimg.com/imgtools/bbdf6c9a2a-e3f6fbc0-fb4d-11eb-a27f-676da10c85f4.png') no-repeat
+    center center;
+  background-size: 100% 100%;
+}
+
+.sku-operate-box {
+  width: 100%;
+  display: flex;
+  padding: 8px 10px;
+  box-sizing: border-box;
+
+  .sku-operate-item {
+    width: 100%;
+    flex-shrink: 1;
+    &:first-child {
+      border-top-left-radius: 20px;
+      border-bottom-left-radius: 20px;
+    }
+    &:last-child {
+      border-top-right-radius: 20px;
+      border-bottom-right-radius: 20px;
+    }
+  }
+
+  .sku-operate-box-dis {
+    width: 100%;
+    flex-shrink: 1;
+    &:first-child {
+      margin-right: 18px;
+    }
+  }
+}
+</style>

+ 503 - 0
src/packages/__VUE/sku/doc.md

@@ -0,0 +1,503 @@
+# Sku 商品规格组件
+
+### 介绍
+
+按需加载请加载对应依赖组件:Popup、InputNumber、Price
+
+### 安装
+
+``` javascript
+import { createApp } from 'vue';
+//vue
+import { Sku, Popup, InputNumber, Price } from '@nutui/nutui';
+//taro
+import { Sku, Popup, InputNumber, Price } from '@nutui/nutui-taro';
+
+const app = createApp();
+app.use(Sku);
+app.use(Popup);
+app.use(InputNumber);
+app.use(Price);
+
+```
+
+## 代码演示
+
+### 基础用法
+
+```html
+<nut-sku
+  v-model:visible="base"
+  :sku="sku"
+  :goods="goods"
+  @selectSku="selectSku"
+  @clickBtnOperate="clickBtnOperate"
+  @close="close"
+></nut-sku>
+```
+
+```javascript
+setup() {
+    const base = ref(false);
+    const data = reactive({
+      sku: [
+          // 具体数据结构见下方文档
+        ],
+      goods: {
+          // 具体数据结构见下方文档
+        }
+    });
+
+    onMounted(() => {});
+    // 切换规格类目
+    const selectSku = (ss: string) => {
+      const { sku, skuIndex, parentSku, parentIndex } = ss;
+      if (sku.disable) return false;
+      data.sku[parentIndex].list.forEach((s) => {
+        s.active = s.id == sku.id;
+      });
+      data.goods = {
+        skuId: sku.id,
+        price: '4599.00',
+        imagePath:
+          '//img14.360buyimg.com/n4/jfs/t1/215845/12/3788/221990/618a5c4dEc71cb4c7/7bd6eb8d17830991.jpg' 
+      };
+    };
+    // 底部操作按钮触发
+    const clickBtnOperate = (op:string)=>{
+      console.log('点击了操作按钮',op)
+    } 
+    // 关闭商品规格弹框
+    const close = ()=>{}
+    return { base, selectSku, clickBtnOperate,close, ...toRefs(data) };
+}
+```
+
+### 不可售
+
+```html
+<nut-sku
+  v-model:visible="notSell"
+  :sku="skuData"
+  :goods="goodsInfo"
+  :btnExtraText="btnExtraText"
+  @changeStepper="changeStepper"
+  @selectSku="selectSku"
+  @close="close"
+>
+  <template #sku-operate>
+    <div class="sku-operate-box">
+      <nut-button class="sku-operate-box-dis" type="warning">查看相似商品</nut-button>
+      <nut-button class="sku-operate-box-dis" type="info">到货通知</nut-button>
+    </div>
+  </template>
+</nut-sku>
+```
+
+```javascript
+setup() {
+    const notSell = ref(false);
+    const data = reactive({
+      sku: [
+          // 数据结构见下方文档
+        ],
+      goods: {
+          // 数据结构见下方文档
+        }
+    });
+
+    const btnExtraText = ref('抱歉,此商品在所选区域暂无存货');
+    // inputNumber 更改
+    const changeStepper = (count: number) => {
+      console.log('购买数量', count);
+    };
+
+    // 切换规格类目
+    const selectSku = (ss: string) => {
+      const { sku, skuIndex, parentSku, parentIndex } = ss;
+      if (sku.disable) return false;
+      data.sku[parentIndex].list.forEach((s) => {
+        s.active = s.id == sku.id;
+      });
+      data.goods = {
+        skuId: sku.id,
+        price: '4599.00',
+        imagePath:
+          '//img14.360buyimg.com/n4/jfs/t1/216079/14/3895/201095/618a5c0cEe0b9e2ba/cf5b98fb6128a09e.jpg' 
+      };
+    };
+    // 底部操作按钮触发
+    const clickBtnOperate = (op:string)=>{
+      console.log('点击了操作按钮',op)
+    } 
+    return { notSell, changeStepper,selectSku,btnExtraText,...toRefs(data) };
+}
+```
+
+```css
+.sku-operate-box {
+  width: 100%;
+  display: flex;
+  padding: 8px 10px;
+  box-sizing: border-box;
+
+  .sku-operate-box-dis{
+    width: 100%;
+    flex-shrink: 1;
+    &:first-child{
+      margin-right: 18px;
+    }
+  }
+}
+```
+
+### 自定义步进器
+
+可以按照需求配置数字输入框的最大值、最小值、文案等
+
+```html
+<nut-sku
+  v-model:visible="customStepper"
+  :sku="sku"
+  :goods="goods"
+  :showSaleLimit="true"
+  :stepperMax="7"
+  :stepperMin="2"
+  :stepperExtraText="stepperExtraText"
+  @changeStepper="changeStepper"
+  @overLimit="overLimit"
+  :btnOptions="['buy', 'cart']"
+  @selectSku="selectSku"
+  @clickBtnOperate="clickBtnOperate"
+  @close="close"
+></nut-sku>
+```
+
+```javascript
+setup() {
+    const customStepper = ref(false);
+    const data = reactive({
+      sku: [
+          // 数据结构见下方文档
+        ],
+      goods: {
+          // 数据结构见下方文档
+        }
+    });
+
+    const stepperExtraText = () => {
+      return `<div style="width:100%;text-align:right;color:#F00">2 件起售</div>`
+    };
+    // inputNumber 更改
+    const changeStepper = (count: number) => {
+      console.log('购买数量', count);
+    };
+
+    // inputNumber 极限值
+    const overLimit = (val: any) => {
+      if (val.action == 'reduce') {
+        Toast.text(`至少买${val.value}件哦`);
+      } else {
+        Toast.text(`最多买${val.value}件哦`);
+      }
+    };
+    // 切换规格类目
+    const selectSku = (ss: string) => {
+      const { sku, skuIndex, parentSku, parentIndex } = ss;
+      if (sku.disable) return false;
+      data.sku[parentIndex].list.forEach((s) => {
+        s.active = s.id == sku.id;
+      });
+      data.goods = {
+        skuId: sku.id,
+        price: '4599.00',
+        imagePath:
+          '//img14.360buyimg.com/n4/jfs/t1/215845/12/3788/221990/618a5c4dEc71cb4c7/7bd6eb8d17830991.jpg' 
+      };
+    };
+    // 底部操作按钮触发
+    const clickBtnOperate = (op:string)=>{
+      console.log('点击了操作按钮',op)
+    } 
+    return { overLimit, changeStepper,selectSku, clickBtnOperate,stepperExtraText,...toRefs(data) };
+}
+```
+
+### 自定义插槽
+
+Sku 组件默认划分为若干区域,这些区域都定义成了插槽,可以按照需求进行替换。
+
+```html
+<nut-sku
+    v-model:visible="customBySlot"
+    :sku="sku"
+    :goods="goods"
+    :btnOptions="['buy', 'cart']"
+    @selectSku="selectSku"
+    @clickBtnOperate="clickBtnOperate"
+    @close="close()"
+>
+    <!-- 商品展示区,价格区域 -->
+    <template #sku-header-price>
+        <div>
+            <nut-price :price="goodsInfo.price" :needSymbol="true" :thousands="false"> </nut-price>
+            <span class="tag"></span>
+        </div>
+    </template> 
+    <!-- 商品展示区,编号区域 -->
+    <template #sku-header-extra>
+        <span class="nut-sku-header-right-extra">重量:0.1kg  编号:{{skuId}}  </span>
+    </template> 
+    <!-- sku 展示区上方与商品信息展示区下方区域,无默认展示内容 -->
+    <template #sku-select-top>
+        <div class="address">
+            <nut-cell style="box-shadow:none;padding:13px 0" title="送至" :desc="addressDesc" @click="showAddressPopup=true"></nut-cell>
+        </div>
+    </template>
+    <!-- 底部按钮操作区 -->
+    <template #sku-operate>
+        <div class="sku-operate-box">
+        <nut-button class="sku-operate-item" shape="square" type="warning">加入购物车</nut-button>
+        <nut-button class="sku-operate-item" shape="square" type="primary">立即购买</nut-button>
+        </div>
+    </template>
+</nut-sku>
+
+<nut-address
+  v-model:visible="showAddressPopup"
+  type="exist"
+  :exist-address="existAddress"
+  @close="close"
+  :is-show-custom-address="false"
+  @selected="selectedAddress"
+  exist-address-title="配送至"
+></nut-address>
+```
+
+```javascript
+setup() {
+    const customBySlot = ref(false);
+    const showAddressPopup = ref(false);
+    const data = reactive({
+      sku: [
+          // 数据结构见下方文档
+        ],
+      goods: {
+          // 数据结构见下方文档
+        }
+    });
+    const addressDesc = ref('(配送地会影响库存,请先确认)');
+    const existAddress = ref([
+      {
+        id: 1,
+        addressDetail: 'th ',
+        cityName: '石景山区',
+        countyName: '城区',
+        provinceName: '北京',
+        selectedAddress: true,
+        townName: ''
+      },
+      {
+        id: 2,
+        addressDetail: '12 ',
+        cityName: '电饭锅',
+        countyName: '扶绥县',
+        provinceName: '北京',
+        selectedAddress: false,
+        townName: ''
+      },
+      {
+        id: 3,
+        addressDetail: '发大水比 ',
+        cityName: '放到',
+        countyName: '广宁街道',
+        provinceName: '钓鱼岛全区',
+        selectedAddress: false,
+        townName: ''
+      },
+      {
+        id: 4,
+        addressDetail: '还是想吧百度吧 ',
+        cityName: '研发',
+        countyName: '八里庄街道',
+        provinceName: '北京',
+        selectedAddress: false,
+        townName: ''
+      }
+    ]);
+
+    // 切换规格类目
+    const selectSku = (ss: string) => {
+      const { sku, skuIndex, parentSku, parentIndex } = ss;
+      if (sku.disable) return false;
+      data.sku[parentIndex].list.forEach((s) => {
+        s.active = s.id == sku.id;
+      });
+      data.goods = {
+        skuId: sku.id,
+        price: '6002.10',
+        imagePath:
+          '//img14.360buyimg.com/n4/jfs/t1/215845/12/3788/221990/618a5c4dEc71cb4c7/7bd6eb8d17830991.jpg' 
+      };
+    };
+    const selectedAddress = (prevExistAdd: any, nowExistAdd: any) => {
+      const { provinceName, countyName, cityName } = nowExistAdd;
+      addressDesc.value = `${provinceName}${countyName}${cityName}`;
+    };
+    // 底部操作按钮触发
+    const clickBtnOperate = (op:string)=>{
+      console.log('点击了操作按钮',op)
+    } 
+    return { customBySlot, selectSku, clickBtnOperate,existAddress,addressDesc,selectedAddress,...toRefs(data) };
+}
+```
+
+
+## API
+
+### Props
+
+| 参数         | 说明                             | 类型   | 默认值           |
+|--------------|----------------------------------|--------|------------------|
+| v-model:visible         | 是否显示商品规格弹框               | boolean |  false              |
+| sku         | 商品 sku 数据 | Array | []               |
+| goods |  商品信息    | Object | - |
+| stepper-max         | 设置 inputNumber 最大值  | [String, Number] | 99999               |
+| stepper-min         | 设置 inputNumber 最小值  | [String, Number] | 1               |
+| btn-options        |           底部按钮设置。['confirm','buy','cart' ] 分别对应确定、立即购买、加入购物车              | Array | ['confirm']           |
+| btn-extra-text | 按钮上部添加文案,默认为空,有值时显示 | String | -            |
+| stepper-title         | 数量选择组件左侧文案 | String | '购买数量'                |
+| stepper-extra-text        |   inputNumber 与标题之间的文案       | [Function, false] | false              |
+| buy-text |  立即购买按钮文案    | String | 立即购买 |
+| add-cart-text          |        加入购物车按钮文案                 | String | 加入购物车             |
+| confirm-text          |           确定按钮文案              | String | 确定             |
+
+### Events
+
+| 事件名 | 说明           | 回调参数     |
+|--------|----------------|--------------|
+| select-sku  | 切换规格类目时触发 | {sku,skuIndex,parentSku,parentIndex} |
+| add  | inputNumber 点击增加按钮时触发 | value |
+| reduce  | inputNumber 点击减少按钮时触发 | value |
+| overLimit  | inputNumber 点击不可用的按钮时触发 | value |
+| change-stepper  | 购买变化时触发 | value |
+| click-btn-operate  | 点击底部按钮时触发 | {type:'confirm',value:'inputNumber value'} |
+| click-close-icon  | 点击左上角关闭 icon 时触发 | - |
+| click-overlay  | 点击遮罩时触发 | - |
+| close  | 关闭弹层时触发 | - |
+
+
+### Slots
+
+Sku 组件默认划分为若干区域,这些区域都定义成了插槽,可以按照需求进行替换。
+
+| 事件名 | 说明           | 
+|--------|----------------|
+| sku-header  | 商品信息展示区,包含商品图片、价格、编号 | 
+| sku-header-price  | 商品信息展示区,价格区域展示| 
+| sku-header-extra  | 商品信息展示区,编号区域展示 | 
+| sku-select-top | sku 展示区上方与商品信息展示区下方区域,无默认展示内容 | 
+| sku-select | sku 展示区 | 
+| sku-stepper  | 数量选择区 | 
+| sku-stepper-bottom  | 数量选择区下方区域 | 
+| sku-operate | 底部按钮操作区域 |
+
+### goods 对象结构
+
+```javascript
+goods:{
+    skuId:'', // 商品信息展示区,商品编号
+    price: "0", // 商品信息展示区,商品价格
+    imagePath: "", // 商品信息展示区,商品图
+}
+
+```
+
+### sku 数组结构
+
+sku 数组中,每一个数组索引代表一个规格类目。其中,list 代表该规格类目下的类目值。每个类目值对象包括:name、id、active(是否选中)、disable(是否可选)
+
+```javascript
+sku : [{
+    id: 1,
+    name: '颜色',
+    list: [{
+        name: '亮黑色',
+        id: 100016015112,
+        active: true,
+        disable: false
+      },
+      {
+        name: '釉白色',
+        id: 100016015142,
+        active: false,
+        disable: false
+      },
+      {
+        name: '秘银色',
+        id: 100016015078,
+        active: false,
+        disable: false
+      },
+      {
+        name: '夏日胡杨',
+        id: 100009064831,
+        active: false,
+        disable: false
+      },
+      {
+        name: '秋日胡杨',
+        id: 100009064830,
+        active: false,
+        disable: false
+      }
+    ]
+  },
+  {
+    id: 2,
+    name: '版本',
+    list: [{
+        name: '8GB+128GB',
+        id: 100016015102,
+        active: true,
+        disable: false
+      },
+      {
+        name: '8GB+256GB',
+        id: 100016015122,
+        active: false,
+        disable: false
+      }
+    ]
+  },
+  {
+    id: 3,
+    name: '版本',
+    list: [{
+        name: '4G(有充版)',
+        id: 100016015103,
+        active: true,
+        disable: false
+      },
+      {
+        name: '5G(有充版)',
+        id: 100016015123,
+        active: false,
+        disable: false
+      },
+      {
+        name: '5G(无充版)',
+        id: 100016015104,
+        active: true,
+        disable: true
+      },
+      {
+        name: '5G(无充)质保换新版',
+        id: 100016015125,
+        active: false,
+        disable: false
+      }
+    ]
+  }
+];
+```

+ 147 - 0
src/packages/__VUE/sku/index.scss

@@ -0,0 +1,147 @@
+.nut-sku {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  padding: 0px;
+  background: $white;
+  &-header {
+    height: 100px;
+    display: flex;
+    flex-shrink: 0;
+    margin-top: 18px;
+    padding: 0 18px;
+    > img {
+      width: 100px;
+      height: 100px;
+      flex-shrink: 0;
+      margin-right: 12px;
+    }
+    &-right {
+      flex: 1;
+      display: flex;
+      flex-direction: column;
+      justify-content: flex-end;
+      &-extra {
+        font-size: 12px;
+        color: $text-color;
+      }
+    }
+  }
+  &-content {
+    flex: 1;
+    overflow-y: auto;
+    overflow-x: hidden;
+    &::-webkit-scrollbar {
+      display: none;
+    }
+    margin-top: 24px;
+    padding: 0 18px;
+  }
+  &-select {
+    &-item {
+      display: flex;
+      flex-direction: column;
+      &-title {
+        height: 13px;
+        font-weight: bold;
+        font-size: 13px;
+        color: $black;
+        margin-bottom: 18px;
+      }
+      &-skus {
+        display: flex;
+        flex-wrap: wrap;
+        &-sku {
+          margin-right: 12px;
+          height: 30px;
+          line-height: 30px;
+          padding: 0 18px;
+          border-radius: 15px;
+          font-size: 11px;
+          color: $black;
+          flex-shrink: 0;
+          margin-bottom: 12px;
+          background: rgba(242, 242, 242, 1);
+          border: 1px solid rgba(242, 242, 242, 1);
+
+          &.active {
+            background: rgba(252, 237, 235, 1);
+            border: $sku-item-border;
+            color: $primary-color;
+          }
+          &.disable {
+            color: $text-color;
+            text-decoration: $sku-item-disable-line;
+          }
+        }
+      }
+    }
+  }
+  &-stepper {
+    display: flex;
+    justify-content: space-between;
+    margin: 10px 0 30px;
+    &-title {
+      font-weight: bold;
+      font-size: 13px;
+      color: $black;
+      margin-right: 12px;
+    }
+    &-limit {
+      flex: 1;
+      display: flex;
+      align-items: center;
+
+      font-size: 12px;
+      color: $text-color;
+    }
+    &-count {
+      display: flex;
+      align-items: center;
+      &-lowestBuy {
+        font-size: 12px;
+        color: $primary-color;
+      }
+    }
+  }
+  &-operate {
+    width: 100%;
+    &-desc {
+      display: block;
+      width: 100%;
+      padding: 10px 0;
+      text-align: center;
+      background: #fbf9da;
+      color: #de6a1c;
+      font-size: 12px;
+    }
+    &-btn {
+      height: 54px;
+      width: 100%;
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      background: $white;
+      text-align: center;
+      padding: 0 18px;
+      box-sizing: border-box;
+      &-item {
+        width: 100%;
+        height: 40px;
+        line-height: 40px;
+        margin-right: 18px;
+        background: $sku-opetate-bg-default;
+        border-radius: 21px;
+        font-size: 15px;
+        color: $white;
+        &:last-child {
+          margin-right: 0;
+        }
+      }
+
+      &-buy {
+        background: $sku-opetate-bg-buy;
+      }
+    }
+  }
+}

+ 278 - 0
src/packages/__VUE/sku/index.taro.vue

@@ -0,0 +1,278 @@
+<template>
+  <nut-popup
+    position="bottom"
+    closeable
+    round
+    v-model:visible="showPopup"
+    @click-close-icon="closePopup('icon')"
+    @click-overlay="closePopup('overlay')"
+    @close="closePopup('close')"
+    style="height: 75%"
+  >
+    <view class="nut-sku">
+      <slot name="sku-header"></slot>
+      <sku-header :goods="goods" v-if="!getSlots('sku-header')">
+        <template #sku-header-price v-if="getSlots('sku-header-price')">
+          <slot name="sku-header-price"></slot>
+        </template>
+
+        <template #sku-header-extra v-if="getSlots('sku-header-extra')">
+          <slot name="sku-header-extra"></slot>
+        </template>
+      </sku-header>
+
+      <view class="nut-sku-content">
+        <slot name="sku-select-top"></slot>
+
+        <slot name="sku-select"></slot>
+        <SkuSelect v-if="!getSlots('sku-select')" :sku="sku" @selectSku="selectSku"></SkuSelect>
+
+        <slot name="sku-stepper"></slot>
+        <sku-stepper
+          v-if="!getSlots('sku-stepper')"
+          :goods="goods"
+          :stepperTitle="stepperTitle"
+          :stepperMax="stepperMax"
+          :stepperMin="stepperMin"
+          :purchased="purchased"
+          :showSaleLimit="showSaleLimit"
+          :showSaleLowest="showSaleLowest"
+          :saleLowestText="saleLowestText"
+          :saleLimitText="saleLimitText"
+          :purchasedText="purchasedText"
+          @add="add"
+          @reduce="reduce"
+          @changeStepper="changeStepper"
+          @stepperOverLimit="stepperOverLimit"
+        ></sku-stepper>
+
+        <slot name="sku-stepper-bottom"></slot>
+      </view>
+
+      <slot name="sku-operate"></slot>
+      <sku-operate
+        v-if="!getSlots('sku-operate')"
+        :btnOptions="btnOptions"
+        :buyText="buyText"
+        :addCartText="addCartText"
+        :confirmText="confirmText"
+        @clickBtnOperate="clickBtnOperate"
+      ></sku-operate>
+    </view>
+  </nut-popup>
+</template>
+<script lang="ts">
+import { ref, watch, onMounted } from 'vue';
+import SkuHeader from './components/SkuHeader.vue';
+import SkuSelect from './components/SkuSelect.vue';
+import SkuStepper from './components/SkuStepper.vue';
+import SkuOperate from './components/SkuOperate.vue';
+import { createComponent } from '../../utils/create';
+const { componentName, create } = createComponent('sku');
+
+export default create({
+  props: {
+    visible: {
+      type: Boolean,
+      default: false
+    },
+
+    sku: {
+      type: Array,
+      default: []
+    },
+
+    goods: {
+      type: Object,
+      default: {}
+    },
+
+    // 是否显示限购文案
+    showSaleLimit: {
+      type: Boolean,
+      default: false
+    },
+
+    // stepper 最大值
+    stepperMax: {
+      type: [Number, String],
+      default: 99999
+    },
+
+    // stepper 最小值
+    stepperMin: {
+      type: [Number, String],
+      default: 1
+    },
+    // 已购数量
+    purchased: {
+      type: [Number, String],
+      default: 0
+    },
+
+    // 是否显示起购文案
+    showSaleLowest: {
+      type: Boolean,
+      default: false
+    },
+
+    // 底部按钮配置  confirm cart  buy
+    btnOptions: {
+      type: Array,
+      default: () => ['confirm']
+    },
+
+    // 数量选择左侧文案
+    stepperTitle: {
+      type: String,
+      default: '购买数量'
+    },
+
+    // 起购文案提示
+    saleLowestText: {
+      type: [Function, Boolean],
+      default: false
+    },
+
+    // 限购文案提示
+    saleLimitText: {
+      type: [Function, Boolean],
+      default: false
+    },
+
+    // 已购文案提示
+    purchasedText: {
+      type: [Function, Boolean],
+      default: false
+    },
+
+    // 立即购买文案
+    buyText: {
+      type: String,
+      default: '立即购买'
+    },
+
+    // 加入购物车文案
+    addCartText: {
+      type: String,
+      default: '加入购物车'
+    },
+
+    // 确定文案
+    confirmText: {
+      type: String,
+      default: '确定'
+    }
+  },
+  emits: [
+    'update:visible',
+    'selectSku',
+    'changeStepper',
+    'clickBtnOperate',
+    'clickCloseIcon',
+    'clickOverlay',
+    'close',
+    'reduce',
+    'add',
+    'overLimit'
+  ],
+
+  components: {
+    SkuHeader,
+    SkuSelect,
+    SkuStepper,
+    SkuOperate
+  },
+
+  setup(props: any, { emit, slots }) {
+    const showPopup = ref(props.visible);
+
+    const goodsCount = ref(props.stepperMin);
+
+    watch(
+      () => props.visible,
+      (value) => {
+        showPopup.value = value;
+      }
+    );
+
+    watch(
+      () => showPopup.value,
+      (value) => {
+        if (value == false) {
+          close();
+        }
+      }
+    );
+
+    const getSlots = (name: string) => slots[name];
+
+    // 商品规格 sku 选择
+    const selectSku = (skus: any) => {
+      emit('selectSku', skus);
+    };
+
+    // 数量计步器变化
+    const changeStepper = (value: number) => {
+      goodsCount.value = value;
+
+      emit('changeStepper', value);
+    };
+
+    // 修改购买数量 add 加  reduce 减
+    const add = (value: number) => {
+      emit('add', value);
+    };
+
+    const reduce = (value: number) => {
+      emit('reduce', value);
+    };
+
+    // 触发极限值
+    const stepperOverLimit = (count: any) => {
+      emit('overLimit', count);
+    };
+
+    // 点击 button 操作
+    const clickBtnOperate = (btn: string) => {
+      emit('clickBtnOperate', {
+        type: btn,
+        value: goodsCount.value
+      });
+    };
+
+    // 关闭
+    const closePopup = (type: string) => {
+      if (type == 'icon') {
+        emit('click-close-icon');
+      }
+
+      if (type == 'overlay') {
+        emit('click-overlay');
+      }
+
+      if (type == 'close') {
+        emit('close');
+      }
+
+      showPopup.value = false;
+    };
+
+    const close = () => {
+      emit('update:visible', false);
+    };
+
+    return {
+      showPopup,
+      closePopup,
+      selectSku,
+      changeStepper,
+      stepperOverLimit,
+      clickBtnOperate,
+      add,
+      reduce,
+      getSlots
+    };
+  }
+});
+</script>

+ 257 - 0
src/packages/__VUE/sku/index.vue

@@ -0,0 +1,257 @@
+<template>
+  <nut-popup
+    position="bottom"
+    closeable
+    round
+    v-model:visible="showPopup"
+    @click-close-icon="closePopup('icon')"
+    @click-overlay="closePopup('overlay')"
+    @close="closePopup('close')"
+    style="height: 75%"
+  >
+    <view class="nut-sku">
+      <slot name="sku-header"></slot>
+      <sku-header :goods="goods" v-if="!getSlots('sku-header')">
+        <template #sku-header-price v-if="getSlots('sku-header-price')">
+          <slot name="sku-header-price"></slot>
+        </template>
+
+        <template #sku-header-extra v-if="getSlots('sku-header-extra')">
+          <slot name="sku-header-extra"></slot>
+        </template>
+      </sku-header>
+
+      <view class="nut-sku-content">
+        <slot name="sku-select-top"></slot>
+
+        <slot name="sku-select"></slot>
+        <SkuSelect v-if="!getSlots('sku-select')" :sku="sku" @selectSku="selectSku"></SkuSelect>
+
+        <slot name="sku-stepper"></slot>
+        <sku-stepper
+          v-if="!getSlots('sku-stepper')"
+          :goods="goods"
+          :stepperTitle="stepperTitle"
+          :stepperMax="stepperMax"
+          :stepperMin="stepperMin"
+          :stepperExtraText="stepperExtraText"
+          @add="add"
+          @reduce="reduce"
+          @changeStepper="changeStepper"
+          @overLimit="stepperOverLimit"
+        ></sku-stepper>
+
+        <slot name="sku-stepper-bottom"></slot>
+      </view>
+
+      <sku-operate
+        :btnOptions="btnOptions"
+        :btnExtraText="btnExtraText"
+        :buyText="buyText"
+        :addCartText="addCartText"
+        :confirmText="confirmText"
+        @clickBtnOperate="clickBtnOperate"
+      >
+        <template #operate-btn v-if="getSlots('sku-operate')">
+          <slot name="sku-operate"></slot>
+        </template>
+      </sku-operate>
+    </view>
+  </nut-popup>
+</template>
+<script lang="ts">
+import { ref, watch, onMounted } from 'vue';
+import SkuHeader from './components/SkuHeader.vue';
+import SkuSelect from './components/SkuSelect.vue';
+import SkuStepper from './components/SkuStepper.vue';
+import SkuOperate from './components/SkuOperate.vue';
+import { createComponent } from '../../utils/create';
+const { componentName, create } = createComponent('sku');
+
+export default create({
+  props: {
+    visible: {
+      type: Boolean,
+      default: false
+    },
+
+    sku: {
+      type: Array,
+      default: []
+    },
+
+    goods: {
+      type: Object,
+      default: {}
+    },
+
+    // stepper 最大值
+    stepperMax: {
+      type: [Number, String],
+      default: 99999
+    },
+
+    // stepper 最小值
+    stepperMin: {
+      type: [Number, String],
+      default: 1
+    },
+
+    // 底部按钮配置  confirm cart  buy
+    btnOptions: {
+      type: Array,
+      default: () => ['confirm']
+    },
+
+    // 数量选择左侧文案
+    stepperTitle: {
+      type: String,
+      default: '购买数量'
+    },
+
+    // stepper 前面文案
+    stepperExtraText: {
+      type: [Function, Boolean],
+      default: false
+    },
+
+    btnExtraText: {
+      type: String,
+      default: ''
+    },
+
+    // 立即购买文案
+    buyText: {
+      type: String,
+      default: '立即购买'
+    },
+
+    // 加入购物车文案
+    addCartText: {
+      type: String,
+      default: '加入购物车'
+    },
+
+    // 确定文案
+    confirmText: {
+      type: String,
+      default: '确定'
+    }
+  },
+  emits: [
+    'update:visible',
+    'selectSku',
+    'changeStepper',
+    'clickBtnOperate',
+    'clickCloseIcon',
+    'clickOverlay',
+    'close',
+    'reduce',
+    'add',
+    'overLimit',
+    'clickOverlay'
+  ],
+
+  components: {
+    SkuHeader,
+    SkuSelect,
+    SkuStepper,
+    SkuOperate
+  },
+
+  setup(props: any, { emit, slots }) {
+    const showPopup = ref(props.visible);
+
+    const goodsCount = ref(props.stepperMin);
+
+    watch(
+      () => props.visible,
+      (value) => {
+        showPopup.value = value;
+      }
+    );
+
+    watch(
+      () => showPopup.value,
+      (value) => {
+        if (value == false) {
+          close();
+        }
+      }
+    );
+
+    onMounted(() => {
+      console.log('更新参数');
+    });
+
+    const getSlots = (name: string) => slots[name];
+
+    // 商品规格 sku 选择
+    const selectSku = (skus: any) => {
+      emit('selectSku', skus);
+    };
+
+    // 数量计步器变化
+    const changeStepper = (value: number) => {
+      goodsCount.value = value;
+
+      emit('changeStepper', value);
+    };
+
+    // 修改购买数量 add 加  reduce 减
+    const add = (value: number) => {
+      emit('add', value);
+    };
+
+    const reduce = (value: number) => {
+      emit('reduce', value);
+    };
+
+    // 触发极限值
+    const stepperOverLimit = (count: any) => {
+      emit('overLimit', count);
+    };
+
+    // 点击 button 操作
+    const clickBtnOperate = (btn: string) => {
+      emit('clickBtnOperate', {
+        type: btn,
+        value: goodsCount.value
+      });
+    };
+
+    // 关闭
+    const closePopup = (type: string) => {
+      if (type == 'icon') {
+        emit('click-close-icon');
+      }
+
+      if (type == 'overlay') {
+        emit('click-overlay');
+      }
+
+      if (type == 'close') {
+        emit('close');
+      }
+
+      showPopup.value = false;
+    };
+
+    const close = () => {
+      emit('update:visible', false);
+    };
+
+    return {
+      showPopup,
+      closePopup,
+      selectSku,
+      changeStepper,
+      stepperOverLimit,
+      clickBtnOperate,
+      add,
+      reduce,
+      getSlots
+    };
+  }
+});
+</script>

+ 16 - 0
src/packages/styles/variables.scss

@@ -362,5 +362,21 @@ $searchbar-background: $white !default;
 $searchbar-input-background: #f7f7f7 !default;
 $searchbar-right-out-color: $black !default;
 
+// sku
+$sku-item-border: 1px solid $primary-color;
+$sku-item-disable-line: line-through;
+$sku-opetate-bg-default: linear-gradient(
+  135deg,
+  rgba(242, 20, 12, 1) 0%,
+  rgba(242, 39, 12, 1) 70%,
+  rgba(242, 77, 12, 1) 100%
+);
+$sku-opetate-bg-buy: linear-gradient(
+  135deg,
+  rgba(255, 186, 13, 1) 0%,
+  rgba(255, 195, 13, 1) 69%,
+  rgba(255, 207, 13, 1) 100%
+);
+
 @import './mixins/index';
 @import './animation/index';

+ 39 - 0
src/packages/utils/util.ts

@@ -0,0 +1,39 @@
+// 变量类型判断
+export const TypeOfFun = (value: any) => {
+  if (null === value) {
+    return 'null';
+  }
+
+  const type = typeof value;
+  if ('undefined' === type || 'string' === type) {
+    return type;
+  }
+
+  const typeString = toString.call(value);
+  switch (typeString) {
+    case '[object Array]':
+      return 'array';
+    case '[object Date]':
+      return 'date';
+    case '[object Boolean]':
+      return 'boolean';
+    case '[object Number]':
+      return 'number';
+    case '[object Function]':
+      return 'function';
+    case '[object RegExp]':
+      return 'regexp';
+    case '[object Object]':
+      if (undefined !== value.nodeType) {
+        if (3 == value.nodeType) {
+          return /\S/.test(value.nodeValue) ? 'textnode' : 'whitespace';
+        } else {
+          return 'element';
+        }
+      } else {
+        return 'object';
+      }
+    default:
+      return 'unknow';
+  }
+};

+ 7 - 1
src/sites/mobile-taro/vue/src/app.config.ts

@@ -76,7 +76,13 @@ export default {
     },
     {
       root: 'business',
-      pages: ['pages/address/index', 'pages/signature/index', 'pages/barrage/index', 'pages/timeselect/index']
+      pages: [
+        'pages/address/index',
+        'pages/signature/index',
+        'pages/barrage/index',
+        'pages/timeselect/index',
+        'pages/sku/index'
+      ]
     }
   ],
   window: {

+ 57 - 0
src/sites/mobile-taro/vue/src/business/pages/sku/data.js

@@ -0,0 +1,57 @@
+export const Sku = [
+  {
+    id: 1,
+    name: '种类',
+    list: [
+      {
+        name: '五香味150g*3',
+        id: 100016015112,
+        _active: true,
+        _disable: false
+      },
+      {
+        name: '五香味150g*8',
+        id: 100016015142,
+        _active: false,
+        _disable: true
+      },
+      {
+        name: '香辣味150g*3',
+        id: 100016015078,
+        _active: false,
+        _disable: false
+      },
+      {
+        name: '香辣味150g*8',
+        id: 100009064831,
+        _active: false,
+        _disable: true
+      }
+    ]
+  },
+  {
+    id: 2,
+    name: '规格',
+    list: [
+      {
+        name: '150g',
+        id: 100016015102,
+        _active: true,
+        _disable: false
+      },
+      {
+        name: '150g*8',
+        id: 100016015122,
+        _active: false,
+        _disable: false
+      }
+    ]
+  }
+];
+
+export const Goods = {
+  skuId: '100016015112',
+  price: '9.10',
+  imagePath:
+    'https://img13.360buyimg.com/imagetools/s750x750_jfs/t1/155184/15/3792/210311/5f98da84Efb6e1da6/d2a3b5ca1fc7019c.jpg'
+};

+ 4 - 0
src/sites/mobile-taro/vue/src/business/pages/sku/index.config.js

@@ -0,0 +1,4 @@
+export default {
+  navigationBarTitleText: 'Sku',
+  disableScroll: true
+};

+ 300 - 0
src/sites/mobile-taro/vue/src/business/pages/sku/index.vue

@@ -0,0 +1,300 @@
+<template>
+  <div class="demo">
+    <h2>基本用法</h2>
+    <nut-cell :title="`基本用法`" desc="" @click="base = true"></nut-cell>
+
+    <h2>限购模式</h2>
+    <nut-cell title="设置限购、已购、起购" desc="" @click="openQuota = true"></nut-cell>
+
+    <h2>自定义计步器</h2>
+    <nut-cell title="自定义计步器" desc="" @click="customStepper = true"></nut-cell>
+
+    <h2>自定义插槽</h2>
+    <nut-cell title="通过插槽自定义设置" desc="" @click="customBySlot = true"></nut-cell>
+
+    <nut-sku
+      v-model:visible="base"
+      :sku="skuData"
+      :goods="goodsInfo"
+      @selectSku="selectSku"
+      @clickBtnOperate="clickBtnOperate"
+      @close="close"
+    ></nut-sku>
+
+    <nut-sku
+      v-model:visible="openQuota"
+      :sku="skuData"
+      :goods="goodsInfo"
+      :showSaleLimit="true"
+      :showSaleLowest="true"
+      :stepperMax="7"
+      :stepperMin="2"
+      :purchased="2"
+      :btnOptions="['buy', 'cart']"
+      @selectSku="selectSku"
+      @clickBtnOperate="clickBtnOperate"
+      @close="close"
+    ></nut-sku>
+
+    <nut-sku
+      v-model:visible="customStepper"
+      :sku="skuData"
+      :goods="goodsInfo"
+      :showSaleLimit="true"
+      :stepperMax="7"
+      :stepperMin="2"
+      :saleLowestText="saleLowestText"
+      :saleLimitText="saleLimitText"
+      @changeStepper="changeStepper"
+      @overLimit="overLimit"
+      :btnOptions="['buy', 'cart']"
+      @selectSku="selectSku"
+      @clickBtnOperate="clickBtnOperate"
+      @close="close"
+    ></nut-sku>
+
+    <nut-sku
+      v-model:visible="customBySlot"
+      :sku="skuData"
+      :goods="goodsInfo"
+      :btnOptions="['buy', 'cart']"
+      @selectSku="selectSku"
+      @clickBtnOperate="clickBtnOperate"
+      @close="close()"
+    >
+      <template #sku-header-price>
+        <div>
+          <nut-price :price="goodsInfo.price" :needSymbol="true" :thousands="false"> </nut-price>
+          <span class="tag"></span>
+        </div>
+      </template>
+
+      <template #sku-header-extra>
+        <span class="nut-sku-header-right-extra">重量:0.1kg 编号:{{ goodsInfo.skuId }} </span>
+      </template>
+
+      <template #sku-operate>
+        <div class="sku-operate-box">
+          <nut-button class="sku-operate-item" shape="square" type="warning">加入购物车</nut-button>
+          <nut-button class="sku-operate-item" shape="square" type="primary">立即购买</nut-button>
+        </div>
+      </template>
+
+      <template #sku-select-top>
+        <div class="address">
+          <nut-cell
+            style="box-shadow: none; padding: 13px 0"
+            title="送至"
+            :desc="addressDesc"
+            @click="showAddressPopup = true"
+          ></nut-cell>
+        </div>
+      </template>
+    </nut-sku>
+
+    <nut-address
+      v-model:visible="showAddressPopup"
+      type="exist"
+      :exist-address="existAddress"
+      @close="close"
+      :is-show-custom-address="false"
+      @selected="selectedAddress"
+      exist-address-title="配送至"
+    ></nut-address>
+  </div>
+</template>
+
+<script lang="ts">
+import { reactive, ref, toRefs, onMounted, defineComponent } from 'vue';
+import { Sku, Goods } from './data';
+
+interface Skus {
+  id: number;
+  name: string;
+  list: SkuItem[];
+  [key: string]: any;
+}
+
+interface SkuItem {
+  id: number;
+  name: string;
+  imagePath: string;
+  [key: string]: any;
+}
+
+interface GoodsProps {
+  skuId: string | number;
+  price: string; // 商品信息展示区,商品价格
+  imagePath: string;
+  [key: string]: any;
+}
+
+interface Data {
+  skuData: Skus[];
+  goodsInfo: GoodsProps;
+}
+
+export default defineComponent({
+  props: {},
+  setup() {
+    const popup = reactive({
+      base: false,
+      openQuota: false,
+      customStepper: false,
+      customBySlot: false,
+
+      showAddressPopup: false
+    });
+
+    const data = reactive<Data>({
+      skuData: [],
+      goodsInfo: {}
+    });
+
+    const saleLowestText = (min: string) => `${min} 件起售`;
+    const saleLimitText = (max: string) => `最多买${max}件`;
+
+    const addressDesc = ref('(配送地会影响库存,请先确认)');
+    const existAddress = ref([
+      {
+        id: 1,
+        addressDetail: 'th ',
+        cityName: '石景山区',
+        countyName: '城区',
+        provinceName: '北京',
+        selectedAddress: true,
+        townName: ''
+      },
+      {
+        id: 2,
+        addressDetail: '12_ ',
+        cityName: '电饭锅',
+        countyName: '扶绥县',
+        provinceName: '北京',
+        selectedAddress: false,
+        townName: ''
+      },
+      {
+        id: 3,
+        addressDetail: '发大水比 ',
+        cityName: '放到',
+        countyName: '广宁街道',
+        provinceName: '钓鱼岛全区',
+        selectedAddress: false,
+        townName: ''
+      },
+      {
+        id: 4,
+        addressDetail: '还是想吧百度吧 ',
+        cityName: '研发',
+        countyName: '八里庄街道',
+        provinceName: '北京',
+        selectedAddress: false,
+        townName: ''
+      }
+    ]);
+
+    onMounted(() => {
+      getData();
+    });
+
+    const getData = () => {
+      setTimeout(() => {
+        data.skuData = Sku;
+        data.goodsInfo = Goods;
+      }, 500);
+    };
+    const selectSku = (s: any) => {
+      const { sku, skuIndex, parentSku, parentIndex } = s;
+
+      if (sku._disable) return false;
+
+      data.skuData[parentIndex].list.forEach((s) => {
+        s._active = s.id == sku.id;
+      });
+
+      data.goodsInfo = {
+        skuId: sku.id,
+        price: '9.10', // 商品信息展示区,商品价格
+        imagePath:
+          'https://img20.360buyimg.com/imagetools/s750x750_jfs/t1/201286/22/5692/60152/6136fb94Eea1a9d48/211f40f9d27e6cea.jpg' // 商品信息展示区,商品图
+      };
+    };
+
+    // stepper 更改
+    const changeStepper = (count: number) => {
+      console.log('购买数量', count);
+    };
+
+    // stepper 极限值
+    const overLimit = (val: any) => {
+      if (val.action == 'reduce') {
+        console.log(`至少买${val.value}件哦`);
+      } else {
+        console.log(`最多买${val.value}件哦`);
+      }
+    };
+
+    const clickBtnOperate = (op: string) => {
+      console.log('点击了操作按钮', op);
+    };
+    // 关闭弹框
+    const close = () => {
+      console.log('选择弹框关闭');
+    };
+
+    const selectedAddress = (prevExistAdd: any, nowExistAdd: any) => {
+      const { provinceName, countyName, cityName } = nowExistAdd;
+      addressDesc.value = `${provinceName}${countyName}${cityName}`;
+    };
+
+    return {
+      selectSku,
+      changeStepper,
+      Goods,
+      clickBtnOperate,
+      close,
+      existAddress,
+      selectedAddress,
+      addressDesc,
+      saleLowestText,
+      saleLimitText,
+      overLimit,
+      ...toRefs(popup),
+      ...toRefs(data)
+    };
+  }
+});
+</script>
+
+<style lang="scss">
+.tag {
+  display: inline-block;
+  width: 50px;
+  height: 15px;
+  font-size: 12px;
+  margin-left: 10px;
+  background: url('//storage.360buyimg.com/imgtools/bbdf6c9a2a-e3f6fbc0-fb4d-11eb-a27f-676da10c85f4.png') no-repeat
+    center center;
+  background-size: 100% 100%;
+}
+
+.sku-operate-box {
+  width: 100%;
+  display: flex;
+  padding: 8px 0px;
+
+  .sku-operate-item {
+    width: 100%;
+    flex-shrink: 1;
+    &:first-child {
+      border-top-left-radius: 20px;
+      border-bottom-left-radius: 20px;
+    }
+    &:last-child {
+      border-top-right-radius: 20px;
+      border-bottom-right-radius: 20px;
+    }
+  }
+}
+</style>