Browse Source

首页,金刚区跳转

WangYiHan 9 months ago
parent
commit
4696d45bc5
100 changed files with 13398 additions and 13 deletions
  1. 1 1
      api/promotion/decorate.js
  2. 53 6
      main.js
  3. 126 1
      package-lock.json
  4. 5 1
      package.json
  5. 12 2
      pages.json
  6. 3 2
      pages/goods_cate/goods_cate.vue
  7. 107 0
      pages/index/components/magicCube.vue
  8. 1209 0
      pages/index/index_new - 副本 (2).vue
  9. 1222 0
      pages/index/index_new.vue
  10. 492 0
      pages/index/service/list.vue
  11. 45 0
      sheep/api/infra/file.js
  12. 53 0
      sheep/api/member/address.js
  13. 132 0
      sheep/api/member/auth.js
  14. 19 0
      sheep/api/member/point.js
  15. 37 0
      sheep/api/member/signin.js
  16. 54 0
      sheep/api/member/social.js
  17. 85 0
      sheep/api/member/user.js
  18. 21 0
      sheep/api/migration/app.js
  19. 14 0
      sheep/api/migration/chat.js
  20. 15 0
      sheep/api/migration/index.js
  21. 44 0
      sheep/api/migration/third.js
  22. 14 0
      sheep/api/pay/channel.js
  23. 22 0
      sheep/api/pay/order.js
  24. 68 0
      sheep/api/pay/wallet.js
  25. 21 0
      sheep/api/product/category.js
  26. 22 0
      sheep/api/product/comment.js
  27. 54 0
      sheep/api/product/favorite.js
  28. 39 0
      sheep/api/product/history.js
  29. 41 0
      sheep/api/product/spu.js
  30. 16 0
      sheep/api/promotion/activity.js
  31. 12 0
      sheep/api/promotion/article.js
  32. 76 0
      sheep/api/promotion/combination.js
  33. 101 0
      sheep/api/promotion/coupon.js
  34. 38 0
      sheep/api/promotion/diy.js
  35. 14 0
      sheep/api/promotion/rewardActivity.js
  36. 33 0
      sheep/api/promotion/seckill.js
  37. 13 0
      sheep/api/system/area.js
  38. 63 0
      sheep/api/trade/afterSale.js
  39. 87 0
      sheep/api/trade/brokerage.js
  40. 50 0
      sheep/api/trade/cart.js
  41. 13 0
      sheep/api/trade/config.js
  42. 13 0
      sheep/api/trade/delivery.js
  43. 146 0
      sheep/api/trade/order.js
  44. 105 0
      sheep/components/s-activity-pop/s-activity-pop.vue
  45. 112 0
      sheep/components/s-address-item/s-address-item.vue
  46. 107 0
      sheep/components/s-auth-modal/components/account-login.vue
  47. 127 0
      sheep/components/s-auth-modal/components/change-mobile.vue
  48. 106 0
      sheep/components/s-auth-modal/components/change-password.vue
  49. 152 0
      sheep/components/s-auth-modal/components/mp-authorization.vue
  50. 119 0
      sheep/components/s-auth-modal/components/reset-password.vue
  51. 119 0
      sheep/components/s-auth-modal/components/sms-login.vue
  52. 151 0
      sheep/components/s-auth-modal/index.scss
  53. 239 0
      sheep/components/s-auth-modal/s-auth-modal.vue
  54. 81 0
      sheep/components/s-block-item/s-block-item.vue
  55. 54 0
      sheep/components/s-block/s-block.vue
  56. 173 0
      sheep/components/s-count-down/s-count-down.vue
  57. 152 0
      sheep/components/s-coupon-block/s-coupon-block.vue
  58. 79 0
      sheep/components/s-coupon-card/s-coupon-card.vue
  59. 109 0
      sheep/components/s-coupon-get/s-coupon-get.vue
  60. 205 0
      sheep/components/s-coupon-list/s-coupon-list.vue
  61. 138 0
      sheep/components/s-coupon-select/s-coupon-select.vue
  62. 66 0
      sheep/components/s-custom-navbar/components/navbar-item.vue
  63. 314 0
      sheep/components/s-custom-navbar/components/navbar.vue
  64. 196 0
      sheep/components/s-custom-navbar/s-custom-navbar.vue
  65. 114 0
      sheep/components/s-discount-list/s-discount-list.vue
  66. 93 0
      sheep/components/s-empty/s-empty.vue
  67. 88 0
      sheep/components/s-float-menu/s-float-menu.vue
  68. 286 0
      sheep/components/s-goods-card/s-goods-card.vue
  69. 721 0
      sheep/components/s-goods-column/s-goods-column.vue
  70. 181 0
      sheep/components/s-goods-item/s-goods-item.vue
  71. 33 0
      sheep/components/s-goods-scroll/s-goods-scroll.vue
  72. 147 0
      sheep/components/s-goods-shelves/s-goods-shelves.vue
  73. 154 0
      sheep/components/s-groupon-block/s-groupon-block.vue
  74. 46 0
      sheep/components/s-hotzone-block/s-hotzone-block.vue
  75. 44 0
      sheep/components/s-image-banner/s-image-banner.vue
  76. 27 0
      sheep/components/s-image-block/s-image-block.vue
  77. 110 0
      sheep/components/s-image-cube/s-image-cube.vue
  78. 242 0
      sheep/components/s-layout/s-layout.vue
  79. 15 0
      sheep/components/s-line-block/s-line-block.vue
  80. 144 0
      sheep/components/s-live-block/s-live-block.vue
  81. 234 0
      sheep/components/s-live-card/s-live-card.vue
  82. 363 0
      sheep/components/s-menu-button/s-menu-button.vue
  83. 82 0
      sheep/components/s-menu-grid/s-menu-grid.vue
  84. 66 0
      sheep/components/s-menu-list/s-menu-list.vue
  85. 118 0
      sheep/components/s-menu-tools/s-menu-tools.vue
  86. 38 0
      sheep/components/s-notice-block/s-notice-block.vue
  87. 108 0
      sheep/components/s-order-card/s-order-card.vue
  88. 85 0
      sheep/components/s-popup-image/s-popup-image.vue
  89. 40 0
      sheep/components/s-richtext-block/s-richtext-block.vue
  90. 164 0
      sheep/components/s-search-block/s-search-block.vue
  91. 160 0
      sheep/components/s-seckill-block/s-seckill-block.vue
  92. 472 0
      sheep/components/s-select-groupon-sku/s-select-groupon-sku.vue
  93. 432 0
      sheep/components/s-select-seckill-sku/s-select-seckill-sku.vue
  94. 406 0
      sheep/components/s-select-sku/s-select-sku.vue
  95. 161 0
      sheep/components/s-share-modal/canvas-poster/index.vue
  96. 121 0
      sheep/components/s-share-modal/canvas-poster/poster/goods.js
  97. 114 0
      sheep/components/s-share-modal/canvas-poster/poster/groupon.js
  98. 32 0
      sheep/components/s-share-modal/canvas-poster/poster/index.js
  99. 61 0
      sheep/components/s-share-modal/canvas-poster/poster/user.js
  100. 87 0
      sheep/components/s-share-modal/canvas-poster/useCanvas.js

+ 1 - 1
api/promotion/decorate.js

@@ -1,7 +1,7 @@
 import request from "@/utils/request.js";
 
 export function getDecorateComponentListByPage(page) {
-  return request.get("app-api/promotion/decorate/list", {
+  return request.get("app-api/promotion/diy-template/used", {
     page
   }, {
     noAuth: true // TODO 芋艿:后续要做调整

+ 53 - 6
main.js

@@ -6,6 +6,7 @@ import util from 'utils/util'
 import configs from './config/app.js'
 import * as Order from './libs/order';
 import uView from "uview-ui";
+
 Vue.use(uView);
 
 Vue.prototype.$util = util;
@@ -22,9 +23,13 @@ Vue.prototype.$Order = Order;
 // #endif
 
 // #ifdef H5
-import { parseQuery } from "./utils";
+import {
+	parseQuery
+} from "./utils";
 import Auth from './libs/wechat';
-import { SPREAD } from './config/cache';
+import {
+	SPREAD
+} from './config/cache';
 Vue.prototype.$wechat = Auth;
 let cookieName = "VCONSOLE",
 	query = parseQuery(),
@@ -44,8 +49,8 @@ if (urlSpread !== undefined) {
 }
 
 if (vconsole !== undefined) {
-  if (vconsole === md5UnCrmeb && Cache.has(cookieName))
-	  Cache.clear(cookieName);
+	if (vconsole === md5UnCrmeb && Cache.has(cookieName))
+		Cache.clear(cookieName);
 } else vconsole = Cache.get(cookieName);
 
 import VConsole from './components/vconsole.min.js'
@@ -61,10 +66,52 @@ if (vconsole !== undefined && vconsole === md5Crmeb) {
 
 App.mpType = 'app'
 
+import * as DecorateApi from '@/api/promotion/decorate.js';
+DecorateApi.getDecorateComponentListByPage(1).then(res => {
+	// TODO 芋艿:暂时写死
+	// uni.setNavigationBarTitle({
+	// 	title: '首页'
+	// })
+	// this.$set(this, "logoUrl", 'https://static.iocoder.cn/ruoyi-vue-pro-logo.png');
+	// this.$set(this, "site_name", '首页');
+	// 将装修内容存到vuex
+	store.commit("TEMPLATE", res.data.home);
+	// // #ifdef H5
+	// this.$store.commit("SET_CHATURL",
+	// 	'https://cschat.antcloud.com.cn/index.htm?tntInstId=jm7_c46J&scene=SCE01197657');
+	// Cache.set('chatUrl', 'https://cschat.antcloud.com.cn/index.htm?tntInstId=jm7_c46J&scene=SCE01197657');
+	// // #endif
+
+	// // 轮播图
+	// const slideShow = res.data.find(item => item.code === 'slide-show');
+	// if (slideShow) {
+	// 	this.$set(this, "slideShows", JSON.parse(slideShow.value));
+	// }
+	// // 菜单
+	// const menu = res.data.find(item => item.code === 'menu');
+	// if (menu) {
+	// 	this.$set(this, "menus", JSON.parse(menu.value));
+	// }
+	// // 滚动新闻
+	// const scrollingNews = res.data.find(item => item.code === 'scrolling-news');
+	// if (scrollingNews) {
+	// 	this.$set(this, "scrollingNews", JSON.parse(scrollingNews.value));
+	// }
+	// // 商品推荐
+	// const productRecommend = res.data.find(item => item.code === 'product-recommend');
+	// if (productRecommend) {
+	// 	this.$set(this, "productRecommends", JSON.parse(productRecommend.value));
+	// 	if (this.productRecommends.length > 0) {
+	// 		this.goodType = this.productRecommends[0].type
+	// 		this.getGroomList();
+	// 	}
+	// }
+})
+
 
 const app = new Vue({
-    ...App,
+	...App,
 	store,
 	Cache
 })
-app.$mount();
+app.$mount();

+ 126 - 1
package-lock.json

@@ -7,9 +7,13 @@
       "dependencies": {
         "@amap/amap-jsapi-loader": "^1.0.1",
         "@vant/weapp": "^1.11.6",
+        "dayjs": "^1.11.12",
+        "luch-request": "^3.1.1",
+        "pinia": "^2.1.7",
         "uview-ui": "^1.8.8",
         "vant": "^2.13.2",
-        "vconsole": "^3.15.1"
+        "vconsole": "^3.15.1",
+        "weixin-js-sdk": "^1.6.5"
       },
       "devDependencies": {
         "sass-loader": "^14.2.1"
@@ -43,6 +47,11 @@
         "node": ">=6.9.0"
       }
     },
+    "node_modules/@dcloudio/types": {
+      "version": "2.6.12",
+      "resolved": "https://registry.npmjs.org/@dcloudio/types/-/types-2.6.12.tgz",
+      "integrity": "sha512-mrCMwcINy1IFjU9VUqLeWBkj404yWs5paLDttBcA+eqUjanuUQbBcTVPqlrGgkyzLXDcV2oDDZRSNxNpXi4kMQ=="
+    },
     "node_modules/@jridgewell/sourcemap-codec": {
       "version": "1.4.15",
       "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
@@ -119,6 +128,11 @@
         "@vue/shared": "3.4.31"
       }
     },
+    "node_modules/@vue/devtools-api": {
+      "version": "6.6.3",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.3.tgz",
+      "integrity": "sha512-0MiMsFma/HqA6g3KLKn+AGpL1kgKhFWszC9U29NfpWK5LE7bjeXxySWJrOJ77hBz+TBrBQ7o4QJqbPbqbs8rJw=="
+    },
     "node_modules/@vue/reactivity": {
       "version": "3.4.31",
       "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.31.tgz",
@@ -196,6 +210,11 @@
       "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
       "peer": true
     },
+    "node_modules/dayjs": {
+      "version": "1.11.12",
+      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.12.tgz",
+      "integrity": "sha512-Rt2g+nTbLlDWZTwwrIXjy9MeiZmSDI375FvZs72ngxx8PDC6YXOeR3q5LAuPzjZQxhiWdRKac7RKV+YyQYfYIg=="
+    },
     "node_modules/entities": {
       "version": "4.5.0",
       "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
@@ -214,6 +233,14 @@
       "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
       "peer": true
     },
+    "node_modules/luch-request": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/luch-request/-/luch-request-3.1.1.tgz",
+      "integrity": "sha512-p7+mlcEtgRcd0OfXC4XZbyiwSr1XgCeqNT7LlVUjnk7InYl/8d5Rk7BUqAYNA2WRafI1wRIUQWRWZRpeUwWR0w==",
+      "dependencies": {
+        "@dcloudio/types": "^2.0.16"
+      }
+    },
     "node_modules/magic-string": {
       "version": "0.30.10",
       "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz",
@@ -258,6 +285,31 @@
       "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==",
       "peer": true
     },
+    "node_modules/pinia": {
+      "version": "2.1.7",
+      "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.1.7.tgz",
+      "integrity": "sha512-+C2AHFtcFqjPih0zpYuvof37SFxMQ7OEG2zV9jRI12i9BOy3YQVAHwdKtyyc8pDcDyIc33WCIsZaCFWU7WWxGQ==",
+      "dependencies": {
+        "@vue/devtools-api": "^6.5.0",
+        "vue-demi": ">=0.14.5"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/posva"
+      },
+      "peerDependencies": {
+        "@vue/composition-api": "^1.4.0",
+        "typescript": ">=4.4.4",
+        "vue": "^2.6.14 || ^3.3.0"
+      },
+      "peerDependenciesMeta": {
+        "@vue/composition-api": {
+          "optional": true
+        },
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/postcss": {
       "version": "8.4.39",
       "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz",
@@ -392,10 +444,40 @@
         }
       }
     },
+    "node_modules/vue-demi": {
+      "version": "0.14.10",
+      "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
+      "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
+      "hasInstallScript": true,
+      "bin": {
+        "vue-demi-fix": "bin/vue-demi-fix.js",
+        "vue-demi-switch": "bin/vue-demi-switch.js"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      },
+      "peerDependencies": {
+        "@vue/composition-api": "^1.0.0-rc.1",
+        "vue": "^3.0.0-0 || ^2.6.0"
+      },
+      "peerDependenciesMeta": {
+        "@vue/composition-api": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/vue-lazyload": {
       "version": "1.2.3",
       "resolved": "https://registry.npmjs.org/vue-lazyload/-/vue-lazyload-1.2.3.tgz",
       "integrity": "sha512-DC0ZwxanbRhx79tlA3zY5OYJkH8FYp3WBAnAJbrcuoS8eye1P73rcgAZhyxFSPUluJUTelMB+i/+VkNU/qVm7g=="
+    },
+    "node_modules/weixin-js-sdk": {
+      "version": "1.6.5",
+      "resolved": "https://registry.npmjs.org/weixin-js-sdk/-/weixin-js-sdk-1.6.5.tgz",
+      "integrity": "sha512-Gph1WAWB2YN/lMOFB/ymb+hbU/wYazzJgu6PMMktCy9cSCeW5wA6Zwt0dpahJbJ+RJEwtTv2x9iIu0U4enuVSQ=="
     }
   },
   "dependencies": {
@@ -418,6 +500,11 @@
         "regenerator-runtime": "^0.14.0"
       }
     },
+    "@dcloudio/types": {
+      "version": "2.6.12",
+      "resolved": "https://registry.npmjs.org/@dcloudio/types/-/types-2.6.12.tgz",
+      "integrity": "sha512-mrCMwcINy1IFjU9VUqLeWBkj404yWs5paLDttBcA+eqUjanuUQbBcTVPqlrGgkyzLXDcV2oDDZRSNxNpXi4kMQ=="
+    },
     "@jridgewell/sourcemap-codec": {
       "version": "1.4.15",
       "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
@@ -494,6 +581,11 @@
         "@vue/shared": "3.4.31"
       }
     },
+    "@vue/devtools-api": {
+      "version": "6.6.3",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.3.tgz",
+      "integrity": "sha512-0MiMsFma/HqA6g3KLKn+AGpL1kgKhFWszC9U29NfpWK5LE7bjeXxySWJrOJ77hBz+TBrBQ7o4QJqbPbqbs8rJw=="
+    },
     "@vue/reactivity": {
       "version": "3.4.31",
       "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.31.tgz",
@@ -557,6 +649,11 @@
       "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
       "peer": true
     },
+    "dayjs": {
+      "version": "1.11.12",
+      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.12.tgz",
+      "integrity": "sha512-Rt2g+nTbLlDWZTwwrIXjy9MeiZmSDI375FvZs72ngxx8PDC6YXOeR3q5LAuPzjZQxhiWdRKac7RKV+YyQYfYIg=="
+    },
     "entities": {
       "version": "4.5.0",
       "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
@@ -569,6 +666,14 @@
       "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
       "peer": true
     },
+    "luch-request": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/luch-request/-/luch-request-3.1.1.tgz",
+      "integrity": "sha512-p7+mlcEtgRcd0OfXC4XZbyiwSr1XgCeqNT7LlVUjnk7InYl/8d5Rk7BUqAYNA2WRafI1wRIUQWRWZRpeUwWR0w==",
+      "requires": {
+        "@dcloudio/types": "^2.0.16"
+      }
+    },
     "magic-string": {
       "version": "0.30.10",
       "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz",
@@ -601,6 +706,15 @@
       "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==",
       "peer": true
     },
+    "pinia": {
+      "version": "2.1.7",
+      "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.1.7.tgz",
+      "integrity": "sha512-+C2AHFtcFqjPih0zpYuvof37SFxMQ7OEG2zV9jRI12i9BOy3YQVAHwdKtyyc8pDcDyIc33WCIsZaCFWU7WWxGQ==",
+      "requires": {
+        "@vue/devtools-api": "^6.5.0",
+        "vue-demi": ">=0.14.5"
+      }
+    },
     "postcss": {
       "version": "8.4.39",
       "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz",
@@ -673,10 +787,21 @@
         "@vue/shared": "3.4.31"
       }
     },
+    "vue-demi": {
+      "version": "0.14.10",
+      "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
+      "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
+      "requires": {}
+    },
     "vue-lazyload": {
       "version": "1.2.3",
       "resolved": "https://registry.npmjs.org/vue-lazyload/-/vue-lazyload-1.2.3.tgz",
       "integrity": "sha512-DC0ZwxanbRhx79tlA3zY5OYJkH8FYp3WBAnAJbrcuoS8eye1P73rcgAZhyxFSPUluJUTelMB+i/+VkNU/qVm7g=="
+    },
+    "weixin-js-sdk": {
+      "version": "1.6.5",
+      "resolved": "https://registry.npmjs.org/weixin-js-sdk/-/weixin-js-sdk-1.6.5.tgz",
+      "integrity": "sha512-Gph1WAWB2YN/lMOFB/ymb+hbU/wYazzJgu6PMMktCy9cSCeW5wA6Zwt0dpahJbJ+RJEwtTv2x9iIu0U4enuVSQ=="
     }
   }
 }

+ 5 - 1
package.json

@@ -2,9 +2,13 @@
   "dependencies": {
     "@amap/amap-jsapi-loader": "^1.0.1",
     "@vant/weapp": "^1.11.6",
+    "dayjs": "^1.11.12",
+    "luch-request": "^3.1.1",
+    "pinia": "^2.1.7",
     "uview-ui": "^1.8.8",
     "vant": "^2.13.2",
-    "vconsole": "^3.15.1"
+    "vconsole": "^3.15.1",
+    "weixin-js-sdk": "^1.6.5"
   },
   "devDependencies": {
     "sass-loader": "^14.2.1"

+ 12 - 2
pages.json

@@ -4,7 +4,17 @@
 	},
 	"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
 		{
-			"path": "pages/index/index",
+			"path": "pages/index/index_new",
+			"style": {
+				"navigationBarTitleText": "",
+				"navigationStyle": "custom"
+				// "app-plus": {
+				// 	"scrollIndicator": false //禁用原生导航栏
+				// }
+			}
+		},
+		{
+			"path": "pages/index/service/list",
 			"style": {
 				"navigationBarTitleText": "",
 				"navigationStyle": "custom"
@@ -629,7 +639,7 @@
 		"borderStyle": "white",
 		"backgroundColor": "#ffffff",
 		"list": [{
-				"pagePath": "pages/index/index",
+				"pagePath": "pages/index/index_new",
 				"iconPath": "static/images/1-001.png",
 				"selectedIconPath": "static/images/1-002.png",
 				"text": "首页"

+ 3 - 2
pages/goods_cate/goods_cate.vue

@@ -76,7 +76,7 @@
 						</view>
 					</view>
 				</view>
-				<view :style='"height:"+(height-300)+"rpx;"' v-if="number < 15" />
+				<!-- <view :style='"height:"+(height-300)+"rpx;"' v-if="number < 15" /> -->
 			</scroll-view>
 		</view>
 		
@@ -222,7 +222,8 @@
 			getAllCategory: function() {
 				console.log('这里执行了')
 				CategoryApi.getCategoryList().then(res => {
-					this.productList = Util.handleTree(res.data);
+					// this.productList = Util.handleTree(res.data);
+					this.productList = res.data.filter(item=>item.parentId==1)
 					console.log(this.productList, 'this.productList')
 					this.getGoodsList(this.productList[0].id)
 					setTimeout(() => {

+ 107 - 0
pages/index/components/magicCube.vue

@@ -0,0 +1,107 @@
+<!-- 装修图文组件:广告魔方 -->
+<template>
+	<div class="ss-cube-wrap" :style="parseAdWrap">
+		<div v-for="(item, index) in data.list" :key="index">
+			<div class="cube-img-wrap" :style="[parseImgStyle(item), { margin: data.space + 'px' }]"
+				@click="navigateTo(item.url)">
+				<image class="cube-img" :src="sheep.$url.cdn(item.imgUrl)" />
+			</div>
+		</div>
+	</div>
+</template>
+
+<script>
+	import sheep from '@/sheep';
+	export default {
+		props: {
+			data: {
+				type: Object,
+				default: () => ({}),
+			},
+			styles: {
+				type: Object,
+				default: () => ({}),
+			},
+		},
+		watch: {
+			data(newVal) {
+				if (newVal) {
+					console.log(newVal, '更新后的data');
+					this.updateComponent(newVal, this.styles);
+				}
+			},
+			styles(newVal) {
+				if (newVal) {
+					console.log(newVal, '更新后的styles');
+					this.updateComponent(this.data, newVal);
+				}
+			}
+		},
+		mounted() {
+			this.updateComponent(this.data, this.styles);
+		},
+		methods: {
+			updateComponent(data, styles) {
+				console.log(data, styles, '88888888');
+				// 在这里处理更新后的 data 和 styles
+			},
+			parseAdWrap() {
+				// 更新 cell 的值
+				this.cell = (this.windowWidth -
+						((this.styles.marginLeft || 0) + (this.styles.marginRight || 0) + (this.styles.padding || 0) * 2)
+						) /
+					4;
+
+				let heightArr = this.data.list.reduce(
+					(prev, cur) => (prev.includes(cur.height + cur.top) ? prev : [...prev, cur.height + cur.top]),
+					[]
+				);
+				let heightMax = Math.max(...heightArr);
+
+				return {
+					height: heightMax * this.cell + 'px',
+					width: this.windowWidth -
+						(this.data?.style?.marginLeft +
+							this.data?.style?.marginRight +
+							this.styles.padding * 2) *
+						2 +
+						'px',
+				};
+			},
+			parseImgStyle(item) {
+				return {
+					width: item.width * this.cell - this.data.space + 'px',
+					height: item.height * this.cell - this.data.space + 'px',
+					left: item.left * this.cell + 'px',
+					top: item.top * this.cell + 'px',
+					'border-top-left-radius': this.data.borderRadiusTop + 'px',
+					'border-top-right-radius': this.data.borderRadiusTop + 'px',
+					'border-bottom-left-radius': this.data.borderRadiusBottom + 'px',
+					'border-bottom-right-radius': this.data.borderRadiusBottom + 'px',
+				};
+			},
+			navigateTo(url) {
+				this.sheep.$router.go(url);
+			},
+		},
+	};
+</script>
+
+<style lang="scss" scoped>
+	.ss-cube-wrap {
+		position: relative;
+		z-index: 2;
+		width: 750rpx;
+	}
+
+	.cube-img-wrap {
+		position: absolute;
+		z-index: 3;
+		overflow: hidden;
+	}
+
+	.cube-img {
+		width: 100%;
+		height: 100%;
+	}
+</style>

+ 1209 - 0
pages/index/index_new - 副本 (2).vue

@@ -0,0 +1,1209 @@
+
+<template>
+	<view>
+		<view class="page-index" :class="{'bgf':navIndex >0}">
+			<!-- #ifdef H5 -->
+			<view class="header">
+				<view class="serch-wrapper flex">
+					<view class="logo">
+						<image :src="logoUrl" mode="" />
+					</view>
+					<navigator url="/pages/goods_search/index" class="input" hover-class="none">
+            <text class="iconfont icon-xiazai5" /> 搜索商品
+          </navigator>
+				</view>
+			</view>
+			<!-- #endif -->
+			<!-- #ifdef MP -->
+			<view class="mp-header">
+				<view class="sys-head" :style="{ height: statusBarHeight }"></view>
+				<view class="serch-box" style="height: 40px;">
+					<view class="serch-wrapper flex">
+						<view class="logo">
+							<image :src="logoUrl" mode=""></image>
+						</view>
+						<navigator url="/pages/goods_search/index" class="input" hover-class="none"><text
+								class="iconfont icon-xiazai5"></text>
+							搜索商品</navigator>
+					</view>
+				</view>
+			</view>
+			<!-- #endif -->
+			<!-- 首页展示 -->
+			<view class="page_content" :style="'margin-top:'+(marTop)+'px;'" v-if="navIndex === 0">
+				<view class="mp-bg"></view>
+				<!-- banner 轮播图 -->
+				<view class="swiper" v-if="slideShows.length">
+					<swiper indicator-dots="true" :autoplay="true" :circular="circular" :interval="interval"
+						:duration="duration" indicator-color="rgba(255,255,255,0.6)" indicator-active-color="#fff">
+						<block v-for="(item,index) in slideShows" :key="index">
+							<swiper-item>
+								<navigator :url='item.url' class='slide-navigator acea-row row-between-wrapper'
+									hover-class='none'>
+									<image :src="item.imgUrl" class="slide-image" lazy-load></image>
+								</navigator>
+							</swiper-item>
+						</block>
+					</swiper>
+				</view>
+
+				<!-- 新闻简报 -->
+				<view class='notice acea-row row-middle row-between' v-if="scrollingNews.length">
+					<view class="pic">
+						<image src="/static/images/xinjian.png" />
+					</view>
+					<text class='line'>|</text>
+					<view class='swipers'>
+						<swiper :indicator-dots="indicatorDots" :autoplay="autoplay" interval="2500" duration="500"
+                    vertical="true" circular="true">
+							<block v-for="(item,index) in scrollingNews" :key='index'>
+								<swiper-item>
+									<navigator class='item' :url='item.url' hover-class='none'>
+										<view class='line1'>{{ item.name }}</view>
+									</navigator>
+								</swiper-item>
+							</block>
+						</swiper>
+					</view>
+					<view class="iconfont icon-xiangyou" />
+				</view>
+
+				<!-- menu 菜单 -->
+				<view class='nav acea-row' v-if="menus.length">
+					<block v-for="(item,index) in menus" :key="index">
+						<navigator class='item' v-if="item.show === '1'" :url='item.url' open-type='switchTab'
+                       hover-class='none'>
+							<view class='pictrue'>
+								<image :src='item.iconUrl'></image>
+							</view>
+							<view class="menu-txt">{{item.title}}</view>
+						</navigator>
+						<navigator class='item' v-else :url='item.url' hover-class='none'>
+							<view class='pictrue'>
+								<image :src='item.iconUrl'></image>
+							</view>
+							<view class="menu-txt">{{item.title}}</view>
+						</navigator>
+					</block>
+				</view>
+				<!-- 智能设备绑定入口 -->
+				<view class="equipment" v-if="equipmentImg">
+					<image :src="equipmentImg" mode="widthFix" style="width: 100%;height: 170rpx;"></image>
+				</view>
+				<!-- 广告模块 -->
+				<!-- <MagicCube v-if="magicCubeData" :data="magicCubeData" :styles="magicCubeData.styles" /> -->
+				
+
+				<!-- 优惠券 -->
+				<view class="couponIndex" v-if="couponList.length>0">
+					<view class="acea-row" style="height: 100%;">
+						<view class="titBox">
+							<view class="tit1">领取优惠券</view>
+							<view class="tit2">福利大礼包,省了又省</view>
+							<navigator class='item' url='/pages/users/user_get_coupon/index' hover-class='none'>
+								<view class="tit3">查看全部 <text class="iconfont icon-xiangyou"></text></view>
+							</navigator>
+						</view>
+						<view class="listBox acea-row">
+							<view class="list" :class='item.canTake ? "listActive" : "listHui"'
+                    v-for="(item, index) in couponList" :key="index">
+								<view class="tit line1" :class='item.canTake ? "titActive" : "pricehui"'>{{ item.name }}</view>
+								<view class="price" :class='item.canTake ?  "icon-color" : "pricehui"'>
+                  <text v-if="item.discountType === 1">{{ fen2yuan(item.discountPrice) }} 元</text>
+                  <text v-else>{{ (item.discountPercent / 10.0).toFixed(1) }} 折</text>
+                </view>
+								<view class="ling icon-color" v-if="item.canTake"
+                      @click="getCoupon(item.id,index)">领取</view>
+								<view class="ling pricehui fonthui" v-else>已领取</view>
+								<view class="priceM">满{{ fen2yuan(item.usePrice) }}元可用</view>
+							</view>
+						</view>
+					</view>
+				</view>
+
+				<!-- 营销活动 -->
+			<!-- 	<a_seckill />
+				<b_combination />
+				<c_bargain / >-->
+
+				<!-- 首页推荐 -->
+				<view class="sticky-box" :style="'top:'+(marTop)+'px;'">
+					<scroll-view class="scroll-view_H" style="width: 100%;" scroll-x="true" scroll-with-animation
+						:scroll-left="tabsScrollLeft" @scroll="scroll">
+						<view class="tab nav-bd" id="tab_list">
+							<view id="tab_item" :class="{ 'active': listActive === index}" class="item"
+                    v-for="(item, index) in productRecommends" :key="index" @click="ProductNavTab(item,index)">
+								<view class="txt">{{item.name}}</view>
+								<view class="label">{{item.tag}}</view>
+							</view>
+						</view>
+					</scroll-view>
+				</view>
+				<!-- 首发新品 -->
+				<view class="index-product-wrapper" :class="iSshowH?'on':''">
+					<view class="list-box animated" :class='tempArr.length > 0?"fadeIn on":""'>
+						<view class="item" v-for="(item,index) in tempArr" :key="index" @click="goDetail(item)">
+							<view class="pictrue">
+								<span class="pictrue_log pictrue_log_class"
+									v-if="item.activityList && item.activityList[0] && item.activityList[0].type === 1">秒杀</span>
+								<span class="pictrue_log pictrue_log_class"
+									v-if="item.activityList && item.activityList[0] && item.activityList[0].type === 2">砍价</span>
+								<span class="pictrue_log pictrue_log_class"
+									v-if="item.activityList && item.activityList[0] && item.activityList[0].type === 3">拼团</span>
+								<image :src="item.picUrl" mode="" />
+							</view>
+							<view class="text-info">
+								<view class="title line1">{{ item.name }}</view>
+								<view class="old-price"><text>¥{{ fen2yuan(item.marketPrice) }}</text></view>
+								<view class="price">
+									<text>¥</text>{{ fen2yuan(item.price) }}
+								</view>
+							</view>
+						</view>
+					</view>
+					<view class='loadingicon acea-row row-center-wrapper' v-if="goodScroll">
+						<text class='loading iconfont icon-jiazai' :hidden='!loading' />
+					</view>
+					<view class="mores-txt flex" v-if="!goodScroll">
+						<text>我是有底线的</text>
+					</view>
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+<script>
+	import Cache from '../../utils/cache';
+	const statusBarHeight = uni.getSystemInfoSync().statusBarHeight + 'px';
+	let app = getApp();
+	import MagicCube from './components/magicCube';
+	import a_seckill from './components/a_seckill';
+	import b_combination from './components/b_combination';
+	import c_bargain from './components/c_bargain';
+	import goodList from '@/components/goodList';
+	import { goShopDetail } from '@/libs/order.js'
+	import { mapGetters } from "vuex";
+	import countDown from '@/components/countDown';
+	import recommend from '@/components/recommend';
+	import { silenceBindingSpread } from '@/utils';
+	import Loading from '@/components/Loading/index.vue';
+  import * as ProductSpuApi from '@/api/product/spu.js';
+  import * as PromotionActivityApi from '@/api/promotion/activity.js';
+  import * as CouponApi from '@/api/promotion/coupon.js';
+  import * as DecorateApi from '@/api/promotion/decorate.js';
+  import * as ProductUtil from '@/utils/product.js';
+  import * as Util from '@/utils/util.js';
+
+	export default {
+		computed: mapGetters(['isLogin', 'uid','template']),
+		components: {
+			goodList,
+			countDown,
+			a_seckill,
+			b_combination,
+			c_bargain,
+			recommend,
+			Loading,
+			MagicCube
+		},
+		data() {
+			return {
+				statusBarHeight: statusBarHeight,
+				navIndex: 0,
+				navTop: [],
+				marTop: 0,
+				configApi: {}, // 分享类容配置
+				tabsScrollLeft: 0, // tabs 当前偏移量
+				scrollLeft: 0,
+
+        slideShows: [], // 轮播图
+        circular: true,
+        interval: 3000,
+        duration: 500,
+        // menus: [], // 菜单
+		menus:[{show:1,url:'/pages/goods_cate/goods_cate',picUrl:'../users/static/vip.png',name:'名字'},{show:1,url:'/pages/goods_cate/goods_cate',picUrl:'../users/static/vip.png',name:'名字'},{show:1,url:'/pages/goods_cate/goods_cate',picUrl:'../users/static/vip.png',name:'名字'},{show:1,url:'/pages/goods_cate/goods_cate',picUrl:'../users/static/vip.png',name:'名字'},{show:1,url:'/pages/goods_cate/goods_cate',picUrl:'../users/static/vip.png',name:'名字'},{show:1,url:'/pages/goods_cate/goods_cate',picUrl:'../users/static/vip.png',name:'名字'},{show:1,url:'/pages/goods_cate/goods_cate',picUrl:'../users/static/vip.png',name:'名字'},{show:1,url:'/pages/goods_cate/goods_cate',picUrl:'../users/static/vip.png',name:'名字'},],
+        scrollingNews: [], // 新闻简报
+        indicatorDots: false,
+        autoplay: true,
+        couponList: [], // 优惠劵列表
+        productRecommends: [], // 商品推荐
+
+        site_name: '首页', // 首页 title
+        logoUrl: "",
+
+        // ========== 精品推荐 ===========
+        goodScroll: true, // 精品推荐开关
+        listActive: 0, // 当前选中项
+        goodType: 1, //精品推荐 Type
+        params: { //精品推荐分页
+          page: 1,
+          limit: 10,
+        },
+        loading: false,
+        tempArr: [], // 精品推荐临时数组
+        iSshowH: false,
+		equipmentImg:'',
+		magicCubeData:{}
+      }
+		},
+		watch: {
+			listActive(newVal) { // 监听当前选中项
+				this.setTabList()
+			}
+		},
+		mounted() {
+			this.setTabList()
+		},
+		onLoad() {
+			console.log(this.template,'templatetemplatetemplatetemplate+++++++++++++++++')
+			this.menus=this.template.components.find(item=>item.id=='MenuGrid').property.list
+			this.slideShows=this.template.components.find(item=>item.id=='Carousel').property.items
+			this.equipmentImg=this.template.components.find(item=>item.id=='ImageBar').property.imgUrl
+			this.magicCubeData=this.template.components.find(item=>item.id=='MagicCube')?.property
+			console.log(this.magicCubeData,9999)
+      // wx.login({
+      //   success (res) {
+      //     if (res.code) {
+      //       console.log(res.code, '======== code 编号 =======')
+      //     }
+      //   }
+      // })
+
+			var that = this;
+			// 获取系统信息
+			uni.getSystemInfo({
+				success(res) {
+					that.$store.commit("SYSTEM_PLATFORM", res.platform);
+				}
+			});
+			uni.getLocation({
+				type: 'gcj02',
+				altitude: true,
+				geocode: true,
+				success: function(res) {
+					try {
+						uni.setStorageSync('user_latitude', res.latitude);
+						uni.setStorageSync('user_longitude', res.longitude);
+					} catch {}
+				}
+			});
+			this.isLogin && silenceBindingSpread();
+			// this.getIndexConfig();
+		},
+		onShow() {
+			uni.setNavigationBarTitle({
+				title: this.site_name
+			})
+		},
+		methods: {
+			// scroll-view滑动事件
+			scroll(e) {
+				this.scrollLeft = e.detail.scrollLeft;
+			},
+			setTabList() {
+				this.$nextTick(() => {
+					this.scrollIntoView()
+				})
+			},
+			// 计算tabs位置
+			scrollIntoView() { // item滚动
+				let lineLeft = 0;
+				this.getElementData('#tab_list', (data) => {
+					let list = data[0]
+					this.getElementData(`#tab_item`, (data) => {
+						let el = data[this.listActive]
+            if (el) {
+              lineLeft = el.width / 2 + (-list.left) + el.left - list.width / 2 - this.scrollLeft
+              this.tabsScrollLeft = this.scrollLeft + lineLeft
+            }
+					})
+				})
+			},
+			getElementData(el, callback) {
+				uni.createSelectorQuery().in(this).selectAll(el).boundingClientRect().exec((data) => {
+					callback(data[0]);
+				});
+			},
+			// 首页数据
+			getIndexConfig: function() {
+				let that = this;
+        DecorateApi.getDecorateComponentListByPage(1).then(res => {
+          // TODO 芋艿:暂时写死
+					uni.setNavigationBarTitle({
+						title: '首页'
+					})
+					this.$set(this, "logoUrl", 'https://static.iocoder.cn/ruoyi-vue-pro-logo.png');
+					this.$set(this, "site_name", '首页');
+					// 将装修内容存到vuex
+					this.$store.commit("TEMPLATE", res.data.home);
+          // #ifdef H5
+          this.$store.commit("SET_CHATURL", 'https://cschat.antcloud.com.cn/index.htm?tntInstId=jm7_c46J&scene=SCE01197657');
+          Cache.set('chatUrl', 'https://cschat.antcloud.com.cn/index.htm?tntInstId=jm7_c46J&scene=SCE01197657');
+          // #endif
+
+          // 轮播图
+          const slideShow = res.data.find(item => item.code === 'slide-show');
+          if (slideShow) {
+            this.$set(this, "slideShows", JSON.parse(slideShow.value));
+          }
+          // 菜单
+          const menu = res.data.find(item => item.code === 'menu');
+          if (menu) {
+            this.$set(this, "menus", JSON.parse(menu.value));
+          }
+          // 滚动新闻
+          const scrollingNews = res.data.find(item => item.code === 'scrolling-news');
+          if (scrollingNews) {
+            this.$set(this, "scrollingNews", JSON.parse(scrollingNews.value));
+          }
+          // 商品推荐
+          const productRecommend = res.data.find(item => item.code === 'product-recommend');
+          if (productRecommend) {
+            this.$set(this, "productRecommends", JSON.parse(productRecommend.value));
+            if (this.productRecommends.length > 0) {
+              this.goodType = this.productRecommends[0].type
+              this.getGroomList();
+            }
+          }
+				})
+        // 获得分享配置
+        this.shareApi();
+        // 获得优惠劵列表
+        this.getcouponList();
+			},
+
+			shareApi: function() {
+        // TODO 芋艿:写死
+        const configApi = {
+          "title": "芋道商城",
+          "synopsis": "芋道商城,好用!",
+          "img": "https://static.iocoder.cn/ruoyi-vue-pro-logo.png"
+        }
+        this.$set(this, 'configApi', configApi);
+        // #ifdef H5
+        this.setOpenShare(configApi);
+        // #endif
+			},
+			// 微信分享;
+			setOpenShare: function(data) {
+				let that = this;
+				if (that.$wechat.isWeixin()) {
+					let configAppMessage = {
+						desc: data.synopsis,
+						title: data.title,
+						link: location.href,
+						imgUrl: data.img
+					};
+					that.$wechat.wechatEvevt(["updateAppMessageShareData", "updateTimelineShareData"], configAppMessage);
+				}
+			},
+
+      // ========== 优惠劵 ===========
+      /**
+       * 获得优惠劵列表
+       */
+      getcouponList() {
+        CouponApi.getCouponTemplateList({ count: 2 }).then(res => {
+          this.$set(this, 'couponList', res.data);
+        }).catch(err => {
+          return this.$util.Tips({
+            title: err
+          });
+        });
+      },
+      /**
+       * 领取优惠劵
+       */
+      getCoupon: function(id, index) {
+        CouponApi.takeCoupon(id).then(res => {
+          // 设置已领取,即不能再领取
+          this.$set(this.couponList[index], 'canTake', res.data !== true);
+          this.$util.Tips({
+            title: '领取成功'
+          });
+        }).catch(err => {
+          return this.$util.Tips({
+            title: err
+          });
+        })
+      },
+
+      // ========== 精品推荐 ===========
+      /**
+       * 首发新品切换
+       */
+      ProductNavTab(item, index) {
+        this.listActive = index
+        this.goodType = item.type
+        this.listActive = index
+        this.tempArr = []
+        this.params.page = 1
+        this.goodScroll = true
+        this.getGroomList(true)
+      },
+      /**
+       * 商品精品推荐
+       */
+			getGroomList(onloadH) {
+				if (!this.goodScroll) {
+          return
+        }
+				if (onloadH) {
+					this.iSshowH = true
+				}
+        this.loading = true
+        ProductSpuApi.getSpuPage({
+          recommendType: this.goodType,
+          pageNo: this.params.page,
+          pageSize: this.params.limit
+        }).then(res => {
+          const good_list = res.data.list;
+          this.iSshowH = false
+					this.loading = false
+					this.goodScroll = good_list.length >= this.params.limit
+					this.params.page++
+
+          // 设置营销活动
+          const spuIds = good_list.map(item => item.id);
+          if (spuIds.length > 0) {
+            PromotionActivityApi.getActivityListBySpuIds(spuIds).then(res => {
+              ProductUtil.setActivityList(good_list, res.data);
+              this.tempArr = this.tempArr.concat(good_list); // 放在此处,避免 Vue 监控不到数组里的元素变化
+            });
+          }
+				})
+			},
+      /**
+       * 前往商品详情
+       */
+      goDetail(item) {
+        goShopDetail(item, this.uid).then(res => {
+          uni.navigateTo({
+            url: `/pages/goods_details/index?id=${item.id}`
+          })
+        })
+      },
+      fen2yuan(price) {
+        return Util.fen2yuan(price)
+      }
+		},
+		/**
+		 * 用户点击右上角分享
+		 */
+		// #ifdef MP
+		onShareAppMessage: function() {
+			return {
+				title: this.configApi.title,
+				imageUrl: this.configApi.img,
+				desc: this.configApi.synopsis,
+				path: '/pages/index/index'
+			};
+		},
+		// #endif
+		onReachBottom() {
+			if (this.navIndex === 0) {
+				// 首页加载更多
+				if (this.params.page !== 1) {
+					this.getGroomList();
+				}
+			}
+		}
+	}
+</script>
+<style>
+	page {
+		display: flex;
+		flex-direction: column;
+		height: 100%;
+		/* #ifdef H5 */
+		background-color: #fff;
+		/* #endif */
+
+	}
+</style>
+<style lang="scss">
+	.notice{
+		width: 100%;
+		height: 70rpx;
+		border-radius: 10rpx;
+		background-color: #fff;
+		margin-bottom: 25rpx;
+		line-height: 70rpx;
+		padding: 0 14rpx;
+		.line {
+			color: #CCCCCC;
+		}
+		.pic{
+			width: 130rpx;
+			height: 36rpx;
+			image{
+				width: 100%;
+				height: 100%;
+				display: block !important;
+			}
+		}
+		.swipers {
+			height: 100%;
+			width: 444rpx;
+			overflow: hidden;
+			swiper {
+				height: 100%;
+				width: 100%;
+				overflow: hidden;
+				font-size: 22rpx;
+				color: #333333;
+			}
+		}
+		.iconfont {
+			color: #999999;
+			font-size: 20rpx;
+		}
+	}
+	.couponIndex {
+		width: auto;
+		height: 238rpx;
+		background-image: url('~@/static/images/yhjsy.png');
+		background-size: 100% 100%;
+		padding-left: 42rpx;
+		margin-bottom: 30rpx;
+
+		.titBox {
+			padding: 47rpx 0;
+			text-align: center;
+			height: 100%;
+
+			.tit1 {
+				color: #FFEBD2;
+				font-size: 34rpx;
+				font-weight: 600;
+			}
+
+			.tit2 {
+				color: #FFEBD2;
+				font-size: 22rpx;
+				margin:10rpx 0 26rpx 0;
+			}
+
+			.tit3 {
+				color: #FFDAAF;
+				font-size: 24rpx;
+				.iconfont {
+					font-size: 20rpx;
+				}
+			}
+		}
+
+		.listBox {
+			padding: 14rpx 0;
+
+			.listActive {
+				background-image: url('~@/static/images/lingyhj.png');
+				background-size: 100% 100%;
+			}
+
+			.listHui {
+				background-image: url('~@/static/images/weiling.png');
+				background-size: 100% 100%;
+			}
+
+			.list {
+				width: 170rpx;
+				height: 210rpx;
+				padding: 16rpx 0;
+				text-align: center;
+				margin-left: 24rpx;
+
+				.tit {
+					font-size: 18rpx;
+					padding: 0 26rpx;
+				}
+
+				.titActive {
+					color: #C99959;
+				}
+
+				.price {
+					font-size: 46rpx;
+					font-weight: 900;
+					margin-top: 4rpx;
+				}
+
+				.pricehui {
+					color: #B2B2B2;
+				}
+                .fonthui{
+					background-color: #F5F5F5 !important;
+				}
+				.yuan {
+					font-size: 24rpx;
+				}
+
+				.ling {
+					font-size: 24rpx;
+					margin-top: 9.5rpx;
+					width: 102rpx;
+					height: 36rpx;
+					line-height: 36rpx;
+					background-color: #FFE5C7;
+					border-radius: 28rpx;
+					margin: auto;
+				}
+
+				.priceM {
+					color: #FFDAAF;
+					font-size: 22rpx;
+					margin-top: 14rpx;
+				}
+			}
+		}
+	}
+
+	.sticky-box {
+		/* #ifndef APP-PLUS-NVUE */
+		display: flex;
+		position: -webkit-sticky;
+		/* #endif */
+		position: sticky;
+		/* #ifdef H5*/
+		top: var(--window-top);
+		/* #endif */
+
+		z-index: 99;
+		flex-direction: row;
+		margin: 0px;
+		background: #f5f5f5;
+		padding: 30rpx 0;
+	}
+
+	.listAll {
+		width: 20%;
+		text-indent: 62rpx;
+		font-size: 30rpx;
+		border-left: 1px #eee solid;
+		margin: 1% 0;
+		padding: 5rpx;
+		position: relative;
+
+		image {
+			position: absolute;
+			left: 20rpx;
+			top: 8rpx;
+		}
+	}
+
+	.tab {
+		position: relative;
+		display: flex;
+		font-size: 28rpx;
+		white-space: nowrap;
+
+		&__item {
+			flex: 1;
+			padding: 0 20rpx;
+			text-align: center;
+			height: 60rpx;
+			line-height: 60rpx;
+			color: #666;
+
+			&.active {
+				color: #09C2C9;
+			}
+		}
+	}
+
+	.tab__line {
+		display: block;
+		height: 6rpx;
+		position: absolute;
+		bottom: 0;
+		left: 0;
+		z-index: 1;
+		border-radius: 3rpx;
+		position: relative;
+		background: #2FC6CD;
+	}
+
+	.scroll-view_H {
+		/* 文本不会换行,文本会在在同一行上继续,直到遇到 <br> 标签为止。 */
+		white-space: nowrap;
+		width: 100%;
+	}
+
+
+	.privacy-wrapper {
+		z-index: 999;
+		position: fixed;
+		left: 0;
+		top: 0;
+		width: 100%;
+		height: 100%;
+		background: #7F7F7F;
+
+		.privacy-box {
+			position: absolute;
+			left: 50%;
+			top: 50%;
+			transform: translate(-50%, -50%);
+			width: 560rpx;
+			padding: 50rpx 45rpx 0;
+			background: #fff;
+			border-radius: 20rpx;
+
+			.title {
+				text-align: center;
+				font-size: 32rpx;
+				text-align: center;
+				color: #333;
+				font-weight: 700;
+			}
+
+			.content {
+				margin-top: 20rpx;
+				line-height: 1.5;
+				font-size: 26rpx;
+				color: #666;
+				text-indent: 54rpx;
+
+				i {
+					font-style: normal;
+					color: $theme-color;
+				}
+			}
+
+			.btn-box {
+				margin-top: 40rpx;
+				text-align: center;
+				font-size: 30rpx;
+
+				.btn-item {
+					height: 82rpx;
+					line-height: 82rpx;
+					background: linear-gradient(90deg, #F67A38 0%, #F11B09 100%);
+					color: #fff;
+					border-radius: 41rpx;
+				}
+
+				.btn {
+					padding: 30rpx 0;
+				}
+			}
+		}
+	}
+
+	.page-index {
+		display: flex;
+		flex-direction: column;
+		min-height: 100%;
+		background: linear-gradient(180deg, #fff 0%, #f5f5f5 100%);
+
+		.header {
+			width: 100%;
+			background-color: $theme-color;
+			padding: 28rpx 30rpx;
+
+			.serch-wrapper {
+				align-items: center;
+
+
+				.logo {
+					width: 118rpx;
+					height: 42rpx;
+					margin-right: 24rpx;
+				}
+
+				image {
+					width: 118rpx;
+					height: 42rpx;
+				}
+
+				.input {
+					display: flex;
+					align-items: center;
+					width: 546rpx;
+					height: 58rpx;
+					padding: 0 0 0 30rpx;
+					background: rgba(247, 247, 247, 1);
+					border: 1px solid rgba(241, 241, 241, 1);
+					border-radius: 29rpx;
+					color: #BBBBBB;
+					font-size: 26rpx;
+
+					.iconfont {
+						margin-right: 20rpx;
+						font-size: 26rpx;
+						color: #666666;
+					}
+				}
+			}
+
+			.tabNav {
+				padding-top: 24rpx;
+			}
+		}
+
+		/* #ifdef MP */
+		.mp-header {
+			z-index: 999;
+			position: fixed;
+			left: 0;
+			top: 0;
+			width: 100%;
+			/* #ifdef H5 */
+			padding-bottom: 20rpx;
+			/* #endif */
+			background-color: $theme-color;
+
+			.serch-wrapper {
+				height: 100%;
+				align-items: center;
+				padding: 0 50rpx 0 53rpx;
+
+				image {
+					width: 118rpx;
+					height: 42rpx;
+					margin-right: 30rpx;
+				}
+
+				.input {
+					display: flex;
+					align-items: center;
+					/* #ifdef MP */
+					width: 305rpx;
+					/* #endif */
+					height: 50rpx;
+					padding: 0 0 0 30rpx;
+					background: rgba(247, 247, 247, 1);
+					border: 1px solid rgba(241, 241, 241, 1);
+					border-radius: 29rpx;
+					color: #BBBBBB;
+					font-size: 28rpx;
+
+					.iconfont {
+						margin-right: 20rpx;
+					}
+				}
+			}
+		}
+
+		/* #endif */
+
+		.page_content {
+			background-color: #f5f5f5;
+			/* #ifdef H5 */
+			// margin-top: 20rpx !important;
+			/* #endif */
+			padding: 0 30rpx;
+
+			.swiper {
+				position: relative;
+				width: 100%;
+				height: 280rpx;
+				margin: 0 auto;
+				border-radius: 10rpx;
+				overflow: hidden;
+				margin-bottom: 25rpx;
+				margin-top: 30rpx;
+				/* #ifdef MP */
+				z-index: 10;
+				margin-top: 20rpx;
+
+				/* #endif */
+				swiper,
+				.swiper-item,
+				image {
+					width: 100%;
+					height: 280rpx;
+					border-radius: 10rpx;
+				}
+			}
+
+			.nav {
+				padding-bottom: 26rpx;
+				background: #fff;
+				opacity: 1;
+				border-radius: 14rpx;
+				width: 100%;
+				margin-bottom: 30rpx;
+
+				.item {
+					display: flex;
+					flex-direction: column;
+					align-items: center;
+					justify-content: center;
+					width: 25%;
+					margin-top: 30rpx;
+
+					image {
+						width: 82rpx;
+						height: 82rpx;
+					}
+				}
+			}
+
+
+			.nav-bd {
+				display: flex;
+				align-items: center;
+				justify-content: space-between;
+
+				.item {
+					display: flex;
+					flex-direction: column;
+					align-items: center;
+					justify-content: center;
+
+					.txt {
+						font-size: 32rpx;
+						color: #282828;
+					}
+
+					.label {
+						display: flex;
+						align-items: center;
+						justify-content: center;
+						width: 124rpx;
+						height: 32rpx;
+						margin-top: 5rpx;
+						font-size: 24rpx;
+						color: #999;
+					}
+
+					&.active {
+						color: $theme-color;
+
+						.txt {
+							color: $theme-color;
+						}
+
+						.label {
+							background: linear-gradient(90deg, $bg-star 0%, $bg-end 100%);
+							border-radius: 16rpx;
+							color: #fff;
+						}
+					}
+				}
+			}
+
+			.index-product-wrapper {
+				margin-bottom: 110rpx;
+
+				&.on {
+					min-height: 1500rpx;
+				}
+
+				.list-box {
+					display: flex;
+					flex-wrap: wrap;
+					justify-content: space-between;
+
+					.item {
+						width: 335rpx;
+						margin-bottom: 20rpx;
+						background-color: #fff;
+						border-radius: 10rpx;
+						overflow: hidden;
+
+						image {
+							width: 100%;
+							height: 330rpx;
+						}
+
+						.text-info {
+							padding: 10rpx 20rpx 15rpx;
+
+							.title {
+								color: #222222;
+							}
+
+							.old-price {
+								margin-top: 8rpx;
+								font-size: 26rpx;
+								color: #AAAAAA;
+								text-decoration: line-through;
+
+								text {
+									margin-right: 2px;
+									font-size: 20rpx;
+								}
+							}
+
+							.price {
+								display: flex;
+								align-items: flex-end;
+								color: $theme-color;
+								font-size: 34rpx;
+								font-weight: 800;
+
+								text {
+									padding-bottom: 4rpx;
+									font-size: 24rpx;
+									font-weight: 800;
+								}
+
+								.txt {
+									display: flex;
+									align-items: center;
+									justify-content: center;
+									width: 28rpx;
+									height: 28rpx;
+									margin-left: 15rpx;
+									margin-bottom: 10rpx;
+									border: 1px solid $theme-color;
+									border-radius: 4rpx;
+									font-size: 22rpx;
+									font-weight: normal;
+								}
+							}
+						}
+					}
+
+					&.on {
+						display: flex;
+					}
+				}
+			}
+		}
+	}
+
+	.productList {
+		/* #ifdef H5 */
+		padding-bottom: 140rpx;
+		/* #endif */
+	}
+
+	.productList .list {
+		padding: 0 20rpx;
+	}
+
+	.productList .list.on {
+		background-color: #fff;
+		border-top: 1px solid #f6f6f6;
+	}
+
+	.productList .list .item {
+		width: 345rpx;
+		margin-top: 20rpx;
+		background-color: #fff;
+		border-radius: 10rpx;
+	}
+
+	.productList .list .item.on {
+		width: 100%;
+		display: flex;
+		border-bottom: 1rpx solid #f6f6f6;
+		padding: 30rpx 0;
+		margin: 0;
+	}
+
+	.productList .list .item .pictrue {
+		position: relative;
+		width: 100%;
+		height: 345rpx;
+	}
+
+	.productList .list .item .pictrue.on {
+		width: 180rpx;
+		height: 180rpx;
+	}
+
+	.productList .list .item .pictrue image {
+		width: 100%;
+		height: 100%;
+		border-radius: 20rpx 20rpx 0 0;
+	}
+
+	.productList .list .item .pictrue image.on {
+		border-radius: 6rpx;
+	}
+
+	.productList .list .item .text {
+		padding: 20rpx 17rpx 26rpx 17rpx;
+		font-size: 30rpx;
+		color: #222;
+	}
+
+	.productList .list .item .text.on {
+		width: 508rpx;
+		padding: 0 0 0 22rpx;
+	}
+
+	.productList .list .item .text .money {
+		font-size: 26rpx;
+		font-weight: bold;
+		margin-top: 8rpx;
+	}
+
+	.productList .list .item .text .money.on {
+		margin-top: 50rpx;
+	}
+
+	.productList .list .item .text .money .num {
+		font-size: 34rpx;
+	}
+
+	.productList .list .item .text .vip {
+		font-size: 22rpx;
+		color: #aaa;
+		margin-top: 7rpx;
+	}
+
+	.productList .list .item .text .vip.on {
+		margin-top: 12rpx;
+	}
+
+	.productList .list .item .text .vip .vip-money {
+		font-size: 24rpx;
+		color: #282828;
+		font-weight: bold;
+	}
+
+	.productList .list .item .text .vip .vip-money image {
+		width: 46rpx;
+		height: 21rpx;
+		margin-left: 4rpx;
+	}
+
+	.pictrue {
+		position: relative;
+	}
+
+	.fixed {
+		z-index: 100;
+		position: fixed;
+		left: 0;
+		top: 0;
+		background: linear-gradient(90deg, red 50%, #ff5400 100%);
+
+	}
+
+	.mores-txt {
+		width: 100%;
+		align-items: center;
+		justify-content: center;
+		height: 70rpx;
+		color: #999;
+		font-size: 24rpx;
+
+		.iconfont {
+			margin-top: 2rpx;
+			font-size: 20rpx;
+		}
+	}
+
+	.menu-txt {
+		font-size: 24rpx;
+		color: #454545;
+	}
+
+	.mp-bg {
+		position: absolute;
+		left: 0;
+		/* #ifdef H5 */
+		top: 98rpx;
+		/* #endif */
+		width: 100%;
+		height: 304rpx;
+		// background: linear-gradient(180deg, #E93323 0%, #F5F5F5 100%, #751A12 100%);
+		// border-radius: 0 0 30rpx 30rpx;
+	}
+</style>

+ 1222 - 0
pages/index/index_new.vue

@@ -0,0 +1,1222 @@
+
+<template>
+	<view>
+		<view class="page-index" :class="{'bgf':navIndex >0}">
+			<!-- #ifdef H5 -->
+			<view class="header">
+				<view class="serch-wrapper flex">
+					<!-- <view class="logo">
+						<image :src="logoUrl" mode="" />
+					</view> -->
+					<navigator url="/pages/goods_search/index" class="input" hover-class="none">
+            <text class="iconfont icon-xiazai5" /> 搜索商品
+          </navigator>
+				</view>
+			</view>
+			<!-- #endif -->
+			<!-- #ifdef MP -->
+			<view class="mp-header">
+				<view class="sys-head" :style="{ height: statusBarHeight }"></view>
+				<view class="serch-box" style="height: 40px;">
+					<view class="serch-wrapper flex">
+						<view class="logo">
+							<image :src="logoUrl" mode=""></image>
+						</view>
+						<navigator url="/pages/goods_search/index" class="input" hover-class="none"><text
+								class="iconfont icon-xiazai5"></text>
+							搜索商品</navigator>
+					</view>
+				</view>
+			</view>
+			<!-- #endif -->
+			<!-- 首页展示 -->
+			<view class="page_content" :style="'margin-top:'+(marTop)+'px;'" v-if="navIndex === 0">
+				<view class="mp-bg"></view>
+				<!-- banner 轮播图 -->
+				<view class="swiper" v-if="slideShows.length">
+					<swiper indicator-dots="true" :autoplay="true" :circular="circular" :interval="interval"
+						:duration="duration" indicator-color="rgba(255,255,255,0.6)" indicator-active-color="#fff">
+						<block v-for="(item,index) in slideShows" :key="index">
+							<swiper-item>
+								<navigator :url='item.url' class='slide-navigator acea-row row-between-wrapper'
+									hover-class='none'>
+									<image :src="item.imgUrl" class="slide-image" lazy-load></image>
+								</navigator>
+							</swiper-item>
+						</block>
+					</swiper>
+				</view>
+
+				<!-- 新闻简报 -->
+				<view class='notice acea-row row-middle row-between' v-if="scrollingNews.length">
+					<view class="pic">
+						<image src="/static/images/xinjian.png" />
+					</view>
+					<text class='line'>|</text>
+					<view class='swipers'>
+						<swiper :indicator-dots="indicatorDots" :autoplay="autoplay" interval="2500" duration="500"
+                    vertical="true" circular="true">
+							<block v-for="(item,index) in scrollingNews" :key='index'>
+								<swiper-item>
+									<navigator class='item' :url='item.url' hover-class='none'>
+										<view class='line1'>{{ item.name }}</view>
+									</navigator>
+								</swiper-item>
+							</block>
+						</swiper>
+					</view>
+					<view class="iconfont icon-xiangyou" />
+				</view>
+
+				<!-- menu 菜单 -->
+				<view class='nav acea-row' v-if="menus.length">
+					<block v-for="(item,index) in menus" :key="index">
+						<navigator class='item' v-if="item.show === '1'" url='pages/index/service/list' open-type='switchTab'
+                       hover-class='none'>
+							<view class='pictrue'>
+								<image :src='item.iconUrl'></image>
+							</view>
+							<view class="menu-txt">{{item.title}}22</view>
+						</navigator>
+						<view class='item' v-else @click="goService" hover-class='none'>
+							<view class='pictrue'>
+								<image :src='item.iconUrl'></image>
+							</view>
+							<view class="menu-txt">{{item.title}}</view>
+						</view>
+					</block>
+				</view>
+				<!-- 智能设备绑定入口 -->
+				<view class="equipment" v-if="equipmentImg">
+					<image :src="equipmentImg" mode="widthFix" style="width: 100%;height: 170rpx;"></image>
+				</view>
+				<!-- 广告模块 -->
+				<!-- <MagicCube v-if="magicCubeData" :data="magicCubeData" :styles="magicCubeData.style" /> -->
+				
+
+				<!-- 优惠券 -->
+				<view class="couponIndex" v-if="couponList.length>0">
+					<view class="acea-row" style="height: 100%;">
+						<view class="titBox">
+							<view class="tit1">领取优惠券</view>
+							<view class="tit2">福利大礼包,省了又省</view>
+							<navigator class='item' url='/pages/users/user_get_coupon/index' hover-class='none'>
+								<view class="tit3">查看全部 <text class="iconfont icon-xiangyou"></text></view>
+							</navigator>
+						</view>
+						<view class="listBox acea-row">
+							<view class="list" :class='item.canTake ? "listActive" : "listHui"'
+                    v-for="(item, index) in couponList" :key="index">
+								<view class="tit line1" :class='item.canTake ? "titActive" : "pricehui"'>{{ item.name }}</view>
+								<view class="price" :class='item.canTake ?  "icon-color" : "pricehui"'>
+                  <text v-if="item.discountType === 1">{{ fen2yuan(item.discountPrice) }} 元</text>
+                  <text v-else>{{ (item.discountPercent / 10.0).toFixed(1) }} 折</text>
+                </view>
+								<view class="ling icon-color" v-if="item.canTake"
+                      @click="getCoupon(item.id,index)">领取</view>
+								<view class="ling pricehui fonthui" v-else>已领取</view>
+								<view class="priceM">满{{ fen2yuan(item.usePrice) }}元可用</view>
+							</view>
+						</view>
+					</view>
+				</view>
+
+				<!-- 营销活动 -->
+			<!-- 	<a_seckill />
+				<b_combination />
+				<c_bargain / >-->
+
+				<!-- 首页推荐 -->
+				<view class="sticky-box" :style="'top:'+(marTop)+'px;'">
+					<scroll-view class="scroll-view_H" style="width: 100%;" scroll-x="true" scroll-with-animation
+						:scroll-left="tabsScrollLeft" @scroll="scroll">
+						<view class="tab nav-bd" id="tab_list">
+							<view id="tab_item" :class="{ 'active': listActive === index}" class="item"
+                    v-for="(item, index) in productRecommends" :key="index" @click="ProductNavTab(item,index)">
+								<view class="txt">{{item.name}}</view>
+								<view class="label">{{item.tag}}</view>
+							</view>
+						</view>
+					</scroll-view>
+				</view>
+				<!-- 首发新品 -->
+				<view class="index-product-wrapper" :class="iSshowH?'on':''">
+					<view class="list-box animated" :class='tempArr.length > 0?"fadeIn on":""'>
+						<view class="item" v-for="(item,index) in tempArr" :key="index" @click="goDetail(item)">
+							<view class="pictrue">
+								<span class="pictrue_log pictrue_log_class"
+									v-if="item.activityList && item.activityList[0] && item.activityList[0].type === 1">秒杀</span>
+								<span class="pictrue_log pictrue_log_class"
+									v-if="item.activityList && item.activityList[0] && item.activityList[0].type === 2">砍价</span>
+								<span class="pictrue_log pictrue_log_class"
+									v-if="item.activityList && item.activityList[0] && item.activityList[0].type === 3">拼团</span>
+								<image :src="item.picUrl" mode="" />
+							</view>
+							<view class="text-info">
+								<view class="title line1">{{ item.name }}</view>
+								<view class="old-price"><text>¥{{ fen2yuan(item.marketPrice) }}</text></view>
+								<view class="price">
+									<text>¥</text>{{ fen2yuan(item.price) }}
+								</view>
+							</view>
+						</view>
+					</view>
+					<view class='loadingicon acea-row row-center-wrapper' v-if="goodScroll">
+						<text class='loading iconfont icon-jiazai' :hidden='!loading' />
+					</view>
+					<view class="mores-txt flex" v-if="!goodScroll">
+						<text>我是有底线的</text>
+					</view>
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+<script>
+	import Cache from '../../utils/cache';
+	const statusBarHeight = uni.getSystemInfoSync().statusBarHeight + 'px';
+	let app = getApp();
+	import MagicCube from './components/magicCube';
+	import a_seckill from './components/a_seckill';
+	import b_combination from './components/b_combination';
+	import c_bargain from './components/c_bargain';
+	import goodList from '@/components/goodList';
+	import { goShopDetail } from '@/libs/order.js'
+	import { mapGetters } from "vuex";
+	import countDown from '@/components/countDown';
+	import recommend from '@/components/recommend';
+	import { silenceBindingSpread } from '@/utils';
+	import Loading from '@/components/Loading/index.vue';
+  import * as ProductSpuApi from '@/api/product/spu.js';
+  import * as PromotionActivityApi from '@/api/promotion/activity.js';
+  import * as CouponApi from '@/api/promotion/coupon.js';
+  import * as DecorateApi from '@/api/promotion/decorate.js';
+  import * as ProductUtil from '@/utils/product.js';
+  import * as Util from '@/utils/util.js';
+
+	export default {
+		computed: mapGetters(['isLogin', 'uid','template']),
+		components: {
+			goodList,
+			countDown,
+			a_seckill,
+			b_combination,
+			c_bargain,
+			recommend,
+			Loading,
+			MagicCube
+		},
+		data() {
+			return {
+				statusBarHeight: statusBarHeight,
+				navIndex: 0,
+				navTop: [],
+				marTop: 0,
+				configApi: {}, // 分享类容配置
+				tabsScrollLeft: 0, // tabs 当前偏移量
+				scrollLeft: 0,
+
+        slideShows: [], // 轮播图
+        circular: true,
+        interval: 3000,
+        duration: 500,
+        // menus: [], // 菜单
+		menus:[{show:1,url:'/pages/goods_cate/goods_cate',picUrl:'../users/static/vip.png',name:'名字'},{show:1,url:'/pages/goods_cate/goods_cate',picUrl:'../users/static/vip.png',name:'名字'},{show:1,url:'/pages/goods_cate/goods_cate',picUrl:'../users/static/vip.png',name:'名字'},{show:1,url:'/pages/goods_cate/goods_cate',picUrl:'../users/static/vip.png',name:'名字'},{show:1,url:'/pages/goods_cate/goods_cate',picUrl:'../users/static/vip.png',name:'名字'},{show:1,url:'/pages/goods_cate/goods_cate',picUrl:'../users/static/vip.png',name:'名字'},{show:1,url:'/pages/goods_cate/goods_cate',picUrl:'../users/static/vip.png',name:'名字'},{show:1,url:'/pages/goods_cate/goods_cate',picUrl:'../users/static/vip.png',name:'名字'},],
+        scrollingNews: [], // 新闻简报
+        indicatorDots: false,
+        autoplay: true,
+        couponList: [], // 优惠劵列表
+        productRecommends: [], // 商品推荐
+
+        site_name: '首页', // 首页 title
+        logoUrl: "",
+
+        // ========== 精品推荐 ===========
+        goodScroll: true, // 精品推荐开关
+        listActive: 0, // 当前选中项
+        goodType: 1, //精品推荐 Type
+        params: { //精品推荐分页
+          page: 1,
+          limit: 10,
+        },
+        loading: false,
+        tempArr: [], // 精品推荐临时数组
+        iSshowH: false,
+		equipmentImg:'',
+		magicCubeData:{}
+      }
+		},
+		watch: {
+			listActive(newVal) { // 监听当前选中项
+				this.setTabList()
+			},
+			 template: {
+			    handler(newVal) {
+			      if (newVal && newVal.components) {
+			        this.menus = newVal.components.find(item => item.id === 'MenuGrid').property.list;
+			        this.slideShows = newVal.components.find(item => item.id === 'Carousel').property.items;
+			        this.equipmentImg = newVal.components.find(item => item.id === 'ImageBar').property.imgUrl;
+			        this.magicCubeData = newVal.components.find(item => item.id === 'MagicCube')?.property;
+					console.log(this.magicCubeData,this.magicCubeData.style,'this.magicCubeDatathis.magicCubeData');
+			      }
+			    },
+			    deep: true,
+			    immediate: true,
+			  },
+		},
+		onLoad() {
+			
+      // wx.login({
+      //   success (res) {
+      //     if (res.code) {
+      //       console.log(res.code, '======== code 编号 =======')
+      //     }
+      //   }
+      // })
+
+			var that = this;
+			// 获取系统信息
+			uni.getSystemInfo({
+				success(res) {
+					that.$store.commit("SYSTEM_PLATFORM", res.platform);
+				}
+			});
+			uni.getLocation({
+				type: 'gcj02',
+				altitude: true,
+				geocode: true,
+				success: function(res) {
+					try {
+						uni.setStorageSync('user_latitude', res.latitude);
+						uni.setStorageSync('user_longitude', res.longitude);
+					} catch {}
+				}
+			});
+			this.isLogin && silenceBindingSpread();
+			// this.getIndexConfig();
+		},
+		onShow() {
+			uni.setNavigationBarTitle({
+				title: this.site_name
+			})
+		},
+		methods: {
+			goService(){
+				uni.navigateTo({
+					url:'/pages/index/service/list',
+					// url:`/pages/index/service/list?id=${item.id}`
+				})
+			},
+			// scroll-view滑动事件
+			scroll(e) {
+				this.scrollLeft = e.detail.scrollLeft;
+			},
+			setTabList() {
+				this.$nextTick(() => {
+					this.scrollIntoView()
+				})
+			},
+			// 计算tabs位置
+			scrollIntoView() { // item滚动
+				let lineLeft = 0;
+				this.getElementData('#tab_list', (data) => {
+					let list = data[0]
+					this.getElementData(`#tab_item`, (data) => {
+						let el = data[this.listActive]
+            if (el) {
+              lineLeft = el.width / 2 + (-list.left) + el.left - list.width / 2 - this.scrollLeft
+              this.tabsScrollLeft = this.scrollLeft + lineLeft
+            }
+					})
+				})
+			},
+			getElementData(el, callback) {
+				uni.createSelectorQuery().in(this).selectAll(el).boundingClientRect().exec((data) => {
+					callback(data[0]);
+				});
+			},
+			// 首页数据
+			getIndexConfig: function() {
+				let that = this;
+        DecorateApi.getDecorateComponentListByPage(1).then(res => {
+          // TODO 芋艿:暂时写死
+					uni.setNavigationBarTitle({
+						title: '首页'
+					})
+					this.$set(this, "logoUrl", 'https://static.iocoder.cn/ruoyi-vue-pro-logo.png');
+					this.$set(this, "site_name", '首页');
+					// 将装修内容存到vuex
+					this.$store.commit("TEMPLATE", res.data.home);
+          // #ifdef H5
+          this.$store.commit("SET_CHATURL", 'https://cschat.antcloud.com.cn/index.htm?tntInstId=jm7_c46J&scene=SCE01197657');
+          Cache.set('chatUrl', 'https://cschat.antcloud.com.cn/index.htm?tntInstId=jm7_c46J&scene=SCE01197657');
+          // #endif
+
+          // 轮播图
+          const slideShow = res.data.find(item => item.code === 'slide-show');
+          if (slideShow) {
+            this.$set(this, "slideShows", JSON.parse(slideShow.value));
+          }
+          // 菜单
+          const menu = res.data.find(item => item.code === 'menu');
+          if (menu) {
+            this.$set(this, "menus", JSON.parse(menu.value));
+          }
+          // 滚动新闻
+          const scrollingNews = res.data.find(item => item.code === 'scrolling-news');
+          if (scrollingNews) {
+            this.$set(this, "scrollingNews", JSON.parse(scrollingNews.value));
+          }
+          // 商品推荐
+          const productRecommend = res.data.find(item => item.code === 'product-recommend');
+          if (productRecommend) {
+            this.$set(this, "productRecommends", JSON.parse(productRecommend.value));
+            if (this.productRecommends.length > 0) {
+              this.goodType = this.productRecommends[0].type
+              this.getGroomList();
+            }
+          }
+				})
+        // 获得分享配置
+        this.shareApi();
+        // 获得优惠劵列表
+        this.getcouponList();
+			},
+
+			shareApi: function() {
+        // TODO 芋艿:写死
+        const configApi = {
+          "title": "芋道商城",
+          "synopsis": "芋道商城,好用!",
+          "img": "https://static.iocoder.cn/ruoyi-vue-pro-logo.png"
+        }
+        this.$set(this, 'configApi', configApi);
+        // #ifdef H5
+        this.setOpenShare(configApi);
+        // #endif
+			},
+			// 微信分享;
+			setOpenShare: function(data) {
+				let that = this;
+				if (that.$wechat.isWeixin()) {
+					let configAppMessage = {
+						desc: data.synopsis,
+						title: data.title,
+						link: location.href,
+						imgUrl: data.img
+					};
+					that.$wechat.wechatEvevt(["updateAppMessageShareData", "updateTimelineShareData"], configAppMessage);
+				}
+			},
+
+      // ========== 优惠劵 ===========
+      /**
+       * 获得优惠劵列表
+       */
+      getcouponList() {
+        CouponApi.getCouponTemplateList({ count: 2 }).then(res => {
+          this.$set(this, 'couponList', res.data);
+        }).catch(err => {
+          return this.$util.Tips({
+            title: err
+          });
+        });
+      },
+      /**
+       * 领取优惠劵
+       */
+      getCoupon: function(id, index) {
+        CouponApi.takeCoupon(id).then(res => {
+          // 设置已领取,即不能再领取
+          this.$set(this.couponList[index], 'canTake', res.data !== true);
+          this.$util.Tips({
+            title: '领取成功'
+          });
+        }).catch(err => {
+          return this.$util.Tips({
+            title: err
+          });
+        })
+      },
+
+      // ========== 精品推荐 ===========
+      /**
+       * 首发新品切换
+       */
+      ProductNavTab(item, index) {
+        this.listActive = index
+        this.goodType = item.type
+        this.listActive = index
+        this.tempArr = []
+        this.params.page = 1
+        this.goodScroll = true
+        this.getGroomList(true)
+      },
+      /**
+       * 商品精品推荐
+       */
+			getGroomList(onloadH) {
+				if (!this.goodScroll) {
+          return
+        }
+				if (onloadH) {
+					this.iSshowH = true
+				}
+        this.loading = true
+        ProductSpuApi.getSpuPage({
+          recommendType: this.goodType,
+          pageNo: this.params.page,
+          pageSize: this.params.limit
+        }).then(res => {
+          const good_list = res.data.list;
+          this.iSshowH = false
+					this.loading = false
+					this.goodScroll = good_list.length >= this.params.limit
+					this.params.page++
+
+          // 设置营销活动
+          const spuIds = good_list.map(item => item.id);
+          if (spuIds.length > 0) {
+            PromotionActivityApi.getActivityListBySpuIds(spuIds).then(res => {
+              ProductUtil.setActivityList(good_list, res.data);
+              this.tempArr = this.tempArr.concat(good_list); // 放在此处,避免 Vue 监控不到数组里的元素变化
+            });
+          }
+				})
+			},
+      /**
+       * 前往商品详情
+       */
+      goDetail(item) {
+        goShopDetail(item, this.uid).then(res => {
+          uni.navigateTo({
+            url: `/pages/goods_details/index?id=${item.id}`
+          })
+        })
+      },
+      fen2yuan(price) {
+        return Util.fen2yuan(price)
+      }
+		},
+		/**
+		 * 用户点击右上角分享
+		 */
+		// #ifdef MP
+		onShareAppMessage: function() {
+			return {
+				title: this.configApi.title,
+				imageUrl: this.configApi.img,
+				desc: this.configApi.synopsis,
+				path: '/pages/index/index'
+			};
+		},
+		// #endif
+		onReachBottom() {
+			if (this.navIndex === 0) {
+				// 首页加载更多
+				if (this.params.page !== 1) {
+					this.getGroomList();
+				}
+			}
+		}
+	}
+</script>
+<style>
+	page {
+		display: flex;
+		flex-direction: column;
+		height: 100%;
+		/* #ifdef H5 */
+		background-color: #fff;
+		/* #endif */
+
+	}
+</style>
+<style lang="scss">
+	.notice{
+		width: 100%;
+		height: 70rpx;
+		border-radius: 10rpx;
+		background-color: #fff;
+		margin-bottom: 25rpx;
+		line-height: 70rpx;
+		padding: 0 14rpx;
+		.line {
+			color: #CCCCCC;
+		}
+		.pic{
+			width: 130rpx;
+			height: 36rpx;
+			image{
+				width: 100%;
+				height: 100%;
+				display: block !important;
+			}
+		}
+		.swipers {
+			height: 100%;
+			width: 444rpx;
+			overflow: hidden;
+			swiper {
+				height: 100%;
+				width: 100%;
+				overflow: hidden;
+				font-size: 22rpx;
+				color: #333333;
+			}
+		}
+		.iconfont {
+			color: #999999;
+			font-size: 20rpx;
+		}
+	}
+	.couponIndex {
+		width: auto;
+		height: 238rpx;
+		background-image: url('~@/static/images/yhjsy.png');
+		background-size: 100% 100%;
+		padding-left: 42rpx;
+		margin-bottom: 30rpx;
+
+		.titBox {
+			padding: 47rpx 0;
+			text-align: center;
+			height: 100%;
+
+			.tit1 {
+				color: #FFEBD2;
+				font-size: 34rpx;
+				font-weight: 600;
+			}
+
+			.tit2 {
+				color: #FFEBD2;
+				font-size: 22rpx;
+				margin:10rpx 0 26rpx 0;
+			}
+
+			.tit3 {
+				color: #FFDAAF;
+				font-size: 24rpx;
+				.iconfont {
+					font-size: 20rpx;
+				}
+			}
+		}
+
+		.listBox {
+			padding: 14rpx 0;
+
+			.listActive {
+				background-image: url('~@/static/images/lingyhj.png');
+				background-size: 100% 100%;
+			}
+
+			.listHui {
+				background-image: url('~@/static/images/weiling.png');
+				background-size: 100% 100%;
+			}
+
+			.list {
+				width: 170rpx;
+				height: 210rpx;
+				padding: 16rpx 0;
+				text-align: center;
+				margin-left: 24rpx;
+
+				.tit {
+					font-size: 18rpx;
+					padding: 0 26rpx;
+				}
+
+				.titActive {
+					color: #C99959;
+				}
+
+				.price {
+					font-size: 46rpx;
+					font-weight: 900;
+					margin-top: 4rpx;
+				}
+
+				.pricehui {
+					color: #B2B2B2;
+				}
+                .fonthui{
+					background-color: #F5F5F5 !important;
+				}
+				.yuan {
+					font-size: 24rpx;
+				}
+
+				.ling {
+					font-size: 24rpx;
+					margin-top: 9.5rpx;
+					width: 102rpx;
+					height: 36rpx;
+					line-height: 36rpx;
+					background-color: #FFE5C7;
+					border-radius: 28rpx;
+					margin: auto;
+				}
+
+				.priceM {
+					color: #FFDAAF;
+					font-size: 22rpx;
+					margin-top: 14rpx;
+				}
+			}
+		}
+	}
+
+	.sticky-box {
+		/* #ifndef APP-PLUS-NVUE */
+		display: flex;
+		position: -webkit-sticky;
+		/* #endif */
+		position: sticky;
+		/* #ifdef H5*/
+		top: var(--window-top);
+		/* #endif */
+
+		z-index: 99;
+		flex-direction: row;
+		margin: 0px;
+		background: #f5f5f5;
+		padding: 30rpx 0;
+	}
+
+	.listAll {
+		width: 20%;
+		text-indent: 62rpx;
+		font-size: 30rpx;
+		border-left: 1px #eee solid;
+		margin: 1% 0;
+		padding: 5rpx;
+		position: relative;
+
+		image {
+			position: absolute;
+			left: 20rpx;
+			top: 8rpx;
+		}
+	}
+
+	.tab {
+		position: relative;
+		display: flex;
+		font-size: 28rpx;
+		white-space: nowrap;
+
+		&__item {
+			flex: 1;
+			padding: 0 20rpx;
+			text-align: center;
+			height: 60rpx;
+			line-height: 60rpx;
+			color: #666;
+
+			&.active {
+				color: #09C2C9;
+			}
+		}
+	}
+
+	.tab__line {
+		display: block;
+		height: 6rpx;
+		position: absolute;
+		bottom: 0;
+		left: 0;
+		z-index: 1;
+		border-radius: 3rpx;
+		position: relative;
+		background: #2FC6CD;
+	}
+
+	.scroll-view_H {
+		/* 文本不会换行,文本会在在同一行上继续,直到遇到 <br> 标签为止。 */
+		white-space: nowrap;
+		width: 100%;
+	}
+
+
+	.privacy-wrapper {
+		z-index: 999;
+		position: fixed;
+		left: 0;
+		top: 0;
+		width: 100%;
+		height: 100%;
+		background: #7F7F7F;
+
+		.privacy-box {
+			position: absolute;
+			left: 50%;
+			top: 50%;
+			transform: translate(-50%, -50%);
+			width: 560rpx;
+			padding: 50rpx 45rpx 0;
+			background: #fff;
+			border-radius: 20rpx;
+
+			.title {
+				text-align: center;
+				font-size: 32rpx;
+				text-align: center;
+				color: #333;
+				font-weight: 700;
+			}
+
+			.content {
+				margin-top: 20rpx;
+				line-height: 1.5;
+				font-size: 26rpx;
+				color: #666;
+				text-indent: 54rpx;
+
+				i {
+					font-style: normal;
+					color: $theme-color;
+				}
+			}
+
+			.btn-box {
+				margin-top: 40rpx;
+				text-align: center;
+				font-size: 30rpx;
+
+				.btn-item {
+					height: 82rpx;
+					line-height: 82rpx;
+					background: linear-gradient(90deg, #F67A38 0%, #F11B09 100%);
+					color: #fff;
+					border-radius: 41rpx;
+				}
+
+				.btn {
+					padding: 30rpx 0;
+				}
+			}
+		}
+	}
+
+	.page-index {
+		display: flex;
+		flex-direction: column;
+		min-height: 100%;
+		background: linear-gradient(180deg, #fff 0%, #f5f5f5 100%);
+
+		.header {
+			width: 100%;
+			// background-color: $theme-color;
+			background-color: #f5f5f5;
+			padding: 28rpx 30rpx;
+
+			.serch-wrapper {
+				align-items: center;
+
+
+				.logo {
+					width: 118rpx;
+					height: 42rpx;
+					margin-right: 24rpx;
+				}
+
+				image {
+					width: 118rpx;
+					height: 42rpx;
+				}
+
+				.input {
+					display: flex;
+					align-items: center;
+					// width: 546rpx;
+					width: 100%;
+					height: 58rpx;
+					padding: 0 0 0 30rpx;
+					// background: rgba(247, 247, 247, 1);
+					background: #fff;
+					border: 1px solid rgba(241, 241, 241, 1);
+					border-radius: 29rpx;
+					color: #BBBBBB;
+					font-size: 26rpx;
+
+					.iconfont {
+						margin-right: 20rpx;
+						font-size: 26rpx;
+						color: #666666;
+					}
+				}
+			}
+
+			.tabNav {
+				padding-top: 24rpx;
+			}
+		}
+
+		/* #ifdef MP */
+		.mp-header {
+			z-index: 999;
+			position: fixed;
+			left: 0;
+			top: 0;
+			width: 100%;
+			/* #ifdef H5 */
+			padding-bottom: 20rpx;
+			/* #endif */
+			background-color: $theme-color;
+
+			.serch-wrapper {
+				height: 100%;
+				align-items: center;
+				padding: 0 50rpx 0 53rpx;
+
+				image {
+					width: 118rpx;
+					height: 42rpx;
+					margin-right: 30rpx;
+				}
+
+				.input {
+					display: flex;
+					align-items: center;
+					/* #ifdef MP */
+					width: 305rpx;
+					/* #endif */
+					height: 50rpx;
+					padding: 0 0 0 30rpx;
+					background: rgba(247, 247, 247, 1);
+					border: 1px solid rgba(241, 241, 241, 1);
+					border-radius: 29rpx;
+					color: #BBBBBB;
+					font-size: 28rpx;
+
+					.iconfont {
+						margin-right: 20rpx;
+					}
+				}
+			}
+		}
+
+		/* #endif */
+
+		.page_content {
+			background-color: #f5f5f5;
+			/* #ifdef H5 */
+			// margin-top: 20rpx !important;
+			/* #endif */
+			padding: 0 30rpx;
+
+			.swiper {
+				position: relative;
+				width: 100%;
+				height: 280rpx;
+				margin: 0 auto;
+				border-radius: 10rpx;
+				overflow: hidden;
+				margin-bottom: 25rpx;
+				/* #ifdef MP */
+				z-index: 10;
+				margin-top: 20rpx;
+
+				/* #endif */
+				swiper,
+				.swiper-item,
+				image {
+					width: 100%;
+					height: 280rpx;
+					border-radius: 10rpx;
+				}
+			}
+
+			.nav {
+				padding-bottom: 26rpx;
+				background: #fff;
+				opacity: 1;
+				border-radius: 14rpx;
+				width: 100%;
+				margin-bottom: 30rpx;
+
+				.item {
+					display: flex;
+					flex-direction: column;
+					align-items: center;
+					justify-content: center;
+					width: 25%;
+					margin-top: 30rpx;
+
+					image {
+						width: 82rpx;
+						height: 82rpx;
+					}
+				}
+			}
+
+
+			.nav-bd {
+				display: flex;
+				align-items: center;
+				justify-content: space-between;
+
+				.item {
+					display: flex;
+					flex-direction: column;
+					align-items: center;
+					justify-content: center;
+
+					.txt {
+						font-size: 32rpx;
+						color: #282828;
+					}
+
+					.label {
+						display: flex;
+						align-items: center;
+						justify-content: center;
+						width: 124rpx;
+						height: 32rpx;
+						margin-top: 5rpx;
+						font-size: 24rpx;
+						color: #999;
+					}
+
+					&.active {
+						color: $theme-color;
+
+						.txt {
+							color: $theme-color;
+						}
+
+						.label {
+							background: linear-gradient(90deg, $bg-star 0%, $bg-end 100%);
+							border-radius: 16rpx;
+							color: #fff;
+						}
+					}
+				}
+			}
+
+			.index-product-wrapper {
+				margin-bottom: 110rpx;
+
+				&.on {
+					min-height: 1500rpx;
+				}
+
+				.list-box {
+					display: flex;
+					flex-wrap: wrap;
+					justify-content: space-between;
+
+					.item {
+						width: 335rpx;
+						margin-bottom: 20rpx;
+						background-color: #fff;
+						border-radius: 10rpx;
+						overflow: hidden;
+
+						image {
+							width: 100%;
+							height: 330rpx;
+						}
+
+						.text-info {
+							padding: 10rpx 20rpx 15rpx;
+
+							.title {
+								color: #222222;
+							}
+
+							.old-price {
+								margin-top: 8rpx;
+								font-size: 26rpx;
+								color: #AAAAAA;
+								text-decoration: line-through;
+
+								text {
+									margin-right: 2px;
+									font-size: 20rpx;
+								}
+							}
+
+							.price {
+								display: flex;
+								align-items: flex-end;
+								color: $theme-color;
+								font-size: 34rpx;
+								font-weight: 800;
+
+								text {
+									padding-bottom: 4rpx;
+									font-size: 24rpx;
+									font-weight: 800;
+								}
+
+								.txt {
+									display: flex;
+									align-items: center;
+									justify-content: center;
+									width: 28rpx;
+									height: 28rpx;
+									margin-left: 15rpx;
+									margin-bottom: 10rpx;
+									border: 1px solid $theme-color;
+									border-radius: 4rpx;
+									font-size: 22rpx;
+									font-weight: normal;
+								}
+							}
+						}
+					}
+
+					&.on {
+						display: flex;
+					}
+				}
+			}
+		}
+	}
+
+	.productList {
+		/* #ifdef H5 */
+		padding-bottom: 140rpx;
+		/* #endif */
+	}
+
+	.productList .list {
+		padding: 0 20rpx;
+	}
+
+	.productList .list.on {
+		background-color: #fff;
+		border-top: 1px solid #f6f6f6;
+	}
+
+	.productList .list .item {
+		width: 345rpx;
+		margin-top: 20rpx;
+		background-color: #fff;
+		border-radius: 10rpx;
+	}
+
+	.productList .list .item.on {
+		width: 100%;
+		display: flex;
+		border-bottom: 1rpx solid #f6f6f6;
+		padding: 30rpx 0;
+		margin: 0;
+	}
+
+	.productList .list .item .pictrue {
+		position: relative;
+		width: 100%;
+		height: 345rpx;
+	}
+
+	.productList .list .item .pictrue.on {
+		width: 180rpx;
+		height: 180rpx;
+	}
+
+	.productList .list .item .pictrue image {
+		width: 100%;
+		height: 100%;
+		border-radius: 20rpx 20rpx 0 0;
+	}
+
+	.productList .list .item .pictrue image.on {
+		border-radius: 6rpx;
+	}
+
+	.productList .list .item .text {
+		padding: 20rpx 17rpx 26rpx 17rpx;
+		font-size: 30rpx;
+		color: #222;
+	}
+
+	.productList .list .item .text.on {
+		width: 508rpx;
+		padding: 0 0 0 22rpx;
+	}
+
+	.productList .list .item .text .money {
+		font-size: 26rpx;
+		font-weight: bold;
+		margin-top: 8rpx;
+	}
+
+	.productList .list .item .text .money.on {
+		margin-top: 50rpx;
+	}
+
+	.productList .list .item .text .money .num {
+		font-size: 34rpx;
+	}
+
+	.productList .list .item .text .vip {
+		font-size: 22rpx;
+		color: #aaa;
+		margin-top: 7rpx;
+	}
+
+	.productList .list .item .text .vip.on {
+		margin-top: 12rpx;
+	}
+
+	.productList .list .item .text .vip .vip-money {
+		font-size: 24rpx;
+		color: #282828;
+		font-weight: bold;
+	}
+
+	.productList .list .item .text .vip .vip-money image {
+		width: 46rpx;
+		height: 21rpx;
+		margin-left: 4rpx;
+	}
+
+	.pictrue {
+		position: relative;
+	}
+
+	.fixed {
+		z-index: 100;
+		position: fixed;
+		left: 0;
+		top: 0;
+		background: linear-gradient(90deg, red 50%, #ff5400 100%);
+
+	}
+
+	.mores-txt {
+		width: 100%;
+		align-items: center;
+		justify-content: center;
+		height: 70rpx;
+		color: #999;
+		font-size: 24rpx;
+
+		.iconfont {
+			margin-top: 2rpx;
+			font-size: 20rpx;
+		}
+	}
+
+	.menu-txt {
+		font-size: 24rpx;
+		color: #454545;
+	}
+
+	.mp-bg {
+		position: absolute;
+		left: 0;
+		/* #ifdef H5 */
+		top: 98rpx;
+		/* #endif */
+		width: 100%;
+		height: 304rpx;
+		// background: linear-gradient(180deg, #E93323 0%, #F5F5F5 100%, #751A12 100%);
+		// border-radius: 0 0 30rpx 30rpx;
+	}
+</style>

+ 492 - 0
pages/index/service/list.vue

@@ -0,0 +1,492 @@
+<template>
+	<view>
+		<view class='my-order'>
+      <!-- 顶部统计 -->
+      <view class='header bg-color'>
+				<view class='picTxt acea-row row-between-wrapper'>
+					<view class='text'>
+						<view class='name'>订单信息</view>
+					</view>
+					<view class='pictrue'>
+						<image src='../../../static/images/orderTime.png'></image>
+					</view>
+				</view>
+			</view>
+			<view class='nav acea-row row-around'>
+        <view class='item' :class='orderStatus === undefined ? "on": ""' @click="statusClick()">
+          <view>全部</view>
+          <view class='num'>{{ orderData.allCount || 0}}</view>
+        </view>
+				<view class='item' :class='orderStatus == 0 ? "on": ""' @click="statusClick(0)">
+					<view>待付款</view>
+					<view class='num'>{{orderData.unpaidCount || 0}}</view>
+				</view>
+				<view class='item' :class='orderStatus == 10 ? "on": ""' @click="statusClick(10)">
+					<view>待发货</view>
+					<view class='num'>{{orderData.undeliveredCount || 0}}</view>
+				</view>
+				<view class='item' :class='orderStatus == 20 ? "on": ""' @click="statusClick(20)">
+					<view>待收货</view>
+					<view class='num '>{{orderData.deliveredCount || 0}}</view>
+				</view>
+				<view class='item' :class='orderStatus == 30 ? "on": ""' @click="statusClick(30, false)">
+					<view>待评价</view>
+					<view class='num'>{{orderData.uncommentedCount || 0}}</view>
+				</view>
+			</view>
+			<view class='list'>
+				<view class='item' v-for="(order, index) in orderList" :key="index">
+					<view @click='goOrderDetails(order.id)'>
+						<view class='title acea-row row-between-wrapper'>
+							<view class="acea-row row-middle">
+								<text class="sign cart-color acea-row row-center-wrapper" v-if="order.typeName">{{ order.typeName }}</text>
+								<view>{{ formatDate(order.createTime) }}</view>
+							</view>
+              <!-- 订单状态 -->
+              <view class='font-color'>{{order.orderStatus}}</view>
+              <view v-if="order.status === 0" class="font-color">待付款</view>
+              <view v-else-if="order.status === 10 && order.deliveryType === 1" class="font-color">待发货</view>
+              <!-- TODO 芋艿:核销逻辑 -->
+              <view v-else-if="order.status === 10 && order.deliveryType === 2" class="font-color">待核销</view>
+              <view v-else-if="order.status === 20" class="font-color">待收货</view>
+              <view v-else-if="order.status === 30 && !order.commentStatus" class="font-color">待评价</view>
+              <view v-else-if="order.status === 30 && order.commentStatus" class="font-color">已完成</view>
+              <view v-else-if="order.status === 40" class="font-color">已关闭</view>
+						</view>
+            <!-- 订单项信息 -->
+            <view class='item-info acea-row row-between row-top' v-for="(item, index) in order.items" :key="index">
+							<view class='pictrue'>
+								<image :src='item.picUrl'></image>
+							</view>
+							<view class='text acea-row row-between'>
+								<view class='name line2'>{{ item.spuName }}</view>
+								<view class='money'>
+									<view>¥{{ fen2yuan(item.price) }}</view>
+									<view>x{{ item.count }}</view>
+								</view>
+							</view>
+						</view>
+            <!-- 订单金额 -->
+            <view class='totalPrice'>共{{ order.productCount }}件商品,总金额
+							<text class='money font-color'>¥{{ fen2yuan(order.payPrice) }}</text>
+						</view>
+					</view>
+          <!-- 订单操作区 -->
+          <view class='bottom acea-row row-right row-middle'>
+						<view class='bnt cancelBnt' v-if="order.status === 0" @click='cancelOrder(index, order.id)'>
+              取消订单
+            </view>
+						<view class='bnt bg-color' v-if="order.status === 0" @click='goPay(order.id, order.payOrderId)'>
+              立即付款
+            </view>
+            <view class='bnt bg-color' v-if="order.status === 30 && !order.commentStatus" @click='goOrderDetails(order.id)'>
+              去评价
+            </view>
+            <view class='bnt cancelBnt' v-if="order.status === 40" @click='delOrder(order.id, index)'>
+              删除订单
+            </view>
+            <view class='bnt bg-color' @click='goOrderDetails(order.id)'>
+              查看详情
+            </view>
+					</view>
+				</view>
+			</view>
+			<view class='loadingicon acea-row row-center-wrapper' v-if="orderList.length>0">
+				<text class='loading iconfont icon-jiazai' :hidden='!loading' /> {{loadTitle}}
+			</view>
+			<view v-if="orderList.length === 0">
+				<emptyPage title="暂无订单~"></emptyPage>
+			</view>
+		</view>
+		<view class='noCart' v-if="orderList.length === 0 && page > 1">
+			<view class='pictrue'>
+				<image src='/images/noOrder.png'></image>
+			</view>
+		</view>
+		<home></home>
+	</view>
+</template>
+<script>
+	import { openOrderSubscribe } from '@/utils/SubscribeMessage.js';
+	import home from '@/components/home';
+	import payment from '@/components/payment';
+	import { toLogin } from '@/libs/login.js';
+	import { mapGetters } from "vuex";
+	import emptyPage from '@/components/emptyPage.vue'
+  import * as OrderApi from '@/api/trade/order.js';
+  import dayjs from '@/plugin/dayjs/dayjs.min.js';
+  import * as Util from '@/utils/util.js';
+  export default {
+		components: {
+			payment,
+			home,
+			emptyPage
+		},
+		data() {
+			return {
+				loading: false, //是否加载中
+				loadend: false, //是否加载完毕
+				loadTitle: '加载更多', //提示语
+        orderList: [], // 订单数组
+        orderData: {}, // 订单详细统计
+        orderStatus: undefined, // Tab 的订单状态
+        commentStatus: undefined, // Tab 的评论状态
+				page: 1,
+				limit: 20,
+			};
+		},
+		computed: mapGetters(['isLogin', 'userInfo']),
+		onShow() {
+			if (!this.isLogin) {
+        toLogin();
+        return;
+			}
+
+      this.loadend = false;
+      this.page = 1;
+      this.$set(this, 'orderList', []);
+      this.getOrderData();
+      this.getOrderList();
+		},
+		methods: {
+			/**
+			 * 生命周期函数--监听页面加载
+			 */
+			onLoad: function(options) {
+				if (options.status) {
+				  this.orderStatus = options.status;
+				}
+			},
+			/**
+			 * 获取订单统计数据
+			 */
+			getOrderData: function() {
+        OrderApi.getOrderCount().then(res => {
+					this.$set(this, 'orderData', res.data);
+				})
+			},
+      /**
+       * 切换类型
+       */
+      statusClick: function(status, commentStatus) {
+		  console.log(status, this.orderStatus,'status === this.orderStatus')
+        if (status === this.orderStatus) {
+          return;
+        }
+        this.orderStatus = status;
+        this.commentStatus = commentStatus;
+        this.loadend = false;
+        this.page = 1;
+        this.$set(this, 'orderList', []);
+        this.getOrderList();
+      },
+      /**
+       * 获取订单列表
+       */
+      getOrderList: function() {
+        if (this.loadend || this.loading) {
+          return;
+        }
+        this.loading = true;
+        this.loadTitle = "加载更多";
+        OrderApi.getOrderPage({
+          status: this.orderStatus,
+          commentStatus: this.commentStatus,
+          pageNo: this.page,
+          pageSize: this.limit
+        }).then(res => {
+          const list = res.data.list || [];
+          list.forEach(item => {
+            item.typeName = item.type === 1 ? '秒杀'
+              : item.type === 2 ? '砍价'
+                : item.type === 3 ? '拼团' : undefined;
+          });
+
+          // 设置结束
+          const loadend = list.length < this.limit;
+          this.orderList = this.$util.SplitArray(list, this.orderList);
+          this.$set(this, 'orderList', this.orderList);
+          this.loadend = loadend;
+          this.loading = false;
+          this.loadTitle = loadend ? "我也是有底线的" : '加载更多';
+          this.page = this.page + 1;
+        }).catch(err => {
+          this.loading = false;
+          this.loadTitle = "加载更多";
+        })
+      },
+			/**
+			 * 打开支付组件
+			 */
+			goPay(id, payOrderId) {
+        const returnUrl = encodeURIComponent('/pages/order_pay_status/index?order_id=' + id);
+        uni.navigateTo({
+          url: `/pages/goods/cashier/index?order_id=${payOrderId}&returnUrl=${returnUrl}`
+        })
+			},
+			/**
+			 * 去订单详情
+			 */
+			goOrderDetails: function(order_id) {
+				if (!order_id) {
+          return this.$util.Tips({
+            title: '缺少订单号无法查看订单详情'
+          });
+        }
+				// #ifdef MP
+				uni.showLoading({
+					title: '正在加载',
+				})
+				openOrderSubscribe().then(() => {
+					uni.hideLoading();
+					uni.navigateTo({
+						url: '/pages/order_details/index?order_id=' + order_id
+					})
+				}).catch(() => {
+					uni.hideLoading();
+				})
+				// #endif
+				// #ifndef MP
+				uni.navigateTo({
+					url: '/pages/order_details/index?order_id=' + order_id
+				})
+				// #endif
+			},
+      /**
+       * 取消订单
+       */
+      cancelOrder: function(index, order_id) {
+        uni.showModal({
+          title: '提示',
+          content: '确认取消该订单?',
+          success: res => {
+            if (res.confirm) {
+              OrderApi.cancelOrder(order_id).then(() => {
+                this.$util.Tips({
+                  title: '取消成功'
+                }, () => {
+                  this.orderList.splice(index, 1);
+                  this.$set(this, 'orderList', this.orderList);
+                  this.getOrderData();
+                })
+              }).catch((err) => {
+                this.$util.Tips({
+                  title: err
+                })
+              });
+            }
+          }
+        });
+      },
+			/**
+			 * 删除订单
+			 */
+			delOrder: function(order_id, index) {
+        uni.showModal({
+          title: '提示',
+          content: '确认删除该订单?',
+          success: res => {
+            if (res.confirm) {
+              OrderApi.deleteOrder(order_id).then(() => {
+                this.$util.Tips({
+                  title: '删除成功'
+                }, () => {
+                  this.orderList.splice(index, 1);
+                  this.$set(this, 'orderList', this.orderList);
+                  this.getOrderData();
+                })
+              }).catch((err) => {
+                this.$util.Tips({
+                  title: err
+                })
+              });
+            }
+          }
+        });
+			},
+
+      fen2yuan(price) {
+        return Util.fen2yuan(price)
+      },
+      formatDate: function(date) {
+        return dayjs(date).format("YYYY-MM-DD HH:mm:ss");
+      }
+		},
+		onReachBottom: function() {
+			this.getOrderList();
+		}
+	}
+</script>
+<style scoped lang="scss">
+	.my-order .header {
+		height: 250rpx;
+		padding: 0 30rpx;
+	}
+
+	.my-order .header .picTxt {
+		height: 190rpx;
+	}
+
+	.my-order .header .picTxt .text {
+		color: rgba(255, 255, 255, 0.8);
+		font-size: 26rpx;
+		font-family: 'Guildford Pro';
+	}
+
+	.my-order .header .picTxt .text .name {
+		font-size: 34rpx;
+		font-weight: bold;
+		color: #fff;
+		margin-bottom: 20rpx;
+	}
+
+	.my-order .header .picTxt .pictrue {
+		width: 122rpx;
+		height: 109rpx;
+	}
+
+	.my-order .header .picTxt .pictrue image {
+		width: 100%;
+		height: 100%;
+	}
+
+	.my-order .nav {
+		background-color: #fff;
+		width: 690rpx;
+		height: 140rpx;
+		border-radius: 14rpx;
+		margin: -60rpx auto 0 auto;
+	}
+
+	.my-order .nav .item {
+		text-align: center;
+		font-size: 26rpx;
+		color: #282828;
+		padding: 26rpx 0;
+	}
+
+	.my-order .nav .item.on {
+		// font-weight: bold;
+		// border-bottom: 5rpx solid #e93323;
+		/* #ifdef H5 || MP */
+		font-weight: bold;
+		/* #endif */
+		border-bottom: 5rpx solid $theme-color;
+	}
+
+	.my-order .nav .item .num {
+		margin-top: 18rpx;
+	}
+
+	.my-order .list {
+		width: 690rpx;
+		margin: 14rpx auto 0 auto;
+	}
+
+	.my-order .list .item {
+		background-color: #fff;
+		border-radius: 14rpx;
+		margin-bottom: 14rpx;
+	}
+
+	.my-order .list .item .title {
+		height: 84rpx;
+		padding: 0 24rpx;
+		border-bottom: 1rpx solid #eee;
+		font-size: 28rpx;
+		color: #282828;
+	}
+
+	.my-order .list .item .title .sign {
+		font-size: 24rpx;
+		padding: 0 13rpx;
+		height: 36rpx;
+		margin-right: 15rpx;
+		border-radius: 18rpx;
+	}
+
+	.my-order .list .item .item-info {
+		padding: 0 24rpx;
+		margin-top: 22rpx;
+	}
+
+	.my-order .list .item .item-info .pictrue {
+		width: 120rpx;
+		height: 120rpx;
+	}
+
+	.my-order .list .item .item-info .pictrue image {
+		width: 100%;
+		height: 100%;
+		border-radius: 14rpx;
+	}
+
+	.my-order .list .item .item-info .text {
+		width: 500rpx;
+		font-size: 28rpx;
+		color: #999;
+	}
+
+	.my-order .list .item .item-info .text .name {
+		width: 350rpx;
+		color: #282828;
+	}
+
+	.my-order .list .item .item-info .text .money {
+		text-align: right;
+	}
+
+	.my-order .list .item .totalPrice {
+		font-size: 26rpx;
+		color: #282828;
+		text-align: right;
+		margin: 27rpx 0 0 30rpx;
+		padding: 0 30rpx 30rpx 0;
+		border-bottom: 1rpx solid #eee;
+	}
+
+	.my-order .list .item .totalPrice .money {
+		font-size: 28rpx;
+		font-weight: bold;
+	}
+
+	.my-order .list .item .bottom {
+		height: 107rpx;
+		padding: 0 30rpx;
+	}
+
+	.my-order .list .item .bottom .bnt {
+		width: 176rpx;
+		height: 60rpx;
+		text-align: center;
+		line-height: 60rpx;
+		color: #fff;
+		border-radius: 50rpx;
+		font-size: 27rpx;
+	}
+
+	.my-order .list .item .bottom .bnt.cancelBnt {
+		border: 1rpx solid #ddd;
+		color: #aaa;
+	}
+
+	.my-order .list .item .bottom .bnt~.bnt {
+		margin-left: 17rpx;
+	}
+
+	.noCart {
+		margin-top: 171rpx;
+		padding-top: 0.1rpx;
+	}
+
+	.noCart .pictrue {
+		width: 414rpx;
+		height: 336rpx;
+		margin: 78rpx auto 56rpx auto;
+	}
+
+	.noCart .pictrue image {
+		width: 100%;
+		height: 100%;
+	}
+</style>

+ 45 - 0
sheep/api/infra/file.js

@@ -0,0 +1,45 @@
+import { baseUrl, apiPath } from '@/sheep/config';
+
+const FileApi = {
+  // 上传文件
+  uploadFile: (file) => {
+    // TODO 芋艿:访问令牌的接入;
+    const token = uni.getStorageSync('token');
+    uni.showLoading({
+      title: '上传中',
+    });
+    return new Promise((resolve, reject) => {
+      uni.uploadFile({
+        url: baseUrl + apiPath + '/infra/file/upload',
+        filePath: file,
+        name: 'file',
+        header: {
+          // Accept: 'text/json',
+          Accept : '*/*',
+          'tenant-id' :'1',
+          // Authorization:  'Bearer test247',
+        },
+        success: (uploadFileRes) => {
+          let result = JSON.parse(uploadFileRes.data);
+          if (result.error === 1) {
+            uni.showToast({
+              icon: 'none',
+              title: result.msg,
+            });
+          } else {
+            return resolve(result);
+          }
+        },
+        fail: (error) => {
+          console.log('上传失败:', error);
+          return resolve(false);
+        },
+        complete: () => {
+          uni.hideLoading();
+        },
+      });
+    });
+  },
+};
+
+export default FileApi;

+ 53 - 0
sheep/api/member/address.js

@@ -0,0 +1,53 @@
+import request from '@/sheep/request';
+
+const AddressApi = {
+  // 获得用户收件地址列表
+  getAddressList: () => {
+    return request({
+      url: '/member/address/list',
+      method: 'GET'
+    });
+  },
+  // 创建用户收件地址
+  createAddress: (data) => {
+    return request({
+      url: '/member/address/create',
+      method: 'POST',
+      data,
+      custom: {
+        showSuccess: true,
+        successMsg: '保存成功'
+      },
+    });
+  },
+  // 更新用户收件地址
+  updateAddress: (data) => {
+    return request({
+      url: '/member/address/update',
+      method: 'PUT',
+      data,
+      custom: {
+        showSuccess: true,
+        successMsg: '更新成功'
+      },
+    });
+  },
+  // 获得用户收件地址
+  getAddress: (id) => {
+    return request({
+      url: '/member/address/get',
+      method: 'GET',
+      params: { id }
+    });
+  },
+  // 删除用户收件地址
+  deleteAddress: (id) => {
+    return request({
+      url: '/member/address/delete',
+      method: 'DELETE',
+      params: { id }
+    });
+  },
+};
+
+export default AddressApi;

+ 132 - 0
sheep/api/member/auth.js

@@ -0,0 +1,132 @@
+import request from '@/sheep/request';
+
+const AuthUtil = {
+  // 使用手机 + 密码登录
+  login: (data) => {
+    return request({
+      url: '/member/auth/login',
+      method: 'POST',
+      data,
+      custom: {
+        showSuccess: true,
+        loadingMsg: '登录中',
+        successMsg: '登录成功',
+      },
+    });
+  },
+  // 使用手机 + 验证码登录
+  smsLogin: (data) => {
+    return request({
+      url: '/member/auth/sms-login',
+      method: 'POST',
+      data,
+      custom: {
+        showSuccess: true,
+        loadingMsg: '登录中',
+        successMsg: '登录成功',
+      },
+    });
+  },
+  // 发送手机验证码
+  sendSmsCode: (mobile, scene) => {
+    return request({
+      url: '/member/auth/send-sms-code',
+      method: 'POST',
+      data: {
+        mobile,
+        scene,
+      },
+      custom: {
+        loadingMsg: '发送中',
+        showSuccess: true,
+        successMsg: '发送成功',
+      },
+    });
+  },
+  // 登出系统
+  logout: () => {
+    return request({
+      url: '/member/auth/logout',
+      method: 'POST',
+    });
+  },
+  // 刷新令牌
+  refreshToken: (refreshToken) => {
+    return request({
+      url: '/member/auth/refresh-token',
+      method: 'POST',
+      params: {
+        refreshToken
+      },
+      custom: {
+        loading: false, // 不用加载中
+        showError: false, // 不展示错误提示
+      },
+    });
+  },
+  // 社交授权的跳转
+  socialAuthRedirect: (type, redirectUri) => {
+    return request({
+      url: '/member/auth/social-auth-redirect',
+      method: 'GET',
+      params: {
+        type,
+        redirectUri,
+      },
+      custom: {
+        showSuccess: true,
+        loadingMsg: '登陆中',
+      },
+    });
+  },
+  // 社交快捷登录
+  socialLogin: (type, code, state) => {
+    return request({
+      url: '/member/auth/social-login',
+      method: 'POST',
+      data: {
+        type,
+        code,
+        state,
+      },
+      custom: {
+        showSuccess: true,
+        loadingMsg: '登陆中',
+      },
+    });
+  },
+  // 微信小程序的一键登录
+  weixinMiniAppLogin: (phoneCode, loginCode, state) => {
+    return request({
+      url: '/member/auth/weixin-mini-app-login',
+      method: 'POST',
+      data: {
+        phoneCode,
+        loginCode,
+        state
+      },
+      custom: {
+        showSuccess: true,
+        loadingMsg: '登陆中',
+        successMsg: '登录成功',
+      },
+    });
+  },
+  // 创建微信 JS SDK 初始化所需的签名
+  createWeixinMpJsapiSignature: (url) => {
+    return request({
+      url: '/member/auth/create-weixin-jsapi-signature',
+      method: 'POST',
+      params: {
+        url
+      },
+      custom: {
+        showError: false,
+        showLoading: false,
+      },
+    })
+  },
+  //
+};
+
+export default AuthUtil;

+ 19 - 0
sheep/api/member/point.js

@@ -0,0 +1,19 @@
+import request from '@/sheep/request';
+
+const PointApi = {
+  // 获得用户积分记录分页
+  getPointRecordPage: (params) => {
+    if (params.addStatus === undefined) {
+      delete params.addStatus
+    }
+    const queryString = Object.keys(params)
+      .map((key) => encodeURIComponent(key) + '=' + params[key])
+      .join('&');
+    return request({
+      url: `/member/point/record/page?${queryString}`,
+      method: 'GET',
+    });
+  }
+};
+
+export default PointApi;

+ 37 - 0
sheep/api/member/signin.js

@@ -0,0 +1,37 @@
+import request from '@/sheep/request';
+
+const SignInApi = {
+  // 获得签到规则列表
+  getSignInConfigList: () => {
+    return request({
+      url: '/member/sign-in/config/list',
+      method: 'GET',
+    });
+  },
+  // 获得个人签到统计
+  getSignInRecordSummary: () => {
+    return request({
+      url: '/member/sign-in/record/get-summary',
+      method: 'GET',
+    });
+  },
+  // 签到
+  createSignInRecord: () => {
+    return request({
+      url: '/member/sign-in/record/create',
+      method: 'POST',
+    });
+  },
+  // 获得签到记录分页
+  getSignRecordPage: (params) => {
+    const queryString = Object.keys(params)
+      .map((key) => encodeURIComponent(key) + '=' + params[key])
+      .join('&');
+    return request({
+      url: `/member/sign-in/record/page?${queryString}`,
+      method: 'GET',
+    });
+  },
+};
+
+export default SignInApi;

+ 54 - 0
sheep/api/member/social.js

@@ -0,0 +1,54 @@
+import request from '@/sheep/request';
+
+const SocialApi = {
+  // 获得社交用户
+  getSocialUser: (type) => {
+    return request({
+      url: '/member/social-user/get',
+      method: 'GET',
+      params: {
+        type
+      },
+      custom: {
+        showLoading: false,
+      },
+    });
+  },
+  // 社交绑定
+  socialBind: (type, code, state) => {
+    return request({
+      url: '/member/social-user/bind',
+      method: 'POST',
+      data: {
+        type,
+        code,
+        state
+      },
+      custom: {
+        custom: {
+          showSuccess: true,
+          loadingMsg: '绑定中',
+          successMsg: '绑定成功',
+        },
+      },
+    });
+  },
+  // 社交绑定
+  socialUnbind: (type, openid) => {
+    return request({
+      url: '/member/social-user/unbind',
+      method: 'DELETE',
+      data: {
+        type,
+        openid
+      },
+      custom: {
+        showLoading: false,
+        loadingMsg: '解除绑定',
+        successMsg: '解绑成功',
+      },
+    });
+  },
+};
+
+export default SocialApi;

+ 85 - 0
sheep/api/member/user.js

@@ -0,0 +1,85 @@
+import request from '@/sheep/request';
+
+const UserApi = {
+  // 获得基本信息
+  getUserInfo: () => {
+    return request({
+      url: '/member/user/get',
+      method: 'GET',
+      custom: {
+        showLoading: false,
+        auth: true,
+      },
+    });
+  },
+  // 修改基本信息
+  updateUser: (data) => {
+    return request({
+      url: '/member/user/update',
+      method: 'PUT',
+      data,
+      custom: {
+        auth: true,
+        showSuccess: true,
+        successMsg: '更新成功'
+      },
+    });
+  },
+  // 修改用户手机
+  updateUserMobile: (data) => {
+    return request({
+      url: '/member/user/update-mobile',
+      method: 'PUT',
+      data,
+      custom: {
+        loadingMsg: '验证中',
+        showSuccess: true,
+        successMsg: '修改成功'
+      },
+    });
+  },
+  // 基于微信小程序的授权码,修改用户手机
+  updateUserMobileByWeixin: (code) => {
+    return request({
+      url: '/member/user/update-mobile-by-weixin',
+      method: 'PUT',
+      data: {
+        code
+      },
+      custom: {
+        showSuccess: true,
+        loadingMsg: '获取中',
+        successMsg: '修改成功'
+      },
+    });
+  },
+  // 修改密码
+  updateUserPassword: (data) => {
+    return request({
+      url: '/member/user/update-password',
+      method: 'PUT',
+      data,
+      custom: {
+        loadingMsg: '验证中',
+        showSuccess: true,
+        successMsg: '修改成功'
+      },
+    });
+  },
+  // 重置密码
+  resetUserPassword: (data) => {
+    return request({
+      url: '/member/user/reset-password',
+      method: 'PUT',
+      data,
+      custom: {
+        loadingMsg: '验证中',
+        showSuccess: true,
+        successMsg: '修改成功'
+      }
+    });
+  },
+
+};
+
+export default UserApi;

+ 21 - 0
sheep/api/migration/app.js

@@ -0,0 +1,21 @@
+import request from '@/sheep/request';
+
+// TODO 芋艿:小程序直播还不支持
+export default {
+  //小程序直播
+  mplive: {
+    getRoomList: (ids) =>
+      request({
+        url: 'app/mplive/getRoomList',
+        method: 'GET',
+        params: {
+          ids: ids.join(','),
+        }
+      }),
+    getMpLink: () =>
+      request({
+        url: 'app/mplive/getMpLink',
+        method: 'GET'
+      }),
+  },
+};

+ 14 - 0
sheep/api/migration/chat.js

@@ -0,0 +1,14 @@
+import request from '@/sheep/request';
+
+// TODO 芋艿:暂不支持 socket 聊天
+export default {
+  // 获取聊天token
+  unifiedToken: () =>
+    request({
+      url: 'unifiedToken',
+      custom: {
+        showError: false,
+        showLoading: false,
+      },
+    }),
+};

+ 15 - 0
sheep/api/migration/index.js

@@ -0,0 +1,15 @@
+// 使用 require.context 读取当前目录下的所有 .js 文件
+const files = require.context('./', false, /\.js$/);
+let api = {};
+
+// 遍历导入的模块,并将其添加到 api 对象中
+files.keys().forEach((key) => {
+  // 通过 key 提取文件名,并将对应模块的 default 属性添加到 api 对象
+  const moduleName = key.replace(/(.*\/)*([^.]+).*/gi, '$2');
+  api = {
+    ...api,
+    [moduleName]: files(key).default,
+  };
+});
+
+export default api;

+ 44 - 0
sheep/api/migration/third.js

@@ -0,0 +1,44 @@
+import request from '@/sheep/request';
+import { baseUrl, apiPath } from '@/sheep/config';
+
+export default {
+  // 微信相关
+  wechat: {
+    // 小程序订阅消息
+    subscribeTemplate: (params) =>
+      request({
+        url: 'third/wechat/subscribeTemplate',
+        method: 'GET',
+        params: {
+          platform: 'miniProgram',
+        },
+        custom: {
+          showError: false,
+          showLoading: false,
+        },
+      }),
+
+    // 获取微信小程序码
+    getWxacode: (path) =>
+      `${baseUrl}${apiPath}third/wechat/wxacode?platform=miniProgram&payload=${encodeURIComponent(
+        JSON.stringify({
+          path,
+        }),
+      )}`,
+  },
+
+  // 苹果相关
+  apple: {
+    // 第三方登录
+    login: (data) =>
+      request({
+        url: 'third/apple/login',
+        method: 'POST',
+        data,
+        custom: {
+          showSuccess: true,
+          loadingMsg: '登陆中',
+        },
+      }),
+  },
+};

+ 14 - 0
sheep/api/pay/channel.js

@@ -0,0 +1,14 @@
+import request from '@/sheep/request';
+
+const PayChannelApi = {
+  // 获得指定应用的开启的支付渠道编码列表
+  getEnableChannelCodeList: (appId) => {
+    return request({
+      url: '/pay/channel/get-enable-code-list',
+      method: 'GET',
+      params: { appId }
+    });
+  },
+};
+
+export default PayChannelApi;

+ 22 - 0
sheep/api/pay/order.js

@@ -0,0 +1,22 @@
+import request from '@/sheep/request';
+
+const PayOrderApi = {
+  // 获得支付订单
+  getOrder: (id) => {
+    return request({
+      url: '/pay/order/get',
+      method: 'GET',
+      params: { id }
+    });
+  },
+  // 提交支付订单
+  submitOrder: (data) => {
+    return request({
+      url: '/pay/order/submit',
+      method: 'POST',
+      data
+    });
+  }
+};
+
+export default PayOrderApi;

+ 68 - 0
sheep/api/pay/wallet.js

@@ -0,0 +1,68 @@
+import request from '@/sheep/request';
+
+const PayWalletApi = {
+  // 获取钱包
+  getPayWallet() {
+    return request({
+      url: '/pay/wallet/get',
+      method: 'GET',
+      custom: {
+        showLoading: false,
+        auth: true,
+      },
+    });
+  },
+  // 获得钱包流水分页
+  getWalletTransactionPage: (params) => {
+    const queryString = Object.keys(params)
+      .map((key) => encodeURIComponent(key) + '=' + params[key])
+      .join('&');
+    return request({
+      url: `/pay/wallet-transaction/page?${queryString}`,
+      method: 'GET',
+    });
+  },
+  // 获得钱包流水统计
+  getWalletTransactionSummary: (params) => {
+    const queryString = `createTime=${params.createTime[0]}&createTime=${params.createTime[1]}`;
+    return request({
+      url: `/pay/wallet-transaction/get-summary?${queryString}`,
+      // url: `/pay/wallet-transaction/get-summary`,
+      method: 'GET',
+      // params: params
+    });
+  },
+  // 获得钱包充值套餐列表
+  getWalletRechargePackageList: () => {
+    return request({
+      url: '/pay/wallet-recharge-package/list',
+      method: 'GET',
+      custom: {
+        showError: false,
+        showLoading: false,
+      },
+    });
+  },
+  // 创建钱包充值记录(发起充值)
+  createWalletRecharge: (data) => {
+    return request({
+      url: '/pay/wallet-recharge/create',
+      method: 'POST',
+      data,
+    });
+  },
+  // 获得钱包充值记录分页
+  getWalletRechargePage: (params) => {
+    return request({
+      url: '/pay/wallet-recharge/page',
+      method: 'GET',
+      params,
+      custom: {
+        showError: false,
+        showLoading: false,
+      },
+    });
+  },
+};
+
+export default PayWalletApi;

+ 21 - 0
sheep/api/product/category.js

@@ -0,0 +1,21 @@
+import request from '@/sheep/request';
+
+const CategoryApi = {
+  // 查询分类列表
+  getCategoryList: () => {
+    return request({
+      url: '/product/category/list',
+      method: 'GET',
+    });
+  },
+  // 查询分类列表,指定编号
+  getCategoryListByIds: (ids) => {
+    return request({
+      url: '/product/category/list-by-ids',
+      method: 'GET',
+      params: { ids },
+    });
+  },
+};
+
+export default CategoryApi;

+ 22 - 0
sheep/api/product/comment.js

@@ -0,0 +1,22 @@
+import request from '@/sheep/request';
+
+const CommentApi = {
+  // 获得商品评价分页
+  getCommentPage: (spuId, pageNo, pageSize, type) => {
+    return request({
+      url: '/product/comment/page',
+      method: 'GET',
+      params: {
+        spuId,
+        pageNo,
+        pageSize,
+        type,
+      },
+      custom: {
+        showLoading: false,
+        showError: false,
+      },
+    });
+  },
+};
+export default CommentApi;

+ 54 - 0
sheep/api/product/favorite.js

@@ -0,0 +1,54 @@
+import request from '@/sheep/request';
+
+const FavoriteApi = {
+  // 获得商品收藏分页
+  getFavoritePage: (data) => {
+    return request({
+      url: '/product/favorite/page',
+      method: 'GET',
+      params: data,
+    });
+  },
+  // 检查是否收藏过商品
+  isFavoriteExists: (spuId) => {
+    return request({
+      url: '/product/favorite/exits',
+      method: 'GET',
+      params: {
+        spuId,
+      },
+    });
+  },
+  // 添加商品收藏
+  createFavorite: (spuId) => {
+    return request({
+      url: '/product/favorite/create',
+      method: 'POST',
+      data: {
+        spuId,
+      },
+      custom: {
+        auth: true,
+        showSuccess: true,
+        successMsg: '收藏成功',
+      },
+    });
+  },
+  // 取消商品收藏
+  deleteFavorite: (spuId) => {
+    return request({
+      url: '/product/favorite/delete',
+      method: 'DELETE',
+      data: {
+        spuId,
+      },
+      custom: {
+        auth: true,
+        showSuccess: true,
+        successMsg: '取消成功',
+      },
+    });
+  },
+};
+
+export default FavoriteApi;

+ 39 - 0
sheep/api/product/history.js

@@ -0,0 +1,39 @@
+import request from '@/sheep/request';
+
+const SpuHistoryApi = {
+  // 删除商品浏览记录
+  deleteBrowseHistory: (spuIds) => {
+    return request({
+      url: '/product/browse-history/delete',
+      method: 'DELETE',
+      data: { spuIds },
+      custom: {
+        showSuccess: true,
+        successMsg: '删除成功',
+      },
+    });
+  },
+  // 清空商品浏览记录
+  cleanBrowseHistory: () => {
+    return request({
+      url: '/product/browse-history/clean',
+      method: 'DELETE',
+      custom: {
+        showSuccess: true,
+        successMsg: '清空成功',
+      },
+    });
+  },
+  // 获得商品浏览记录分页
+  getBrowseHistoryPage: (data) => {
+    return request({
+      url: '/product/browse-history/page',
+      method: 'GET',
+      data,
+      custom: {
+        showLoading: false
+      },
+    });
+  },
+};
+export default SpuHistoryApi;

+ 41 - 0
sheep/api/product/spu.js

@@ -0,0 +1,41 @@
+import request from '@/sheep/request';
+
+const SpuApi = {
+  // 获得商品 SPU 列表
+  getSpuListByIds: (ids) => {
+    return request({
+      url: '/product/spu/list-by-ids',
+      method: 'GET',
+      params: { ids },
+      custom: {
+        showLoading: false,
+        showError: false,
+      },
+    });
+  },
+  // 获得商品 SPU 分页
+  getSpuPage: (params) => {
+    return request({
+      url: '/product/spu/page',
+      method: 'GET',
+      params,
+      custom: {
+        showLoading: false,
+        showError: false,
+      },
+    });
+  },
+  // 查询商品
+  getSpuDetail: (id) => {
+    return request({
+      url: '/product/spu/get-detail',
+      method: 'GET',
+      params: { id },
+      custom: {
+        showLoading: false,
+        showError: false,
+      },
+    });
+  },
+};
+export default SpuApi;

+ 16 - 0
sheep/api/promotion/activity.js

@@ -0,0 +1,16 @@
+import request from '@/sheep/request';
+
+const ActivityApi = {
+  // 获得单个商品,近期参与的每个活动
+  getActivityListBySpuId: (spuId) => {
+    return request({
+      url: '/promotion/activity/list-by-spu-id',
+      method: 'GET',
+      params: {
+        spuId,
+      },
+    });
+  },
+};
+
+export default ActivityApi;

+ 12 - 0
sheep/api/promotion/article.js

@@ -0,0 +1,12 @@
+import request from '@/sheep/request';
+
+export default {
+    // 获得文章详情
+    getArticle: (id, title) => {
+        return request({
+            url: '/promotion/article/get',
+            method: 'GET',
+            params: { id, title }
+        });
+    }
+}

+ 76 - 0
sheep/api/promotion/combination.js

@@ -0,0 +1,76 @@
+import request from '@/sheep/request';
+
+// 拼团 API
+const CombinationApi = {
+  // 获得拼团活动列表
+  getCombinationActivityList: (count) => {
+    return request({
+      url: '/promotion/combination-activity/list',
+      method: 'GET',
+      params: { count },
+    });
+  },
+
+  // 获得拼团活动分页
+  getCombinationActivityPage: (params) => {
+    return request({
+      url: '/promotion/combination-activity/page',
+      method: 'GET',
+      params,
+    });
+  },
+
+  // 获得拼团活动明细
+  getCombinationActivity: (id) => {
+    return request({
+      url: '/promotion/combination-activity/get-detail',
+      method: 'GET',
+      params: {
+        id,
+      },
+    });
+  },
+
+  // 获得最近 n 条拼团记录(团长发起的)
+  getHeadCombinationRecordList: (activityId, status, count) => {
+    return request({
+      url: '/promotion/combination-record/get-head-list',
+      method: 'GET',
+      params: {
+        activityId,
+        status,
+        count,
+      },
+    });
+  },
+
+  // 获得我的拼团记录分页
+  getCombinationRecordPage: (params) => {
+    return request({
+      url: "/promotion/combination-record/page",
+      method: 'GET',
+      params
+    });
+  },
+
+  // 获得拼团记录明细
+  getCombinationRecordDetail: (id) => {
+    return request({
+      url: '/promotion/combination-record/get-detail',
+      method: 'GET',
+      params: {
+        id,
+      },
+    });
+  },
+
+  // 获得拼团记录的概要信息
+  getCombinationRecordSummary: () => {
+    return request({
+      url: '/promotion/combination-record/get-summary',
+      method: 'GET',
+    });
+  },
+};
+
+export default CombinationApi;

+ 101 - 0
sheep/api/promotion/coupon.js

@@ -0,0 +1,101 @@
+import request from '@/sheep/request';
+
+const CouponApi = {
+  // 获得优惠劵模板列表
+  getCouponTemplateListByIds: (ids) => {
+    return request({
+      url: '/promotion/coupon-template/list-by-ids',
+      method: 'GET',
+      params: { ids },
+      custom: {
+        showLoading: false, // 不展示 Loading,避免领取优惠劵时,不成功提示
+        showError: false,
+      },
+    });
+  },
+  // 获得优惠劵模版列表
+  getCouponTemplateList: (spuId, productScope, count) => {
+    return request({
+      url: '/promotion/coupon-template/list',
+      method: 'GET',
+      params: { spuId, productScope, count },
+    });
+  },
+  // 获得优惠劵模版分页
+  getCouponTemplatePage: (params) => {
+    return request({
+      url: '/promotion/coupon-template/page',
+      method: 'GET',
+      params,
+    });
+  },
+  // 获得优惠劵模版
+  getCouponTemplate: (id) => {
+    return request({
+      url: '/promotion/coupon-template/get',
+      method: 'GET',
+      params: { id },
+    });
+  },
+  // 我的优惠劵列表
+  getCouponPage: (params) => {
+    return request({
+      url: '/promotion/coupon/page',
+      method: 'GET',
+      params,
+    });
+  },
+  // 领取优惠券
+  takeCoupon: (templateId) => {
+    return request({
+      url: '/promotion/coupon/take',
+      method: 'POST',
+      data: { templateId },
+      custom: {
+        auth: true,
+        showLoading: true,
+        loadingMsg: '领取中',
+        showSuccess: true,
+        successMsg: '领取成功',
+      },
+    });
+  },
+  // 获得优惠劵
+  getCoupon: (id) => {
+    return request({
+      url: '/promotion/coupon/get',
+      method: 'GET',
+      params: { id },
+    });
+  },
+  // 获得未使用的优惠劵数量
+  getUnusedCouponCount: () => {
+    return request({
+      url: '/promotion/coupon/get-unused-count',
+      method: 'GET',
+      custom: {
+        showLoading: false,
+        auth: true,
+      },
+    });
+  },
+  // 获得匹配指定商品的优惠劵列表
+  getMatchCouponList: (price, spuIds, skuIds, categoryIds) => {
+    return request({
+      url: '/promotion/coupon/match-list',
+      method: 'GET',
+      params: {
+        price,
+        spuIds: spuIds.join(','),
+        skuIds: skuIds.join(','),
+        categoryIds: categoryIds.join(','),
+      },
+      custom: {
+        showError: false,
+        showLoading: false, // 避免影响 settlementOrder 结算的结果
+      },
+    });
+  }
+};
+
+export default CouponApi;

+ 38 - 0
sheep/api/promotion/diy.js

@@ -0,0 +1,38 @@
+import request from '@/sheep/request';
+
+const DiyApi = {
+  getUsedDiyTemplate: () => {
+    return request({
+      url: '/promotion/diy-template/used',
+      method: 'GET',
+      custom: {
+        showError: false,
+        showLoading: false,
+      },
+    });
+  },
+  getDiyTemplate: (id) => {
+    return request({
+      url: '/promotion/diy-template/get',
+      method: 'GET',
+      params: {
+        id
+      },
+      custom: {
+        showError: false,
+        showLoading: false,
+      },
+    });
+  },
+  getDiyPage: (id) => {
+    return request({
+      url: '/promotion/diy-page/get',
+      method: 'GET',
+      params: {
+        id
+      }
+    });
+  },
+};
+
+export default DiyApi;

+ 14 - 0
sheep/api/promotion/rewardActivity.js

@@ -0,0 +1,14 @@
+import request from '@/sheep/request';
+
+const RewardActivityApi = {
+  // 获得满减送活动
+  getRewardActivity: (id) => {
+    return request({
+      url: '/promotion/reward-activity/get',
+      method: 'GET',
+      params: { id },
+    });
+  }
+};
+
+export default RewardActivityApi;

+ 33 - 0
sheep/api/promotion/seckill.js

@@ -0,0 +1,33 @@
+import request from "@/sheep/request";
+
+const SeckillApi = {
+  // 获得秒杀时间段列表
+  getSeckillConfigList: () => {
+    return request({ url: 'promotion/seckill-config/list', method: 'GET' });
+  },
+
+  // 获得当前秒杀活动
+  getNowSeckillActivity: () => {
+    return request({ url: 'promotion/seckill-activity/get-now', method: 'GET' });
+  },
+
+  // 获得秒杀活动分页
+  getSeckillActivityPage: (params) => {
+    return request({ url: 'promotion/seckill-activity/page', method: 'GET', params });
+  },
+
+  /**
+   * 获得秒杀活动明细
+   * @param {number} id 秒杀活动编号
+   * @return {*}
+   */
+  getSeckillActivity: (id) => {
+    return request({
+      url: 'promotion/seckill-activity/get-detail',
+      method: 'GET',
+      params: { id }
+    });
+  }
+}
+
+export default SeckillApi;

+ 13 - 0
sheep/api/system/area.js

@@ -0,0 +1,13 @@
+import request from '@/sheep/request';
+
+const AreaApi = {
+  // 获得地区树
+  getAreaTree: () => {
+    return request({
+      url: '/system/area/tree',
+      method: 'GET'
+    });
+  },
+};
+
+export default AreaApi;

+ 63 - 0
sheep/api/trade/afterSale.js

@@ -0,0 +1,63 @@
+import request from '@/sheep/request';
+
+const AfterSaleApi = {
+  // 获得售后分页
+  getAfterSalePage: (params) => {
+    return request({
+      url: `/trade/after-sale/page`,
+      method: 'GET',
+      params,
+      custom: {
+        showLoading: false,
+      },
+    });
+  },
+  // 创建售后
+  createAfterSale: (data) => {
+    return request({
+      url: `/trade/after-sale/create`,
+      method: 'POST',
+      data,
+    });
+  },
+  // 获得售后
+  getAfterSale: (id) => {
+    return request({
+      url: `/trade/after-sale/get`,
+      method: 'GET',
+      params: {
+        id,
+      },
+    });
+  },
+  // 取消售后
+  cancelAfterSale: (id) => {
+    return request({
+      url: `/trade/after-sale/cancel`,
+      method: 'DELETE',
+      params: {
+        id,
+      },
+    });
+  },
+  // 获得售后日志列表
+  getAfterSaleLogList: (afterSaleId) => {
+    return request({
+      url: `/trade/after-sale-log/list`,
+      method: 'GET',
+      params: {
+        afterSaleId,
+      },
+    });
+  },
+  // 退回货物
+  deliveryAfterSale: (data) => {
+    return request({
+      url: `/trade/after-sale/delivery`,
+      method: 'PUT',
+      data,
+    });
+  }
+};
+
+export default AfterSaleApi;

+ 87 - 0
sheep/api/trade/brokerage.js

@@ -0,0 +1,87 @@
+import request from '@/sheep/request';
+
+const BrokerageApi = {
+  // 获得个人分销信息
+  getBrokerageUser: () => {
+    return request({
+      url: '/trade/brokerage-user/get',
+      method: 'GET'
+    });
+  },
+  // 获得个人分销统计
+  getBrokerageUserSummary: () => {
+    return request({
+      url: '/trade/brokerage-user/get-summary',
+      method: 'GET',
+    });
+  },
+  // 获得分销记录分页
+  getBrokerageRecordPage: (params) => {
+    if (params.status === undefined) {
+      delete params.status
+    }
+    const queryString = Object.keys(params)
+      .map((key) => encodeURIComponent(key) + '=' + params[key])
+      .join('&');
+    return request({
+      url: `/trade/brokerage-record/page?${queryString}`,
+      method: 'GET',
+    });
+  },
+  // 创建分销提现
+  createBrokerageWithdraw: (data) => {
+    return request({
+      url: '/trade/brokerage-withdraw/create',
+      method: 'POST',
+      data,
+    });
+  },
+  // 获得商品的分销金额
+  getProductBrokeragePrice: (spuId) => {
+    return request({
+      url: '/trade/brokerage-record/get-product-brokerage-price',
+      method: 'GET',
+      params: {
+        spuId
+      }
+    });
+  },
+  // 获得分销用户排行(基于佣金)
+  getRankByPrice: (params) => {
+    const queryString = `times=${params.times[0]}&times=${params.times[1]}`;
+    return request({
+      url: `/trade/brokerage-user/get-rank-by-price?${queryString}`,
+      method: 'GET',
+    });
+  },
+  // 获得分销用户排行分页(基于佣金)
+  getBrokerageUserChildSummaryPageByPrice: (params) => {
+    const queryString = Object.keys(params)
+      .map((key) => encodeURIComponent(key) + '=' + params[key])
+      .join('&');
+    return request({
+      url: `/trade/brokerage-user/rank-page-by-price?${queryString}`,
+      method: 'GET',
+    });
+  },
+  // 获得分销用户排行分页(基于用户量)
+  getBrokerageUserRankPageByUserCount: (params) => {
+    const queryString = Object.keys(params)
+      .map((key) => encodeURIComponent(key) + '=' + params[key])
+      .join('&');
+    return request({
+      url: `/trade/brokerage-user/rank-page-by-user-count?${queryString}`,
+      method: 'GET',
+    });
+  },
+  // 获得下级分销统计分页
+  getBrokerageUserChildSummaryPage: (params) => {
+    return request({
+      url: '/trade/brokerage-user/child-summary-page',
+      method: 'GET',
+      params,
+    })
+  }
+}
+
+export default BrokerageApi

+ 50 - 0
sheep/api/trade/cart.js

@@ -0,0 +1,50 @@
+import request from '@/sheep/request';
+
+const CartApi = {
+  addCart: (data) => {
+    return request({
+      url: '/trade/cart/add',
+      method: 'POST',
+      data: data,
+      custom: {
+        showSuccess: true,
+        successMsg: '已添加到购物车~',
+      }
+    });
+  },
+  updateCartCount: (data) => {
+    return request({
+      url: '/trade/cart/update-count',
+      method: 'PUT',
+      data: data
+    });
+  },
+  updateCartSelected: (data) => {
+    return request({
+      url: '/trade/cart/update-selected',
+      method: 'PUT',
+      data: data
+    });
+  },
+  deleteCart: (ids) => {
+    return request({
+      url: '/trade/cart/delete',
+      method: 'DELETE',
+      params: {
+        ids
+      }
+    });
+  },
+  getCartList: () => {
+    return request({
+      url: '/trade/cart/list',
+      method: 'GET',
+      custom: {
+        showLoading: false,
+        auth: true,
+      },
+    });
+  },
+};
+
+export default CartApi;

+ 13 - 0
sheep/api/trade/config.js

@@ -0,0 +1,13 @@
+import request from '@/sheep/request';
+
+const TradeConfigApi = {
+  // 获得交易配置
+  getTradeConfig: () => {
+    return request({
+      url: `/trade/config/get`,
+      method: 'GET',
+    });
+  },
+};
+
+export default TradeConfigApi;

+ 13 - 0
sheep/api/trade/delivery.js

@@ -0,0 +1,13 @@
+import request from '@/sheep/request';
+
+const DeliveryApi = {
+  // 获得快递公司列表
+  getDeliveryExpressList: () => {
+    return request({
+      url: `/trade/delivery/express/list`,
+      method: 'get',
+    });
+  }
+};
+
+export default DeliveryApi;

+ 146 - 0
sheep/api/trade/order.js

@@ -0,0 +1,146 @@
+import request from '@/sheep/request';
+
+const OrderApi = {
+  // 计算订单信息
+  settlementOrder: (data) => {
+    const data2 = {
+      ...data,
+    };
+    // 移除多余字段
+    if (!(data.couponId > 0)) {
+      delete data2.couponId;
+    }
+    if (!(data.addressId > 0)) {
+      delete data2.addressId;
+    }
+    if (!(data.combinationActivityId > 0)) {
+      delete data2.combinationActivityId;
+    }
+    if (!(data.combinationHeadId > 0)) {
+      delete data2.combinationHeadId;
+    }
+    if (!(data.seckillActivityId > 0)) {
+      delete data2.seckillActivityId;
+    }
+    // 解决 SpringMVC 接受 List<Item> 参数的问题
+    delete data2.items;
+    for (let i = 0; i < data.items.length; i++) {
+      data2[encodeURIComponent('items[' + i + '' + '].skuId')] = data.items[i].skuId + '';
+      data2[encodeURIComponent('items[' + i + '' + '].count')] = data.items[i].count + '';
+      if (data.items[i].cartId) {
+        data2[encodeURIComponent('items[' + i + '' + '].cartId')] = data.items[i].cartId + '';
+      }
+    }
+    const queryString = Object.keys(data2)
+      .map((key) => key + '=' + data2[key])
+      .join('&');
+    return request({
+      url: `/trade/order/settlement?${queryString}`,
+      method: 'GET',
+      custom: {
+        showError: true,
+        showLoading: true,
+      },
+    });
+  },
+  // 创建订单
+  createOrder: (data) => {
+    return request({
+      url: `/trade/order/create`,
+      method: 'POST',
+      data,
+    });
+  },
+  // 获得订单
+  getOrder: (id) => {
+    return request({
+      url: `/trade/order/get-detail`,
+      method: 'GET',
+      params: {
+        id,
+      },
+      custom: {
+        showLoading: false,
+      },
+    });
+  },
+  // 订单列表
+  getOrderPage: (params) => {
+    return request({
+      url: '/trade/order/page',
+      method: 'GET',
+      params,
+      custom: {
+        showLoading: false,
+      },
+    });
+  },
+  // 确认收货
+  receiveOrder: (id) => {
+    return request({
+      url: `/trade/order/receive`,
+      method: 'PUT',
+      params: {
+        id,
+      },
+    });
+  },
+  // 取消订单
+  cancelOrder: (id) => {
+    return request({
+      url: `/trade/order/cancel`,
+      method: 'DELETE',
+      params: {
+        id,
+      },
+    });
+  },
+  // 删除订单
+  deleteOrder: (id) => {
+    return request({
+      url: `/trade/order/delete`,
+      method: 'DELETE',
+      params: {
+        id,
+      },
+    });
+  },
+  // 获得交易订单的物流轨迹
+  getOrderExpressTrackList: (id) => {
+    return request({
+      url: `/trade/order/get-express-track-list`,
+      method: 'GET',
+      params: {
+        id,
+      },
+    });
+  },
+  aaa: (data) => {
+    return request({
+      url: `http://api.tanshuapi.com/api/exp/v1/index`,
+      method: 'GET',
+      data,
+    });
+  },
+  // 获得交易订单数量
+  getOrderCount: () => {
+    return request({
+      url: '/trade/order/get-count',
+      method: 'GET',
+      custom: {
+        showLoading: false,
+        auth: true,
+      },
+    });
+  },
+  // 创建单个评论
+  createOrderItemComment: (data) => {
+    return request({
+      url: `/trade/order/item/create-comment`,
+      method: 'POST',
+      data,
+    });
+  },
+};
+
+export default OrderApi;

+ 105 - 0
sheep/components/s-activity-pop/s-activity-pop.vue

@@ -0,0 +1,105 @@
+<!-- 商品信息:满减送等营销活动的弹窗 -->
+<template>
+  <su-popup :show="show" type="bottom" round="20" @close="emits('close')" showClose>
+    <view class="model-box">
+      <view class="title ss-m-t-16 ss-m-l-20 ss-flex">营销活动</view>
+      <scroll-view
+        class="model-content ss-m-t-50"
+        scroll-y
+        :scroll-with-animation="false"
+        :enable-back-to-top="true"
+      >
+        <view v-for="item in state.activityInfo" :key="item.id">
+          <view class="ss-flex ss-col-top ss-m-b-40" @tap="onGoodsList(item)">
+            <view class="model-content-tag ss-flex ss-row-center">满减</view>
+            <view class="ss-m-l-20 model-content-title ss-flex-1">
+              <view class="ss-m-b-24" v-for="rule in state.activityMap[item.id]?.rules" :key="rule">
+                {{ formatRewardActivityRule(state.activityMap[item.id], rule) }}
+              </view>
+            </view>
+            <text class="cicon-forward" />
+          </view>
+        </view>
+      </scroll-view>
+    </view>
+  </su-popup>
+</template>
+<script setup>
+  import sheep from '@/sheep';
+  import { computed, reactive, watch } from 'vue';
+  import RewardActivityApi from '@/sheep/api/promotion/rewardActivity';
+  import { formatRewardActivityRule } from '@/sheep/hooks/useGoods';
+
+  const props = defineProps({
+    modelValue: {
+      type: Object,
+      default() {},
+    },
+    show: {
+      type: Boolean,
+      default: false,
+    },
+  });
+  const emits = defineEmits(['close']);
+  const state = reactive({
+    activityInfo: computed(() => props.modelValue),
+    activityMap: {}
+  });
+
+  watch(
+    () => props.show,
+    () => {
+      // 展示的情况下,加载每个活动的详细信息
+      if (props.show) {
+        state.activityInfo?.forEach(activity => {
+          RewardActivityApi.getRewardActivity(activity.id).then(res => {
+            if (res.code !== 0) {
+              return;
+            }
+            state.activityMap[activity.id] = res.data;
+          })
+        });
+      }
+    },
+  );
+
+  function onGoodsList(e) {
+    sheep.$router.go('/pages/activity/index', {
+      activityId: e.id,
+    });
+  }
+</script>
+<style lang="scss" scoped>
+  .model-box {
+    height: 60vh;
+    .title {
+      font-size: 36rpx;
+      height: 80rpx;
+      font-weight: bold;
+      color: #333333;
+    }
+  }
+  .model-content {
+    padding: 0 20rpx;
+    box-sizing: border-box;
+    .model-content-tag {
+      background: rgba(#ff6911, 0.1);
+      font-size: 24rpx;
+      font-weight: 500;
+      color: #ff6911;
+      line-height: 42rpx;
+      width: 68rpx;
+      height: 32rpx;
+      border-radius: 5rpx;
+    }
+    .model-content-title {
+      font-size: 26rpx;
+      font-weight: 500;
+      color: #333333;
+    }
+    .cicon-forward {
+      font-size: 28rpx;
+      color: #999999;
+    }
+  }
+</style>

+ 112 - 0
sheep/components/s-address-item/s-address-item.vue

@@ -0,0 +1,112 @@
+<!-- 地址卡片 -->
+<template>
+  <view
+    class="address-item ss-flex ss-row-between ss-col-center"
+    :class="[{ 'border-bottom': props.hasBorderBottom }]"
+  >
+    <view class="item-left" v-if="!isEmpty(props.item)">
+      <view class="area-text ss-flex ss-col-center">
+        <uni-tag
+          class="ss-m-r-10"
+          size="small"
+          custom-style="background-color: var(--ui-BG-Main); border-color: var(--ui-BG-Main); color: #fff;"
+          v-if="props.item.defaultStatus"
+          text="默认"
+        />
+        {{ props.item.areaName }}
+      </view>
+      <view class="address-text">
+        {{ props.item.detailAddress }}
+      </view>
+      <view class="person-text">
+        {{ props.item.name }} {{ props.item.mobile }}
+      </view>
+    </view>
+    <view v-else>
+      <view class="address-text ss-m-b-10">请选择收货地址</view>
+    </view>
+    <slot>
+      <button class="ss-reset-button edit-btn" @tap.stop="onEdit">
+        <view class="edit-icon ss-flex ss-row-center ss-col-center">
+          <image :src="sheep.$url.static('/static/img/shop/user/address/edit.png')" />
+        </view>
+      </button>
+    </slot>
+  </view>
+</template>
+
+<script setup>
+  /**
+   * 基础组件 - 地址卡片
+   *
+   * @param {String}  icon = _icon-edit    - icon
+   *
+   * @event {Function()} click			 - 点击
+   * @event {Function()} actionClick		 - 点击工具栏
+   *
+   * @slot 								 - 默认插槽
+   */
+  import sheep from '@/sheep';
+  import { isEmpty } from 'lodash';
+  const props = defineProps({
+    item: {
+      type: Object,
+      default() {},
+    },
+    hasBorderBottom: {
+      type: Boolean,
+      defult: true,
+    },
+  });
+
+  const onEdit = () => {
+    sheep.$router.go('/pages/user/address/edit', {
+      id: props.item.id,
+    });
+  };
+</script>
+
+<style lang="scss" scoped>
+  .address-item {
+    padding: 24rpx 30rpx;
+
+    .item-left {
+      width: 600rpx;
+    }
+
+    .area-text {
+      font-size: 26rpx;
+      font-weight: 400;
+      color: $dark-9;
+    }
+
+    .address-text {
+      font-size: 32rpx;
+      font-weight: 500;
+      color: #333333;
+      line-height: 48rpx;
+    }
+
+    .person-text {
+      font-size: 28rpx;
+      font-weight: 400;
+      color: $dark-9;
+    }
+  }
+
+  .edit-btn {
+    width: 44rpx;
+    height: 44rpx;
+    background: $gray-f;
+    border-radius: 50%;
+
+    .edit-icon {
+      width: 24rpx;
+      height: 24rpx;
+    }
+  }
+  image {
+    width: 100%;
+    height: 100%;
+  }
+</style>

+ 107 - 0
sheep/components/s-auth-modal/components/account-login.vue

@@ -0,0 +1,107 @@
+<!-- 账号密码登录 accountLogin  -->
+<template>
+  <view>
+    <!-- 标题栏 -->
+    <view class="head-box ss-m-b-60 ss-flex-col">
+      <view class="ss-flex ss-m-b-20">
+        <view class="head-title-active head-title-line" @tap="showAuthModal('smsLogin')">
+          短信登录
+        </view>
+        <view class="head-title ss-m-r-40 head-title-animation">账号登录</view>
+      </view>
+      <view class="head-subtitle">如果未设置过密码,请点击忘记密码</view>
+    </view>
+
+    <!-- 表单项 -->
+    <uni-forms
+      ref="accountLoginRef"
+      v-model="state.model"
+      :rules="state.rules"
+      validateTrigger="bind"
+      labelWidth="140"
+      labelAlign="center"
+    >
+      <uni-forms-item name="mobile" label="账号">
+        <uni-easyinput placeholder="请输入账号" v-model="state.model.mobile" :inputBorder="false">
+          <template v-slot:right>
+            <button class="ss-reset-button forgot-btn" @tap="showAuthModal('resetPassword')">
+              忘记密码
+            </button>
+          </template>
+        </uni-easyinput>
+      </uni-forms-item>
+
+      <uni-forms-item name="password" label="密码">
+        <uni-easyinput
+          type="password"
+          placeholder="请输入密码"
+          v-model="state.model.password"
+          :inputBorder="false"
+        >
+          <template v-slot:right>
+            <button class="ss-reset-button login-btn-start" @tap="accountLoginSubmit">登录</button>
+          </template>
+        </uni-easyinput>
+      </uni-forms-item>
+    </uni-forms>
+  </view>
+</template>
+
+<script setup>
+  import { ref, reactive, unref } from 'vue';
+  import sheep from '@/sheep';
+  import { mobile, password } from '@/sheep/validate/form';
+  import { showAuthModal, closeAuthModal } from '@/sheep/hooks/useModal';
+  import AuthUtil from '@/sheep/api/member/auth';
+
+  const accountLoginRef = ref(null);
+
+  const emits = defineEmits(['onConfirm']);
+
+  const props = defineProps({
+    agreeStatus: {
+      type: Boolean,
+      default: false,
+    },
+  });
+
+  // 数据
+  const state = reactive({
+    model: {
+      mobile: '', // 账号
+      password: '', // 密码
+    },
+    rules: {
+      mobile,
+      password,
+    },
+  });
+
+  // 账号登录
+  async function accountLoginSubmit() {
+    // 表单验证
+    const validate = await unref(accountLoginRef)
+      .validate()
+      .catch((error) => {
+        console.log('error: ', error);
+      });
+    if (!validate) return;
+
+    // 同意协议
+    if (!props.agreeStatus) {
+      emits('onConfirm', true)
+      sheep.$helper.toast('请勾选同意');
+      return;
+    }
+
+    // 提交数据
+    const { code, data } = await AuthUtil.login(state.model);
+    if (code === 0) {
+      closeAuthModal();
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  @import '../index.scss';
+</style>

+ 127 - 0
sheep/components/s-auth-modal/components/change-mobile.vue

@@ -0,0 +1,127 @@
+<!-- 绑定/更换手机号 changeMobile  -->
+<template>
+  <view>
+    <!-- 标题栏 -->
+    <view class="head-box ss-m-b-60">
+      <view class="head-title ss-m-b-20">
+        {{ userInfo.mobile ? '更换手机号' : '绑定手机号' }}
+      </view>
+      <view class="head-subtitle">为了您的账号安全,请使用本人手机号码</view>
+    </view>
+
+    <!-- 表单项 -->
+    <uni-forms
+      ref="changeMobileRef"
+      v-model="state.model"
+      :rules="state.rules"
+      validateTrigger="bind"
+      labelWidth="140"
+      labelAlign="center"
+    >
+      <uni-forms-item name="mobile" label="手机号">
+        <uni-easyinput
+          placeholder="请输入手机号"
+          v-model="state.model.mobile"
+          :inputBorder="false"
+          type="number"
+        >
+          <template v-slot:right>
+            <button
+              class="ss-reset-button code-btn-start"
+              :disabled="state.isMobileEnd"
+              :class="{ 'code-btn-end': state.isMobileEnd }"
+              @tap="getSmsCode('changeMobile', state.model.mobile)"
+            >
+              {{ getSmsTimer('changeMobile') }}
+            </button>
+          </template>
+        </uni-easyinput>
+      </uni-forms-item>
+
+      <uni-forms-item name="code" label="验证码">
+        <uni-easyinput
+          placeholder="请输入验证码"
+          v-model="state.model.code"
+          :inputBorder="false"
+          type="number"
+          maxlength="4"
+        >
+          <template v-slot:right>
+            <button class="ss-reset-button login-btn-start" @tap="changeMobileSubmit">
+              确认
+            </button>
+          </template>
+        </uni-easyinput>
+      </uni-forms-item>
+    </uni-forms>
+
+    <!-- 微信独有:读取手机号 -->
+    <button
+      v-if="'WechatMiniProgram' === sheep.$platform.name"
+      class="ss-reset-button type-btn"
+      open-type="getPhoneNumber"
+      @getphonenumber="getPhoneNumber"
+    >
+      使用微信手机号
+    </button>
+  </view>
+</template>
+
+<script setup>
+  import { computed, ref, reactive, unref } from 'vue';
+  import sheep from '@/sheep';
+  import { code, mobile } from '@/sheep/validate/form';
+  import { closeAuthModal, getSmsCode, getSmsTimer } from '@/sheep/hooks/useModal';
+  import UserApi from '@/sheep/api/member/user';
+
+  const changeMobileRef = ref(null);
+  const userInfo = computed(() => sheep.$store('user').userInfo);
+
+  // 数据
+  const state = reactive({
+    isMobileEnd: false, // 手机号输入完毕
+    model: {
+      mobile: '', // 手机号
+      code: '', // 验证码
+    },
+    rules: {
+      code,
+      mobile,
+    },
+  });
+
+  // 绑定手机号
+  async function changeMobileSubmit() {
+    const validate = await unref(changeMobileRef)
+      .validate()
+      .catch((error) => {
+        console.log('error: ', error);
+      });
+    if (!validate) {
+      return;
+    }
+    // 提交更新请求
+    const { code } = await UserApi.updateUserMobile(state.model);
+    if (code !== 0) {
+      return;
+    }
+    sheep.$store('user').getInfo();
+    closeAuthModal();
+  }
+
+  // 使用微信手机号
+  async function getPhoneNumber(e) {
+    if (e.detail.errMsg !== 'getPhoneNumber:ok') {
+      return;
+    }
+    const result = await sheep.$platform.useProvider().bindUserPhoneNumber(e.detail);
+    if (result) {
+      sheep.$store('user').getInfo();
+      closeAuthModal();
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  @import '../index.scss';
+</style>

+ 106 - 0
sheep/components/s-auth-modal/components/change-password.vue

@@ -0,0 +1,106 @@
+<!-- 修改密码(登录时)  -->
+<template>
+  <view>
+    <!-- 标题栏 -->
+    <view class="head-box ss-m-b-60">
+      <view class="head-title ss-m-b-20">修改密码</view>
+      <view class="head-subtitle">如密码丢失或未设置,请点击忘记密码重新设置</view>
+    </view>
+
+    <!-- 表单项 -->
+    <uni-forms
+      ref="changePasswordRef"
+      v-model="state.model"
+      :rules="state.rules"
+      validateTrigger="bind"
+      labelWidth="140"
+      labelAlign="center"
+    >
+      <uni-forms-item name="code" label="验证码">
+        <uni-easyinput
+          placeholder="请输入验证码"
+          v-model="state.model.code"
+          type="number"
+          maxlength="4"
+          :inputBorder="false"
+        >
+          <template v-slot:right>
+            <button
+              class="ss-reset-button code-btn code-btn-start"
+              :disabled="state.isMobileEnd"
+              :class="{ 'code-btn-end': state.isMobileEnd }"
+              @tap="getSmsCode('changePassword')"
+            >
+              {{ getSmsTimer('resetPassword') }}
+            </button>
+          </template>
+        </uni-easyinput>
+      </uni-forms-item>
+
+      <uni-forms-item name="reNewPassword" label="密码">
+        <uni-easyinput
+          type="password"
+          placeholder="请输入密码"
+          v-model="state.model.password"
+          :inputBorder="false"
+        >
+          <template v-slot:right>
+            <button class="ss-reset-button login-btn-start" @tap="changePasswordSubmit">
+              确认
+            </button>
+          </template>
+        </uni-easyinput>
+      </uni-forms-item>
+    </uni-forms>
+
+    <button class="ss-reset-button type-btn" @tap="closeAuthModal">
+      取消修改
+    </button>
+  </view>
+</template>
+
+<script setup>
+  import { ref, reactive, unref } from 'vue';
+  import { code, password } from '@/sheep/validate/form';
+  import { closeAuthModal, getSmsCode, getSmsTimer } from '@/sheep/hooks/useModal';
+  import UserApi from '@/sheep/api/member/user';
+
+  const changePasswordRef = ref(null);
+
+  // 数据
+  const state = reactive({
+    model: {
+      mobile: '', // 手机号
+      code: '', // 验证码
+      password: '', // 密码
+    },
+    rules: {
+      code,
+      password,
+    },
+  });
+
+  // 更改密码
+  async function changePasswordSubmit() {
+    // 参数校验
+    const validate = await unref(changePasswordRef)
+      .validate()
+      .catch((error) => {
+        console.log('error: ', error);
+      });
+    if (!validate) {
+      return;
+    }
+    // 发起请求
+    const { code } = await UserApi.updateUserPassword(state.model);
+    if (code !== 0) {
+      return;
+    }
+    // 成功后,只需要关闭弹窗
+    closeAuthModal();
+  }
+</script>
+
+<style lang="scss" scoped>
+  @import '../index.scss';
+</style>

+ 152 - 0
sheep/components/s-auth-modal/components/mp-authorization.vue

@@ -0,0 +1,152 @@
+<!-- 微信授权信息 mpAuthorization  -->
+<template>
+  <view>
+    <!-- 标题栏 -->
+    <view class="head-box ss-m-b-60 ss-flex-col">
+      <view class="ss-flex ss-m-b-20">
+        <view class="head-title ss-m-r-40 head-title-animation">授权信息</view>
+      </view>
+      <view class="head-subtitle">完善您的头像、昵称、手机号</view>
+    </view>
+
+    <!-- 表单项 -->
+    <uni-forms
+      ref="accountLoginRef"
+      v-model="state.model"
+      :rules="state.rules"
+      validateTrigger="bind"
+      labelWidth="140"
+      labelAlign="center"
+    >
+      <!-- 获取头像昵称:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/userProfile.html -->
+      <uni-forms-item name="avatar" label="头像">
+        <button
+          class="ss-reset-button avatar-btn"
+          open-type="chooseAvatar"
+          @chooseavatar="onChooseAvatar"
+        >
+          <image
+            class="avatar-img"
+            :src="sheep.$url.cdn(state.model.avatar)"
+            mode="aspectFill"
+            @tap="sheep.$router.go('/pages/user/info')"
+          />
+          <text class="cicon-forward" />
+        </button>
+      </uni-forms-item>
+      <uni-forms-item name="nickname" label="昵称">
+        <uni-easyinput
+          type="nickname"
+          placeholder="请输入昵称"
+          v-model="state.model.nickname"
+          :inputBorder="false"
+        />
+      </uni-forms-item>
+      <view class="foot-box">
+        <button class="ss-reset-button authorization-btn" @tap="onConfirm"> 确认授权 </button>
+      </view>
+    </uni-forms>
+  </view>
+</template>
+
+<script setup>
+  import { computed, ref, reactive } from 'vue';
+  import sheep from '@/sheep';
+  import { closeAuthModal } from '@/sheep/hooks/useModal';
+  import FileApi from '@/sheep/api/infra/file';
+  import UserApi from '@/sheep/api/member/user';
+
+  const props = defineProps({
+    agreeStatus: {
+      type: Boolean,
+      default: false,
+    },
+  });
+
+  const userInfo = computed(() => sheep.$store('user').userInfo);
+
+  const accountLoginRef = ref(null);
+
+  // 数据
+  const state = reactive({
+    model: {
+      nickname: userInfo.value.nickname,
+      avatar: userInfo.value.avatar,
+    },
+    rules: {},
+    disabledStyle: {
+      color: '#999',
+      disableColor: '#fff',
+    },
+  });
+
+  // 选择头像(来自微信)
+  function onChooseAvatar(e) {
+    const tempUrl = e.detail.avatarUrl || '';
+    uploadAvatar(tempUrl);
+  }
+
+  // 选择头像(来自文件系统)
+  async function uploadAvatar(tempUrl) {
+    if (!tempUrl) {
+      return;
+    }
+    let { data } = await FileApi.uploadFile(tempUrl);
+    state.model.avatar = data;
+  }
+
+  // 确认授权
+  async function onConfirm() {
+    const { model } = state;
+    const { nickname, avatar } = model;
+    if (!nickname) {
+      sheep.$helper.toast('请输入昵称');
+      return;
+    }
+    if (!avatar) {
+      sheep.$helper.toast('请选择头像');
+      return;
+    }
+    // 发起更新
+    const { code } = await UserApi.updateUser({
+      avatar: state.model.avatar,
+      nickname: state.model.nickname,
+    });
+    // 更新成功
+    if (code === 0) {
+      sheep.$helper.toast('授权成功');
+      await sheep.$store('user').getInfo();
+      closeAuthModal();
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  @import '../index.scss';
+
+  .foot-box {
+    width: 100%;
+    display: flex;
+    justify-content: center;
+  }
+  .authorization-btn {
+    width: 686rpx;
+    height: 80rpx;
+    background-color: var(--ui-BG-Main);
+    border-radius: 40rpx;
+    color: #fff;
+  }
+  .avatar-img {
+    width: 72rpx;
+    height: 72rpx;
+    border-radius: 36rpx;
+  }
+  .cicon-forward {
+    font-size: 30rpx;
+    color: #595959;
+  }
+  .avatar-btn {
+    width: 100%;
+    justify-content: space-between;
+  }
+</style>

+ 119 - 0
sheep/components/s-auth-modal/components/reset-password.vue

@@ -0,0 +1,119 @@
+<!-- 重置密码(未登录时)  -->
+<template>
+  <view>
+    <!-- 标题栏 -->
+    <view class="head-box ss-m-b-60">
+      <view class="head-title ss-m-b-20">重置密码</view>
+      <view class="head-subtitle">为了您的账号安全,设置密码前请先进行安全验证</view>
+    </view>
+
+    <!-- 表单项 -->
+    <uni-forms
+      ref="resetPasswordRef"
+      v-model="state.model"
+      :rules="state.rules"
+      validateTrigger="bind"
+      labelWidth="140"
+      labelAlign="center"
+    >
+      <uni-forms-item name="mobile" label="手机号">
+        <uni-easyinput
+          placeholder="请输入手机号"
+          v-model="state.model.mobile"
+          type="number"
+          :inputBorder="false"
+        >
+          <template v-slot:right>
+            <button
+              class="ss-reset-button code-btn code-btn-start"
+              :disabled="state.isMobileEnd"
+              :class="{ 'code-btn-end': state.isMobileEnd }"
+              @tap="getSmsCode('resetPassword', state.model.mobile)"
+            >
+              {{ getSmsTimer('resetPassword') }}
+            </button>
+          </template>
+        </uni-easyinput>
+      </uni-forms-item>
+
+      <uni-forms-item name="code" label="验证码">
+        <uni-easyinput
+          placeholder="请输入验证码"
+          v-model="state.model.code"
+          type="number"
+          maxlength="4"
+          :inputBorder="false"
+        />
+      </uni-forms-item>
+
+      <uni-forms-item name="password" label="密码">
+        <uni-easyinput
+          type="password"
+          placeholder="请输入密码"
+          v-model="state.model.password"
+          :inputBorder="false"
+        >
+          <template v-slot:right>
+            <button class="ss-reset-button login-btn-start" @tap="resetPasswordSubmit">
+              确认
+            </button>
+          </template>
+        </uni-easyinput>
+      </uni-forms-item>
+    </uni-forms>
+
+    <button v-if="!isLogin" class="ss-reset-button type-btn" @tap="showAuthModal('accountLogin')">
+      返回登录
+    </button>
+  </view>
+</template>
+
+<script setup>
+  import { computed, ref, reactive, unref } from 'vue';
+  import sheep from '@/sheep';
+  import { code, mobile, password } from '@/sheep/validate/form';
+  import { showAuthModal, closeAuthModal, getSmsCode, getSmsTimer } from '@/sheep/hooks/useModal';
+  import UserApi from '@/sheep/api/member/user';
+
+  const resetPasswordRef = ref(null);
+  const isLogin = computed(() => sheep.$store('user').isLogin);
+
+  // 数据
+  const state = reactive({
+    isMobileEnd: false, // 手机号输入完毕
+    model: {
+      mobile: '', // 手机号
+      code: '', // 验证码
+      password: '', // 密码
+    },
+    rules: {
+      code,
+      mobile,
+      password,
+    },
+  });
+
+  // 重置密码
+  const resetPasswordSubmit = async () => {
+    // 参数校验
+    const validate = await unref(resetPasswordRef)
+      .validate()
+      .catch((error) => {
+        console.log('error: ', error);
+      });
+    if (!validate) {
+      return;
+    }
+    // 发起请求
+    const { code } = await UserApi.resetUserPassword(state.model);
+    if (code !== 0) {
+      return;
+    }
+    // 成功后,用户重新登录
+    showAuthModal('accountLogin')
+  };
+</script>
+
+<style lang="scss" scoped>
+  @import '../index.scss';
+</style>

+ 119 - 0
sheep/components/s-auth-modal/components/sms-login.vue

@@ -0,0 +1,119 @@
+<!-- 短信登录 - smsLogin  -->
+<template>
+  <view>
+    <!-- 标题栏 -->
+    <view class="head-box ss-m-b-60">
+      <view class="ss-flex ss-m-b-20">
+        <view class="head-title head-title-line head-title-animation">短信登录</view>
+        <view class="head-title-active ss-m-r-40" @tap="showAuthModal('accountLogin')">
+          账号登录
+        </view>
+      </view>
+      <view class="head-subtitle">未注册的手机号,验证后自动注册账号</view>
+    </view>
+
+    <!-- 表单项 -->
+    <uni-forms
+      ref="smsLoginRef"
+      v-model="state.model"
+      :rules="state.rules"
+      validateTrigger="bind"
+      labelWidth="140"
+      labelAlign="center"
+    >
+      <uni-forms-item name="mobile" label="手机号">
+        <uni-easyinput
+          placeholder="请输入手机号"
+          v-model="state.model.mobile"
+          :inputBorder="false"
+          type="number"
+        >
+          <template v-slot:right>
+            <button
+              class="ss-reset-button code-btn code-btn-start"
+              :disabled="state.isMobileEnd"
+              :class="{ 'code-btn-end': state.isMobileEnd }"
+              @tap="getSmsCode('smsLogin', state.model.mobile)"
+            >
+              {{ getSmsTimer('smsLogin') }}
+            </button>
+          </template>
+        </uni-easyinput>
+      </uni-forms-item>
+
+      <uni-forms-item name="code" label="验证码">
+        <uni-easyinput
+          placeholder="请输入验证码"
+          v-model="state.model.code"
+          :inputBorder="false"
+          type="number"
+          maxlength="4"
+        >
+          <template v-slot:right>
+            <button class="ss-reset-button login-btn-start" @tap="smsLoginSubmit"> 登录 </button>
+          </template>
+        </uni-easyinput>
+      </uni-forms-item>
+    </uni-forms>
+  </view>
+</template>
+
+<script setup>
+  import { ref, reactive, unref } from 'vue';
+  import sheep from '@/sheep';
+  import { code, mobile } from '@/sheep/validate/form';
+  import { showAuthModal, closeAuthModal, getSmsCode, getSmsTimer } from '@/sheep/hooks/useModal';
+  import AuthUtil from '@/sheep/api/member/auth';
+
+  const smsLoginRef = ref(null);
+
+  const emits = defineEmits(['onConfirm']);
+
+  const props = defineProps({
+    agreeStatus: {
+      type: Boolean,
+      default: false,
+    },
+  });
+
+  // 数据
+  const state = reactive({
+    isMobileEnd: false, // 手机号输入完毕
+    codeText: '获取验证码',
+    model: {
+      mobile: '', // 手机号
+      code: '', // 验证码
+    },
+    rules: {
+      code,
+      mobile,
+    },
+  });
+
+  // 短信登录
+  async function smsLoginSubmit() {
+    // 参数校验
+    const validate = await unref(smsLoginRef)
+      .validate()
+      .catch((error) => {
+        console.log('error: ', error);
+      });
+    if (!validate) {
+      return;
+    }
+    if (!props.agreeStatus) {
+      emits('onConfirm', true)
+      sheep.$helper.toast('请勾选同意');
+      return;
+    }
+    // 提交数据
+    const { code } = await AuthUtil.smsLogin(state.model);
+    if (code === 0) {
+      closeAuthModal();
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  @import '../index.scss';
+</style>

+ 151 - 0
sheep/components/s-auth-modal/index.scss

@@ -0,0 +1,151 @@
+@keyframes title-animation {
+  0% {
+    font-size: 32rpx;
+  }
+  100% {
+    font-size: 36rpx;
+  }
+}
+
+.login-wrap {
+  padding: 50rpx 34rpx;
+  min-height: 500rpx;
+  background-color: #fff;
+  border-radius: 20rpx 20rpx 0 0;
+}
+
+.head-box {
+  .head-title {
+    min-width: 160rpx;
+    font-size: 36rpx;
+    font-weight: bold;
+    color: #333333;
+    line-height: 36rpx;
+  }
+  .head-title-active {
+    width: 160rpx;
+    font-size: 32rpx;
+    font-weight: 600;
+    color: #999;
+    line-height: 36rpx;
+  }
+  .head-title-animation {
+    animation-name: title-animation;
+    animation-duration: 0.1s;
+    animation-timing-function: ease-out;
+    animation-fill-mode: forwards;
+  }
+  .head-title-line {
+    position: relative;
+    &::before {
+      content: '';
+      width: 1rpx;
+      height: 34rpx;
+      background-color: #e4e7ed;
+      position: absolute;
+      left: -30rpx;
+      top: 50%;
+      transform: translateY(-50%);
+    }
+  }
+  .head-subtitle {
+    font-size: 26rpx;
+    font-weight: 400;
+    color: #afb6c0;
+    text-align: left;
+    display: flex;
+  }
+}
+
+// .code-btn[disabled] {
+// 	background-color: #fff;
+// }
+.code-btn-start {
+  width: 160rpx;
+  height: 56rpx;
+  line-height: normal;
+  border: 2rpx solid var(--ui-BG-Main);
+  border-radius: 28rpx;
+  font-size: 26rpx;
+  font-weight: 400;
+  color: var(--ui-BG-Main);
+  opacity: 1;
+}
+
+.forgot-btn {
+  width: 160rpx;
+  line-height: 56rpx;
+  font-size: 30rpx;
+  font-weight: 500;
+  color: #999;
+}
+
+.login-btn-start {
+  width: 158rpx;
+  height: 56rpx;
+  line-height: normal;
+  background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
+  border-radius: 28rpx;
+  font-size: 26rpx;
+  font-weight: 500;
+  color: #fff;
+}
+
+.type-btn {
+  padding: 20rpx;
+  margin: 40rpx auto;
+  width: 200rpx;
+  font-size: 30rpx;
+  font-weight: 500;
+  color: #999999;
+}
+
+.auto-login-box {
+  width: 100%;
+  .auto-login-btn {
+    width: 68rpx;
+    height: 68rpx;
+    border-radius: 50%;
+    margin: 0 30rpx;
+  }
+  .auto-login-img {
+    width: 68rpx;
+    height: 68rpx;
+    border-radius: 50%;
+  }
+}
+
+.agreement-box {
+  margin: 80rpx auto 0;
+  .protocol-check {
+    transform: scale(0.7);
+  }
+  .agreement-text {
+    font-size: 26rpx;
+    font-weight: 500;
+    color: #999999;
+    .tcp-text {
+      color: var(--ui-BG-Main);
+    }
+  }
+}
+
+// 修改密码
+.editPwd-btn-box {
+  .save-btn {
+    width: 690rpx;
+    line-height: 70rpx;
+    background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
+    border-radius: 35rpx;
+    font-size: 28rpx;
+    font-weight: 500;
+    color: #ffffff;
+  }
+  .forgot-btn {
+    width: 690rpx;
+    line-height: 70rpx;
+    font-size: 28rpx;
+    font-weight: 500;
+    color: #999999;
+  }
+}

+ 239 - 0
sheep/components/s-auth-modal/s-auth-modal.vue

@@ -0,0 +1,239 @@
+<template>
+  <!-- 规格弹窗 -->
+  <su-popup :show="authType !== ''" round="10" :showClose="true" @close="closeAuthModal">
+    <view class="login-wrap">
+      <!-- 1. 账号密码登录 accountLogin -->
+      <account-login
+        v-if="authType === 'accountLogin'"
+        :agreeStatus="state.protocol"
+        @onConfirm="onConfirm"
+      />
+
+      <!-- 2. 短信登录  smsLogin -->
+      <sms-login v-if="authType === 'smsLogin'" :agreeStatus="state.protocol" @onConfirm="onConfirm" />
+
+      <!-- 3. 忘记密码 resetPassword-->
+      <reset-password v-if="authType === 'resetPassword'" />
+
+      <!-- 4. 绑定手机号 changeMobile -->
+      <change-mobile v-if="authType === 'changeMobile'" />
+
+      <!-- 5. 修改密码 changePassword-->
+      <changePassword v-if="authType === 'changePassword'" />
+
+      <!-- 6. 微信小程序授权 -->
+      <mp-authorization v-if="authType === 'mpAuthorization'" />
+
+      <!-- 7. 第三方登录 -->
+      <view
+        v-if="['accountLogin', 'smsLogin'].includes(authType)"
+        class="auto-login-box ss-flex ss-flex-col ss-row-center ss-col-center"
+      >
+        <!-- 7.1 微信小程序的快捷登录 -->
+        <view v-if="sheep.$platform.name === 'WechatMiniProgram'" class="ss-flex register-box">
+          <view class="register-title">还没有账号?</view>
+          <button class="ss-reset-button login-btn" open-type="getPhoneNumber" @getphonenumber="getPhoneNumber">
+            快捷登录
+          </button>
+          <view class="circle" />
+        </view>
+
+        <!-- 7.2 微信的公众号、App、小程序的登录,基于 openid + code -->
+        <button
+          v-if="
+            ['WechatOfficialAccount', 'WechatMiniProgram', 'App'].includes(sheep.$platform.name) &&
+            sheep.$platform.isWechatInstalled
+          "
+          @tap="thirdLogin('wechat')"
+          class="ss-reset-button auto-login-btn"
+        >
+          <image
+            class="auto-login-img"
+            :src="sheep.$url.static('/static/img/shop/platform/wechat.png')"
+          />
+        </button>
+
+        <!-- 7.3 iOS 登录 TODO 芋艿:等后面搞 App 再弄 -->
+        <button
+          v-if="sheep.$platform.os === 'ios' && sheep.$platform.name === 'App'"
+          @tap="thirdLogin('apple')"
+          class="ss-reset-button auto-login-btn"
+        >
+          <image
+            class="auto-login-img"
+            :src="sheep.$url.static('/static/img/shop/platform/apple.png')"
+          />
+        </button>
+      </view>
+
+      <!-- 用户协议的勾选 -->
+      <view
+        v-if="['accountLogin', 'smsLogin'].includes(authType)"
+        class="agreement-box ss-flex ss-row-center"
+        :class="{ shake: currentProtocol }"
+      >
+        <label class="radio ss-flex ss-col-center" @tap="onChange">
+          <radio
+            :checked="state.protocol"
+            color="var(--ui-BG-Main)"
+            style="transform: scale(0.8)"
+            @tap.stop="onChange"
+          />
+          <view class="agreement-text ss-flex ss-col-center ss-m-l-8">
+            我已阅读并遵守
+            <view class="tcp-text" @tap.stop="onProtocol('用户协议')">
+              《用户协议》
+            </view>
+            <view class="agreement-text">与</view>
+            <view class="tcp-text" @tap.stop="onProtocol('隐私协议')">
+              《隐私协议》
+            </view>
+          </view>
+        </label>
+      </view>
+      <view class="safe-box"/>
+    </view>
+  </su-popup>
+</template>
+
+<script setup>
+  import { computed, reactive, ref } from 'vue';
+  import sheep from '@/sheep';
+  import accountLogin from './components/account-login.vue';
+  import smsLogin from './components/sms-login.vue';
+  import resetPassword from './components/reset-password.vue';
+  import changeMobile from './components/change-mobile.vue';
+  import changePassword from './components/change-password.vue';
+  import mpAuthorization from './components/mp-authorization.vue';
+  import { closeAuthModal, showAuthModal } from '@/sheep/hooks/useModal';
+
+  const appInfo = computed(() => sheep.$store('app').info);
+
+  const modalStore = sheep.$store('modal');
+  // 授权弹窗类型
+  const authType = computed(() => modalStore.auth);
+
+  const state = reactive({
+    protocol: false,
+  });
+
+  const currentProtocol = ref(false);
+
+  // 勾选协议
+  function onChange() {
+    state.protocol = !state.protocol;
+  }
+
+  // 查看协议
+  function onProtocol(title) {
+    closeAuthModal();
+    sheep.$router.go('/pages/public/richtext', {
+      title,
+    });
+  }
+
+  // 点击登录 / 注册事件
+  function onConfirm(e) {
+    currentProtocol.value = e;
+    setTimeout(() => {
+      currentProtocol.value = false;
+    }, 1000);
+  }
+
+  // 第三方授权登陆(微信小程序、Apple)
+  const thirdLogin = async (provider) => {
+    if (!state.protocol) {
+      currentProtocol.value = true;
+      setTimeout(() => {
+        currentProtocol.value = false;
+      }, 1000);
+      sheep.$helper.toast('请勾选同意');
+      return;
+    }
+    const loginRes = await sheep.$platform.useProvider(provider).login();
+    if (loginRes) {
+      closeAuthModal();
+      // 触发小程序授权信息弹框
+      // #ifdef MP-WEIXIN
+      showAuthModal('mpAuthorization');
+      // #endif
+    }
+  };
+
+  // 微信小程序的“手机号快速验证”:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/getPhoneNumber.html
+  const getPhoneNumber = async (e) => {
+    if (e.detail.errMsg !== 'getPhoneNumber:ok') {
+      sheep.$helper.toast('快捷登录失败');
+      return;
+    }
+    let result = await sheep.$platform.useProvider().mobileLogin(e.detail);
+    if (result) {
+      closeAuthModal();
+    }
+  };
+</script>
+
+<style lang="scss" scoped>
+  @import './index.scss';
+
+  .shake {
+    animation: shake 0.05s linear 4 alternate;
+  }
+
+  @keyframes shake {
+    from {
+      transform: translateX(-10rpx);
+    }
+    to {
+      transform: translateX(10rpx);
+    }
+  }
+
+  .register-box {
+    position: relative;
+    justify-content: center;
+    .register-btn {
+      color: #999999;
+      font-size: 30rpx;
+      font-weight: 500;
+    }
+    .register-title {
+      color: #999999;
+      font-size: 30rpx;
+      font-weight: 400;
+      margin-right: 24rpx;
+    }
+    .or-title {
+      margin: 0 16rpx;
+      color: #999999;
+      font-size: 30rpx;
+      font-weight: 400;
+    }
+    .login-btn {
+      color: var(--ui-BG-Main);
+      font-size: 30rpx;
+      font-weight: 500;
+    }
+    .circle {
+      position: absolute;
+      right: 0rpx;
+      top: 18rpx;
+      width: 8rpx;
+      height: 8rpx;
+      border-radius: 8rpx;
+      background: var(--ui-BG-Main);
+    }
+  }
+  .safe-box {
+    height: calc(constant(safe-area-inset-bottom) / 5 * 3);
+    height: calc(env(safe-area-inset-bottom) / 5 * 3);
+  }
+
+  .tcp-text {
+    color: var(--ui-BG-Main);
+  }
+
+  .agreement-text {
+    color: $dark-9;
+  }
+</style>

+ 81 - 0
sheep/components/s-block-item/s-block-item.vue

@@ -0,0 +1,81 @@
+<template>
+  <view>
+    <!-- 基础组件:搜索框 -->
+    <s-search-block v-if="type === 'SearchBar'" :data="data" :styles="styles" :navbar="false" />
+    <!-- 基础组件:公告栏 -->
+    <s-notice-block v-if="type === 'NoticeBar'" :data="data" />
+    <!-- 基础组件:菜单导航 -->
+    <s-menu-button v-if="type === 'MenuSwiper'" :data="data" :styles="styles" />
+    <!-- 基础组件:列表导航 -->
+    <s-menu-list v-if="type === 'MenuList'" :data="data" />
+    <!-- 基础组件:宫格导航 -->
+    <s-menu-grid v-if="type === 'MenuGrid'" :data="data" />
+    <!-- 基础组件:弹窗广告 -->
+    <s-popup-image v-if="type === 'Popover'" :data="data" />
+    <!-- 基础组件:悬浮按钮 -->
+    <s-float-menu v-if="type === 'FloatingActionButton'" :data="data" />
+
+    <!-- 图文组件:图片展示 -->
+    <s-image-block v-if="type === 'ImageBar'" :data="data" :styles="styles" />
+    <!-- 图文组件:图片轮播 -->
+    <s-image-banner v-if="type === 'Carousel'" :data="data" :styles="styles" />
+    <!-- 基础组件:标题栏 -->
+    <s-title-block v-if="type === 'TitleBar'" :data="data" :styles="styles" />
+    <!-- 图文组件:广告魔方 -->
+    <s-image-cube v-if="type === 'MagicCube'" :data="data" :styles="styles" />
+    <!-- 图文组件:视频播放 -->
+    <s-video-block v-if="type === 'VideoPlayer'" :data="data" :styles="styles" />
+    <!-- 基础组件:分割线 -->
+    <s-line-block v-if="type === 'Divider'" :data="data" />
+    <!-- 图文组件:热区 -->
+    <s-hotzone-block v-if="type === 'HotZone'" :data="data" :styles="styles" />
+
+    <!-- 商品组件:商品卡片 -->
+    <s-goods-card v-if="type === 'ProductCard'" :data="data" :styles="styles" />
+    <!-- 商品组件:商品栏 -->
+    <s-goods-shelves v-if="type === 'ProductList'" :data="data" :styles="styles" />
+
+    <!-- 营销组件:拼团 -->
+    <s-groupon-block v-if="type === 'PromotionCombination'" :data="data" :styles="styles" />
+    <!-- 营销组件:秒杀 -->
+    <s-seckill-block v-if="type === 'PromotionSeckill'" :data="data" :styles="styles" />
+    <!-- 营销组件:小程序直播(暂时没有这个功能) -->
+    <s-live-block v-if="type === 'MpLive'" :data="data" :styles="styles" />
+    <!-- 营销组件:优惠券 -->
+    <s-coupon-block v-if="type === 'CouponCard'" :data="data" :styles="styles" />
+    <!-- 营销组件:文章 -->
+    <s-richtext-block v-if="type === 'PromotionArticle'" :data="data" :styles="styles" />
+
+    <!-- 用户组件:用户卡片 -->
+    <s-user-card v-if="type === 'UserCard'" />
+    <!-- 用户组件:用户订单 -->
+    <s-order-card v-if="type === 'UserOrder'" :data="data" />
+    <!-- 用户组件:用户资产 -->
+    <s-wallet-card v-if="type === 'UserWallet'" />
+    <!-- 用户组件:用户卡券 -->
+    <s-coupon-card v-if="type === 'UserCoupon'" />
+  </view>
+</template>
+
+<script setup>
+  /**
+   * 装修组件 - 组件集
+   */
+  const props = defineProps({
+    type: {
+      type: String,
+      default: '',
+    },
+    data: {
+      type: Object,
+      default() {},
+    },
+    styles: {
+      type: Object,
+      default() {},
+    },
+  });
+  function onSearch() {}
+</script>
+
+<style></style>

+ 54 - 0
sheep/components/s-block/s-block.vue

@@ -0,0 +1,54 @@
+<!-- 装修组件容器 -->
+<template>
+  <view :style="[elStyles, elBackground]"><slot /></view>
+</template>
+
+<script setup>
+  /**
+   * 容器组件 - 装修组件的样式容器
+   */
+  import { computed, provide, unref } from 'vue';
+  import sheep from '@/sheep';
+
+  const props = defineProps({
+    styles: {
+      type: Object,
+      default() {},
+    },
+  });
+
+  // 组件样式
+
+  const elBackground = computed(() => {
+    if (props.styles) {
+      if (props.styles.bgType === 'color')
+        return { background: props.styles.bgColor };
+      if (props.styles.bgType === 'img')
+        return {
+          background: `url(${sheep.$url.cdn(
+            props.styles.bgImage,
+          )}) no-repeat top center / 100% auto`,
+        };
+    }
+  });
+
+  const elStyles = computed(() => {
+    if (props.styles) {
+      return {
+        marginTop: `${props.styles.marginTop || 0}px`,
+        marginBottom: `${props.styles.marginBottom || 0}px`,
+        marginLeft: `${props.styles.marginLeft || 0}px`,
+        marginRight: `${props.styles.marginRight || 0}px`,
+        paddingTop: `${props.styles.paddingTop || 0}px`,
+        paddingRight: `${props.styles.paddingRight || 0}px`,
+        paddingBottom: `${props.styles.paddingBottom || 0}px`,
+        paddingLeft: `${props.styles.paddingLeft || 0}px`,
+        borderTopLeftRadius: `${props.styles.borderTopLeftRadius || 0}px`,
+        borderTopRightRadius: `${props.styles.borderTopRightRadius || 0}px`,
+        borderBottomRightRadius: `${props.styles.borderBottomRightRadius || 0}px`,
+        borderBottomLeftRadius: `${props.styles.borderBottomLeftRadius || 0}px`,
+        overflow: 'hidden',
+      };
+    }
+  });
+</script>

+ 173 - 0
sheep/components/s-count-down/s-count-down.vue

@@ -0,0 +1,173 @@
+<template>
+	<view class="time" :style="justifyLeft">
+		<text class="" v-if="tipText">{{ tipText }}</text>
+		<text class="styleAll p6" v-if="isDay === true"
+			:style="{background:bgColor.bgColor,color:bgColor.Color}">{{ day }}{{bgColor.isDay?'天':''}}</text>
+		<text class="timeTxt" v-if="dayText"
+			:style="{width:bgColor.timeTxtwidth,color:bgColor.bgColor}">{{ dayText }}</text>
+		<text class="styleAll" :class='isCol?"timeCol":""'
+			:style="{background:bgColor.bgColor,color:bgColor.Color,width:bgColor.width}">{{ hour }}</text>
+		<text class="timeTxt" v-if="hourText" :class='isCol?"whit":""'
+			:style="{width:bgColor.timeTxtwidth,color:bgColor.bgColor}">{{ hourText }}</text>
+		<text class="styleAll" :class='isCol?"timeCol":""'
+			:style="{background:bgColor.bgColor,color:bgColor.Color,width:bgColor.width}">{{ minute }}</text>
+		<text class="timeTxt" v-if="minuteText" :class='isCol?"whit":""'
+			:style="{width:bgColor.timeTxtwidth,color:bgColor.bgColor}">{{ minuteText }}</text>
+		<text class="styleAll" :class='isCol?"timeCol":""'
+			:style="{background:bgColor.bgColor,color:bgColor.Color,width:bgColor.width}">{{ second }}</text>
+		<text class="timeTxt" v-if="secondText">{{ secondText }}</text>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: "countDown",
+		props: {
+			justifyLeft: {
+				type: String,
+				default: ""
+			},
+			//距离开始提示文字
+			tipText: {
+				type: String,
+				default: "倒计时"
+			},
+			dayText: {
+				type: String,
+				default: "天"
+			},
+			hourText: {
+				type: String,
+				default: "时"
+			},
+			minuteText: {
+				type: String,
+				default: "分"
+			},
+			secondText: {
+				type: String,
+				default: "秒"
+			},
+			datatime: {
+				type: Number,
+				default: 0
+			},
+			isDay: {
+				type: Boolean,
+				default: true
+			},
+			isCol: {
+				type: Boolean,
+				default: false
+			},
+			bgColor: {
+				type: Object,
+				default: null
+			}
+		},
+		data: function() {
+			return {
+				day: "00",
+				hour: "00",
+				minute: "00",
+				second: "00"
+			};
+		},
+		created: function() {
+			this.show_time();
+		},
+		mounted: function() {},
+		methods: {
+			show_time: function() {
+				let that = this;
+
+				function runTime() {
+					//时间函数
+					let intDiff = that.datatime - Date.parse(new Date()) / 1000; //获取数据中的时间戳的时间差;
+					let day = 0,
+						hour = 0,
+						minute = 0,
+						second = 0;
+					if (intDiff > 0) {
+						//转换时间
+						if (that.isDay === true) {
+							day = Math.floor(intDiff / (60 * 60 * 24));
+						} else {
+							day = 0;
+						}
+						hour = Math.floor(intDiff / (60 * 60)) - day * 24;
+						minute = Math.floor(intDiff / 60) - day * 24 * 60 - hour * 60;
+						second =
+							Math.floor(intDiff) -
+							day * 24 * 60 * 60 -
+							hour * 60 * 60 -
+							minute * 60;
+						if (hour <= 9) hour = "0" + hour;
+						if (minute <= 9) minute = "0" + minute;
+						if (second <= 9) second = "0" + second;
+						that.day = day;
+						that.hour = hour;
+						that.minute = minute;
+						that.second = second;
+					} else {
+						that.day = "00";
+						that.hour = "00";
+						that.minute = "00";
+						that.second = "00";
+					}
+				}
+				runTime();
+				setInterval(runTime, 1000);
+			}
+		}
+	};
+</script>
+
+<style scoped>
+	.p6 {
+		padding: 0 8rpx;
+	}
+
+	.styleAll {
+		/* color: #fff; */
+		font-size: 24rpx;
+		height: 36rpx;
+		line-height: 36rpx;
+		border-radius: 6rpx;
+		text-align: center;
+		/* padding: 0 6rpx; */
+	}
+
+	.timeTxt {
+		text-align: center;
+		/* width: 16rpx; */
+		height: 36rpx;
+		line-height: 36rpx;
+		display: inline-block;
+	}
+
+	.whit {
+		color: #fff !important;
+	}
+
+	.time {
+		display: flex;
+		justify-content: center;
+	}
+
+	.red {
+		color: #fc4141;
+		margin: 0 4rpx;
+	}
+
+	.timeCol {
+		/* width: 40rpx;
+		height: 40rpx;
+		line-height: 40rpx;
+		text-align:center;
+		border-radius: 6px;
+		background: #fff;
+		font-size: 24rpx; */
+		color: #E93323;
+	}
+</style>

+ 152 - 0
sheep/components/s-coupon-block/s-coupon-block.vue

@@ -0,0 +1,152 @@
+<!-- 装修营销组件:优惠券  -->
+<template>
+  <scroll-view class="scroll-box" scroll-x scroll-anchoring>
+    <view class="coupon-box ss-flex">
+      <view
+        class="coupon-item"
+        :style="[couponBg, { marginLeft: `${data.space}px` }]"
+        v-for="(item, index) in couponList"
+        :key="index"
+      >
+        <su-coupon
+          :size="SIZE_LIST[columns - 1]"
+          :textColor="data.textColor"
+          background=""
+          :couponId="item.id"
+          :title="item.name"
+          :type="formatCouponDiscountType(item)"
+          :value="formatCouponDiscountValue(item)"
+          :sellBy="formatValidityType(item)"
+        >
+          <template v-slot:btn>
+            <!-- 两列时,领取按钮坚排 -->
+            <button
+              v-if="columns === 2"
+              @click.stop="onGetCoupon(item.id)"
+              class="ss-reset-button card-btn vertical"
+              :style="[btnStyles]"
+            >
+              <view class="btn-text">立即领取</view>
+            </button>
+            <button
+              v-else
+              class="ss-reset-button card-btn"
+              :style="[btnStyles]"
+              @click.stop="onGetCoupon(item.id)"
+            >
+              立即领取
+            </button>
+          </template>
+        </su-coupon>
+      </view>
+    </view>
+  </scroll-view>
+</template>
+
+<script setup>
+  import sheep from '@/sheep';
+  import CouponApi from '@/sheep/api/promotion/coupon';
+  import { ref, onMounted } from 'vue';
+  import { CouponTemplateValidityTypeEnum, PromotionDiscountTypeEnum } from "@/sheep/util/const";
+  import { floatToFixed2, formatDate } from "@/sheep/util";
+
+  const props = defineProps({
+    data: {
+      type: Object,
+      default: () => ({}),
+    },
+    styles: {
+      type: Object,
+      default: () => ({}),
+    },
+  });
+  const { columns, button } = props.data;
+  const SIZE_LIST = ['lg', 'md', 'xs']
+  const couponBg = {
+    background: `url(${sheep.$url.cdn(props.data.bgImg)}) no-repeat top center / 100% 100%`,
+  };
+  const btnStyles = {
+    background: button.bgColor,
+    color: button.color,
+  };
+
+  // 格式化【折扣类型】
+  const formatCouponDiscountType = (coupon) => {
+    if(coupon.discountType === PromotionDiscountTypeEnum.PRICE.type) {
+      return 'reduce'
+    }
+    if(coupon.discountType === PromotionDiscountTypeEnum.PERCENT.type) {
+      return 'percent'
+    }
+    return `未知【${coupon.discountType}】`
+  }
+
+  // 格式化【折扣】
+  const formatCouponDiscountValue = (coupon) => {
+    if(coupon.discountType === PromotionDiscountTypeEnum.PRICE.type) {
+      return floatToFixed2(coupon.discountPrice)
+    }
+    if(coupon.discountType === PromotionDiscountTypeEnum.PERCENT.type) {
+      return coupon.discountPercent
+    }
+    return `未知【${coupon.discountType}】`
+  }
+
+  // 格式化【有效期限】
+  const formatValidityType = (row) => {
+    if (row.validityType === CouponTemplateValidityTypeEnum.DATE.type) {
+      return `${formatDate(row.validStartTime)} 至 ${formatDate(row.validEndTime)}`
+    }
+    if (row.validityType === CouponTemplateValidityTypeEnum.TERM.type) {
+      return `领取后第 ${row.fixedStartTerm} - ${row.fixedEndTerm} 天内可用`
+    }
+    return '未知【' + row.validityType + '】'
+  }
+
+  const couponList = ref([]);
+  // 立即领取优惠券
+  async function onGetCoupon(id) {
+    const { error, msg } = await CouponApi.takeCoupon(id);
+    if (error === 0) {
+      uni.showToast({
+        title: msg,
+        icon: 'none',
+      });
+      return
+    }
+    await getCouponTemplateList()
+  }
+  const getCouponTemplateList = async () => {
+    const { data } = await CouponApi.getCouponTemplateListByIds(props.data.couponIds.join(','));
+    couponList.value = data;
+  }
+  onMounted(() => {
+    getCouponTemplateList()
+  });
+</script>
+
+<style lang="scss" scoped>
+  .card-btn {
+    width: 140rpx;
+    height: 50rpx;
+    border-radius: 25rpx;
+    font-size: 24rpx;
+    line-height: 50rpx;
+    &.vertical {
+      width: 50rpx;
+      height: 140rpx;
+      margin: auto 20rpx auto 0;
+
+      .btn-text {
+        font-size: 24rpx;
+        text-align: center;
+        writing-mode: vertical-lr;
+      }
+    }
+  }
+  .coupon-item {
+    &:nth-of-type(1) {
+      margin-left: 0 !important;
+    }
+  }
+</style>

+ 79 - 0
sheep/components/s-coupon-card/s-coupon-card.vue

@@ -0,0 +1,79 @@
+<!-- 装修用户组件:用户卡券 -->
+<template>
+	<view class="ss-coupon-menu-wrap ss-flex ss-col-center">
+		<view class="menu-item ss-flex-col ss-row-center ss-col-center" v-for="item in props.list" :key="item.title"
+			@tap="sheep.$router.go(item.path, { type: item.type })"
+			:class="item.type === 'all' ? 'menu-wallet' : 'ss-flex-1'">
+			<image class="item-icon" :src="sheep.$url.static(item.icon)" mode="aspectFit"></image>
+			<view class="menu-title ss-m-t-28">{{ item.title }}</view>
+		</view>
+	</view>
+</template>
+
+<script setup>
+	/**
+	 * 装修组件 - 优惠券菜单
+	 */
+	import sheep from '@/sheep';
+
+	// 接收参数
+	const props = defineProps({
+		list: {
+			type: Array,
+			default () {
+				return [{
+						title: '已领取',
+						value: '0',
+						icon: '/static/img/shop/order/nouse_coupon.png',
+						path: '/pages/coupon/list',
+						type: 'geted',
+					},
+					{
+						title: '已使用',
+						value: '0',
+						icon: '/static/img/shop/order/useend_coupon.png',
+						path: '/pages/coupon/list',
+						type: 'used',
+					},
+					{
+						title: '已失效',
+						value: '0',
+						icon: '/static/img/shop/order/out_coupon.png',
+						path: '/pages/coupon/list',
+						type: 'expired',
+					},
+					{
+					  title: '领券中心',
+					  value: '0',
+					  icon: '/static/img/shop/order/all_coupon.png',
+					  path: '/pages/coupon/list',
+					  type: 'all',
+					},
+				];
+			},
+		},
+	});
+</script>
+
+<style lang="scss" scoped>
+	.ss-coupon-menu-wrap {
+		.menu-item {
+			height: 160rpx;
+
+			.menu-title {
+				font-size: 24rpx;
+				line-height: 24rpx;
+				color: #333333;
+			}
+
+			.item-icon {
+				width: 44rpx;
+				height: 44rpx;
+			}
+		}
+
+		.menu-wallet {
+			width: 144rpx;
+		}
+	}
+</style>

+ 109 - 0
sheep/components/s-coupon-get/s-coupon-get.vue

@@ -0,0 +1,109 @@
+<!-- 商品详情 - 优惠劵领取 -->
+<template>
+  <su-popup
+    :show="show"
+    type="bottom"
+    round="20"
+    @close="emits('close')"
+    showClose
+    backgroundColor="#f2f2f2"
+  >
+    <view class="model-box">
+      <view class="title ss-m-t-16 ss-m-l-20 ss-flex">优惠券</view>
+      <scroll-view
+        class="model-content"
+        scroll-y
+        :scroll-with-animation="false"
+        :enable-back-to-top="true"
+      >
+        <view class="subtitle ss-m-l-20">可使用优惠券</view>
+        <view v-for="item in state.couponInfo" :key="item.id">
+          <s-coupon-list :data="item">
+            <template #default>
+              <button
+                class="ss-reset-button card-btn ss-flex ss-row-center ss-col-center"
+                :class="!item.canTake ? 'boder-btn' : ''"
+                @click.stop="getBuy(item.id)"
+                :disabled="!item.canTake"
+              >
+                {{ item.canTake ? '立即领取' : '已领取' }}
+              </button>
+            </template>
+          </s-coupon-list>
+        </view>
+      </scroll-view>
+    </view>
+  </su-popup>
+</template>
+<script setup>
+  import { computed, reactive } from 'vue';
+
+  const props = defineProps({
+    modelValue: {
+      type: Object,
+      default() {},
+    },
+    show: {
+      type: Boolean,
+      default: false,
+    },
+  });
+
+  const emits = defineEmits(['get', 'close']);
+
+  const state = reactive({
+    couponInfo: computed(() => props.modelValue)
+  });
+
+  // 领取优惠劵
+  const getBuy = (id) => {
+    emits('get', id);
+  };
+</script>
+<style lang="scss" scoped>
+  .model-box {
+    height: 60vh;
+    .title {
+      font-size: 36rpx;
+      height: 80rpx;
+      font-weight: bold;
+      color: #333333;
+    }
+    .subtitle {
+      font-size: 26rpx;
+      font-weight: 500;
+      color: #333333;
+    }
+  }
+  .model-content {
+    height: 54vh;
+  }
+  .modal-footer {
+    width: 100%;
+    height: 120rpx;
+    background: #fff;
+  }
+  .confirm-btn {
+    width: 710rpx;
+    margin-left: 20rpx;
+    height: 80rpx;
+    background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
+    border-radius: 40rpx;
+    color: #fff;
+  }
+  // 优惠券按钮
+  .card-btn {
+    // width: 144rpx;
+    padding: 0 16rpx;
+    height: 50rpx;
+    border-radius: 40rpx;
+    background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
+    color: #ffffff;
+    font-size: 24rpx;
+    font-weight: 400;
+  }
+  .boder-btn {
+    background: linear-gradient(90deg, var(--ui-BG-Main-opacity-4), var(--ui-BG-Main-light));
+    color: #fff !important;
+  }
+</style>

+ 205 - 0
sheep/components/s-coupon-list/s-coupon-list.vue

@@ -0,0 +1,205 @@
+<template>
+  <view class="ss-m-20" :style="{ opacity: disabled ? '0.5' : '1' }">
+    <view class="content">
+      <view
+        class="tag ss-flex ss-row-center"
+        :class="isDisable ? 'disabled-bg-color' : 'info-bg-color'"
+      >
+        {{ data.discountType === 1 ? '满减券' : '折扣券' }}
+      </view>
+      <view class="title ss-m-x-30 ss-p-t-18">
+        <view class="ss-flex ss-row-between">
+          <view
+            class="value-text ss-flex-1 ss-m-r-10"
+            :class="isDisable ? 'disabled-color' : 'info-color'"
+          >
+            {{ data.name }}
+          </view>
+          <view>
+            <view
+              class="ss-flex ss-col-bottom"
+              :class="isDisable ? 'disabled-color' : 'price-text'"
+            >
+              <view class="value-reduce ss-m-b-10" v-if="data.discountType === 1">¥</view>
+              <view class="value-price">
+                {{
+                  data.discountType === 1
+                    ? fen2yuan(data.discountPrice)
+                    : data.discountPercent / 10.0
+                }}
+              </view>
+              <view class="value-discount ss-m-b-10 ss-m-l-4" v-if="data.discountType === 2"
+                >折</view
+              >
+            </view>
+          </view>
+        </view>
+        <view class="ss-flex ss-row-between ss-m-t-16">
+          <view
+            class="sellby-text"
+            :class=" isDisable ? 'disabled-color' : 'subtitle-color'"
+            v-if="data.validityType === 2"
+          >
+            有效期:领取后 {{ data.fixedEndTerm }} 天内可用
+          </view>
+          <view
+            class="sellby-text"
+            :class=" isDisable ? 'disabled-color' : 'subtitle-color'"
+            v-else
+          >
+            有效期: {{ sheep.$helper.timeFormat(data.validStartTime, 'yyyy-mm-dd') }} 至
+            {{ sheep.$helper.timeFormat(data.validEndTime, 'yyyy-mm-dd') }}
+          </view>
+          <view
+            class="value-enough"
+            :class="isDisable ? 'disabled-color' : 'subtitle-color'"
+          >
+            满 {{ fen2yuan(data.usePrice) }} 可用
+          </view>
+        </view>
+      </view>
+    </view>
+
+    <!-- TODO 芋艿:可优化,增加优惠劵的描述 -->
+    <view class="desc ss-flex ss-row-between">
+      <view>
+        <view class="desc-title">{{ data.description }}</view>
+        <view>
+          <slot name="reason" />
+        </view>
+      </view>
+      <view>
+        <slot />
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup>
+  import { computed, reactive } from 'vue';
+  import { fen2yuan } from '../../hooks/useGoods';
+  import sheep from '../../index';
+
+  const state = reactive({});
+
+  const isDisable = computed(() => {
+    if (props.type === 'coupon') {
+      return false;
+    }
+    return props.data.status !== 1;
+  });
+
+  // 接受参数
+  const props = defineProps({
+    data: {
+      type: Object,
+      default: {},
+    },
+    disabled: {
+      type: Boolean,
+      default: false,
+    },
+    type: {
+      type: String,
+      default: 'coupon', // coupon 优惠劵模版;user 用户优惠劵
+    },
+  });
+</script>
+
+<style lang="scss" scoped>
+  .info-bg-color {
+    background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
+  }
+
+  .disabled-bg-color {
+    background: #999;
+  }
+
+  .info-color {
+    color: #333;
+  }
+
+  .subtitle-color {
+    color: #666;
+  }
+
+  .disabled-color {
+    color: #999;
+  }
+
+  .content {
+    width: 100%;
+    background: #fff;
+    border-radius: 20rpx 20rpx 0 0;
+    -webkit-mask: radial-gradient(circle at 12rpx 100%, #0000 12rpx, red 0) -12rpx;
+    box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.04);
+
+    .tag {
+      width: 100rpx;
+
+      color: #fff;
+      height: 40rpx;
+      font-size: 24rpx;
+      border-radius: 20rpx 0 20rpx 0;
+    }
+
+    .title {
+      padding-bottom: 22rpx;
+      border-bottom: 2rpx dashed #d3d3d3;
+
+      .value-text {
+        font-size: 32rpx;
+        font-weight: 600;
+      }
+
+      .sellby-text {
+        font-size: 24rpx;
+        font-weight: 400;
+      }
+
+      .value-price {
+        font-size: 64rpx;
+        font-weight: 500;
+        line-height: normal;
+        font-family: OPPOSANS;
+      }
+
+      .value-reduce {
+        line-height: normal;
+        font-size: 32rpx;
+      }
+
+      .value-discount {
+        line-height: normal;
+        font-size: 28rpx;
+      }
+
+      .value-enough {
+        font-size: 24rpx;
+        font-weight: 400;
+        font-family: OPPOSANS;
+      }
+    }
+  }
+
+  .desc {
+    width: 100%;
+    background: #fff;
+    -webkit-mask: radial-gradient(circle at 12rpx 0%, #0000 12rpx, red 0) -12rpx;
+    box-shadow: rgba(#000, 0.1);
+    box-sizing: border-box;
+    padding: 24rpx 30rpx;
+    box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.04);
+    border-radius: 0 0 20rpx 20rpx;
+
+    .desc-title {
+      font-size: 24rpx;
+      color: #999;
+      font-weight: 400;
+    }
+  }
+
+  .price-text {
+    color: #ff0000;
+  }
+</style>

+ 138 - 0
sheep/components/s-coupon-select/s-coupon-select.vue

@@ -0,0 +1,138 @@
+<!-- 订单确认的优惠劵选择弹窗 -->
+<template>
+  <su-popup
+    :show="show"
+    type="bottom"
+    round="20"
+    @close="emits('close')"
+    showClose
+    backgroundColor="#f2f2f2"
+  >
+    <view class="model-box">
+      <view class="title ss-m-t-16 ss-m-l-20 ss-flex">优惠券</view>
+      <scroll-view
+        class="model-content"
+        scroll-y
+        :scroll-with-animation="false"
+        :enable-back-to-top="true"
+      >
+        <view class="subtitle ss-m-l-20">可使用优惠券</view>
+        <view v-for="(item, index) in state.couponInfo" :key="index">
+          <s-coupon-list :data="item" type="user" :disabled="false">
+            <template #default>
+              <label class="ss-flex ss-col-center" @tap="radioChange(item.id)">
+                <radio
+                  color="var(--ui-BG-Main)"
+                  style="transform: scale(0.8)"
+                  :checked="state.couponId === item.id"
+                  @tap.stop="radioChange(item.id)"
+                />
+              </label>
+            </template>
+          </s-coupon-list>
+        </view>
+        <!-- TODO 芋艿:未来接口需要支持下
+        <view class="subtitle ss-m-t-40 ss-m-l-20">不可使用优惠券</view>
+        <view v-for="item in state.couponInfo.cannot_use" :key="item.id">
+          <s-coupon-list :data="item" type="user" :disabled="true">
+            <template v-slot:reason>
+              <view class="ss-flex ss-m-t-24">
+                <view class="reason-title"> 不可用原因:</view>
+                <view class="reason-desc">{{ item.cannot_use_msg }}</view>
+              </view>
+            </template>
+          </s-coupon-list>
+        </view>
+      -->
+      </scroll-view>
+    </view>
+    <view class="modal-footer ss-flex">
+      <button class="confirm-btn ss-reset-button" @tap="onConfirm">确认</button>
+    </view>
+  </su-popup>
+</template>
+<script setup>
+  import { computed, reactive } from 'vue';
+
+  const props = defineProps({
+    modelValue: { // 优惠劵列表
+      type: Object,
+      default() {},
+    },
+    show: {
+      type: Boolean,
+      default: false,
+    },
+  });
+
+  const emits = defineEmits(['confirm', 'close']);
+
+  const state = reactive({
+    couponInfo: computed(() => props.modelValue), // 优惠劵列表
+    couponId: 0, // 选中的优惠劵编号
+  });
+
+  // 选中优惠劵
+  function radioChange(couponId) {
+    if (state.couponId === couponId) {
+      state.couponId = 0;
+    } else {
+      state.couponId = couponId;
+    }
+  }
+
+  // 确认优惠劵
+  const onConfirm = () => {
+    emits('confirm', state.couponId);
+  }
+</script>
+<style lang="scss" scoped>
+  :deep() {
+    .uni-checkbox-input {
+      background-color: var(--ui-BG-Main);
+    }
+  }
+
+  .model-box {
+    height: 60vh;
+  }
+  .title {
+    font-size: 36rpx;
+    height: 80rpx;
+    font-weight: bold;
+    color: #333333;
+  }
+  .subtitle {
+    font-size: 26rpx;
+    font-weight: 500;
+    color: #333333;
+  }
+  .model-content {
+    height: 54vh;
+  }
+  .modal-footer {
+    width: 100%;
+    height: 120rpx;
+    background: #fff;
+  }
+  .confirm-btn {
+    width: 710rpx;
+    margin-left: 20rpx;
+    height: 80rpx;
+    background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
+    border-radius: 40rpx;
+    color: #fff;
+  }
+  .reason-title {
+    font-weight: 600;
+    font-size: 20rpx;
+    line-height: 26rpx;
+    color: #ff0003;
+  }
+  .reason-desc {
+    font-weight: 600;
+    font-size: 20rpx;
+    line-height: 26rpx;
+    color: #434343;
+  }
+</style>

+ 66 - 0
sheep/components/s-custom-navbar/components/navbar-item.vue

@@ -0,0 +1,66 @@
+<!-- 顶部导航栏 - 单元格 -->
+<template>
+  <view class="ss-flex ss-col-center">
+    <!-- 类型一: 文字 -->
+    <view
+      v-if="data.type === 'text'"
+      class="nav-title inline"
+      :style="[{ color: data.textColor, width: width }]"
+    >
+      {{ data.text }}
+    </view>
+    <!-- 类型二: 图片 -->
+    <view
+      v-if="data.type === 'image'"
+      :style="[{ width: width }]"
+      class="menu-icon-wrap ss-flex ss-row-center ss-col-center"
+      @tap="sheep.$router.go(data.url)"
+    >
+      <image class="nav-image" :src="sheep.$url.cdn(data.imgUrl)" mode="aspectFit"></image>
+    </view>
+    <!-- 类型三: 搜索框 -->
+    <view class="ss-flex-1" v-if="data.type === 'search'" :style="[{ width: width }]">
+      <s-search-block
+        :placeholder="data.placeholder || '搜索关键字'"
+        :radius="data.borderRadius"
+        elBackground="#fff"
+        :height="height"
+        :width="width"
+        @click="sheep.$router.go('/pages/index/search')"
+      ></s-search-block>
+    </view>
+  </view>
+</template>
+
+<script setup>
+  import sheep from '@/sheep';
+  import { computed } from 'vue';
+
+  // 接收参数
+  const props = defineProps({
+    data: {
+      type: Object,
+      default: () => ({}),
+    },
+    width: {
+      type: String,
+      default: '1px',
+    },
+  });
+
+  const height = computed(() => sheep.$platform.capsule.height);
+</script>
+
+<style lang="scss" scoped>
+  .nav-title {
+    font-size: 36rpx;
+    color: #333;
+    text-align: center;
+  }
+
+  .menu-icon-wrap {
+    .nav-image {
+      height: 24px;
+    }
+  }
+</style>

+ 314 - 0
sheep/components/s-custom-navbar/components/navbar.vue

@@ -0,0 +1,314 @@
+<template>
+  <su-fixed
+    :noFixed="props.noFixed"
+    :alway="props.alway"
+    :bgStyles="props.bgStyles"
+    :val="0"
+    :index="props.zIndex"
+    noNav
+    :bg="props.bg"
+    :ui="props.ui"
+    :opacity="props.opacity"
+    :placeholder="props.placeholder"
+    :sticky="props.sticky"
+  >
+    <su-status-bar />
+    <!-- 
+      :class="[{ 'border-bottom': !props.opacity && props.bg != 'bg-none' }]"
+     -->
+    <view class="ui-navbar-box">
+      <view
+        class="ui-bar"
+        :class="
+          props.status == '' ? `text-a` : props.status == 'light' ? 'text-white' : 'text-black'
+        "
+        :style="[{ height: sys_navBar - sys_statusBar + 'px' }]"
+      >
+        <slot name="item"></slot>
+        <view class="right">
+          <!-- #ifdef MP -->
+          <view :style="[state.capsuleStyle]"></view>
+          <!-- #endif -->
+        </view>
+      </view>
+    </view>
+  </su-fixed>
+</template>
+
+<script setup>
+  /**
+   * 标题栏 - 基础组件navbar
+   *
+   * @param {Number}  zIndex = 100  							- 层级
+   * @param {Boolean}  back = true 							- 是否返回上一页
+   * @param {String}  backtext = ''  							- 返回文本
+   * @param {String}  bg = 'bg-white'  						- 公共Class
+   * @param {String}  status = ''  							- 状态栏颜色
+   * @param {Boolean}  alway = true							- 是否常驻
+   * @param {Boolean}  opacity = false  						- 是否开启透明渐变
+   * @param {Boolean}  opacityBg = false  					- 开启滑动渐变后,返回按钮是否添加背景
+   * @param {Boolean}  noFixed = false  						- 是否浮动
+   * @param {String}  ui = ''									- 公共Class
+   * @param {Boolean}  capsule = false  						- 是否开启胶囊返回
+   * @param {Boolean}  stopBack = false 					    - 是否禁用返回
+   * @param {Boolean}  placeholder = true 					- 是否开启占位
+   * @param {Object}   bgStyles = {} 					    	- 背景样式
+   *
+   */
+
+  import { computed, reactive, onBeforeMount } from 'vue';
+  import sheep from '@/sheep';
+
+  // 本地数据
+  const state = reactive({
+    statusCur: '',
+    capsuleStyle: {},
+    capsuleBack: {},
+  });
+
+  const sys_statusBar = sheep.$platform.device.statusBarHeight;
+  const sys_navBar = sheep.$platform.navbar;
+
+  const props = defineProps({
+    sticky: Boolean,
+    zIndex: {
+      type: Number,
+      default: 100,
+    },
+    back: {
+      //是否返回上一页
+      type: Boolean,
+      default: true,
+    },
+    backtext: {
+      //返回文本
+      type: String,
+      default: '',
+    },
+    bg: {
+      type: String,
+      default: 'bg-white',
+    },
+    status: {
+      //状态栏颜色 可以选择light dark/其他字符串视为黑色
+      type: String,
+      default: '',
+    },
+    // 常驻
+    alway: {
+      type: Boolean,
+      default: true,
+    },
+    opacity: {
+      //是否开启滑动渐变
+      type: Boolean,
+      default: false,
+    },
+    opacityBg: {
+      //开启滑动渐变后 返回按钮是否添加背景
+      type: Boolean,
+      default: false,
+    },
+    noFixed: {
+      //是否浮动
+      type: Boolean,
+      default: false,
+    },
+    ui: {
+      type: String,
+      default: '',
+    },
+    capsule: {
+      //是否开启胶囊返回
+      type: Boolean,
+      default: false,
+    },
+    stopBack: {
+      type: Boolean,
+      default: false,
+    },
+    placeholder: {
+      type: [Boolean],
+      default: true,
+    },
+    bgStyles: {
+      type: Object,
+      default() {},
+    },
+  });
+
+  const emits = defineEmits(['navback']);
+
+  onBeforeMount(() => {
+    init();
+  });
+
+  // 返回
+  const onNavback = () => {
+    sheep.$router.back();
+  };
+
+  // 初始化
+  const init = () => {
+    state.capsuleStyle = {
+      width: sheep.$platform.capsule.width + 'px',
+      height: sheep.$platform.capsule.height + 'px',
+      margin: '0 ' + (sheep.$platform.device.windowWidth - sheep.$platform.capsule.right) + 'px',
+    };
+
+    state.capsuleBack = state.capsuleStyle;
+  };
+</script>
+
+<style lang="scss" scoped>
+  .ui-navbar-box {
+    background-color: transparent;
+    width: 100%;
+
+    .ui-bar {
+      position: relative;
+      z-index: 2;
+      white-space: nowrap;
+      display: flex;
+      position: relative;
+      align-items: center;
+      justify-content: space-between;
+
+      .left {
+        @include flex-bar;
+
+        .back {
+          @include flex-bar;
+
+          .back-icon {
+            @include flex-center;
+            width: 56rpx;
+            height: 56rpx;
+            margin: 0 10rpx;
+            font-size: 46rpx !important;
+
+            &.opacityIcon {
+              position: relative;
+              border-radius: 50%;
+              background-color: rgba(127, 127, 127, 0.5);
+
+              &::after {
+                content: '';
+                display: block;
+                position: absolute;
+                height: 200%;
+                width: 200%;
+                left: 0;
+                top: 0;
+                border-radius: inherit;
+                transform: scale(0.5);
+                transform-origin: 0 0;
+                opacity: 0.1;
+                border: 1px solid currentColor;
+                pointer-events: none;
+              }
+
+              &::before {
+                transform: scale(0.9);
+              }
+            }
+          }
+
+          /* #ifdef  MP-ALIPAY */
+          ._icon-back {
+            opacity: 0;
+          }
+
+          /* #endif */
+        }
+
+        .capsule {
+          @include flex-bar;
+          border-radius: 100px;
+          position: relative;
+
+          &.dark {
+            background-color: rgba(255, 255, 255, 0.5);
+          }
+
+          &.light {
+            background-color: rgba(0, 0, 0, 0.15);
+          }
+
+          &::after {
+            content: '';
+            display: block;
+            position: absolute;
+            height: 60%;
+            width: 1px;
+            left: 50%;
+            top: 20%;
+            background-color: currentColor;
+            opacity: 0.1;
+            pointer-events: none;
+          }
+
+          &::before {
+            content: '';
+            display: block;
+            position: absolute;
+            height: 200%;
+            width: 200%;
+            left: 0;
+            top: 0;
+            border-radius: inherit;
+            transform: scale(0.5);
+            transform-origin: 0 0;
+            opacity: 0.1;
+            border: 1px solid currentColor;
+            pointer-events: none;
+          }
+
+          .capsule-back,
+          .capsule-home {
+            @include flex-center;
+            flex: 1;
+          }
+
+          &.isFristPage {
+            .capsule-back,
+            &::after {
+              display: none;
+            }
+          }
+        }
+      }
+
+      .right {
+        @include flex-bar;
+
+        .right-content {
+          @include flex;
+          flex-direction: row-reverse;
+        }
+      }
+
+      .center {
+        @include flex-center;
+        text-overflow: ellipsis;
+        text-align: center;
+        flex: 1;
+
+        .image {
+          display: block;
+          height: 36px;
+          max-width: calc(100vw - 200px);
+        }
+      }
+    }
+
+    .ui-bar-bg {
+      position: absolute;
+      width: 100%;
+      height: 100%;
+      top: 0;
+      z-index: 1;
+      pointer-events: none;
+    }
+  }
+</style>

+ 196 - 0
sheep/components/s-custom-navbar/s-custom-navbar.vue

@@ -0,0 +1,196 @@
+<!-- 顶部导航栏 -->
+<template>
+  <navbar
+    :alway="isAlways"
+    :back="false"
+    bg=""
+    :placeholder="isPlaceholder"
+    :bgStyles="bgStyles"
+    :opacity="isOpacity"
+    :sticky="sticky"
+  >
+    <template #item>
+      <view class="nav-box">
+        <view class="nav-icon" v-if="showLeftButton">
+          <view class="icon-box ss-flex" :class="{ 'inner-icon-box': data.styleType === 'inner' }">
+            <view class="icon-button icon-button-left ss-flex ss-row-center" @tap="onClickLeft">
+              <text class="sicon-back" v-if="hasHistory" />
+              <text class="sicon-home" v-else />
+            </view>
+            <view class="line"></view>
+            <view class="icon-button icon-button-right ss-flex ss-row-center" @tap="onClickRight">
+              <text class="sicon-more" />
+            </view>
+          </view>
+        </view>
+        <view
+          class="nav-item"
+          v-for="(item, index) in navList"
+          :key="index"
+          :style="[parseImgStyle(item)]"
+          :class="[{ 'ss-flex ss-col-center ss-row-center': item.type !== 'search' }]"
+        >
+          <navbar-item :data="item" :width="parseImgStyle(item).width" />
+        </view>
+      </view>
+    </template>
+  </navbar>
+</template>
+
+<script setup>
+  /**
+   *  装修组件 - 自定义标题栏
+   *
+   *
+   * @property {Number | String}  alwaysShow = [0,1]			    - 是否常驻
+   * @property {Number | String}  styleType = [inner]			   	- 是否沉浸式
+   * @property {String | Number} type 		 					- 标题背景模式
+   * @property {String} color 		 							- 页面背景色
+   * @property {String} src 		 								- 页面背景图片
+   */
+  import { computed, unref } from 'vue';
+  import sheep from '@/sheep';
+  import Navbar from './components/navbar.vue';
+  import NavbarItem from './components/navbar-item.vue';
+  import { showMenuTools } from '@/sheep/hooks/useModal';
+
+  const props = defineProps({
+    data: {
+      type: Object,
+      default: () => ({}),
+    },
+    showLeftButton: {
+      type: Boolean,
+      default: false,
+    },
+  });
+  const hasHistory = sheep.$router.hasHistory();
+  const sticky = computed(() => {
+    if (props.data.styleType === 'inner') {
+      if (props.data.alwaysShow) {
+        return false;
+      }
+    }
+    if (props.data.styleType === 'normal') {
+      return false;
+    }
+  });
+  const navList = computed(() => {
+    // #ifdef MP
+    return props.data.mapCells || [];
+    // #endif
+    return props.data.otherCells || [];
+  });
+  // 页面宽度
+  const windowWidth = sheep.$platform.device.windowWidth;
+  // 单元格宽度
+  const cell = computed(() => {
+    if (unref(navList).length) {
+      // 默认宽度为8个格子,微信公众号右上角有胶囊按钮所以是6个格子
+      let cell = (windowWidth - 90) / 8;
+      // #ifdef MP
+      cell = (windowWidth - 80 - unref(sheep.$platform.capsule).width) / 6;
+      // #endif
+      return cell;
+    }
+  });
+  // 解析位置
+  const parseImgStyle = (item) => {
+    let obj = {
+      width: item.width * cell.value + (item.width - 1) * 10 + 'px',
+      left: item.left * cell.value + (item.left + 1) * 10 + 'px',
+      'border-radius': item.borderRadius + 'px',
+    };
+    return obj;
+  };
+  const isAlways = computed(() =>
+    props.data.styleType === 'inner' ? Boolean(props.data.alwaysShow) : true,
+  );
+  const isOpacity = computed(() =>
+    props.data.styleType === 'normal'
+      ? false
+      : props.showLeftButton
+      ? false
+      : props.data.styleType === 'inner',
+  );
+  const isPlaceholder = computed(() => props.data.styleType === 'normal');
+  const bgStyles = computed(() => {
+    return {
+      background:
+          props.data.bgType === 'img' && props.data.bgImg
+          ? `url(${sheep.$url.cdn(props.data.bgImg)}) no-repeat top center / 100% 100%`
+          : props.data.bgColor
+    };
+  });
+  // 左侧按钮:返回上一页或首页
+  function onClickLeft() {
+    if (hasHistory) {
+      sheep.$router.back();
+    } else {
+      sheep.$router.go('/pages/index/index');
+    }
+  }
+  // 右侧按钮:打开快捷菜单
+  function onClickRight() {
+    showMenuTools();
+  }
+</script>
+
+<style lang="scss" scoped>
+  .nav-box {
+    width: 750rpx;
+    position: relative;
+    height: 100%;
+
+    .nav-item {
+      position: absolute;
+      top: 50%;
+      transform: translateY(-50%);
+    }
+    .nav-icon {
+      position: absolute;
+      top: 50%;
+      transform: translateY(-50%);
+      left: 20rpx;
+      .inner-icon-box {
+        border: 1px solid rgba(#fff, 0.4);
+        background: none !important;
+      }
+      .icon-box {
+        background: #ffffff;
+        box-shadow: 0px 0px 4rpx rgba(51, 51, 51, 0.08),
+          0px 4rpx 6rpx 2rpx rgba(102, 102, 102, 0.12);
+        border-radius: 30rpx;
+        width: 134rpx;
+        height: 56rpx;
+        margin-left: 8rpx;
+        .line {
+          width: 2rpx;
+          height: 24rpx;
+          background: #e5e5e7;
+        }
+        .sicon-back {
+          font-size: 32rpx;
+        }
+        .sicon-home {
+          font-size: 32rpx;
+        }
+        .sicon-more {
+          font-size: 32rpx;
+        }
+        .icon-button {
+          width: 67rpx;
+          height: 56rpx;
+          &-left:hover {
+            background: rgba(0, 0, 0, 0.16);
+            border-radius: 30rpx 0px 0px 30rpx;
+          }
+          &-right:hover {
+            background: rgba(0, 0, 0, 0.16);
+            border-radius: 0px 30rpx 30rpx 0px;
+          }
+        }
+      }
+    }
+  }
+</style>

+ 114 - 0
sheep/components/s-discount-list/s-discount-list.vue

@@ -0,0 +1,114 @@
+<template>
+  <su-popup
+    :show="show"
+    type="bottom"
+    round="20"
+    @close="emits('close')"
+    showClose
+    backgroundColor="#f2f2f2"
+  >
+    <view class="model-box">
+      <view class="title ss-m-t-38 ss-m-l-20 ss-m-b-40">活动优惠</view>
+      <scroll-view
+        class="model-content ss-m-l-20"
+        scroll-y
+        :scroll-with-animation="false"
+        :enable-back-to-top="true"
+      >
+        <view v-for="(item, index) in state.orderInfo.promo_infos" :key="index">
+          <view class="ss-flex ss-m-b-40 subtitle">
+            <view>共{{ item.goods_ids.length }}件,</view>
+            <view v-if="item.activity_type === 'full_discount'">
+              满{{ item.discount_rule.full }}打{{ item.discount_rule.discount }}折,已减
+            </view>
+            <view v-if="item.activity_type === 'full_gift'">满赠</view>
+            <view v-if="item.activity_type === 'full_reduce'">
+              满{{ item.discount_rule.full }}减{{ item.discount_rule.discount }},已减
+            </view>
+            <view class="price-text">¥{{ item.promo_discount_money || '0.00' }}</view>
+          </view>
+          <scroll-view class="scroll-box" scroll-x scroll-anchoring>
+            <view class="ss-flex">
+              <view v-for="i in item.goods_ids" :key="i">
+                <image class="content-img" :src="sheep.$url.cdn(getGoodsImg(i))" />
+              </view>
+            </view>
+          </scroll-view>
+        </view>
+      </scroll-view>
+    </view>
+    <view class="modal-footer ss-flex">
+      <button class="confirm-btn ss-reset-button" @tap="emits('close')">确认</button>
+    </view>
+  </su-popup>
+</template>
+<script setup>
+  import { computed, reactive } from 'vue';
+  import sheep from '@/sheep';
+  const props = defineProps({
+    promoInfo: {
+      type: Array,
+      default: () => [],
+    },
+    goodsList: {
+      type: Array,
+      default: () => [],
+    },
+    modelValue: {
+      type: Object,
+      default() {},
+    },
+    show: {
+      type: Boolean,
+      default: false,
+    },
+  });
+  const emits = defineEmits(['close']);
+  const state = reactive({
+    orderInfo: computed(() => props.modelValue),
+  });
+  const getGoodsImg = (e) => {
+    let goodsImg = '';
+    state.orderInfo.goods_list.forEach((i) => {
+      if (e == i.goods_id) {
+        goodsImg = i.goods.image;
+      }
+    });
+    return goodsImg;
+  };
+</script>
+<style lang="scss" scoped>
+  .model-box {
+    height: 60vh;
+  }
+  .model-content {
+    height: 54vh;
+  }
+  .modal-footer {
+    width: 100%;
+    height: 120rpx;
+    background: #fff;
+  }
+  .confirm-btn {
+    width: 710rpx;
+    margin-left: 20rpx;
+    height: 80rpx;
+    background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
+    border-radius: 40rpx;
+    color: #fff;
+  }
+  .content-img {
+    width: 140rpx;
+    height: 140rpx;
+    margin-right: 20rpx;
+    margin-bottom: 20rpx;
+  }
+  .subtitle {
+    font-size: 28rpx;
+    font-weight: 500;
+    color: #333333;
+  }
+  .price-text {
+    color: #ff3000;
+  }
+</style>

+ 93 - 0
sheep/components/s-empty/s-empty.vue

@@ -0,0 +1,93 @@
+<template>
+  <view
+    class="ss-flex-col ss-col-center ss-row-center empty-box"
+    :style="[{ paddingTop: paddingTop + 'rpx' }]"
+  >
+    <view class=""><image class="empty-icon" :src="icon" mode="widthFix"></image></view>
+    <view class="empty-text ss-m-t-28 ss-m-b-40">
+      <text v-if="text !== ''">{{ text }}</text>
+    </view>
+    <button class="ss-reset-button empty-btn" v-if="showAction" @tap="clickAction">
+      {{ actionText }}
+    </button>
+  </view>
+</template>
+
+<script setup>
+  import sheep from '@/sheep';
+  /**
+   * 容器组件 - 装修组件的样式容器
+   */
+
+  const props = defineProps({
+    // 图标
+    icon: {
+      type: String,
+      default: '',
+    },
+    // 描述
+    text: {
+      type: String,
+      default: '',
+    },
+    // 是否显示button
+    showAction: {
+      type: Boolean,
+      default: false,
+    },
+    // button 文字
+    actionText: {
+      type: String,
+      default: '',
+    },
+    // 链接
+    actionUrl: {
+      type: String,
+      default: '',
+    },
+    // 间距
+    paddingTop: {
+      type: String,
+      default: '260',
+    },
+    //主题色
+    buttonColor: {
+      type: String,
+      default: 'var(--ui-BG-Main)',
+    },
+  });
+
+  const emits = defineEmits(['clickAction']);
+
+  function clickAction() {
+    if (props.actionUrl !== '') {
+      sheep.$router.go(props.actionUrl);
+    }
+    emits('clickAction');
+  }
+</script>
+
+<style lang="scss" scoped>
+  .empty-box {
+    width: 100%;
+  }
+  .empty-icon {
+    width: 240rpx;
+  }
+
+  .empty-text {
+    font-size: 26rpx;
+    font-weight: 500;
+    color: #999999;
+  }
+
+  .empty-btn {
+    width: 320rpx;
+    height: 70rpx;
+    border: 2rpx solid v-bind('buttonColor');
+    border-radius: 35rpx;
+    font-weight: 500;
+    color: v-bind('buttonColor');
+    font-size: 28rpx;
+  }
+</style>

+ 88 - 0
sheep/components/s-float-menu/s-float-menu.vue

@@ -0,0 +1,88 @@
+<!-- 装修基础组件:悬浮按钮 -->
+<template>
+  <!-- 模态背景:展开时显示,点击后折叠 -->
+  <view class="modal-bg" v-if="fabRef?.isShow" @click="handleCollapseFab"></view>
+  <!-- 悬浮按钮 -->
+  <uni-fab
+    ref="fabRef"
+    horizontal="right"
+    vertical="bottom"
+    :direction="state.direction"
+    :pattern="state.pattern"
+    :content="state.content"
+    @trigger="handleOpenLink"
+  />
+</template>
+<script setup>
+  /**
+   * 悬浮按钮
+   */
+
+  import sheep from '@/sheep';
+  import { reactive, ref, unref } from 'vue';
+  import { onBackPress } from '@dcloudio/uni-app';
+
+  // 定义属性
+  const props = defineProps({
+    data: {
+      type: Object,
+      default() {},
+    }
+  })
+
+  // 悬浮按钮配置: https://uniapp.dcloud.net.cn/component/uniui/uni-fab.html#fab-props
+  const state = reactive({
+    // 可选样式配置项
+    pattern: [],
+    // 展开菜单内容配置项
+    content: [],
+    // 展开菜单显示方式:horizontal-水平显示,vertical-垂直显示
+    direction: '',
+  });
+
+  // 悬浮按钮引用
+  const fabRef = ref(null);
+  // 按钮方向
+  state.direction = props.data.direction;
+  props.data?.list.forEach((item) => {
+    // 按钮文字
+    const text = props.data?.showText ? item.text : ''
+    // 生成内容配置项
+    state.content.push({ iconPath: sheep.$url.cdn(item.imgUrl), url: item.url, text });
+    // 生成样式配置项
+    state.pattern.push({ color: item.textColor });
+  });
+
+  // 处理链接跳转
+  function handleOpenLink(e) {
+    sheep.$router.go(e.item.url);
+  }
+
+  // 折叠
+  function handleCollapseFab() {
+    if (unref(fabRef)?.isShow) {
+      unref(fabRef)?.close();
+    }
+  }
+
+  // 按返回值后,折叠悬浮按钮
+  onBackPress(() => {
+    if (unref(fabRef)?.isShow) {
+      unref(fabRef)?.close();
+      return true;
+    }
+    return false;
+  });
+</script>
+<style lang="scss" scoped>
+  /* 模态背景 */
+  .modal-bg {
+    position: fixed;
+    left: 0;
+    top: 0;
+    z-index: 11;
+    width: 100%;
+    height: 100%;
+    background-color: rgba(#000000, 0.4);
+  }
+</style>

+ 286 - 0
sheep/components/s-goods-card/s-goods-card.vue

@@ -0,0 +1,286 @@
+<!-- 装修商品组件:商品卡片 -->
+<template>
+  <!-- 商品卡片 -->
+  <view>
+    <!-- 布局1. 单列大图(上图,下内容)-->
+    <view v-if="layoutType === LayoutTypeEnum.ONE_COL_BIG_IMG && state.goodsList.length" class="goods-sl-box">
+      <view
+        class="goods-box"
+        v-for="item in state.goodsList"
+        :key="item.id"
+        :style="[{ marginBottom: data.space * 2 + 'rpx' }]"
+      >
+        <s-goods-column
+          class=""
+          size="sl"
+          :goodsFields="data.fields"
+          :tagStyle="data.badge"
+          :data="item"
+          :titleColor="data.fields.name?.color"
+          :subTitleColor="data.fields.introduction.color"
+          :topRadius="data.borderRadiusTop"
+          :bottomRadius="data.borderRadiusBottom"
+          @click="sheep.$router.go('/pages/goods/index', { id: item.id })"
+        >
+          <!-- 购买按钮 -->
+          <template v-slot:cart>
+            <button class="ss-reset-button cart-btn" :style="[buyStyle]">
+              {{ btnBuy.type === 'text' ? btnBuy.text : '' }}
+            </button>
+          </template>
+        </s-goods-column>
+      </view>
+    </view>
+
+    <!-- 布局2. 双列(每一列:上图,下内容)-->
+    <view
+      v-if="layoutType === LayoutTypeEnum.TWO_COL && state.goodsList.length"
+      class="goods-md-wrap ss-flex ss-flex-wrap ss-col-top"
+    >
+      <view class="goods-list-box">
+        <view
+          class="left-list"
+          :style="[{ paddingRight: data.space + 'rpx', marginBottom: data.space + 'px' }]"
+          v-for="item in state.leftGoodsList"
+          :key="item.id"
+        >
+          <s-goods-column
+            class="goods-md-box"
+            size="md"
+            :goodsFields="data.fields"
+            :tagStyle="data.badge"
+            :data="item"
+            :titleColor="data.fields.name?.color"
+            :subTitleColor="data.fields.introduction.color"
+            :topRadius="data.borderRadiusTop"
+            :bottomRadius="data.borderRadiusBottom"
+            :titleWidth="330 - marginLeft - marginRight"
+            @click="sheep.$router.go('/pages/goods/index', { id: item.id })"
+            @getHeight="calculateGoodsColumn($event, 'left')"
+          >
+            <!-- 购买按钮 -->
+            <template v-slot:cart>
+              <button class="ss-reset-button cart-btn" :style="[buyStyle]">
+                {{ btnBuy.type === 'text' ? btnBuy.text : '' }}
+              </button>
+            </template>
+          </s-goods-column>
+        </view>
+      </view>
+      <view class="goods-list-box">
+        <view
+          class="right-list"
+          :style="[{ paddingLeft: data.space + 'rpx', marginBottom: data.space + 'px' }]"
+          v-for="item in state.rightGoodsList"
+          :key="item.id"
+        >
+          <s-goods-column
+            class="goods-md-box"
+            size="md"
+            :goodsFields="data.fields"
+            :tagStyle="data.badge"
+            :data="item"
+            :titleColor="data.fields.name?.color"
+            :subTitleColor="data.fields.introduction.color"
+            :topRadius="data.borderRadiusTop"
+            :bottomRadius="data.borderRadiusBottom"
+            :titleWidth="330 - marginLeft - marginRight"
+            @click="sheep.$router.go('/pages/goods/index', { id: item.id })"
+            @getHeight="calculateGoodsColumn($event, 'right')"
+          >
+            <!-- 购买按钮 -->
+            <template v-slot:cart>
+              <button class="ss-reset-button cart-btn" :style="[buyStyle]">
+                {{ btnBuy.type === 'text' ? btnBuy.text : '' }}
+              </button>
+            </template>
+          </s-goods-column>
+        </view>
+      </view>
+    </view>
+
+    <!-- 布局3. 单列小图(左图,右内容) -->
+    <view v-if="layoutType === LayoutTypeEnum.ONE_COL_SMALL_IMG && state.goodsList.length" class="goods-lg-box">
+      <view
+        class="goods-box"
+        :style="[{ marginBottom: data.space + 'px' }]"
+        v-for="item in state.goodsList"
+        :key="item.id"
+      >
+        <s-goods-column
+          class="goods-card"
+          size="lg"
+          :goodsFields="data.fields"
+          :data="item"
+          :tagStyle="data.badge"
+          :titleColor="data.fields.name?.color"
+          :subTitleColor="data.fields.introduction.color"
+          :topRadius="data.borderRadiusTop"
+          :bottomRadius="data.borderRadiusBottom"
+          @tap="sheep.$router.go('/pages/goods/index', { id: item.id })"
+        >
+          <!-- 购买按钮 -->
+          <template v-slot:cart>
+            <button class="ss-reset-button cart-btn" :style="[buyStyle]">
+              {{ btnBuy.type === 'text' ? btnBuy.text : '' }}
+            </button>
+          </template>
+        </s-goods-column>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup>
+  /**
+   * 商品卡片
+   */
+  import { computed, reactive, onMounted } from 'vue';
+  import sheep from '@/sheep';
+  import SpuApi from '@/sheep/api/product/spu';
+
+  // 布局类型
+  const LayoutTypeEnum = {
+    // 单列大图
+    ONE_COL_BIG_IMG: 'oneColBigImg',
+    // 双列
+    TWO_COL: 'twoCol',
+    // 单列小图
+    ONE_COL_SMALL_IMG: 'oneColSmallImg',
+  }
+
+  const state = reactive({
+    goodsList: [],
+    leftGoodsList: [],
+    rightGoodsList: [],
+  });
+  const props = defineProps({
+    data: {
+      type: Object,
+      default() {},
+    },
+    styles: {
+      type: Object,
+      default() {},
+    },
+  });
+
+  const { layoutType, btnBuy, spuIds } = props.data ?? {};
+  const { marginLeft, marginRight } = props.styles ?? {};
+
+  // 购买按钮样式
+  const buyStyle = computed(() => {
+    if (btnBuy.type === 'text') {
+      // 文字按钮:线性渐变背景颜色
+      return {
+        background: `linear-gradient(to right, ${btnBuy.bgBeginColor}, ${btnBuy.bgEndColor})`,
+      };
+    }
+    if (btnBuy.type === 'img') {
+      // 图片按钮
+      return {
+        width: '54rpx',
+        height: '54rpx',
+        background: `url(${sheep.$url.cdn(btnBuy.imgUrl)}) no-repeat`,
+        backgroundSize: '100% 100%',
+      };
+    }
+  });
+
+  //region 商品瀑布流布局
+  // 下一个要处理的商品索引
+  let count = 0;
+  // 左列的高度
+  let leftHeight = 0;
+  // 右列的高度
+  let rightHeight = 0;
+
+  /**
+   * 计算商品在左列还是右列
+   * @param height 商品的高度
+   * @param where 添加到哪一列
+   */
+  function calculateGoodsColumn(height = 0, where = 'left') {
+    // 处理完
+    if (!state.goodsList[count]) return;
+    // 增加列的高度
+    if (where === 'left') leftHeight += height;
+    if (where === 'right') rightHeight += height;
+    // 添加到矮的一列
+    if (leftHeight <= rightHeight) {
+      state.leftGoodsList.push(state.goodsList[count]);
+    } else {
+      state.rightGoodsList.push(state.goodsList[count]);
+    }
+    // 计数
+    count++;
+  }
+  //endregion
+
+  /**
+   * 根据商品编号列表,获取商品列表
+   * @param ids 商品编号列表
+   * @return {Promise<undefined>} 商品列表
+   */
+  async function getGoodsListByIds(ids) {
+    const { data } = await SpuApi.getSpuListByIds(ids);
+    return data;
+  }
+
+  // 初始化
+  onMounted(async () => {
+    // 加载商品列表
+    state.goodsList = await getGoodsListByIds(spuIds.join(','));
+    // 只有双列布局时需要
+    if (layoutType === LayoutTypeEnum.TWO_COL){
+      // 分列
+      calculateGoodsColumn();
+    }
+  });
+</script>
+
+<style lang="scss" scoped>
+  .goods-md-wrap {
+    width: 100%;
+  }
+
+  .goods-list-box {
+    width: 50%;
+    box-sizing: border-box;
+    .left-list {
+      &:nth-last-child(1) {
+        margin-bottom: 0 !important;
+      }
+    }
+    .right-list {
+      &:nth-last-child(1) {
+        margin-bottom: 0 !important;
+      }
+    }
+  }
+
+  .goods-box {
+    &:nth-last-of-type(1) {
+      margin-bottom: 0 !important;
+    }
+  }
+
+  .goods-md-box,
+  .goods-sl-box,
+  .goods-lg-box {
+    position: relative;
+
+    .cart-btn {
+      position: absolute;
+      bottom: 18rpx;
+      right: 20rpx;
+      z-index: 11;
+      height: 50rpx;
+      line-height: 50rpx;
+      padding: 0 20rpx;
+      border-radius: 25rpx;
+      font-size: 24rpx;
+      color: #fff;
+    }
+  }
+</style>

+ 721 - 0
sheep/components/s-goods-column/s-goods-column.vue

@@ -0,0 +1,721 @@
+<!-- 页面 -->
+<template>
+  <view class="ss-goods-wrap">
+    <!-- xs卡片:横向紧凑型,一行放两个,图片左内容右边  -->
+    <view
+      v-if="size === 'xs'"
+      class="xs-goods-card ss-flex ss-col-stretch"
+      :style="[elStyles]"
+      @tap="onClick"
+    >
+      <view v-if="tagStyle.show" class="tag-icon-box">
+        <image class="tag-icon" :src="sheep.$url.cdn(tagStyle.src || tagStyle.imgUrl)"></image>
+      </view>
+      <image class="xs-img-box" :src="sheep.$url.cdn(data.image || data.picUrl)" mode="aspectFit"></image>
+      <view
+        v-if="goodsFields.title?.show || goodsFields.name?.show || goodsFields.price?.show"
+        class="xs-goods-content ss-flex-col ss-row-around"
+      >
+        <view
+          v-if="goodsFields.title?.show || goodsFields.name?.show"
+          class="xs-goods-title ss-line-1"
+          :style="[{ color: titleColor, width: titleWidth ? titleWidth + 'rpx' : '' }]"
+        >
+          {{ data.title || data.name }}
+        </view>
+        <view
+          v-if="goodsFields.price?.show"
+          class="xs-goods-price font-OPPOSANS"
+          :style="[{ color: goodsFields.price.color }]"
+        >
+          <text class="price-unit ss-font-24">{{ priceUnit }}</text>
+          {{ isArray(data.price) ? fen2yuan(data.price[0]) : fen2yuan(data.price) }}
+        </view>
+      </view>
+    </view>
+
+    <!-- sm卡片:竖向紧凑,一行放三个,图上内容下 -->
+    <view v-if="size === 'sm'" class="sm-goods-card ss-flex-col" :style="[elStyles]" @tap="onClick">
+      <view v-if="tagStyle.show" class="tag-icon-box">
+        <image class="tag-icon" :src="sheep.$url.cdn(tagStyle.src || tagStyle.imgUrl)"></image>
+      </view>
+      <image class="sm-img-box" :src="sheep.$url.cdn(data.image || data.picUrl)" mode="aspectFill"></image>
+
+      <view
+        v-if="goodsFields.title?.show || goodsFields.name?.show || goodsFields.price?.show"
+        class="sm-goods-content"
+        :style="[{ color: titleColor, width: titleWidth ? titleWidth + 'rpx' : '' }]"
+      >
+        <view v-if="goodsFields.title?.show || goodsFields.name?.show" class="sm-goods-title ss-line-1 ss-m-b-16">
+          {{ data.title || data.name }}
+        </view>
+        <view
+          v-if="goodsFields.price?.show"
+          class="sm-goods-price font-OPPOSANS"
+          :style="[{ color: goodsFields.price.color }]"
+        >
+          <text class="price-unit ss-font-24">{{ priceUnit }}</text>
+          {{ isArray(data.price) ? fen2yuan(data.price[0]) : fen2yuan(data.price) }}
+        </view>
+      </view>
+    </view>
+
+    <!-- md卡片:竖向,一行放两个,图上内容下 -->
+    <view v-if="size === 'md'" class="md-goods-card ss-flex-col" :style="[elStyles]" @tap="onClick">
+      <view v-if="tagStyle.show" class="tag-icon-box">
+        <image class="tag-icon" :src="sheep.$url.cdn(tagStyle.src || tagStyle.imgUrl)"></image>
+      </view>
+      <image class="md-img-box" :src="sheep.$url.cdn(data.image || data.picUrl)" mode="widthFix"></image>
+      <view
+        class="md-goods-content ss-flex-col ss-row-around ss-p-b-20 ss-p-t-20 ss-p-x-16"
+        :id="elId"
+      >
+        <view
+          v-if="goodsFields.title?.show || goodsFields.name?.show"
+          class="md-goods-title ss-line-1"
+          :style="[{ color: titleColor, width: titleWidth ? titleWidth + 'rpx' : '' }]"
+        >
+          {{ data.title || data.name }}
+        </view>
+        <view
+          v-if="goodsFields.subtitle?.show || goodsFields.introduction?.show"
+          class="md-goods-subtitle ss-m-t-16 ss-line-1"
+          :style="[{ color: subTitleColor, background: subTitleBackground }]"
+        >
+          {{ data.subtitle || data.introduction }}
+        </view>
+        <slot name="activity">
+          <view v-if="data.promos?.length" class="tag-box ss-flex-wrap ss-flex ss-col-center">
+            <view
+              class="activity-tag ss-m-r-10 ss-m-t-16"
+              v-for="item in data.promos"
+              :key="item.id"
+            >
+              {{ item.title }}
+            </view>
+          </view>
+        </slot>
+        <view class="ss-flex ss-col-bottom">
+          <view
+            v-if="goodsFields.price?.show"
+            class="md-goods-price ss-m-t-16 font-OPPOSANS ss-m-r-10"
+            :style="[{ color: goodsFields.price.color }]"
+          >
+            <text class="price-unit ss-font-24">{{ priceUnit }}</text>
+            {{ isArray(data.price) ? fen2yuan(data.price[0]) : fen2yuan(data.price) }}
+          </view>
+
+          <view
+            v-if="(goodsFields.original_price?.show||goodsFields.marketPrice?.show) &&( data.original_price > 0|| data.marketPrice > 0)"
+            class="goods-origin-price ss-m-t-16 font-OPPOSANS ss-flex"
+            :style="[{ color: originPriceColor }]"
+          >
+            <text class="price-unit ss-font-20">{{ priceUnit }}</text>
+            <view class="ss-m-l-8">{{ fen2yuan(data.marketPrice) }}</view>
+          </view>
+        </view>
+
+        <view class="ss-m-t-16 ss-flex ss-col-center ss-flex-wrap">
+          <view class="sales-text">{{ salesAndStock }}</view>
+        </view>
+      </view>
+
+      <slot name="cart">
+        <view class="cart-box ss-flex ss-col-center ss-row-center">
+          <image class="cart-icon" src="/static/img/shop/tabbar/category2.png" mode="" />
+        </view>
+      </slot>
+    </view>
+
+    <!-- lg卡片:横向型,一行放一个,图片左内容右边  -->
+    <view
+      v-if="size === 'lg'"
+      class="lg-goods-card ss-flex ss-col-stretch"
+      :style="[elStyles]"
+      @tap="onClick"
+    >
+      <view v-if="tagStyle.show" class="tag-icon-box">
+        <image class="tag-icon" :src="sheep.$url.cdn(tagStyle.src || tagStyle.imgUrl)"></image>
+      </view>
+      <view v-if="seckillTag" class="seckill-tag ss-flex ss-row-center"> 秒杀 </view>
+      <view v-if="grouponTag" class="groupon-tag ss-flex ss-row-center">
+        <view class="tag-icon">拼团</view>
+      </view>
+      <image class="lg-img-box" :src="sheep.$url.cdn(data.image || data.picUrl)" mode="aspectFill"></image>
+      <view class="lg-goods-content ss-flex-1 ss-flex-col ss-row-between ss-p-b-10 ss-p-t-20">
+        <view>
+          <view
+            v-if="goodsFields.title?.show || goodsFields.name?.show"
+            class="lg-goods-title ss-line-2"
+            :style="[{ color: titleColor }]"
+          >
+            {{ data.title || data.name }}
+          </view>
+          <view
+            v-if="goodsFields.subtitle?.show || goodsFields.introduction?.show"
+            class="lg-goods-subtitle ss-m-t-10 ss-line-1"
+            :style="[{ color: subTitleColor, background: subTitleBackground }]"
+          >
+            {{ data.subtitle || data.introduction }}
+          </view>
+        </view>
+        <view>
+          <slot name="activity">
+            <view v-if="data.promos?.length" class="tag-box ss-flex ss-col-center">
+              <view class="activity-tag ss-m-r-10" v-for="item in data.promos" :key="item.id">
+                {{ item.title }}
+              </view>
+            </view>
+          </slot>
+          <view class="ss-flex ss-col-bottom ss-m-t-10">
+            <view
+              v-if="goodsFields.price?.show"
+              class="lg-goods-price ss-m-r-12 ss-flex ss-col-bottom font-OPPOSANS"
+              :style="[{ color: goodsFields.price.color }]"
+            >
+              <text class="ss-font-24">{{ priceUnit }}</text>
+              {{ isArray(data.price) ? fen2yuan(data.price[0]) : fen2yuan(data.price) }}
+            </view>
+            <view
+              v-if="(goodsFields.original_price?.show||goodsFields.marketPrice?.show) &&( data.original_price > 0|| data.marketPrice > 0)"
+              class="goods-origin-price ss-flex ss-col-bottom font-OPPOSANS"
+              :style="[{ color: originPriceColor }]"
+            >
+              <text class="price-unit ss-font-20">{{ priceUnit }}</text>
+              <view class="ss-m-l-8">{{ fen2yuan(data.marketPrice) }}</view>
+            </view>
+          </view>
+          <view class="ss-m-t-8 ss-flex ss-col-center ss-flex-wrap">
+            <view class="sales-text">{{ salesAndStock }}</view>
+          </view>
+        </view>
+      </view>
+
+      <slot name="cart">
+        <view class="buy-box ss-flex ss-col-center ss-row-center" v-if="buttonShow">
+          去购买
+        </view>
+      </slot>
+    </view>
+
+    <!-- sl卡片:竖向型,一行放一个,图片上内容下边 -->
+    <view v-if="size === 'sl'" class="sl-goods-card ss-flex-col" :style="[elStyles]" @tap="onClick">
+      <view v-if="tagStyle.show" class="tag-icon-box">
+        <image class="tag-icon" :src="sheep.$url.cdn(tagStyle.src || tagStyle.imgUrl)"></image>
+      </view>
+
+      <image class="sl-img-box" :src="sheep.$url.cdn(data.image || data.picUrl)" mode="aspectFill"></image>
+
+      <view class="sl-goods-content">
+        <view>
+          <view
+            v-if="goodsFields.title?.show || goodsFields.name?.show"
+            class="sl-goods-title ss-line-1"
+            :style="[{ color: titleColor }]"
+          >
+            {{ data.title || data.name }}
+          </view>
+          <view
+            v-if="goodsFields.subtitle?.show || goodsFields.introduction?.show"
+            class="sl-goods-subtitle ss-m-t-16"
+            :style="[{ color: subTitleColor, background: subTitleBackground }]"
+          >
+            {{ data.subtitle || data.introduction }}
+          </view>
+        </view>
+        <view>
+          <slot name="activity">
+            <view v-if="data.promos?.length" class="tag-box ss-flex ss-col-center ss-flex-wrap">
+              <view
+                class="activity-tag ss-m-r-10 ss-m-t-16"
+                v-for="item in data.promos"
+                :key="item.id"
+              >
+                {{ item.title }}
+              </view>
+            </view>
+          </slot>
+          <view v-if="goodsFields.price?.show" class="ss-flex ss-col-bottom font-OPPOSANS">
+            <view class="sl-goods-price ss-m-r-12" :style="[{ color: goodsFields.price.color }]">
+              <text class="price-unit ss-font-24">{{ priceUnit }}</text>
+              {{ isArray(data.price) ? fen2yuan(data.price[0]) : fen2yuan(data.price) }}
+            </view>
+            <view
+              v-if="(goodsFields.original_price?.show||goodsFields.marketPrice?.show) &&( data.original_price > 0|| data.marketPrice > 0)"
+              class="goods-origin-price ss-m-t-16 font-OPPOSANS ss-flex"
+              :style="[{ color: originPriceColor }]"
+            >
+              <text class="price-unit ss-font-20">{{ priceUnit }}</text>
+              <view class="ss-m-l-8">{{ fen2yuan(data.marketPrice) }}</view>
+            </view>
+          </view>
+          <view class="ss-m-t-16 ss-flex ss-flex-wrap">
+            <view class="sales-text">{{ salesAndStock }}</view>
+          </view>
+        </view>
+      </view>
+
+      <slot name="cart"
+        ><view class="buy-box ss-flex ss-col-center ss-row-center">去购买</view></slot
+      >
+    </view>
+  </view>
+</template>
+
+<script setup>
+  /**
+   * 商品卡片
+   *
+   * @property {Array} size = [xs | sm | md | lg | sl ] 			 	- 列表数据
+   * @property {String} tag 											- md及以上才有
+   * @property {String} img 											- 图片
+   * @property {String} background 									- 背景色
+   * @property {String} topRadius 									- 上圆角
+   * @property {String} bottomRadius 									- 下圆角
+   * @property {String} title 										- 标题
+   * @property {String} titleColor 									- 标题颜色
+   * @property {Number} titleWidth = 0								- 标题宽度,默认0,单位rpx
+   * @property {String} subTitle 										- 副标题
+   * @property {String} subTitleColor									- 副标题颜色
+   * @property {String} subTitleBackground 							- 副标题背景
+   * @property {String | Number} price 								- 价格
+   * @property {String} priceColor 									- 价格颜色
+   * @property {String | Number} originPrice 							- 原价/划线价
+   * @property {String} originPriceColor 								- 原价颜色
+   * @property {String | Number} sales 								- 销售数量
+   * @property {String} salesColor									- 销售数量颜色
+   *
+   * @slots activity												 	- 活动插槽
+   * @slots cart														- 购物车插槽,默认包含文字,背景色,文字颜色 || 图片 || 行为
+   *
+   * @event {Function()} click 										- 点击卡片
+   *
+   */
+  import { computed, reactive, getCurrentInstance, onMounted, nextTick } from 'vue';
+  import sheep from '@/sheep';
+  import { fen2yuan, formatSales } from '@/sheep/hooks/useGoods';
+  import { formatStock } from '@/sheep/hooks/useGoods';
+  import goodsCollectVue from '@/pages/user/goods-collect.vue';
+  import { isArray } from 'lodash';
+
+  // 数据
+  const state = reactive({});
+
+  // 接收参数
+  const props = defineProps({
+    goodsFields: {
+      type: [Array, Object],
+      default() {
+        return {
+          // 商品价格
+          price: { show: true },
+          // 库存
+          stock: { show: true },
+          // 商品名称
+          name: { show: true },
+          // 商品介绍
+          introduction: { show: true },
+          // 市场价
+          marketPrice: { show: true },
+          // 销量
+          salesCount: { show: true },
+        };
+      },
+    },
+    tagStyle: {
+      type: Object,
+      default: {},
+    },
+    data: {
+      type: Object,
+      default: {},
+    },
+    size: {
+      type: String,
+      default: 'sl',
+    },
+    background: {
+      type: String,
+      default: '',
+    },
+    topRadius: {
+      type: Number,
+      default: 0,
+    },
+    bottomRadius: {
+      type: Number,
+      default: 0,
+    },
+    titleWidth: {
+      type: Number,
+      default: 0,
+    },
+    titleColor: {
+      type: String,
+      default: '#333',
+    },
+    priceColor: {
+      type: String,
+      default: '',
+    },
+    originPriceColor: {
+      type: String,
+      default: '#C4C4C4',
+    },
+    priceUnit: {
+      type: String,
+      default: '¥',
+    },
+    subTitleColor: {
+      type: String,
+      default: '#999999',
+    },
+    subTitleBackground: {
+      type: String,
+      default: '',
+    },
+    buttonShow: {
+      type: Boolean,
+      default: true,
+    },
+    seckillTag: {
+      type: Boolean,
+      default: false,
+    },
+    grouponTag: {
+      type: Boolean,
+      default: false,
+    },
+  });
+
+  // 组件样式
+  const elStyles = computed(() => {
+    return {
+      background: props.background,
+      'border-top-left-radius': props.topRadius + 'px',
+      'border-top-right-radius': props.topRadius + 'px',
+      'border-bottom-left-radius': props.bottomRadius + 'px',
+      'border-bottom-right-radius': props.bottomRadius + 'px',
+    };
+  });
+
+  // 格式化销量、库存信息
+  const salesAndStock = computed(() => {
+    let text = [];
+    if (props.goodsFields.salesCount?.show) {
+      text.push(formatSales(props.data.sales_show_type, props.data.salesCount));
+    }
+    if (props.goodsFields.stock?.show) {
+      text.push(formatStock(props.data.stock_show_type, props.data.stock));
+    }
+    return text.join(' | ');
+  });
+
+  // 返回事件
+  const emits = defineEmits(['click', 'getHeight']);
+
+  const onClick = () => {
+    emits('click');
+  };
+
+  // 获取卡片实时高度
+  const { proxy } = getCurrentInstance();
+  const elId = `sheep_${Math.ceil(Math.random() * 10e5).toString(36)}`;
+  function getGoodsPriceCardWH() {
+    if (props.size === 'md') {
+      const view = uni.createSelectorQuery().in(proxy);
+      view.select(`#${elId}`).fields({ size: true, scrollOffset: true });
+      view.exec((data) => {
+        let totalHeight = 0;
+        const goodsPriceCard = data[0];
+        if (props.data.image_wh) {
+          totalHeight =
+            (goodsPriceCard.width / props.data.image_wh.w) * props.data.image_wh.h +
+            goodsPriceCard.height;
+        } else {
+          totalHeight = goodsPriceCard.width;
+        }
+        emits('getHeight', totalHeight);
+      });
+    }
+  }
+  onMounted(() => {
+    nextTick(() => {
+      getGoodsPriceCardWH();
+    });
+  });
+</script>
+
+<style lang="scss" scoped>
+  .tag-icon-box {
+    position: absolute;
+    left: 0;
+    top: 0;
+    z-index: 2;
+    .tag-icon {
+      width: 72rpx;
+      height: 44rpx;
+    }
+  }
+  .seckill-tag {
+    position: absolute;
+    left: 0;
+    top: 0;
+    z-index: 2;
+    width: 68rpx;
+    height: 38rpx;
+    background: linear-gradient(90deg, #ff5854 0%, #ff2621 100%);
+    border-radius: 10rpx 0px 10rpx 0px;
+    font-size: 24rpx;
+    font-weight: 500;
+    color: #ffffff;
+    line-height: 32rpx;
+  }
+  .groupon-tag {
+    position: absolute;
+    left: 0;
+    top: 0;
+    z-index: 2;
+    width: 68rpx;
+    height: 38rpx;
+    background: linear-gradient(90deg, #fe832a 0%, #ff6600 100%);
+    border-radius: 10rpx 0px 10rpx 0px;
+    font-size: 24rpx;
+    font-weight: 500;
+    color: #ffffff;
+    line-height: 32rpx;
+  }
+  .goods-img {
+    width: 100%;
+    height: 100%;
+    background-color: #f5f5f5;
+  }
+  .price-unit {
+    margin-right: -4px;
+  }
+  .sales-text {
+    display: table;
+    font-size: 24rpx;
+    transform: scale(0.8);
+    margin-left: 0rpx;
+    color: #c4c4c4;
+  }
+
+  .activity-tag {
+    font-size: 20rpx;
+    color: #ff0000;
+    line-height: 30rpx;
+    padding: 0 10rpx;
+    border: 1px solid rgba(#ff0000, 0.25);
+    border-radius: 4px;
+    flex-shrink: 0;
+  }
+
+  .goods-origin-price {
+    font-size: 20rpx;
+    color: #c4c4c4;
+    line-height: 36rpx;
+    text-decoration: line-through;
+  }
+
+  // xs
+  .xs-goods-card {
+    overflow: hidden;
+    // max-width: 375rpx;
+    background-color: $white;
+    position: relative;
+
+    .xs-img-box {
+      width: 128rpx;
+      height: 128rpx;
+      margin-right: 20rpx;
+    }
+
+    .xs-goods-title {
+      font-size: 26rpx;
+      color: #333;
+      font-weight: 500;
+    }
+
+    .xs-goods-price {
+      font-size: 30rpx;
+      color: $red;
+    }
+  }
+
+  // sm
+  .sm-goods-card {
+    overflow: hidden;
+    // width: 223rpx;
+    // width: 100%;
+    background-color: $white;
+    position: relative;
+
+    .sm-img-box {
+      // width: 228rpx;
+      width: 100%;
+      height: 208rpx;
+    }
+    .sm-goods-content {
+      padding: 20rpx 16rpx;
+      box-sizing: border-box;
+    }
+    .sm-goods-title {
+      font-size: 26rpx;
+      color: #333;
+    }
+
+    .sm-goods-price {
+      font-size: 30rpx;
+      color: $red;
+    }
+  }
+
+  // md
+  .md-goods-card {
+    overflow: hidden;
+    width: 100%;
+    position: relative;
+    z-index: 1;
+    background-color: $white;
+    position: relative;
+
+    .md-img-box {
+      width: 100%;
+    }
+
+    .md-goods-title {
+      font-size: 26rpx;
+      color: #333;
+      width: 100%;
+    }
+    .md-goods-subtitle {
+      font-size: 24rpx;
+      font-weight: 400;
+      color: #999999;
+    }
+
+    .md-goods-price {
+      font-size: 30rpx;
+      color: $red;
+      line-height: 36rpx;
+    }
+
+    .cart-box {
+      width: 54rpx;
+      height: 54rpx;
+      background: linear-gradient(90deg, #fe8900, #ff5e00);
+      border-radius: 50%;
+      position: absolute;
+      bottom: 50rpx;
+      right: 20rpx;
+      z-index: 2;
+
+      .cart-icon {
+        width: 30rpx;
+        height: 30rpx;
+      }
+    }
+  }
+
+  // lg
+  .lg-goods-card {
+    overflow: hidden;
+    position: relative;
+    z-index: 1;
+    background-color: $white;
+    height: 280rpx;
+
+    .lg-img-box {
+      width: 280rpx;
+      height: 280rpx;
+      margin-right: 20rpx;
+    }
+
+    .lg-goods-title {
+      font-size: 28rpx;
+      font-weight: 500;
+      color: #333333;
+      // line-height: 36rpx;
+      // width: 410rpx;
+    }
+    .lg-goods-subtitle {
+      font-size: 24rpx;
+      font-weight: 400;
+      color: #999999;
+      // line-height: 30rpx;
+      // width: 410rpx;
+    }
+
+    .lg-goods-price {
+      font-size: 30rpx;
+      color: $red;
+      line-height: 36rpx;
+    }
+
+    .buy-box {
+      position: absolute;
+      bottom: 20rpx;
+      right: 20rpx;
+      z-index: 2;
+      width: 120rpx;
+      height: 50rpx;
+      background: linear-gradient(90deg, #fe8900, #ff5e00);
+      border-radius: 25rpx;
+      font-size: 24rpx;
+      color: #ffffff;
+    }
+    .tag-box {
+      width: 100%;
+    }
+  }
+
+  // sl
+
+  .sl-goods-card {
+    overflow: hidden;
+    position: relative;
+    z-index: 1;
+    width: 100%;
+    background-color: $white;
+    .sl-goods-content {
+      padding: 20rpx 20rpx;
+      box-sizing: border-box;
+    }
+    .sl-img-box {
+      width: 100%;
+      height: 360rpx;
+    }
+
+    .sl-goods-title {
+      font-size: 26rpx;
+      color: #333;
+      font-weight: 500;
+    }
+    .sl-goods-subtitle {
+      font-size: 24rpx;
+      font-weight: 400;
+      color: #999999;
+      line-height: 30rpx;
+    }
+
+    .sl-goods-price {
+      font-size: 30rpx;
+      color: $red;
+      line-height: 36rpx;
+    }
+
+    .buy-box {
+      position: absolute;
+      bottom: 20rpx;
+      right: 20rpx;
+      z-index: 2;
+      width: 148rpx;
+      height: 50rpx;
+      background: linear-gradient(90deg, #fe8900, #ff5e00);
+      border-radius: 25rpx;
+      font-size: 24rpx;
+      color: #ffffff;
+    }
+  }
+</style>

+ 181 - 0
sheep/components/s-goods-item/s-goods-item.vue

@@ -0,0 +1,181 @@
+<template>
+  <view>
+    <view>
+      <slot name="top"></slot>
+    </view>
+    <view
+      class="ss-order-card-warp ss-flex ss-col-stretch ss-row-between bg-white"
+      :style="[{ borderRadius: radius + 'rpx', marginBottom: marginBottom + 'rpx' }]"
+    >
+      <view class="img-box ss-m-r-24">
+        <image class="order-img" :src="sheep.$url.cdn(img)" mode="aspectFill"></image>
+      </view>
+      <view
+        class="box-right ss-flex-col ss-row-between"
+        :style="[{ width: titleWidth ? titleWidth + 'rpx' : '' }]"
+      >
+        <view class="title-text ss-line-2" v-if="title">{{ title }}</view>
+        <view v-if="skuString" class="spec-text ss-m-t-8 ss-m-b-12">{{ skuString }}</view>
+        <view class="groupon-box">
+          <slot name="groupon"></slot>
+        </view>
+        <view class="ss-flex">
+          <view class="ss-flex ss-col-center">
+            <view
+              class="price-text ss-flex ss-col-center"
+              :style="[{ color: priceColor }]"
+              v-if="price && Number(price) > 0"
+            >
+              ¥{{ fen2yuan(price) }}
+            </view>
+            <view v-if="num" class="total-text ss-flex ss-col-center">x {{ num }}</view>
+            <slot name="priceSuffix"></slot>
+          </view>
+        </view>
+        <view class="tool-box">
+          <slot name="tool"></slot>
+        </view>
+        <view>
+          <slot name="rightBottom"></slot>
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup>
+  import sheep from '@/sheep';
+  import { computed } from 'vue';
+  import { fen2yuan } from '@/sheep/hooks/useGoods';
+  /**
+   * 订单卡片
+   *
+   * @property {String} img 											- 图片
+   * @property {String} title 										- 标题
+   * @property {Number} titleWidth = 0								- 标题宽度,默认0,单位rpx
+   * @property {String} skuText 										- 规格
+   * @property {String | Number} price 								- 价格
+   * @property {String} priceColor 									- 价格颜色
+   * @property {Number | String} num									- 数量
+   *
+   */
+  const props = defineProps({
+    img: {
+      type: String,
+      default: 'https://img1.baidu.com/it/u=1601695551,235775011&fm=26&fmt=auto',
+    },
+    title: {
+      type: String,
+      default: '',
+    },
+    titleWidth: {
+      type: Number,
+      default: 0,
+    },
+    skuText: {
+      type: [String, Array],
+      default: '',
+    },
+    price: {
+      type: [String, Number],
+      default: '',
+    },
+    priceColor: {
+      type: [String],
+      default: '',
+    },
+    num: {
+      type: [String, Number],
+      default: 0,
+    },
+    score: {
+      type: [String, Number],
+      default: '',
+    },
+    radius: {
+      type: [String],
+      default: '',
+    },
+    marginBottom: {
+      type: [String],
+      default: '',
+    },
+  });
+  const skuString = computed(() => {
+    if (!props.skuText) {
+      return '';
+    }
+    if (typeof props.skuText === 'object') {
+      return props.skuText.join(',');
+    }
+    return props.skuText;
+  });
+</script>
+
+<style lang="scss" scoped>
+  .score-img {
+    width: 36rpx;
+    height: 36rpx;
+    margin: 0 4rpx;
+  }
+  .ss-order-card-warp {
+    padding: 20rpx;
+
+    .img-box {
+      width: 164rpx;
+      height: 164rpx;
+      border-radius: 10rpx;
+      overflow: hidden;
+
+      .order-img {
+        width: 164rpx;
+        height: 164rpx;
+      }
+    }
+
+    .box-right {
+      flex: 1;
+      // width: 500rpx;
+      // height: 164rpx;
+      position: relative;
+
+      .tool-box {
+        position: absolute;
+        right: 0rpx;
+        bottom: -10rpx;
+      }
+    }
+
+    .title-text {
+      font-size: 28rpx;
+      font-weight: 500;
+      line-height: 40rpx;
+    }
+
+    .spec-text {
+      font-size: 24rpx;
+      font-weight: 400;
+      color: $dark-9;
+      min-width: 0;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      display: -webkit-box;
+      -webkit-line-clamp: 1;
+      -webkit-box-orient: vertical;
+    }
+
+    .price-text {
+      font-size: 24rpx;
+      font-weight: 500;
+      font-family: OPPOSANS;
+    }
+
+    .total-text {
+      font-size: 24rpx;
+      font-weight: 400;
+      line-height: 24rpx;
+      color: $dark-9;
+      margin-left: 8rpx;
+    }
+  }
+</style>

+ 33 - 0
sheep/components/s-goods-scroll/s-goods-scroll.vue

@@ -0,0 +1,33 @@
+<!-- 商品组 - 横向滚动商品(目前暂时没用到) -->
+<template>
+  <view class="goods-scroll-box">
+    <scroll-view class="scroll-box" scroll-x scroll-anchoring>
+      <view class="goods-box ss-flex">
+        <view v-for="(item, index) in list" :key="index">
+          <s-goods-column
+            class="goods-card ss-m-l-20"
+            size="sm"
+            :data="item"
+            :titleWidth="200 - marginLeft - marginRight"
+          />
+        </view>
+      </view>
+    </scroll-view>
+  </view>
+</template>
+
+<script setup>
+  /**
+   * 商品组 - 横向滚动商品
+   */
+  const props = defineProps({
+    list: {
+      type: Array,
+      default() {
+        return [];
+      },
+    },
+  });
+</script>
+
+<style lang="scss" scoped></style>

+ 147 - 0
sheep/components/s-goods-shelves/s-goods-shelves.vue

@@ -0,0 +1,147 @@
+<!-- 装修商品组件:商品栏 -->
+<template>
+  <view>
+    <!-- 布局1. 两列商品,图片左文案右 -->
+    <view
+      v-if="layoutType === 'twoCol'"
+      class="goods-xs-box ss-flex ss-flex-wrap"
+      :style="[{ margin: '-' + data.space + 'rpx' }]"
+    >
+      <view
+        class="goods-xs-list"
+        v-for="item in goodsList"
+        :key="item.id"
+        :style="[
+          {
+            padding: data.space + 'rpx',
+          },
+        ]"
+      >
+        <s-goods-column
+          class="goods-card"
+          size="xs"
+          :goodsFields="data.fields"
+          :tagStyle="data.badge"
+          :data="item"
+          :titleColor="data.fields.name?.color"
+          :topRadius="data.borderRadiusTop"
+          :bottomRadius="data.borderRadiusBottom"
+          :titleWidth="(454 - marginRight * 2 - data.space * 2 - marginLeft * 2) / 2"
+          @click="sheep.$router.go('/pages/goods/index', { id: item.id })"
+        />
+      </view>
+    </view>
+    <!-- 布局. 三列商品:图片上文案下 -->
+    <view
+      v-if="layoutType === 'threeCol'"
+      class="goods-sm-box ss-flex ss-flex-wrap"
+      :style="[{ margin: '-' + data.space + 'rpx' }]"
+    >
+      <view
+        v-for="item in goodsList"
+        :key="item.id"
+        class="goods-card-box"
+        :style="[
+          {
+            padding: data.space + 'rpx',
+          },
+        ]"
+      >
+        <s-goods-column
+          class="goods-card"
+          size="sm"
+          :goodsFields="data.fields"
+          :tagStyle="data.badge"
+          :data="item"
+          :titleColor="data.fields.name?.color"
+          :topRadius="data.borderRadiusTop"
+          :bottomRadius="data.borderRadiusBottom"
+          @click="sheep.$router.go('/pages/goods/index', { id: item.id })"
+        />
+      </view>
+    </view>
+
+    <!-- 布局3. 一行商品,水平滑动 -->
+    <view v-if="layoutType === 'horizSwiper'" class="">
+      <scroll-view class="scroll-box goods-scroll-box" scroll-x scroll-anchoring>
+        <view class="goods-box ss-flex">
+          <view
+            class="goods-card-box"
+            v-for="item in goodsList"
+            :key="item.id"
+            :style="[{ marginRight: data.space * 2 + 'rpx' }]"
+          >
+            <s-goods-column
+              class="goods-card"
+              size="sm"
+              :goodsFields="data.fields"
+              :tagStyle="data.badge"
+              :data="item"
+              :titleColor="data.fields.name?.color"
+              :titleWidth="(750 - marginRight * 2 - data.space * 4 - marginLeft * 2) / 3"
+              @click="sheep.$router.go('/pages/goods/index', { id: item.id })"
+            />
+          </view>
+        </view>
+      </scroll-view>
+    </view>
+  </view>
+</template>
+
+<script setup>
+  /**
+   * 商品栏
+   */
+  import { onMounted, ref, computed } from 'vue';
+  import sheep from '@/sheep';
+  import SpuApi from "@/sheep/api/product/spu";
+
+  const props = defineProps({
+    data: {
+      type: Object,
+      default() {},
+    },
+    styles: {
+      type: Object,
+      default() {},
+    },
+  });
+  const { layoutType, spuIds } = props.data;
+  let { marginLeft, marginRight } = props.styles;
+  const goodsList = ref([]);
+  onMounted(async () => {
+    if (spuIds.length > 0) {
+      let { data } = await SpuApi.getSpuListByIds(spuIds.join(','));
+      goodsList.value = data;
+    }
+  });
+</script>
+
+<style lang="scss" scoped>
+  .goods-xs-box {
+    // margin: 0 auto;
+    width: 100%;
+    .goods-xs-list {
+      box-sizing: border-box;
+      flex-shrink: 0;
+      overflow: hidden;
+      width: 50%;
+    }
+  }
+
+  .goods-sm-box {
+    margin: 0 auto;
+    box-sizing: border-box;
+    .goods-card-box {
+      flex-shrink: 0;
+      overflow: hidden;
+      width: 33.3%;
+      box-sizing: border-box;
+    }
+  }
+  .goods-scroll-box {
+    margin: 0 auto;
+    width: 100%;
+    box-sizing: border-box;
+  }
+</style>

+ 154 - 0
sheep/components/s-groupon-block/s-groupon-block.vue

@@ -0,0 +1,154 @@
+<!-- 装修组件 - 拼团 -->
+<template>
+  <view>
+    <view
+      v-if="layoutType === 'threeCol'"
+      class="goods-sm-box ss-flex ss-flex-wrap"
+      :style="[{ margin: '-' + data.space + 'rpx' }]"
+    >
+      <view
+        v-for="product in productList"
+        :key="product.id"
+        class="goods-card-box"
+        :style="[
+          {
+            padding: data.space + 'rpx',
+          },
+        ]"
+      >
+        <s-goods-column
+          class="goods-card"
+          size="sm"
+          :goodsFields="data.fields"
+          :tagStyle="tagStyle"
+          :data="product"
+          :titleColor="data.fields.name?.color"
+          :topRadius="data.borderRadiusTop"
+          :bottomRadius="data.borderRadiusBottom"
+          @click="
+            sheep.$router.go('/pages/goods/groupon', {
+              id: props.data.activityId,
+            })
+          "
+        ></s-goods-column>
+      </view>
+    </view>
+    <!-- 样式2 一行一个 图片左 文案右 -->
+    <view class="goods-box" v-if="layoutType === 'oneCol'">
+      <view
+        class="goods-list"
+        v-for="(product, index) in productList"
+        :key="index"
+        :style="[{ marginBottom: space + 'px' }]"
+      >
+        <s-goods-column
+          class="goods-card"
+          size="lg"
+          :goodsFields="data.fields"
+          :tagStyle="tagStyle"
+          :data="product"
+          :titleColor="data.fields.name?.color"
+          :subTitleColor="data.fields.introduction?.color"
+          :topRadius="data.borderRadiusTop"
+          :bottomRadius="data.borderRadiusBottom"
+          @click="
+            sheep.$router.go('/pages/goods/groupon', {
+              id: props.data.activityId,
+            })
+          "
+        >
+          <template v-slot:cart>
+            <button class="ss-reset-button cart-btn" :style="[buyStyle]">
+              {{ btnBuy?.type === 'text' ? btnBuy.text : '' }}
+            </button>
+          </template>
+        </s-goods-column>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup>
+  /**
+   * 拼团
+   */
+  import { computed, onMounted, ref } from 'vue';
+  import sheep from '@/sheep';
+  import SpuApi from "@/sheep/api/product/spu";
+  import CombinationApi from "@/sheep/api/promotion/combination";
+
+  // 接收参数
+  const props = defineProps({
+    data: {
+      type: Object,
+      default() {},
+    },
+    styles: {
+      type: Object,
+      default() {},
+    },
+  });
+
+  let { layoutType, tagStyle, btnBuy, space } = props.data;
+  let { marginLeft, marginRight } = props.styles;
+
+  // 购买按钮样式
+  const buyStyle = computed(() => {
+    let btnBuy = props.data.btnBuy;
+    if (btnBuy?.type === 'text') {
+      return {
+        background: `linear-gradient(to right, ${btnBuy.bgBeginColor}, ${btnBuy.bgEndColor})`,
+      };
+    }
+
+    if (btnBuy?.type === 'img') {
+      return {
+        width: '54rpx',
+        height: '54rpx',
+        background: `url(${sheep.$url.cdn(btnBuy.imgUrl)}) no-repeat`,
+        backgroundSize: '100% 100%',
+      };
+    }
+  });
+
+  const productList = ref([]);
+  onMounted(async () => {
+    // todo:@owen 与Yudao结构不一致,待重构
+    const { data: activity } = await CombinationApi.getCombinationActivity(props.data.activityId);
+    const { data: spu } = await SpuApi.getSpuDetail(activity.spuId)
+    productList.value = [spu];
+  });
+</script>
+
+<style lang="scss" scoped>
+  .goods-list {
+    position: relative;
+    .cart-btn {
+      position: absolute;
+      bottom: 10rpx;
+      right: 20rpx;
+      z-index: 11;
+      height: 50rpx;
+      line-height: 50rpx;
+      padding: 0 20rpx;
+      border-radius: 25rpx;
+      font-size: 24rpx;
+      color: #fff;
+    }
+  }
+  .goods-list {
+    &:nth-last-of-type(1) {
+      margin-bottom: 0 !important;
+    }
+  }
+  .goods-sm-box {
+    margin: 0 auto;
+    box-sizing: border-box;
+    .goods-card-box {
+      flex-shrink: 0;
+      overflow: hidden;
+      width: 33.3%;
+      box-sizing: border-box;
+    }
+  }
+</style>

+ 46 - 0
sheep/components/s-hotzone-block/s-hotzone-block.vue

@@ -0,0 +1,46 @@
+<!-- 装修图文组件:热区 -->
+<template>
+  <view class="hotzone-wrap">
+    <image :src="sheep.$url.cdn(data.imgUrl)" style="width: 100%" mode="widthFix"></image>
+    <view
+      class="hotzone-box"
+      v-for="(item, index) in data.list"
+      :key="index"
+      :style="[
+        {
+          top: `${item.top}px`,
+          left: `${item.left}px`,
+          width: `${item.width}px`,
+          height: `${item.height}px`,
+        },
+      ]"
+      @tap.stop="sheep.$router.go(item.url)"
+    >
+    </view>
+  </view>
+</template>
+
+<script setup>
+  import sheep from '@/sheep';
+
+  // 接收参数
+  const props = defineProps({
+    data: {
+      type: Object,
+      default: () => ({}),
+    },
+    styles: {
+      type: Object,
+      default: () => ({}),
+    },
+  });
+</script>
+
+<style lang="scss" scoped>
+  .hotzone-wrap {
+    position: relative;
+  }
+  .hotzone-box {
+    position: absolute;
+  }
+</style>

+ 44 - 0
sheep/components/s-image-banner/s-image-banner.vue

@@ -0,0 +1,44 @@
+<!-- 装修图文组件:图片轮播 -->
+<template>
+  <su-swiper
+    :list="imgList"
+    :dotStyle="data.indicator === 'dot' ? 'long' : 'tag'"
+    imageMode="scaleToFill"
+    dotCur="bg-mask-40"
+    :seizeHeight="300"
+    :autoplay="data.autoplay"
+    :interval="data.interval * 1000"
+    :mode="data.type"
+  />
+</template>
+
+<script setup>
+  import { computed } from 'vue';
+  import sheep from '@/sheep';
+
+  // 轮播图
+  const props = defineProps({
+    data: {
+      type: Object,
+      default: () => ({}),
+    },
+    styles: {
+      type: Object,
+      default: () => ({}),
+    },
+  });
+
+  const imgList = computed(() =>
+      props.data.items.map((item) => {
+        const src = item.type === 'img' ? item.imgUrl : item.videoUrl;
+        return {
+          ...item,
+          type: item.type === 'img' ? 'image' : 'video',
+          src: sheep.$url.cdn(src),
+          poster: sheep.$url.cdn(item.imgUrl),
+        };
+      }),
+  );
+</script>
+
+<style></style>

+ 27 - 0
sheep/components/s-image-block/s-image-block.vue

@@ -0,0 +1,27 @@
+<!-- 装修图文组件:图片展示 -->
+<template>
+  <view @tap="sheep.$router.go(data?.url)">
+    <su-image :src="sheep.$url.cdn(data.imgUrl)" mode="widthFix" />
+  </view>
+</template>
+
+<script setup>
+  /**
+   * 图片组件
+   */
+  import sheep from '@/sheep';
+
+  // 接收参数
+  const props = defineProps({
+    data: {
+      type: Object,
+      default: () => ({}),
+    },
+    styles: {
+      type: Object,
+      default: () => ({}),
+    },
+  });
+</script>
+
+<style lang="scss" scoped></style>

+ 110 - 0
sheep/components/s-image-cube/s-image-cube.vue

@@ -0,0 +1,110 @@
+<!-- 装修图文组件:广告魔方 -->
+<template>
+  <view class="ss-cube-wrap" :style="[parseAdWrap]">
+    <view v-for="(item, index) in data.list" :key="index">
+      <view
+        class="cube-img-wrap"
+        :style="[parseImgStyle(item), { margin: data.space + 'px' }]"
+        @tap="sheep.$router.go(item.url)"
+      >
+        <image class="cube-img" :src="sheep.$url.cdn(item.imgUrl)" mode="aspectFill"></image>
+      </view>
+    </view>
+  </view>
+</template>
+<script setup>
+  /**
+/**
+ * 广告魔方
+ *
+ * @property {Array<Object>} list 			- 魔方列表
+ * @property {Object} styles 				- 组件样式
+ * @property {String} background 			- 组件背景色
+ * @property {Number} topSpace 				- 组件顶部间距
+ * @property {Number} bottomSpace 			- 组件底部间距
+ * @property {Number} leftSpace 			- 容器左间距
+ * @property {Number} rightSpace 			- 容器右间距
+ * @property {Number} imgSpace 				- 图片间距
+ * @property {Number} imgTopRadius 			- 图片上圆角
+ * @property {Number} imgBottomRadius 		- 图片下圆角
+ *
+ */
+
+  import { computed, inject, unref } from 'vue';
+  import sheep from '@/sheep';
+
+  // 参数
+  const props = defineProps({
+    data: {
+      type: Object,
+      default() {},
+    },
+    styles: {
+      type: Object,
+      default() {},
+    },
+  });
+
+  // 单元格大小
+  const windowWidth = sheep.$platform.device.windowWidth;
+  const cell = computed(() => {
+    return (
+      (windowWidth -
+        ((props.styles.marginLeft || 0) + (props.styles.marginRight || 0) + (props.styles.padding || 0) * 2)) /
+      4
+    );
+  });
+
+  //包裹容器高度
+  const parseAdWrap = computed(() => {
+    let heightArr = props.data.list.reduce(
+      (prev, cur) => (prev.includes(cur.height + cur.top) ? prev : [...prev, cur.height + cur.top]),
+      [],
+    );
+    let heightMax = Math.max(...heightArr);
+    return {
+      height: heightMax * cell.value + 'px',
+      width:
+        windowWidth -
+        (props.data?.style?.marginLeft +
+          props.data?.style?.marginRight +
+          props.styles.padding * 2) *
+          2 +
+        'px',
+    };
+  });
+
+  // 解析图片大小位置
+  const parseImgStyle = (item) => {
+    let obj = {
+      width: item.width * cell.value - props.data.space + 'px',
+      height: item.height * cell.value - props.data.space + 'px',
+      left: item.left * cell.value + 'px',
+      top: item.top * cell.value + 'px',
+      'border-top-left-radius': props.data.borderRadiusTop + 'px',
+      'border-top-right-radius': props.data.borderRadiusTop + 'px',
+      'border-bottom-left-radius': props.data.borderRadiusBottom + 'px',
+      'border-bottom-right-radius': props.data.borderRadiusBottom + 'px',
+    };
+    return obj;
+  };
+</script>
+
+<style lang="scss" scoped>
+  .ss-cube-wrap {
+    position: relative;
+    z-index: 2;
+    width: 750rpx;
+  }
+
+  .cube-img-wrap {
+    position: absolute;
+    z-index: 3;
+    overflow: hidden;
+  }
+
+  .cube-img {
+    width: 100%;
+    height: 100%;
+  }
+</style>

+ 242 - 0
sheep/components/s-layout/s-layout.vue

@@ -0,0 +1,242 @@
+<template>
+  <view
+    class="page-app"
+    :class="['theme-' + sys.mode, 'main-' + sys.theme, 'font-' + sys.fontSize]"
+  >
+    <view class="page-main" :style="[bgMain]">
+      <!-- 顶部导航栏-情况1:默认通用顶部导航栏 -->
+      <su-navbar
+        v-if="navbar === 'normal'"
+        :title="title"
+        statusBar
+        :color="color"
+        :tools="tools"
+        :opacityBgUi="opacityBgUi"
+        @search="(e) => emits('search', e)"
+        :defaultSearch="defaultSearch"
+      />
+
+      <!-- 顶部导航栏-情况2:装修组件导航栏-标准 -->
+      <s-custom-navbar
+        v-else-if="navbar === 'custom' && navbarMode === 'normal'"
+        :data="navbarStyle"
+        :showLeftButton="showLeftButton"
+      />
+      <view class="page-body" :style="[bgBody]">
+        <!-- 顶部导航栏-情况3:沉浸式头部 -->
+        <su-inner-navbar v-if="navbar === 'inner'" :title="title" />
+        <view
+          v-if="navbar === 'inner'"
+          :style="[{ paddingTop: sheep.$platform.navbar + 'px' }]"
+        ></view>
+
+        <!-- 顶部导航栏-情况4:装修组件导航栏-沉浸式 -->
+        <s-custom-navbar v-if="navbar === 'custom' && navbarMode === 'inner'" :data="navbarStyle" :showLeftButton="showLeftButton" />
+
+        <!-- 页面内容插槽 -->
+        <slot />
+
+        <!-- 底部导航 -->
+        <s-tabbar v-if="tabbar !== ''" :path="tabbar" />
+      </view>
+    </view>
+
+    <view class="page-modal">
+      <!-- 全局授权弹窗 -->
+      <s-auth-modal />
+      <!-- 全局分享弹窗 -->
+      <s-share-modal :shareInfo="shareInfo" />
+      <!-- 全局快捷入口 -->
+      <s-menu-tools />
+    </view>
+  </view>
+</template>
+
+<script setup>
+  /**
+   * 模板组件 - 提供页面公共组件,属性,方法
+   */
+  import { computed, reactive, ref } from 'vue';
+  import sheep from '@/sheep';
+  import { isEmpty } from 'lodash';
+  import { onShow } from '@dcloudio/uni-app';
+  // #ifdef MP-WEIXIN
+  import { onShareAppMessage } from '@dcloudio/uni-app';
+  // #endif
+
+  const props = defineProps({
+    title: {
+      type: String,
+      default: '',
+    },
+    navbar: {
+      type: String,
+      default: 'normal',
+    },
+    opacityBgUi: {
+      type: String,
+      default: 'bg-white',
+    },
+    color: {
+      type: String,
+      default: '',
+    },
+    tools: {
+      type: String,
+      default: 'title',
+    },
+    keyword: {
+      type: String,
+      default: '',
+    },
+    navbarStyle: {
+      type: Object,
+      default: () => ({
+        styleType: '',
+        type: '',
+        color: '',
+        src: '',
+        list: [],
+        alwaysShow: 0,
+      }),
+    },
+    bgStyle: {
+      type: Object,
+      default: () => ({
+        src: '',
+        color: 'var(--ui-BG-1)',
+      }),
+    },
+    tabbar: {
+      type: [String, Boolean],
+      default: '',
+    },
+    onShareAppMessage: {
+      type: [Boolean, Object],
+      default: true,
+    },
+    leftWidth: {
+      type: [Number, String],
+      default: 100,
+    },
+    rightWidth: {
+      type: [Number, String],
+      default: 100,
+    },
+    defaultSearch: {
+      type: String,
+      default: '',
+    },
+    //展示返回按钮
+    showLeftButton: {
+      type: Boolean,
+      default: false,
+    },
+  });
+  const emits = defineEmits(['search']);
+
+  const sysStore = sheep.$store('sys');
+  const userStore = sheep.$store('user');
+  const appStore = sheep.$store('app');
+  const modalStore = sheep.$store('modal');
+  const sys = computed(() => sysStore);
+
+  // 导航栏模式(因为有自定义导航栏 需要计算)
+  const navbarMode = computed(() => {
+    if (props.navbar === 'normal' || props.navbarStyle.styleType === 'normal') {
+      return 'normal';
+    }
+    return 'inner';
+  });
+
+  // 背景1
+  const bgMain = computed(() => {
+    if (navbarMode.value === 'inner') {
+      return {
+        background: `${props.bgStyle.backgroundColor} url(${sheep.$url.cdn(
+          props.bgStyle.backgroundImage,
+        )}) no-repeat top center / 100% auto`,
+      };
+    }
+    return {};
+  });
+
+  // 背景2
+  const bgBody = computed(() => {
+    if (navbarMode.value === 'normal') {
+      return {
+        background: `${props.bgStyle.backgroundColor} url(${sheep.$url.cdn(
+          props.bgStyle.backgroundImage,
+        )}) no-repeat top center / 100% auto`,
+      };
+    }
+    return {};
+  });
+
+  // 分享信息
+  const shareInfo = computed(() => {
+    if (props.onShareAppMessage === true) {
+      return sheep.$platform.share.getShareInfo();
+    } else {
+      if (!isEmpty(props.onShareAppMessage)) {
+        sheep.$platform.share.updateShareInfo(props.onShareAppMessage);
+        return props.onShareAppMessage;
+      }
+    }
+    return {};
+  });
+
+  // #ifdef MP-WEIXIN
+  // 微信小程序分享
+  onShareAppMessage(() => {
+    return {
+      title: shareInfo.value.title,
+      path: shareInfo.value.path,
+      imageUrl: shareInfo.value.image,
+    };
+  });
+  // #endif
+
+  onShow(() => {
+    if (!isEmpty(shareInfo.value)) {
+      sheep.$platform.share.updateShareInfo(shareInfo.value);
+    }
+  });
+</script>
+
+<style lang="scss" scoped>
+  .page-app {
+    position: relative;
+    color: var(--ui-TC);
+    background-color: var(--ui-BG-1) !important;
+    z-index: 2;
+    display: flex;
+    width: 100%;
+    height: 100vh;
+
+    .page-main {
+      position: absolute;
+      z-index: 1;
+      width: 100%;
+      min-height: 100%;
+      display: flex;
+      flex-direction: column;
+
+      .page-body {
+        width: 100%;
+        position: relative;
+        z-index: 1;
+        flex: 1;
+      }
+
+      .page-img {
+        width: 100vw;
+        height: 100vh;
+        position: absolute;
+        top: 0;
+        left: 0;
+        z-index: 0;
+      }
+    }
+  }
+</style>

+ 15 - 0
sheep/components/s-line-block/s-line-block.vue

@@ -0,0 +1,15 @@
+<!-- 装修基础组件:分割线 -->
+<template>
+  <su-subline v-bind="data"></su-subline>
+</template>
+
+<script setup>
+  const props = defineProps({
+    data: {
+      type: Object,
+      default: {},
+    },
+  });
+</script>
+
+<style></style>

+ 144 - 0
sheep/components/s-live-block/s-live-block.vue

@@ -0,0 +1,144 @@
+<template>
+  <view>
+    <view
+      v-if="mode === 2 && state.liveList.length"
+      class="goods-md-wrap ss-flex ss-flex-wrap ss-col-top"
+      :style="[{ margin: '-' + data.space + 'rpx' }]"
+    >
+      <view
+        :style="[
+          {
+            padding: data.space + 'rpx',
+          },
+        ]"
+        class="goods-list-box"
+        v-for="item in state.liveList"
+        :key="item.id"
+      >
+        <s-live-card
+          class="goods-md-box"
+          size="md"
+          :goodsFields="goodsFields"
+          :data="item"
+          :titleColor="goodsFields.name?.color"
+          :subTitleColor="goodsFields.anchor_name?.color"
+          :topRadius="data.borderRadiusTop"
+          :bottomRadius="data.borderRadiusBottom"
+          @click="goRoom(item.roomid)"
+        >
+        </s-live-card>
+      </view>
+    </view>
+    <view v-if="mode === 1 && state.liveList.length" class="goods-lg-box">
+      <view
+        class="goods-box"
+        :style="[{ marginBottom: data.space + 'px' }]"
+        v-for="item in state.liveList"
+        :key="item.id"
+      >
+        <s-live-card
+          class="goods-card"
+          size="sl"
+          :goodsFields="goodsFields"
+          :data="item"
+          :titleColor="goodsFields.name?.color"
+          :subTitleColor="goodsFields.anchor_name.color"
+          :topRadius="data.borderRadiusTop"
+          :bottomRadius="data.borderRadiusBottom"
+          @tap="goRoom(item.roomid)"
+        >
+        </s-live-card>
+      </view>
+    </view>
+  </view>
+</template>
+<script setup>
+  import { reactive, onMounted } from 'vue';
+  import sheep from '@/sheep';
+
+  const state = reactive({
+    liveList: [],
+    mpLink: '',
+  });
+  const props = defineProps({
+    data: {
+      type: Object,
+      default() {},
+    },
+    styles: {
+      type: Object,
+      default() {},
+    },
+  });
+  const { mode, goodsFields, mpliveIds } = props.data ?? {};
+  const { marginLeft, marginRight } = props.styles ?? {};
+
+  async function getLiveListByIds(ids) {
+    const { data } = await sheep.$api.app.mplive.getRoomList(ids);
+    return data;
+  }
+  function goRoom(id) {
+    // #ifdef MP-WEIXIN
+    uni.navigateTo({
+      url: `plugin-private://wx2b03c6e691cd7370/pages/live-player-plugin?room_id=${id}`,
+    });
+    // #endif
+
+    // #ifndef MP-WEIXIN
+    uni.showModal({
+      title: '提示',
+      confirmText: '允许',
+      content: '将打开小程序访问',
+      success: async function (res) {
+        if (res.confirm) {
+          getMpLink();
+        }
+      },
+    });
+    // #endif
+  }
+
+  function goMpLink() {
+    // #ifdef H5
+    window.location = state.mpLink;
+    // #endif
+    // #ifdef APP-PLUS
+    plus.runtime.openURL(state.mpLink);
+    // #endif
+  }
+
+  async function getMpLink() {
+    // #ifndef MP-WEIXIN
+    if (state.mpLink === '') {
+      const { error, data } = await sheep.$api.app.mplive.getMpLink();
+      if (error === 0) {
+        state.mpLink = data;
+      }
+    }
+    goMpLink();
+    // #endif
+  }
+
+  onMounted(async () => {
+    state.liveList = await getLiveListByIds(mpliveIds);
+  });
+</script>
+<style lang="scss" scoped>
+  .goods-list-box {
+    width: 50%;
+    flex-shrink: 0;
+    box-sizing: border-box;
+    overflow: hidden;
+  }
+
+  .goods-box {
+    &:nth-last-of-type(1) {
+      margin-bottom: 0 !important;
+    }
+  }
+
+  .goods-md-box,
+  .goods-sl-box {
+    position: relative;
+  }
+</style>

+ 234 - 0
sheep/components/s-live-card/s-live-card.vue

@@ -0,0 +1,234 @@
+<template>
+  <view>
+    <!-- md卡片:竖向,一行放两个,图上内容下 -->
+    <view v-if="size === 'md'" class="md-goods-card ss-flex-col" :style="[elStyles]" @tap="onClick">
+      <view class="icon-box ss-flex">
+        <image class="icon" :src="state.liveStatus[data.status].img"></image>
+        <view class="title ss-m-l-16">{{ state.liveStatus[data.status].title }}</view>
+      </view>
+      <img class="md-img-box" :src="sheep.$url.cdn(data.feeds_img)" referrerpolicy="no-referrer">
+      <view class="md-goods-content">
+        <view class="md-goods-title ss-line-1" :style="[{ color: titleColor }]">
+          {{ data.name }}
+        </view>
+        <view class="md-goods-subtitle ss-m-t-14 ss-line-1" :style="[{ color: subTitleColor }]">
+          主播:{{ data.anchor_name }}
+        </view>
+      </view>
+    </view>
+    <!-- sl卡片:竖向型,一行放一个,图片上内容下边 -->
+    <view v-if="size === 'sl'" class="sl-goods-card ss-flex-col" :style="[elStyles]" @tap="onClick">
+      <view class="icon-box ss-flex">
+        <image class="icon" :src="state.liveStatus[data.status].img"></image>
+        <view class="title ss-m-l-16">{{ state.liveStatus[data.status].title }}</view>
+      </view>
+      <img class="sl-img-box" :src="sheep.$url.cdn(data.feeds_img)" referrerpolicy="no-referrer">
+      <view class="sl-goods-content">
+        <view class="sl-goods-title ss-line-1" :style="[{ color: titleColor }]">
+          {{ data.name }}
+        </view>
+        <view class="sl-goods-subtitle ss-m-t-14 ss-line-1" :style="[{ color: subTitleColor }]">
+          主播:{{ data.anchor_name }}
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+<script setup>
+  import { computed, reactive } from 'vue';
+  import sheep from '@/sheep';
+  /**
+   * 直播卡片
+   *
+   * @property {String} img 											- 图片
+   * @property {String} title 										- 标题
+   * @property {Number} titleWidth = 0								- 标题宽度,默认0,单位rpx
+   * @property {String} skuText 										- 规格
+   * @property {String | Number} score 								- 积分
+   * @property {String | Number} price 								- 价格
+   * @property {String | Number} originalPrice 						- 单购价
+   * @property {String} priceColor 									- 价格颜色
+   * @property {Number | String} num									- 数量
+   *
+   */
+  const props = defineProps({
+    goodsFields: {
+      type: [Array, Object],
+      default() {
+        return {};
+      },
+    },
+    tagStyle: {
+      type: Object,
+      default: {},
+    },
+    data: {
+      type: Object,
+      default: {},
+    },
+    size: {
+      type: String,
+      default: 'sl',
+    },
+    background: {
+      type: String,
+      default: '',
+    },
+    topRadius: {
+      type: Number,
+      default: 0,
+    },
+    bottomRadius: {
+      type: Number,
+      default: 0,
+    },
+    titleColor: {
+      type: String,
+      default: '#333',
+    },
+    subTitleColor: {
+      type: String,
+      default: '#999999',
+    },
+  });
+  // 组件样式
+  const elStyles = computed(() => {
+    return {
+      background: props.background,
+      'border-top-left-radius': props.topRadius + 'px',
+      'border-top-right-radius': props.topRadius + 'px',
+      'border-bottom-left-radius': props.bottomRadius + 'px',
+      'border-bottom-right-radius': props.bottomRadius + 'px',
+    };
+  });
+  const state = reactive({
+    liveStatus: {
+      101: {
+        img: sheep.$url.static('/static/img/shop/app/mplive/living.png'),
+        title: '直播中',
+      },
+      102: {
+        img: sheep.$url.static('/static/img/shop/app/mplive/start.png'),
+        title: '未开始',
+      },
+      103: {
+        img: sheep.$url.static('/static/img/shop/app/mplive/ended.png'),
+        title: '已结束',
+      },
+    },
+  });
+  const emits = defineEmits(['click', 'getHeight']);
+  const onClick = () => {
+    emits('click');
+  };
+</script>
+
+<style lang="scss" scoped>
+  // md
+  .md-goods-card {
+    overflow: hidden;
+    width: 100%;
+    height: 424rpx;
+    position: relative;
+    z-index: 1;
+    background-color: $white;
+    .icon-box {
+      position: absolute;
+      left: 20rpx;
+      top: 10rpx;
+      width: 136rpx;
+      height: 40rpx;
+      background: rgba(#000000, 0.5);
+      border-radius: 20rpx;
+      z-index: 1;
+      .icon {
+        width: 40rpx;
+        height: 40rpx;
+        border-radius: 20rpx 0px 20rpx 20rpx;
+      }
+      .title {
+        font-size: 24rpx;
+        font-weight: 500;
+        color: #ffffff;
+      }
+    }
+    .md-goods-content {
+      position: absolute;
+      left: 0;
+      bottom: 0;
+      padding: 20rpx;
+      width: 100%;
+      background: linear-gradient(360deg, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0.02) 100%);
+    }
+
+    .md-img-box {
+      width: 100%;
+      height: 100%;
+      object-fit: cover;
+    }
+
+    .md-goods-title {
+      font-size: 26rpx;
+      color: #333;
+      width: 100%;
+    }
+    .md-goods-subtitle {
+      font-size: 24rpx;
+      font-weight: 400;
+      color: #999999;
+    }
+  }
+  .sl-goods-card {
+    overflow: hidden;
+    position: relative;
+    z-index: 1;
+    width: 100%;
+    height: 400rpx;
+    background-color: $white;
+    .icon-box {
+      position: absolute;
+      left: 20rpx;
+      top: 10rpx;
+      width: 136rpx;
+      height: 40rpx;
+      background: rgba(#000000, 0.5);
+      border-radius: 20rpx;
+      z-index: 1;
+      .icon {
+        width: 40rpx;
+        height: 40rpx;
+        border-radius: 20rpx 0px 20rpx 20rpx;
+      }
+      .title {
+        font-size: 24rpx;
+        font-weight: 500;
+        color: #ffffff;
+      }
+    }
+    .sl-goods-content {
+      position: absolute;
+      left: 0;
+      bottom: 0;
+      padding: 20rpx;
+      width: 100%;
+      background: linear-gradient(360deg, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0.02) 100%);
+    }
+
+    .sl-img-box {
+      width: 100%;
+      height: 100%;
+      object-fit: cover;
+    }
+
+    .sl-goods-title {
+      font-size: 26rpx;
+      color: #333;
+      width: 100%;
+    }
+    .sl-goods-subtitle {
+      font-size: 24rpx;
+      font-weight: 400;
+      color: #999999;
+    }
+  }
+</style>

+ 363 - 0
sheep/components/s-menu-button/s-menu-button.vue

@@ -0,0 +1,363 @@
+<!-- 装修基础组件:菜单导航(金刚区) -->
+<template>
+  <!-- 包裹层 -->
+  <view
+    class="ui-swiper"
+    :class="[props.mode, props.bg, props.ui]"
+    :style="[{ height: swiperHeight + (menuList.length > 1 ? 50 : 0) + 'rpx' }]"
+  >
+    <!-- 轮播 -->
+    <swiper
+      :circular="props.circular"
+      :current="state.cur"
+      :autoplay="props.autoplay"
+      :interval="props.interval"
+      :duration="props.duration"
+      :style="[{ height: swiperHeight + 'rpx' }]"
+      @change="swiperChange"
+    >
+      <swiper-item
+        v-for="(arr, index) in menuList"
+        :key="index"
+        :class="{ cur: state.cur == index }"
+      >
+        <!-- 宫格 -->
+        <view class="grid-wrap">
+          <view
+            v-for="(item, index) in arr"
+            :key="index"
+            class="grid-item ss-flex ss-flex-col ss-col-center ss-row-center"
+            :style="[{ width: `${100 * (1 / data.column)}%`, height: '200rpx' }]"
+            hover-class="ss-hover-btn"
+            @tap="sheep.$router.go(item.url)"
+          >
+            <view class="menu-box ss-flex ss-flex-col ss-col-center ss-row-center">
+              <view
+                v-if="item.badge.show"
+                class="tag-box"
+                :style="[{ background: item.badge.bgColor, color: item.badge.textColor }]"
+              >
+                {{ item.badge.text }}
+              </view>
+              <image
+                v-if="item.iconUrl"
+                class="menu-icon"
+                :style="[
+                  {
+                    width: props.iconSize + 'rpx',
+                    height: props.iconSize + 'rpx',
+                  },
+                ]"
+                :src="sheep.$url.cdn(item.iconUrl)"
+                mode="aspectFill"
+              ></image>
+              <view
+                v-if="data.layout === 'iconText'"
+                class="menu-title"
+                :style="[{ color: item.titleColor }]"
+              >
+                {{ item.title }}
+              </view>
+            </view>
+          </view>
+        </view>
+      </swiper-item>
+    </swiper>
+    <!-- 指示点 -->
+    <template v-if="menuList.length > 1">
+      <view class="ui-swiper-dot" :class="props.dotStyle" v-if="props.dotStyle != 'tag'">
+        <view
+          class="line-box"
+          v-for="(item, index) in menuList.length"
+          :key="index"
+          :class="[state.cur == index ? 'cur' : '', props.dotCur]"
+        ></view>
+      </view>
+      <view class="ui-swiper-dot" :class="props.dotStyle" v-if="props.dotStyle == 'tag'">
+        <view class="ui-tag radius" :class="[props.dotCur]" style="pointer-events: none">
+          <view style="transform: scale(0.7)">{{ state.cur + 1 }} / {{ menuList.length }}</view>
+        </view>
+      </view>
+    </template>
+  </view>
+</template>
+
+<script setup>
+  /**
+   * 轮播menu
+   *
+   * @property {Boolean} circular = false  		- 是否采用衔接滑动,即播放到末尾后重新回到开头
+   * @property {Boolean} autoplay = true  		- 是否自动切换
+   * @property {Number} interval = 5000  			- 自动切换时间间隔
+   * @property {Number} duration = 500  			- 滑动动画时长,app-nvue不支持
+   * @property {Array} list = [] 					- 轮播数据
+   * @property {String} ui = ''  					- 样式class
+   * @property {String} mode  					- 模式
+   * @property {String} dotStyle  				- 指示点样式
+   * @property {String} dotCur= 'ui-BG-Main' 		- 当前指示点样式,默认主题色
+   * @property {String} bg  						- 背景
+   *
+   * @property {String|Number} col = 4  			- 一行数量
+   *  @property {String|Number} row = 1 			- 几行
+   * @property {String} hasBorder 				- 是否有边框
+   * @property {String} borderColor 				- 边框颜色
+   * @property {String} background		  		- 背景
+   * @property {String} hoverClass 				- 按压样式类
+   * @property {String} hoverStayTime 		  	- 动画时间
+   *
+   * @property {Array} list 		  				- 导航列表
+   * @property {Number} iconSize 		  			- 图标大小
+   * @property {String} color 		  			- 标题颜色
+   *
+   */
+
+  import { reactive, computed } from 'vue';
+  import sheep from '@/sheep';
+
+  // 数据
+  const state = reactive({
+    cur: 0,
+  });
+
+  // 接收参数
+
+  const props = defineProps({
+    data: {
+      type: Object,
+      default() {},
+    },
+    styles: {
+      type: Object,
+      default() {},
+    },
+    circular: {
+      type: Boolean,
+      default: true,
+    },
+    autoplay: {
+      type: Boolean,
+      default: false,
+    },
+    interval: {
+      type: Number,
+      default: 5000,
+    },
+    duration: {
+      type: Number,
+      default: 500,
+    },
+
+    ui: {
+      type: String,
+      default: '',
+    },
+    mode: {
+      //default
+      type: String,
+      default: 'default',
+    },
+    dotStyle: {
+      type: String,
+      default: 'long', //default long tag
+    },
+    dotCur: {
+      type: String,
+      default: 'ui-BG-Main',
+    },
+    bg: {
+      type: String,
+      default: 'bg-none',
+    },
+    height: {
+      type: Number,
+      default: 300,
+    },
+
+    // 是否有边框
+    hasBorder: {
+      type: Boolean,
+      default: true,
+    },
+    // 边框颜色
+    borderColor: {
+      type: String,
+      default: 'red',
+    },
+    background: {
+      type: String,
+      default: 'blue',
+    },
+    hoverClass: {
+      type: String,
+      default: 'ss-hover-class', //'none'为没有hover效果
+    },
+    // 一排宫格数
+    col: {
+      type: [Number, String],
+      default: 3,
+    },
+    iconSize: {
+      type: Number,
+      default: 80,
+    },
+    color: {
+      type: String,
+      default: '#000',
+    },
+  });
+
+  // 生成数据
+  const menuList = computed(() => splitData(props.data.list, props.data.row * props.data.column));
+  const swiperHeight = computed(() => props.data.row * (props.data.layout === 'iconText' ? 200 : 180));
+  const windowWidth = sheep.$platform.device.windowWidth;
+
+  // current 改变时会触发 change 事件
+  const swiperChange = (e) => {
+    state.cur = e.detail.current;
+  };
+
+  // 重组数据
+  const splitData = (oArr = [], length = 1) => {
+    let arr = [];
+    let minArr = [];
+    oArr.forEach((c) => {
+      if (minArr.length === length) {
+        minArr = [];
+      }
+      if (minArr.length === 0) {
+        arr.push(minArr);
+      }
+      minArr.push(c);
+    });
+
+    return arr;
+  };
+</script>
+
+<style lang="scss" scoped>
+  .grid-wrap {
+    width: 100%;
+    display: flex;
+    position: relative;
+    box-sizing: border-box;
+    overflow: hidden;
+    flex-wrap: wrap;
+    align-items: center;
+  }
+  .menu-box {
+    position: relative;
+    z-index: 1;
+    transform: translate(0, 0);
+
+    .tag-box {
+      position: absolute;
+      z-index: 2;
+      top: 0;
+      right: -6rpx;
+      font-size: 2em;
+      line-height: 1;
+      padding: 0.4em 0.6em 0.3em;
+      transform: scale(0.4) translateX(0.5em) translatey(-0.6em);
+      transform-origin: 100% 0;
+      border-radius: 200rpx;
+      white-space: nowrap;
+    }
+
+    .menu-icon {
+      transform: translate(0, 0);
+      width: 80rpx;
+      height: 80rpx;
+      padding-bottom: 10rpx;
+    }
+
+    .menu-title {
+      font-size: 24rpx;
+      color: #333;
+    }
+  }
+
+  ::v-deep(.ui-swiper) {
+    position: relative;
+    z-index: 1;
+
+    .ui-swiper-dot {
+      position: absolute;
+      width: 100%;
+      bottom: 20rpx;
+      height: 30rpx;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      z-index: 2;
+
+      &.default .line-box {
+        display: inline-flex;
+        border-radius: 50rpx;
+        width: 6px;
+        height: 6px;
+        border: 2px solid transparent;
+        margin: 0 10rpx;
+        opacity: 0.3;
+        position: relative;
+        justify-content: center;
+        align-items: center;
+
+        &.cur {
+          width: 8px;
+          height: 8px;
+          opacity: 1;
+          border: 0px solid transparent;
+        }
+
+        &.cur::after {
+          content: '';
+          border-radius: 50rpx;
+          width: 4px;
+          height: 4px;
+          background-color: #fff;
+        }
+      }
+
+      &.long .line-box {
+        display: inline-block;
+        border-radius: 100rpx;
+        width: 6px;
+        height: 6px;
+        margin: 0 10rpx;
+        opacity: 0.3;
+        position: relative;
+
+        &.cur {
+          width: 24rpx;
+          opacity: 1;
+        }
+
+        &.cur::after {
+        }
+      }
+
+      &.line {
+        bottom: 20rpx;
+
+        .line-box {
+          display: inline-block;
+          width: 30px;
+          height: 3px;
+          opacity: 0.3;
+          position: relative;
+
+          &.cur {
+            opacity: 1;
+          }
+        }
+      }
+
+      &.tag {
+        justify-content: flex-end;
+        position: absolute;
+        bottom: 20rpx;
+        right: 20rpx;
+      }
+    }
+  }
+</style>

+ 82 - 0
sheep/components/s-menu-grid/s-menu-grid.vue

@@ -0,0 +1,82 @@
+<!-- 装修基础组件:宫格导航 -->
+<template>
+  <uni-grid :showBorder="Boolean(data.border)" :column="data.column">
+    <uni-grid-item
+      v-for="(item, index) in data.list"
+      :key="index"
+      @tap="sheep.$router.go(item.url)"
+    >
+      <view class="grid-item-box ss-flex ss-flex-col ss-row-center ss-col-center">
+        <view class="img-box">
+          <view
+            class="tag-box"
+            v-if="item.badge.show"
+            :style="[{ background: item.badge.bgColor, color: item.badge.textColor }]"
+          >
+            {{ item.badge.text }}
+          </view>
+          <image class="menu-image" :src="sheep.$url.cdn(item.iconUrl)"></image>
+        </view>
+
+        <view class="title-box ss-flex ss-flex-col ss-row-center ss-col-center">
+          <view class="grid-text" :style="[{ color: item.titleColor }]">
+            {{ item.title }}
+          </view>
+          <view class="grid-tip" :style="[{ color: item.subtitleColor }]">
+            {{ item.subtitle }}
+          </view>
+        </view>
+      </view>
+    </uni-grid-item>
+  </uni-grid>
+</template>
+
+<script setup>
+  import sheep from '@/sheep';
+
+  const props = defineProps({
+    data: {
+      type: Object,
+      default() {},
+    },
+  });
+</script>
+
+<style lang="scss" scoped>
+  .menu-image {
+    width: 24px;
+    height: 24px;
+  }
+  .grid-item-box {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    height: 100%;
+    .img-box {
+      position: relative;
+      .tag-box {
+        position: absolute;
+        z-index: 2;
+        top: 0;
+        right: 0;
+        font-size: 2em;
+        line-height: 1;
+        padding: 0.4em 0.6em 0.3em;
+        transform: scale(0.4) translateX(0.5em) translatey(-0.6em);
+        transform-origin: 100% 0;
+        border-radius: 200rpx;
+        white-space: nowrap;
+      }
+    }
+
+    .title-box {
+      .grid-tip {
+        font-size: 24rpx;
+        white-space: nowrap;
+        text-align: center;
+      }
+    }
+  }
+</style>

+ 66 - 0
sheep/components/s-menu-list/s-menu-list.vue

@@ -0,0 +1,66 @@
+<!-- 装修基础组件:列表导航 -->
+<template>
+  <view class="menu-list-wrap">
+    <uni-list :border="true">
+      <uni-list-item
+        v-for="(item, index) in data.list"
+        :key="index"
+        showArrow
+        clickable
+        @tap="sheep.$router.go(item.url)"
+      >
+        <template v-slot:header>
+          <view class="ss-flex ss-col-center">
+            <image
+              v-if="item.iconUrl"
+              class="list-icon"
+              :src="sheep.$url.cdn(item.iconUrl)"
+              mode="aspectFit"
+            ></image>
+            <view
+              class="title-text ss-flex ss-row-center ss-col-center ss-m-l-20"
+              :style="[{ color: item.titleColor }]"
+            >
+              {{ item.title }}
+            </view>
+          </view>
+        </template>
+        <template v-slot:footer>
+          <view
+            class="notice-text ss-flex ss-row-center ss-col-center"
+            :style="[{ color: item.subtitleColor }]"
+          >
+            {{ item.subtitle }}
+          </view>
+        </template>
+      </uni-list-item>
+    </uni-list>
+  </view>
+</template>
+
+<script setup>
+  /**
+   * cell
+   */
+  import sheep from '@/sheep';
+  const props = defineProps({
+    data: {
+      type: Object,
+      default: () => ({}),
+    },
+  });
+</script>
+
+<style lang="scss">
+  .list-icon {
+    width: 20px;
+    height: 20px;
+  }
+  .notice-text {
+  }
+  .menu-list-wrap {
+    ::v-deep .uni-list {
+      background-color: transparent;
+    }
+  }
+</style>

+ 118 - 0
sheep/components/s-menu-tools/s-menu-tools.vue

@@ -0,0 +1,118 @@
+<!-- 全局 - 快捷入口 -->
+<template>
+  <su-popup :show="show" type="top" round="20" backgroundColor="#F0F0F0" @close="closeMenuTools">
+    <su-status-bar />
+    <view class="tools-wrap ss-m-x-30 ss-m-b-16">
+      <view class="title ss-m-b-34 ss-p-t-20">快捷菜单</view>
+      <view class="container-list ss-flex ss-flex-wrap">
+        <view class="list-item ss-m-b-24" v-for="item in list" :key="item.title">
+          <view class="ss-flex-col ss-col-center">
+            <button
+              class="ss-reset-button list-image ss-flex ss-row-center ss-col-center"
+              @tap="onClick(item)"
+            >
+              <image v-if="show" :src="sheep.$url.static(item.icon)" class="list-icon" />
+            </button>
+            <view class="list-title ss-m-t-20">{{ item.title }}</view>
+          </view>
+        </view>
+      </view>
+    </view>
+  </su-popup>
+</template>
+
+<script setup>
+  import { reactive, computed } from 'vue';
+  import sheep from '@/sheep';
+  import { showMenuTools, closeMenuTools } from '@/sheep/hooks/useModal';
+
+  const show = computed(() => sheep.$store('modal').menu);
+
+  function onClick(item) {
+    closeMenuTools();
+    if (item.url) sheep.$router.go(item.url);
+  }
+
+  const list = [
+    {
+      url: '/pages/index/index',
+      icon: '/static/img/shop/tools/home.png',
+      title: '首页',
+    },
+    {
+      url: '/pages/index/search',
+      icon: '/static/img/shop/tools/search.png',
+      title: '搜索',
+    },
+    {
+      url: '/pages/index/user',
+      icon: '/static/img/shop/tools/user.png',
+      title: '个人中心',
+    },
+    {
+      url: '/pages/index/cart',
+      icon: '/static/img/shop/tools/cart.png',
+      title: '购物车',
+    },
+    {
+      url: '/pages/user/goods-log',
+      icon: '/static/img/shop/tools/browse.png',
+      title: '浏览记录',
+    },
+    {
+      url: '/pages/user/goods-collect',
+      icon: '/static/img/shop/tools/collect.png',
+      title: '我的收藏',
+    },
+    {
+      url: '/pages/chat/index',
+      icon: '/static/img/shop/tools/service.png',
+      title: '客服',
+    },
+  ];
+</script>
+
+<style lang="scss" scoped>
+  .tools-wrap {
+    // background: #F0F0F0;
+    // box-shadow: 0px 0px 28rpx 7rpx rgba(0, 0, 0, 0.13);
+    // opacity: 0.98;
+    // border-radius: 0 0 20rpx 20rpx;
+
+    .title {
+      font-size: 36rpx;
+      font-weight: bold;
+      color: #333333;
+    }
+
+    .list-item {
+      width: calc(25vw - 20rpx);
+
+      .list-image {
+        width: 104rpx;
+        height: 104rpx;
+        border-radius: 52rpx;
+        background: var(--ui-BG);
+
+        .list-icon {
+          width: 54rpx;
+          height: 54rpx;
+        }
+      }
+
+      .list-title {
+        font-size: 26rpx;
+        font-weight: 500;
+        color: #333333;
+      }
+    }
+  }
+
+  .uni-popup {
+    top: 0 !important;
+  }
+
+  :deep(.button-hover) {
+    background: #fafafa !important;
+  }
+</style>

+ 38 - 0
sheep/components/s-notice-block/s-notice-block.vue

@@ -0,0 +1,38 @@
+<template>
+  <view class="ss-flex ss-col-center notice-wrap">
+    <image class="icon-img" :src="sheep.$url.cdn(data.iconUrl)" mode="heightFix"></image>
+    <!-- todo:@owen 暂时只支持一个公告   -->
+    <su-notice-bar
+      style="flex: 1"
+      :showIcon="false"
+      scrollable
+      single
+      :text="data.contents[0].text"
+      :speed="50"
+      :color="data.textColor"
+      @tap="sheep.$router.go(data.contents[0].url)"
+    />
+  </view>
+</template>
+
+<script setup>
+  /**
+   * 装修组件  - 通知栏
+   *
+   */
+  import sheep from '@/sheep';
+  const props = defineProps({
+    data: {
+      type: Object,
+      default() {},
+    },
+  });
+</script>
+
+<style lang="scss" scoped>
+  .notice-wrap {
+    .icon-img {
+      height: 56rpx;
+    }
+  }
+</style>

+ 108 - 0
sheep/components/s-order-card/s-order-card.vue

@@ -0,0 +1,108 @@
+<!-- 装修用户组件:用户订单 -->
+<template>
+  <view class="ss-order-menu-wrap ss-flex ss-col-center">
+    <view
+      class="menu-item ss-flex-1 ss-flex-col ss-row-center ss-col-center"
+      v-for="item in orderMap"
+      :key="item.title"
+      @tap="sheep.$router.go(item.path, { type: item.value })"
+    >
+      <uni-badge
+        class="uni-badge-left-margin"
+        :text="numData.orderCount[item.count]"
+        absolute="rightTop"
+        size="small"
+      >
+        <image class="item-icon" :src="sheep.$url.static(item.icon)" mode="aspectFit" />
+      </uni-badge>
+      <view class="menu-title ss-m-t-28">{{ item.title }}</view>
+    </view>
+  </view>
+</template>
+
+<script setup>
+  /**
+   * 装修组件 - 订单菜单组
+   */
+  import sheep from '@/sheep';
+  import { computed } from 'vue';
+
+  const orderMap = [
+    {
+      title: '待付款',
+      value: '1',
+      icon: '/static/img/shop/order/no_pay.png',
+      path: '/pages/order/list',
+      type: 'unpaid',
+      count: 'unpaidCount',
+    },
+    {
+      title: '待收货',
+      value: '3',
+      icon: '/static/img/shop/order/no_take.png',
+      path: '/pages/order/list',
+      type: 'noget',
+      count: 'deliveredCount',
+    },
+    {
+      title: '待评价',
+      value: '4',
+      icon: '/static/img/shop/order/no_comment.png',
+      path: '/pages/order/list',
+      type: 'nocomment',
+      count: 'uncommentedCount',
+    },
+    {
+      title: '售后单',
+      value: '0',
+      icon: '/static/img/shop/order/change_order.png',
+      path: '/pages/order/aftersale/list',
+      type: 'aftersale',
+      count: 'afterSaleCount',
+    },
+    {
+      title: '全部订单',
+      value: '0',
+      icon: '/static/img/shop/order/all_order.png',
+      path: '/pages/order/list',
+    },
+  ];
+
+  const numData = computed(() => sheep.$store('user').numData);
+</script>
+
+<style lang="scss" scoped>
+  .ss-order-menu-wrap {
+    .menu-item {
+      height: 160rpx;
+      position: relative;
+      z-index: 10;
+      .menu-title {
+        font-size: 24rpx;
+        line-height: 24rpx;
+        color: #333333;
+      }
+      .item-icon {
+        width: 44rpx;
+        height: 44rpx;
+      }
+      .num-icon {
+        position: absolute;
+        right: 18rpx;
+        top: 18rpx;
+        // width: 40rpx;
+        padding: 0 8rpx;
+        height: 26rpx;
+        background: #ff4d4f;
+        border-radius: 13rpx;
+        color: #fefefe;
+        display: flex;
+        align-items: center;
+        .num {
+          font-size: 24rpx;
+          transform: scale(0.8);
+        }
+      }
+    }
+  }
+</style>

+ 85 - 0
sheep/components/s-popup-image/s-popup-image.vue

@@ -0,0 +1,85 @@
+<template>
+  <view>
+    <view v-for="(item, index) in popupList" :key="index">
+      <su-popup
+        v-if="index === currentIndex"
+        :show="item.isShow"
+        type="center"
+        backgroundColor="none"
+        round="0"
+        :showClose="true"
+        :isMaskClick="false"
+        @close="onClose(index)"
+      >
+        <view class="img-box">
+          <image
+            class="modal-img"
+            :src="sheep.$url.cdn(item.imgUrl)"
+            mode="widthFix"
+            @tap.stop="onPopup(item.url)"
+          />
+        </view>
+      </su-popup>
+    </view>
+  </view>
+</template>
+
+<script setup>
+  import sheep from '@/sheep';
+  import { computed, ref } from 'vue';
+  import { saveAdvHistory } from '@/sheep/hooks/useModal';
+
+  // 定义属性
+  const props = defineProps({
+    data: {
+      type: Object,
+      default() {},
+    }
+  })
+
+  // const modalStore = sheep.$store('modal');
+  const modalStore = JSON.parse(uni.getStorageSync('modal-store') || '{}');
+  console.log(modalStore)
+  const advHistory = modalStore.advHistory || [];
+  const currentIndex = ref(0);
+  const popupList = computed(() => {
+    const list = props.data.list || [];
+    const newList = [];
+    if (list.length > 0) {
+      list.forEach((adv) => {
+        if (adv.showType === 'once' && advHistory.includes(adv.imgUrl)) {
+          adv.isShow = false;
+        } else {
+          adv.isShow = true;
+          newList.push(adv);
+        }
+
+        // 记录弹窗已显示过
+        saveAdvHistory(adv);
+      });
+    }
+    return newList;
+  });
+
+  // 跳转链接
+  function onPopup(path) {
+    sheep.$router.go(path);
+  }
+
+  // 关闭
+  function onClose(index) {
+    currentIndex.value = index + 1;
+    popupList.value[index].isShow = false;
+  }
+</script>
+
+<style lang="scss" scoped>
+  .img-box {
+    width: 610rpx;
+    // height: 800rpx;
+  }
+  .modal-img {
+    width: 100%;
+    height: 100%;
+  }
+</style>

+ 40 - 0
sheep/components/s-richtext-block/s-richtext-block.vue

@@ -0,0 +1,40 @@
+<!-- 装修营销组件:营销文章 -->
+<template>
+  <view
+    :style="[
+      {
+        marginLeft: styles.marginLeft + 'px',
+        marginRight: styles.marginRight + 'px',
+        marginBottom: styles.marginBottom + 'px',
+        marginTop: styles.marginTop + 'px',
+        padding: styles.padding + 'px',
+      },
+    ]"
+  >
+    <mp-html class="richtext" :content="state.content"></mp-html>
+  </view>
+</template>
+<script setup>
+  import { reactive, onMounted } from 'vue';
+  import ArticleApi from '@/sheep/api/promotion/article';
+
+  const props = defineProps({
+    data: {
+      type: Object,
+      default: {},
+    },
+    styles: {
+      type: Object,
+      default() {},
+    },
+  });
+
+  const state = reactive({
+    content: '',
+  });
+
+  onMounted(async () => {
+    const { data } = await ArticleApi.getArticle(props.data.id);
+    state.content = data.content;
+  });
+</script>

+ 164 - 0
sheep/components/s-search-block/s-search-block.vue

@@ -0,0 +1,164 @@
+<template>
+  <view
+    class="search-content ss-flex ss-col-center ss-row-between"
+    @tap="click"
+    :style="[
+      {
+        borderRadius: radius + 'px',
+        background: elBackground,
+        height: height + 'px',
+        width: width,
+      },
+    ]"
+    :class="[{ 'border-content': navbar }]"
+  >
+    <view class="ss-flex ss-col-center" v-if="navbar">
+      <view class="search-icon _icon-search ss-m-l-10" :style="[{ color: props.iconColor }]"></view>
+      <view class="search-input ss-flex-1 ss-line-1" :style="[{ color: fontColor, width: width }]">
+        {{ placeholder }}
+      </view>
+    </view>
+    <uni-search-bar
+      v-if="!navbar"
+      class="ss-flex-1"
+      :radius="data.borderRadius"
+      :placeholder="data.placeholder"
+      cancelButton="none"
+      clearButton="none"
+      @confirm="onSearch"
+      v-model="state.searchVal"
+    />
+    <view class="keyword-link ss-flex">
+      <view v-for="(item, index) in data.hotKeywords" :key="index">
+        <view
+          class="ss-m-r-16"
+          :style="[{ color: data.textColor }]"
+          @tap.stop="sheep.$router.go('/pages/goods/list', { keyword: item })"
+          >{{ item }}</view
+        >
+      </view>
+    </view>
+    <view v-if="data.hotKeywords && data.hotKeywords.length && navbar" class="ss-flex">
+      <button
+        class="ss-reset-button keyword-btn"
+        v-for="(item, index) in data.hotKeywords"
+        :key="index"
+        :style="[{ color: data.textColor, marginRight: '10rpx' }]"
+      >
+        {{ item }}
+      </button>
+    </view>
+  </view>
+</template>
+
+<script setup>
+  /**
+   * 基础组件 - 搜索栏
+   *
+   * @property {String} elBackground 			- 输入框背景色
+   * @property {String} iconColor 			- 图标颜色
+   * @property {String} fontColor 		  	- 字体颜色
+   * @property {Number} placeholder 			- 默认placeholder
+   * @property {Number} topRadius 			- 组件上圆角
+   * @property {Number} bottomRadius 			- 组件下圆角
+   *
+   * @slot keywords							- 关键字
+   * @event {Function} click 					- 点击组件时触发
+   */
+
+  import { computed, reactive } from 'vue';
+  import sheep from '@/sheep';
+
+  // 组件数据
+  const state = reactive({
+    searchVal: '',
+  });
+
+  // 事件页面
+  const emits = defineEmits(['click']);
+
+  // 接收参数
+  const props = defineProps({
+    data: {
+      type: Object,
+      default: () => ({}),
+    },
+    // 输入框背景色
+    elBackground: {
+      type: String,
+      default: '',
+    },
+    height: {
+      type: Number,
+      default: 36,
+    },
+    // 图标颜色
+    iconColor: {
+      type: String,
+      default: '#b0b3bf',
+    },
+    // 字体颜色
+    fontColor: {
+      type: String,
+      default: '#b0b3bf',
+    },
+    // placeholder
+    placeholder: {
+      type: String,
+      default: '这是一个搜索框',
+    },
+    radius: {
+      type: Number,
+      default: 10,
+    },
+    width: {
+      type: String,
+      default: '100%',
+    },
+    navbar: {
+      type: Boolean,
+      default: true,
+    },
+  });
+
+  // 点击
+  const click = () => {
+    emits('click');
+  };
+
+  function onSearch(e) {
+    if (e.value) {
+      sheep.$router.go('/pages/goods/list', { keyword: e.value });
+      setTimeout(() => {
+        state.searchVal = '';
+      }, 100);
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .border-content {
+    border: 2rpx solid #eee;
+  }
+
+  .search-content {
+    flex: 1;
+    // height: 80rpx;
+    position: relative;
+
+    .search-icon {
+      font-size: 38rpx;
+      margin-right: 20rpx;
+    }
+
+    .keyword-link {
+      position: absolute;
+      right: 16rpx;
+      top: 18rpx;
+    }
+
+    .search-input {
+      font-size: 28rpx;
+    }
+  }
+</style>

+ 160 - 0
sheep/components/s-seckill-block/s-seckill-block.vue

@@ -0,0 +1,160 @@
+<!-- 装修组件 - 秒杀 -->
+<template>
+  <view>
+    <!-- 样式一:三列 - 上图下文 -->
+    <view
+      v-if="layoutType === 'threeCol'"
+      class="goods-sm-box ss-flex ss-flex-wrap"
+      :style="[{ margin: '-' + data.space + 'rpx' }]"
+    >
+      <view
+        v-for="product in productList"
+        :key="product.id"
+        class="goods-card-box"
+        :style="[
+          {
+            padding: data.space + 'rpx',
+          },
+        ]"
+      >
+        <s-goods-column
+          class="goods-card"
+          size="sm"
+          :goodsFields="data.fields"
+          :tagStyle="tagStyle"
+          :data="product"
+          :titleColor="data.fields.name?.color"
+          :topRadius="data.borderRadiusTop"
+          :bottomRadius="data.borderRadiusBottom"
+          @click="
+            sheep.$router.go('/pages/goods/seckill', {
+              id: props.data.activityId,
+            })
+          "
+        ></s-goods-column>
+      </view>
+    </view>
+    <!-- 样式二:一列 - 左图右文 -->
+    <view class="goods-box" v-if="layoutType === 'oneCol'">
+      <view
+        class="goods-list"
+        v-for="(product, index) in productList"
+        :key="index"
+        :style="[{ marginBottom: space + 'px' }]"
+      >
+        <s-goods-column
+          class="goods-card"
+          size="lg"
+          :goodsFields="data.fields"
+          :tagStyle="tagStyle"
+          :data="product"
+          :titleColor="data.fields.name?.color"
+          :subTitleColor="data.fields.introduction?.color"
+          :topRadius="data.borderRadiusTop"
+          :bottomRadius="data.borderRadiusBottom"
+          @click="
+            sheep.$router.go('/pages/goods/seckill', {
+              id: props.data.activityId,
+            })
+          "
+        >
+          <template v-slot:cart>
+            <button class="ss-reset-button cart-btn" :style="[buyStyle]">
+              {{ btnBuy?.type === 'text' ? btnBuy.text : '' }}
+            </button>
+          </template>
+        </s-goods-column>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup>
+  /**
+   * 秒杀商品列表
+   *
+   * @property {Array} list 商品列表
+   */
+  import { computed, onMounted, ref } from 'vue';
+  import sheep from '@/sheep';
+  import SeckillApi from "@/sheep/api/promotion/seckill";
+  import SpuApi from "@/sheep/api/product/spu";
+
+  // 接收参数
+  const props = defineProps({
+    data: {
+      type: Object,
+      default() {},
+    },
+    styles: {
+      type: Object,
+      default() {},
+    },
+  });
+
+  let { layoutType, tagStyle, btnBuy, space } = props.data;
+  let { marginLeft, marginRight } = props.styles;
+
+  // 购买按钮样式
+  const buyStyle = computed(() => {
+    let btnBuy = props.data.btnBuy;
+    if (btnBuy?.type === 'text') {
+      return {
+        background: `linear-gradient(to right, ${btnBuy.bgBeginColor}, ${btnBuy.bgEndColor})`,
+      };
+    }
+    if (btnBuy?.type === 'img') {
+      return {
+        width: '54rpx',
+        height: '54rpx',
+        background: `url(${sheep.$url.cdn(btnBuy.imgUrl)}) no-repeat`,
+        backgroundSize: '100% 100%',
+      };
+    }
+  });
+
+  // 商品列表
+  const productList = ref([]);
+  // 查询秒杀活动商品
+  onMounted(async () => {
+    // todo:@owen 与Yudao结构不一致,待重构
+    const { data: activity } = await SeckillApi.getSeckillActivity(props.data.activityId);
+    const { data: spu } = await SpuApi.getSpuDetail(activity.spuId)
+    productList.value = [spu];
+  });
+</script>
+
+<style lang="scss" scoped>
+  .header-box {
+    height: 100rpx;
+  }
+
+  .goods-list {
+    position: relative;
+    &:nth-last-child(1) {
+      margin-bottom: 0 !important;
+    }
+    .cart-btn {
+      position: absolute;
+      bottom: 10rpx;
+      right: 20rpx;
+      z-index: 11;
+      height: 50rpx;
+      line-height: 50rpx;
+      padding: 0 20rpx;
+      border-radius: 25rpx;
+      font-size: 24rpx;
+      color: #fff;
+    }
+  }
+  .goods-sm-box {
+    margin: 0 auto;
+    box-sizing: border-box;
+    .goods-card-box {
+      flex-shrink: 0;
+      overflow: hidden;
+      width: 33.3%;
+      box-sizing: border-box;
+    }
+  }
+</style>

+ 472 - 0
sheep/components/s-select-groupon-sku/s-select-groupon-sku.vue

@@ -0,0 +1,472 @@
+<template>
+  <!-- 拼团商品规格弹窗 -->
+  <su-popup :show="show" round="10" @close="emits('close')">
+    <!-- SKU 信息 -->
+    <view class="ss-modal-box bg-white ss-flex-col">
+      <view class="modal-header ss-flex ss-col-center">
+        <view class="header-left ss-m-r-30">
+          <image class="sku-image" :src="sheep.$url.cdn(state.selectedSku.picUrl || goodsInfo.picUrl)" mode="aspectFill" />
+        </view>
+        <view class="header-right ss-flex-col ss-row-between ss-flex-1">
+          <view class="goods-title ss-line-2">
+            <view class="tig ss-flex ss-col-center">
+              <view class="tig-icon ss-flex ss-col-center ss-row-center">
+                <view class="groupon-tag">
+                  <image :src="sheep.$url.static('/static/img/shop/goods/groupon-tag-white.png')" />
+                </view>
+              </view>
+              <view class="tig-title">拼团价</view>
+            </view>
+            <view class="info-title">
+              {{ goodsInfo.name }}
+            </view>
+          </view>
+          <view class="header-right-bottom ss-flex ss-col-center ss-row-between">
+            <view class="price-text"> {{ fen2yuan(goodsInfo.price) }}</view>
+
+            <view class="stock-text ss-m-l-20">
+              库存{{ state.selectedSku.stock || goodsInfo.stock }}件
+            </view>
+          </view>
+        </view>
+      </view>
+      <view class="modal-content ss-flex-1">
+        <scroll-view scroll-y="true" class="modal-content-scroll">
+          <view class="sku-item ss-m-b-20" v-for="property in propertyList" :key="property.id">
+            <view class="label-text ss-m-b-20">{{ property.name }}</view>
+            <view class="ss-flex ss-col-center ss-flex-wrap">
+              <button class="ss-reset-button spec-btn" v-for="value in property.values" :class="[
+                  {
+                    'checked-btn': state.currentPropertyArray[property.id] === value.id,
+                  },
+                  {
+                    'disabled-btn': value.disabled === true,
+                  },
+                ]" :key="value.id" :disabled="value.disabled === true" @tap="onSelectSku(property.id, value.id)">
+                {{ value.name }}
+              </button>
+            </view>
+          </view>
+          <view class="buy-num-box ss-flex ss-col-center ss-row-between">
+            <view class="label-text">购买数量</view>
+            <su-number-box :min="1" :max="state.selectedSku.stock" :step="1"
+                           v-model="state.selectedSku.count" @change="onNumberChange($event)" activity="groupon" />
+          </view>
+        </scroll-view>
+      </view>
+
+      <!-- 操作区 -->
+      <view class="modal-footer ss-p-y-20">
+        <view class="buy-box ss-flex ss-col-center ss-flex ss-col-center ss-row-center">
+          <view class="ss-flex">
+            <button class="ss-reset-button origin-price-btn ss-flex-col">
+              <view class="btn-title">{{ grouponNum + '人团' }}</view>
+            </button>
+            <button class="ss-reset-button btn-tox ss-flex-col" @tap="onBuy">
+              <view class="btn-price">{{ fen2yuan(goodsInfo.price) }}</view>
+              <view v-if="grouponAction === 'create'">立即开团</view>
+              <view v-else-if="grouponAction === 'join'">参与拼团</view>
+            </button>
+          </view>
+        </view>
+      </view>
+    </view>
+  </su-popup>
+</template>
+
+<script setup>
+  import { computed, reactive, watch } from 'vue';
+  import sheep from '@/sheep';
+  import {convertProductPropertyList, fen2yuan} from '@/sheep/hooks/useGoods';
+
+  const headerBg = sheep.$url.css('/static/img/shop/goods/groupon-btn-long.png');
+  const emits = defineEmits(['change', 'addCart', 'buy', 'close', 'ladder']);
+  const props = defineProps({
+    show: {
+      type: Boolean,
+      default: false,
+    },
+    goodsInfo: {
+      type: Object,
+      default () {},
+    },
+    grouponAction: {
+      type: String,
+      default: 'create',
+    },
+    grouponNum: {
+      type: [Number, String],
+      default: 0,
+    },
+  });
+  const state = reactive({
+    selectedSku: {}, // 选中的 SKU
+    currentPropertyArray: [], // 当前选中的属性,实际是个 Map。key 是 property 编号,value 是 value 编号
+    grouponNum: props.grouponNum,
+  });
+
+  const propertyList = convertProductPropertyList(props.goodsInfo.skus);
+
+  // SKU 列表
+  const skuList = computed(() => {
+    let skuPrices = props.goodsInfo.skus;
+    for (let price of skuPrices) {
+      price.value_id_array = price.properties.map((item) => item.valueId)
+    }
+    return skuPrices;
+  });
+
+  watch(
+    () => state.selectedSku,
+    (newVal) => {
+      emits('change', newVal);
+    }, {
+      immediate: true, // 立即执行
+      deep: true, // 深度监听
+    },
+  );
+
+  // 输入框改变数量
+  function onNumberChange(e) {
+    if (e === 0) return;
+    if (state.selectedSku.count === e) return;
+    state.selectedSku.count = e;
+  }
+
+  // 点击购买
+  function onBuy() {
+    if (!state.selectedSku.id || state.selectedSku.id <= 0) {
+      sheep.$helper.toast('请选择规格');
+      return;
+    }
+    if (state.selectedSku.stock <= 0) {
+      sheep.$helper.toast('库存不足');
+      return;
+    }
+    emits('buy', state.selectedSku);
+  }
+
+  // 改变禁用状态:计算每个 property 属性值的按钮,是否禁用
+  function changeDisabled(isChecked = false, propertyId = 0, valueId = 0) {
+    let newSkus = []; // 所有可以选择的 sku 数组
+    if (isChecked) {
+      // 情况一:选中 property
+      // 获得当前点击选中 property 的、所有可用 SKU
+      for (let price of skuList.value) {
+        if (price.stock <= 0) {
+          continue;
+        }
+        if (price.value_id_array.indexOf(valueId) >= 0) {
+          newSkus.push(price);
+        }
+      }
+    } else {
+      // 情况二:取消选中 property
+      // 当前所选 property 下,所有可以选择的 SKU
+      newSkus = getCanUseSkuList();
+    }
+
+    // 所有存在并且有库存未选择的 SKU 的 value 属性值 id
+    let noChooseValueIds = [];
+    for (let price of newSkus) {
+      noChooseValueIds = noChooseValueIds.concat(price.value_id_array);
+    }
+    noChooseValueIds = Array.from(new Set(noChooseValueIds)); // 去重
+
+    if (isChecked) {
+      // 去除当前选中的 value 属性值 id
+      let index = noChooseValueIds.indexOf(valueId);
+      noChooseValueIds.splice(index, 1);
+    } else {
+      // 循环去除当前已选择的 value 属性值 id
+      state.currentPropertyArray.forEach((currentPropertyId) => {
+        if (currentPropertyId.toString() !== '') {
+          return;
+        }
+        // currentPropertyId 为空是反选 填充的
+        let index = noChooseValueIds.indexOf(currentPropertyId);
+        if (index >= 0) {
+          // currentPropertyId 存在于 noChooseValueIds
+          noChooseValueIds.splice(index, 1);
+        }
+      });
+    }
+
+    // 当前已选择的 property 数组
+    let choosePropertyIds = [];
+    if (!isChecked) {
+      // 当前已选择的 property
+      state.currentPropertyArray.forEach((currentPropertyId, currentValueId) => {
+        if (currentPropertyId !== '') {
+          // currentPropertyId 为空是反选 填充的
+          choosePropertyIds.push(currentValueId);
+        }
+      });
+    } else {
+      // 当前点击选择的 property
+      choosePropertyIds = [propertyId];
+    }
+
+    for (let propertyIndex in propertyList) {
+      // 当前点击的 property、或者取消选择时候,已选中的 property 不进行处理
+      if (choosePropertyIds.indexOf(propertyList[propertyIndex]['id']) >= 0) {
+        continue;
+      }
+      // 如果当前 property id 不存在于有库存的 SKU 中,则禁用
+      for (let valueIndex in propertyList[propertyIndex]['values']) {
+        propertyList[propertyIndex]['values'][valueIndex]['disabled'] =
+            noChooseValueIds.indexOf(propertyList[propertyIndex]['values'][valueIndex]['id']) < 0; // true 禁用 or false 不禁用
+      }
+    }
+  }
+
+  // 当前所选属性下,获取所有有库存的 SKU 们
+  function getCanUseSkuList() {
+    let newSkus = [];
+    for (let sku of skuList.value) {
+      if (sku.stock <= 0) {
+        continue;
+      }
+      let isOk = true;
+      state.currentPropertyArray.forEach((propertyId) => {
+        // propertyId 不为空,并且,这个 条 sku 没有被选中,则排除
+        if (propertyId.toString() !== '' && sku.value_id_array.indexOf(propertyId) < 0) {
+          isOk = false;
+        }
+      });
+      if (isOk) {
+        newSkus.push(sku);
+      }
+    }
+    return newSkus;
+  }
+
+  // 选择规格
+  function onSelectSku(propertyId, valueId) {
+    // 清空已选择
+    let isChecked = true; // 选中 or 取消选中
+    if (state.currentPropertyArray[propertyId] !== undefined && state.currentPropertyArray[propertyId] === valueId) {
+      // 点击已被选中的,删除并填充 ''
+      isChecked = false;
+      state.currentPropertyArray.splice(propertyId, 1, '');
+    } else {
+      // 选中
+      state.currentPropertyArray[propertyId] = valueId;
+    }
+
+    // 选中的 property 大类
+    let choosePropertyId = [];
+    state.currentPropertyArray.forEach((currentPropertyId) => {
+      if (currentPropertyId !== '') {
+        // currentPropertyId 为空是反选 填充的
+        choosePropertyId.push(currentPropertyId);
+      }
+    });
+
+    // 当前所选 property 下,所有可以选择的 SKU 们
+    let newSkuList = getCanUseSkuList();
+
+    // 判断所有 property 大类是否选择完成
+    if (choosePropertyId.length === propertyList.length && newSkuList.length) {
+      newSkuList[0].count = state.selectedSku.count || 1;
+      state.selectedSku = newSkuList[0];
+    } else {
+      state.selectedSku = {};
+    }
+
+    // 改变 property 禁用状态
+    changeDisabled(isChecked, propertyId, valueId);
+  }
+
+  changeDisabled(false);
+  // TODO 芋艿:待讨论的优化点:1)单规格,要不要默认选中;2)默认要不要选中第一个规格
+</script>
+
+<style lang="scss" scoped>
+  // 购买
+  .buy-btn {
+    margin: 0 20rpx;
+    width: 100%;
+    height: 80rpx;
+    border-radius: 40rpx;
+    background: linear-gradient(90deg, #ff6000, #fe832a);
+    color: #fff;
+  }
+  .btn-tox {
+    width: 382rpx;
+    height: 80rpx;
+    font-size: 24rpx;
+    font-weight: 600;
+    margin-left: -50rpx;
+    background-image: v-bind(headerBg);
+    background-repeat: no-repeat;
+    background-size: 100% 100%;
+    color: #ffffff;
+    line-height: normal;
+    border-radius: 0px 40rpx 40rpx 0px;
+
+    .btn-price {
+      font-family: OPPOSANS;
+
+      &::before {
+        content: '¥';
+      }
+    }
+  }
+  .origin-price-btn {
+    width: 370rpx;
+    height: 80rpx;
+    background: rgba(#ff5651, 0.1);
+    color: #ff6000;
+    border-radius: 40rpx 0px 0px 40rpx;
+    line-height: normal;
+    font-size: 24rpx;
+    font-weight: 500;
+
+    .btn-price {
+      font-family: OPPOSANS;
+
+      &::before {
+        content: '¥';
+      }
+    }
+
+    .btn-title {
+      font-size: 28rpx;
+    }
+  }
+
+  .ss-modal-box {
+    border-radius: 30rpx 30rpx 0 0;
+    max-height: 1000rpx;
+
+    .modal-header {
+      position: relative;
+      padding: 80rpx 20rpx 40rpx;
+
+      .sku-image {
+        width: 160rpx;
+        height: 160rpx;
+        border-radius: 10rpx;
+      }
+
+      .header-right {
+        height: 160rpx;
+      }
+
+      .close-icon {
+        position: absolute;
+        top: 10rpx;
+        right: 20rpx;
+        font-size: 46rpx;
+        opacity: 0.2;
+      }
+
+      .goods-title {
+        font-size: 28rpx;
+        font-weight: 500;
+        line-height: 42rpx;
+        position: relative;
+        .tig {
+          border: 2rpx solid #ff6000;
+          border-radius: 4rpx;
+          width: 126rpx;
+          height: 38rpx;
+          position: absolute;
+          left: 0;
+          top: 0;
+
+          .tig-icon {
+            width: 40rpx;
+            height: 40rpx;
+            background: #ff6000;
+            margin-left: -2rpx;
+            border-radius: 4rpx 0 0 4rpx;
+
+            .groupon-tag {
+              width: 32rpx;
+              height: 32rpx;
+            }
+          }
+
+          .tig-title {
+            font-size: 24rpx;
+            font-weight: 500;
+            line-height: normal;
+            color: #ff6000;
+            width: 86rpx;
+            display: flex;
+            justify-content: center;
+            align-items: center;
+          }
+        }
+        .info-title {
+          text-indent: 132rpx;
+        }
+      }
+
+      .price-text {
+        font-size: 30rpx;
+        font-weight: 500;
+        color: $red;
+        font-family: OPPOSANS;
+
+        &::before {
+          content: '¥';
+          font-size: 24rpx;
+        }
+      }
+
+      .stock-text {
+        font-size: 26rpx;
+        color: #999999;
+      }
+    }
+
+    .modal-content {
+      padding: 0 20rpx;
+
+      .modal-content-scroll {
+        max-height: 600rpx;
+
+        .label-text {
+          font-size: 26rpx;
+          font-weight: 500;
+        }
+
+        .buy-num-box {
+          height: 100rpx;
+        }
+
+        .spec-btn {
+          height: 60rpx;
+          min-width: 100rpx;
+          padding: 0 30rpx;
+          background: #f4f4f4;
+          border-radius: 30rpx;
+          color: #434343;
+          font-size: 26rpx;
+          margin-right: 10rpx;
+          margin-bottom: 10rpx;
+        }
+
+        .checked-btn {
+          background: linear-gradient(90deg, #ff6000, #fe832a);
+          font-weight: 500;
+          color: #ffffff;
+        }
+
+        .disabled-btn {
+          font-weight: 400;
+          color: #c6c6c6;
+          background: #f8f8f8;
+        }
+      }
+    }
+  }
+
+  image {
+    width: 100%;
+    height: 100%;
+  }
+</style>

+ 432 - 0
sheep/components/s-select-seckill-sku/s-select-seckill-sku.vue

@@ -0,0 +1,432 @@
+<!-- 秒杀商品的 SKU 选择,和 s-select-sku.vue 类似 -->
+<template>
+	<!-- 规格弹窗 -->
+	<su-popup :show="show" round="10" @close="emits('close')">
+		<!-- SKU 信息 -->
+		<view class="ss-modal-box bg-white ss-flex-col">
+			<view class="modal-header ss-flex ss-col-center">
+				<!-- 规格图 -->
+				<view class="header-left ss-m-r-30">
+					<image
+						class="sku-image"
+						:src="sheep.$url.cdn(state.selectedSku.picUrl || state.goodsInfo.picUrl)"
+						mode="aspectFill"
+					>
+					</image>
+				</view>
+				<view class="header-right ss-flex-col ss-row-between ss-flex-1">
+					<!-- 名称 -->
+					<view class="goods-title ss-line-2">{{ state.goodsInfo.name }}</view>
+					<view class="header-right-bottom ss-flex ss-col-center ss-row-between">
+						<!-- 价格 -->
+						<view class="price-text">
+							{{ fen2yuan(state.selectedSku.price || state.goodsInfo.price) }}
+						</view>
+						<!-- 秒杀价格标签 -->
+						<view class="tig ss-flex ss-col-center">
+							<view class="tig-icon ss-flex ss-col-center ss-row-center">
+	              <text class="cicon-alarm"></text>
+              </view>
+              <view class="tig-title">秒杀价</view>
+            </view>
+            <!-- 库存 -->
+            <view class="stock-text ss-m-l-20">
+              库存{{ state.selectedSku.stock || state.goodsInfo.stock }}件
+            </view>
+          </view>
+        </view>
+      </view>
+      <view class="modal-content ss-flex-1">
+        <scroll-view scroll-y="true" class="modal-content-scroll">
+          <view class="sku-item ss-m-b-20" v-for="property in propertyList" :key="property.id">
+            <view class="label-text ss-m-b-20">{{ property.name }}</view>
+            <view class="ss-flex ss-col-center ss-flex-wrap">
+              <button
+                class="ss-reset-button spec-btn"
+                v-for="value in property.values"
+                :class="[
+                  {
+                    'checked-btn': state.currentPropertyArray[property.id] === value.id,
+                  },
+                  {
+                    'disabled-btn': value.disabled === true,
+                  },
+                ]"
+                :key="value.id"
+                :disabled="value.disabled === true"
+                @tap="onSelectSku(property.id, value.id)"
+              >
+                {{ value.name }}
+              </button>
+            </view>
+          </view>
+          <view class="buy-num-box ss-flex ss-col-center ss-row-between">
+            <view class="label-text">购买数量</view>
+            <su-number-box
+              :min="1"
+              :max="min([singleLimitCount, state.selectedSku.stock])"
+              :step="1"
+              v-model="state.selectedSku.count"
+              @change="onBuyCountChange($event)"
+              activity="seckill"
+            ></su-number-box>
+          </view>
+        </scroll-view>
+      </view>
+      <view class="modal-footer">
+        <view class="buy-box ss-flex ss-col-center ss-flex ss-col-center ss-row-center">
+          <button class="ss-reset-button buy-btn" @tap="onBuy">确认</button>
+        </view>
+      </view>
+    </view>
+  </su-popup>
+</template>
+
+<script setup>
+  /**
+   * 秒杀活动SKU选择,
+   * 与s-select-sku的区别:多一个秒杀价的标签、没有加入购物车按钮、立即购买按钮叫确认、秒杀有最大购买数量限制
+   * 差别不大,可以考虑合并 todo @芋艿
+   */
+  // 按钮状态: active,nostock
+  import { computed, reactive, watch } from 'vue';
+  import sheep from '@/sheep';
+  import {convertProductPropertyList, fen2yuan} from "@/sheep/hooks/useGoods";
+  import {min} from "lodash";
+  const emits = defineEmits(['change', 'addCart', 'buy', 'close']);
+  const props = defineProps({
+    modelValue: {
+      type: Object,
+      default() {},
+    },
+    show: {
+      type: Boolean,
+      default: false,
+    },
+    // 单次限购数量
+    singleLimitCount: {
+      type: Number,
+      default: 1,
+    }
+  });
+  const state = reactive({
+    goodsInfo: computed(() => props.modelValue),
+    selectedSku: {},
+    currentPropertyArray: [],
+  });
+
+  const propertyList = convertProductPropertyList(state.goodsInfo.skus);
+  // SKU 列表
+  const skuList = computed(() => {
+    let skuPrices = state.goodsInfo.skus;
+    for (let price of skuPrices) {
+      price.value_id_array = price.properties.map((item) => item.valueId)
+    }
+    return skuPrices;
+  });
+
+  if (!state.goodsInfo.is_sku) {
+    state.selectedSku = state.goodsInfo.skus[0];
+  }
+
+  watch(
+    () => state.selectedSku,
+    (newVal) => {
+      emits('change', newVal);
+    },
+    {
+      immediate: true, // 立即执行
+      deep: true, // 深度监听
+    },
+  );
+
+  const onBuy = () => {
+    if (state.selectedSku.id) {
+      if (state.selectedSku.stock <= 0) {
+        sheep.$helper.toast('库存不足');
+      } else {
+        emits('buy', state.selectedSku);
+      }
+    } else {
+      sheep.$helper.toast('请选择规格');
+    }
+  };
+
+  // 购买数量改变
+  function onBuyCountChange(buyCount) {
+    if (buyCount > 0 && state.selectedSku.count !== buyCount) {
+      state.selectedSku.count = buyCount;
+    }
+  }
+
+  // 改变禁用状态
+  const changeDisabled = (isChecked = false, propertyId = 0, valueId = 0) => {
+    let newSkus = []; // 所有可以选择的 sku 数组
+    if (isChecked) {
+      // 情况一:选中 property
+      // 获得当前点击选中 property 的、所有可用 SKU
+      for (let price of skuList.value) {
+        if (price.stock <= 0) {
+          continue;
+        }
+        if (price.value_id_array.indexOf(valueId) >= 0) {
+          newSkus.push(price);
+        }
+      }
+    } else {
+      // 情况二:取消选中 property
+      // 当前所选 property 下,所有可以选择的 SKU
+      newSkus = getCanUseSkuList();
+    }
+
+    // 所有存在并且有库存未选择的 SKU 的 value 属性值 id
+    let noChooseValueIds = [];
+    for (let price of newSkus) {
+      noChooseValueIds = noChooseValueIds.concat(price.value_id_array);
+    }
+    noChooseValueIds = Array.from(new Set(noChooseValueIds)); // 去重
+
+    if (isChecked) {
+      // 去除当前选中的 value 属性值 id
+      let index = noChooseValueIds.indexOf(valueId);
+      noChooseValueIds.splice(index, 1);
+    } else {
+      // 循环去除当前已选择的 value 属性值 id
+      state.currentPropertyArray.forEach((currentPropertyId) => {
+        if (currentPropertyId.toString() !== '') {
+          return;
+        }
+        // currentPropertyId 为空是反选 填充的
+        let index = noChooseValueIds.indexOf(currentPropertyId);
+        if (index >= 0) {
+          // currentPropertyId 存在于 noChooseValueIds
+          noChooseValueIds.splice(index, 1);
+        }
+      });
+    }
+
+    // 当前已选择的 property 数组
+    let choosePropertyIds = [];
+    if (!isChecked) {
+      // 当前已选择的 property
+      state.currentPropertyArray.forEach((currentPropertyId, currentValueId) => {
+        if (currentPropertyId !== '') {
+          // currentPropertyId 为空是反选 填充的
+          choosePropertyIds.push(currentValueId);
+        }
+      });
+    } else {
+      // 当前点击选择的 property
+      choosePropertyIds = [propertyId];
+    }
+
+    for (let propertyIndex in propertyList) {
+      // 当前点击的 property、或者取消选择时候,已选中的 property 不进行处理
+      if (choosePropertyIds.indexOf(propertyList[propertyIndex]['id']) >= 0) {
+        continue;
+      }
+      // 如果当前 property id 不存在于有库存的 SKU 中,则禁用
+      for (let valueIndex in propertyList[propertyIndex]['values']) {
+        propertyList[propertyIndex]['values'][valueIndex]['disabled'] =
+            noChooseValueIds.indexOf(propertyList[propertyIndex]['values'][valueIndex]['id']) < 0; // true 禁用 or false 不禁用
+      }
+    }
+  };
+
+  // 获取可用的(有库存的)SKU 列表
+  const getCanUseSkuList = () => {
+    let newSkus = [];
+    for (let sku of skuList.value) {
+      if (sku.stock <= 0) {
+        continue;
+      }
+      let isOk = true;
+      state.currentPropertyArray.forEach((propertyId) => {
+        // propertyId 不为空,并且,这个 条 sku 没有被选中,则排除
+        if (propertyId.toString() !== '' && sku.value_id_array.indexOf(propertyId) < 0) {
+          isOk = false;
+        }
+      });
+      if (isOk) {
+        newSkus.push(sku);
+      }
+    }
+    return newSkus;
+  };
+
+  // 选择规格
+  const onSelectSku = (propertyId, valueId) => {
+    // 清空已选择
+    let isChecked = true; // 选中 or 取消选中
+    if (state.currentPropertyArray[propertyId] !== undefined && state.currentPropertyArray[propertyId] === valueId) {
+      // 点击已被选中的,删除并填充 ''
+      isChecked = false;
+      state.currentPropertyArray.splice(propertyId, 1, '');
+    } else {
+      // 选中
+      state.currentPropertyArray[propertyId] = valueId;
+    }
+
+    // 选中的 property 大类
+    let choosePropertyId = [];
+    state.currentPropertyArray.forEach((currentPropertyId) => {
+      if (currentPropertyId !== '') {
+        // currentPropertyId 为空是反选 填充的
+        choosePropertyId.push(currentPropertyId);
+      }
+    });
+
+    // 当前所选 property 下,所有可以选择的 SKU 们
+    let newSkuList = getCanUseSkuList();
+
+    // 判断所有 property 大类是否选择完成
+    if (choosePropertyId.length === propertyList.length && newSkuList.length) {
+      newSkuList[0].count = state.selectedSku.count || 1;
+      state.selectedSku = newSkuList[0];
+    } else {
+      state.selectedSku = {};
+    }
+
+    // 改变 property 禁用状态
+    changeDisabled(isChecked, propertyId, valueId);
+  };
+
+  changeDisabled(false);
+</script>
+
+<style lang="scss" scoped>
+  // 购买
+  .buy-box {
+    padding: 10rpx 20rpx;
+
+    .buy-btn {
+      width: 100%;
+      height: 80rpx;
+      border-radius: 40rpx;
+      background: linear-gradient(90deg, #ff5854, #ff2621);
+      color: #fff;
+    }
+  }
+
+  .ss-modal-box {
+    border-radius: 30rpx 30rpx 0 0;
+    max-height: 1000rpx;
+
+    .modal-header {
+      position: relative;
+      padding: 80rpx 20rpx 40rpx;
+
+      .sku-image {
+        width: 160rpx;
+        height: 160rpx;
+        border-radius: 10rpx;
+      }
+
+      .header-right {
+        height: 160rpx;
+      }
+
+      .close-icon {
+        position: absolute;
+        top: 10rpx;
+        right: 20rpx;
+        font-size: 46rpx;
+        opacity: 0.2;
+      }
+
+      .goods-title {
+        font-size: 28rpx;
+        font-weight: 500;
+        line-height: 42rpx;
+      }
+
+      .price-text {
+        font-size: 30rpx;
+        font-weight: 500;
+        color: $red;
+        font-family: OPPOSANS;
+
+        &::before {
+          content: '¥';
+          font-size: 24rpx;
+        }
+      }
+
+      .stock-text {
+        font-size: 26rpx;
+        color: #999999;
+      }
+    }
+
+    .modal-content {
+      padding: 0 20rpx;
+
+      .modal-content-scroll {
+        max-height: 600rpx;
+
+        .label-text {
+          font-size: 26rpx;
+          font-weight: 500;
+        }
+
+        .buy-num-box {
+          height: 100rpx;
+        }
+
+        .spec-btn {
+          height: 60rpx;
+          min-width: 100rpx;
+          padding: 0 30rpx;
+          background: #f4f4f4;
+          border-radius: 30rpx;
+          color: #434343;
+          font-size: 26rpx;
+          margin-right: 10rpx;
+          margin-bottom: 10rpx;
+        }
+
+        .checked-btn {
+          background: linear-gradient(90deg, #ff5854, #ff2621);
+          font-weight: 500;
+          color: #ffffff;
+        }
+
+        .disabled-btn {
+          font-weight: 400;
+          color: #c6c6c6;
+          background: #f8f8f8;
+        }
+      }
+    }
+  }
+
+  .tig {
+    border: 2rpx solid #ff5854;
+    border-radius: 4rpx;
+    width: 126rpx;
+    height: 38rpx;
+
+    .tig-icon {
+      width: 40rpx;
+      height: 40rpx;
+      background: #ff5854;
+      border-radius: 4rpx 0 0 4rpx;
+
+      .cicon-alarm {
+        font-size: 32rpx;
+        color: #fff;
+      }
+    }
+
+    .tig-title {
+      font-size: 24rpx;
+      font-weight: 500;
+      line-height: normal;
+      color: #ff6000;
+      width: 86rpx;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+    }
+  }
+</style>

+ 406 - 0
sheep/components/s-select-sku/s-select-sku.vue

@@ -0,0 +1,406 @@
+<template>
+	<!-- 规格弹窗 -->
+	<su-popup :show="show" round="10" @close="emits('close')">
+    <!-- SKU 信息 -->
+		<view class="ss-modal-box bg-white ss-flex-col">
+			<view class="modal-header ss-flex ss-col-center">
+				<view class="header-left ss-m-r-30">
+					<image class="sku-image" :src="state.selectedSku.picUrl || goodsInfo.picUrl" mode="aspectFill" />
+				</view>
+				<view class="header-right ss-flex-col ss-row-between ss-flex-1">
+					<view class="goods-title ss-line-2">{{ goodsInfo.name }}</view>
+					<view class="header-right-bottom ss-flex ss-col-center ss-row-between">
+						<view class="ss-flex">
+							<view class="price-text">
+								{{ fen2yuan( state.selectedSku.price || goodsInfo.price) }}
+							</view>
+						</view>
+						<view class="stock-text ss-m-l-20">
+							{{ formatStock('exact', state.selectedSku.stock || goodsInfo.stock) }}
+						</view>
+					</view>
+				</view>
+			</view>
+
+      <!-- 属性选择 -->
+			<view class="modal-content ss-flex-1">
+				<scroll-view scroll-y="true" class="modal-content-scroll" @touchmove.stop>
+					<view class="sku-item ss-m-b-20" v-for="property in propertyList" :key="property.id">
+						<view class="label-text ss-m-b-20">{{ property.name }}</view>
+						<view class="ss-flex ss-col-center ss-flex-wrap">
+							<button class="ss-reset-button spec-btn" v-for="value in property.values" :class="[
+                  {
+                    'ui-BG-Main-Gradient': state.currentPropertyArray[property.id] === value.id,
+                  },
+                  {
+                    'disabled-btn': value.disabled === true,
+                  },
+                ]" :key="value.id" :disabled="value.disabled === true" @tap="onSelectSku(property.id, value.id)">
+								{{ value.name }}
+							</button>
+						</view>
+					</view>
+					<view class="buy-num-box ss-flex ss-col-center ss-row-between ss-m-b-40">
+						<view class="label-text">购买数量</view>
+						<su-number-box :min="1" :max="state.selectedSku.stock" :step="1"
+                           v-model="state.selectedSku.goods_num" @change="onNumberChange($event)" />
+					</view>
+				</scroll-view>
+			</view>
+
+      <!-- 操作区 -->
+			<view class="modal-footer border-top">
+				<view class="buy-box ss-flex ss-col-center ss-flex ss-col-center ss-row-center">
+					<button class="ss-reset-button add-btn ui-Shadow-Main" @tap="onAddCart">加入购物车</button>
+					<button class="ss-reset-button buy-btn ui-Shadow-Main" @tap="onBuy">立即购买</button>
+				</view>
+			</view>
+		</view>
+	</su-popup>
+</template>
+
+<script setup>
+	import { computed, reactive, watch } from 'vue';
+	import sheep from '@/sheep';
+  import { formatStock, convertProductPropertyList, fen2yuan } from '@/sheep/hooks/useGoods';
+
+	const emits = defineEmits(['change', 'addCart', 'buy', 'close']);
+	const props = defineProps({
+		goodsInfo: {
+			type: Object,
+			default () {},
+		},
+		show: {
+			type: Boolean,
+			default: false,
+		}
+	});
+
+	const state = reactive({
+		selectedSku: {}, // 选中的 SKU
+		currentPropertyArray: [], // 当前选中的属性,实际是个 Map。key 是 property 编号,value 是 value 编号
+	});
+
+	const propertyList = convertProductPropertyList(props.goodsInfo.skus);
+
+	// SKU 列表
+	const skuList = computed(() => {
+		let skuPrices = props.goodsInfo.skus;
+    for (let price of skuPrices) {
+      price.value_id_array = price.properties.map((item) => item.valueId)
+    }
+		return skuPrices;
+	});
+
+	watch(
+		() => state.selectedSku,
+		(newVal) => {
+			emits('change', newVal);
+		}, {
+			immediate: true, // 立即执行
+			deep: true, // 深度监听
+		},
+	);
+
+  // 输入框改变数量
+  function onNumberChange(e) {
+    if (e === 0) return;
+    if (state.selectedSku.goods_num === e) return;
+    state.selectedSku.goods_num = e;
+  }
+
+  // 加入购物车
+	function onAddCart() {
+		if (state.selectedSku.id <= 0) {
+      sheep.$helper.toast('请选择规格');
+      return;
+		}
+    if (state.selectedSku.stock <= 0) {
+      sheep.$helper.toast('库存不足');
+      return;
+    }
+
+    emits('addCart', state.selectedSku);
+	}
+
+  // 立即购买
+	function onBuy() {
+    if (state.selectedSku.id <= 0) {
+      sheep.$helper.toast('请选择规格');
+      return;
+    }
+    if (state.selectedSku.stock <= 0) {
+      sheep.$helper.toast('库存不足');
+      return;
+    }
+    emits('buy', state.selectedSku);
+	}
+
+	// 改变禁用状态:计算每个 property 属性值的按钮,是否禁用
+	function changeDisabled(isChecked = false, propertyId = 0, valueId = 0) {
+    let newSkus = []; // 所有可以选择的 sku 数组
+		if (isChecked) {
+			// 情况一:选中 property
+			// 获得当前点击选中 property 的、所有可用 SKU
+			for (let price of skuList.value) {
+				if (price.stock <= 0) {
+					continue;
+				}
+				if (price.value_id_array.indexOf(valueId) >= 0) {
+					newSkus.push(price);
+				}
+			}
+		} else {
+			// 情况二:取消选中 property
+			// 当前所选 property 下,所有可以选择的 SKU
+			newSkus = getCanUseSkuList();
+		}
+
+		// 所有存在并且有库存未选择的 SKU 的 value 属性值 id
+		let noChooseValueIds = [];
+		for (let price of newSkus) {
+			noChooseValueIds = noChooseValueIds.concat(price.value_id_array);
+		}
+		noChooseValueIds = Array.from(new Set(noChooseValueIds)); // 去重
+
+		if (isChecked) {
+			// 去除当前选中的 value 属性值 id
+			let index = noChooseValueIds.indexOf(valueId);
+			noChooseValueIds.splice(index, 1);
+		} else {
+			// 循环去除当前已选择的 value 属性值 id
+			state.currentPropertyArray.forEach((currentPropertyId) => {
+				if (currentPropertyId.toString() !== '') {
+          return;
+				}
+        // currentPropertyId 为空是反选 填充的
+        let index = noChooseValueIds.indexOf(currentPropertyId);
+        if (index >= 0) {
+          // currentPropertyId 存在于 noChooseValueIds
+          noChooseValueIds.splice(index, 1);
+        }
+			});
+		}
+
+    // 当前已选择的 property 数组
+		let choosePropertyIds = [];
+		if (!isChecked) {
+			// 当前已选择的 property
+			state.currentPropertyArray.forEach((currentPropertyId, currentValueId) => {
+				if (currentPropertyId !== '') {
+					// currentPropertyId 为空是反选 填充的
+					choosePropertyIds.push(currentValueId);
+				}
+			});
+		} else {
+			// 当前点击选择的 property
+			choosePropertyIds = [propertyId];
+		}
+
+    for (let propertyIndex in propertyList) {
+			// 当前点击的 property、或者取消选择时候,已选中的 property 不进行处理
+			if (choosePropertyIds.indexOf(propertyList[propertyIndex]['id']) >= 0) {
+				continue;
+			}
+      // 如果当前 property id 不存在于有库存的 SKU 中,则禁用
+      for (let valueIndex in propertyList[propertyIndex]['values']) {
+				propertyList[propertyIndex]['values'][valueIndex]['disabled'] =
+          noChooseValueIds.indexOf(propertyList[propertyIndex]['values'][valueIndex]['id']) < 0; // true 禁用 or false 不禁用
+			}
+		}
+	}
+
+	// 当前所选属性下,获取所有有库存的 SKU 们
+	function getCanUseSkuList() {
+		let newSkus = [];
+		for (let sku of skuList.value) {
+			if (sku.stock <= 0) {
+				continue;
+			}
+      let isOk = true;
+      state.currentPropertyArray.forEach((propertyId) => {
+				// propertyId 不为空,并且,这个 条 sku 没有被选中,则排除
+				if (propertyId.toString() !== '' && sku.value_id_array.indexOf(propertyId) < 0) {
+					isOk = false;
+				}
+			});
+			if (isOk) {
+				newSkus.push(sku);
+			}
+		}
+		return newSkus;
+	}
+
+	// 选择规格
+	function onSelectSku(propertyId, valueId) {
+		// 清空已选择
+		let isChecked = true; // 选中 or 取消选中
+		if (state.currentPropertyArray[propertyId] !== undefined && state.currentPropertyArray[propertyId] === valueId) {
+			// 点击已被选中的,删除并填充 ''
+			isChecked = false;
+			state.currentPropertyArray.splice(propertyId, 1, '');
+		} else {
+			// 选中
+			state.currentPropertyArray[propertyId] = valueId;
+		}
+
+    // 选中的 property 大类
+		let choosePropertyId = [];
+		state.currentPropertyArray.forEach((currentPropertyId) => {
+			if (currentPropertyId !== '') {
+				// currentPropertyId 为空是反选 填充的
+				choosePropertyId.push(currentPropertyId);
+			}
+		});
+
+		// 当前所选 property 下,所有可以选择的 SKU 们
+		let newSkuList = getCanUseSkuList();
+
+		// 判断所有 property 大类是否选择完成
+		if (choosePropertyId.length === propertyList.length && newSkuList.length) {
+			newSkuList[0].goods_num = state.selectedSku.goods_num || 1;
+			state.selectedSku = newSkuList[0];
+		} else {
+			state.selectedSku = {};
+		}
+
+		// 改变 property 禁用状态
+		changeDisabled(isChecked, propertyId, valueId);
+	}
+
+	changeDisabled(false);
+  // TODO 芋艿:待讨论的优化点:1)单规格,要不要默认选中;2)默认要不要选中第一个规格
+</script>
+
+<style lang="scss" scoped>
+	// 购买
+	.buy-box {
+		padding: 10rpx 0;
+
+		.add-btn {
+			width: 356rpx;
+			height: 80rpx;
+			border-radius: 40rpx 0 0 40rpx;
+			background-color: var(--ui-BG-Main-light);
+			color: var(--ui-BG-Main);
+		}
+
+		.buy-btn {
+			width: 356rpx;
+			height: 80rpx;
+			border-radius: 0 40rpx 40rpx 0;
+			background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
+			color: #fff;
+		}
+
+		.score-btn {
+			width: 100%;
+			margin: 0 20rpx;
+			height: 80rpx;
+			border-radius: 40rpx;
+			background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
+			color: #fff;
+		}
+	}
+
+	.ss-modal-box {
+		border-radius: 30rpx 30rpx 0 0;
+		max-height: 1000rpx;
+
+		.modal-header {
+			position: relative;
+			padding: 80rpx 20rpx 40rpx;
+
+			.sku-image {
+				width: 160rpx;
+				height: 160rpx;
+				border-radius: 10rpx;
+			}
+
+			.header-right {
+				height: 160rpx;
+			}
+
+			.close-icon {
+				position: absolute;
+				top: 10rpx;
+				right: 20rpx;
+				font-size: 46rpx;
+				opacity: 0.2;
+			}
+
+			.goods-title {
+				font-size: 28rpx;
+				font-weight: 500;
+				line-height: 42rpx;
+			}
+
+			.score-img {
+				width: 36rpx;
+				height: 36rpx;
+				margin: 0 4rpx;
+			}
+
+			.score-text {
+				font-size: 30rpx;
+				font-weight: 500;
+				color: $red;
+				font-family: OPPOSANS;
+			}
+
+			.price-text {
+				font-size: 30rpx;
+				font-weight: 500;
+				color: $red;
+				font-family: OPPOSANS;
+
+				&::before {
+					content: '¥';
+					font-size: 30rpx;
+					font-weight: 500;
+					color: $red;
+				}
+			}
+
+			.stock-text {
+				font-size: 26rpx;
+				color: #999999;
+			}
+		}
+
+		.modal-content {
+			padding: 0 20rpx;
+
+			.modal-content-scroll {
+				max-height: 600rpx;
+
+				.label-text {
+					font-size: 26rpx;
+					font-weight: 500;
+				}
+
+				.buy-num-box {
+					height: 100rpx;
+				}
+
+				.spec-btn {
+					height: 60rpx;
+					min-width: 100rpx;
+					padding: 0 30rpx;
+					background: #f4f4f4;
+					border-radius: 30rpx;
+					color: #434343;
+					font-size: 26rpx;
+					margin-right: 10rpx;
+					margin-bottom: 10rpx;
+				}
+
+				.disabled-btn {
+					font-weight: 400;
+					color: #c6c6c6;
+					background: #f8f8f8;
+				}
+			}
+		}
+	}
+</style>

+ 161 - 0
sheep/components/s-share-modal/canvas-poster/index.vue

@@ -0,0 +1,161 @@
+<!-- 海报弹窗 -->
+<template>
+  <su-popup :show="show" round="10" @close="onClosePoster" type="center" class="popup-box">
+    <view class="ss-flex-col ss-col-center ss-row-center">
+      <view
+        v-if="poster.src === ''"
+        class="poster-title ss-flex ss-row-center"
+        :style="{
+          height: poster.height + 'px',
+          width: poster.width + 'px',
+        }"
+      >
+        海报加载中...
+      </view>
+      <image
+        v-else
+        class="poster-img"
+        :src="poster.src"
+        :style="{
+          height: poster.height + 'px',
+          width: poster.width + 'px',
+        }"
+        :show-menu-by-longpress="true"
+      />
+      <canvas
+        class="hideCanvas"
+        :canvas-id="poster.canvasId"
+        :id="poster.canvasId"
+        :style="{
+          height: poster.height + 'px',
+          width: poster.width + 'px',
+        }"
+      />
+      <view
+        class="poster-btn-box ss-m-t-20 ss-flex ss-row-between ss-col-center"
+        v-if="poster.src !== ''"
+      >
+        <button class="cancel-btn ss-reset-button" @tap="onClosePoster">取消</button>
+        <button class="save-btn ss-reset-button ui-BG-Main" @tap="onSavePoster">
+          {{
+            ['wechatOfficialAccount', 'H5'].includes(sheep.$platform.name)
+              ? '长按图片保存'
+              : '保存图片'
+          }}
+        </button>
+      </view>
+    </view>
+  </su-popup>
+</template>
+
+<script setup>
+  import { reactive, getCurrentInstance } from 'vue';
+  import sheep from '@/sheep';
+  import useCanvas from './useCanvas';
+
+  const props = defineProps({
+    show: {
+      type: Boolean,
+      default: false,
+    },
+    shareInfo: {
+      type: Object,
+      default() {},
+    },
+  });
+
+  const poster = reactive({
+    canvasId: 'canvasId',
+    width: sheep.$platform.device.windowWidth * 0.9,
+    height: 600,
+    src: '',
+  });
+
+  const emits = defineEmits(['success', 'close']);
+  const vm = getCurrentInstance();
+
+  const onClosePoster = () => {
+    emits('close');
+  };
+
+  // 保存海报图片
+  const onSavePoster = () => {
+    if (['WechatOfficialAccount', 'H5'].includes(sheep.$platform.name)) {
+      sheep.$helper.toast('请长按图片保存');
+      return;
+    }
+
+    uni.saveImageToPhotosAlbum({
+      filePath: poster.src,
+      success: (res) => {
+        onClosePoster();
+        sheep.$helper.toast('保存成功');
+      },
+      fail: (err) => {
+        sheep.$helper.toast('保存失败');
+        console.log('图片保存失败:', err);
+      },
+    });
+  };
+
+  // 使用 canvas 生成海报
+  async function getPoster(params) {
+    poster.src = '';
+
+    poster.shareInfo = props.shareInfo;
+    // #ifdef APP-PLUS
+    poster.canvasId = 'canvasId-' + new Date().getTime();
+    // #endif
+    const canvas = await useCanvas(poster, vm);
+    return canvas;
+  }
+
+  defineExpose({
+    getPoster,
+  });
+</script>
+
+<style lang="scss" scoped>
+  .popup-box {
+    position: relative;
+  }
+  .poster-title {
+    color: #999;
+  }
+  // 分享海报
+  .poster-btn-box {
+    width: 600rpx;
+    position: absolute;
+    left: 50%;
+    transform: translateX(-50%);
+    bottom: -80rpx;
+    .cancel-btn {
+      width: 240rpx;
+      height: 70rpx;
+      line-height: 70rpx;
+      background: $white;
+      border-radius: 35rpx;
+      font-size: 28rpx;
+      font-weight: 500;
+      color: $dark-9;
+    }
+    .save-btn {
+      width: 240rpx;
+      height: 70rpx;
+      line-height: 70rpx;
+      border-radius: 35rpx;
+      font-size: 28rpx;
+      font-weight: 500;
+    }
+  }
+
+  .poster-img {
+    border-radius: 20rpx;
+  }
+  .hideCanvas {
+    position: fixed;
+    top: -99999rpx;
+    left: -99999rpx;
+    z-index: -99999;
+  }
+</style>

+ 121 - 0
sheep/components/s-share-modal/canvas-poster/poster/goods.js

@@ -0,0 +1,121 @@
+import sheep from '@/sheep';
+import { formatImageUrlProtocol } from './index';
+
+const goods = (poster) => {
+  const width = poster.width;
+  const userInfo = sheep.$store('user').userInfo;
+
+  return {
+    background: formatImageUrlProtocol(sheep.$url.cdn(sheep.$store('app').platform.share.posterInfo.goods_bg)),
+    list: [
+      {
+        name: 'nickname',
+        type: 'text',
+        val: userInfo.nickname,
+        x: width * 0.22,
+        y: width * 0.06,
+        paintbrushProps: {
+          fillStyle: '#333',
+          font: {
+            fontSize: 16,
+            fontFamily: 'sans-serif',
+          },
+        },
+      },
+      {
+        name: 'avatar',
+        type: 'image',
+        val: formatImageUrlProtocol(sheep.$url.cdn(userInfo.avatar)),
+        x: width * 0.04,
+        y: width * 0.04,
+        width: width * 0.14,
+        height: width * 0.14,
+        d: width * 0.14,
+      },
+      {
+        name: 'goodsImage',
+        type: 'image',
+        val: formatImageUrlProtocol(poster.shareInfo.poster.image),
+        x: width * 0.03,
+        y: width * 0.21,
+        width: width * 0.94,
+        height: width * 0.94,
+        r: 10,
+      },
+      {
+        name: 'goodsTitle',
+        type: 'text',
+        val: poster.shareInfo.poster.title,
+        x: width * 0.04,
+        y: width * 1.18,
+        maxWidth: width * 0.91,
+        line: 2,
+        lineHeight: 5,
+        paintbrushProps: {
+          fillStyle: '#333',
+          font: {
+            fontSize: 14,
+          },
+        },
+      },
+      {
+        name: 'goodsPrice',
+        type: 'text',
+        val: '¥' + poster.shareInfo.poster.price,
+        x: width * 0.04,
+        y: width * 1.3,
+        paintbrushProps: {
+          fillStyle: '#ff0000',
+          font: {
+            fontSize: 20,
+            fontFamily: 'OPPOSANS',
+          },
+        },
+      },
+      {
+        name: 'goodsOriginalPrice',
+        type: 'text',
+        val:
+          poster.shareInfo.poster.original_price > 0
+            ? '¥' + poster.shareInfo.poster.original_price
+            : '',
+        x: width * 0.3,
+        y: width * 1.32,
+        paintbrushProps: {
+          fillStyle: '#999',
+          font: {
+            fontSize: 10,
+            fontFamily: 'OPPOSANS',
+          },
+        },
+        textDecoration: {
+          line: 'line-through',
+          style: 'solide',
+        },
+      },
+      // #ifndef MP-WEIXIN
+      {
+        name: 'qrcode',
+        type: 'qrcode',
+        val: poster.shareInfo.link,
+        x: width * 0.75,
+        y: width * 1.3,
+        size: width * 0.2,
+      },
+      // #endif
+      // #ifdef MP-WEIXIN
+      {
+        name: 'wxacode',
+        type: 'image',
+        val: sheep.$api.third.wechat.getWxacode(poster.shareInfo.path),
+        x: width * 0.75,
+        y: width * 1.3,
+        width: width * 0.2,
+        height: width * 0.2,
+      },
+      // #endif
+    ],
+  };
+};
+
+export default goods;

+ 114 - 0
sheep/components/s-share-modal/canvas-poster/poster/groupon.js

@@ -0,0 +1,114 @@
+import sheep from '@/sheep';
+import { formatImageUrlProtocol } from './index';
+
+const groupon = (poster) => {
+  const width = poster.width;
+  const userInfo = sheep.$store('user').userInfo;
+
+  return {
+    background: formatImageUrlProtocol(sheep.$url.cdn(sheep.$store('app').platform.share.posterInfo.groupon_bg)),
+    list: [
+      {
+        name: 'nickname',
+        type: 'text',
+        val: userInfo.nickname,
+        x: width * 0.22,
+        y: width * 0.06,
+        paintbrushProps: {
+          fillStyle: '#333',
+          font: {
+            fontSize: 16,
+            fontFamily: 'sans-serif',
+          },
+        },
+      },
+      {
+        name: 'avatar',
+        type: 'image',
+        val: formatImageUrlProtocol(sheep.$url.cdn(userInfo.avatar)),
+        x: width * 0.04,
+        y: width * 0.04,
+        width: width * 0.14,
+        height: width * 0.14,
+        d: width * 0.14,
+      },
+      {
+        name: 'goodsImage',
+        type: 'image',
+        val: formatImageUrlProtocol(poster.shareInfo.poster.image),
+        x: width * 0.03,
+        y: width * 0.21,
+        width: width * 0.94,
+        height: width * 0.94,
+        r: 10,
+      },
+      {
+        name: 'goodsTitle',
+        type: 'text',
+        val: poster.shareInfo.poster.title,
+        x: width * 0.04,
+        y: width * 1.18,
+        maxWidth: width * 0.91,
+        line: 2,
+        lineHeight: 5,
+        paintbrushProps: {
+          fillStyle: '#333',
+          font: {
+            fontSize: 14,
+          },
+        },
+      },
+      {
+        name: 'goodsPrice',
+        type: 'text',
+        val: '¥' + poster.shareInfo.poster.price,
+        x: width * 0.04,
+        y: width * 1.3,
+        paintbrushProps: {
+          fillStyle: '#ff0000',
+          font: {
+            fontSize: 20,
+            fontFamily: 'OPPOSANS',
+          },
+        },
+      },
+      {
+        name: 'grouponNum',
+        type: 'text',
+        val: '2人团',
+        x: width * 0.3,
+        y: width * 1.32,
+        paintbrushProps: {
+          fillStyle: '#ff0000',
+          font: {
+            fontSize: 10,
+            fontFamily: 'OPPOSANS',
+          },
+        },
+      },
+      // #ifndef MP-WEIXIN
+      {
+        name: 'qrcode',
+        type: 'qrcode',
+        val: poster.shareInfo.link,
+        x: width * 0.75,
+        y: width * 1.3,
+        size: width * 0.2,
+      },
+      // #endif
+      // #ifdef MP-WEIXIN
+      {
+        name: 'wxacode',
+        type: 'image',
+        val: sheep.$api.third.wechat.getWxacode(poster.shareInfo.path),
+        x: width * 0.75,
+        y: width * 1.3,
+        width: width * 0.2,
+        height: width * 0.2,
+      },
+      // #endif
+    ],
+  };
+};
+
+export default groupon;

+ 32 - 0
sheep/components/s-share-modal/canvas-poster/poster/index.js

@@ -0,0 +1,32 @@
+import user from './user';
+import goods from './goods';
+import groupon from './groupon';
+
+export function getPosterData(options) {
+  switch (options.shareInfo.poster.type) {
+    case 'user':
+      return user(options);
+    case 'goods':
+      return goods(options);
+    case 'groupon':
+      return groupon(options);
+  }
+}
+
+export function formatImageUrlProtocol(url) {
+  // #ifdef H5
+  // H5平台 https协议下需要转换
+  if (window.location.protocol === 'https:' && url.indexOf('http:') === 0) {
+    url = url.replace('http:', 'https:');
+  }
+  // #endif
+
+  // #ifdef MP-WEIXIN
+  // 小程序平台 需要强制转换为https协议
+  if (url.indexOf('http:') === 0) {
+    url = url.replace('http:', 'https:');
+  }
+  // #endif
+
+  return url;
+}

+ 61 - 0
sheep/components/s-share-modal/canvas-poster/poster/user.js

@@ -0,0 +1,61 @@
+import sheep from '@/sheep';
+import { formatImageUrlProtocol } from './index';
+
+const user = (poster) => {
+  const width = poster.width;
+  const userInfo = sheep.$store('user').userInfo;
+
+  return {
+    background: formatImageUrlProtocol(sheep.$url.cdn(sheep.$store('app').platform.share.posterInfo.user_bg)),
+    list: [
+      {
+        name: 'nickname',
+        type: 'text',
+        val: userInfo.nickname,
+        x: width / 2,
+        y: width * 0.4,
+        paintbrushProps: {
+          textAlign: 'center',
+          fillStyle: '#333',
+          font: {
+            fontSize: 14,
+            fontFamily: 'sans-serif',
+          },
+        },
+      },
+      {
+        name: 'avatar',
+        type: 'image',
+        val: formatImageUrlProtocol(sheep.$url.cdn(userInfo.avatar)),
+        x: width * 0.4,
+        y: width * 0.16,
+        width: width * 0.2,
+        height: width * 0.2,
+        d: width * 0.2,
+      },
+      // #ifndef MP-WEIXIN
+      {
+        name: 'qrcode',
+        type: 'qrcode',
+        val: poster.shareInfo.link,
+        x: width * 0.35,
+        y: width * 0.84,
+        size: width * 0.3,
+      },
+      // #endif
+      // #ifdef MP-WEIXIN
+      {
+        name: 'wxacode',
+        type: 'image',
+        val: sheep.$api.third.wechat.getWxacode(poster.shareInfo.path),
+        x: width * 0.35,
+        y: width * 0.84,
+        width: width * 0.3,
+        height: width * 0.3,
+      },
+      // #endif
+    ],
+  };
+};
+
+export default user;

+ 87 - 0
sheep/components/s-share-modal/canvas-poster/useCanvas.js

@@ -0,0 +1,87 @@
+/**
+ * Shopro + qs-canvas 绘制海报
+ * @version 1.0.0
+ * @author lidongtony
+ * @param {Object} options - 海报参数
+ * @param {Object} vm - 自定义组件实例
+ */
+import QSCanvas from 'qs-canvas';
+import { getPosterData } from './poster';
+
+export default async function useCanvas(options, vm) {
+  const width = options.width;
+  const qsc = new QSCanvas(
+    {
+      canvasId: options.canvasId,
+      width: options.width,
+      height: options.height,
+      setCanvasWH: (canvas) => {
+        options.height = canvas.height;
+      },
+    },
+    vm,
+  );
+
+  let drawer = getPosterData(options);
+
+  // 绘制背景图
+  const background = await qsc.drawImg({
+    type: 'image',
+    val: drawer.background,
+    x: 0,
+    y: 0,
+    width,
+    mode: 'widthFix',
+    zIndex: 0,
+  });
+  await qsc.updateCanvasWH({
+    width: background.width,
+    height: background.bottom,
+  });
+
+  let list = drawer.list;
+
+  for (let i = 0; i < list.length; i++) {
+    let item = list[i];
+    // 绘制文字
+    if (item.type === 'text') {
+      await qsc.drawText(item);
+    }
+    // 绘制图片
+    if (item.type === 'image') {
+      if (item.d) {
+        qsc.setCircle({
+          x: item.x,
+          y: item.y,
+          d: item.d,
+          clip: true,
+        });
+      }
+
+      if (item.r) {
+        qsc.setRect({
+          x: item.x,
+          y: item.y,
+          height: item.height,
+          width: item.width,
+          r: item.r,
+          clip: true,
+        });
+      }
+      await qsc.drawImg(item);
+      qsc.restore();
+    }
+
+    // 绘制二维码
+    if (item.type === 'qrcode') {
+      await qsc.drawQrCode(item);
+    }
+  }
+
+  await qsc.draw();
+  // 延迟执行, 防止不稳定
+  setTimeout(async () => {
+    options.src = await qsc.toImage();
+  }, 100);
+  return options;
+}

Some files were not shown because too many files changed in this diff