萤火科技 há 1 ano atrás
pai
commit
83a64cc0c2
100 ficheiros alterados com 6414 adições e 0 exclusões
  1. 3 0
      .gitignore
  2. 20 0
      .hbuilderx/launch.json
  3. 13 0
      .vite/deps/_metadata.json
  4. 1 0
      .vite/deps/package.json
  5. 766 0
      .vite/deps/weixin-js-sdk.js
  6. 3 0
      .vite/deps/weixin-js-sdk.js.map
  7. 83 0
      App.vue
  8. 191 0
      LICENSE
  9. 69 0
      README.md
  10. 14 0
      androidPrivacy.json
  11. 47 0
      api/address.js
  12. 11 0
      api/article/category.js
  13. 17 0
      api/article/index.js
  14. 11 0
      api/balance.js
  15. 11 0
      api/balance/log.js
  16. 17 0
      api/captcha.js
  17. 35 0
      api/cart.js
  18. 35 0
      api/cashier/index.js
  19. 11 0
      api/category/index.js
  20. 19 0
      api/checkout.js
  21. 23 0
      api/comment.js
  22. 16 0
      api/coupon.js
  23. 11 0
      api/express.js
  24. 37 0
      api/goods/index.js
  25. 11 0
      api/goods/service.js
  26. 11 0
      api/help.js
  27. 29 0
      api/login/index.js
  28. 22 0
      api/myCoupon.js
  29. 42 0
      api/order.js
  30. 17 0
      api/order/comment.js
  31. 13 0
      api/page.js
  32. 11 0
      api/points/log.js
  33. 27 0
      api/recharge.js
  34. 11 0
      api/recharge/order.js
  35. 35 0
      api/refund.js
  36. 17 0
      api/region.js
  37. 11 0
      api/setting.js
  38. 13 0
      api/store.js
  39. 18 0
      api/upload.js
  40. 34 0
      api/user.js
  41. 11 0
      api/user/coupon.js
  42. 43 0
      app.scss
  43. 7 0
      common/constant/index.js
  44. 0 0
      common/data/region.js
  45. 11 0
      common/enum/Client.js
  46. 10 0
      common/enum/coupon/ApplyRange.js
  47. 10 0
      common/enum/coupon/CouponType.js
  48. 10 0
      common/enum/coupon/ExpireType.js
  49. 5 0
      common/enum/coupon/index.js
  50. 85 0
      common/enum/enum.js
  51. 10 0
      common/enum/goods/GoodsType.js
  52. 10 0
      common/enum/goods/SpecType.js
  53. 4 0
      common/enum/goods/index.js
  54. 11 0
      common/enum/order/DeliveryStatus.js
  55. 10 0
      common/enum/order/DeliveryType.js
  56. 9 0
      common/enum/order/OrderSource.js
  57. 12 0
      common/enum/order/OrderStatus.js
  58. 10 0
      common/enum/order/OrderType.js
  59. 10 0
      common/enum/order/PayStatus.js
  60. 10 0
      common/enum/order/ReceiptStatus.js
  61. 11 0
      common/enum/order/delivery/DeliveryMethod.js
  62. 3 0
      common/enum/order/delivery/index.js
  63. 17 0
      common/enum/order/index.js
  64. 11 0
      common/enum/order/refund/AuditStatus.js
  65. 12 0
      common/enum/order/refund/RefundStatus.js
  66. 10 0
      common/enum/order/refund/RefundType.js
  67. 9 0
      common/enum/order/refund/index.js
  68. 11 0
      common/enum/payment/Method.js
  69. 3 0
      common/enum/payment/index.js
  70. 37 0
      common/enum/setting/Key.js
  71. 12 0
      common/enum/store/page/category/Style.js
  72. 3 0
      common/enum/store/page/category/index.js
  73. 77 0
      common/model/Region.js
  74. 85 0
      common/model/Setting.js
  75. 78 0
      common/model/Store.js
  76. 36 0
      components/add-cart-btn/index.vue
  77. 178 0
      components/add-cart-popup/index.vue
  78. 57 0
      components/avatar-image/index.vue
  79. 194 0
      components/countdown/index.vue
  80. 60 0
      components/customer-btn/index.vue
  81. 67 0
      components/empty/index.vue
  82. 1389 0
      components/goods-sku-popup/index.vue
  83. 473 0
      components/goods-sku-popup/number-box/index.vue
  84. 43 0
      components/page/diyComponents/ICPLicense/index.vue
  85. 123 0
      components/page/diyComponents/article/index.vue
  86. 133 0
      components/page/diyComponents/banner/index.vue
  87. 31 0
      components/page/diyComponents/blank/index.vue
  88. 209 0
      components/page/diyComponents/coupon/index.vue
  89. 39 0
      components/page/diyComponents/empty.vue
  90. 385 0
      components/page/diyComponents/goods/index.vue
  91. 36 0
      components/page/diyComponents/guide/index.vue
  92. 64 0
      components/page/diyComponents/hotZone/index.vue
  93. 44 0
      components/page/diyComponents/image/index.vue
  94. 17 0
      components/page/diyComponents/mixin.js
  95. 94 0
      components/page/diyComponents/navBar/index.vue
  96. 40 0
      components/page/diyComponents/notice/index.vue
  97. 39 0
      components/page/diyComponents/officialAccount/index.vue
  98. 33 0
      components/page/diyComponents/richText/index.vue
  99. 84 0
      components/page/diyComponents/search/index.vue
  100. 93 0
      components/page/diyComponents/service/index.vue

+ 3 - 0
.gitignore

@@ -0,0 +1,3 @@
+/unpackage
+/更新节点.txt
+/待处理.txt

+ 20 - 0
.hbuilderx/launch.json

@@ -0,0 +1,20 @@
+{ // launch.json 配置了启动调试时相关设置,configurations下节点名称可为 app-plus/h5/mp-weixin/mp-baidu/mp-alipay/mp-qq/mp-toutiao/mp-360/
+  // launchtype项可配置值为local或remote, local代表前端连本地云函数,remote代表前端连云端云函数
+    "version": "0.0",
+    "configurations": [{
+     	"default" : 
+     	{
+     		"launchtype" : "remote"
+     	},
+     	"h5" : 
+     	{
+     		"launchtype" : "remote"
+     	},
+     	"mp-weixin" : 
+     	{
+     		"launchtype" : "remote"
+     	},
+     	"type" : "uniCloud"
+     }
+    ]
+}

+ 13 - 0
.vite/deps/_metadata.json

@@ -0,0 +1,13 @@
+{
+  "hash": "4ae7f80a",
+  "browserHash": "741dc287",
+  "optimized": {
+    "weixin-js-sdk": {
+      "src": "../../node_modules/weixin-js-sdk/index.js",
+      "file": "weixin-js-sdk.js",
+      "fileHash": "8d88497e",
+      "needsInterop": true
+    }
+  },
+  "chunks": {}
+}

+ 1 - 0
.vite/deps/package.json

@@ -0,0 +1 @@
+{"type":"module"}

+ 766 - 0
.vite/deps/weixin-js-sdk.js

@@ -0,0 +1,766 @@
+var __getOwnPropNames = Object.getOwnPropertyNames;
+var __commonJS = (cb, mod) => function __require() {
+  return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
+};
+
+// ../../../../www/@hbuilder/yoshop2-pro/node_modules/weixin-js-sdk/index.js
+var require_weixin_js_sdk = __commonJS({
+  "../../../../www/@hbuilder/yoshop2-pro/node_modules/weixin-js-sdk/index.js"(exports, module) {
+    !function(e, n) {
+      module.exports = n(e);
+    }(window, function(o, e) {
+      if (!o.jWeixin) {
+        var n, c = {
+          config: "preVerifyJSAPI",
+          onMenuShareTimeline: "menu:share:timeline",
+          onMenuShareAppMessage: "menu:share:appmessage",
+          onMenuShareQQ: "menu:share:qq",
+          onMenuShareWeibo: "menu:share:weiboApp",
+          onMenuShareQZone: "menu:share:QZone",
+          previewImage: "imagePreview",
+          getLocation: "geoLocation",
+          openProductSpecificView: "openProductViewWithPid",
+          addCard: "batchAddCard",
+          openCard: "batchViewCard",
+          chooseWXPay: "getBrandWCPayRequest",
+          openEnterpriseRedPacket: "getRecevieBizHongBaoRequest",
+          startSearchBeacons: "startMonitoringBeacons",
+          stopSearchBeacons: "stopMonitoringBeacons",
+          onSearchBeacons: "onBeaconsInRange",
+          consumeAndShareCard: "consumedShareCard",
+          openAddress: "editAddress"
+        }, a = function() {
+          var e2 = {};
+          for (var n2 in c)
+            e2[c[n2]] = n2;
+          return e2;
+        }(), i = o.document, t = i.title, r = navigator.userAgent.toLowerCase(), s = navigator.platform.toLowerCase(), d = !(!s.match("mac") && !s.match("win")), u = -1 != r.indexOf("wxdebugger"), l = -1 != r.indexOf("micromessenger"), p = -1 != r.indexOf("android"), f = -1 != r.indexOf("iphone") || -1 != r.indexOf("ipad"), m = (n = r.match(/micromessenger\/(\d+\.\d+\.\d+)/) || r.match(/micromessenger\/(\d+\.\d+)/)) ? n[1] : "", g = {
+          initStartTime: L(),
+          initEndTime: 0,
+          preVerifyStartTime: 0,
+          preVerifyEndTime: 0
+        }, h = {
+          version: 1,
+          appId: "",
+          initTime: 0,
+          preVerifyTime: 0,
+          networkType: "",
+          isPreVerifyOk: 1,
+          systemType: f ? 1 : p ? 2 : -1,
+          clientVersion: m,
+          url: encodeURIComponent(location.href)
+        }, v = {}, S = { _completes: [] }, y = { state: 0, data: {} };
+        O(function() {
+          g.initEndTime = L();
+        });
+        var I = false, _ = [], w = {
+          config: function(e2) {
+            B("config", v = e2);
+            var t2 = false !== v.check;
+            O(function() {
+              if (t2)
+                M(
+                  c.config,
+                  {
+                    verifyJsApiList: C(v.jsApiList),
+                    verifyOpenTagList: C(v.openTagList)
+                  },
+                  function() {
+                    S._complete = function(e4) {
+                      g.preVerifyEndTime = L(), y.state = 1, y.data = e4;
+                    }, S.success = function(e4) {
+                      h.isPreVerifyOk = 0;
+                    }, S.fail = function(e4) {
+                      S._fail ? S._fail(e4) : y.state = -1;
+                    };
+                    var t3 = S._completes;
+                    return t3.push(function() {
+                      !function() {
+                        if (!(d || u || v.debug || m < "6.0.2" || h.systemType < 0)) {
+                          var i3 = new Image();
+                          h.appId = v.appId, h.initTime = g.initEndTime - g.initStartTime, h.preVerifyTime = g.preVerifyEndTime - g.preVerifyStartTime, w.getNetworkType({
+                            isInnerInvoke: true,
+                            success: function(e4) {
+                              h.networkType = e4.networkType;
+                              var n3 = "https://open.weixin.qq.com/sdk/report?v=" + h.version + "&o=" + h.isPreVerifyOk + "&s=" + h.systemType + "&c=" + h.clientVersion + "&a=" + h.appId + "&n=" + h.networkType + "&i=" + h.initTime + "&p=" + h.preVerifyTime + "&u=" + h.url;
+                              i3.src = n3;
+                            }
+                          });
+                        }
+                      }();
+                    }), S.complete = function(e4) {
+                      for (var n3 = 0, i3 = t3.length; n3 < i3; ++n3)
+                        t3[n3]();
+                      S._completes = [];
+                    }, S;
+                  }()
+                ), g.preVerifyStartTime = L();
+              else {
+                y.state = 1;
+                for (var e3 = S._completes, n2 = 0, i2 = e3.length; n2 < i2; ++n2)
+                  e3[n2]();
+                S._completes = [];
+              }
+            }), w.invoke || (w.invoke = function(e3, n2, i2) {
+              o.WeixinJSBridge && WeixinJSBridge.invoke(e3, x(n2), i2);
+            }, w.on = function(e3, n2) {
+              o.WeixinJSBridge && WeixinJSBridge.on(e3, n2);
+            });
+          },
+          ready: function(e2) {
+            0 != y.state ? e2() : (S._completes.push(e2), !l && v.debug && e2());
+          },
+          error: function(e2) {
+            m < "6.0.2" || (-1 == y.state ? e2(y.data) : S._fail = e2);
+          },
+          checkJsApi: function(e2) {
+            M(
+              "checkJsApi",
+              { jsApiList: C(e2.jsApiList) },
+              (e2._complete = function(e3) {
+                if (p) {
+                  var n2 = e3.checkResult;
+                  n2 && (e3.checkResult = JSON.parse(n2));
+                }
+                e3 = function(e4) {
+                  var n3 = e4.checkResult;
+                  for (var i2 in n3) {
+                    var t2 = a[i2];
+                    t2 && (n3[t2] = n3[i2], delete n3[i2]);
+                  }
+                  return e4;
+                }(e3);
+              }, e2)
+            );
+          },
+          onMenuShareTimeline: function(e2) {
+            P(
+              c.onMenuShareTimeline,
+              {
+                complete: function() {
+                  M(
+                    "shareTimeline",
+                    {
+                      title: e2.title || t,
+                      desc: e2.title || t,
+                      img_url: e2.imgUrl || "",
+                      link: e2.link || location.href,
+                      type: e2.type || "link",
+                      data_url: e2.dataUrl || ""
+                    },
+                    e2
+                  );
+                }
+              },
+              e2
+            );
+          },
+          onMenuShareAppMessage: function(n2) {
+            P(
+              c.onMenuShareAppMessage,
+              {
+                complete: function(e2) {
+                  "favorite" === e2.scene ? M("sendAppMessage", {
+                    title: n2.title || t,
+                    desc: n2.desc || "",
+                    link: n2.link || location.href,
+                    img_url: n2.imgUrl || "",
+                    type: n2.type || "link",
+                    data_url: n2.dataUrl || ""
+                  }) : M(
+                    "sendAppMessage",
+                    {
+                      title: n2.title || t,
+                      desc: n2.desc || "",
+                      link: n2.link || location.href,
+                      img_url: n2.imgUrl || "",
+                      type: n2.type || "link",
+                      data_url: n2.dataUrl || ""
+                    },
+                    n2
+                  );
+                }
+              },
+              n2
+            );
+          },
+          onMenuShareQQ: function(e2) {
+            P(
+              c.onMenuShareQQ,
+              {
+                complete: function() {
+                  M(
+                    "shareQQ",
+                    {
+                      title: e2.title || t,
+                      desc: e2.desc || "",
+                      img_url: e2.imgUrl || "",
+                      link: e2.link || location.href
+                    },
+                    e2
+                  );
+                }
+              },
+              e2
+            );
+          },
+          onMenuShareWeibo: function(e2) {
+            P(
+              c.onMenuShareWeibo,
+              {
+                complete: function() {
+                  M(
+                    "shareWeiboApp",
+                    {
+                      title: e2.title || t,
+                      desc: e2.desc || "",
+                      img_url: e2.imgUrl || "",
+                      link: e2.link || location.href
+                    },
+                    e2
+                  );
+                }
+              },
+              e2
+            );
+          },
+          onMenuShareQZone: function(e2) {
+            P(
+              c.onMenuShareQZone,
+              {
+                complete: function() {
+                  M(
+                    "shareQZone",
+                    {
+                      title: e2.title || t,
+                      desc: e2.desc || "",
+                      img_url: e2.imgUrl || "",
+                      link: e2.link || location.href
+                    },
+                    e2
+                  );
+                }
+              },
+              e2
+            );
+          },
+          updateTimelineShareData: function(e2) {
+            M(
+              "updateTimelineShareData",
+              { title: e2.title, link: e2.link, imgUrl: e2.imgUrl },
+              e2
+            );
+          },
+          updateAppMessageShareData: function(e2) {
+            M(
+              "updateAppMessageShareData",
+              { title: e2.title, desc: e2.desc, link: e2.link, imgUrl: e2.imgUrl },
+              e2
+            );
+          },
+          startRecord: function(e2) {
+            M("startRecord", {}, e2);
+          },
+          stopRecord: function(e2) {
+            M("stopRecord", {}, e2);
+          },
+          onVoiceRecordEnd: function(e2) {
+            P("onVoiceRecordEnd", e2);
+          },
+          playVoice: function(e2) {
+            M("playVoice", { localId: e2.localId }, e2);
+          },
+          pauseVoice: function(e2) {
+            M("pauseVoice", { localId: e2.localId }, e2);
+          },
+          stopVoice: function(e2) {
+            M("stopVoice", { localId: e2.localId }, e2);
+          },
+          onVoicePlayEnd: function(e2) {
+            P("onVoicePlayEnd", e2);
+          },
+          uploadVoice: function(e2) {
+            M(
+              "uploadVoice",
+              {
+                localId: e2.localId,
+                isShowProgressTips: 0 == e2.isShowProgressTips ? 0 : 1
+              },
+              e2
+            );
+          },
+          downloadVoice: function(e2) {
+            M(
+              "downloadVoice",
+              {
+                serverId: e2.serverId,
+                isShowProgressTips: 0 == e2.isShowProgressTips ? 0 : 1
+              },
+              e2
+            );
+          },
+          translateVoice: function(e2) {
+            M(
+              "translateVoice",
+              {
+                localId: e2.localId,
+                isShowProgressTips: 0 == e2.isShowProgressTips ? 0 : 1
+              },
+              e2
+            );
+          },
+          chooseImage: function(e2) {
+            M(
+              "chooseImage",
+              {
+                scene: "1|2",
+                count: e2.count || 9,
+                sizeType: e2.sizeType || ["original", "compressed"],
+                sourceType: e2.sourceType || ["album", "camera"]
+              },
+              (e2._complete = function(e3) {
+                if (p) {
+                  var n2 = e3.localIds;
+                  try {
+                    n2 && (e3.localIds = JSON.parse(n2));
+                  } catch (e4) {
+                  }
+                }
+              }, e2)
+            );
+          },
+          getLocation: function(e2) {
+          },
+          previewImage: function(e2) {
+            M(c.previewImage, { current: e2.current, urls: e2.urls }, e2);
+          },
+          uploadImage: function(e2) {
+            M(
+              "uploadImage",
+              {
+                localId: e2.localId,
+                isShowProgressTips: 0 == e2.isShowProgressTips ? 0 : 1
+              },
+              e2
+            );
+          },
+          downloadImage: function(e2) {
+            M(
+              "downloadImage",
+              {
+                serverId: e2.serverId,
+                isShowProgressTips: 0 == e2.isShowProgressTips ? 0 : 1
+              },
+              e2
+            );
+          },
+          getLocalImgData: function(e2) {
+            false === I ? (I = true, M(
+              "getLocalImgData",
+              { localId: e2.localId },
+              (e2._complete = function(e3) {
+                if (I = false, 0 < _.length) {
+                  var n2 = _.shift();
+                  wx.getLocalImgData(n2);
+                }
+              }, e2)
+            )) : _.push(e2);
+          },
+          getNetworkType: function(e2) {
+            M(
+              "getNetworkType",
+              {},
+              (e2._complete = function(e3) {
+                e3 = function(e4) {
+                  var n2 = e4.errMsg;
+                  e4.errMsg = "getNetworkType:ok";
+                  var i2 = e4.subtype;
+                  if (delete e4.subtype, i2)
+                    e4.networkType = i2;
+                  else {
+                    var t2 = n2.indexOf(":"), o2 = n2.substring(t2 + 1);
+                    switch (o2) {
+                      case "wifi":
+                      case "edge":
+                      case "wwan":
+                        e4.networkType = o2;
+                        break;
+                      default:
+                        e4.errMsg = "getNetworkType:fail";
+                    }
+                  }
+                  return e4;
+                }(e3);
+              }, e2)
+            );
+          },
+          openLocation: function(e2) {
+            M(
+              "openLocation",
+              {
+                latitude: e2.latitude,
+                longitude: e2.longitude,
+                name: e2.name || "",
+                address: e2.address || "",
+                scale: e2.scale || 28,
+                infoUrl: e2.infoUrl || ""
+              },
+              e2
+            );
+          },
+          getLocation: function(e2) {
+            M(
+              c.getLocation,
+              { type: (e2 = e2 || {}).type || "wgs84" },
+              (e2._complete = function(e3) {
+                delete e3.type;
+              }, e2)
+            );
+          },
+          hideOptionMenu: function(e2) {
+            M("hideOptionMenu", {}, e2);
+          },
+          showOptionMenu: function(e2) {
+            M("showOptionMenu", {}, e2);
+          },
+          closeWindow: function(e2) {
+            M("closeWindow", {}, e2 = e2 || {});
+          },
+          hideMenuItems: function(e2) {
+            M("hideMenuItems", { menuList: e2.menuList }, e2);
+          },
+          showMenuItems: function(e2) {
+            M("showMenuItems", { menuList: e2.menuList }, e2);
+          },
+          hideAllNonBaseMenuItem: function(e2) {
+            M("hideAllNonBaseMenuItem", {}, e2);
+          },
+          showAllNonBaseMenuItem: function(e2) {
+            M("showAllNonBaseMenuItem", {}, e2);
+          },
+          scanQRCode: function(e2) {
+            M(
+              "scanQRCode",
+              {
+                needResult: (e2 = e2 || {}).needResult || 0,
+                scanType: e2.scanType || ["qrCode", "barCode"]
+              },
+              (e2._complete = function(e3) {
+                if (f) {
+                  var n2 = e3.resultStr;
+                  if (n2) {
+                    var i2 = JSON.parse(n2);
+                    e3.resultStr = i2 && i2.scan_code && i2.scan_code.scan_result;
+                  }
+                }
+              }, e2)
+            );
+          },
+          openAddress: function(e2) {
+            M(
+              c.openAddress,
+              {},
+              (e2._complete = function(e3) {
+                e3 = function(e4) {
+                  return e4.postalCode = e4.addressPostalCode, delete e4.addressPostalCode, e4.provinceName = e4.proviceFirstStageName, delete e4.proviceFirstStageName, e4.cityName = e4.addressCitySecondStageName, delete e4.addressCitySecondStageName, e4.countryName = e4.addressCountiesThirdStageName, delete e4.addressCountiesThirdStageName, e4.detailInfo = e4.addressDetailInfo, delete e4.addressDetailInfo, e4;
+                }(e3);
+              }, e2)
+            );
+          },
+          openProductSpecificView: function(e2) {
+            M(
+              c.openProductSpecificView,
+              {
+                pid: e2.productId,
+                view_type: e2.viewType || 0,
+                ext_info: e2.extInfo
+              },
+              e2
+            );
+          },
+          addCard: function(e2) {
+            for (var n2 = e2.cardList, i2 = [], t2 = 0, o2 = n2.length; t2 < o2; ++t2) {
+              var r2 = n2[t2], a2 = { card_id: r2.cardId, card_ext: r2.cardExt };
+              i2.push(a2);
+            }
+            M(
+              c.addCard,
+              { card_list: i2 },
+              (e2._complete = function(e3) {
+                var n3 = e3.card_list;
+                if (n3) {
+                  for (var i3 = 0, t3 = (n3 = JSON.parse(n3)).length; i3 < t3; ++i3) {
+                    var o3 = n3[i3];
+                    o3.cardId = o3.card_id, o3.cardExt = o3.card_ext, o3.isSuccess = !!o3.is_succ, delete o3.card_id, delete o3.card_ext, delete o3.is_succ;
+                  }
+                  e3.cardList = n3, delete e3.card_list;
+                }
+              }, e2)
+            );
+          },
+          chooseCard: function(e2) {
+            M(
+              "chooseCard",
+              {
+                app_id: v.appId,
+                location_id: e2.shopId || "",
+                sign_type: e2.signType || "SHA1",
+                card_id: e2.cardId || "",
+                card_type: e2.cardType || "",
+                card_sign: e2.cardSign,
+                time_stamp: e2.timestamp + "",
+                nonce_str: e2.nonceStr
+              },
+              (e2._complete = function(e3) {
+                e3.cardList = e3.choose_card_info, delete e3.choose_card_info;
+              }, e2)
+            );
+          },
+          openCard: function(e2) {
+            for (var n2 = e2.cardList, i2 = [], t2 = 0, o2 = n2.length; t2 < o2; ++t2) {
+              var r2 = n2[t2], a2 = { card_id: r2.cardId, code: r2.code };
+              i2.push(a2);
+            }
+            M(c.openCard, { card_list: i2 }, e2);
+          },
+          consumeAndShareCard: function(e2) {
+            M(
+              c.consumeAndShareCard,
+              { consumedCardId: e2.cardId, consumedCode: e2.code },
+              e2
+            );
+          },
+          chooseWXPay: function(e2) {
+            M(c.chooseWXPay, V(e2), e2);
+          },
+          openEnterpriseRedPacket: function(e2) {
+            M(c.openEnterpriseRedPacket, V(e2), e2);
+          },
+          startSearchBeacons: function(e2) {
+            M(c.startSearchBeacons, { ticket: e2.ticket }, e2);
+          },
+          stopSearchBeacons: function(e2) {
+            M(c.stopSearchBeacons, {}, e2);
+          },
+          onSearchBeacons: function(e2) {
+            P(c.onSearchBeacons, e2);
+          },
+          openEnterpriseChat: function(e2) {
+            M(
+              "openEnterpriseChat",
+              { useridlist: e2.userIds, chatname: e2.groupName },
+              e2
+            );
+          },
+          launchMiniProgram: function(e2) {
+            M(
+              "launchMiniProgram",
+              {
+                targetAppId: e2.targetAppId,
+                path: function(e3) {
+                  if ("string" == typeof e3 && 0 < e3.length) {
+                    var n2 = e3.split("?")[0], i2 = e3.split("?")[1];
+                    return n2 += ".html", void 0 !== i2 ? n2 + "?" + i2 : n2;
+                  }
+                }(e2.path),
+                envVersion: e2.envVersion
+              },
+              e2
+            );
+          },
+          openBusinessView: function(e2) {
+            M(
+              "openBusinessView",
+              {
+                businessType: e2.businessType,
+                queryString: e2.queryString || "",
+                envVersion: e2.envVersion
+              },
+              (e2._complete = function(n2) {
+                if (p) {
+                  var e3 = n2.extraData;
+                  if (e3)
+                    try {
+                      n2.extraData = JSON.parse(e3);
+                    } catch (e4) {
+                      n2.extraData = {};
+                    }
+                }
+              }, e2)
+            );
+          },
+          miniProgram: {
+            navigateBack: function(e2) {
+              e2 = e2 || {}, O(function() {
+                M(
+                  "invokeMiniProgramAPI",
+                  { name: "navigateBack", arg: { delta: e2.delta || 1 } },
+                  e2
+                );
+              });
+            },
+            navigateTo: function(e2) {
+              O(function() {
+                M(
+                  "invokeMiniProgramAPI",
+                  { name: "navigateTo", arg: { url: e2.url } },
+                  e2
+                );
+              });
+            },
+            redirectTo: function(e2) {
+              O(function() {
+                M(
+                  "invokeMiniProgramAPI",
+                  { name: "redirectTo", arg: { url: e2.url } },
+                  e2
+                );
+              });
+            },
+            switchTab: function(e2) {
+              O(function() {
+                M(
+                  "invokeMiniProgramAPI",
+                  { name: "switchTab", arg: { url: e2.url } },
+                  e2
+                );
+              });
+            },
+            reLaunch: function(e2) {
+              O(function() {
+                M(
+                  "invokeMiniProgramAPI",
+                  { name: "reLaunch", arg: { url: e2.url } },
+                  e2
+                );
+              });
+            },
+            postMessage: function(e2) {
+              O(function() {
+                M(
+                  "invokeMiniProgramAPI",
+                  { name: "postMessage", arg: e2.data || {} },
+                  e2
+                );
+              });
+            },
+            getEnv: function(e2) {
+              O(function() {
+                e2({ miniprogram: "miniprogram" === o.__wxjs_environment });
+              });
+            }
+          }
+        }, T = 1, k = {};
+        return i.addEventListener(
+          "error",
+          function(e2) {
+            if (!p) {
+              var n2 = e2.target, i2 = n2.tagName, t2 = n2.src;
+              if ("IMG" == i2 || "VIDEO" == i2 || "AUDIO" == i2 || "SOURCE" == i2) {
+                if (-1 != t2.indexOf("wxlocalresource://")) {
+                  e2.preventDefault(), e2.stopPropagation();
+                  var o2 = n2["wx-id"];
+                  if (o2 || (o2 = T++, n2["wx-id"] = o2), k[o2])
+                    return;
+                  k[o2] = true, wx.ready(function() {
+                    wx.getLocalImgData({
+                      localId: t2,
+                      success: function(e3) {
+                        n2.src = e3.localData;
+                      }
+                    });
+                  });
+                }
+              }
+            }
+          },
+          true
+        ), i.addEventListener(
+          "load",
+          function(e2) {
+            if (!p) {
+              var n2 = e2.target, i2 = n2.tagName;
+              n2.src;
+              if ("IMG" == i2 || "VIDEO" == i2 || "AUDIO" == i2 || "SOURCE" == i2) {
+                var t2 = n2["wx-id"];
+                t2 && (k[t2] = false);
+              }
+            }
+          },
+          true
+        ), e && (o.wx = o.jWeixin = w), w;
+      }
+      function M(n2, e2, i2) {
+        o.WeixinJSBridge ? WeixinJSBridge.invoke(n2, x(e2), function(e3) {
+          A(n2, e3, i2);
+        }) : B(n2, i2);
+      }
+      function P(n2, i2, t2) {
+        o.WeixinJSBridge ? WeixinJSBridge.on(n2, function(e2) {
+          t2 && t2.trigger && t2.trigger(e2), A(n2, e2, i2);
+        }) : B(n2, t2 || i2);
+      }
+      function x(e2) {
+        return (e2 = e2 || {}).appId = v.appId, e2.verifyAppId = v.appId, e2.verifySignType = "sha1", e2.verifyTimestamp = v.timestamp + "", e2.verifyNonceStr = v.nonceStr, e2.verifySignature = v.signature, e2;
+      }
+      function V(e2) {
+        return {
+          timeStamp: e2.timestamp + "",
+          nonceStr: e2.nonceStr,
+          package: e2.package,
+          paySign: e2.paySign,
+          signType: e2.signType || "SHA1"
+        };
+      }
+      function A(e2, n2, i2) {
+        "openEnterpriseChat" != e2 && "openBusinessView" !== e2 || (n2.errCode = n2.err_code), delete n2.err_code, delete n2.err_desc, delete n2.err_detail;
+        var t2 = n2.errMsg;
+        t2 || (t2 = n2.err_msg, delete n2.err_msg, t2 = function(e3, n3) {
+          var i3 = e3, t3 = a[i3];
+          t3 && (i3 = t3);
+          var o3 = "ok";
+          if (n3) {
+            var r2 = n3.indexOf(":");
+            "confirm" == (o3 = n3.substring(r2 + 1)) && (o3 = "ok"), "failed" == o3 && (o3 = "fail"), -1 != o3.indexOf("failed_") && (o3 = o3.substring(7)), -1 != o3.indexOf("fail_") && (o3 = o3.substring(5)), "access denied" != (o3 = (o3 = o3.replace(/_/g, " ")).toLowerCase()) && "no permission to execute" != o3 || (o3 = "permission denied"), "config" == i3 && "function not exist" == o3 && (o3 = "ok"), "" == o3 && (o3 = "fail");
+          }
+          return n3 = i3 + ":" + o3;
+        }(e2, t2), n2.errMsg = t2), (i2 = i2 || {})._complete && (i2._complete(n2), delete i2._complete), t2 = n2.errMsg || "", v.debug && !i2.isInnerInvoke && alert(JSON.stringify(n2));
+        var o2 = t2.indexOf(":");
+        switch (t2.substring(o2 + 1)) {
+          case "ok":
+            i2.success && i2.success(n2);
+            break;
+          case "cancel":
+            i2.cancel && i2.cancel(n2);
+            break;
+          default:
+            i2.fail && i2.fail(n2);
+        }
+        i2.complete && i2.complete(n2);
+      }
+      function C(e2) {
+        if (e2) {
+          for (var n2 = 0, i2 = e2.length; n2 < i2; ++n2) {
+            var t2 = e2[n2], o2 = c[t2];
+            o2 && (e2[n2] = o2);
+          }
+          return e2;
+        }
+      }
+      function B(e2, n2) {
+        if (!(!v.debug || n2 && n2.isInnerInvoke)) {
+          var i2 = a[e2];
+          i2 && (e2 = i2), n2 && n2._complete && delete n2._complete, console.log('"' + e2 + '",', n2 || "");
+        }
+      }
+      function L() {
+        return new Date().getTime();
+      }
+      function O(e2) {
+        l && (o.WeixinJSBridge ? e2() : i.addEventListener && i.addEventListener("WeixinJSBridgeReady", e2, false));
+      }
+    });
+  }
+});
+export default require_weixin_js_sdk();
+//# sourceMappingURL=weixin-js-sdk.js.map

Diff do ficheiro suprimidas por serem muito extensas
+ 3 - 0
.vite/deps/weixin-js-sdk.js.map


+ 83 - 0
App.vue

@@ -0,0 +1,83 @@
+<script>
+  import store from '@/store'
+  import StoreModel from '@/common/model/Store'
+  import { getSceneData } from './core/app'
+  import { isObject } from './utils/util'
+
+  export default {
+
+    /**
+     * 全局变量
+     */
+    globalData: {},
+
+    /**
+     * 初始化完成时触发
+     */
+    onLaunch({ path, query, scene }) {
+      // #ifdef MP
+      // 小程序主动更新
+      this.updateManager()
+      // #endif
+      // app启动参数
+      this.onStartupQuery(isObject(query) ? query : {})
+      // 获取商城基础信息
+      this.getStoreInfo()
+    },
+
+    methods: {
+
+      // app启动参数
+      onStartupQuery(query) {
+        // 获取二维码场景值
+        // const scene = getSceneData(query)
+      },
+
+      // 获取商城基础信息
+      getStoreInfo() {
+        StoreModel.data(false)
+      },
+
+      // 小程序主动更新
+      updateManager() {
+        const updateManager = uni.getUpdateManager()
+        updateManager.onCheckForUpdate(res => {
+          // 请求完新版本信息的回调
+          // console.log(res.hasUpdate)
+        })
+        updateManager.onUpdateReady(() => {
+          uni.showModal({
+            title: '更新提示',
+            content: '新版本已经准备好,即将重启应用',
+            showCancel: false,
+            success(res) {
+              if (res.confirm) {
+                // 新的版本已经下载好,调用 applyUpdate 应用新版本并重启
+                updateManager.applyUpdate()
+              }
+            }
+          })
+        })
+        updateManager.onUpdateFailed(() => {
+          // 新的版本下载失败
+          uni.showModal({
+            title: '更新提示',
+            content: '新版本下载失败',
+            showCancel: false
+          })
+        })
+      }
+
+    }
+
+  }
+</script>
+
+<style lang="scss">
+  /* uView库样式 */
+  @import "./uni_modules/vk-uview-ui/index.scss";
+  /* 项目基础样式 */
+  @import "./app.scss";
+  /* iconfont图标库 */
+  @import "./utils/iconfont.scss";
+</style>

+ 191 - 0
LICENSE

@@ -0,0 +1,191 @@
+Apache License
+Version 2.0, January 2004
+http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+"License" shall mean the terms and conditions for use, reproduction, and
+distribution as defined by Sections 1 through 9 of this document.
+
+"Licensor" shall mean the copyright owner or entity authorized by the copyright
+owner that is granting the License.
+
+"Legal Entity" shall mean the union of the acting entity and all other entities
+that control, are controlled by, or are under common control with that entity.
+For the purposes of this definition, "control" means (i) the power, direct or
+indirect, to cause the direction or management of such entity, whether by
+contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
+outstanding shares, or (iii) beneficial ownership of such entity.
+
+"You" (or "Your") shall mean an individual or Legal Entity exercising
+permissions granted by this License.
+
+"Source" form shall mean the preferred form for making modifications, including
+but not limited to software source code, documentation source, and configuration
+files.
+
+"Object" form shall mean any form resulting from mechanical transformation or
+translation of a Source form, including but not limited to compiled object code,
+generated documentation, and conversions to other media types.
+
+"Work" shall mean the work of authorship, whether in Source or Object form, made
+available under the License, as indicated by a copyright notice that is included
+in or attached to the work (an example is provided in the Appendix below).
+
+"Derivative Works" shall mean any work, whether in Source or Object form, that
+is based on (or derived from) the Work and for which the editorial revisions,
+annotations, elaborations, or other modifications represent, as a whole, an
+original work of authorship. For the purposes of this License, Derivative Works
+shall not include works that remain separable from, or merely link (or bind by
+name) to the interfaces of, the Work and Derivative Works thereof.
+
+"Contribution" shall mean any work of authorship, including the original version
+of the Work and any modifications or additions to that Work or Derivative Works
+thereof, that is intentionally submitted to Licensor for inclusion in the Work
+by the copyright owner or by an individual or Legal Entity authorized to submit
+on behalf of the copyright owner. For the purposes of this definition,
+"submitted" means any form of electronic, verbal, or written communication sent
+to the Licensor or its representatives, including but not limited to
+communication on electronic mailing lists, source code control systems, and
+issue tracking systems that are managed by, or on behalf of, the Licensor for
+the purpose of discussing and improving the Work, but excluding communication
+that is conspicuously marked or otherwise designated in writing by the copyright
+owner as "Not a Contribution."
+
+"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
+of whom a Contribution has been received by Licensor and subsequently
+incorporated within the Work.
+
+2. Grant of Copyright License.
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable copyright license to reproduce, prepare Derivative Works of,
+publicly display, publicly perform, sublicense, and distribute the Work and such
+Derivative Works in Source or Object form.
+
+3. Grant of Patent License.
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable (except as stated in this section) patent license to make, have
+made, use, offer to sell, sell, import, and otherwise transfer the Work, where
+such license applies only to those patent claims licensable by such Contributor
+that are necessarily infringed by their Contribution(s) alone or by combination
+of their Contribution(s) with the Work to which such Contribution(s) was
+submitted. If You institute patent litigation against any entity (including a
+cross-claim or counterclaim in a lawsuit) alleging that the Work or a
+Contribution incorporated within the Work constitutes direct or contributory
+patent infringement, then any patent licenses granted to You under this License
+for that Work shall terminate as of the date such litigation is filed.
+
+4. Redistribution.
+
+You may reproduce and distribute copies of the Work or Derivative Works thereof
+in any medium, with or without modifications, and in Source or Object form,
+provided that You meet the following conditions:
+
+You must give any other recipients of the Work or Derivative Works a copy of
+this License; and
+You must cause any modified files to carry prominent notices stating that You
+changed the files; and
+You must retain, in the Source form of any Derivative Works that You distribute,
+all copyright, patent, trademark, and attribution notices from the Source form
+of the Work, excluding those notices that do not pertain to any part of the
+Derivative Works; and
+If the Work includes a "NOTICE" text file as part of its distribution, then any
+Derivative Works that You distribute must include a readable copy of the
+attribution notices contained within such NOTICE file, excluding those notices
+that do not pertain to any part of the Derivative Works, in at least one of the
+following places: within a NOTICE text file distributed as part of the
+Derivative Works; within the Source form or documentation, if provided along
+with the Derivative Works; or, within a display generated by the Derivative
+Works, if and wherever such third-party notices normally appear. The contents of
+the NOTICE file are for informational purposes only and do not modify the
+License. You may add Your own attribution notices within Derivative Works that
+You distribute, alongside or as an addendum to the NOTICE text from the Work,
+provided that such additional attribution notices cannot be construed as
+modifying the License.
+You may add Your own copyright statement to Your modifications and may provide
+additional or different license terms and conditions for use, reproduction, or
+distribution of Your modifications, or for any such Derivative Works as a whole,
+provided Your use, reproduction, and distribution of the Work otherwise complies
+with the conditions stated in this License.
+
+5. Submission of Contributions.
+
+Unless You explicitly state otherwise, any Contribution intentionally submitted
+for inclusion in the Work by You to the Licensor shall be under the terms and
+conditions of this License, without any additional terms or conditions.
+Notwithstanding the above, nothing herein shall supersede or modify the terms of
+any separate license agreement you may have executed with Licensor regarding
+such Contributions.
+
+6. Trademarks.
+
+This License does not grant permission to use the trade names, trademarks,
+service marks, or product names of the Licensor, except as required for
+reasonable and customary use in describing the origin of the Work and
+reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty.
+
+Unless required by applicable law or agreed to in writing, Licensor provides the
+Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
+including, without limitation, any warranties or conditions of TITLE,
+NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
+solely responsible for determining the appropriateness of using or
+redistributing the Work and assume any risks associated with Your exercise of
+permissions under this License.
+
+8. Limitation of Liability.
+
+In no event and under no legal theory, whether in tort (including negligence),
+contract, or otherwise, unless required by applicable law (such as deliberate
+and grossly negligent acts) or agreed to in writing, shall any Contributor be
+liable to You for damages, including any direct, indirect, special, incidental,
+or consequential damages of any character arising as a result of this License or
+out of the use or inability to use the Work (including but not limited to
+damages for loss of goodwill, work stoppage, computer failure or malfunction, or
+any and all other commercial damages or losses), even if such Contributor has
+been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability.
+
+While redistributing the Work or Derivative Works thereof, You may choose to
+offer, and charge a fee for, acceptance of support, warranty, indemnity, or
+other liability obligations and/or rights consistent with this License. However,
+in accepting such obligations, You may act only on Your own behalf and on Your
+sole responsibility, not on behalf of any other Contributor, and only if You
+agree to indemnify, defend, and hold each Contributor harmless for any liability
+incurred by, or claims asserted against, such Contributor by reason of your
+accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work
+
+To apply the Apache License to your work, attach the following boilerplate
+notice, with the fields enclosed by brackets "{}" replaced with your own
+identifying information. (Don't include the brackets!) The text should be
+enclosed in the appropriate comment syntax for the file format. We also
+recommend that a file or class name and description of purpose be included on
+the same "printed page" as the copyright notice for easier identification within
+third-party archives.
+
+   Copyright 2018 萤火科技
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

+ 69 - 0
README.md

@@ -0,0 +1,69 @@
+# 萤火商城V2.0开源版 [uni-app端]
+
+#### 项目介绍
+萤火商城V2.0,是2021年全新推出的一款轻量级、高性能、前后端分离的电商系统,支持微信小程序 + H5+ 公众号 + APP,前后端源码完全开源,看见及所得,完美支持二次开发,可学习可商用,让您快速搭建个性化独立商城。
+
+    如果对您有帮助,您可以点右上角 “Star” 收藏一下 ,获取第一时间更新,谢谢!
+
+#### 源码下载
+1. 主商城端(又称后端、服务端,PHP开发 用于管理后台和提供api接口)
+
+    下载地址:[https://gitee.com/xany/yoshop2.0](https://gitee.com/xany/yoshop2.0)
+
+2. 用户端(也叫客户端、前端,uniapp开发 用于生成H5和微信小程序)
+
+    下载地址:[https://gitee.com/xany/yoshop2.0-uniapp](https://gitee.com/xany/yoshop2.0-uniapp)
+
+#### 如何使用uni-app端
+
+##### 一、导入uniapp项目
+
+    1. 首先下载HBuilderX并安装,地址:https://www.dcloud.io/hbuilderx.html
+    2. 打开HBuilderX -> 顶部菜单栏 -> 文件 -> 导入 -> 从本地目录导入 -> 选择uniapp端项目目录
+    3. 找到config.js文件,找到里面的apiUrl项,填入已搭建的后端url地址
+    4. 打开manifest.json文件,选择微信小程序配置,填写小程序的appid
+
+##### 二、本地调试
+
+    1. 打开HBuilderX -> 顶部菜单栏 -> 运行 -> 运行到浏览器 -> Chrome
+    2. 如果请求后端api时 提示跨域错误,可安装Chrome插件:【Allow CORS: Access-Control-Allow-Origin】,地址:https://chrome.google.com/webstore/detail/allow-cors-access-control/lhobafahddgcelffkeicbaginigeejlf
+
+##### 三、打包发行(H5)
+
+    1. 打开HBuilderX -> 顶部菜单栏 -> 发行 -> 网站H5-手机版
+    2. 打包后的文件路径:/unpackage/dist/build/h5
+    3. 将打包完成的所有文件 复制到商城后端/pulic目录下,全部替换
+
+##### 四、打包发行(微信小程序)
+
+    1. 下载微信开发者工具并安装,地址:https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html
+    2. 打开HBuilderX -> 顶部菜单栏 -> 发行 -> 小程序-微信
+    3. 打包后的文件路径:/unpackage/dist/build/mp-weixin
+    5. 打开微信开发者工具 导入 打包完成的项目
+    6. 检查没有运行错误,在右上方上传小程序
+
+
+#### UNIAPP-页面展示
+![前端展示](https://images.gitee.com/uploads/images/2021/0316/215102_7bcb0802_2166072.png "前端展示.png")
+
+#### 系统演示
+![前端演示二维码](https://images.gitee.com/uploads/images/2021/0316/104516_3778337e_2166072.png "111.png")
+
+
+
+
+#### 版权须知
+
+1. 允许个人学习研究使用,支持二次开发,允许商业用途(仅限自运营)。
+2. 允许商业用途,但仅限自运营,如果商用必须保留版权信息,望自觉遵守。
+3. 不允许对程序代码以任何形式任何目的的再发行或出售,否则将追究侵权者法律责任。
+
+
+本项目包含的第三方源码和二进制文件之版权信息另行标注。
+
+版权所有Copyright © 2017-2021 By 萤火科技 (https://www.yiovo.com) All rights reserved。
+
+
+
+
+

+ 14 - 0
androidPrivacy.json

@@ -0,0 +1,14 @@
+{
+    "version" : "1",
+    "prompt" : "template",
+    "title" : "服务协议和隐私政策",
+    "message" : "  请你务必审慎阅读、充分理解“服务协议”和“隐私政策”各条款,包括但不限于:为了更好的向你提供服务,我们需要收集你的设备标识、操作日志等信息用于分析、优化应用性能。<br/>  你可阅读<a href=\"static/protocol.html\">《服务协议》</a>和<a href=\"static/privacy.html\">《隐私政策》</a>了解详细信息。如果你同意,请点击下面按钮开始接受我们的服务。",
+    "buttonAccept" : "同意并接受",
+    "buttonRefuse" : "暂不同意",
+    "second" : {
+        "title" : "确认提示",
+        "message" : "  进入应用前,你需先同意<a href=\"static/protocol.html\">《服务协议》</a>和<a href=\"static/privacy.html\">《隐私政策》</a>,否则将退出应用。",
+        "buttonAccept" : "同意并继续",
+        "buttonRefuse" : "退出应用"
+    }
+}

+ 47 - 0
api/address.js

@@ -0,0 +1,47 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  list: 'address/list',
+  defaultId: 'address/defaultId',
+  detail: 'address/detail',
+  add: 'address/add',
+  edit: 'address/edit',
+  setDefault: 'address/setDefault',
+  remove: 'address/remove'
+}
+
+// 收货地址列表
+export const list = (param) => {
+  return request.get(api.list, param)
+}
+
+// 默认收货地址ID
+export const defaultId = (param) => {
+  return request.get(api.defaultId, param)
+}
+
+// 收货地址详情
+export const detail = (addressId) => {
+  return request.get(api.detail, { addressId })
+}
+
+// 新增收货地址
+export const add = (data) => {
+  return request.post(api.add, { form: data })
+}
+
+// 编辑收货地址
+export const edit = (addressId, data) => {
+  return request.post(api.edit, { addressId, form: data })
+}
+
+// 设置默认收货地址
+export const setDefault = (addressId) => {
+  return request.post(api.setDefault, { addressId })
+}
+
+// 删除收货地址
+export const remove = (addressId) => {
+  return request.post(api.remove, { addressId })
+}

+ 11 - 0
api/article/category.js

@@ -0,0 +1,11 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  list: 'article.category/list'
+}
+
+// 分类列表
+export function list() {
+  return request.get(api.list)
+}

+ 17 - 0
api/article/index.js

@@ -0,0 +1,17 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  list: 'article/list',
+  detail: 'article/detail'
+}
+
+// 文章列表
+export function list(param, option) {
+  return request.get(api.list, param, option)
+}
+
+// 文章详情
+export function detail(articleId) {
+  return request.get(api.detail, { articleId })
+}

+ 11 - 0
api/balance.js

@@ -0,0 +1,11 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  list: 'balance.log/list'
+}
+
+// 余额账单明细列表
+export const list = (param) => {
+  return request.get(api.list, param)
+}

+ 11 - 0
api/balance/log.js

@@ -0,0 +1,11 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  list: 'balance.log/list'
+}
+
+// 余额账单明细
+export const list = (param) => {
+  return request.get(api.list, param)
+}

+ 17 - 0
api/captcha.js

@@ -0,0 +1,17 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  image: 'captcha/image',
+  sendSmsCaptcha: 'captcha/sendSmsCaptcha'
+}
+
+// 图形验证码
+export function image() {
+  return request.get(api.image, {}, { load: false })
+}
+
+// 发送短信验证码
+export function sendSmsCaptcha(data) {
+  return request.post(api.sendSmsCaptcha, data)
+}

+ 35 - 0
api/cart.js

@@ -0,0 +1,35 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  list: 'cart/list',
+  total: 'cart/total',
+  add: 'cart/add',
+  update: 'cart/update',
+  clear: 'cart/clear'
+}
+
+// 购物车列表
+export const list = () => {
+  return request.get(api.list, {}, { load: false })
+}
+
+// 购物车商品总数量
+export const total = () => {
+  return request.get(api.total, {}, { load: false })
+}
+
+// 加入购物车
+export const add = (goodsId, goodsSkuId, goodsNum) => {
+  return request.post(api.add, { goodsId, goodsSkuId, goodsNum })
+}
+
+// 更新购物车商品数量
+export const update = (goodsId, goodsSkuId, goodsNum) => {
+  return request.post(api.update, { goodsId, goodsSkuId, goodsNum }, { isPrompt: false })
+}
+
+// 删除购物车中指定记录
+export const clear = (cartIds = []) => {
+  return request.post(api.clear, { cartIds })
+}

+ 35 - 0
api/cashier/index.js

@@ -0,0 +1,35 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  orderInfo: 'cashier/orderInfo',
+  orderPay: 'cashier/orderPay',
+  tradeQuery: 'cashier/tradeQuery',
+}
+
+/**
+ * 获取支付订单的信息
+ * @param {Number} orderId
+ * @param {Object} param
+ */
+export function orderInfo(orderId, param) {
+  return request.get(api.orderInfo, { orderId, ...param })
+}
+
+/**
+ * 确认支付
+ * @param {Number} orderId
+ * @param {Object} data
+ */
+export function orderPay(orderId, data) {
+  return request.post(api.orderPay, { orderId, ...data })
+}
+
+/**
+ * 交易查询
+ * 查询第三方支付订单是否付款成功
+ * @param {Object} param
+ */
+export function tradeQuery(param) {
+  return request.get(api.tradeQuery, param)
+}

+ 11 - 0
api/category/index.js

@@ -0,0 +1,11 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  list: 'category/list'
+}
+
+// 页面数据
+export function list() {
+  return request.get(api.list)
+}

+ 19 - 0
api/checkout.js

@@ -0,0 +1,19 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  order: 'checkout/order',
+  submit: 'checkout/submit',
+}
+
+// mode: 结算模式 (buyNow立即购买 cart购物车)
+
+// 结算台订单信息
+export const order = (mode, param) => {
+  return request.get(api.order, { mode, ...param })
+}
+
+// 结算台订单提交
+export const submit = (mode, data) => {
+  return request.post(api.submit, { mode, ...data }, { isPrompt: false })
+}

+ 23 - 0
api/comment.js

@@ -0,0 +1,23 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  list: 'comment/list',
+  listRows: 'comment/listRows',
+  total: 'comment/total'
+}
+
+// 商品评价列表
+export const list = (goodsId, param, option) => {
+  return request.get(api.list, { ...param, goodsId }, option)
+}
+
+// 商品评价列表 (限制数量, 用于商品详情页展示)
+export const listRows = (goodsId, limit = 5) => {
+  return request.get(api.listRows, { goodsId, limit })
+}
+
+// 商品评分总数
+export const total = (goodsId) => {
+  return request.get(api.total, { goodsId })
+}

+ 16 - 0
api/coupon.js

@@ -0,0 +1,16 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  list: 'coupon/list'
+}
+
+// 优惠券列表
+export const list = (param, option) => {
+  const options = {
+    isPrompt: true, //(默认 true 说明:本接口抛出的错误是否提示)
+    load: true, //(默认 true 说明:本接口是否提示加载动画)
+    ...option
+  }
+  return request.get(api.list, param, options)
+}

+ 11 - 0
api/express.js

@@ -0,0 +1,11 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  list: 'express/list'
+}
+
+// 物流公司列表
+export const list = (param) => {
+  return request.get(api.list, param)
+}

+ 37 - 0
api/goods/index.js

@@ -0,0 +1,37 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  list: 'goods/list',
+  detail: 'goods/detail',
+  basic: 'goods/basic',
+  specData: 'goods/specData',
+  skuInfo: 'goods/skuInfo'
+}
+
+// 商品列表
+export const list = (param, option) => {
+  return request.get(api.list, param, option)
+}
+
+// 商品详情(详细数据)
+export const detail = (goodsId, verifyStatus = true, param = {}) => {
+  verifyStatus = Number(verifyStatus)
+  return request.get(api.detail, { goodsId, verifyStatus, ...param })
+}
+
+// 商品详情(基本数据)
+export const basic = (goodsId, verifyStatus = true, param = {}) => {
+  verifyStatus = Number(verifyStatus)
+  return request.get(api.basic, { goodsId, verifyStatus, ...param })
+}
+
+// 获取商品规格数据
+export const specData = (goodsId) => {
+  return request.get(api.specData, { goodsId })
+}
+
+// 获取商品的指定SKU信息
+export const skuInfo = (goodsId, goodsSkuId, param) => {
+  return request.get(api.skuInfo, { goodsId, goodsSkuId, ...param })
+}

+ 11 - 0
api/goods/service.js

@@ -0,0 +1,11 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  list: 'goods.service/list'
+}
+
+// 商品评价列表
+export function list(goodsId) {
+  return request.get(api.list, { goodsId })
+}

+ 11 - 0
api/help.js

@@ -0,0 +1,11 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  list: 'help/list'
+}
+
+// 帮助中心列表
+export const list = (param) => {
+  return request.get(api.list, param)
+}

+ 29 - 0
api/login/index.js

@@ -0,0 +1,29 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  login: 'passport/login',
+  loginMpWx: 'passport/loginMpWx',
+  loginMpWxMobile: 'passport/loginMpWxMobile',
+  isPersonalMpweixin: 'passport/isPersonalMpweixin',
+}
+
+// 用户登录 (手机号+验证码)
+export function login(data) {
+  return request.post(api.login, data)
+}
+
+// 微信小程序快捷登录 (获取微信用户基本信息)
+export function loginMpWx(data, option) {
+  return request.post(api.loginMpWx, data, option)
+}
+
+// 是否需要填写昵称头像 (微信小程序端)
+export function isPersonalMpweixin(data, option) {
+  return request.post(api.isPersonalMpweixin, data, option)
+}
+
+// 微信小程序快捷登录 (授权手机号)
+export function loginMpWxMobile(data, option) {
+  return request.post(api.loginMpWxMobile, data, option)
+}

+ 22 - 0
api/myCoupon.js

@@ -0,0 +1,22 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  list: 'myCoupon/list',
+  receive: 'myCoupon/receive'
+}
+
+// 我的优惠券列表
+export const list = (param, option) => {
+  const options = {
+    isPrompt: true, //(默认 true 说明:本接口抛出的错误是否提示)
+    load: true, //(默认 true 说明:本接口是否提示加载动画)
+    ...option
+  }
+  return request.get(api.list, param, options)
+}
+
+// 领取优惠券
+export const receive = (couponId, data, option) => {
+  return request.post(api.receive, { couponId, ...data }, option)
+}

+ 42 - 0
api/order.js

@@ -0,0 +1,42 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  todoCounts: 'order/todoCounts',
+  list: 'order/list',
+  detail: 'order/detail',
+  express: 'order/express',
+  cancel: 'order/cancel',
+  receipt: 'order/receipt',
+  pay: 'order/pay'
+}
+
+// 当前用户待处理的订单数量
+export function todoCounts(param, option) {
+  return request.get(api.todoCounts, param, option)
+}
+
+// 我的订单列表
+export function list(param, option) {
+  return request.get(api.list, param, option)
+}
+
+// 订单详情
+export function detail(orderId, param) {
+  return request.get(api.detail, { orderId, ...param })
+}
+
+// 获取物流信息
+export function express(orderId, param) {
+  return request.get(api.express, { orderId, ...param })
+}
+
+// 取消订单
+export function cancel(orderId, data) {
+  return request.post(api.cancel, { orderId, ...data })
+}
+
+// 确认收货
+export function receipt(orderId, data) {
+  return request.post(api.receipt, { orderId, ...data })
+}

+ 17 - 0
api/order/comment.js

@@ -0,0 +1,17 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  list: 'order.comment/list',
+  submit: 'order.comment/submit'
+}
+
+// 待评价订单商品列表
+export const list = (orderId, param) => {
+  return request.get(api.list, { orderId, ...param })
+}
+
+// 创建商品评价
+export const submit = (orderId, data) => {
+  return request.post(api.submit, { orderId, form: data })
+}

+ 13 - 0
api/page.js

@@ -0,0 +1,13 @@
+import request from '@/utils/request'
+
+// api地址
+const apiUri = {
+  detail: 'page/detail'
+}
+
+// 页面数据
+export function detail(pageId) {
+  return request.get(apiUri.detail, {
+    pageId
+  })
+}

+ 11 - 0
api/points/log.js

@@ -0,0 +1,11 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  list: 'points.log/list'
+}
+
+// 积分明细列表
+export const list = (param) => {
+  return request.get(api.list, param)
+}

+ 27 - 0
api/recharge.js

@@ -0,0 +1,27 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  center: 'recharge/center',
+  submit: 'recharge/submit',
+  tradeQuery: 'recharge/tradeQuery'
+}
+
+// 充值中心页面数据
+export const center = (param) => {
+  return request.get(api.center, param)
+}
+
+// 确认充值订单
+export const submit = (data) => {
+  return request.post(api.submit, { form: data })
+}
+
+/**
+ * 交易查询
+ * 查询第三方支付订单是否付款成功
+ * @param {Object} param
+ */
+export function tradeQuery(param) {
+  return request.get(api.tradeQuery, param)
+}

+ 11 - 0
api/recharge/order.js

@@ -0,0 +1,11 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  list: 'recharge.order/list'
+}
+
+// 我的充值记录列表
+export const list = (param) => {
+  return request.get(api.list, param)
+}

+ 35 - 0
api/refund.js

@@ -0,0 +1,35 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  list: 'refund/list',
+  goods: 'refund/goods',
+  apply: 'refund/apply',
+  detail: 'refund/detail',
+  delivery: 'refund/delivery'
+}
+
+// 售后单列表
+export const list = (param, option) => {
+  return request.get(api.list, param, option)
+}
+
+// 订单商品详情
+export const goods = (orderGoodsId, param) => {
+  return request.get(api.goods, { orderGoodsId, ...param })
+}
+
+// 申请售后
+export const apply = (orderGoodsId, data) => {
+  return request.post(api.apply, { orderGoodsId, form: data })
+}
+
+// 售后单详情
+export const detail = (orderRefundId, param) => {
+  return request.get(api.detail, { orderRefundId, ...param })
+}
+
+// 用户发货
+export const delivery = (orderRefundId, data) => {
+  return request.post(api.delivery, { orderRefundId, form: data })
+}

+ 17 - 0
api/region.js

@@ -0,0 +1,17 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  all: 'region/all',
+  tree: 'region/tree'
+}
+
+// 获取所有地区
+export const all = (param) => {
+  return request.get(api.all, param)
+}
+
+// 获取所有地区(树状)
+export const tree = (param) => {
+  return request.get(api.tree, param)
+}

+ 11 - 0
api/setting.js

@@ -0,0 +1,11 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  data: 'setting/data'
+}
+
+// 设置项详情
+export function data() {
+  return request.get(api.data)
+}

+ 13 - 0
api/store.js

@@ -0,0 +1,13 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  data: 'store/data'
+}
+
+// 获取商城基础信息
+const data = () => {
+  return request.get(api.data)
+}
+
+export default { data }

+ 18 - 0
api/upload.js

@@ -0,0 +1,18 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  image: 'upload/image'
+}
+
+// 图片上传
+export const image = (files, checkLogin = true) => {
+  // 文件上传大小, 2M
+  const maxSize = 1024 * 1024 * 2
+  // 执行上传
+  return new Promise((resolve, reject) => {
+    request.urlFileUpload({ name: 'file', files, maxSize, data: { test: 123, checkLogin: Number(checkLogin) } })
+      .then(result => resolve(result.map(item => item.data.fileInfo.file_id), result))
+      .catch(reject)
+  })
+}

+ 34 - 0
api/user.js

@@ -0,0 +1,34 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  userInfo: 'user/info',
+  assets: 'user/assets',
+  bindMobile: 'user/bindMobile',
+  personal: 'user/personal'
+}
+
+// 当前登录的用户信息
+export const info = (param, option) => {
+  const options = {
+    isPrompt: true, //(默认 true 说明:本接口抛出的错误是否提示)
+    load: true, //(默认 true 说明:本接口是否提示加载动画)
+    ...option
+  }
+  return request.get(api.userInfo, param, options)
+}
+
+// 账户资产
+export const assets = (param, option) => {
+  return request.get(api.assets, param, option)
+}
+
+// 绑定手机号
+export const bindMobile = (data, option) => {
+  return request.post(api.bindMobile, data, option)
+}
+
+// 修改个人信息(头像昵称)
+export const personal = (data, option) => {
+  return request.post(api.personal, data, option)
+}

+ 11 - 0
api/user/coupon.js

@@ -0,0 +1,11 @@
+import request from '@/utils/request'
+
+// api地址
+const api = {
+  receive: 'user.coupon/receive'
+}
+
+// 优惠券列表
+export const receive = (data) => {
+  return request.post(api.receive, data)
+}

+ 43 - 0
app.scss

@@ -0,0 +1,43 @@
+/* utils.scss */
+@import "/utils/utils.scss";
+
+page {
+  background: #fafafa;
+}
+
+@keyframes rotate {
+  0% {
+    transform: rotate(0deg) scale(1);
+  }
+
+  100% {
+    transform: rotate(360deg) scale(1);
+  }
+}
+
+/* #ifdef H5*/
+
+uni-page {
+  box-shadow: 0 1rpx 44rpx rgba(169, 169, 169, .3);
+}
+
+.uni-app--showlayout+uni-tabbar.uni-tabbar-bottom,
+.uni-app--showlayout+uni-tabbar.uni-tabbar-bottom .uni-tabbar,
+.uni-app--showlayout+uni-tabbar.uni-tabbar-top,
+.uni-app--showlayout+uni-tabbar.uni-tabbar-top .uni-tabbar {
+  left: var(--window-left);
+  right: var(--window-right);
+}
+  
+.footer-fixed {
+  left: var(--window-left) !important;
+  right: var(--window-right) !important;
+}
+
+.u-mask,.u-drawer {
+  left: var(--window-left) !important;
+  right: var(--window-right) !important;
+}
+
+/* #endif */
+

+ 7 - 0
common/constant/index.js

@@ -0,0 +1,7 @@
+export const paginate = {
+  data: [], // 列表数据
+  current_page: 1, // 当前页码
+  last_page: 1, // 最大页码
+  per_page: 15, // 每页记录数
+  total: 0, // 总记录数
+}

Diff do ficheiro suprimidas por serem muito extensas
+ 0 - 0
common/data/region.js


+ 11 - 0
common/enum/Client.js

@@ -0,0 +1,11 @@
+import Enum from './enum'
+
+/**
+ * 枚举类:客户端类型
+ * ClientEnum
+ */
+export default new Enum([
+  { key: 'APP', name: 'APP端', value: 'APP' },
+  { key: 'H5', name: 'H5端', value: 'H5' },
+  { key: 'MP_WEIXIN', name: '微信小程序端', value: 'MP-WEIXIN' }
+])

+ 10 - 0
common/enum/coupon/ApplyRange.js

@@ -0,0 +1,10 @@
+import Enum from '../enum'
+
+/**
+ * 枚举类:优惠券适用范围
+ * ApplyRangeEnum
+ */
+export default new Enum([
+  { key: 'ALL', name: '全部商品', value: 10 },
+  { key: 'SOME_GOODS', name: '指定商品', value: 20 }
+])

+ 10 - 0
common/enum/coupon/CouponType.js

@@ -0,0 +1,10 @@
+import Enum from '../enum'
+
+/**
+ * 枚举类:优惠券类型
+ * CouponTypeEnum
+ */
+export default new Enum([
+  { key: 'FULL_DISCOUNT', name: '满减券', value: 10 },
+  { key: 'DISCOUNT', name: '折扣券', value: 20 }
+])

+ 10 - 0
common/enum/coupon/ExpireType.js

@@ -0,0 +1,10 @@
+import Enum from '../enum'
+
+/**
+ * 枚举类:优惠券到期类型
+ * ExpireTypeEnum
+ */
+export default new Enum([
+  { key: 'RECEIVE', name: '领取后', value: 10 },
+  { key: 'FIXED_TIME', name: '固定时间', value: 20 }
+])

+ 5 - 0
common/enum/coupon/index.js

@@ -0,0 +1,5 @@
+import ApplyRangeEnum from './ApplyRange'
+import ExpireTypeEnum from './ExpireType'
+import CouponTypeEnum from './CouponType'
+
+export { ApplyRangeEnum, CouponTypeEnum, ExpireTypeEnum }

+ 85 - 0
common/enum/enum.js

@@ -0,0 +1,85 @@
+/**
+ * 枚举类
+ * Enum.IMAGE.name                => "图片"
+ * Enum.getNameByKey('IMAGE')     => "图片"
+ * Enum.getValueByKey('IMAGE')    => 10
+ * Enum.getNameByValue(10)        => "图片"
+ * Enum.getData()                 => [{key: "IMAGE", name: "图片", value: 10}]
+ */
+class Enum {
+  constructor (param) {
+    const keyArr = []
+    const valueArr = []
+
+    if (!Array.isArray(param)) {
+      throw new Error('param is not an array!')
+    }
+
+    param.map(element => {
+      if (!element.key || !element.name) {
+        return
+      }
+      // 保存key值组成的数组,方便A.getName(name)类型的调用
+      keyArr.push(element.key)
+      valueArr.push(element.value)
+      // 根据key生成不同属性值,以便A.B.name类型的调用
+      this[element.key] = element
+      if (element.key !== element.value) {
+        this[element.value] = element
+      }
+    })
+
+
+    // 保存源数组
+    this.data = param
+    this.keyArr = keyArr
+    this.valueArr = valueArr
+
+    // 防止被修改
+    // Object.freeze(this)
+  }
+
+  // 根据key得到对象
+  keyOf (key) {
+    return this.data[this.keyArr.indexOf(key)]
+  }
+
+  // 根据key得到对象
+  valueOf (key) {
+    return this.data[this.valueArr.indexOf(key)]
+  }
+
+  // 根据key获取name值
+  getNameByKey (key) {
+    const prop = this.keyOf(key)
+    if (!prop) {
+      throw new Error('No enum constant' + key)
+    }
+    return prop.name
+  }
+
+  // 根据value获取name值
+  getNameByValue (value) {
+    const prop = this.valueOf(value)
+    if (!prop) {
+      throw new Error('No enum constant' + value)
+    }
+    return prop.name
+  }
+
+  // 根据key获取value值
+  getValueByKey (key) {
+    const prop = this.keyOf(key)
+    if (!prop) {
+      throw new Error('No enum constant' + key)
+    }
+    return prop.key
+  }
+
+  // 返回源数组
+  getData () {
+    return this.data
+  }
+}
+
+export default Enum

+ 10 - 0
common/enum/goods/GoodsType.js

@@ -0,0 +1,10 @@
+import Enum from '../enum'
+
+/**
+ * 枚举类:商品类型
+ * GoodsTypeEnum
+ */
+export default new Enum([
+  { key: 'PHYSICAL', name: '实物商品', value: 10 },
+  { key: 'VIRTUAL', name: '虚拟商品', value: 20 }
+])

+ 10 - 0
common/enum/goods/SpecType.js

@@ -0,0 +1,10 @@
+import Enum from '../enum'
+
+/**
+ * 枚举类:商品规格类型
+ * SpecTypeEnum
+ */
+export default new Enum([
+  { key: 'SINGLE', name: '单规格', value: 10 },
+  { key: 'MULTI', name: '多规格', value: 20 }
+])

+ 4 - 0
common/enum/goods/index.js

@@ -0,0 +1,4 @@
+import GoodsTypeEnum from './GoodsType'
+import SpecTypeEnum from './SpecType'
+
+export { GoodsTypeEnum, SpecTypeEnum }

+ 11 - 0
common/enum/order/DeliveryStatus.js

@@ -0,0 +1,11 @@
+import Enum from '../enum'
+
+/**
+ * 枚举类:订单发货状态
+ * DeliveryStatusEnum
+ */
+export default new Enum([
+  { key: 'NOT_DELIVERED', name: '未发货', value: 10 },
+  { key: 'DELIVERED', name: '已发货', value: 20 },
+  { key: 'PART_DELIVERED', name: '部分发货', value: 30 }
+])

+ 10 - 0
common/enum/order/DeliveryType.js

@@ -0,0 +1,10 @@
+import Enum from '../enum'
+
+/**
+ * 枚举类:配送方式
+ * DeliveryTypeEnum
+ */
+export default new Enum([
+  { key: 'EXPRESS', name: '快递配送', value: 10 },
+  { key: 'NOTHING', name: '无需配送', value: 30 }
+])

+ 9 - 0
common/enum/order/OrderSource.js

@@ -0,0 +1,9 @@
+import Enum from '../enum'
+
+/**
+ * 枚举类:订单来源
+ * OrderSourceEnum
+ */
+export default new Enum([
+  { key: 'MAIN', name: '普通订单', value: 10 }
+])

+ 12 - 0
common/enum/order/OrderStatus.js

@@ -0,0 +1,12 @@
+import Enum from '../enum'
+
+/**
+ * 枚举类:订单状态
+ * OrderStatusEnum
+ */
+export default new Enum([
+  { key: 'NORMAL', name: '进行中', value: 10 },
+  { key: 'CANCELLED', name: '已取消', value: 20 },
+  { key: 'APPLY_CANCEL', name: '待取消', value: 21 },
+  { key: 'COMPLETED', name: '已完成', value: 30 }
+])

+ 10 - 0
common/enum/order/OrderType.js

@@ -0,0 +1,10 @@
+import Enum from '../enum'
+
+/**
+ * 枚举类:订单类型
+ * OrderTypeEnum
+ */
+export default new Enum([
+  { key: 'PHYSICAL', name: '实物订单', value: 10 },
+  { key: 'VIRTUAL', name: '虚拟订单', value: 20 }
+])

+ 10 - 0
common/enum/order/PayStatus.js

@@ -0,0 +1,10 @@
+import Enum from '../enum'
+
+/**
+ * 枚举类:订单支付状态
+ * PayStatusEnum
+ */
+export default new Enum([
+  { key: 'PENDING', name: '待支付', value: 10 },
+  { key: 'SUCCESS', name: '已支付', value: 20 }
+])

+ 10 - 0
common/enum/order/ReceiptStatus.js

@@ -0,0 +1,10 @@
+import Enum from '../enum'
+
+/**
+ * 枚举类:订单收货状态
+ * ReceiptStatusEnum
+ */
+export default new Enum([
+  { key: 'NOT_RECEIVED', name: '未收货', value: 10 },
+  { key: 'RECEIVED', name: '已收货', value: 20 }
+])

+ 11 - 0
common/enum/order/delivery/DeliveryMethod.js

@@ -0,0 +1,11 @@
+import Enum from '../../enum'
+
+/**
+ * 枚举类:发货方式
+ * DeliveryMethodEnum
+ */
+export default new Enum([
+  { key: 'MANUAL', name: '手动发货', value: 10 },
+  { key: 'UNWANTED', name: '无需物流', value: 20 },
+  { key: 'EORDER', name: '电子面单', value: 30 }
+])

+ 3 - 0
common/enum/order/delivery/index.js

@@ -0,0 +1,3 @@
+import DeliveryMethodEnum from './DeliveryMethod'
+
+export { DeliveryMethodEnum }

+ 17 - 0
common/enum/order/index.js

@@ -0,0 +1,17 @@
+import DeliveryStatusEnum from './DeliveryStatus'
+import DeliveryTypeEnum from './DeliveryType'
+import OrderSourceEnum from './OrderSource'
+import OrderStatusEnum from './OrderStatus'
+import PayStatusEnum from './PayStatus'
+import ReceiptStatusEnum from './ReceiptStatus'
+import OrderTypeEnum from './OrderType'
+
+export {
+    DeliveryStatusEnum,
+    DeliveryTypeEnum,
+    OrderSourceEnum,
+    OrderStatusEnum,
+    PayStatusEnum,
+    ReceiptStatusEnum,
+    OrderTypeEnum
+}

+ 11 - 0
common/enum/order/refund/AuditStatus.js

@@ -0,0 +1,11 @@
+import Enum from '../../enum'
+
+/**
+ * 枚举类:商家审核状态
+ * AuditStatusEnum
+ */
+export default new Enum([
+  { key: 'WAIT', name: '待审核', value: 0 },
+  { key: 'REVIEWED', name: '已同意', value: 10 },
+  { key: 'REJECTED', name: '已拒绝', value: 20 }
+])

+ 12 - 0
common/enum/order/refund/RefundStatus.js

@@ -0,0 +1,12 @@
+import Enum from '../../enum'
+
+/**
+ * 枚举类:售后单状态
+ * RefundStatusEnum
+ */
+export default new Enum([
+  { key: 'NORMAL', name: '进行中', value: 0 },
+  { key: 'REJECTED', name: '已拒绝', value: 10 },
+  { key: 'COMPLETED', name: '已完成', value: 20 },
+  { key: 'CANCELLED', name: '已取消', value: 30 }
+])

+ 10 - 0
common/enum/order/refund/RefundType.js

@@ -0,0 +1,10 @@
+import Enum from '../../enum'
+
+/**
+ * 枚举类:售后类型
+ * RefundTypeEnum
+ */
+export default new Enum([
+  { key: 'RETURN', name: '退货退款', value: 10 },
+  { key: 'EXCHANGE', name: '换货', value: 20 }
+])

+ 9 - 0
common/enum/order/refund/index.js

@@ -0,0 +1,9 @@
+import AuditStatusEnum from './AuditStatus'
+import RefundStatusEnum from './RefundStatus'
+import RefundTypeEnum from './RefundType'
+
+export {
+    AuditStatusEnum,
+    RefundStatusEnum,
+    RefundTypeEnum
+}

+ 11 - 0
common/enum/payment/Method.js

@@ -0,0 +1,11 @@
+import Enum from '../enum'
+
+/**
+ * 枚举类:支付方式类型
+ * PayMethodEnum
+ */
+export default new Enum([
+  { key: 'WECHAT', name: '微信支付', value: 'wechat' },
+  { key: 'ALIPAY', name: '支付宝支付', value: 'alipay' },
+  { key: 'BALANCE', name: '余额支付', value: 'balance' }
+])

+ 3 - 0
common/enum/payment/index.js

@@ -0,0 +1,3 @@
+import PayMethodEnum from './Method'
+
+export { PayMethodEnum }

+ 37 - 0
common/enum/setting/Key.js

@@ -0,0 +1,37 @@
+import Enum from '../enum'
+
+/**
+ * 枚举类:设置项索引
+ * SettingKeyEnum
+ */
+export default new Enum([{
+    key: 'REGISTER',
+    name: '账户注册设置',
+    value: 'register'
+  },
+  {
+    key: 'APP_THEME',
+    name: '店铺页面风格',
+    value: 'app_theme'
+  },
+  {
+    key: 'PAGE_CATEGORY_TEMPLATE',
+    name: '分类页模板',
+    value: 'page_category_template'
+  },
+  {
+    key: 'POINTS',
+    name: '积分设置',
+    value: 'points'
+  },
+  {
+    key: 'RECHARGE',
+    name: '充值设置',
+    value: 'recharge'
+  },
+  {
+    key: 'CUSTOMER',
+    name: '商城客服设置',
+    value: 'customer'
+  }
+])

+ 12 - 0
common/enum/store/page/category/Style.js

@@ -0,0 +1,12 @@
+import Enum from '../../../enum'
+
+/**
+ * 枚举类:地址类型
+ * PageCategoryStyleEnum
+ */
+export default new Enum([
+  { key: 'ONE_LEVEL_BIG', name: '一级分类[大图]', value: 10 },
+  { key: 'ONE_LEVEL_SMALL', name: '一级分类[小图]', value: 11 },
+  { key: 'TWO_LEVEL', name: '二级分类', value: 20 },
+  { key: 'COMMODITY', name: '一级分类+商品', value: 30 }
+])

+ 3 - 0
common/enum/store/page/category/index.js

@@ -0,0 +1,3 @@
+import PageCategoryStyleEnum from './Style'
+
+export { PageCategoryStyleEnum }

+ 77 - 0
common/model/Region.js

@@ -0,0 +1,77 @@
+import * as Api from '@/api/region'
+import storage from '@/utils/storage'
+import regionData from '../data/region'
+
+const REGION_TREE = 'region_tree'
+
+/**
+ * 商品分类 model类
+ * RegionModel
+ */
+export default {
+
+  // 从服务端获取全部地区数据(树状)
+  getTreeDataFromApi() {
+    return new Promise((resolve, reject) => {
+      Api.tree().then(result => resolve(result.data.list))
+    })
+  },
+
+  // 获取所有地区(树状) 从storage中获取
+  getTreeData() {
+    return new Promise((resolve, reject) => {
+      // 判断缓存中是否存在
+      const data = storage.get(REGION_TREE)
+      // 从服务端获取全部地区数据
+      if (data) {
+        resolve(data)
+      } else {
+        this.getTreeDataFromApi().then(list => {
+          // 缓存24小时
+          storage.set(REGION_TREE, list, 24 * 60 * 60)
+          resolve(list)
+        })
+      }
+    })
+  },
+
+  // 同步获取所有地区(树状)
+  getTreeDataSync() {
+    return regionData
+  },
+
+  // 获取所有地区的总数
+  getCitysCount() {
+    return new Promise((resolve, reject) => {
+      // 获取所有地区(树状)
+      this.getTreeData().then(data => {
+        const cityIds = []
+        // 遍历省份
+        for (const pidx in data) {
+          const province = data[pidx]
+          // 遍历城市
+          for (const cidx in province.city) {
+            const cityItem = province.city[cidx]
+            cityIds.push(cityItem.id)
+          }
+        }
+        resolve(cityIds.length)
+      })
+    })
+  },
+
+  // 同步获取所有地区的总数
+  getCitysCountSync() {
+    const regionData = this.getTreeDataSync()
+    const cityIds = []
+    for (const pidx in regionData) {
+      const province = regionData[pidx]
+      for (const cidx in province.city) {
+        const cityItem = province.city[cidx]
+        cityIds.push(cityItem.id)
+      }
+    }
+    return cityIds.length
+  }
+
+}

+ 85 - 0
common/model/Setting.js

@@ -0,0 +1,85 @@
+import store from '@/store'
+import Config from '@/core/config'
+import storage from '@/utils/storage'
+import * as SettingApi from '@/api/setting'
+import SettingKeyEnum from '@/common/enum/setting/Key'
+import platform from '@/core/platform'
+
+const CACHE_KEY = 'Setting'
+
+// 写入缓存, 到期时间10分钟
+const setStorage = data => {
+  const expireTime = 10 * 60
+  storage.set(CACHE_KEY, data, expireTime)
+}
+
+// 获取缓存中的数据
+const getStorage = () => {
+  return storage.get(CACHE_KEY)
+}
+
+// 获取后端接口商城设置 (最新)
+const getApiData = () => {
+  return new Promise((resolve, reject) => {
+    SettingApi.data()
+      .then(result => {
+        resolve(result.data.setting)
+      })
+  })
+}
+
+/**
+ * 获取商城设置
+ * 有缓存的情况下返回缓存, 没有缓存从后端api获取
+ * @param {bool} isCache 是否从缓存中获取 [优点不用每次请求后端api 缺点后台更新设置后需等待时效性]
+ */
+const data = (isCache = undefined) => {
+  if (isCache == undefined) {
+    isCache = Config.get('enabledSettingCache')
+  }
+  return new Promise((resolve, reject) => {
+    const cacheData = getStorage()
+    if (isCache && cacheData) {
+      resolve(cacheData)
+    } else {
+      getApiData().then(data => {
+        setStorage(data)
+        resolve(data)
+      })
+    }
+  })
+}
+
+// 获取商城设置(指定项)
+const item = (key, isCache = undefined) => {
+  return new Promise((resolve, reject) => {
+    data(isCache).then(setting => resolve(setting[key]))
+  })
+}
+
+// 设置全局自定义主题
+const setAppTheme = () => {
+  return new Promise((resolve, reject) => {
+    item(SettingKeyEnum.APP_THEME.value).then(appTheme => {
+      store.dispatch('SetAppTheme', appTheme)
+      resolve()
+    })
+  })
+}
+
+// 是否显示客服按钮 (微信小程序客服只有在微信小程序端显示)
+const isShowCustomerBtn = async () => {
+  const setting = await item(SettingKeyEnum.CUSTOMER.value, true)
+  if (!setting.enabled) {
+    return false
+  }
+  return setting.provider === 'mpwxkf' && platform === 'MP-WEIXIN'
+}
+
+export default {
+  setStorage,
+  data,
+  item,
+  setAppTheme,
+  isShowCustomerBtn
+}

+ 78 - 0
common/model/Store.js

@@ -0,0 +1,78 @@
+import store from '@/store'
+import StoreApi from '@/api/store'
+import storage from '@/utils/storage'
+import SettingModel from '@/common/model/Setting'
+
+const CACHE_KEY = 'Store'
+
+// 写入缓存, 到期时间10分钟
+const setStorage = data => {
+  const expireTime = 10 * 60
+  storage.set(CACHE_KEY, data, expireTime)
+}
+
+// 获取缓存中的数据
+const getStorage = () => {
+  return storage.get(CACHE_KEY)
+}
+
+// 获取后端接口商城设置 (最新)
+const getApiData = () => {
+  return new Promise((resolve, reject) => {
+    StoreApi.data().then(result => {
+      // 初始化商城数据
+      initStoreData(result.data)
+      resolve(result.data)
+    })
+  })
+}
+
+// 初始化商城数据
+const initStoreData = data => {
+  // 将商城基本信息写入缓存
+  setStorage(data)
+  // 设置商城设置缓存
+  SettingModel.setStorage(data.setting)
+  // 设置全局自定义主题
+  SettingModel.setAppTheme()
+}
+
+/**
+ * 获取商城基础信息
+ * 有缓存的情况下返回缓存, 没有缓存从后端api获取
+ */
+const data = (isCache = true) => {
+  return new Promise((resolve, reject) => {
+    const cacheData = getStorage()
+    if (isCache && cacheData) {
+      resolve(cacheData)
+    } else {
+      getApiData().then(data => {
+        resolve(data)
+      })
+    }
+  })
+}
+
+// 获取商城基本信息
+const storeInfo = () => {
+  return new Promise((resolve, reject) => {
+    data().then(data => resolve(data.storeInfo))
+  })
+}
+
+// 获取H5端访问地址
+const h5Url = () => {
+  return new Promise((resolve, reject) => {
+    data().then(data => {
+      const h5Url = data.clientData.h5.setting.baseUrl
+      resolve(h5Url)
+    })
+  })
+}
+
+export default {
+  data,
+  storeInfo,
+  h5Url
+}

+ 36 - 0
components/add-cart-btn/index.vue

@@ -0,0 +1,36 @@
+<template>
+  <view class="add-cart" @click.stop="handleAddCart">
+    <text class="icon iconfont" :class="[`icon-jiagou${btnStyle}`]"></text>
+  </view>
+</template>
+
+<script>
+  export default {
+    props: {
+      // 购物车按钮样式 1 2 3
+      btnStyle: {
+        type: Number,
+        default: 1
+      },
+    },
+    data() {
+      return {
+        value: false,
+        goodsInfo: {}
+      }
+    },
+    methods: {
+      handleAddCart() {
+        this.$emit('handleAddCart')
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .add-cart {
+    font-size: 38rpx;
+    padding: 0 20rpx;
+    color: $main-bg;
+  }
+</style>

+ 178 - 0
components/add-cart-popup/index.vue

@@ -0,0 +1,178 @@
+<template>
+  <goods-sku-popup :modelValue="visible" @input="onChangeValue" border-radius="20" :localdata="localdata" :mode="2" :maskCloseAble="true"
+    :priceColor="appTheme.mainBg" :buyNowBackgroundColor="appTheme.mainBg" :addCartColor="appTheme.viceText" :addCartBackgroundColor="appTheme.viceBg"
+    :activedStyle="{ color: appTheme.mainBg, borderColor: appTheme.mainBg, backgroundColor: activedBtnBackgroundColor }"
+    @add-cart="addCart" buyNowText="立即购买" />
+</template>
+
+<script>
+  import { setCartTotalNum } from '@/core/app'
+  import { hex2rgba } from '@/utils/color'
+  import { SpecTypeEnum } from '@/common/enum/goods'
+  import * as CartApi from '@/api/cart'
+  import * as GoodsApi from '@/api/goods'
+  import GoodsSkuPopup from '@/components/goods-sku-popup'
+
+  export default {
+    components: {
+      GoodsSkuPopup
+    },
+    props: {
+      // 购物车按钮样式 1 2 3
+      btnStyle: {
+        type: Number,
+        default: 1
+      },
+    },
+    data() {
+      return {
+        // 是否显示
+        visible: false,
+        // 主商品信息
+        goods: {},
+        // SKU商品信息
+        localdata: {}
+      }
+    },
+    computed: {
+      // 规格按钮选中时的背景色
+      activedBtnBackgroundColor() {
+        return hex2rgba(this.appTheme.mainBg, 0.1)
+      }
+    },
+    methods: {
+
+      // 加入购物车事件
+      async handle(goods) {
+        this.goods = goods
+        if (goods.spec_type == SpecTypeEnum.SINGLE.value) {
+          this.singleEvent()
+        }
+        if (goods.spec_type == SpecTypeEnum.MULTI.value) {
+          this.multiEvent()
+        }
+      },
+
+      // 单规格商品事件
+      singleEvent() {
+        const { goods } = this
+        this.addCart({
+          goods_id: goods.goods_id,
+          goods_sku_id: '0',
+          buy_num: 1
+        })
+      },
+
+      // 多规格商品事件
+      async multiEvent() {
+        const app = this
+        const { goods } = app
+        // 获取商品的规格信息
+        const { data: { specData } } = await GoodsApi.specData(goods.goods_id)
+        goods.skuList = specData.skuList
+        goods.specList = specData.specList
+        // 整理SKU商品信息
+        app.localdata = {
+          _id: goods.goods_id,
+          name: goods.goods_name,
+          goods_thumb: goods.goods_image,
+          sku_list: app.getSkuList(),
+          spec_list: app.getSpecList()
+        }
+        this.visible = true
+      },
+
+      // 监听组件显示隐藏
+      onChangeValue(val) {
+        this.visible = val
+      },
+
+      // 整理商品SKU列表 (多规格)
+      getSkuList() {
+        const app = this
+        const { goods: { goods_name, goods_image, skuList } } = app
+        const skuData = []
+        skuList.forEach(item => {
+          skuData.push({
+            _id: item.id,
+            goods_sku_id: item.goods_sku_id,
+            goods_id: item.goods_id,
+            goods_name: goods_name,
+            image: item.image_url ? item.image_url : goods_image,
+            price: item.goods_price * 100,
+            stock: item.stock_num,
+            spec_value_ids: item.spec_value_ids,
+            sku_name_arr: app.getSkuNameArr(item.spec_value_ids)
+          })
+        })
+        return skuData
+      },
+
+      // 获取sku记录的规格值列表
+      getSkuNameArr(specValueIds) {
+        const app = this
+        const defaultData = ['默认']
+        const skuNameArr = []
+        if (specValueIds) {
+          specValueIds.forEach((valueId, groupIndex) => {
+            const specValueName = app.getSpecValueName(valueId, groupIndex)
+            skuNameArr.push(specValueName)
+          })
+        }
+        return skuNameArr.length ? skuNameArr : defaultData
+      },
+
+      // 获取指定的规格值名称
+      getSpecValueName(valueId, groupIndex) {
+        const app = this
+        const { goods: { specList } } = app
+        const res = specList[groupIndex].valueList.find(specValue => {
+          return specValue.spec_value_id == valueId
+        })
+        return res.spec_value
+      },
+
+      // 整理规格数据 (多规格)
+      getSpecList() {
+        const { goods: { specList } } = this
+        const defaultData = [{ name: '默认', list: [{ name: '默认' }] }]
+        const specData = []
+        specList.forEach(group => {
+          const children = []
+          group.valueList.forEach(specValue => {
+            children.push({ name: specValue.spec_value })
+          })
+          specData.push({
+            name: group.spec_name,
+            list: children
+          })
+        })
+        return specData.length ? specData : defaultData
+      },
+
+      // 加入购物车按钮
+      addCart(selectShop) {
+        const app = this
+        const { goods_id, goods_sku_id, buy_num } = selectShop
+        CartApi.add(goods_id, goods_sku_id, buy_num)
+          .then(result => {
+            // 显示成功
+            app.$toast(result.message, 1000, false)
+            // 隐藏当前弹窗
+            app.onChangeValue(false)
+            // 购物车商品总数量
+            const cartTotal = result.data.cartTotal
+            // 缓存购物车数量
+            setCartTotalNum(cartTotal)
+            // 传递给父级
+            app.$emit('addCart', cartTotal)
+          })
+      }
+
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 57 - 0
components/avatar-image/index.vue

@@ -0,0 +1,57 @@
+<template>
+  <view class="avatar-image">
+    <image class="image"
+      :style="{ width: `${width}rpx`, height: `${width}rpx`, borderWidth: `${borderWidth}rpx`, borderColor: borderColor }"
+      :src="url ? url : '/static/default-avatar.png'"></image>
+  </view>
+</template>
+
+<script>
+  export default {
+
+    /**
+     * 组件的属性列表
+     * 用于组件自定义设置
+     */
+    props: {
+      url: {
+        type: String,
+        default: ''
+      },
+      width: {
+        type: Number,
+        default: 90
+      },
+      borderWidth: {
+        type: Number,
+        default: 0
+      },
+      borderColor: {
+        type: String,
+        default: '#000000'
+      }
+    },
+
+    data() {
+      return {
+
+      }
+    },
+
+    methods: {
+
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .avatar-image {
+    .image {
+      display: block;
+      width: 60rpx;
+      height: 60rpx;
+      border-radius: 50%;
+      border-style: solid;
+    }
+  }
+</style>

+ 194 - 0
components/countdown/index.vue

@@ -0,0 +1,194 @@
+<template>
+  <view v-if="date" class="count-down" :style="{ color: textColor, fontSize: `${fontSize}rpx` }">
+    <view :class="[`${theme}-theme`, `separator-${separator}`]">
+      <!-- <block v-if="dynamic.day != '00'">
+        <text class="dynamic-value">{{ dynamic.day }}</text>
+        <text class="separator">{{ separatorText.day }}</text>
+      </block> -->
+      <block v-if="dynamic.day > 0">
+        <text class="dynamic-value">{{ dynamic.day }}</text>
+        <text class="separator">{{ separatorText.day }}</text>
+      </block>
+      <text class="dynamic-value" :style="{ backgroundColor: customNumBgColor, color: customNumColor }">{{ dynamic.hou }}</text>
+      <text class="separator">{{ separatorText.hou }}</text>
+      <text class="dynamic-value" :style="{ backgroundColor: customNumBgColor, color: customNumColor }">{{ dynamic.min }}</text>
+      <text class="separator">{{ separatorText.min }}</text>
+      <text class="dynamic-value" :style="{ backgroundColor: customNumBgColor, color: customNumColor }">{{ dynamic.sec }}</text>
+      <text class="separator">{{ separatorText.sec }}</text>
+    </view>
+  </view>
+</template>
+
+<script>
+  import { formatDate } from '@/utils/util';
+
+  export default {
+    props: {
+      // 截止的时间
+      date: {
+        type: String,
+        default: ''
+      },
+      // 分隔符, colon为英文冒号,zh为中文
+      separator: {
+        type: String,
+        default: 'zh'
+      },
+      // 组件主题样式, text为纯文本,custom为带背景色
+      theme: {
+        type: String,
+        default: 'text'
+      },
+      // custom: 倒计时文字颜色 (分隔符和纯文本)
+      textColor: {
+        type: String,
+        default: '#303133'
+      },
+      fontSize: {
+        type: Number,
+        default: 26
+      },
+      // custom: 倒计时数字背景颜色
+      customNumBgColor: {
+        type: String,
+        default: '#252525'
+      },
+      // custom: 倒计时数字颜色
+      customNumColor: {
+        type: String,
+        default: '#fff'
+      },
+    },
+    data() {
+      return {
+        // 倒计时数据
+        dynamic: {
+          day: '0',
+          hou: '00',
+          min: '00',
+          sec: '00'
+        },
+        // 分隔符文案
+        separatorText: {
+          day: '天',
+          hou: '时',
+          min: '分',
+          sec: '秒'
+        }
+      };
+    },
+    created() {
+      // 分隔符文案
+      this.setSeparatorText();
+      // 开始倒计时
+      this.onTime();
+    },
+    methods: {
+      // 分隔符文案
+      setSeparatorText() {
+        const sText = this.separatorText;
+        if (this.separator === 'colon') {
+          sText.day = ':'
+          sText.hou = sText.min = ':'
+          sText.sec = ''
+        }
+        this.separatorText = sText
+      },
+
+      // 开始倒计时
+      onTime(deep = 0) {
+        const app = this;
+        const dynamic = {};
+
+        // 获取当前时间,同时得到活动结束时间数组
+        const newTime = new Date().getTime();
+        // 对结束时间进行处理渲染到页面
+        const endTime = new Date(formatDate(app.date)).getTime();
+
+        // 如果活动未结束,对时间进行处理
+        if (endTime - newTime <= 0) {
+          return false;
+        }
+
+        const diffTime = (endTime - newTime) / 1000;
+        // 获取时、分、秒
+        const day = parseInt(diffTime / 86400),
+          hou = parseInt((diffTime % 86400) / 3600),
+          min = parseInt(((diffTime % 86400) % 3600) / 60),
+          sec = parseInt(((diffTime % 86400) % 3600) % 60);
+        dynamic.day = day;
+        dynamic.hou = app.timeFormat(hou);
+        dynamic.min = app.timeFormat(min);
+        dynamic.sec = app.timeFormat(sec);
+
+        // 渲染,然后每隔一秒执行一次倒计时函数
+        app.dynamic = dynamic;
+        // 判断倒计时是否结束
+        const isEnd = app.isEnd();
+        // 结束后执行回调函数
+        if (isEnd) {
+          deep > 0 && app.$emit('finish');
+        }
+        // 重复执行
+        if (!isEnd) {
+          setTimeout(() => {
+            app.onTime(++deep);
+          }, 100);
+        }
+      },
+
+      // 判断倒计时是否结束
+      isEnd() {
+        const { dynamic } = this;
+        return dynamic.day == '00' && dynamic.hou == '00' && dynamic.min == '00' && dynamic.sec == '00';
+      },
+
+      // 小于10的格式化函数
+      timeFormat(value) {
+        return value < 10 ? '0' + value : value;
+      }
+    }
+  };
+</script>
+
+<style lang="scss" scoped>
+  .count-down {
+    font-size: 26rpx;
+  }
+
+  // 分隔符
+  .separator {
+    padding: 0 2rpx;
+  }
+
+  // 纯文本主题
+  .text-theme {
+
+    // 冒号分隔符
+    .separator-colon .separator {
+      padding: 0 5rpx;
+    }
+
+    .dynamic-value {
+      background: none !important;
+    }
+  }
+
+  // 背景主题
+  .custom-theme {
+    .dynamic-value {
+      display: inline-block;
+      background: #252525;
+      width: 40rpx;
+      height: 40rpx;
+      line-height: 40rpx;
+      text-align: center;
+      border-radius: 8rpx;
+      color: #fff;
+    }
+
+    .separator {
+      padding: 0 6rpx;
+    }
+  }
+</style>

+ 60 - 0
components/customer-btn/index.vue

@@ -0,0 +1,60 @@
+<template>
+  <view v-if="isShow">
+    <!-- 微信小程序端、H5端、APP端 -->
+    <!-- #ifdef MP-WEIXIN || H5 || APP-PLUS -->
+    <button class="btn-normal" :open-type="setting.provider == 'mpwxkf' ? 'contact' : ''" :show-message-card="showCard"
+      :send-message-path="cardPath" :send-message-title="cardTitle" :send-message-img="cardImage" @click="handleContact()">
+      <slot></slot>
+    </button>
+    <!-- #endif -->
+  </view>
+</template>
+
+<script>
+  import SettingKeyEnum from '@/common/enum/setting/Key'
+  import SettingModel from '@/common/model/Setting'
+
+  export default {
+    props: {
+      // 是否显示消息卡片
+      showCard: {
+        Type: Boolean,
+        default: false
+      },
+      // 消息卡片标题
+      cardTitle: {
+        Type: String,
+        default: ''
+      },
+      // 消息卡片图片
+      cardImage: {
+        Type: String,
+        default: ''
+      },
+      // 消息卡片点击跳转的路径
+      cardPath: {
+        Type: String,
+        default: ''
+      },
+    },
+    data() {
+      return {
+        isShow: false,
+        setting: {}
+      }
+    },
+    async created() {
+      // 是否显示在线客服按钮
+      this.isShow = await SettingModel.isShowCustomerBtn()
+      // 商城客服设置
+      this.setting = await SettingModel.item(SettingKeyEnum.CUSTOMER.value, true)
+    },
+    methods: {
+
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 67 - 0
components/empty/index.vue

@@ -0,0 +1,67 @@
+<template>
+  <view v-if="!isLoading" class="empty-content" :style="customStyle">
+    <view class="empty-icon">
+      <image class="image" src="/static/empty.png" mode="scaleToFill"></image>
+    </view>
+    <view class="tips">{{ tips }}</view>
+    <slot name="slot"></slot>
+  </view>
+</template>
+
+<script>
+  export default {
+
+    /**
+     * 组件的属性列表
+     * 用于组件自定义设置
+     */
+    props: {
+      // 正在加载
+      isLoading: {
+        type: Boolean,
+        default: false
+      },
+      // 自定义样式
+      customStyle: {
+        type: Object,
+        default () {
+          return {}
+        }
+      },
+      // 提示的问题
+      tips: {
+        type: String,
+        default: '亲,暂无相关数据'
+      }
+    },
+
+    data() {
+      return {}
+    },
+
+    methods: {
+
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .empty-content {
+    box-sizing: border-box;
+    width: 100%;
+    padding: 140rpx 50rpx;
+    text-align: center;
+
+    .tips {
+      font-size: 28rpx;
+      color: gray;
+      margin: 50rpx 0;
+    }
+
+    .empty-icon .image {
+      width: 280rpx;
+      height: 184rpx;
+    }
+
+  }
+</style>

+ 1389 - 0
components/goods-sku-popup/index.vue

@@ -0,0 +1,1389 @@
+<template>
+  <view class="goods-sku-popup" catchtouchmove="true" :class="valueCom && complete ? 'show' : 'none'" @touchmove.stop.prevent="moveHandle"
+    @click.stop="stop">
+    <!-- 页面内容开始 -->
+    <view class="mask" @click="close('mask')"></view>
+    <view class="layer attr-content" :class="{'safe-area-inset-bottom':safeAreaInsetBottom }"
+      :style="{ borderRadius: borderRadius + 'rpx ' + borderRadius + 'rpx 0 0' }">
+      <view class="specification-wrapper">
+        <scroll-view class="specification-wrapper-content" scroll-y="true">
+          <view class="specification-header">
+            <view class="specification-left">
+              <image class="product-img" :src="selectShop.image ? selectShop.image : goodsInfo[goodsThumbName]"
+                :style="{ backgroundColor: goodsThumbBackgroundColor }" mode="aspectFill" @click="previewImage"></image>
+            </view>
+            <view class="specification-right">
+              <view class="price-content" :style="{ color: themeColorFn('priceColor') }">
+                <text class="sign">¥</text>
+                <text class="price" :class="priceCom.length > 16 ? 'price2' : ''">{{ priceCom }}</text>
+              </view>
+              <view class="inventory" v-if="!hideStock">{{ stockText }}:{{ stockCom }}</view>
+              <view class="inventory" v-else></view>
+              <view class="choose" v-show="isManyCom">
+                <text v-if="!selectArr.every(val => val == '')">已选:{{ selectArr.join(' ') }}</text>
+              </view>
+            </view>
+          </view>
+
+          <view class="specification-content">
+            <view v-show="isManyCom" class="specification-item" v-for="(item, index1) in goodsInfo[specListName]" :key="index1">
+              <view class="item-title">{{ item.name }}</view>
+              <view class="item-wrapper">
+                <view class="item-content" v-for="(item_value, index2) in item.list" :key="index2"
+                  :class="[item_value.ishow ? '' : 'noactived', subIndex[index1] == index2 ? 'actived' : '']" :style="[
+										item_value.ishow ? '' : themeColorFn('disableStyle'),
+										item_value.ishow ? themeColorFn('btnStyle') : '',
+										subIndex[index1] == index2 ? themeColorFn('activedStyle') : ''
+									]" @click="skuClick(item_value, index1, index2)">
+                  {{ item_value.name }}
+                </view>
+              </view>
+            </view>
+            <!-- 显示限购 -->
+            <view class="number-box-view">
+              <view style="flex: 1; display: flex; align-items: center;">数量</view>
+              <view style="flex: 4; display: flex; align-items: center; justify-content: flex-end;">
+                <text v-if="maxBuyNum > 0" style="margin-right: 18rpx; font-size: 26rpx; color: #e74800;">限购{{ maxBuyNum }}件</text>
+                <number-box v-model="selectNum" :min="minBuyNum || 1" :max="maxBuyNumCom" :step="stepBuyNum || 1"
+                  :step-strictly="stepStrictly" :positive-integer="true">
+                </number-box>
+              </view>
+            </view>
+          </view>
+        </scroll-view>
+        <view class="close" @click="close('close')" v-if="showClose != false">
+          <image class="close-item" :src="closeImage"></image>
+        </view>
+      </view>
+
+      <view class="btn-wrapper" v-if="outFoStock || mode == 4">
+        <view class="sure" style="color:#ffffff;background-color:#cccccc">{{ noStockText }}</view>
+      </view>
+      <view class="btn-wrapper" v-else-if="mode == 5">
+        <view class="sure" style="color:#ffffff;background-color:#cccccc">{{ disableText }}</view>
+      </view>
+
+      <view class="btn-wrapper" v-else-if="mode == 1">
+        <view class="sure add-cart" style="border-radius:38rpx 0rpx 0rpx 38rpx;" :style="{
+						color: themeColorFn('addCartColor'),
+						backgroundColor: themeColorFn('addCartBackgroundColor')
+					}" @click="addCart">
+          {{ addCartText }}
+        </view>
+
+        <view class="sure" style="border-radius:0rpx 38rpx 38rpx 0rpx;" :style="{
+						color: themeColorFn('buyNowColor'),
+						backgroundColor: themeColorFn('buyNowBackgroundColor')
+					}" @click="buyNow">
+          {{ buyNowText }}
+        </view>
+      </view>
+      <view class="btn-wrapper" v-else-if="mode == 2">
+        <view class="sure add-cart" :style="{
+						color: themeColorFn('addCartColor'),
+						backgroundColor: themeColorFn('addCartBackgroundColor')
+					}" @click="addCart">
+          {{ addCartText }}
+        </view>
+      </view>
+      <view class="btn-wrapper" v-else-if="mode == 3">
+        <view class="sure" :style="{
+						color: themeColorFn('buyNowColor'),
+						backgroundColor: themeColorFn('buyNowBackgroundColor')
+					}" @click="buyNow">
+          {{ buyNowText }}
+        </view>
+      </view>
+    </view>
+    <!-- 页面内容结束 -->
+  </view>
+</template>
+
+<script>
+  import NumberBox from './number-box'
+  var vk; // vk依赖
+  var goodsCache = {}; // 本地商品缓存
+  export default {
+    name: 'GoodsSkuPopup',
+    components: { NumberBox },
+    emits: ['update:modelValue', 'input', 'update-goods', 'open', 'close', 'add-cart', 'buy-now', 'cart', 'buy', 'num-change'],
+    props: {
+      // true 组件显示 false 组件隐藏
+      value: {
+        Type: Boolean,
+        default: false
+      },
+      modelValue: {
+        Type: Boolean,
+        default: false
+      },
+      // vk云函数路由模式参数开始-----------------------------------------------------------
+      // 商品id
+      goodsId: {
+        Type: String,
+        default: ''
+      },
+      // vk路由模式框架下的云函数地址
+      action: {
+        Type: String,
+        default: ''
+      },
+      // vk云函数路由模式参数结束-----------------------------------------------------------
+      // 该商品已抢完时的按钮文字
+      noStockText: {
+        Type: String,
+        default: '该商品已抢完'
+      },
+      // 库存文字
+      stockText: {
+        Type: String,
+        default: '库存'
+      },
+      // 商品表id的字段名
+      goodsIdName: {
+        Type: String,
+        default: '_id'
+      },
+      // sku表id的字段名
+      skuIdName: {
+        Type: String,
+        default: '_id'
+      },
+      // sku_list的字段名
+      skuListName: {
+        Type: String,
+        default: 'sku_list'
+      },
+      // spec_list的字段名
+      specListName: {
+        Type: String,
+        default: 'spec_list'
+      },
+      // 库存的字段名 默认 stock
+      stockName: {
+        Type: String,
+        default: 'stock'
+      },
+      // sku组合路径的字段名
+      skuArrName: {
+        Type: String,
+        default: 'sku_name_arr'
+      },
+      // 默认单规格时的规格组名称
+      defaultSingleSkuName: {
+        Type: String,
+        default: '默认'
+      },
+      // 模式 1:都显示  2:只显示购物车 3:只显示立即购买 4:显示缺货按钮 5:禁用 默认 1
+      mode: {
+        Type: Number,
+        default: 1
+      },
+      // 点击遮罩是否关闭组件 true 关闭 false 不关闭 默认true
+      maskCloseAble: {
+        Type: Boolean,
+        default: true
+      },
+      // 顶部圆角值
+      borderRadius: {
+        Type: [String, Number],
+        default: 0
+      },
+      // 商品缩略图字段名(未选择sku时)
+      goodsThumbName: {
+        Type: [String],
+        default: 'goods_thumb'
+      },
+      // 商品缩略图背景颜色,如#999999
+      goodsThumbBackgroundColor: {
+        Type: String,
+        default: '#999999'
+      },
+      // 最小购买数量 默认 1
+      minBuyNum: {
+        Type: [Number, String],
+        default: 1
+      },
+      // 最大购买数量 默认 100000
+      maxBuyNum: {
+        Type: [Number, String],
+        default: 100000
+      },
+      // 步进器步长 默认 1
+      stepBuyNum: {
+        Type: [Number, String],
+        default: 1
+      },
+      // 是否只能输入 step 的倍数
+      stepStrictly: {
+        Type: Boolean,
+        default: false
+      },
+      // 自定义获取商品信息的函数,支付宝小程序不支持该属性,请使用localdata属性
+      customAction: {
+        Type: [Function],
+        default: null
+      },
+      // 本地数据源
+      localdata: {
+        type: Object
+      },
+      // 价格的字体颜色
+      priceColor: {
+        Type: String
+      },
+      // 立即购买按钮的文字
+      buyNowText: {
+        Type: String,
+        default: '立即购买'
+      },
+      // 立即购买按钮的字体颜色
+      buyNowColor: {
+        Type: String
+      },
+      // 立即购买按钮的背景颜色
+      buyNowBackgroundColor: {
+        Type: String
+      },
+      // 加入购物车按钮的文字
+      addCartText: {
+        Type: String,
+        default: '加入购物车'
+      },
+      // 加入购物车按钮的字体颜色
+      addCartColor: {
+        Type: String
+      },
+      // 加入购物车按钮的背景颜色
+      addCartBackgroundColor: {
+        Type: String
+      },
+      // 不可点击时,按钮的样式
+      disableStyle: {
+        Type: Object,
+        default: null
+      },
+      // 按钮点击时的样式
+      activedStyle: {
+        Type: Object,
+        default: null
+      },
+      // 按钮常态的样式
+      btnStyle: {
+        Type: Object,
+        default: null
+      },
+      // 是否显示右上角关闭按钮
+      showClose: {
+        Type: Boolean,
+        default: true
+      },
+      // 关闭按钮的图片地址 https://img.alicdn.com/imgextra/i1/121022687/O1CN01ImN0O11VigqwzpLiK_!!121022687.png
+      closeImage: {
+        Type: String,
+        default: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACoAAAAqCAYAAADFw8lbAAAEyUlEQVR42sSZeWwNURTGp4OqtBo7sSXELragdkpQsRRJ1Zr4hyJiJ9YgxNIg1qANiT+E1i5IY0kVVWtQEbuEKLFGUSH27/ANN5PXmTvzupzkl/tm8t6b7517lnvvC0lKSjJ8WmnQAUSDFqABqALKgl8gD7wE90E2SAeXwFf1SxISErQeVtKHwCgwFsSDSIf3hYFKoCkYDBaDdyAViHdueHmoF6FtwDLQ23b/E7gM7oIcejIERIDaoBFoC8qA8mA8SQNz6W1XC9GY+nCQCCYAk/c+gF0gBZwH312+IxR0BCPBUIaH2A+wHsxHCHxx+gLT5QGN6a2JfG8uvVCDws9oiDQYlxkMGfHyQvARlADTwcXk5OT6foV2kS8ATXidymlcyen1a/Jjl9IJh3hPkjELYqO8Cu0KjjNZvtETw5jFBWXPmGSTGQKSeOn5iQ0kVLL0CINfPNcPbDMKyRCbGzEMBJ+ZD8cChYFdqGTqfsWT8otPGoVsEHsMwxDFs3shNsxJ6BrQ0Po8OGUUkVHsNCVml+cntB1jUWwn2GEUsTEMrASbDK+2CCQ0kYX6nfLLisMmKqUr0S60M+jG10vAm+JSCa8+x7CKlzHwaktV6DiObzUzPJIxFO1BQ12wGtTReO9GetVgY/kjNJzZbcWmTjHfxw51AsRqvL8eOAtmsJuFu3g1l+1ZLB5eDTVZ3K0P7tL0TkWOpSg61kVkBtuuNRthGs+wtJST5aQI7cEbkkRXNYVKgX6kIdYuUhYzMQwxN8tiExCLFqHNeSF9/aem0BzGp5PYQCJ7c/Gsk1RfuSD6U1dNpcDf9ZigTmKbMRZ9iVTsHscGJluW2FMf1SSQWGnBmaB6kCJVTVVNJZE++Cx9drEllS1KMCINpURFmEbBWA63Fz9s95cGIdJgp/zXmT4pZcOvSUzuZttTbblmnc3PIjjmidDXvKgdhMh0JdbzuCjWrbNOVovjS5P7bkPJ/mBESkz2BO0166ybNeJ431S2q+01NntuIq3E0amzjiZtk9tssWyTDzO4525bACK9NAUn68TtkNhpEXpOSagRml+S6iLSSeweHv242Qhl13rRyvoDvDlKyTQny/ZQJ+1iH7vVbEx7OR5UiKVIO7VicgvHCtwrudloMIV7/0uadVYW57O4Wvvi8v4pymlKkrpwvsDeLLZAY2pkwbAB3PSQfC+4cH7l4k1ZH8zkZRq8ecO+Z5rN40JJqnXFuGfaxPCTLjcn0OZOpnArXw8HY4paIbw5CcMgXq6HN2/mt6+XGLrN15tBryIUGavMpCTrfKcDCKkAceA9S8nhAOehhSUyhXpkBxxnP4YM1InugP7cBkjBPcqVUWFYCEROxXiQz5JlXV+IfKh7mpfJac+lZ6V87QXVClBkTc7YWsWTPSDyitfzUTlJlj8TbvE6jluDOdwZ+jX57GLO3ADeuyZrDYi86vV81FD2UVGsmT+5Zl0BnkhoseOEaogL46pqO4v/IqUEyalIR4h85BgjHv6+aUWRMbb7EstX6O0cpT1Gco0ry8fWygLDMjmDnQeBt3Qe7uVfkeugDwVLcsVzGsuwLXbV+I63XNAkG5r/hvgRqgqWs6pJPKrsbvz/Q6yyun0w/h6lP+BnzrCpfPMT2L8FGAA7k1GZ/vnaqAAAAABJRU5ErkJggg=='
+      },
+      // 是否隐藏库存显示
+      hideStock: {
+        Type: Boolean,
+        default: false
+      },
+      // 颜色主题
+      theme: {
+        Type: String,
+        default: 'default'
+      },
+      // 请求中的提示
+      actionTips: {
+        Type: String,
+        default: '请求中...'
+      },
+      // 默认选中的SKU
+      defaultSelect: {
+        Type: Object
+      },
+      // 是否使用缓存
+      useCache: {
+        Type: Boolean,
+        default: true
+      },
+      /**
+       * 默认商品,设置该值可快速展示商品
+       * 逻辑: 先展示 defaultGoods 信息,再取数据库,再更新页面(通常为更新库存)
+       */
+      defaultGoods: {
+        Type: Object
+      },
+      /**
+       * 金额是否需要除以100
+       * 1:金额会除以100
+       * 0:金额不会除以100
+       */
+      amountType: {
+        Type: Number,
+        default: 1
+      },
+      // 每次选择完SKU后,购买数量归1,如果有最小购买数量,则设置为最小购买数量
+      selectedInit: {
+        Type: Boolean,
+        default: false
+      },
+      // 是否开启底部安全区适配,默认true
+      safeAreaInsetBottom: {
+        Type: Boolean,
+        default: true
+      },
+      // 按钮禁用提示的文字
+      disableText: {
+        Type: String,
+        default: "暂不支持购买"
+      }
+    },
+    data() {
+      return {
+        complete: false, // 组件是否加载完成
+        goodsInfo: {}, // 商品信息
+        isShow: false, // true 显示 false 隐藏
+        initKey: true, // 是否需要初始化 true 是 false 否
+        // #ifndef MP-BAIDU
+        shopItemInfo: {}, // 存放要和选中的值进行匹配的数据(因百度小程序setData不支持中文字段,故不编译shopItemInfo变量)
+        // #endif
+        selectArr: [], // 存放被选中的值
+        subIndex: [], // 是否选中 因为不确定是多规格还是单规格,所以这里定义数组来判断
+        selectShop: {}, // 存放最后选中的商品
+        selectNum: this.minBuyNum || 1, // 选中数量
+        outFoStock: false, // 是否全部sku都缺货
+        openTime: 0,
+        themeColor: {
+          // 默认主题
+          default: {
+            priceColor: 'rgb(254, 86, 10)',
+            buyNowColor: '#ffffff',
+            buyNowBackgroundColor: 'rgb(254, 86, 10)',
+            addCartColor: '#ffffff',
+            addCartBackgroundColor: 'rgb(255, 148, 2)',
+            btnStyle: {
+              color: '#333333',
+              borderColor: '#f4f4f4',
+              backgroundColor: '#ffffff'
+            },
+            activedStyle: {
+              color: 'rgb(254, 86, 10)',
+              borderColor: 'rgb(254, 86, 10)',
+              backgroundColor: 'rgba(254,86,10,0.1)'
+            },
+            disableStyle: {
+              color: '#c3c3c3',
+              borderColor: '#f6f6f6',
+              backgroundColor: '#f6f6f6'
+            }
+          },
+          // 红黑主题
+          'red-black': {
+            priceColor: 'rgb(255, 68, 68)',
+            buyNowColor: '#ffffff',
+            buyNowBackgroundColor: 'rgb(255, 68, 68)',
+            addCartColor: '#ffffff',
+            addCartBackgroundColor: 'rgb(85, 85, 85)',
+            activedStyle: {
+              color: 'rgb(255, 68, 68)',
+              borderColor: 'rgb(255, 68, 68)',
+              backgroundColor: 'rgba(255,68,68,0.1)'
+            }
+          },
+          // 黑白主题
+          'black-white': {
+            priceColor: 'rgb(47, 47, 52)',
+            buyNowColor: '#ffffff',
+            buyNowBackgroundColor: 'rgb(47, 47, 52)',
+            addCartColor: 'rgb(47, 47, 52)',
+            addCartBackgroundColor: 'rgb(235, 236, 242)',
+            // btnStyle:{
+            // 	color:"rgb(47, 47, 52)",
+            // 	borderColor:"rgba(235,236,242,0.5)",
+            // 	backgroundColor:"rgba(235,236,242,0.5)",
+            // },
+            activedStyle: {
+              color: 'rgb(47, 47, 52)',
+              borderColor: 'rgba(47,47,52,0.12)',
+              backgroundColor: 'rgba(47,47,52,0.12)'
+            }
+          },
+          // 咖啡色主题
+          coffee: {
+            priceColor: 'rgb(195, 167, 105)',
+            buyNowColor: '#ffffff',
+            buyNowBackgroundColor: 'rgb(195, 167, 105)',
+            addCartColor: 'rgb(195, 167, 105)',
+            addCartBackgroundColor: 'rgb(243, 238, 225)',
+            activedStyle: {
+              color: 'rgb(195, 167, 105)',
+              borderColor: 'rgb(195, 167, 105)',
+              backgroundColor: 'rgba(195, 167, 105,0.1)'
+            }
+          },
+          // 浅绿色主题
+          green: {
+            priceColor: 'rgb(99, 190, 114)',
+            buyNowColor: '#ffffff',
+            buyNowBackgroundColor: 'rgb(99, 190, 114)',
+            addCartColor: 'rgb(99, 190, 114)',
+            addCartBackgroundColor: 'rgb(225, 244, 227)',
+            activedStyle: {
+              color: 'rgb(99, 190, 114)',
+              borderColor: 'rgb(99, 190, 114)',
+              backgroundColor: 'rgba(99, 190, 114,0.1)'
+            }
+          }
+        }
+      };
+    },
+    created() {
+      let that = this;
+      vk = that.vk;
+      if (that.valueCom) {
+        that.open();
+      }
+    },
+    mounted() {},
+    methods: {
+      // 初始化
+      init(notAutoClick) {
+        let that = this;
+        // 清空之前的数据
+        that.selectArr = [];
+        that.subIndex = [];
+        that.selectShop = {};
+        that.selectNum = that.minBuyNum || 1;
+        that.outFoStock = false;
+        that.shopItemInfo = {};
+        let specListName = that.specListName;
+        that.goodsInfo[specListName].map(item => {
+          that.selectArr.push('');
+          that.subIndex.push(-1);
+        });
+        that.checkItem(); // 计算sku里面规格形成路径
+        that.checkInpath(-1); // 传-1是为了不跳过循环
+        if (!notAutoClick) that.autoClickSku(); // 自动选择sku策略
+      },
+      // 使用vk路由模式框架获取商品信息
+      findGoodsInfo(obj = {}) {
+        let that = this;
+        let { useCache } = obj;
+        if (typeof vk == 'undefined') {
+          that.toast('custom-action必须是function', 'none');
+          return false;
+        }
+        let { actionTips } = that;
+        let actionTitle = '';
+        let actionAoading = false;
+        if (actionTips !== 'custom') {
+          actionTitle = useCache ? '' : '请求中...';
+        } else {
+          actionAoading = useCache ? false : true;
+        }
+        vk.callFunction({
+          url: that.action,
+          title: actionTitle,
+          loading: actionAoading,
+          data: {
+            goods_id: that.goodsId
+          },
+          success(data) {
+            that.updateGoodsInfo(data.goodsInfo);
+            // 更新缓存
+            goodsCache[that.goodsId] = data.goodsInfo;
+            that.$emit('update-goods', data.goodsInfo);
+          },
+          fail() {
+            that.updateValue(false);
+          }
+        });
+      },
+      updateValue(value) {
+        let that = this;
+        if (value) {
+          that.$emit('open', true);
+          that.$emit('input', true);
+          that.$emit('update:modelValue', true);
+        } else {
+          that.$emit('input', false);
+          that.$emit('close', 'close');
+          that.$emit('update:modelValue', false);
+        }
+      },
+      // 更新商品信息(库存、名称、图片)
+      updateGoodsInfo(goodsInfo) {
+        let that = this;
+        // goodsInfo.sku_list.map((item, index) => {
+        // 	item.sku_name_arr = ["20ml/瓶"];
+        // });
+        let { skuListName } = that;
+        if (JSON.stringify(that.goodsInfo) === '{}' || that.goodsInfo[that.goodsIdName] !== goodsInfo[that.goodsIdName]) {
+          that.goodsInfo = goodsInfo;
+          that.initKey = true;
+        } else {
+          that.goodsInfo[skuListName] = goodsInfo[skuListName];
+        }
+        if (that.initKey) {
+          that.initKey = false;
+
+          // 是否默认选中第一个SKU
+          // 条件是 单规格商品或者多规格单SKU商品
+          const skuList = that.goodsInfo[that.skuListName]
+          const autoClickSku = !that.isManyCom ? true : skuList.length === 1
+          that.init(!autoClickSku)
+        }
+        // 更新选中sku的库存信息
+        let select_sku_info = that.getListItem(that.goodsInfo[skuListName], that.skuIdName, that.selectShop[that.skuIdName]);
+        Object.assign(that.selectShop, select_sku_info);
+        that.defaultSelectSku();
+        that.complete = true;
+      },
+      async open() {
+        let that = this;
+        that.openTime = new Date().getTime();
+        let findGoodsInfoRun = true;
+        let skuListName = that.skuListName;
+        // 先获取缓存中的商品信息
+        let useCache = false;
+        let goodsInfo = goodsCache[that.goodsId];
+        if (goodsInfo && that.useCache) {
+          useCache = true;
+          that.updateGoodsInfo(goodsInfo);
+        } else {
+          that.complete = false;
+        }
+        if (that.customAction && typeof that.customAction === 'function') {
+          try {
+            goodsInfo = await that
+              .customAction({
+                useCache,
+                goodsId: that.goodsId,
+                goodsInfo,
+                close: function() {
+                  setTimeout(function() {
+                    that.close();
+                  }, 500);
+                }
+              })
+              .catch(err => {
+                setTimeout(function() {
+                  that.close();
+                }, 500);
+              });
+          } catch (err) {
+            let { message = '' } = err;
+            if (message.indexOf('.catch is not a function') > -1) {
+              that.toast('custom-action必须返回一个Promise', 'none');
+              setTimeout(function() {
+                that.close();
+              }, 500);
+              return false;
+            }
+          }
+          // 更新缓存
+          goodsCache[that.goodsId] = goodsInfo;
+          if (goodsInfo && typeof goodsInfo == 'object' && JSON.stringify(goodsInfo) != '{}') {
+            findGoodsInfoRun = false;
+            that.updateGoodsInfo(goodsInfo);
+            that.updateValue(true);
+          } else {
+            that.toast('未获取到商品信息', 'none');
+            that.$emit('input', false);
+            return false;
+          }
+        } else if (typeof that.localdata !== 'undefined' && that.localdata !== null) {
+          goodsInfo = that.localdata;
+          if (goodsInfo && typeof goodsInfo == 'object' && JSON.stringify(goodsInfo) != '{}') {
+            findGoodsInfoRun = false;
+            that.updateGoodsInfo(goodsInfo);
+            that.updateValue(true);
+          } else {
+            that.toast('未获取到商品信息', 'none');
+            that.$emit('input', false);
+            return false;
+          }
+        } else {
+          if (findGoodsInfoRun) that.findGoodsInfo({ useCache });
+        }
+      },
+      // 监听 - 弹出层收起
+      close(s) {
+        let that = this;
+        if (new Date().getTime() - that.openTime < 400) {
+          return false;
+        }
+        if (s == 'mask') {
+          if (that.maskCloseAble !== false) {
+            that.$emit('input', false);
+            that.$emit('close', 'mask');
+            that.$emit('update:modelValue', false);
+          }
+        } else {
+          that.$emit('input', false);
+          that.$emit('close', 'close');
+          that.$emit('update:modelValue', false);
+        }
+      },
+      moveHandle() {
+        //禁止父元素滑动
+      },
+      // sku按钮的点击事件
+      skuClick(value, index1, index2) {
+        let that = this;
+        if (value.ishow) {
+          if (that.selectArr[index1] != value.name) {
+            that.$set(that.selectArr, index1, value.name);
+            that.$set(that.subIndex, index1, index2);
+          } else {
+            that.$set(that.selectArr, index1, '');
+            that.$set(that.subIndex, index1, -1);
+          }
+          that.checkInpath(index1);
+          // 如果全部选完
+          that.checkSelectShop();
+        }
+      },
+      // 检测是否已经选完sku
+      checkSelectShop() {
+        let that = this;
+        // 如果全部选完
+        if (that.selectArr.every(item => item != '')) {
+          that.selectShop = that.shopItemInfo[that.getArrayToSting(that.selectArr)];
+          let stock = that.selectShop[that.stockName];
+          if (typeof stock !== 'undefined' && that.selectNum > stock) {
+            that.selectNum = stock;
+          }
+          if (that.selectNum > that.maxBuyNum) {
+            that.selectNum = that.maxBuyNum;
+          }
+          if (that.selectNum < that.minBuyNum) {
+            that.selectNum = that.minBuyNum;
+          }
+          if (that.selectedInit) {
+            that.selectNum = that.minBuyNum || 1;
+          }
+        } else {
+          that.selectShop = {};
+        }
+      },
+      // 检查路径
+      checkInpath(clickIndex) {
+        let that = this;
+        let specListName = that.specListName;
+        //console.time('筛选可选路径需要的时间是');
+        //循环所有属性判断哪些属性可选
+        //当前选中的兄弟节点和已选中属性不需要循环
+        let specList = that.goodsInfo[specListName];
+        for (let i = 0, len = specList.length; i < len; i++) {
+          if (i == clickIndex) {
+            continue;
+          }
+          let len2 = specList[i].list.length;
+          for (let j = 0; j < len2; j++) {
+            if (that.subIndex[i] != -1 && j == that.subIndex[i]) {
+              continue;
+            }
+            let choosed_copy = [...that.selectArr];
+            that.$set(choosed_copy, i, specList[i].list[j].name);
+            let choosed_copy2 = choosed_copy.filter(item => item !== '' && typeof item !== 'undefined');
+            if (that.shopItemInfo.hasOwnProperty(that.getArrayToSting(choosed_copy2))) {
+              specList[i].list[j].ishow = true;
+            } else {
+              specList[i].list[j].ishow = false;
+            }
+          }
+        }
+        that.$set(that.goodsInfo, specListName, specList);
+        // console.timeEnd('筛选可选路径需要的时间是');
+      },
+      // 计算sku里面规格形成路径
+      checkItem() {
+        let that = this;
+        // console.time('计算有多小种可选路径需要的时间是');
+        let { stockName } = that;
+        let skuListName = that.skuListName;
+        // 去除库存小于等于0的商品sku
+        let originalSkuList = that.goodsInfo[skuListName];
+        let skuList = [];
+        let stockNum = 0;
+        originalSkuList.map((skuItem, index) => {
+          if (skuItem[stockName] > 0) {
+            skuList.push(skuItem);
+            stockNum += skuItem[stockName];
+          }
+        });
+        if (stockNum <= 0) {
+          that.outFoStock = true;
+        }
+        // 计算有多小种可选路径
+        let result = skuList.reduce(
+          (arrs, items) => {
+            return arrs.concat(
+              items[that.skuArrName].reduce(
+                (arr, item) => {
+                  return arr.concat(
+                    arr.map(item2 => {
+                      // 利用对象属性的唯一性实现二维数组去重
+                      //console.log(1,that.shopItemInfo,that.getArrayToSting([...item2, item]),item2,item,items);
+                      if (!that.shopItemInfo.hasOwnProperty(that.getArrayToSting([...item2, item]))) {
+                        that.shopItemInfo[that.getArrayToSting([...item2, item])] = items;
+                      }
+                      return [...item2, item];
+                    })
+                  );
+                },
+                [
+                  []
+                ]
+              )
+            );
+          },
+          [
+            []
+          ]
+        );
+        // console.timeEnd('计算有多小种可选路径需要的时间是');
+      },
+      getArrayToSting(arr) {
+        let str = '';
+        arr.map((item, index) => {
+          item = item.replace(/\./g, '。');
+          if (index == 0) {
+            str += item;
+          } else {
+            str += ',' + item;
+          }
+        });
+        return str;
+      },
+      // 检测sku选项是否已全部选完,且有库存
+      checkSelectComplete(obj = {}) {
+        let that = this;
+        let clickTime = new Date().getTime();
+        if (that.clickTime && clickTime - that.clickTime < 400) {
+          return false;
+        }
+        that.clickTime = clickTime;
+        let { selectShop, selectNum, stockText, stockName } = that;
+        if (!selectShop || !selectShop[that.skuIdName]) {
+          that.toast('请先选择对应规格', 'none');
+          return false;
+        }
+        if (selectNum <= 0) {
+          that.toast('购买数量必须>0', 'none');
+          return false;
+        }
+        // 判断库存
+        if (selectNum > selectShop[stockName]) {
+          that.toast(stockText + '不足', 'none');
+          return false;
+        }
+        if (typeof obj.success == 'function') obj.success(selectShop);
+      },
+      // 加入购物车
+      addCart() {
+        let that = this;
+        that.checkSelectComplete({
+          success: function(selectShop) {
+            selectShop.buy_num = that.selectNum;
+            that.$emit('add-cart', selectShop);
+            that.$emit('cart', selectShop);
+            // setTimeout(function() {
+            // 	that.init();
+            // }, 300);
+          }
+        });
+      },
+      // 立即购买
+      buyNow() {
+        let that = this;
+        that.checkSelectComplete({
+          success: function(selectShop) {
+            selectShop.buy_num = that.selectNum;
+            that.$emit('buy-now', selectShop);
+            that.$emit('buy', selectShop);
+          }
+        });
+      },
+      // 弹窗
+      toast(title, icon) {
+        uni.showToast({
+          title: title,
+          icon: icon
+        });
+      },
+      // 获取对象数组中的某一个item,根据指定的键值
+      getListItem(list, key, value) {
+        let that = this;
+        let item;
+        for (let i in list) {
+          if (typeof value == 'object') {
+            if (JSON.stringify(list[i][key]) === JSON.stringify(value)) {
+              item = list[i];
+              break;
+            }
+          } else {
+            if (list[i][key] === value) {
+              item = list[i];
+              break;
+            }
+          }
+        }
+        return item;
+      },
+      getListIndex(list, key, value) {
+        let that = this;
+        let index = -1;
+        for (let i = 0; i < list.length; i++) {
+          if (list[i][key] === value) {
+            index = i;
+            break;
+          }
+        }
+        return index;
+      },
+      // 自动选择sku前提是只有一组sku,默认自动选择最前面的有库存的sku
+      autoClickSku() {
+        let that = this;
+        let { stockName } = that;
+        let skuList = that.goodsInfo[that.skuListName];
+        let specListArr = that.goodsInfo[that.specListName];
+        if (specListArr.length == 1) {
+          let specList = specListArr[0].list;
+          for (let i = 0; i < specList.length; i++) {
+            let sku = that.getListItem(skuList, that.skuArrName, [specList[i].name]);
+            if (sku && sku[stockName] > 0) {
+              that.skuClick(specList[i], 0, i);
+              break;
+            }
+          }
+        }
+      },
+      // 主题颜色
+      themeColorFn(name) {
+        let that = this;
+        let { theme, themeColor } = that;
+        let color = that[name] ? that[name] : themeColor[theme][name];
+        return color;
+      },
+      defaultSelectSku() {
+        let that = this;
+        let { defaultSelect } = that;
+        if (defaultSelect && defaultSelect.sku && defaultSelect.sku.length > 0) {
+          that.selectSku(defaultSelect);
+        }
+      },
+      /**
+      	 * 主动方法 - 设置sku
+      	that.$refs.skuPopup.selectSku({
+      		sku:["红色","256G","公开版"],
+      		num:5
+      	});
+      	 */
+      selectSku(obj = {}) {
+        let that = this;
+        let { sku: skuArr, num: selectNum } = obj;
+        let specListArr = that.goodsInfo[that.specListName];
+        if (skuArr && specListArr.length === skuArr.length) {
+          // 先清空
+          let skuClickArr = [];
+          let clickKey = true;
+          for (let index = 0; index < skuArr.length; index++) {
+            let skuName = skuArr[index];
+            let specList = specListArr[index].list;
+            let index1 = index;
+            let index2 = that.getListIndex(specList, 'name', skuName);
+            if (index2 == -1) {
+              clickKey = false;
+              break;
+            }
+            skuClickArr.push({
+              spec: specList[index2],
+              index1: index1,
+              index2: index2
+            });
+          }
+          if (clickKey) {
+            that.init(true);
+            skuClickArr.map(item => {
+              that.skuClick(item.spec, item.index1, item.index2);
+            });
+          }
+        }
+        if (selectNum > 0) that.selectNum = selectNum;
+      },
+      priceFilter(n = 0) {
+        let that = this;
+        if (typeof n == 'string') {
+          n = parseFloat(n);
+        }
+        if (that.amountType === 0) {
+          return n.toFixed(2);
+        } else {
+          return (n / 100).toFixed(2);
+        }
+      },
+      pushGoodsCache(goodsInfo) {
+        let that = this;
+        let { goodsIdName } = that;
+        goodsCache[goodsInfo[goodsIdName]] = goodsInfo;
+      },
+      // 用于阻止冒泡
+      stop() {},
+      // 图片预览
+      previewImage() {
+        let that = this;
+        let { selectShop, goodsInfo, goodsThumbName } = that;
+        let src = selectShop.image ? selectShop.image : goodsInfo[goodsThumbName];
+        if (src) {
+          uni.previewImage({
+            urls: [src]
+          });
+        }
+      },
+      getMaxStock() {
+        let maxStock = 0;
+        let that = this;
+        let { selectShop = {}, goodsInfo = {}, skuListName, stockName } = that;
+        if (selectShop[stockName]) {
+          maxStock = selectShop[stockName];
+        } else {
+          let skuList = goodsInfo[skuListName];
+          if (skuList && skuList.length > 0) {
+            let valueArr = [];
+            skuList.map((skuItem, index) => {
+              valueArr.push(skuItem[stockName]);
+            });
+            let max = Math.max(...valueArr);
+            maxStock = max;
+          }
+        }
+        return maxStock;
+      },
+      numChange(e) {
+        this.$emit("num-change", e.value);
+      }
+    },
+    // 计算属性
+    computed: {
+      valueCom() {
+        // #ifndef VUE3
+        return this.value;
+        // #endif
+
+        // #ifdef VUE3
+        return this.modelValue;
+        // #endif
+      },
+      // 最大购买数量
+      maxBuyNumCom() {
+        let that = this;
+        let maxStock = that.getMaxStock();
+        let max = that.maxBuyNum || 100000;
+        // 最大购买量不能超过当前商品的库存
+        if (max > maxStock) {
+          max = maxStock;
+        }
+        return max;
+      },
+      // 是否是多规格
+      isManyCom() {
+        let that = this;
+        let { goodsInfo, defaultSingleSkuName, specListName } = that;
+        let isMany = true;
+        if (
+          goodsInfo[specListName] &&
+          goodsInfo[specListName].length === 1 &&
+          goodsInfo[specListName][0].list.length === 1 &&
+          goodsInfo[specListName][0].name === defaultSingleSkuName
+        ) {
+          isMany = false;
+        }
+        return isMany;
+      },
+      // 默认价格区间计算
+      priceCom() {
+        let str = '';
+        let that = this;
+        let { selectShop = {}, goodsInfo = {}, skuListName, skuIdName } = that;
+        if (selectShop[skuIdName]) {
+          str = that.priceFilter(selectShop.price);
+        } else {
+          let skuList = goodsInfo[skuListName];
+          if (skuList && skuList.length > 0) {
+            let valueArr = [];
+            skuList.map((skuItem, index) => {
+              valueArr.push(skuItem.price);
+            });
+            let min = that.priceFilter(Math.min(...valueArr));
+            let max = that.priceFilter(Math.max(...valueArr));
+            if (min === max) {
+              str = min + '';
+            } else {
+              str = `${min} - ${max}`;
+            }
+          }
+        }
+        return str;
+      },
+      // 库存显示
+      stockCom() {
+        let str = '';
+        let that = this;
+        let { selectShop = {}, goodsInfo = {}, skuListName, stockName } = that;
+        if (selectShop[stockName]) {
+          str = selectShop[stockName];
+        } else {
+          let skuList = goodsInfo[skuListName];
+          if (skuList && skuList.length > 0) {
+            let valueArr = [];
+            skuList.map((skuItem, index) => {
+              valueArr.push(skuItem[stockName]);
+            });
+            let min = Math.min(...valueArr);
+            let max = Math.max(...valueArr);
+            if (min === max) {
+              str = min;
+            } else {
+              str = `${min} - ${max}`;
+            }
+          }
+        }
+        return str;
+      }
+    },
+    watch: {
+      valueCom(newVal, oldValue) {
+        let that = this;
+        if (newVal) {
+          that.open();
+        }
+      },
+      defaultGoods: {
+        immediate: true,
+        handler: function(newVal, oldValue) {
+          let that = this;
+          let { goodsIdName } = that;
+          if (typeof newVal === 'object' && newVal && newVal[goodsIdName] && !goodsCache[newVal[goodsIdName]]) {
+            that.pushGoodsCache(newVal);
+          }
+        }
+      }
+    }
+  };
+</script>
+
+<style lang="scss" scoped>
+  /*  sku弹出层 */
+  .goods-sku-popup {
+    position: fixed;
+    left: var(--window-left);
+    right: var(--window-right);
+    bottom: var(--window-bottom);
+    top: var(--window-top);
+    z-index: 21;
+    overflow: hidden;
+
+    &.show {
+      display: block;
+
+      .mask {
+        animation: showPopup 0.2s linear both;
+      }
+
+      .layer {
+        animation: showLayer 0.2s linear both;
+        bottom: var(--window-bottom);
+      }
+    }
+
+    &.hide {
+      .mask {
+        animation: hidePopup 0.2s linear both;
+      }
+
+      .layer {
+        animation: hideLayer 0.2s linear both;
+      }
+    }
+
+    &.none {
+      display: none;
+    }
+
+    .mask {
+      position: fixed;
+      left: var(--window-left);
+      right: var(--window-right);
+      top: var(--window-top);
+      // width: 100%;
+      height: 100%;
+      z-index: 1;
+      background-color: rgba(0, 0, 0, 0.65);
+    }
+
+    .layer {
+      display: flex;
+      flex-direction: column;
+      position: fixed;
+      left: var(--window-left);
+      right: var(--window-right);
+      bottom: var(--window-bottom);
+      z-index: 99;
+      border-radius: 10rpx 10rpx 0 0;
+      background-color: #fff;
+      // 设置ios刘海屏底部横线安全区域
+      padding-bottom: constant(safe-area-inset-bottom);
+      padding-bottom: env(safe-area-inset-bottom);
+
+      .specification-wrapper {
+        width: 100%;
+        padding: 30rpx 25rpx;
+        box-sizing: border-box;
+
+        .specification-wrapper-content {
+          width: 100%;
+          max-height: 900rpx;
+          min-height: 300rpx;
+
+          &::-webkit-scrollbar {
+            /*隐藏滚轮*/
+            display: none;
+          }
+
+          .specification-header {
+            width: 100%;
+            display: flex;
+            flex-direction: row;
+            position: relative;
+            margin-bottom: 40rpx;
+
+            .specification-left {
+              width: 180rpx;
+              height: 180rpx;
+              flex: 0 0 180rpx;
+
+              .product-img {
+                width: 180rpx;
+                height: 180rpx;
+              }
+            }
+
+            .specification-right {
+              flex: 1;
+              padding: 0 50rpx 10rpx 28rpx;
+              box-sizing: border-box;
+              display: flex;
+              flex-direction: column;
+              justify-content: center;
+              font-weight: 500;
+
+              .price-content {
+                color: #fe560a;
+                margin-bottom: 10rpx;
+
+                .sign {
+                  font-size: 28rpx;
+                  margin-right: 4rpx;
+                }
+
+                .price {
+                  margin-left: 4rpx;
+                  font-size: 48rpx;
+                }
+
+                .price2 {
+                  margin-left: 4rpx;
+                  font-size: 36rpx;
+                }
+              }
+
+              .inventory {
+                font-size: 24rpx;
+                color: #525252;
+                margin-bottom: 14rpx;
+              }
+
+              .choose {
+                font-size: 24rpx;
+                color: #525252;
+                min-height: 32rpx;
+              }
+            }
+          }
+
+          .specification-content {
+            font-weight: 500;
+
+            .specification-item {
+              margin-bottom: 40rpx;
+
+              &:last-child {
+                margin-bottom: 0;
+              }
+
+              .item-title {
+                margin-bottom: 20rpx;
+                font-size: 28rpx;
+                color: #999999;
+              }
+
+              .item-wrapper {
+                display: flex;
+                flex-direction: row;
+                flex-flow: wrap;
+                margin-bottom: -20rpx;
+
+                .item-content {
+                  display: inline-block;
+                  padding: 10rpx 35rpx;
+                  font-size: 24rpx;
+                  border-radius: 10rpx;
+                  background-color: #ffffff;
+                  color: #333333;
+                  margin-right: 20rpx;
+                  margin-bottom: 20rpx;
+                  border: 2rpx solid #f4f4f4;
+                  box-sizing: border-box;
+
+                  &.actived {
+                    border-color: #fe560a;
+                    color: #fe560a;
+                  }
+
+                  &.noactived {
+                    // background-color: #f6f6f6;
+                    // border-color: #f6f6f6;
+                    // color: #c3c3c3;
+                    color: #c8c9cc;
+                    background: #f2f3f5;
+                    border-color: #f2f3f5;
+                  }
+                }
+              }
+            }
+
+            .number-box-view {
+              display: flex;
+              padding-top: 20rpx;
+            }
+          }
+        }
+
+        .close {
+          position: absolute;
+          top: 30rpx;
+          right: 25rpx;
+          width: 50rpx;
+          height: 50rpx;
+          display: flex;
+          justify-content: center;
+          align-items: center;
+
+          .close-item {
+            width: 40rpx;
+            height: 40rpx;
+          }
+        }
+      }
+
+      .btn-wrapper {
+        display: flex;
+        width: 100%;
+        height: 120rpx;
+        flex: 0 0 120rpx;
+        align-items: center;
+        justify-content: space-between;
+        padding: 0 26rpx;
+        box-sizing: border-box;
+
+        .layer-btn {
+          width: 335rpx;
+          height: 76rpx;
+          border-radius: 38rpx;
+          color: #fff;
+          font-weight: 500;
+          font-size: 28rpx;
+          display: flex;
+          justify-content: center;
+          align-items: center;
+
+          &.add-cart {
+            background: #ffbe46;
+          }
+
+          &.buy {
+            background: #fe560a;
+          }
+        }
+
+        .sure {
+          margin: 0 auto;
+          width: 95%;
+          max-width: 1200rpx;
+          height: 80rpx;
+          border-radius: 38rpx;
+          color: #fff;
+          font-weight: 500;
+          font-size: 28rpx;
+          background: #fe560a;
+          display: flex;
+          justify-content: center;
+          align-items: center;
+        }
+
+        .sure.add-cart {
+          background: #ff9402;
+        }
+      }
+
+      .btn-wrapper.safe-area-inset-bottom {
+        padding-bottom: 0;
+        padding-bottom: constant(safe-area-inset-bottom);
+        padding-bottom: env(safe-area-inset-bottom);
+      }
+    }
+
+    @keyframes showPopup {
+      0% {
+        opacity: 0;
+      }
+
+      100% {
+        opacity: 1;
+      }
+    }
+
+    @keyframes hidePopup {
+      0% {
+        opacity: 1;
+      }
+
+      100% {
+        opacity: 0;
+      }
+    }
+
+    @keyframes showLayer {
+      0% {
+        transform: translateY(120%);
+      }
+
+      100% {
+        transform: translateY(0%);
+      }
+    }
+
+    @keyframes hideLayer {
+      0% {
+        transform: translateY(0);
+      }
+
+      100% {
+        transform: translateY(120%);
+      }
+    }
+  }
+</style>

+ 473 - 0
components/goods-sku-popup/number-box/index.vue

@@ -0,0 +1,473 @@
+<!-- 步进器 -->
+<template>
+	<view class="number-box">
+		<view
+			class="u-icon-minus"
+			:class="{ 'u-icon-disabled': disabled || inputVal <= min }"
+			:style="{
+				background: bgColor,
+				height: inputHeight + 'rpx',
+				color: color,
+				fontSize: size + 'rpx',
+				minHeight: '1.4em'
+			}"
+			@click="emptyClick"
+			@touchstart.prevent="btnTouchStart('minus')"
+			@touchend.stop.prevent="clearTimer"
+		>
+			<view :style="'font-size:' + (Number(size) + 10) + 'rpx'" class="num-btn">-</view>
+		</view>
+		<input
+			v-model="inputVal"
+			:disabled="disabledInput || disabled"
+			:cursor-spacing="getCursorSpacing"
+			:class="{ 'u-input-disabled': disabled }"
+			class="u-number-input"
+			type="number"
+			:style="{
+				color: color,
+				fontSize: size + 'rpx',
+				background: bgColor,
+				height: inputHeight + 'rpx',
+				width: inputWidth + 'rpx'
+			}"
+			@blur="onBlur"
+			@click="showInput=true"
+		/>
+		<view
+			class="u-icon-plus"
+			:class="{ 'u-icon-disabled': disabled || inputVal >= max }"
+			:style="{
+				background: bgColor,
+				height: inputHeight + 'rpx',
+				color: color,
+				fontSize: size + 'rpx',
+				minHeight: '1.4em'
+			}"
+			@click="emptyClick"
+			@touchstart.prevent="btnTouchStart('plus')"
+			@touchend.stop.prevent="clearTimer"
+		>
+			<view :style="'font-size:' + (Number(size) + 10) + 'rpx'" class="num-btn">+</view>
+		</view>
+	</view>
+</template>
+<script>
+/**
+ * numberBox 步进器(此为uview组件改造)
+ * @description 该组件一般用于商城购物选择物品数量的场景。注意:该输入框只能输入大于或等于0的整数,不支持小数输入
+ * @tutorial https://www.uviewui.com/components/numberBox.html
+ * @property {Number} value 输入框初始值(默认1)
+ * @property {String} bg-color 输入框和按钮的背景颜色(默认#F2F3F5)
+ * @property {Number} min 用户可输入的最小值(默认0)
+ * @property {Number} max 用户可输入的最大值(默认99999)
+ * @property {Number} step 步长,每次加或减的值(默认1)
+ * @property {Number} stepFirst 步进值,首次增加或最后减的值(默认step值和一致)
+ * @property {Boolean} disabled 是否禁用操作,禁用后无法加减或手动修改输入框的值(默认false)
+ * @property {Boolean} disabled-input 是否禁止输入框手动输入值(默认false)
+ * @property {Boolean} positive-integer 是否只能输入正整数(默认true)
+ * @property {String | Number} size 输入框文字和按钮字体大小,单位rpx(默认26)
+ * @property {String} color 输入框文字和加减按钮图标的颜色(默认#323233)
+ * @property {String | Number} input-width 输入框宽度,单位rpx(默认80)
+ * @property {String | Number} input-height 输入框和按钮的高度,单位rpx(默认50)
+ * @property {String | Number} index 事件回调时用以区分当前发生变化的是哪个输入框
+ * @property {Boolean} long-press 是否开启长按连续递增或递减(默认true)
+ * @property {String | Number} press-time 开启长按触发后,每触发一次需要多久,单位ms(默认250)
+ * @property {String | Number} cursor-spacing 指定光标于键盘的距离,避免键盘遮挡输入框,单位rpx(默认200)
+ * @event {Function} change 输入框内容发生变化时触发,对象形式
+ * @event {Function} blur 输入框失去焦点时触发,对象形式
+ * @event {Function} minus 点击减少按钮时触发(按钮可点击情况下),对象形式
+ * @event {Function} plus 点击增加按钮时触发(按钮可点击情况下),对象形式
+ * @example <number-box :min="1" :max="100"></number-box>
+ */
+export default {
+	name: 'NumberBox',
+	emits: ['update:modelValue', 'input', 'change', 'blur', 'plus', 'minus'],
+	props: {
+		// 预显示的数字
+		value: {
+			type: Number,
+			default: 1
+		},
+		modelValue: {
+			type: Number,
+			default: 1
+		},
+		// 背景颜色
+		bgColor: {
+			type: String,
+			default: '#FFFFFF'
+		},
+		// 最小值
+		min: {
+			type: Number,
+			default: 0
+		},
+		// 最大值
+		max: {
+			type: Number,
+			default: 99999
+		},
+		// 步进值,每次加或减的值
+		step: {
+			type: Number,
+			default: 1
+		},
+		// 步进值,首次增加或最后减的值
+		stepFirst: {
+			type: Number,
+			default: 0
+		},
+		// 是否只能输入 step 的倍数
+		stepStrictly: {
+			type: Boolean,
+			default: false
+		},
+		// 是否禁用加减操作
+		disabled: {
+			type: Boolean,
+			default: false
+		},
+		// input的字体大小,单位rpx
+		size: {
+			type: [Number, String],
+			default: 26
+		},
+		// 加减图标的颜色
+		color: {
+			type: String,
+			default: '#323233'
+		},
+		// input宽度,单位rpx
+		inputWidth: {
+			type: [Number, String],
+			default: 80
+		},
+		// input高度,单位rpx
+		inputHeight: {
+			type: [Number, String],
+			default: 50
+		},
+		// index索引,用于列表中使用,让用户知道是哪个numberbox发生了变化,一般使用for循环出来的index值即可
+		index: {
+			type: [Number, String],
+			default: ''
+		},
+		// 是否禁用输入框,与disabled作用于输入框时,为OR的关系,即想要禁用输入框,又可以加减的话
+		// 设置disabled为false,disabledInput为true即可
+		disabledInput: {
+			type: Boolean,
+			default: false
+		},
+		// 输入框于键盘之间的距离
+		cursorSpacing: {
+			type: [Number, String],
+			default: 100
+		},
+		// 是否开启长按连续递增或递减
+		longPress: {
+			type: Boolean,
+			default: true
+		},
+		// 开启长按触发后,每触发一次需要多久
+		pressTime: {
+			type: [Number, String],
+			default: 250
+		},
+		// 是否只能输入大于或等于0的整数(正整数)
+		positiveInteger: {
+			type: Boolean,
+			default: true
+		}
+	},
+	watch: {
+		valueCom(v1, v2) {
+			// 只有value的改变是来自外部的时候,才去同步inputVal的值,否则会造成循环错误
+			if (!this.changeFromInner) {
+				this.inputVal = v1;
+				// 因为inputVal变化后,会触发this.handleChange(),在其中changeFromInner会再次被设置为true,
+				// 造成外面修改值,也导致被认为是内部修改的混乱,这里进行this.$nextTick延时,保证在运行周期的最后处
+				// 将changeFromInner设置为false
+				this.$nextTick(function() {
+					this.changeFromInner = false;
+				});
+			}
+		},
+		inputVal(v1, v2) {
+			// 为了让用户能够删除所有输入值,重新输入内容,删除所有值后,内容为空字符串
+			if (v1 == '') return;
+			let value = 0;
+			// 首先判断是否数值,并且在min和max之间,如果不是,使用原来值
+			let tmp = this.isNumber(v1);
+			if (tmp && v1 >= this.min && v1 <= this.max) value = v1;
+			else value = v2;
+			// 判断是否只能输入大于等于0的整数
+			if (this.positiveInteger) {
+				// 小于0,或者带有小数点,
+				if (v1 < 0 || String(v1).indexOf('.') !== -1) {
+					value = v2;
+					// 双向绑定input的值,必须要使用$nextTick修改显示的值
+					this.$nextTick(() => {
+						this.inputVal = v2;
+					});
+				}
+			}
+			// 发出change事件
+			this.handleChange(value, 'change');
+		},
+		min(v1) {
+			if (v1 !== undefined && v1 != '' && this.valueCom < v1) {
+				this.$emit('input', v1);
+				this.$emit('update:modelValue', v1);
+			}
+		},
+		max(v1) {
+			if (v1 !== undefined && v1 != '' && this.valueCom > v1) {
+				this.$emit('input', v1);
+				this.$emit('update:modelValue', v1);
+			}
+		}
+	},
+	data() {
+		return {
+			inputVal: 1, // 输入框中的值,不能直接使用props中的value,因为应该改变props的状态
+			timer: null, // 用作长按的定时器
+			changeFromInner: false, // 值发生变化,是来自内部还是外部
+			innerChangeTimer: null ,// 内部定时器
+			showInput:false,
+		};
+	},
+	created() {
+		this.inputVal = Number(this.valueCom);
+	},
+	computed: {
+		valueCom() {
+			// #ifndef VUE3
+			return this.value;
+			// #endif
+
+			// #ifdef VUE3
+			return this.modelValue;
+			// #endif
+		},
+		getCursorSpacing() {
+			// 先将值转为px单位,再转为数值
+			return Number(uni.upx2px(this.cursorSpacing));
+		}
+	},
+	methods: {
+		// 空点击事件,主要用于解决PC端H5由于无click事件导致触摸位置不准确的问题
+		emptyClick(){
+			
+		},
+		// 触摸事件开始
+		btnTouchStart(callback) {
+			// 先执行一遍方法,否则会造成松开手时,就执行了clearTimer,导致无法实现功能
+			this[callback]();
+			// 如果没开启长按功能,直接返回
+			if (!this.longPress) return;
+			clearInterval(this.timer); //再次清空定时器,防止重复注册定时器
+			this.timer = null;
+			this.timer = setInterval(() => {
+				// 执行加或减函数
+				this[callback]();
+			}, this.pressTime);
+		},
+		// 清除定时器
+		clearTimer() {
+			this.$nextTick(() => {
+				clearInterval(this.timer);
+				this.timer = null;
+			});
+		},
+		// 减
+		minus() {
+			this.computeVal('minus');
+		},
+		// 加
+		plus() {
+			this.computeVal('plus');
+		},
+		// 为了保证小数相加减出现精度溢出的问题
+		calcPlus(num1, num2) {
+			let baseNum, baseNum1, baseNum2;
+			try {
+				baseNum1 = num1.toString().split('.')[1].length;
+			} catch (e) {
+				baseNum1 = 0;
+			}
+			try {
+				baseNum2 = num2.toString().split('.')[1].length;
+			} catch (e) {
+				baseNum2 = 0;
+			}
+			baseNum = Math.pow(10, Math.max(baseNum1, baseNum2));
+			let precision = baseNum1 >= baseNum2 ? baseNum1 : baseNum2; //精度
+			return ((num1 * baseNum + num2 * baseNum) / baseNum).toFixed(precision);
+		},
+		// 为了保证小数相加减出现精度溢出的问题
+		calcMinus(num1, num2) {
+			let baseNum, baseNum1, baseNum2;
+			try {
+				baseNum1 = num1.toString().split('.')[1].length;
+			} catch (e) {
+				baseNum1 = 0;
+			}
+			try {
+				baseNum2 = num2.toString().split('.')[1].length;
+			} catch (e) {
+				baseNum2 = 0;
+			}
+			baseNum = Math.pow(10, Math.max(baseNum1, baseNum2));
+			let precision = baseNum1 >= baseNum2 ? baseNum1 : baseNum2;
+			return ((num1 * baseNum - num2 * baseNum) / baseNum).toFixed(precision);
+		},
+		computeVal(type) {
+			uni.hideKeyboard();
+			if (this.disabled) return;
+			let value = 0;
+			// 新增stepFirst开始
+			// 减
+			if (type === 'minus') {
+				if (this.stepFirst > 0 && this.inputVal == this.stepFirst) {
+					value = this.min;
+				} else {
+					value = this.calcMinus(this.inputVal, this.step);
+				}
+			} else if (type === 'plus') {
+				if (this.stepFirst > 0 && this.inputVal < this.stepFirst) {
+					value = this.stepFirst;
+				} else {
+					value = this.calcPlus(this.inputVal, this.step);
+				}
+			}
+			if (this.stepStrictly) {
+				let strictly = value % this.step;
+				if (strictly > 0) {
+					value -= strictly;
+				}
+			}
+			if (value > this.max) {
+				value = this.max;
+			} else if (value < this.min) {
+				value = this.min;
+			}
+			// 新增stepFirst结束
+			this.inputVal = value;
+			this.handleChange(value, type);
+		},
+		// 处理用户手动输入的情况
+		onBlur(event) {
+			let val = 0;
+			let value = event.detail.value;
+			// 如果为非0-9数字组成,或者其第一位数值为0,直接让其等于min值
+			// 这里不直接判断是否正整数,是因为用户传递的props min值可能为0
+			if (!/(^\d+$)/.test(value) || value[0] == 0) val = this.min;
+			val = +value;
+
+			// 新增stepFirst开始
+			if (this.stepFirst > 0 && this.inputVal < this.stepFirst && this.inputVal > 0) {
+				val = this.stepFirst;
+			}
+			// 新增stepFirst结束
+			if (this.stepStrictly) {
+				let strictly = val % this.step;
+				if (strictly > 0) {
+					val -= strictly;
+				}
+			}
+			if (val > this.max) {
+				val = this.max;
+			} else if (val < this.min) {
+				val = this.min;
+			}
+			this.$nextTick(() => {
+				this.inputVal = val;
+			});
+			this.handleChange(val, 'blur');
+		},
+		handleChange(value, type) {
+			if (this.disabled) return;
+			// 清除定时器,避免造成混乱
+			if (this.innerChangeTimer) {
+				clearTimeout(this.innerChangeTimer);
+				this.innerChangeTimer = null;
+			}
+			// 发出input事件,修改通过v-model绑定的值,达到双向绑定的效果
+			this.changeFromInner = true;
+			// 一定时间内,清除changeFromInner标记,否则内部值改变后
+			// 外部通过程序修改value值,将会无效
+			this.innerChangeTimer = setTimeout(() => {
+				this.changeFromInner = false;
+			}, 150);
+			this.$emit('input', Number(value));
+			this.$emit('update:modelValue', Number(value));
+			this.$emit(type, {
+				// 转为Number类型
+				value: Number(value),
+				index: this.index
+			});
+		},
+		/**
+		 * 验证十进制数字
+		 */
+		isNumber(value) {
+			return /^(?:-?\d+|-?\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/.test(value);
+		}
+	}
+};
+</script>
+
+<style lang="scss" scoped>
+.number-box {
+	display: inline-flex;
+	align-items: center;
+	box-sizing: border-box;
+}
+
+.u-number-input {
+	position: relative;
+	text-align: center;
+	padding: 0;
+	margin: 0rpx;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	border: 2rpx solid #f4f4f4;
+	border-left: 0;
+	border-right: 0;
+	box-sizing: border-box;
+}
+
+.u-icon-plus,
+.u-icon-minus {
+	width: 60rpx;
+	display: flex;
+	justify-content: center;
+	align-items: center;
+	border: 2rpx solid #f4f4f4;
+	box-sizing: border-box;
+}
+
+.u-icon-plus {
+	border-radius: 0 8rpx 8rpx 0;
+}
+
+.u-icon-minus {
+	border-radius: 8rpx 0 0 8rpx;
+}
+
+.u-icon-disabled {
+	color: #c8c9cc !important;
+	background-color: #f2f3f5 !important;
+}
+
+.u-input-disabled {
+	color: #c8c9cc !important;
+	background-color: #f2f3f5 !important;
+}
+.num-btn {
+	font-weight: 550;
+	line-height: 50rpx;
+}
+</style>

+ 43 - 0
components/page/diyComponents/ICPLicense/index.vue

@@ -0,0 +1,43 @@
+<template>
+  <!-- 网站备案号 -->
+  <!-- #ifdef H5 -->
+  <view class="diy-ICPLicense"
+    :style="{ padding: `${itemStyle.paddingTop * 2}rpx ${itemStyle.paddingLeft * 2}rpx`, background: itemStyle.background }">
+    <view class="line" :style="{ textAlign: itemStyle.textAlign }">
+      <text :style="{ fontSize: `${itemStyle.fontSize * 2}rpx`, color: itemStyle.textColor }" target="_blank"
+        @click="onLink({ type: 'URL', param: { url: params.link } })">{{ params.text }}</text>
+    </view>
+  </view>
+  <!-- #endif -->
+</template>
+
+<script>
+  import mixin from '../mixin'
+
+  export default {
+
+    /**
+     * 组件的属性列表
+     * 用于组件自定义设置
+     */
+    props: {
+      itemStyle: Object,
+      params: Object,
+    },
+
+    mixins: [mixin],
+
+    /**
+     * 组件的方法列表
+     * 更新属性和数据的方法与更新页面数据的方法类似
+     */
+    methods: {
+
+    }
+
+  }
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 123 - 0
components/page/diyComponents/article/index.vue

@@ -0,0 +1,123 @@
+<template>
+  <!-- 文章组 -->
+  <view class="diy-article">
+    <view class="article-item" :class="[`show-type__${item.show_type}`]" v-for="(item, index) in dataList" :key="index"
+      @click="onTargetDetail(item.article_id)">
+      <!-- 小图模式 -->
+      <block v-if="item.show_type == 10">
+        <view class="article-item__left flex-box">
+          <view class="article-item__title">
+            <text class="twoline-hide">{{ item.title }}</text>
+          </view>
+          <view class="article-item__footer m-top10">
+            <text class="article-views f-24 col-8">{{ item.show_views }}次浏览</text>
+          </view>
+        </view>
+        <view class="article-item__image">
+          <image class="image" mode="widthFix" :src="item.image_url"></image>
+        </view>
+      </block>
+      <!-- 大图模式 -->
+      <block v-if="item.show_type == 20">
+        <view class="article-item__title">
+          <text class="twoline-hide">{{ item.title }}</text>
+        </view>
+        <view class="article-item__image m-top20">
+          <image class="image" mode="widthFix" :src="item.image_url"></image>
+        </view>
+        <view class="article-item__footer m-top10">
+          <text class="article-views f-24 col-8">{{ item.show_views }}次浏览</text>
+        </view>
+      </block>
+    </view>
+  </view>
+</template>
+
+<script>
+  export default {
+    name: "Article",
+    /**
+     * 组件的属性列表
+     * 用于组件自定义设置
+     */
+    props: {
+      itemIndex: String,
+      params: Object,
+      dataList: Array
+    },
+
+    /**
+     * 组件的方法列表
+     * 更新属性和数据的方法与更新页面数据的方法类似
+     */
+    methods: {
+
+      /**
+       * 跳转文章详情页
+       */
+      onTargetDetail(id) {
+        uni.navigateTo({
+          url: '/pages/article/detail?articleId=' + id
+        })
+      }
+
+    }
+
+  }
+</script>
+
+<style lang="scss" scoped>
+  .diy-article {
+    background: #f7f7f7;
+
+    .article-item {
+      margin-bottom: 20rpx;
+      padding: 30rpx;
+      background: #fff;
+
+      &:last-child {
+        margin-bottom: 0;
+      }
+
+      .article-item__title {
+        max-height: 74rpx;
+        font-size: 28rpx;
+        line-height: 38rpx;
+        color: #333;
+      }
+
+      .article-item__image .image {
+        display: block;
+      }
+
+    }
+
+
+
+  }
+
+  /* 小图模式 */
+
+  .show-type__10 {
+    display: flex;
+
+    .article-item__left {
+      padding-right: 20rpx;
+    }
+
+    .article-item__title {
+      // min-height: 72rpx;
+    }
+
+    .article-item__image .image {
+      width: 240rpx;
+    }
+
+  }
+
+  /* 大图模式 */
+
+  .show-type__20 .article-item__image .image {
+    width: 100%;
+  }
+</style>

+ 133 - 0
components/page/diyComponents/banner/index.vue

@@ -0,0 +1,133 @@
+<template>
+  <view class="diy-banner"
+    :style="{ padding: `${itemStyle.paddingTop * 2}rpx ${itemStyle.paddingLeft * 2}rpx`, background: itemStyle.background, height: `${imgHeights[swiperIndex]}px` }">
+    <!-- 图片轮播 -->
+    <swiper :autoplay="autoplay" class="swiper-box" :duration="duration" :circular="true" :interval="itemStyle.interval * 1000"
+      @change="onChangeItem" :style="{ borderRadius: `${itemStyle.borderRadius * 2}rpx` }">
+      <swiper-item class="swiper-item" v-for="(dataItem, index) in dataList" :key="index">
+        <image mode="widthFix" class="slide-image" :src="dataItem.imgUrl" @click="onLink(dataItem.link)" @load="onLoadImage" />
+      </swiper-item>
+    </swiper>
+    <!-- 指示点 -->
+    <view class="indicator-dots" :class="itemStyle.btnShape"
+      :style="{ '--padding-top': `${itemStyle.paddingTop > 0 ? itemStyle.paddingTop * 2 : 1 }rpx` }">
+      <view class="dots-item" :class="{ active: swiperIndex == index }" :style="{ backgroundColor: itemStyle.btnColor }"
+        v-for="(dataItem, index) in dataList" :key="index"></view>
+    </view>
+  </view>
+</template>
+
+<script>
+  import mixin from '../mixin'
+
+  export default {
+    props: {
+      itemIndex: String,
+      itemStyle: Object,
+      params: Object,
+      dataList: Array
+    },
+    mixins: [mixin],
+    data() {
+      return {
+        windowWidth: 750,
+        indicatorDots: false, // 是否显示面板指示点
+        autoplay: true, // 是否自动切换
+        duration: 800, // 滑动动画时长
+        imgHeights: [], // 每张图片对应的容器高度
+        swiperIndex: 0 // 当前banne所在滑块指针
+      }
+    },
+    created() {
+      const { windowWidth } = uni.getWindowInfo()
+      this.windowWidth = windowWidth > 750 ? 750 : windowWidth
+      // #ifdef H5
+      // 此处取决于pages.json中的maxWidth
+      this.windowWidth = windowWidth > 450 ? 450 : windowWidth
+      // #endif
+    },
+    methods: {
+      // 计算图片高度
+      onLoadImage({ detail }) {
+        const app = this
+        // 图片的宽高比
+        const ratio = detail.width / detail.height
+        // 计算容器高度值
+        const viewHeight = (app.windowWidth - (app.itemStyle.paddingLeft * 2)) / ratio
+        // 每张图片对应的容器高度
+        app.imgHeights.push(viewHeight + (app.itemStyle.paddingTop * 2))
+      },
+
+      // 记录当前指针
+      onChangeItem({ detail }) {
+        this.swiperIndex = detail.current
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .diy-banner {
+    position: relative;
+  }
+
+  // swiper组件
+  .swiper-box {
+    height: 100%;
+    overflow: hidden;
+
+    .swiper-item {}
+
+    .slide-image {
+      width: 100%;
+      height: 100%;
+      margin: 0 auto;
+      display: block;
+    }
+  }
+
+  // 指示点
+  .indicator-dots {
+    --padding-top: 0;
+    height: 28rpx;
+    padding: 0 20rpx;
+    position: absolute;
+    left: 0;
+    right: 0;
+    bottom: calc(var(--padding-top) + 20rpx);
+    opacity: 0.8;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+
+    .dots-item {
+      margin-right: 8rpx;
+      width: 16rpx;
+      height: 16rpx;
+      background-color: #fff;
+
+      &:last-child {
+        margin-right: 0;
+      }
+
+      &.active {
+        background-color: #313131 !important;
+      }
+    }
+
+    // 长方形
+    &.rectangle .dots-item {
+      width: 32rpx;
+      height: 16rpx;
+    }
+
+    // 正方形
+    &.square .dots-item {}
+
+    // 圆形
+    &.round .dots-item {
+      border-radius: 20rpx;
+    }
+
+  }
+</style>

+ 31 - 0
components/page/diyComponents/blank/index.vue

@@ -0,0 +1,31 @@
+<template>
+  <!-- 辅助空白 -->
+  <view class="diy-blank" :style="{ height: `${itemStyle.height * 2}rpx`, background: itemStyle.background }">
+  </view>
+</template>
+
+<script>
+  export default {
+
+    /**
+     * 组件的属性列表
+     * 用于组件自定义设置
+     */
+    props: {
+      itemStyle: Object
+    },
+
+    /**
+     * 组件的方法列表
+     * 更新属性和数据的方法与更新页面数据的方法类似
+     */
+    methods: {
+
+    }
+
+  }
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 209 - 0
components/page/diyComponents/coupon/index.vue

@@ -0,0 +1,209 @@
+<template>
+  <!-- 优惠券组 -->
+  <view v-if="couponList.length" class="diy-coupon" :style="{ padding: `${itemStyle.paddingTop * 2}rpx 0`, background: itemStyle.background }">
+    <scroll-view :scroll-x="true">
+      <view class="coupon-wrapper">
+        <view class="coupon-item" :class="{ disable: !dataItem.state.value }" v-for="(dataItem, index) in couponList" :key="index"
+          :style="{ marginRight: `${itemStyle.marginRight * 2}rpx` }">
+          <text class="before" :style="{ background: itemStyle.background }"></text>
+          <view class="left-content" :style="{ background: itemStyle.couponBgColor, color: itemStyle.couponTextColor }">
+            <view class="content-top">
+              <template v-if="dataItem.coupon_type == 10">
+                <text class="unit">¥</text>
+                <text class="price">{{ dataItem.reduce_price }}</text>
+              </template>
+              <template v-if="dataItem.coupon_type == 20">
+                <text class="price">{{ dataItem.discount }}折</text>
+              </template>
+            </view>
+            <view class="content-bottom">
+              <text class="f-22">满{{ dataItem.min_price }}元可用</text>
+            </view>
+          </view>
+          <view class="right-receive" :style="{ background: itemStyle.receiveBgColor, color: itemStyle.receiveTextColor }"
+            @click="handleReceive(index, dataItem)">
+            <block v-if="dataItem.state.value">
+              <text>立即</text>
+              <text>领取</text>
+            </block>
+            <text v-else>{{ dataItem.state.text }}</text>
+          </view>
+        </view>
+      </view>
+    </scroll-view>
+  </view>
+</template>
+
+<script>
+  import mixin from '../mixin'
+  import * as MyCouponApi from '@/api/myCoupon'
+  import { cloneObj } from '@/utils/util'
+
+  export default {
+
+    /**
+     * 组件的属性列表
+     * 用于组件自定义设置
+     */
+    props: {
+      itemIndex: String,
+      itemStyle: Object,
+      params: Object,
+      dataList: Array
+    },
+
+    data() {
+      return {
+        // 优惠券列表
+        couponList: [],
+        // 防止重复提交
+        disable: false
+      }
+    },
+    watch: {
+      // 这里监听dataList并写入到data中, 因为领取事件不能直接修改props中的属性
+      dataList: {
+        handler(data) {
+          this.couponList = cloneObj(data)
+          // console.log(this.couponList)
+        },
+        immediate: true,
+        deep: true
+      }
+    },
+
+    mixins: [mixin],
+
+    /**
+     * 组件的方法列表
+     * 更新属性和数据的方法与更新页面数据的方法类似
+     */
+    methods: {
+
+      // 立即领取事件
+      handleReceive(index, item) {
+        const app = this
+        if (app.disable || !item.state.value|| !item.coupon_id) {
+          return
+        }
+        app.disable = true
+        MyCouponApi.receive(item.coupon_id, {}, { load: false })
+          .then(result => {
+            // 显示领取成功提示
+            app.$success(result.message)
+            // 将优惠券设置为已领取
+            app.setReceived(index, item)
+          })
+          .finally(() => app.disable = false)
+      },
+
+      // 将优惠券设置为已领取
+      setReceived(index, item) {
+        const app = this
+        // #ifdef VUE2
+        app.$set(app.couponList, index, {
+          ...item,
+          state: { value: 0, text: '已领取' }
+        })
+        // #endif
+        // #ifdef VUE3
+        app.couponList[index] = {
+          ...item,
+          state: { value: 0, text: '已领取' }
+        }
+        // #endif
+      }
+
+    }
+
+  }
+</script>
+
+<style lang="scss" scoped>
+  .diy-coupon {
+
+    .coupon-wrapper {
+      display: flex;
+      width: max-content;
+      height: 130rpx;
+      padding: 0 24rpx;
+
+      &::-webkit-scrollbar {
+        display: none;
+      }
+    }
+  }
+
+  .coupon-item {
+    flex-shrink: 0;
+    width: 300rpx;
+    height: 130rpx;
+    position: relative;
+    color: #fff;
+    overflow: hidden;
+    box-sizing: border-box;
+    margin-right: 40rpx;
+    display: flex;
+
+    &:last-child {
+      margin-right: 0 !important;
+    }
+
+    &.disable {
+      .left-content {
+        background: linear-gradient(-113deg, #bdbdbd, #a2a1a2) !important;
+      }
+
+      .right-receive {
+        background-color: #949494 !important;
+      }
+    }
+
+    .before {
+      content: "";
+      position: absolute;
+      z-index: 1;
+      width: 32rpx;
+      height: 32rpx;
+      top: 45%;
+      left: -16rpx;
+      transform: translateY(-50%);
+      border-radius: 80%;
+      background-color: #fff;
+    }
+
+    .left-content {
+      display: flex;
+      flex-direction: column;
+      justify-content: center;
+      align-items: center;
+      position: relative;
+      width: 70%;
+      height: 100%;
+      background-color: #E5004F;
+      font-size: 24rpx;
+
+      .content-top {
+        .unit {
+          font-size: 15px;
+        }
+
+        .price {
+          font-size: 44rpx;
+        }
+      }
+    }
+
+    .right-receive {
+      display: flex;
+      flex-direction: column;
+      justify-content: center;
+      align-items: center;
+      width: 30%;
+      height: 100%;
+      background-color: #4e4e4e;
+      text-align: center;
+      font-size: 24rpx;
+    }
+  }
+</style>

+ 39 - 0
components/page/diyComponents/empty.vue

@@ -0,0 +1,39 @@
+<template>
+  <view class="diy-123123">
+
+  </view>
+</template>
+
+<script>
+  import mixin from '../mixin'
+  
+  export default {
+
+    /**
+     * 组件的属性列表
+     * 用于组件自定义设置
+     */
+    props: {
+      itemIndex: String,
+      itemStyle: Object,
+      params: Object,
+      dataList: Array
+    },
+
+    mixins: [mixin],
+
+    /**
+     * 组件的方法列表
+     * 更新属性和数据的方法与更新页面数据的方法类似
+     */
+    methods: {
+
+
+    }
+
+  }
+</script>
+
+<style lang="scss" scoped>
+  
+</style>

+ 385 - 0
components/page/diyComponents/goods/index.vue

@@ -0,0 +1,385 @@
+<template>
+  <!-- 商品组 -->
+  <scroll-view class="scroll-view" :scroll-x="itemStyle.display === 'slide'" :style="{ background: itemStyle.background }">
+    <view class="diy-goods">
+      <view class="goods-list" :class="[`display-${itemStyle.display}`, `column-${itemStyle.column}`]"
+        :style="{ padding: `${itemStyle.paddingY * 2}rpx ${itemStyle.paddingX * 2}rpx` }">
+        <view class="goods-item" v-for="(dataItm, dataIdx) in dataList" :key="dataIdx" :class="[`display-${itemStyle.cardType}`]"
+          :style="{ marginBottom: `${itemStyle.itemMargin * 2}rpx`, borderRadius: `${itemStyle.borderRadius * 2}rpx` }"
+          @click="handleGoodsItem(dataItm.goods_id)">
+          <!-- 单列商品 -->
+          <template v-if="itemStyle.column === 1">
+            <view class="flex">
+              <view class="goods-item-left">
+                <image class="image" :src="dataItm.goods_image"></image>
+              </view>
+              <view class="goods-item-right">
+                <view class="goods-info">
+                  <view v-if="inArray('goodsName', itemStyle.show)" class="goods-name"
+                    :class="[ itemStyle.goodsNameRows == 'two' ? 'twoline-hide' : 'oneline-hide', `row-${itemStyle.goodsNameRows}` ]">
+                    {{ dataItm.goods_name }}
+                  </view>
+                  <view v-if="inArray('sellingPoint', itemStyle.show)" class="goods-selling">
+                    <text class="selling oneline-hide" :style="{ color: itemStyle.sellingColor }">{{ dataItm.selling_point }}</text>
+                  </view>
+                  <view v-if="inArray('goodsSales', itemStyle.show)" class="goods-sales oneline-hide">
+                    <text class="sales">已售{{ dataItm.goods_sales }}</text>
+                    <!-- <text class="line">|</text>
+                                      <text>惊艳度100%</text>-->
+                  </view>
+                  <view class="footer">
+                    <view class="goods-price oneline-hide" :style="{ color: itemStyle.priceColor }">
+                      <template v-if="inArray('goodsPrice', itemStyle.show)">
+                        <text class="unit">¥</text>
+                        <text class="value">{{ dataItm.goods_price_min }}</text>
+                        <!-- <text class="unit2">到手价</text> -->
+                      </template>
+                      <text v-if="inArray('linePrice', itemStyle.show) && dataItm.line_price_min > 0" class="line-price">
+                        <text class="unit">¥</text>
+                        <text class="value">{{ dataItm.line_price_min }}</text>
+                      </text>
+                    </view>
+                    <view v-show="inArray('cartBtn', itemStyle.show) && itemStyle.column < 3" class="action">
+                      <view class="btn-cart" :style="{ color: itemStyle.btnCartColor }" @click.stop="handleAddCart(dataItm)">
+                        <text class="cart-icon iconfont" :class="[`icon-jiagou${itemStyle.btnCartStyle}`]"></text>
+                      </view>
+                    </view>
+                  </view>
+                </view>
+              </view>
+            </view>
+          </template>
+          <!-- 两列/三列 -->
+          <template v-else>
+            <view class="goods-image">
+              <image class="image" mode="aspectFill" :src="dataItm.goods_image"></image>
+            </view>
+            <view class="goods-info">
+              <view v-if="inArray('goodsName', itemStyle.show)" class="goods-name"
+                :class="[ itemStyle.goodsNameRows == 'two' ? 'twoline-hide' : 'oneline-hide', `row-${itemStyle.goodsNameRows}` ]">
+                {{ dataItm.goods_name }}
+              </view>
+              <view v-if="inArray('sellingPoint', itemStyle.show)" class="goods-selling">
+                <text class="selling oneline-hide" :style="{ color: itemStyle.sellingColor }">{{ dataItm.selling_point }}</text>
+              </view>
+              <view v-if="inArray('goodsSales', itemStyle.show)" class="goods-sales oneline-hide">
+                <text class="sales">已售{{ dataItm.goods_sales }}</text>
+                <!-- <text class="line">|</text>
+                            <text>惊艳度100%</text>-->
+              </view>
+              <view class="footer">
+                <view v-if="inArray('goodsPrice', itemStyle.show)" class="goods-price oneline-hide"
+                  :style="{ color: itemStyle.priceColor }">
+                  <text class="unit">¥</text>
+                  <text class="value">{{ dataItm.goods_price_min }}</text>
+                  <!-- <text class="unit2">到手价</text> -->
+                  <text v-if="inArray('linePrice', itemStyle.show) && dataItm.line_price_min > 0" class="line-price">
+                    <text class="unit">¥</text>
+                    <text class="value">{{ dataItm.line_price_min }}</text>
+                  </text>
+                </view>
+                <view v-show="inArray('cartBtn', itemStyle.show) && itemStyle.column < 3" class="action">
+                  <view class="btn-cart" :style="{ color: itemStyle.btnCartColor }" @click.stop="handleAddCart(dataItm)">
+                    <text class="cart-icon iconfont" :class="[`icon-jiagou${itemStyle.btnCartStyle}`]"></text>
+                  </view>
+                </view>
+              </view>
+            </view>
+          </template>
+        </view>
+      </view>
+    </view>
+    <!-- 加入购物车组件 -->
+    <AddCartPopup ref="AddCartPopup" @addCart="onAddCart" />
+  </scroll-view>
+</template>
+
+<script>
+  import { inArray } from '@/utils/util'
+  import { setCartTabBadge } from '@/core/app'
+  import AddCartPopup from '@/components/add-cart-popup'
+
+  export default {
+    components: {
+      AddCartPopup
+    },
+    props: {
+      itemIndex: String,
+      itemStyle: Object,
+      params: Object,
+      dataList: Array
+    },
+    data() {
+      return { inArray }
+    },
+    methods: {
+
+      // 跳转商品详情页
+      handleGoodsItem(goodsId) {
+        if (goodsId !== undefined) {
+          this.$navTo(`pages/goods/detail`, { goodsId })
+        }
+      },
+
+      // 点击加入购物车
+      handleAddCart(item) {
+        this.$refs.AddCartPopup.handle(item)
+      },
+
+      // 更新购物车角标
+      onAddCart(total) {
+        setCartTabBadge()
+      }
+
+    }
+
+  }
+</script>
+
+<style lang="scss" scoped>
+  .diy-goods {
+    // 解决 margin-bottom: -10px 导致的容器异常
+    display: flex;
+    flex-direction: column;
+    // overflow: hidden; // 会导致横向无法滑动
+
+    .goods-list {
+      margin-bottom: -10px;
+
+      // 横向滑动
+      &.display-slide {
+        display: flex;
+        width: max-content;
+
+        .goods-item {
+          flex-shrink: 1;
+          margin-right: 20rpx !important;
+
+          &:last-child {
+            margin-right: 0 !important;
+          }
+        }
+
+        // 双列
+        &.column-2 {
+          .goods-item {
+            width: 160px;
+          }
+        }
+
+        // 3列
+        &.column-3 {
+          .goods-item {
+            width: 220px;
+          }
+        }
+      }
+
+      // 列表平铺
+      &.display-list {
+        display: flex;
+        flex-wrap: wrap;
+      }
+
+      // 单列
+      &.column-1 {
+        margin-bottom: 0;
+
+        .goods-item {
+          margin-bottom: 12rpx;
+          padding: 20rpx;
+          box-sizing: border-box;
+          background: #fff;
+          flex-basis: 100%;
+          margin-right: 0;
+
+          &:last-child {
+            margin-bottom: 0 !important;
+          }
+
+          .goods-info {
+            padding: 0;
+
+            .footer {
+              position: absolute;
+              bottom: 16rpx;
+              width: 100%;
+            }
+          }
+
+        }
+
+        // 商品图片
+        .goods-item-left {
+          background: #fff;
+          margin-right: 20rpx;
+
+          .image {
+            display: block;
+            width: 240rpx;
+            height: 240rpx;
+          }
+        }
+
+        .goods-item-right {
+          position: relative;
+          flex: 1;
+          width: 0; // 解决文字超出无法隐藏
+        }
+
+      }
+
+      // 2列
+      &.column-2 {
+        .goods-item {
+          flex-basis: 48.6%;
+
+          &:nth-child(2n) {
+            margin-right: 0;
+          }
+        }
+      }
+
+      // 3列
+      &.column-3 {
+        .goods-item {
+          flex-basis: 31.46666%;
+
+          &:nth-child(3n) {
+            margin-right: 0;
+          }
+        }
+
+        .goods-name {
+          font-size: 26rpx !important;
+        }
+      }
+
+      // 商品内容
+      .goods-item {
+        flex-shrink: 0;
+        background-color: #fff;
+        flex: 0 1 48.6%;
+        margin-right: 2.8%;
+        border-radius: 16rpx;
+        overflow: hidden;
+        margin-bottom: 20rpx;
+
+        &.display-card {
+          box-shadow: 0 4rpx 10rpx rgba(0, 0, 0, 0.07);
+        }
+
+        .goods-image {
+          position: relative;
+          width: 100%;
+          height: 0;
+          padding-bottom: 100%;
+          overflow: hidden;
+
+          &:after {
+            content: '';
+            display: block;
+            margin-top: 100%;
+          }
+
+          .image {
+            position: absolute;
+            top: 0;
+            left: 0;
+            display: block;
+            width: 100%;
+            height: 100%;
+            object-fit: cover;
+          }
+        }
+
+        .goods-info {
+          padding: 20rpx;
+
+          .goods-name {
+            font-size: 27rpx;
+            color: #000;
+            margin-bottom: 8rpx;
+            // line-height: 1.3; // 用户端
+
+            &.row-two {
+              // min-height: 68rpx; // 用户端
+            }
+          }
+
+          .goods-selling {
+            display: flex; // 解决文字超出无法隐藏
+            font-size: 24rpx;
+            margin-bottom: 8rpx;
+            min-height: 38rpx;
+          }
+
+          .goods-sales {
+            font-size: 24rpx;
+            color: #959595;
+            margin-bottom: 8rpx;
+
+            .line {
+              margin: 0 8rpx;
+            }
+          }
+
+        }
+
+        .footer {
+          display: flex;
+          justify-content: space-between;
+          align-items: center;
+
+          .goods-price {
+            position: relative;
+            color: #ff1051;
+
+            .unit {
+              font-size: 20rpx;
+            }
+
+            .value {
+              font-size: 30rpx;
+            }
+
+            .unit2 {
+              margin-left: 4rpx;
+              font-size: 22rpx;
+            }
+
+            .line-price {
+              margin-left: 6rpx;
+              color: #959595;
+
+              .unit {
+                text-decoration: line-through;
+                font-size: 22rpx;
+              }
+
+              .value {
+                text-decoration: line-through;
+                font-size: 22rpx;
+              }
+            }
+
+          }
+
+          .action {
+
+            .btn-cart {
+              text-align: center;
+
+              .cart-icon {
+                font-size: 36rpx;
+              }
+            }
+
+          }
+
+        }
+
+      }
+    }
+  }
+</style>

+ 36 - 0
components/page/diyComponents/guide/index.vue

@@ -0,0 +1,36 @@
+<template>
+  <!-- 辅助线 -->
+  <view class="diy-guide" :style="{ padding: `${itemStyle.paddingTop * 2}rpx 0`, background: itemStyle.background }">
+    <view class="line" :style="{ borderTop: `${itemStyle.lineHeight * 2}rpx ${itemStyle.lineStyle} ${itemStyle.lineColor}` }">
+    </view>
+  </view>
+</template>
+
+<script>
+
+  export default {
+
+    /**
+     * 组件的属性列表
+     * 用于组件自定义设置
+     */
+    props: {
+      itemStyle: Object
+    },
+
+    /**
+     * 组件的方法列表
+     * 更新属性和数据的方法与更新页面数据的方法类似
+     */
+    methods: {
+
+    }
+
+  }
+</script>
+
+<style lang="scss" scoped>
+  .diy-guide .line {
+    width: 100%;
+  }
+</style>

+ 64 - 0
components/page/diyComponents/hotZone/index.vue

@@ -0,0 +1,64 @@
+<template>
+  <!-- 热区 -->
+  <view class="diy-hotZone"
+    :style="{ padding: `${itemStyle.paddingTop * 2}rpx ${itemStyle.paddingLeft * 2}rpx`, background: itemStyle.background }">
+    <view class="bg-image" :style="{ borderRadius: `${itemStyle.borderRadius * 2}rpx` }">
+      <image class="image" :src="data.imgUrl" mode="widthFix"></image>
+    </view>
+    <view class="zone-item" v-for="(item, index) in data.maps" :key="index"
+      :style="{ width: `${item.width}rpx`, height: `${item.height}rpx`, left: `${item.left}rpx`, top: `${item.top}rpx` }"
+      @click="onLink(item.link)">
+    </view>
+  </view>
+</template>
+
+<script>
+  import mixin from '../mixin'
+
+  export default {
+
+    /**
+     * 组件的属性列表
+     * 用于组件自定义设置
+     */
+    props: {
+      itemIndex: String,
+      itemStyle: Object,
+      params: Object,
+      data: Object
+    },
+
+    mixins: [mixin],
+
+    /**
+     * 组件的方法列表
+     * 更新属性和数据的方法与更新页面数据的方法类似
+     */
+    methods: {
+
+    }
+
+  }
+</script>
+
+<style lang="scss" scoped>
+  .diy-hotZone {
+    position: relative;
+    overflow: hidden;
+  }
+
+  .bg-image {
+    overflow: hidden;
+    z-index: 1;
+
+    .image {
+      display: block;
+      width: 100%;
+    }
+  }
+
+  .zone-item {
+    position: absolute;
+    z-index: 2;
+  }
+</style>

+ 44 - 0
components/page/diyComponents/image/index.vue

@@ -0,0 +1,44 @@
+<template>
+  <!-- 单图组 -->
+  <view class="diy-imageSingle"
+    :style="{ padding: `${itemStyle.paddingTop * 2}rpx ${itemStyle.paddingLeft * 2}rpx`, background: itemStyle.background }">
+    <view class="item-image" v-for="(dataItem, index) in dataList" :key="index"
+      :style="{ marginBottom: `${itemStyle.itemMargin * 2}rpx`, borderRadius: `${itemStyle.borderRadius * 2}rpx` }">
+      <view class="nav-to" @click="onLink(dataItem.link)">
+        <image class="image" :src="dataItem.imgUrl" mode="widthFix"></image>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+  import mixin from '../mixin'
+
+  export default {
+    props: {
+      itemIndex: String,
+      itemStyle: Object,
+      params: Object,
+      dataList: Array
+    },
+    mixins: [mixin],
+    methods: {
+
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .item-image {
+    overflow: hidden;
+
+    &:last-child {
+      margin-bottom: 0 !important;
+    }
+
+    .image {
+      display: block;
+      width: 100%;
+    }
+  }
+</style>

+ 17 - 0
components/page/diyComponents/mixin.js

@@ -0,0 +1,17 @@
+import { onLink } from '@/core/app'
+import { urlDecode } from '@/utils/util'
+
+export default {
+  data() {
+    return {}
+  },
+  methods: {
+
+    /**
+     * link对象点击事件
+     * 支持tabBar页面
+     */
+    onLink
+  },
+
+}

+ 94 - 0
components/page/diyComponents/navBar/index.vue

@@ -0,0 +1,94 @@
+<template>
+  <!-- 导航组 -->
+  <view class="diy-navBar"
+    :style="{ padding: `${itemStyle.paddingTop}px 0`, background: itemStyle.background, color: itemStyle.textColor }">
+    <view class="data-list" :class="[`avg-sm-${itemStyle.rowsNum}`]">
+      <view class="item-nav" v-for="(dataItem, index) in dataList" :key="index" @click="onLink(dataItem.link)">
+        <view class="item-image">
+          <image class="image" :style="{ width: `${itemStyle.imageSize * 2}rpx`, height: `${itemStyle.imageSize * 2}rpx` }" mode="scaleToFill"
+            :src="dataItem.imgUrl"></image>
+        </view>
+        <view class="item-text oneline-hide">{{ dataItem.text }}</view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+  import mixin from '../mixin'
+
+  export default {
+    name: "NavBar",
+    /**
+     * 组件的属性列表
+     * 用于组件自定义设置
+     */
+    props: {
+      itemIndex: String,
+      itemStyle: Object,
+      params: Object,
+      dataList: Array
+    },
+
+    mixins: [mixin],
+
+    /**
+     * 组件的方法列表
+     * 更新属性和数据的方法与更新页面数据的方法类似
+     */
+    methods: {
+
+    }
+
+  }
+</script>
+
+<style lang="scss" scoped>
+  .diy-navBar .data-list::after {
+    clear: both;
+    content: " ";
+    display: table;
+  }
+
+  .diy-navBar {
+    display: flex;
+    flex-direction: column;
+
+    .data-list {
+      margin-bottom: -20rpx;
+    }
+
+    .item-nav {
+      float: left;
+      text-align: center;
+      margin-bottom: 20rpx;
+      font-size: 26rpx;
+
+      .item-image {
+        text-align: center;
+        margin-bottom: 8rpx;
+
+        .image {
+          display: block;
+          width: 88rpx;
+          height: 88rpx;
+          margin: 0 auto;
+        }
+      }
+    }
+
+    // 分列布局
+    .avg-sm-3>.item-nav {
+      width: 33.33333333%;
+    }
+
+    .avg-sm-4>.item-nav {
+      width: 25%;
+    }
+
+    .avg-sm-5>.item-nav {
+      width: 20%;
+    }
+
+  }
+</style>

+ 40 - 0
components/page/diyComponents/notice/index.vue

@@ -0,0 +1,40 @@
+<template>
+  <!-- 店铺公告 -->
+  <view class="diy-notice" :style="{ padding: `${itemStyle.paddingTop * 2}rpx 0` }"
+    @click="onLink(params.link)">
+    <u-notice-bar padding="12rpx 24rpx" :volume-icon="params.showIcon" :autoplay="params.scrollable"
+      :bg-color="itemStyle.background" :color="itemStyle.textColor" :list="[params.text]" :font-size="itemStyle.fontSize * 2"></u-notice-bar>
+  </view>
+</template>
+
+<script>
+  import mixin from '../mixin'
+
+  export default {
+
+    /**
+     * 组件的属性列表
+     * 用于组件自定义设置
+     */
+    props: {
+      itemStyle: Object,
+      params: Object
+    },
+
+    mixins: [mixin],
+
+    /**
+     * 组件的方法列表
+     * 更新属性和数据的方法与更新页面数据的方法类似
+     */
+    methods: {
+
+
+    }
+
+  }
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 39 - 0
components/page/diyComponents/officialAccount/index.vue

@@ -0,0 +1,39 @@
+<template>
+  <!-- 关注公众号 -->
+  <!-- #ifdef MP-WEIXIN -->
+  <view class="diy-officialAccount">
+    <official-account></official-account>
+  </view>
+  <!-- #endif -->
+</template>
+
+<script>
+  import mixin from '../mixin'
+
+  export default {
+
+    /**
+     * 组件的属性列表
+     * 用于组件自定义设置
+     */
+    props: {
+      itemIndex: String
+    },
+
+    mixins: [mixin],
+
+    /**
+     * 组件的方法列表
+     * 更新属性和数据的方法与更新页面数据的方法类似
+     */
+    methods: {
+
+
+    }
+
+  }
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 33 - 0
components/page/diyComponents/richText/index.vue

@@ -0,0 +1,33 @@
+<template>
+  <!-- 富文本 -->
+  <view class="diy-richText"
+    :style="{ padding: `${itemStyle.paddingTop * 2}rpx ${itemStyle.paddingLeft * 2}rpx`, background: itemStyle.background }">
+    <mp-html :content="params.content" />
+  </view>
+</template>
+
+<script>
+  export default {
+    /**
+     * 组件的属性列表
+     * 用于组件自定义设置
+     */
+    props: {
+      itemStyle: Object,
+      params: Object
+    },
+    /**
+     * 组件的方法列表
+     * 更新属性和数据的方法与更新页面数据的方法类似
+     */
+    methods: {
+
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .diy-richText {
+    font-size: 28rpx;
+  }
+</style>

+ 84 - 0
components/page/diyComponents/search/index.vue

@@ -0,0 +1,84 @@
+<template>
+  <!-- 搜索框 -->
+  <view class="diy-search" :class="{ sticky: params.sticky }"
+    :style="{ background: itemStyle.background, padding: `${itemStyle.paddingY * 2}rpx ${itemStyle.paddingX * 2}rpx` }">
+    <view class="inner" :class="itemStyle.searchStyle" @click="onTargetSearch">
+      <view class="search-input"
+        :style="{ textAlign: itemStyle.textAlign, background: itemStyle.searchBg, color: itemStyle.searchFontColor }">
+        <text class="search-icon iconfont icon-search"></text>
+        <text> {{ params.placeholder }}</text>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+  export default {
+
+    /**
+     * 组件的属性列表
+     * 用于组件自定义设置
+     */
+    props: {
+      itemIndex: String,
+      itemStyle: Object,
+      params: Object
+    },
+
+    /**
+     * 组件的方法列表
+     * 更新属性和数据的方法与更新页面数据的方法类似
+     */
+    methods: {
+
+      /**
+       * 跳转到搜索页面
+       */
+      onTargetSearch() {
+        this.$navTo('pages/search/index')
+      }
+
+    }
+
+  }
+</script>
+
+<style lang="scss" scoped>
+  .diy-search {
+    background: #f1f1f2;
+    padding: 20rpx 20rpx;
+
+    &.sticky {
+      position: sticky;
+      top: var(--window-top);
+      z-index: 100;
+    }
+  }
+
+  .inner {
+    height: 60rpx;
+    background: #fff;
+    overflow: hidden;
+
+    &.radius {
+      border-radius: 10rpx;
+    }
+
+    &.round {
+      border-radius: 60rpx;
+    }
+
+    .search-input {
+      height: 60rpx;
+      //width: 100%;
+      line-height: 60rpx;
+      color: #999999;
+      padding: 0 24rpx;
+      font-size: 26rpx;
+
+      .search-icon {
+        margin-right: 12rpx;
+      }
+    }
+  }
+</style>

+ 93 - 0
components/page/diyComponents/service/index.vue

@@ -0,0 +1,93 @@
+<template>
+  <!-- 在线客服 -->
+  <view v-if="isShow" class="diy-service" :style="{ '--right': `${itemStyle.right * 2}rpx`, '--bottom': `${itemStyle.bottom * 2}rpx` }">
+    <!-- 拨打电话 -->
+    <block v-if="params.type === 'phone'">
+      <view class="service-icon" @click="onMakePhoneCall">
+        <image class="image" :src="params.image"></image>
+      </view>
+    </block>
+    <!-- 在线聊天 -->
+    <block v-else-if="params.type === 'chat'">
+      <customer-btn>
+        <view class="service-icon">
+          <image class="image" :src="params.image"></image>
+        </view>
+      </customer-btn>
+    </block>
+  </view>
+</template>
+
+<script>
+  import CustomerBtn from '@/components/customer-btn'
+  // import { rpx2px } from '@/utils/util'
+  import SettingModel from '@/common/model/Setting'
+
+  export default {
+    components: {
+      CustomerBtn
+    },
+    props: {
+      itemStyle: Object,
+      params: Object
+    },
+    data() {
+      return {
+        isShow: false
+      }
+    },
+    // computed: {
+    //   right() {
+    //     return rpx2px(2 * this.itemStyle.right)
+    //   },
+    //   bottom() {
+    //     return rpx2px(2 * this.itemStyle.bottom)
+    //   }
+    // },
+    async created() {
+      if (this.params.type === 'phone') {
+        this.isShow = true
+      }
+      if (this.params.type === 'chat') {
+        this.isShow = await SettingModel.isShowCustomerBtn()
+      }
+    },
+    methods: {
+
+      /**
+       * 点击拨打电话
+       */
+      onMakePhoneCall(e) {
+        uni.makePhoneCall({
+          phoneNumber: this.params.tel
+        })
+      }
+
+    }
+
+  }
+</script>
+
+<style lang="scss" scoped>
+  .diy-service {
+    position: fixed;
+    z-index: 999;
+    right: calc(var(--window-right) + var(--right));
+    // 设置ios刘海屏底部横线安全区域
+    bottom: calc(constant(safe-area-inset-bottom) + var(--window-bottom) + var(--bottom));
+    bottom: calc(env(safe-area-inset-bottom) + var(--window-bottom) + var(--bottom));
+
+    .service-icon {
+      padding: 10rpx;
+
+      .image {
+        display: block;
+        width: 90rpx;
+        height: 90rpx;
+        border-radius: 50%;
+        box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
+      }
+    }
+
+  }
+</style>

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff