Yin Bin hace 4 meses
commit
c7374f77c1
Se han modificado 100 ficheros con 17257 adiciones y 0 borrados
  1. 31 0
      .env
  2. 11 0
      .gitignore
  3. BIN
      .image/common/mall-feature.png
  4. BIN
      .image/common/mall-preview.png
  5. BIN
      .image/common/project-vs.png
  6. BIN
      .image/common/ruoyi-vue-pro-architecture.png
  7. BIN
      .image/common/ruoyi-vue-pro-biz.png
  8. BIN
      .image/common/yudao-cloud-architecture.png
  9. BIN
      .image/common/yudao-roadmap.png
  10. BIN
      .image/mall/会员详情.png
  11. BIN
      .image/mall/商品详情.png
  12. BIN
      .image/mall/店铺装修.png
  13. BIN
      .image/mall/营销中心.png
  14. BIN
      .image/mall/订单详情.png
  15. 6 0
      .prettierignore
  16. 10 0
      .prettierrc
  17. 39 0
      App.vue
  18. 21 0
      LICENSE
  19. 56 0
      README.md
  20. 3 0
      androidPrivacy.json
  21. 17 0
      index.html
  22. 9 0
      jsconfig.json
  23. 15 0
      main.js
  24. 240 0
      manifest.json
  25. 103 0
      package.json
  26. 689 0
      pages.json
  27. 538 0
      pages/activity/groupon/detail.vue
  28. 225 0
      pages/activity/groupon/list.vue
  29. 239 0
      pages/activity/groupon/order.vue
  30. 214 0
      pages/activity/index.vue
  31. 76 0
      pages/activity/point/list.vue
  32. 461 0
      pages/activity/seckill/list.vue
  33. 401 0
      pages/app/sign.vue
  34. 21 0
      pages/chat/components/goods.vue
  35. 102 0
      pages/chat/components/messageInput.vue
  36. 94 0
      pages/chat/components/messageList.vue
  37. 301 0
      pages/chat/components/messageListItem.vue
  38. 114 0
      pages/chat/components/order.vue
  39. 151 0
      pages/chat/components/select-popup.vue
  40. 166 0
      pages/chat/components/toolsPopup.vue
  41. 187 0
      pages/chat/index.vue
  42. 19 0
      pages/chat/util/constants.js
  43. 58 0
      pages/chat/util/emoji.js
  44. 160 0
      pages/commission/commission-ranking.vue
  45. 125 0
      pages/commission/components/account-info.vue
  46. 160 0
      pages/commission/components/account-type-select.vue
  47. 101 0
      pages/commission/components/commission-auth.vue
  48. 114 0
      pages/commission/components/commission-info.vue
  49. 181 0
      pages/commission/components/commission-log.vue
  50. 145 0
      pages/commission/components/commission-menu.vue
  51. 166 0
      pages/commission/goods.vue
  52. 46 0
      pages/commission/index.vue
  53. 328 0
      pages/commission/order.vue
  54. 158 0
      pages/commission/promoter.vue
  55. 602 0
      pages/commission/team.vue
  56. 518 0
      pages/commission/wallet.vue
  57. 463 0
      pages/commission/withdraw.vue
  58. 390 0
      pages/coupon/detail.vue
  59. 222 0
      pages/coupon/list.vue
  60. 190 0
      pages/goods/comment/add.vue
  61. 168 0
      pages/goods/comment/list.vue
  62. 94 0
      pages/goods/components/detail/comment-item.vue
  63. 97 0
      pages/goods/components/detail/detail-activity-tip.vue
  64. 31 0
      pages/goods/components/detail/detail-cell-sku.vue
  65. 60 0
      pages/goods/components/detail/detail-cell.vue
  66. 106 0
      pages/goods/components/detail/detail-comment-card.vue
  67. 52 0
      pages/goods/components/detail/detail-content-card.vue
  68. 256 0
      pages/goods/components/detail/detail-navbar.vue
  69. 40 0
      pages/goods/components/detail/detail-progress.vue
  70. 177 0
      pages/goods/components/detail/detail-skeleton.vue
  71. 169 0
      pages/goods/components/detail/detail-tabbar.vue
  72. 141 0
      pages/goods/components/groupon/groupon-card-list.vue
  73. 103 0
      pages/goods/components/list/list-goods-card.vue
  74. 93 0
      pages/goods/components/list/list-navbar.vue
  75. 550 0
      pages/goods/groupon.vue
  76. 669 0
      pages/goods/index.vue
  77. 407 0
      pages/goods/list.vue
  78. 480 0
      pages/goods/point.vue
  79. 564 0
      pages/goods/seckill.vue
  80. 192 0
      pages/index/cart.vue
  81. 237 0
      pages/index/category.vue
  82. 26 0
      pages/index/components/first-one.vue
  83. 66 0
      pages/index/components/first-two.vue
  84. 80 0
      pages/index/components/second-one.vue
  85. 88 0
      pages/index/index.vue
  86. 39 0
      pages/index/login.vue
  87. 51 0
      pages/index/page.vue
  88. 119 0
      pages/index/search.vue
  89. 42 0
      pages/index/user.vue
  90. 278 0
      pages/order/addressSelection.vue
  91. 351 0
      pages/order/aftersale/apply.vue
  92. 379 0
      pages/order/aftersale/detail.vue
  93. 210 0
      pages/order/aftersale/list.vue
  94. 77 0
      pages/order/aftersale/log-item.vue
  95. 38 0
      pages/order/aftersale/log.vue
  96. 195 0
      pages/order/aftersale/return-delivery.vue
  97. 507 0
      pages/order/confirm.vue
  98. 673 0
      pages/order/detail.vue
  99. 162 0
      pages/order/express/log.vue
  100. 504 0
      pages/order/list.vue

+ 31 - 0
.env

@@ -0,0 +1,31 @@
+# 版本号
+SHOPRO_VERSION=v2.3.0
+
+# 后端接口 - 正式环境(通过 process.env.NODE_ENV 非 development)
+SHOPRO_BASE_URL=http://api-dashboard.yudao.iocoder.cn
+
+# 后端接口 - 测试环境(通过 process.env.NODE_ENV = development)
+SHOPRO_DEV_BASE_URL=http://127.0.0.1:48080
+### SHOPRO_DEV_BASE_URL = http://yunai.natapp1.cc
+
+# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持 S3 服务
+SHOPRO_UPLOAD_TYPE=server
+
+# 后端接口前缀(一般不建议调整)
+SHOPRO_API_PATH=/app-api
+
+# 后端 websocket 接口前缀
+SHOPRO_WEBSOCKET_PATH=/infra/ws
+
+# 开发环境运行端口
+SHOPRO_DEV_PORT=3000
+
+# 客户端静态资源地址 空=默认使用服务端指定的CDN资源地址前缀 | local=本地  |  http(s)://xxx.xxx=自定义静态资源地址前缀
+SHOPRO_STATIC_URL=http://test.yudao.iocoder.cn
+### SHOPRO_STATIC_URL = https://file.sheepjs.com
+
+# 是否开启直播  1 开启直播 | 0 关闭直播 (小程序官方后台未审核开通直播权限时请勿开启)
+SHOPRO_MPLIVE_ON=0
+
+# 租户ID 默认 1
+SHOPRO_TENANT_ID=1

+ 11 - 0
.gitignore

@@ -0,0 +1,11 @@
+unpackage/*
+node_modules/*
+.idea/*
+deploy.sh
+.hbuilderx/
+.vscode/
+**/.DS_Store
+yarn.lock
+package-lock.json
+*.keystore
+pnpm-lock.yaml

BIN
.image/common/mall-feature.png


BIN
.image/common/mall-preview.png


BIN
.image/common/project-vs.png


BIN
.image/common/ruoyi-vue-pro-architecture.png


BIN
.image/common/ruoyi-vue-pro-biz.png


BIN
.image/common/yudao-cloud-architecture.png


BIN
.image/common/yudao-roadmap.png


BIN
.image/mall/会员详情.png


BIN
.image/mall/商品详情.png


BIN
.image/mall/店铺装修.png


BIN
.image/mall/营销中心.png


BIN
.image/mall/订单详情.png


+ 6 - 0
.prettierignore

@@ -0,0 +1,6 @@
+/unpackage/*
+/node_modules/**
+/uni_modules/**
+/public/*
+**/*.svg
+**/*.sh

+ 10 - 0
.prettierrc

@@ -0,0 +1,10 @@
+{
+  "printWidth": 100,
+  "semi": true,
+  "vueIndentScriptAndStyle": true,
+  "singleQuote": true,
+  "trailingComma": "all",
+  "proseWrap": "never",
+  "htmlWhitespaceSensitivity": "strict",
+  "endOfLine": "auto"
+}

+ 39 - 0
App.vue

@@ -0,0 +1,39 @@
+<script setup>
+  import { onLaunch, onShow, onError } from '@dcloudio/uni-app';
+  import { ShoproInit } from './sheep';
+
+  onLaunch(() => {
+    // 隐藏原生导航栏 使用自定义底部导航
+    uni.hideTabBar();
+
+    // 加载Shopro底层依赖
+    ShoproInit();
+  });
+
+  onError((err) => {
+    console.log('AppOnError:', err);
+  });
+
+  onShow((options) => {
+    // #ifdef APP-PLUS
+    // 获取urlSchemes参数
+    const args = plus.runtime.arguments;
+    if (args) {
+    } 
+
+    // 获取剪贴板
+    uni.getClipboardData({
+      success: (res) => { },
+    });
+    // #endif
+
+    // #ifdef MP-WEIXIN
+    // 确认收货回调结果
+    console.log(options,'options');
+    // #endif
+  });
+</script>
+
+<style lang="scss">
+  @import '@/sheep/scss/index.scss';
+</style>

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2022 lidongtony
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 56 - 0
README.md

@@ -0,0 +1,56 @@
+**严肃声明:现在、未来都不会有商业版本,所有代码全部开源!!**
+
+**「我喜欢写代码,乐此不疲」**  
+**「我喜欢做开源,以此为乐」**
+
+我 🐶 在上海艰苦奋斗,早中晚在 top3 大厂认真搬砖,夜里为开源做贡献。
+
+如果这个项目让你有所收获,记得 Star 关注哦,这对我是非常不错的鼓励与支持。
+
+## 🐶 新手必读
+
+* 演示地址:<https://doc.iocoder.cn/mall-preview/>
+* 启动文档:<https://doc.iocoder.cn/quick-start/>
+* 视频教程:<https://doc.iocoder.cn/video/>
+
+## 🐯 商城简介
+
+**芋道商城**,基于 [芋道开发平台](https://github.com/YunaiV/ruoyi-vue-pro) 构建,以开发者为中心,打造中国第一流的 Java 开源商城系统,全部开源,个人与企业可 100% 免费使用。
+
+> 有任何问题,或者想要的功能,可以在 Issues 中提给艿艿。
+>
+> 😜 给项目点点 Star 吧,这对我们真的很重要!
+
+![功能图](/.image/common/mall-feature.png)
+
+* 基于 uni-app + Vue3 开发,支持微信小程序、微信公众号、H5 移动端,未来会支持支付宝小程序、抖音小程序等
+* 支持 SaaS 多租户,可满足商品、订单、支付、会员、优惠券、秒杀、拼团、砍价、分销、积分等多种经营需求
+
+## 🔥 后端架构
+
+支持 Spring Boot、Spring Cloud 两种架构:
+
+① Spring Boot 单体架构:<https://doc.iocoder.cn>
+
+![架构图](/.image/common/ruoyi-vue-pro-architecture.png)
+
+② Spring Cloud 微服务架构:<https://cloud.iocoder.cn>
+
+![架构图](/.image/common/yudao-cloud-architecture.png)
+
+## 🐱 移动端预览
+
+![移动端预览](/.image/common/mall-preview.png)
+
+## 🐶 管理端预览
+
+![店铺装修](/.image/mall/店铺装修.png)
+
+![会员详情](/.image/mall/会员详情.png)
+
+![商品详情](/.image/mall/商品详情.png)
+
+![订单详情](/.image/mall/订单详情.png)
+
+![营销中心](/.image/mall/营销中心.png)
+

+ 3 - 0
androidPrivacy.json

@@ -0,0 +1,3 @@
+{
+    "prompt" : "template"
+}

+ 17 - 0
index.html

@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta
+      name="viewport"
+      content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
+    />
+    <title></title>
+    <!--preload-links-->
+    <!--app-context-->
+  </head>
+  <body>
+    <div id="app"><!--app-html--></div>
+    <script type="module" src="/main.js"></script>
+  </body>
+</html>

+ 9 - 0
jsconfig.json

@@ -0,0 +1,9 @@
+{
+  "compilerOptions": {
+    "jsx": "preserve",
+    "baseUrl": ".",
+    "paths": {
+      "@/*": ["./*"]
+    }
+  }
+}

+ 15 - 0
main.js

@@ -0,0 +1,15 @@
+import App from './App';
+import { createSSRApp } from 'vue';
+import { setupPinia } from './sheep/store';
+
+
+export function createApp() {
+
+  const app = createSSRApp(App);
+  
+  setupPinia(app);
+
+  return {
+    app,
+  };
+}

+ 240 - 0
manifest.json

@@ -0,0 +1,240 @@
+{
+  "name": "芋道商城",
+  "appid": "__UNI__460BC4C",
+  "description": "基于 uni-app + Vue3 技术驱动的在线商城系统,内含诸多功能与丰富的活动,期待您的使用和反馈。",
+  "versionName": "2.1.0",
+  "versionCode": "183",
+  "transformPx": false,
+  "app-plus": {
+    "usingComponents": true,
+    "nvueCompiler": "uni-app",
+    "nvueStyleCompiler": "uni-app",
+    "compilerVersion": 3,
+    "nvueLaunchMode": "fast",
+    "splashscreen": {
+      "alwaysShowBeforeRender": true,
+      "waiting": true,
+      "autoclose": true,
+      "delay": 0
+    },
+    "safearea": {
+      "bottom": {
+        "offset": "none"
+      }
+    },
+    "modules": {
+      "Payment": {},
+      "Share": {},
+      "VideoPlayer": {},
+      "OAuth": {}
+    },
+    "distribute": {
+      "android": {
+        "permissions": [
+          "<uses-feature android:name=\"android.hardware.camera\"/>",
+          "<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
+          "<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\"/>",
+          "<uses-permission android:name=\"android.permission.VIBRATE\"/>",
+          "<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>",
+          "<uses-permission android:name=\"android.permission.ACCESS_MOCK_LOCATION\"/>",
+          "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
+          "<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
+          "<uses-permission android:name=\"android.permission.CALL_PHONE\"/>",
+          "<uses-permission android:name=\"android.permission.CAMERA\"/>",
+          "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
+          "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
+          "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
+          "<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
+          "<uses-permission android:name=\"android.permission.GET_TASKS\"/>",
+          "<uses-permission android:name=\"android.permission.INTERNET\"/>",
+          "<uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\"/>",
+          "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
+          "<uses-permission android:name=\"android.permission.READ_CONTACTS\"/>",
+          "<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
+          "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
+          "<uses-permission android:name=\"android.permission.READ_SMS\"/>",
+          "<uses-permission android:name=\"android.permission.RECEIVE_BOOT_COMPLETED\"/>",
+          "<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>",
+          "<uses-permission android:name=\"android.permission.SEND_SMS\"/>",
+          "<uses-permission android:name=\"android.permission.SYSTEM_ALERT_WINDOW\"/>",
+          "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
+          "<uses-permission android:name=\"android.permission.WRITE_CONTACTS\"/>",
+          "<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/>",
+          "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>",
+          "<uses-permission android:name=\"android.permission.WRITE_SMS\"/>",
+          "<uses-permission android:name=\"android.permission.RECEIVE_USER_PRESENT\"/>"
+        ],
+        "minSdkVersion": 21,
+        "schemes": "shopro"
+      },
+      "ios": {
+        "urlschemewhitelist": [
+          "baidumap",
+          "iosamap"
+        ],
+        "dSYMs": false,
+        "privacyDescription": {
+          "NSPhotoLibraryUsageDescription": "需要同意访问您的相册选取图片才能完善该条目",
+          "NSPhotoLibraryAddUsageDescription": "需要同意访问您的相册才能保存该图片",
+          "NSCameraUsageDescription": "需要同意访问您的摄像头拍摄照片才能完善该条目",
+          "NSUserTrackingUsageDescription": "开启追踪并不会获取您在其它站点的隐私信息,该行为仅用于标识设备,保障服务安全和提升浏览体验"
+        },
+        "urltypes": "shopro",
+        "capabilities": {
+          "entitlements": {
+            "com.apple.developer.associated-domains": [
+              "applinks:shopro.sheepjs.com"
+            ]
+          }
+        },
+        "idfa": true
+      },
+      "sdkConfigs": {
+        "speech": {
+          "ifly": {}
+        },
+        "ad": {},
+        "oauth": {
+          "apple": {},
+          "weixin": {
+            "appid": "wxae7a0c156da9383b",
+            "UniversalLinks": "https://shopro.sheepjs.com/uni-universallinks/__UNI__082C0BA/"
+          }
+        },
+        "payment": {
+          "weixin": {
+            "__platform__": [
+              "ios",
+              "android"
+            ],
+            "appid": "wxae7a0c156da9383b",
+            "UniversalLinks": "https://shopro.sheepjs.com/uni-universallinks/__UNI__082C0BA/"
+          },
+          "alipay": {
+            "__platform__": [
+              "ios",
+              "android"
+            ]
+          }
+        },
+        "share": {
+          "weixin": {
+            "appid": "wxae7a0c156da9383b",
+            "UniversalLinks": "https://shopro.sheepjs.com/uni-universallinks/__UNI__082C0BA/"
+          }
+        }
+      },
+      "orientation": [
+        "portrait-primary"
+      ],
+      "splashscreen": {
+        "androidStyle": "common",
+        "iosStyle": "common",
+        "useOriginalMsgbox": true
+      },
+      "icons": {
+        "android": {
+          "hdpi": "unpackage/res/icons/72x72.png",
+          "xhdpi": "unpackage/res/icons/96x96.png",
+          "xxhdpi": "unpackage/res/icons/144x144.png",
+          "xxxhdpi": "unpackage/res/icons/192x192.png"
+        },
+        "ios": {
+          "appstore": "unpackage/res/icons/1024x1024.png",
+          "ipad": {
+            "app": "unpackage/res/icons/76x76.png",
+            "app@2x": "unpackage/res/icons/152x152.png",
+            "notification": "unpackage/res/icons/20x20.png",
+            "notification@2x": "unpackage/res/icons/40x40.png",
+            "proapp@2x": "unpackage/res/icons/167x167.png",
+            "settings": "unpackage/res/icons/29x29.png",
+            "settings@2x": "unpackage/res/icons/58x58.png",
+            "spotlight": "unpackage/res/icons/40x40.png",
+            "spotlight@2x": "unpackage/res/icons/80x80.png"
+          },
+          "iphone": {
+            "app@2x": "unpackage/res/icons/120x120.png",
+            "app@3x": "unpackage/res/icons/180x180.png",
+            "notification@2x": "unpackage/res/icons/40x40.png",
+            "notification@3x": "unpackage/res/icons/60x60.png",
+            "settings@2x": "unpackage/res/icons/58x58.png",
+            "settings@3x": "unpackage/res/icons/87x87.png",
+            "spotlight@2x": "unpackage/res/icons/80x80.png",
+            "spotlight@3x": "unpackage/res/icons/120x120.png"
+          }
+        }
+      }
+    }
+  },
+  "quickapp": {},
+  "quickapp-native": {
+    "icon": "/static/logo.png",
+    "package": "com.example.demo",
+    "features": [
+      {
+        "name": "system.clipboard"
+      }
+    ]
+  },
+  "quickapp-webview": {
+    "icon": "/static/logo.png",
+    "package": "com.example.demo",
+    "minPlatformVersion": 1070,
+    "versionName": "1.0.0",
+    "versionCode": 100
+  },
+  "mp-weixin": {
+    "appid": "wx66186af0759f47c9",
+    "setting": {
+      "urlCheck": false,
+      "minified": true,
+      "postcss": false,
+      "es6": false
+    },
+    "optimization": {
+      "subPackages": true
+    },
+    "plugins": {},
+    "lazyCodeLoading": "requiredComponents",
+    "usingComponents": {},
+    "permission": {},
+    "requiredPrivateInfos": [
+      "chooseAddress"
+    ]
+  },
+  "mp-alipay": {
+    "usingComponents": true
+  },
+  "mp-baidu": {
+    "usingComponents": true
+  },
+  "mp-toutiao": {
+    "usingComponents": true
+  },
+  "mp-jd": {
+    "usingComponents": true
+  },
+  "h5": {
+    "template": "index.html",
+    "router": {
+      "mode": "history",
+      "base": "/"
+    },
+    "sdkConfigs": {
+      "maps": {}
+    },
+    "async": {
+      "timeout": 20000
+    },
+    "title": "芋道商城",
+    "optimization": {
+      "treeShaking": {
+        "enable": true
+      }
+    }
+  },
+  "vueVersion": "3",
+  "_spaceID": "192b4892-5452-4e1d-9f09-eee1ece40639",
+  "locale": "zh-Hans",
+  "fallbackLocale": "zh-Hans"
+}

+ 103 - 0
package.json

@@ -0,0 +1,103 @@
+{
+  "id": "shopro",
+  "name": "shopro",
+  "displayName": "芋道商城",
+  "version": "2.3.0",
+  "description": "芋道商城,一套代码,同时发行到iOS、Android、H5、微信小程序多个平台,请使用手机扫码快速体验强大功能",
+  "scripts": {
+    "prettier": "prettier --write  \"{pages,sheep}/**/*.{js,json,tsx,css,less,scss,vue,html,md}\""
+  },
+  "repository": "https://github.com/sheepjs/shop.git",
+  "keywords": [
+    "商城",
+    "B2C",
+    "商城模板"
+  ],
+  "author": "",
+  "license": "MIT",
+  "bugs": {
+    "url": "https://github.com/sheepjs/shop/issues"
+  },
+  "homepage": "https://github.com/dcloudio/hello-uniapp#readme",
+  "dcloudext": {
+    "category": [
+      "前端页面模板",
+      "uni-app前端项目模板"
+    ],
+    "sale": {
+      "regular": {
+        "price": "0.00"
+      },
+      "sourcecode": {
+        "price": "0.00"
+      }
+    },
+    "contact": {
+      "qq": ""
+    },
+    "declaration": {
+      "ads": "无",
+      "data": "无",
+      "permissions": "无"
+    },
+    "npmurl": ""
+  },
+  "uni_modules": {
+    "dependencies": [],
+    "encrypt": [],
+    "platforms": {
+      "cloud": {
+        "tcb": "u",
+        "aliyun": "u"
+      },
+      "client": {
+        "App": {
+          "app-vue": "y",
+          "app-nvue": "u"
+        },
+        "H5-mobile": {
+          "Safari": "y",
+          "Android Browser": "y",
+          "微信浏览器(Android)": "y",
+          "QQ浏览器(Android)": "y"
+        },
+        "H5-pc": {
+          "Chrome": "y",
+          "IE": "y",
+          "Edge": "y",
+          "Firefox": "y",
+          "Safari": "y"
+        },
+        "小程序": {
+          "微信": "y",
+          "阿里": "u",
+          "百度": "u",
+          "字节跳动": "u",
+          "QQ": "u",
+          "京东": "u"
+        },
+        "快应用": {
+          "华为": "u",
+          "联盟": "u"
+        },
+        "Vue": {
+          "vue2": "u",
+          "vue3": "y"
+        }
+      }
+    }
+  },
+  "dependencies": {
+    "dayjs": "^1.11.7",
+    "lodash": "^4.17.21",
+    "lodash-es": "^4.17.21",
+    "luch-request": "^3.0.8",
+    "pinia": "^2.0.33",
+    "pinia-plugin-persist-uni": "^1.2.0",
+    "weixin-js-sdk": "^1.6.0"
+  },
+  "devDependencies": {
+    "prettier": "^2.8.7",
+    "vconsole": "^3.15.0"
+  }
+}

+ 689 - 0
pages.json

@@ -0,0 +1,689 @@
+{
+	"easycom": {
+		"autoscan": true,
+		"custom": {
+			"^s-(.*)": "@/sheep/components/s-$1/s-$1.vue",
+			"^su-(.*)": "@/sheep/ui/su-$1/su-$1.vue"
+		}
+	},
+	"pages": [{
+			"path": "pages/index/index",
+			"aliasPath": "/",
+			"style": {
+				"navigationBarTitleText": "首页",
+				"enablePullDownRefresh": true
+			},
+			"meta": {
+				"auth": false,
+				"sync": true,
+				"title": "首页",
+				"group": "商城"
+			}
+		},
+		{
+			"path": "pages/index/user",
+			"style": {
+				"navigationBarTitleText": "个人中心",
+				"enablePullDownRefresh": true
+			},
+			"meta": {
+				"sync": true,
+				"title": "个人中心",
+				"group": "商城"
+			}
+		},
+		{
+			"path": "pages/index/category",
+			"style": {
+				"navigationBarTitleText": "商品分类"
+			},
+			"meta": {
+				"sync": true,
+				"title": "商品分类",
+				"group": "商城"
+			}
+		},
+		{
+			"path": "pages/index/cart",
+			"style": {
+				"navigationBarTitleText": "购物车"
+			},
+			"meta": {
+				"sync": true,
+				"title": "购物车",
+				"group": "商城"
+			}
+		},
+		{
+			"path": "pages/index/login",
+			"style": {
+				"navigationBarTitleText": "登录"
+			}
+		},
+		{
+			"path": "pages/index/search",
+			"style": {
+				"navigationBarTitleText": "搜索"
+			},
+			"meta": {
+				"sync": true,
+				"title": "搜索",
+				"group": "商城"
+			}
+		},
+		{
+			"path": "pages/index/page",
+			"style": {
+				"navigationBarTitleText": ""
+			},
+			"meta": {
+				"auth": false,
+				"sync": true,
+				"title": "自定义页面",
+				"group": "商城"
+			}
+		}
+	],
+	"subPackages": [{
+			"root": "pages/goods",
+			"pages": [{
+					"path": "index",
+					"style": {
+						"navigationBarTitleText": "商品详情"
+					},
+					"meta": {
+						"sync": true,
+						"title": "普通商品",
+						"group": "商品"
+					}
+				},
+				{
+					"path": "groupon",
+					"style": {
+						"navigationBarTitleText": "拼团商品"
+					},
+					"meta": {
+						"sync": true,
+						"title": "拼团商品",
+						"group": "商品"
+					}
+				},
+
+				{
+					"path": "seckill",
+					"style": {
+						"navigationBarTitleText": "秒杀商品"
+					},
+					"meta": {
+						"sync": true,
+						"title": "秒杀商品",
+						"group": "商品"
+					}
+				},
+                {
+                    "path": "point",
+                    "style": {
+                      "navigationBarTitleText": "积分商品"
+                    },
+                    "meta": {
+                      "sync": true,
+                      "title": "积分商品",
+                      "group": "商品"
+                    }
+                },
+				{
+					"path": "list",
+					"style": {
+						"navigationBarTitleText": "商品列表"
+					},
+					"meta": {
+						"sync": true,
+						"title": "商品列表",
+						"group": "商品"
+					}
+				},
+				{
+					"path": "comment/add",
+					"style": {
+						"navigationBarTitleText": "评价商品"
+					},
+					"meta": {
+						"auth": true
+					}
+				},
+				{
+					"path": "comment/list",
+					"style": {
+						"navigationBarTitleText": "商品评价"
+					}
+				}
+			]
+		},
+		{
+			"root": "pages/order",
+			"pages": [{
+					"path": "detail",
+					"style": {
+						"navigationBarTitleText": "订单详情"
+					},
+					"meta": {
+						"auth": true,
+						"title": "订单详情"
+					}
+				},
+				{
+					"path": "confirm",
+					"style": {
+						"navigationBarTitleText": "确认订单"
+					},
+					"meta": {
+						"auth": true,
+						"title": "确认订单"
+					}
+				},
+				{
+					"path": "list",
+					"style": {
+						"navigationBarTitleText": "我的订单",
+						"enablePullDownRefresh": true
+					},
+					"meta": {
+						"auth": true,
+						"sync": true,
+						"title": "用户订单",
+						"group": "订单中心"
+					}
+				},
+				{
+					"path": "aftersale/apply",
+					"style": {
+						"navigationBarTitleText": "申请售后"
+					},
+					"meta": {
+						"auth": true,
+						"title": "申请售后"
+					}
+				},
+                {
+                  "path": "aftersale/return-delivery",
+                  "style": {
+                    "navigationBarTitleText": "退货物流"
+                  },
+                  "meta": {
+                    "auth": true,
+                    "title": "退货物流"
+                  }
+                },
+				{
+					"path": "aftersale/list",
+					"style": {
+						"navigationBarTitleText": "售后列表"
+					},
+					"meta": {
+						"auth": true,
+						"sync": true,
+						"title": "售后订单",
+						"group": "订单中心"
+					}
+				},
+				{
+					"path": "aftersale/detail",
+					"style": {
+						"navigationBarTitleText": "售后详情"
+					},
+					"meta": {
+						"auth": true,
+						"title": "售后详情"
+					}
+				},
+				{
+					"path": "aftersale/log",
+					"style": {
+						"navigationBarTitleText": "售后进度"
+					},
+					"meta": {
+						"auth": true,
+						"title": "售后进度"
+					}
+				},
+				{
+					"path": "express/log",
+					"style": {
+						"navigationBarTitleText": "物流轨迹"
+					},
+					"meta": {
+						"auth": true,
+						"title": "物流轨迹"
+					}
+				}
+			]
+		},
+		{
+			"root": "pages/user",
+			"pages": [{
+					"path": "info",
+					"style": {
+						"navigationBarTitleText": "我的信息"
+					},
+					"meta": {
+						"auth": true,
+						"sync": true,
+						"title": "用户信息",
+						"group": "用户中心"
+					}
+				},
+				{
+					"path": "goods-collect",
+					"style": {
+						"navigationBarTitleText": "我的收藏"
+					},
+					"meta": {
+						"auth": true,
+						"sync": true,
+						"title": "商品收藏",
+						"group": "用户中心"
+					}
+				},
+				{
+					"path": "goods-log",
+					"style": {
+						"navigationBarTitleText": "我的足迹"
+					},
+					"meta": {
+						"auth": true,
+						"sync": true,
+						"title": "浏览记录",
+						"group": "用户中心"
+					}
+				},
+				{
+					"path": "address/list",
+					"style": {
+						"navigationBarTitleText": "收货地址"
+					},
+					"meta": {
+						"auth": true,
+						"sync": true,
+						"title": "地址管理",
+						"group": "用户中心"
+					}
+				},
+				{
+					"path": "address/edit",
+					"style": {
+						"navigationBarTitleText": "编辑地址"
+					},
+					"meta": {
+						"auth": true,
+						"title": "编辑地址"
+					}
+				},
+                {
+                  "path": "goods_details_store/index",
+                  "style": {
+                    "navigationBarTitleText": "自提门店"
+                  },
+                  "meta": {
+                    "auth": true,
+                    "sync": true,
+                    "title": "地址管理",
+                    "group": "用户中心"
+                  }
+                },
+				{
+					"path": "wallet/money",
+					"style": {
+						"navigationBarTitleText": "我的余额"
+					},
+					"meta": {
+						"auth": true,
+						"sync": true,
+						"title": "用户余额",
+						"group": "用户中心"
+					}
+				},
+				{
+					"path": "wallet/score",
+					"style": {
+						"navigationBarTitleText": "我的积分"
+					},
+					"meta": {
+						"auth": true,
+						"sync": true,
+						"title": "用户积分",
+						"group": "用户中心"
+					}
+				}
+			]
+		},
+		{
+			"root": "pages/commission",
+			"pages": [{
+					"path": "index",
+					"style": {
+						"navigationBarTitleText": "分销"
+					},
+					"meta": {
+						"auth": true,
+						"sync": true,
+						"title": "分销中心",
+						"group": "分销商城"
+					}
+				},
+				{
+					"path": "wallet",
+					"style": {
+						"navigationBarTitleText": "我的佣金"
+					},
+					"meta": {
+						"auth": true,
+						"sync": true,
+						"title": "用户佣金",
+						"group": "分销中心"
+					}
+				},
+				{
+					"path": "goods",
+					"style": {
+						"navigationBarTitleText": "推广商品"
+					},
+					"meta": {
+						"auth": true,
+						"sync": true,
+						"title": "推广商品",
+						"group": "分销商城"
+					}
+				},
+				{
+					"path": "order",
+					"style": {
+						"navigationBarTitleText": "分销订单"
+					},
+					"meta": {
+						"auth": true,
+						"sync": true,
+						"title": "分销订单",
+						"group": "分销商城"
+					}
+				},
+				{
+					"path": "team",
+					"style": {
+						"navigationBarTitleText": "我的团队"
+					},
+					"meta": {
+						"auth": true,
+						"sync": true,
+						"title": "我的团队",
+						"group": "分销商城"
+					}
+				}, {
+					"path": "promoter",
+					"style": {
+						"navigationBarTitleText": "推广人排行榜"
+					},
+					"meta": {
+						"auth": true,
+						"sync": true,
+						"title": "推广人排行榜",
+						"group": "分销商城"
+					}
+				}, {
+					"path": "commission-ranking",
+					"style": {
+						"navigationBarTitleText": "佣金排行榜"
+					},
+					"meta": {
+						"auth": true,
+						"sync": true,
+						"title": "佣金排行榜",
+						"group": "分销商城"
+					}
+				}, {
+                "path": "withdraw",
+                "style": {
+                  "navigationBarTitleText": "申请提现"
+                },
+                "meta": {
+                  "auth": true,
+                  "sync": true,
+                  "title": "申请提现",
+                  "group": "分销商城"
+                }
+              }
+			]
+		},
+		{
+			"root": "pages/app",
+			"pages": [{
+				"path": "sign",
+				"style": {
+					"navigationBarTitleText": "签到中心"
+				},
+				"meta": {
+					"auth": true,
+					"sync": true,
+					"title": "签到中心",
+					"group": "应用"
+				}
+			}]
+		},
+		{
+			"root": "pages/public",
+			"pages": [{
+					"path": "setting",
+					"style": {
+						"navigationBarTitleText": "系统设置"
+					},
+					"meta": {
+						"sync": true,
+						"title": "系统设置",
+						"group": "通用"
+					}
+				},
+				{
+					"path": "richtext",
+					"style": {
+						"navigationBarTitleText": "富文本"
+					},
+					"meta": {
+						"sync": true,
+						"title": "富文本",
+						"group": "通用"
+					}
+				},
+				{
+					"path": "faq",
+					"style": {
+						"navigationBarTitleText": "常见问题"
+					},
+					"meta": {
+						"sync": true,
+						"title": "常见问题",
+						"group": "通用"
+					}
+				},
+				{
+					"path": "error",
+					"style": {
+						"navigationBarTitleText": "错误页面"
+					}
+				},
+				{
+					"path": "webview",
+					"style": {
+						"navigationBarTitleText": ""
+					}
+				}
+			]
+		},
+		{
+			"root": "pages/coupon",
+			"pages": [{
+					"path": "list",
+					"style": {
+						"navigationBarTitleText": "领券中心"
+					},
+					"meta": {
+						"sync": true,
+						"title": "领券中心",
+						"group": "优惠券"
+					}
+				},
+				{
+					"path": "detail",
+					"style": {
+						"navigationBarTitleText": "优惠券"
+					},
+					"meta": {
+						"auth": false,
+						"sync": true,
+						"title": "优惠券详情",
+						"group": "优惠券"
+					}
+				}
+			]
+		},
+		{
+			"root": "pages/chat",
+			"pages": [{
+				"path": "index",
+				"style": {
+					"navigationBarTitleText": "客服"
+				},
+				"meta": {
+					"auth": true,
+					"sync": true,
+					"title": "客服",
+					"group": "客服"
+				}
+			}]
+		},
+		{
+			"root": "pages/pay",
+			"pages": [{
+					"path": "index",
+					"style": {
+						"navigationBarTitleText": "收银台"
+					}
+				},
+				{
+					"path": "result",
+					"style": {
+						"navigationBarTitleText": "支付结果"
+					}
+				},
+				{
+					"path": "recharge",
+					"style": {
+						"navigationBarTitleText": "充值余额"
+					},
+					"meta": {
+						"auth": true,
+						"sync": true,
+						"title": "充值余额",
+						"group": "支付"
+					}
+				},
+				{
+					"path": "recharge-log",
+					"style": {
+						"navigationBarTitleText": "充值记录"
+					},
+					"meta": {
+						"auth": true,
+						"sync": true,
+						"title": "充值记录",
+						"group": "支付"
+					}
+				}
+			]
+		},
+		{
+			"root": "pages/activity",
+			"pages": [{
+					"path": "groupon/detail",
+					"style": {
+						"navigationBarTitleText": "拼团详情"
+					}
+				},
+				{
+					"path": "groupon/order",
+					"style": {
+						"navigationBarTitleText": "我的拼团",
+						"enablePullDownRefresh": true
+					},
+					"meta": {
+						"auth": true,
+						"sync": true,
+						"title": "拼团订单",
+						"group": "营销活动"
+					}
+				},
+				{
+					"path": "index",
+					"style": {
+						"navigationBarTitleText": "营销商品"
+					},
+					"meta": {
+						"sync": true,
+						"title": "营销商品",
+						"group": "营销活动"
+					}
+				},
+				{
+					"path": "groupon/list",
+					"style": {
+						"navigationBarTitleText": "拼团活动"
+					},
+					"meta": {
+						"sync": true,
+						"title": "拼团活动",
+						"group": "营销活动"
+					}
+				},
+				{
+					"path": "seckill/list",
+					"style": {
+						"navigationBarTitleText": "秒杀活动"
+					},
+					"meta": {
+						"sync": true,
+						"title": "秒杀活动",
+						"group": "营销活动"
+					}
+				},
+                {
+                  "path": "point/list",
+                  "style": {
+                    "navigationBarTitleText": "积分商城"
+                  },
+                  "meta": {
+                    "sync": true,
+                    "title": "积分商城",
+                    "group": "营销活动"
+                  }
+                }
+			]
+		}
+	],
+	"globalStyle": {
+		"navigationBarTextStyle": "black",
+		"navigationBarTitleText": "芋道商城",
+		"navigationBarBackgroundColor": "#FFFFFF",
+		"backgroundColor": "#FFFFFF",
+		"navigationStyle": "custom"
+	},
+	"tabBar": {
+		"list": [{
+				"pagePath": "pages/index/index"
+			},
+			{
+				"pagePath": "pages/index/cart"
+			},
+			{
+				"pagePath": "pages/index/user"
+			}
+		]
+	}
+}

+ 538 - 0
pages/activity/groupon/detail.vue

@@ -0,0 +1,538 @@
+<!-- 拼团订单的详情 -->
+<template>
+  <s-layout
+    title="拼团详情"
+    class="detail-wrap"
+    :navbar="state.data && !state.loading ? 'inner' : 'normal'"
+    :onShareAppMessage="shareInfo"
+  >
+    <view v-if="state.loading"></view>
+    <view v-if="state.data && !state.loading">
+      <!-- 团长信息 + 活动信息 -->
+      <view
+        class="recharge-box"
+        v-if="state.data.headRecord"
+        :style="[
+          {
+            marginTop: '-' + Number(statusBarHeight + 88) + 'rpx',
+            paddingTop: Number(statusBarHeight + 108) + 'rpx',
+          },
+        ]"
+      >
+        <s-goods-item
+          class="goods-box"
+          :img="state.data.headRecord.picUrl"
+          :title="state.data.headRecord.spuName"
+          :price="state.data.headRecord.combinationPrice"
+          priceColor="#E1212B"
+          @tap="
+            sheep.$router.go('/pages/goods/groupon', {
+              id: state.data.headRecord.activityId,
+            })
+          "
+          :style="[{ top: Number(statusBarHeight + 108) + 'rpx' }]"
+        >
+          <template #groupon>
+            <view class="ss-flex">
+              <view class="sales-title">{{ state.data.headRecord.userSize }}人团</view>
+              <view class="num-title ss-m-l-20">已拼{{ state.data.headRecord.userCount }}件</view>
+            </view>
+          </template>
+        </s-goods-item>
+      </view>
+
+      <view class="countdown-box detail-card ss-p-t-44 ss-flex-col ss-col-center">
+        <!-- 情况一:拼团成功 -->
+        <view v-if="state.data.headRecord.status === 1">
+          <view v-if="state.data.orderId">
+            <view class="countdown-title ss-flex">
+              <text class="cicon-check-round" />
+              恭喜您~拼团成功
+            </view>
+          </view>
+          <view v-else>
+            <view class="countdown-title ss-flex">
+              <text class="cicon-info" />
+              抱歉~该团已满员
+            </view>
+          </view>
+        </view>
+
+        <!-- 情况二:拼团失败 -->
+        <view v-if="state.data.headRecord.status === 2">
+          <view class="countdown-title ss-flex">
+            <text class="cicon-info"></text>
+            {{ state.data.orderId ? '拼团超时,已自动退款' : '该团已解散' }}
+          </view>
+        </view>
+
+        <!-- 情况三:拼团进行中 -->
+        <view v-if="state.data.headRecord.status === 0">
+          <view v-if="state.data.headRecord.expireTime <= new Date().getTime()">
+            <view class="countdown-title ss-flex">
+              <text class="cicon-info"></text>
+              拼团已结束,请关注下次活动
+            </view>
+          </view>
+          <view class="countdown-title ss-flex" v-else>
+            还差
+            <view class="num"
+              >{{ state.data.headRecord.userSize - state.data.headRecord.userCount }}人</view
+            >
+            拼团成功
+            <view class="ss-flex countdown-time">
+              <view class="countdown-h ss-flex ss-row-center">{{ endTime.h }}</view>
+              <view class="ss-m-x-4">:</view>
+              <view class="countdown-num ss-flex ss-row-center">
+                {{ endTime.m }}
+              </view>
+              <view class="ss-m-x-4">:</view>
+              <view class="countdown-num ss-flex ss-row-center">
+                {{ endTime.s }}
+              </view>
+            </view>
+          </view>
+        </view>
+
+        <!-- 拼团的记录列表,展示每个参团人 -->
+        <view class="ss-m-t-60 ss-flex ss-flex-wrap ss-row-center">
+          <!-- 团长 -->
+          <view class="header-avatar ss-m-r-24 ss-m-b-20">
+            <image :src="sheep.$url.cdn(state.data.headRecord.avatar)" class="avatar-img"></image>
+            <view class="header-tag ss-flex ss-col-center ss-row-center">团长</view>
+          </view>
+          <!-- 团员 -->
+          <view
+            class="header-avatar ss-m-r-24 ss-m-b-20"
+            v-for="item in state.data.memberRecords"
+            :key="item.id"
+          >
+            <image :src="sheep.$url.cdn(item.avatar)" class="avatar-img"></image>
+            <view
+              class="header-tag ss-flex ss-col-center ss-row-center"
+              v-if="item.is_leader == '1'"
+            >
+              团长
+            </view>
+          </view>
+          <!-- 还有几个坑位 -->
+          <view
+            class="default-avatar ss-m-r-24 ss-m-b-20"
+            v-for="item in state.remainNumber"
+            :key="item"
+          >
+            <image
+              :src="sheep.$url.static('/static/img/shop/avatar/unknown.png')"
+              class="avatar-img"
+            ></image>
+          </view>
+        </view>
+      </view>
+
+      <!-- 情况一:拼团成功;情况二:拼团失败 -->
+      <view
+        v-if="state.data.headRecord.status === 1 || state.data.headRecord.status === 2"
+        class="ss-m-t-40 ss-flex ss-row-center"
+      >
+        <button
+          class="ss-reset-button order-btn"
+          v-if="state.data.orderId"
+          @tap="onDetail(state.data.orderId)"
+        >
+          查看订单
+        </button>
+        <button class="ss-reset-button join-btn" v-else @tap="onCreateGroupon"> 我要开团 </button>
+      </view>
+
+      <!-- 情况三:拼团进行中,查看订单或参加或邀请好友或参加 -->
+      <view v-if="state.data.headRecord.status === 0" class="ss-m-t-40 ss-flex ss-row-center">
+        <view v-if="state.data.headRecord.expireTime <= new Date().getTime()">
+          <button
+            class="ss-reset-button join-btn"
+            v-if="state.data.orderId"
+            @tap="onDetail(state.data.orderId)"
+          >
+            查看订单
+          </button>
+          <button
+            class="ss-reset-button disabled-btn"
+            v-else
+            disabled
+            @tap="onDetail(state.data.orderId)"
+          >
+            去参团
+          </button>
+        </view>
+        <view v-else class="ss-flex ss-row-center">
+          <view v-if="state.data.orderId">
+            <button class="ss-reset-button join-btn" :disabled="endTime.ms <= 0" @tap="onShare">
+              邀请好友来拼团
+            </button>
+          </view>
+          <view v-else>
+            <button
+              class="ss-reset-button join-btn"
+              :disabled="endTime.ms <= 0"
+              @tap="onJoinGroupon()"
+            >
+              立即参团
+            </button>
+          </view>
+        </view>
+      </view>
+
+      <view v-if="!isEmpty(state.goodsInfo)">
+        <!-- 规格与数量弹框 -->
+        <s-select-groupon-sku
+          :show="state.showSelectSku"
+          :goodsInfo="state.goodsInfo"
+          :grouponAction="state.grouponAction"
+          :grouponNum="state.grouponNum"
+          @buy="onBuy"
+          @change="onSkuChange"
+          @close="state.showSelectSku = false"
+        />
+      </view>
+
+    </view>
+
+    <s-empty v-if="!state.data && !state.loading" icon="/static/goods-empty.png" />
+  </s-layout>
+</template>
+
+<script setup>
+  import { computed, reactive } from 'vue';
+  import sheep from '@/sheep';
+  import { onLoad } from '@dcloudio/uni-app';
+  import { fen2yuan, useDurationTime } from '@/sheep/hooks/useGoods';
+  import { showShareModal } from '@/sheep/hooks/useModal';
+  import { isEmpty } from 'lodash-es';
+  import CombinationApi from '@/sheep/api/promotion/combination';
+  import SpuApi from '@/sheep/api/product/spu';
+
+  const headerBg = sheep.$url.css('/static/img/shop/user/withdraw_bg.png');
+  const statusBarHeight = sheep.$platform.device.statusBarHeight * 2;
+  const state = reactive({
+    data: {}, // 拼团详情
+    goodsId: 0, // 商品ID
+    goodsInfo: {}, // 商品信息
+    showSelectSku: false, // 显示规格弹框
+    selectedSkuPrice: {}, // 选中的规格价格
+    activity: {}, // 团购活动
+    grouponId: 0, // 团购ID
+    grouponNum: 0, // 团购人数
+    grouponAction: 'create', // 团购操作
+    combinationHeadId: null, // 拼团团长编号
+    loading: true,
+  });
+
+  const shareInfo = computed(() => {
+    if (isEmpty(state.data)) return {};
+    return sheep.$platform.share.getShareInfo(
+      {
+        title: state.data.headRecord.spuName,
+        image: sheep.$url.cdn(state.data.headRecord.picUrl),
+        desc: state.data.goods?.subtitle,
+        params: {
+          page: '5',
+          query: state.data.headRecord.id,
+        },
+      },
+      {
+        type: 'groupon', // 邀请拼团海报
+        title: state.data.headRecord.spuName, // 商品标题
+        image: sheep.$url.cdn(state.data.headRecord.picUrl), // 商品主图
+        price: fen2yuan(state.data.headRecord.combinationPrice), // 商品价格
+      },
+    );
+  });
+
+  // 订单详情
+  function onDetail(orderId) {
+    sheep.$router.go('/pages/order/detail', {
+      id: orderId,
+    });
+  }
+
+  // 去开团
+  function onCreateGroupon() {
+    state.grouponAction = 'create';
+    state.grouponId = 0;
+    state.showSelectSku = true;
+  }
+
+  // 规格变更
+  function onSkuChange(e) {
+    state.selectedSkuPrice = e;
+  }
+
+  // 立即参团
+  function onJoinGroupon() {
+    state.grouponAction = 'join';
+    state.grouponId = state.data.headRecord.activityId;
+    state.combinationHeadId = state.data.headRecord.id;
+    state.grouponNum = state.data.headRecord.userSize;
+    state.showSelectSku = true;
+  }
+
+  // 立即购买
+  function onBuy(sku) {
+    sheep.$router.go('/pages/order/confirm', {
+      data: JSON.stringify({
+        order_type: 'goods',
+        combinationActivityId: state.activity.id,
+        combinationHeadId: state.combinationHeadId,
+        items: [
+          {
+            skuId: sku.id,
+            count: sku.count,
+          },
+        ],
+      }),
+    });
+  }
+
+  const endTime = computed(() => {
+    return useDurationTime(state.data.headRecord.expireTime);
+  });
+
+  // 获取拼团团队详情
+  async function getGrouponDetail(id) {
+    const { code, data } = await CombinationApi.getCombinationRecordDetail(id);
+    if (code === 0) {
+      state.data = data;
+      const remainNumber = Number(state.data.headRecord.userSize - state.data.headRecord.userCount);
+      state.remainNumber = remainNumber > 0 ? remainNumber : 0;
+
+      // 获取活动信息
+      const { data: activity } = await CombinationApi.getCombinationActivity(
+        data.headRecord.activityId,
+      );
+      state.activity = activity;
+      state.grouponNum = activity.userSize;
+      // 加载商品信息
+      const { data: spu } = await SpuApi.getSpuDetail(activity.spuId);
+      state.goodsId = spu.id;
+      // 默认显示最低价
+      activity.products.forEach((product) => {
+        spu.price = Math.min(spu.price, product.combinationPrice); // 设置 SPU 的最低价格
+      });
+      state.goodsInfo = spu;
+      // 价格、库存使用活动的
+      spu.skus.forEach((sku) => {
+        const product = activity.products.find((product) => product.skuId === sku.id);
+        if (product) {
+          sku.price = product.combinationPrice;
+        } else {
+          // 找不到可能是没配置,则不能发起秒杀
+          sku.stock = 0;
+        }
+      });
+    } else {
+      state.data = null;
+    }
+    state.loading = false;
+  }
+
+  function onShare() {
+    showShareModal();
+  }
+
+  onLoad((options) => {
+    getGrouponDetail(options.id);
+  });
+</script>
+
+<style lang="scss" scoped>
+  .recharge-box {
+    position: relative;
+    margin-bottom: 120rpx;
+    background: v-bind(headerBg) center/750rpx 100% no-repeat,
+      linear-gradient(115deg, #f44739 0%, #ff6600 100%);
+    border-radius: 0 0 5% 5%;
+    height: 100rpx;
+
+    .goods-box {
+      width: 710rpx;
+      border-radius: 20rpx;
+      position: absolute;
+      left: 20rpx;
+      box-sizing: border-box;
+    }
+
+    .sales-title {
+      height: 32rpx;
+      background: rgba(#ffe0e2, 0.29);
+      border-radius: 16rpx;
+      font-size: 24rpx;
+      font-weight: 400;
+      padding: 6rpx 20rpx;
+      color: #f7979c;
+    }
+
+    .num-title {
+      font-size: 24rpx;
+      font-weight: 400;
+      color: #999999;
+    }
+  }
+
+  .countdown-time {
+    font-size: 26rpx;
+    font-weight: 500;
+    color: #383a46;
+    .countdown-h {
+      font-size: 24rpx;
+      font-family: OPPOSANS;
+      font-weight: 500;
+      color: #ffffff;
+      padding: 0 4rpx;
+      margin-left: 16rpx;
+      height: 40rpx;
+      background: linear-gradient(90deg, #ff6000 0%, #fe832a 100%);
+      border-radius: 6rpx;
+    }
+    .countdown-num {
+      font-size: 24rpx;
+      font-family: OPPOSANS;
+      font-weight: 500;
+      color: #ffffff;
+      width: 40rpx;
+      height: 40rpx;
+      background: linear-gradient(90deg, #ff6000 0%, #fe832a 100%);
+      border-radius: 6rpx;
+    }
+  }
+
+  .countdown-box {
+    // height: 364rpx;
+    background: #ffffff;
+    border-radius: 10rpx;
+    box-sizing: border-box;
+
+    .countdown-title {
+      font-size: 28rpx;
+      font-weight: 500;
+      color: #333333;
+
+      .cicon-check-round {
+        color: #42b111;
+        margin-right: 24rpx;
+      }
+
+      .cicon-info {
+        color: #d71e08;
+        margin-right: 24rpx;
+      }
+
+      .num {
+        color: #ff6000;
+      }
+    }
+
+    .header-avatar {
+      width: 86rpx;
+      height: 86rpx;
+      background: #ececec;
+      border-radius: 50%;
+      border: 4rpx solid #edc36c;
+      position: relative;
+      box-sizing: border-box;
+
+      .avatar-img {
+        width: 100%;
+        height: 100%;
+        border-radius: 50%;
+      }
+
+      .header-tag {
+        width: 72rpx;
+        height: 36rpx;
+        font-size: 24rpx;
+        line-height: nor;
+        background: linear-gradient(132deg, #f3dfb1, #f3dfb1, #ecbe60);
+        border-radius: 16rpx;
+        position: absolute;
+        left: 4rpx;
+        top: -36rpx;
+      }
+    }
+    .default-avatar {
+      width: 86rpx;
+      height: 86rpx;
+      background: #ececec;
+      border-radius: 50%;
+      .avatar-img {
+        width: 100%;
+        height: 100%;
+        border-radius: 50%;
+      }
+    }
+
+    .user-avatar {
+      width: 86rpx;
+      height: 86rpx;
+      background: #ececec;
+      border-radius: 50%;
+    }
+  }
+  .order-btn {
+    width: 668rpx;
+    height: 70rpx;
+    border: 2rpx solid #dfdfdf;
+    border-radius: 35rpx;
+    color: #999999;
+    font-weight: 500;
+    font-size: 26rpx;
+    line-height: normal;
+  }
+
+  .disabled-btn {
+    width: 668rpx;
+    height: 70rpx;
+    background: #dddddd;
+    border-radius: 35rpx;
+    color: #999999;
+    font-weight: 500;
+    font-size: 28rpx;
+    line-height: normal;
+  }
+
+  .join-btn {
+    width: 668rpx;
+    height: 70rpx;
+    background: linear-gradient(90deg, #ff6000 0%, #fe832a 100%);
+    box-shadow: 0px 8rpx 6rpx 0px rgba(255, 104, 4, 0.22);
+    border-radius: 35rpx;
+    color: #fff;
+    font-weight: 500;
+    font-size: 28rpx;
+    line-height: normal;
+  }
+
+  .detail-cell-wrap {
+    width: 100%;
+    padding: 10rpx 20rpx;
+    box-sizing: border-box;
+    border-top: 2rpx solid #dfdfdf;
+    background-color: #fff;
+    // min-height: 60rpx;
+
+    .label-text {
+      font-size: 28rpx;
+      font-weight: 400;
+    }
+
+    .cell-content {
+      font-size: 28rpx;
+      font-weight: 500;
+      color: $dark-6;
+    }
+
+    .right-forwrad-icon {
+      font-size: 28rpx;
+      font-weight: 500;
+      color: $dark-9;
+    }
+  }
+</style>

+ 225 - 0
pages/activity/groupon/list.vue

@@ -0,0 +1,225 @@
+<!-- 拼团活动列表 -->
+<template>
+  <s-layout navbar="inner" :bgStyle="{ color: '#FE832A' }">
+    <view class="page-bg" :style="[{ marginTop: '-' + Number(statusBarHeight + 88) + 'rpx' }]" />
+    <view class="list-content">
+      <!-- 参团会员统计 -->
+      <view class="content-header ss-flex-col ss-col-center ss-row-center">
+        <view class="content-header-title ss-flex ss-row-center">
+          <view
+            v-for="(item, index) in state.summaryData.avatars"
+            :key="index"
+            class="picture"
+            :style="index === 6 ? 'position: relative' : 'position: static'"
+          >
+            <span class="avatar" :style="`background-image: url(${item})`" />
+            <span v-if="index === 6 && state.summaryData.avatars.length > 3" class="mengceng">
+              <i>···</i>
+            </span>
+          </view>
+          <text class="pic_count">{{ state.summaryData.userCount || 0 }}人参与</text>
+        </view>
+      </view>
+      <scroll-view
+        class="scroll-box"
+        :style="{ height: pageHeight + 'rpx' }"
+        scroll-y="true"
+        :scroll-with-animation="false"
+        :enable-back-to-top="true"
+      >
+        <view class="goods-box ss-m-b-20" v-for="item in state.pagination.list" :key="item.id">
+          <s-goods-column
+            class=""
+            size="lg"
+            :data="item"
+            :grouponTag="true"
+            @click="sheep.$router.go('/pages/goods/groupon', { id: item.id })"
+          >
+            <template v-slot:cart>
+              <button class="ss-reset-button cart-btn">去拼团</button>
+            </template>
+          </s-goods-column>
+        </view>
+        <uni-load-more
+          v-if="state.pagination.total > 0"
+          :status="state.loadStatus"
+          :content-text="{
+            contentdown: '上拉加载更多',
+          }"
+          @tap="loadMore"
+        />
+      </scroll-view>
+    </view>
+  </s-layout>
+</template>
+<script setup>
+  import { reactive } from 'vue';
+  import { onLoad, onReachBottom } from '@dcloudio/uni-app';
+  import sheep from '@/sheep';
+  import CombinationApi from '@/sheep/api/promotion/combination';
+
+  const { safeAreaInsets, safeArea } = sheep.$platform.device;
+  const sysNavBar = sheep.$platform.navbar;
+  const statusBarHeight = sheep.$platform.device.statusBarHeight * 2;
+  const pageHeight =
+    (safeArea.height + safeAreaInsets.bottom) * 2 + statusBarHeight - sysNavBar - 350;
+  const headerBg = sheep.$url.css('/static/img/shop/goods/groupon-header.png');
+
+  const state = reactive({
+    pagination: {
+      list: [],
+      total: 0,
+      pageNo: 1,
+      pageSize: 10,
+    },
+    loadStatus: '',
+    summaryData: {},
+  });
+
+  // 加载统计数据
+  const getSummary = async () => {
+    const { data } = await CombinationApi.getCombinationRecordSummary();
+    state.summaryData = data;
+  };
+
+  // 加载活动列表
+  async function getList() {
+    state.loadStatus = 'loading';
+    const { data } = await CombinationApi.getCombinationActivityPage({
+      pageNo: state.pagination.pageNo,
+      pageSize: state.pagination.pageSize,
+    });
+    data.list.forEach((activity) => {
+      state.pagination.list.push({ ...activity, price: activity.combinationPrice });
+    });
+    state.pagination.total = data.total;
+    state.loadStatus = state.pagination.list.length < state.pagination.total ? 'more' : 'noMore';
+  }
+
+  // 加载更多
+  function loadMore() {
+    if (state.loadStatus === 'noMore') {
+      return;
+    }
+    state.pagination.pageNo++;
+    getList();
+  }
+
+  // 上拉加载更多
+  onReachBottom(() => loadMore());
+
+  // 页面初始化
+  onLoad(() => {
+    getSummary();
+    getList();
+  });
+</script>
+<style lang="scss" scoped>
+  .page-bg {
+    width: 100%;
+    height: 458rpx;
+    margin-top: -88rpx;
+    background: v-bind(headerBg) no-repeat;
+    background-size: 100% 100%;
+  }
+  .list-content {
+    position: relative;
+    z-index: 3;
+    margin: -190rpx 20rpx 0 20rpx;
+    background: #fff;
+    border-radius: 20rpx 20rpx 0 0;
+    .content-header {
+      width: 100%;
+      border-radius: 20rpx 20rpx 0 0;
+      height: 100rpx;
+      background: linear-gradient(180deg, #fff4f7, #ffe4d1);
+      .content-header-title {
+        width: 100%;
+        font-size: 30rpx;
+        font-weight: 500;
+        color: #ff2923;
+        line-height: 30rpx;
+        position: relative;
+        .more {
+          position: absolute;
+          right: 30rpx;
+          top: 0;
+          font-size: 24rpx;
+          font-weight: 400;
+          color: #999999;
+          line-height: 30rpx;
+        }
+
+        .picture {
+          display: inline-table;
+        }
+
+        .avatar {
+          width: 38rpx;
+          height: 38rpx;
+          display: inline-table;
+          vertical-align: middle;
+          -webkit-user-select: none;
+          -moz-user-select: none;
+          -ms-user-select: none;
+          user-select: none;
+          border-radius: 50%;
+          background-repeat: no-repeat;
+          background-size: cover;
+          background-position: 0 0;
+          margin-right: -10rpx;
+          box-shadow: 0 0 0 1px #fe832a;
+        }
+
+        .pic_count {
+          margin-left: 30rpx;
+          font-size: 22rpx;
+          font-weight: 500;
+          width: auto;
+          height: auto;
+          background: linear-gradient(90deg, #ff6600 0%, #fe832a 100%);
+          color: #ffffff;
+          border-radius: 19rpx;
+          padding: 4rpx 14rpx;
+        }
+
+        .mengceng {
+          width: 40rpx;
+          height: 40rpx;
+          line-height: 36rpx;
+          background: rgba(51, 51, 51, 0.6);
+          text-align: center;
+          border-radius: 50%;
+          opacity: 1;
+          position: absolute;
+          left: -2rpx;
+          color: #fff;
+          top: 2rpx;
+          i {
+            font-style: normal;
+            font-size: 20rpx;
+          }
+        }
+      }
+    }
+    .scroll-box {
+      height: 900rpx;
+      .goods-box {
+        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;
+          background: linear-gradient(90deg, #ff6600 0%, #fe832a 100%);
+        }
+      }
+    }
+  }
+</style>

+ 239 - 0
pages/activity/groupon/order.vue

@@ -0,0 +1,239 @@
+<!-- 我的拼团订单列表 -->
+<template>
+  <s-layout title="我的拼团">
+    <su-sticky bgColor="#fff">
+      <su-tabs
+        :list="tabMaps"
+        :scrollable="false"
+        @change="onTabsChange"
+        :current="state.currentTab"
+      ></su-tabs>
+    </su-sticky>
+    <s-empty v-if="state.pagination.total === 0" icon="/static/goods-empty.png" />
+    <view v-if="state.pagination.total > 0">
+      <view
+        class="order-list-card-box bg-white ss-r-10 ss-m-t-14 ss-m-20"
+        v-for="record in state.pagination.list"
+        :key="record.id"
+      >
+        <view class="order-card-header ss-flex ss-col-center ss-row-between ss-p-x-20">
+          <view class="order-no">拼团编号:{{ record.id }}</view>
+          <view class="ss-font-26" :class="formatOrderColor(record)">
+            {{ tabMaps.find((item) => item.value === record.status).name }}
+          </view>
+        </view>
+        <view class="border-bottom">
+          <s-goods-item
+            :img="record.picUrl"
+            :title="record.spuName"
+            :price="record.combinationPrice"
+          >
+            <template #groupon>
+              <view class="ss-flex">
+                <view class="sales-title"> {{ record.userSize }} 人团 </view>
+              </view>
+            </template>
+          </s-goods-item>
+        </view>
+        <view class="order-card-footer ss-flex ss-row-right ss-p-x-20">
+          <button
+            class="detail-btn ss-reset-button"
+            @tap="sheep.$router.go('/pages/order/detail', { id: record.orderId })"
+          >
+            订单详情
+          </button>
+          <button
+            class="tool-btn ss-reset-button"
+            :class="{ 'ui-BG-Main-Gradient': record.status === 0 }"
+            @tap="sheep.$router.go('/pages/activity/groupon/detail', { id: record.id })"
+          >
+            {{ record.status === 0 ? '邀请拼团' : '拼团详情' }}
+          </button>
+        </view>
+      </view>
+    </view>
+    <uni-load-more
+      v-if="state.pagination.total > 0"
+      :status="state.loadStatus"
+      :content-text="{
+        contentdown: '上拉加载更多',
+      }"
+      @tap="loadMore"
+    />
+  </s-layout>
+</template>
+
+<script setup>
+  import { reactive } from 'vue';
+  import { onLoad, onReachBottom, onPullDownRefresh } from '@dcloudio/uni-app';
+  import sheep from '@/sheep';
+  import _ from 'lodash-es';
+  import { formatOrderColor } from '@/sheep/hooks/useGoods';
+  import { resetPagination } from '@/sheep/util';
+  import CombinationApi from '@/sheep/api/promotion/combination';
+
+  // 数据
+  const state = reactive({
+    currentTab: 0,
+    pagination: {
+      list: [],
+      total: 0,
+      pageNo: 1,
+      pageSize: 5,
+    },
+    loadStatus: '',
+    deleteOrderId: 0,
+  });
+
+  const tabMaps = [
+    {
+      name: '全部',
+    },
+    {
+      name: '进行中',
+      value: 0,
+    },
+    {
+      name: '拼团成功',
+      value: 1,
+    },
+    {
+      name: '拼团失败',
+      value: 2,
+    },
+  ];
+
+  // 切换选项卡
+  function onTabsChange(e) {
+    resetPagination(state.pagination);
+    state.currentTab = e.index;
+    getGrouponList();
+  }
+
+  // 获取订单列表
+  async function getGrouponList() {
+    state.loadStatus = 'loading';
+    const { code, data } = await CombinationApi.getCombinationRecordPage({
+      pageNo: state.pagination.pageNo,
+      pageSize: state.pagination.pageSize,
+      status: tabMaps[state.currentTab].value,
+    });
+    if (code !== 0) {
+      return;
+    }
+    state.pagination.list = _.concat(state.pagination.list, data.list);
+    state.pagination.total = data.total;
+    state.loadStatus = state.pagination.list.length < state.pagination.total ? 'more' : 'noMore';
+  }
+
+  onLoad((options) => {
+    if (options.type) {
+      state.currentTab = options.type;
+    }
+    getGrouponList();
+  });
+
+  // 加载更多
+  function loadMore() {
+    if (state.loadStatus === 'noMore') {
+      return;
+    }
+    state.pagination.pageNo++;
+    getGrouponList();
+  }
+
+  // 上拉加载更多
+  onReachBottom(() => {
+    loadMore();
+  });
+
+  //下拉刷新
+  onPullDownRefresh(() => {
+    getGrouponList();
+    setTimeout(function () {
+      uni.stopPullDownRefresh();
+    }, 800);
+  });
+</script>
+
+<style lang="scss" scoped>
+  .swiper-box {
+    flex: 1;
+
+    .swiper-item {
+      height: 100%;
+      width: 100%;
+    }
+  }
+
+  .order-list-card-box {
+    .order-card-header {
+      height: 80rpx;
+
+      .order-no {
+        font-size: 26rpx;
+        font-weight: 500;
+      }
+    }
+
+    .order-card-footer {
+      height: 100rpx;
+
+      .detail-btn {
+        width: 210rpx;
+        height: 66rpx;
+        border: 2rpx solid #dfdfdf;
+        border-radius: 33rpx;
+        font-size: 26rpx;
+        font-weight: 400;
+        color: #999999;
+        margin-right: 20rpx;
+      }
+      .tool-btn {
+        width: 210rpx;
+        height: 66rpx;
+        border-radius: 33rpx;
+        font-size: 26rpx;
+        font-weight: 400;
+        margin-right: 20rpx;
+        background: #f6f6f6;
+      }
+
+      .invite-btn {
+        width: 210rpx;
+        height: 66rpx;
+        background: linear-gradient(90deg, #fe832a, #ff6600);
+        box-shadow: 0px 8rpx 6rpx 0px rgba(255, 104, 4, 0.22);
+        border-radius: 33rpx;
+        color: #fff;
+        font-size: 26rpx;
+        font-weight: 500;
+      }
+    }
+  }
+
+  .sales-title {
+    height: 32rpx;
+    background: rgba(#ffe0e2, 0.29);
+    border-radius: 16rpx;
+    font-size: 24rpx;
+    font-weight: 400;
+    padding: 6rpx 20rpx;
+    color: #f7979c;
+  }
+
+  .num-title {
+    font-size: 24rpx;
+    font-weight: 400;
+    color: #999999;
+  }
+  .warning-color {
+    color: #faad14;
+  }
+  .danger-color {
+    color: #ff3000;
+  }
+  .success-color {
+    color: #52c41a;
+  }
+</style>

+ 214 - 0
pages/activity/index.vue

@@ -0,0 +1,214 @@
+<!-- 指定满减送的活动列表 -->
+<template>
+  <s-layout class="activity-wrap" :title="state.activityInfo.title">
+    <!-- 活动信息 -->
+    <su-sticky bgColor="#fff">
+      <view class="ss-flex ss-col-top tip-box">
+        <view class="type-text ss-flex ss-row-center">满减:</view>
+        <view class="ss-flex-1">
+          <view class="tip-content" v-for="item in state.activityInfo.rules" :key="item">
+            {{ item.description }}
+          </view>
+        </view>
+        <image class="activity-left-image" src="/static/activity-left.png" />
+        <image class="activity-right-image" src="/static/activity-right.png" />
+      </view>
+    </su-sticky>
+
+    <!-- 商品信息 -->
+    <view class="ss-flex ss-flex-wrap ss-p-x-20 ss-m-t-20 ss-col-top">
+      <view class="goods-list-box">
+        <view class="left-list" v-for="item in state.leftGoodsList" :key="item.id">
+          <s-goods-column
+            class="goods-md-box"
+            size="md"
+            :data="item"
+            @click="sheep.$router.go('/pages/goods/index', { id: item.id })"
+            @getHeight="mountMasonry($event, 'left')"
+          >
+            <template v-slot:cart>
+              <button class="ss-reset-button cart-btn"> </button>
+            </template>
+          </s-goods-column>
+        </view>
+      </view>
+      <view class="goods-list-box">
+        <view class="right-list" v-for="item in state.rightGoodsList" :key="item.id">
+          <s-goods-column
+            class="goods-md-box"
+            size="md"
+            :data="item"
+            @click="sheep.$router.go('/pages/goods/index', { id: item.id })"
+            @getHeight="mountMasonry($event, 'right')"
+          >
+            <template v-slot:cart>
+              <button class="ss-reset-button cart-btn" />
+            </template>
+          </s-goods-column>
+        </view>
+      </view>
+    </view>
+
+    <uni-load-more
+      v-if="state.pagination.total > 0"
+      :status="state.loadStatus"
+      :content-text="{
+        contentdown: '上拉加载更多',
+      }"
+      @tap="loadMore"
+    />
+  </s-layout>
+</template>
+<script setup>
+  import { reactive } from 'vue';
+  import { onLoad, onReachBottom } from '@dcloudio/uni-app';
+  import sheep from '@/sheep';
+  import _ from 'lodash-es';
+  import RewardActivityApi from '@/sheep/api/promotion/rewardActivity';
+  import SpuApi from '@/sheep/api/product/spu';
+  import { appendSettlementProduct } from '@/sheep/hooks/useGoods';
+  import OrderApi from '@/sheep/api/trade/order';
+
+  const state = reactive({
+    activityId: 0, // 获得编号
+    activityInfo: {}, // 获得信息
+
+    pagination: {
+      list: [],
+      total: 1,
+      pageNo: 1,
+      pageSize: 8,
+    },
+    loadStatus: '',
+    leftGoodsList: [],
+    rightGoodsList: [],
+  });
+
+  // 加载瀑布流
+  let count = 0;
+  let leftHeight = 0;
+  let rightHeight = 0;
+
+  function mountMasonry(height = 0, where = 'left') {
+    if (!state.pagination.list[count]) return;
+
+    if (where === 'left') {
+      leftHeight += height;
+    } else {
+      rightHeight += height;
+    }
+    if (leftHeight <= rightHeight) {
+      state.leftGoodsList.push(state.pagination.list[count]);
+    } else {
+      state.rightGoodsList.push(state.pagination.list[count]);
+    }
+    count++;
+  }
+
+  // 加载商品信息
+  async function getList() {
+    // 处理拓展参数
+    const params = {};
+    if (state.activityInfo.productScope === 2) {
+      params.ids = state.activityInfo.productSpuIds.join(',');
+    } else if (state.activityInfo.productScope === 3) {
+      params.categoryIds = state.activityInfo.productSpuIds.join(',');
+    }
+    // 请求数据
+    state.loadStatus = 'loading';
+    const { code, data } = await SpuApi.getSpuPage({
+      pageNo: state.pagination.pageNo,
+      pageSize: state.pagination.pageSize,
+      ...params,
+    });
+    if (code !== 0) {
+      return;
+    }
+    // 拼接结算信息(营销)
+    await OrderApi.getSettlementProduct(data.list.map((item) => item.id).join(',')).then((res) => {
+      if (res.code !== 0) {
+        return;
+      }
+      appendSettlementProduct(data.list, res.data);
+    });
+    state.pagination.list = _.concat(state.pagination.list, data.list);
+    state.pagination.total = data.total;
+    state.loadStatus = state.pagination.list.length < state.pagination.total ? 'more' : 'noMore';
+    mountMasonry();
+  }
+
+  // 加载活动信息
+  async function getActivity(id) {
+    const { code, data } = await RewardActivityApi.getRewardActivity(id);
+    if (code === 0) {
+      state.activityInfo = data;
+    }
+  }
+
+  // 加载更多
+  function loadMore() {
+    if (state.loadStatus === 'noMore') {
+      return;
+    }
+    state.pagination.pageNo++;
+    getList();
+  }
+
+  // 上拉加载更多
+  onReachBottom(() => {
+    loadMore();
+  });
+
+  onLoad(async (options) => {
+    state.activityId = options.activityId;
+    await getActivity(state.activityId);
+    await getList(state.activityId);
+  });
+</script>
+<style lang="scss" scoped>
+  .goods-list-box {
+    width: 50%;
+    box-sizing: border-box;
+    .left-list {
+      margin-right: 10rpx;
+      margin-bottom: 20rpx;
+    }
+    .right-list {
+      margin-left: 10rpx;
+      margin-bottom: 20rpx;
+    }
+  }
+  .tip-box {
+    background: #fff0e7;
+    padding: 20rpx;
+    width: 100%;
+    position: relative;
+    box-sizing: border-box;
+    .activity-left-image {
+      position: absolute;
+      bottom: 0;
+      left: 0;
+      width: 58rpx;
+      height: 36rpx;
+    }
+    .activity-right-image {
+      position: absolute;
+      top: 0;
+      right: 0;
+      width: 72rpx;
+      height: 50rpx;
+    }
+    .type-text {
+      font-size: 26rpx;
+      font-weight: 500;
+      color: #ff6000;
+      line-height: 42rpx;
+    }
+    .tip-content {
+      font-size: 26rpx;
+      font-weight: 500;
+      color: #ff6000;
+      line-height: 42rpx;
+    }
+  }
+</style>

+ 76 - 0
pages/activity/point/list.vue

@@ -0,0 +1,76 @@
+<!-- 积分商城:商品列表  -->
+<template>
+  <s-layout title="积分商城" navbar="normal" :leftWidth="0" :rightWidth="0">
+    <scroll-view
+      class="scroll-box"
+      :style="{ height: pageHeight + 'rpx' }"
+      scroll-y="true"
+      :scroll-with-animation="false"
+      :enable-back-to-top="true"
+    >
+      <s-point-card ref="sPointCardRef" class="ss-p-x-20 ss-m-t-20"/>
+      <s-empty
+        v-if="activityTotal === 0"
+        icon="/static/goods-empty.png"
+        text="暂无积分商品"
+      ></s-empty>
+      <uni-load-more
+        v-if="activityTotal > 0"
+        :status="loadStatus"
+        :content-text="{
+            contentdown: '上拉加载更多',
+          }"
+        @tap="loadMore"
+      />
+    </scroll-view>
+  </s-layout>
+</template>
+<script setup>
+  import sheep from '@/sheep';
+  import { onLoad, onReachBottom } from '@dcloudio/uni-app';
+  import { reactive, ref } from 'vue';
+  import PointApi from '@/sheep/api/promotion/point';
+  import SLayout from '@/sheep/components/s-layout/s-layout.vue';
+
+  // 计算页面高度
+  const { safeAreaInsets, safeArea } = sheep.$platform.device;
+  const statusBarHeight = sheep.$platform.device.statusBarHeight * 2;
+  const pageHeight =
+    (safeArea.height + safeAreaInsets.bottom) * 2 + statusBarHeight - sheep.$platform.navbar - 350;
+
+  const sPointCardRef = ref();
+  // 查询活动列表
+  const activityPageParams = reactive({
+    pageNo: 1, // 页码
+    pageSize: 5, // 每页数量
+  });
+
+  const activityTotal = ref(0); // 活动总数
+  const activityCount = ref(0); // 已加载活动数量
+  const loadStatus = ref(''); // 页面加载状态
+  async function getActivityList() {
+    loadStatus.value = 'loading';
+    const { data } = await PointApi.getPointActivityPage(activityPageParams);
+    await sPointCardRef.value.concatActivity(data.list);
+    activityCount.value = sPointCardRef.value.getActivityCount();
+    activityTotal.value = data.total;
+
+    loadStatus.value = activityCount.value < activityTotal.value ? 'more' : 'noMore';
+  }
+
+  // 加载更多
+  function loadMore() {
+    if (loadStatus.value !== 'noMore') {
+      activityPageParams.pageNo += 1;
+      getActivityList();
+    }
+  }
+
+  // 上拉加载更多
+  onReachBottom(() => {
+    loadMore();
+  });
+  onLoad(() => {
+    getActivityList();
+  });
+</script>

+ 461 - 0
pages/activity/seckill/list.vue

@@ -0,0 +1,461 @@
+<!-- 秒杀活动列表 -->
+<template>
+  <s-layout navbar="inner" :bgStyle="{ color: 'rgb(245,28,19)' }">
+    <!--顶部背景图-->
+    <view
+      class="page-bg"
+      :style="[{ marginTop: '-' + Number(statusBarHeight + 88) + 'rpx' }]"
+    ></view>
+    <!-- 时间段轮播图 -->
+    <view class="header" v-if="activeTimeConfig?.sliderPicUrls?.length > 0">
+      <swiper
+        indicator-dots="true"
+        autoplay="true"
+        :circular="true"
+        interval="3000"
+        duration="1500"
+        indicator-color="rgba(255,255,255,0.6)"
+        indicator-active-color="#fff"
+      >
+        <block v-for="(picUrl, index) in activeTimeConfig.sliderPicUrls" :key="index">
+          <swiper-item class="borRadius14">
+            <image :src="picUrl" class="slide-image borRadius14" lazy-load />
+          </swiper-item>
+        </block>
+      </swiper>
+    </view>
+    <!-- 时间段列表 -->
+    <view class="flex align-center justify-between ss-p-25">
+      <!-- 左侧图标 -->
+      <view class="time-icon">
+        <!-- TODO 芋艿:图片统一维护 -->
+        <image
+          class="ss-w-100 ss-h-100"
+          src="http://mall.yudao.iocoder.cn/static/images/priceTag.png"
+        />
+      </view>
+      <scroll-view
+        class="time-list"
+        :scroll-into-view="activeTimeElId"
+        scroll-x
+        scroll-with-animation
+      >
+        <view
+          v-for="(config, index) in timeConfigList"
+          :key="index"
+          :class="['item', { active: activeTimeIndex === index }]"
+          :id="`timeItem${index}`"
+          @tap="handleChangeTimeConfig(index, config.id)"
+        >
+          <!-- 活动起始时间 -->
+          <view class="time">{{ config.startTime }}</view>
+          <!-- 活动状态 -->
+          <view class="status">{{ config?.status }}</view>
+        </view>
+      </scroll-view>
+    </view>
+
+    <!-- 内容区 -->
+    <view class="list-content">
+      <!-- 活动倒计时 -->
+      <view class="content-header ss-flex-col ss-col-center ss-row-center">
+        <view class="content-header-box ss-flex ss-row-center">
+          <view
+            class="countdown-box ss-flex"
+            v-if="activeTimeConfig?.status === TimeStatusEnum.STARTED"
+          >
+            <view class="countdown-title ss-m-r-12">距结束</view>
+            <view class="ss-flex countdown-time">
+              <view class="ss-flex countdown-h">{{ countDown.h }}</view>
+              <view class="ss-m-x-4">:</view>
+              <view class="countdown-num ss-flex ss-row-center">{{ countDown.m }}</view>
+              <view class="ss-m-x-4">:</view>
+              <view class="countdown-num ss-flex ss-row-center">{{ countDown.s }}</view>
+            </view>
+          </view>
+          <view v-else> {{ activeTimeConfig?.status }} </view>
+        </view>
+      </view>
+
+      <!-- 活动列表 -->
+      <scroll-view
+        class="scroll-box"
+        :style="{ height: pageHeight + 'rpx' }"
+        scroll-y="true"
+        :scroll-with-animation="false"
+        :enable-back-to-top="true"
+      >
+        <view class="goods-box ss-m-b-20" v-for="activity in activityList" :key="activity.id">
+          <s-goods-column
+            size="lg"
+            :data="{ ...activity, price: activity.seckillPrice }"
+            :goodsFields="goodsFields"
+            :seckillTag="true"
+          >
+            <!-- 抢购进度 -->
+            <template #activity>
+              <view class="limit">
+                限量
+                <text class="ss-m-l-5">{{ activity.stock }} {{ activity.unitName }}</text>
+              </view>
+              <su-progress :percentage="activity.percent" strokeWidth="10" textInside isAnimate />
+            </template>
+            <!-- 抢购按钮 -->
+            <template #cart>
+              <button
+                :class="[
+                  'ss-reset-button cart-btn',
+                  { disabled: activeTimeConfig?.status === TimeStatusEnum.END },
+                ]"
+                v-if="activeTimeConfig?.status === TimeStatusEnum.WAIT_START"
+              >
+                <span>未开始</span>
+              </button>
+              <button
+                :class="[
+                  'ss-reset-button cart-btn',
+                  { disabled: activeTimeConfig?.status === TimeStatusEnum.END },
+                ]"
+                @click="sheep.$router.go('/pages/goods/seckill', { id: activity.id })"
+                v-else-if="activeTimeConfig?.status === TimeStatusEnum.STARTED"
+              >
+                <span>马上抢</span>
+              </button>
+              <button
+                :class="[
+                  'ss-reset-button cart-btn',
+                  { disabled: activeTimeConfig?.status === TimeStatusEnum.END },
+                ]"
+                v-else
+              >
+                <span>已结束</span>
+              </button>
+            </template>
+          </s-goods-column>
+        </view>
+        <uni-load-more
+          v-if="activityTotal > 0"
+          :status="loadStatus"
+          :content-text="{
+            contentdown: '上拉加载更多',
+          }"
+          @tap="loadMore"
+        />
+      </scroll-view>
+    </view>
+  </s-layout>
+</template>
+<script setup>
+  import { reactive, computed, ref, nextTick } from 'vue';
+  import { onLoad, onReachBottom } from '@dcloudio/uni-app';
+  import sheep from '@/sheep';
+  import { useDurationTime } from '@/sheep/hooks/useGoods';
+  import SeckillApi from '@/sheep/api/promotion/seckill';
+  import dayjs from 'dayjs';
+  import { TimeStatusEnum } from '@/sheep/util/const';
+
+  // 计算页面高度
+  const { safeAreaInsets, safeArea } = sheep.$platform.device;
+  const statusBarHeight = sheep.$platform.device.statusBarHeight * 2;
+  const pageHeight =
+    (safeArea.height + safeAreaInsets.bottom) * 2 + statusBarHeight - sheep.$platform.navbar - 350;
+  const headerBg = sheep.$url.css('/static/img/shop/goods/seckill-header.png');
+
+  // 商品控件显示的字段(不显示库存、销量。改为显示自定义的进度条)
+  const goodsFields = {
+    name: {
+      show: true,
+    },
+    introduction: {
+      show: true,
+    },
+    price: {
+      show: true,
+    },
+    marketPrice: {
+      show: true,
+    },
+  };
+
+  //#region 时间段
+  // 时间段列表
+  const timeConfigList = ref([]);
+  // 查询时间段
+  const getSeckillConfigList = async () => {
+    const { data } = await SeckillApi.getSeckillConfigList();
+    const now = dayjs();
+    const today = now.format('YYYY-MM-DD');
+    const select = ref([]);
+    // 判断时间段的状态
+    data.forEach((config, index) => {
+      const startTime = dayjs(`${today} ${config.startTime}`);
+      const endTime = dayjs(`${today} ${config.endTime}`);
+      select.value[index] = config.id;
+      if (now.isBefore(startTime)) {
+        config.status = TimeStatusEnum.WAIT_START;
+      } else if (now.isAfter(endTime)) {
+        config.status = TimeStatusEnum.END;
+      } else {
+        config.status = TimeStatusEnum.STARTED;
+        activeTimeIndex.value = index;
+      }
+    });
+    timeConfigList.value = data;
+    // 默认选中进行中的活动
+    handleChangeTimeConfig(activeTimeIndex.value, select.value[activeTimeIndex.value]);
+    // 滚动到进行中的时间段
+    scrollToTimeConfig(activeTimeIndex.value);
+  };
+
+  // 滚动到指定时间段
+  const activeTimeElId = ref(''); // 当前选中的时间段的元素ID
+  const scrollToTimeConfig = (index) => {
+    nextTick(() => (activeTimeElId.value = `timeItem${index}`));
+  };
+
+  // 切换时间段
+  const activeTimeIndex = ref(0); // 当前选中的时间段的索引
+  const activeTimeConfig = computed(() => timeConfigList.value[activeTimeIndex.value]); // 当前选中的时间段
+  const handleChangeTimeConfig = (index, id) => {
+    activeTimeIndex.value = index;
+
+    // 查询活动列表
+    activityPageParams.pageNo = 1;
+    activityPageParams.configId = id;
+    activityList.value = [];
+    getActivityList();
+  };
+
+  // 倒计时
+  const countDown = computed(() => {
+    const endTime = activeTimeConfig.value?.endTime;
+    if (endTime) {
+      return useDurationTime(`${dayjs().format('YYYY-MM-DD')} ${endTime}`);
+    }
+  });
+
+  //#endregion
+
+  //#region 分页查询活动列表
+
+  // 查询活动列表
+  const activityPageParams = reactive({
+    configId: 0, // 时间段 ID
+    pageNo: 1, // 页码
+    pageSize: 5, // 每页数量
+  });
+  const activityTotal = ref(0); // 活动总数
+  const activityList = ref([]); // 活动列表
+  const loadStatus = ref(''); // 页面加载状态
+  async function getActivityList() {
+    loadStatus.value = 'loading';
+    const { data } = await SeckillApi.getSeckillActivityPage(activityPageParams);
+    data.list.forEach((activity) => {
+      // 计算抢购进度
+      activity.percent = parseInt(
+        (100 * (activity.totalStock - activity.stock)) / activity.totalStock,
+      );
+    });
+    activityList.value = activityList.value.concat(...data.list);
+    activityTotal.value = data.total;
+
+    loadStatus.value = activityList.value.length < activityTotal.value ? 'more' : 'noMore';
+  }
+
+  // 加载更多
+  function loadMore() {
+    if (loadStatus.value !== 'noMore') {
+      activityPageParams.pageNo += 1;
+      getActivityList();
+    }
+  }
+  // 上拉加载更多
+  onReachBottom(() => loadMore());
+
+  //#endregion
+
+  // 页面初始化
+  onLoad(async () => {
+    await getSeckillConfigList();
+  });
+</script>
+<style lang="scss" scoped>
+  // 顶部背景图
+  .page-bg {
+    width: 100%;
+    height: 458rpx;
+    background: v-bind(headerBg) no-repeat;
+    background-size: 100% 100%;
+  }
+
+  // 时间段轮播图
+  .header {
+    width: 710rpx;
+    height: 330rpx;
+    margin: -276rpx auto 0 auto;
+    border-radius: 14rpx;
+    overflow: hidden;
+
+    swiper {
+      height: 330rpx !important;
+      border-radius: 14rpx;
+      overflow: hidden;
+    }
+
+    image {
+      width: 100%;
+      height: 100%;
+      border-radius: 14rpx;
+      overflow: hidden;
+
+      img {
+        border-radius: 14rpx;
+      }
+    }
+  }
+
+  // 时间段列表:左侧图标
+  .time-icon {
+    width: 75rpx;
+    height: 70rpx;
+  }
+
+  // 时间段列表
+  .time-list {
+    width: 596rpx;
+    white-space: nowrap;
+
+    // 时间段
+    .item {
+      display: inline-block;
+      font-size: 20rpx;
+      color: #666;
+      text-align: center;
+      box-sizing: border-box;
+      margin-right: 30rpx;
+      width: 130rpx;
+
+      // 开始时间
+      .time {
+        font-size: 36rpx;
+        font-weight: 600;
+        color: #333;
+      }
+
+      // 选中的时间段
+      &.active {
+        .time {
+          color: var(--ui-BG-Main);
+        }
+
+        // 状态
+        .status {
+          height: 30rpx;
+          line-height: 30rpx;
+          border-radius: 15rpx;
+          width: 128rpx;
+          background: linear-gradient(90deg, var(--ui-BG-Main) 0%, var(--ui-BG-Main-gradient) 100%);
+          color: #fff;
+        }
+      }
+    }
+  }
+
+  // 内容区
+  .list-content {
+    position: relative;
+    z-index: 3;
+    margin: 0 20rpx 0 20rpx;
+    background: #fff;
+    border-radius: 20rpx 20rpx 0 0;
+
+    .content-header {
+      width: 100%;
+      border-radius: 20rpx 20rpx 0 0;
+      height: 150rpx;
+      background: linear-gradient(180deg, #fff4f7, #ffe6ec);
+
+      .content-header-box {
+        width: 678rpx;
+        height: 64rpx;
+        background: rgba($color: #fff, $alpha: 0.66);
+        border-radius: 32px;
+
+        // 场次倒计时内容
+        .countdown-title {
+          font-size: 28rpx;
+          font-weight: 500;
+          color: #333333;
+          line-height: 28rpx;
+        }
+
+        // 场次倒计时
+        .countdown-time {
+          font-size: 28rpx;
+          color: rgba(#ed3c30, 0.23);
+
+          // 场次倒计时:小时部分
+          .countdown-h {
+            font-size: 24rpx;
+            font-family: OPPOSANS;
+            font-weight: 500;
+            color: #ffffff;
+            padding: 0 4rpx;
+            height: 40rpx;
+            background: rgba(#ed3c30, 0.23);
+            border-radius: 6rpx;
+          }
+
+          // 场次倒计时:分钟、秒
+          .countdown-num {
+            font-size: 24rpx;
+            font-family: OPPOSANS;
+            font-weight: 500;
+            color: #ffffff;
+            width: 40rpx;
+            height: 40rpx;
+            background: rgba(#ed3c30, 0.23);
+            border-radius: 6rpx;
+          }
+        }
+      }
+    }
+
+    // 活动列表
+    .scroll-box {
+      height: 900rpx;
+
+      // 活动
+      .goods-box {
+        position: relative;
+
+        // 抢购按钮
+        .cart-btn {
+          position: absolute;
+          bottom: 10rpx;
+          right: 20rpx;
+          z-index: 11;
+          height: 44rpx;
+          line-height: 50rpx;
+          padding: 0 20rpx;
+          border-radius: 25rpx;
+          font-size: 24rpx;
+          color: #fff;
+          background: linear-gradient(90deg, #ff6600 0%, #fe832a 100%);
+
+          &.disabled {
+            background: $gray-b;
+            color: #fff;
+          }
+        }
+
+        // 秒杀限量商品数
+        .limit {
+          font-size: 22rpx;
+          color: $dark-9;
+          margin-bottom: 5rpx;
+        }
+      }
+    }
+  }
+</style>

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 401 - 0
pages/app/sign.vue


+ 21 - 0
pages/chat/components/goods.vue

@@ -0,0 +1,21 @@
+<template>
+  <s-goods-item
+    :title="goodsData.spuName"
+    :img="goodsData.picUrl"
+    :price="goodsData.price"
+    :skuText="goodsData.introduction"
+    priceColor="#FF3000"
+    :titleWidth="400"
+  />
+</template>
+
+<script setup>
+
+  const props = defineProps({
+    goodsData: {
+      type: Object,
+      default: {},
+    },
+  });
+</script>
+

+ 102 - 0
pages/chat/components/messageInput.vue

@@ -0,0 +1,102 @@
+<template>
+  <view class="send-wrap ss-flex">
+    <view class="left ss-flex ss-flex-1">
+      <uni-easyinput
+        class="ss-flex-1 ss-p-l-22"
+        :inputBorder="false"
+        :clearable="false"
+        v-model="message"
+        placeholder="请输入你要咨询的问题"
+      ></uni-easyinput>
+    </view>
+    <text class="sicon-basic bq" @tap.stop="onTools('emoji')"></text>
+    <text
+      v-if="!message"
+      class="sicon-edit"
+      :class="{ 'is-active': toolsMode === 'tools' }"
+      @tap.stop="onTools('tools')"
+    ></text>
+    <button v-if="message" class="ss-reset-button send-btn" @tap="sendMessage">
+      发送
+    </button>
+  </view>
+</template>
+
+<script setup>
+  import { computed } from 'vue';
+  /**
+   * 消息发送组件
+   */
+  const props = defineProps({
+    // 消息
+    modelValue: {
+      type: String,
+      default: '',
+    },
+    // 工具模式
+    toolsMode: {
+      type: String,
+      default: '',
+    },
+  });
+  const emits = defineEmits(['update:modelValue', 'onTools', 'sendMessage']);
+  const message = computed({
+    get() {
+      return props.modelValue;
+    },
+    set(newValue) {
+      emits(`update:modelValue`, newValue);
+    }
+  });
+
+
+  // 打开工具菜单
+  function onTools(mode) {
+    emits('onTools', mode);
+  }
+
+  // 发送消息
+  function sendMessage() {
+    emits('sendMessage');
+  }
+</script>
+
+<style scoped lang="scss">
+  .send-wrap {
+    padding: 18rpx 20rpx;
+    background: #fff;
+
+    .left {
+      height: 64rpx;
+      border-radius: 32rpx;
+      background: var(--ui-BG-1);
+    }
+
+    .bq {
+      font-size: 50rpx;
+      margin-left: 10rpx;
+    }
+
+    .sicon-edit {
+      font-size: 50rpx;
+      margin-left: 10rpx;
+      transform: rotate(0deg);
+      transition: all linear 0.2s;
+
+      &.is-active {
+        transform: rotate(45deg);
+      }
+    }
+
+    .send-btn {
+      width: 100rpx;
+      height: 60rpx;
+      line-height: 60rpx;
+      border-radius: 30rpx;
+      background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
+      font-size: 26rpx;
+      color: #fff;
+      margin-left: 11rpx;
+    }
+  }
+</style>

+ 94 - 0
pages/chat/components/messageList.vue

@@ -0,0 +1,94 @@
+<template>
+  <!--  聊天虚拟列表  -->
+  <z-paging ref="pagingRef" v-model="messageList" use-chat-record-mode use-virtual-list
+            cell-height-mode="dynamic" default-page-size="20" :auto-clean-list-when-reload="false"
+            safe-area-inset-bottom bottom-bg-color="#f8f8f8" :back-to-top-style="backToTopStyle"
+            :auto-show-back-to-top="showNewMessageTip" @backToTopClick="onBackToTopClick"
+            @scrolltoupper="onScrollToUpper" @query="queryList">
+    <template #top>
+      <!-- 撑一下顶部导航 -->
+      <view :style="{ height: sys_navBar + 'px' }"></view>
+    </template>
+    <!-- style="transform: scaleY(-1)"必须写,否则会导致列表倒置!!! -->
+    <!-- 注意不要直接在chat-item组件标签上设置style,因为在微信小程序中是无效的,请包一层view -->
+    <template #cell="{item,index}">
+      <view style="transform: scaleY(-1)">
+        <!--  消息渲染  -->
+        <MessageListItem :message="item" :message-index="index" :message-list="messageList"></MessageListItem>
+      </view>
+    </template>
+    <!-- 底部聊天输入框 -->
+    <template #bottom>
+      <slot name="bottom"></slot>
+    </template>
+    <!-- 查看最新消息 -->
+    <template #backToTop>
+      <text>有新消息</text>
+    </template>
+  </z-paging>
+</template>
+
+<script setup>
+  import MessageListItem from '@/pages/chat/components/messageListItem.vue';
+  import { reactive, ref } from 'vue';
+  import KeFuApi from '@/sheep/api/promotion/kefu';
+  import { isEmpty } from '@/sheep/helper/utils';
+  import sheep from '@/sheep';
+  
+  const sys_navBar = sheep.$platform.navbar;
+  const messageList = ref([]); // 消息列表
+  const showNewMessageTip = ref(false); // 显示有新消息提示
+  const backToTopStyle = reactive({
+    'width': '100px',
+    'background-color': '#fff',
+    'border-radius': '30px',
+    'box-shadow': '0 2px 4px rgba(0, 0, 0, 0.1)',
+    'display': 'flex',
+    'justifyContent': 'center',
+    'alignItems': 'center',
+  }); // 返回顶部样式
+  const queryParams = reactive({
+    pageNo: 1,
+    pageSize: 10,
+  });
+  const pagingRef = ref(null); // 虚拟列表
+  const queryList = async (pageNo, pageSize) => {
+    // 组件加载时会自动触发此方法,因此默认页面加载时会自动触发,无需手动调用
+    // 这里的pageNo和pageSize会自动计算好,直接传给服务器即可
+    queryParams.pageNo = pageNo;
+    queryParams.pageSize = pageSize;
+    await getMessageList();
+  };
+  // 获得消息分页列表
+  const getMessageList = async () => {
+    const { data } = await KeFuApi.getKefuMessagePage(queryParams);
+    if (isEmpty(data.list)) {
+      return;
+    }
+    pagingRef.value.completeByTotal(data.list, data.total);
+  };
+  /** 刷新消息列表 */
+  const refreshMessageList = (message = undefined) => {
+    if (message !== undefined) {
+      showNewMessageTip.value = true;
+      // 追加数据
+      pagingRef.value.addChatRecordData([message], false);
+      return;
+    }
+    pagingRef.value.reload();
+  };
+  /** 滚动到最新消息 */
+  const onBackToTopClick = (event) => {
+    event(false); // 禁用默认操作
+    pagingRef.value.scrollToBottom();
+  };
+  /** 监听滚动到底部事件(因为 scroll 翻转了顶就是底) */
+  const onScrollToUpper = () => {
+    // 若已是第一页则不做处理
+    if (queryParams.pageNo === 1) {
+      return;
+    }
+    showNewMessageTip.value = false;
+  };
+  defineExpose({ getMessageList, refreshMessageList });
+</script>

+ 301 - 0
pages/chat/components/messageListItem.vue

@@ -0,0 +1,301 @@
+<template>
+  <view class="chat-box">
+    <!--  消息渲染  -->
+    <view class="message-item ss-flex-col scroll-item">
+      <view class="ss-flex ss-row-center ss-col-center">
+        <!-- 日期 -->
+        <view
+          v-if="
+            message.contentType !== KeFuMessageContentTypeEnum.SYSTEM &&
+            showTime(message, messageIndex)
+          "
+          class="date-message"
+        >
+          {{ formatDate(message.createTime) }}
+        </view>
+        <!-- 系统消息 -->
+        <view
+          v-if="message.contentType === KeFuMessageContentTypeEnum.SYSTEM"
+          class="system-message"
+        >
+          {{ message.content }}
+        </view>
+      </view>
+      <!-- 消息体渲染管理员消息和用户消息并左右展示  -->
+      <view
+        v-if="message.contentType !== KeFuMessageContentTypeEnum.SYSTEM"
+        class="ss-flex ss-col-top"
+        :class="[
+          message.senderType === UserTypeEnum.ADMIN
+            ? `ss-row-left`
+            : message.senderType === UserTypeEnum.MEMBER
+            ? `ss-row-right`
+            : '',
+        ]"
+      >
+        <!-- 客服头像 -->
+        <image
+          v-show="message.senderType === UserTypeEnum.ADMIN"
+          class="chat-avatar ss-m-r-24"
+          :src="
+            sheep.$url.cdn(message.senderAvatar) ||
+            sheep.$url.static('/static/img/shop/chat/default.png')
+          "
+          mode="aspectFill"
+        ></image>
+        <!-- 内容 -->
+        <template v-if="message.contentType === KeFuMessageContentTypeEnum.TEXT">
+          <view class="message-box" :class="{ admin: message.senderType === UserTypeEnum.ADMIN }">
+            <mp-html :content="replaceEmoji(message.content)" />
+          </view>
+        </template>
+        <template v-if="message.contentType === KeFuMessageContentTypeEnum.IMAGE">
+          <view
+            class="message-box"
+            :class="{ admin: message.senderType === UserTypeEnum.ADMIN }"
+            :style="{ width: '200rpx' }"
+          >
+            <su-image
+              class="message-img"
+              isPreview
+              :previewList="[sheep.$url.cdn(message.content)]"
+              :current="0"
+              :src="sheep.$url.cdn(message.content)"
+              :height="200"
+              :width="200"
+              mode="aspectFill"
+            ></su-image>
+          </view>
+        </template>
+        <template v-if="message.contentType === KeFuMessageContentTypeEnum.PRODUCT">
+          <GoodsItem
+            :goodsData="getMessageContent(message)"
+            @tap="sheep.$router.go('/pages/goods/index', { id: getMessageContent(message).spuId })"
+          />
+        </template>
+        <template v-if="message.contentType === KeFuMessageContentTypeEnum.ORDER">
+          <OrderItem
+            :orderData="getMessageContent(message)"
+            @tap="sheep.$router.go('/pages/order/detail', { id: getMessageContent(message).id })"
+          />
+        </template>
+        <!-- user头像 -->
+        <image
+          v-if="message.senderType === UserTypeEnum.MEMBER"
+          class="chat-avatar ss-m-l-24"
+          :src="
+            sheep.$url.cdn(message.senderAvatar) ||
+            sheep.$url.static('/static/img/shop/chat/default.png')
+          "
+          mode="aspectFill"
+        >
+        </image>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup>
+  import { computed, unref } from 'vue';
+  import dayjs from 'dayjs';
+  import { KeFuMessageContentTypeEnum, UserTypeEnum } from '@/pages/chat/util/constants';
+  import { emojiList } from '@/pages/chat/util/emoji';
+  import sheep from '@/sheep';
+  import { formatDate } from '@/sheep/util';
+  import GoodsItem from '@/pages/chat/components/goods.vue';
+  import OrderItem from '@/pages/chat/components/order.vue';
+
+  const props = defineProps({
+    // 消息
+    message: {
+      type: Object,
+      default: () => ({}),
+    },
+    // 消息索引
+    messageIndex: {
+      type: Number,
+      default: 0,
+    },
+    // 消息列表
+    messageList: {
+      type: Array,
+      default: () => [],
+    },
+  });
+  const getMessageContent = computed(() => (item) => JSON.parse(item.content)); // 解析消息内容
+
+  //======================= 工具 =======================
+
+  const showTime = computed(() => (item, index) => {
+    if (unref(props.messageList)[index + 1]) {
+      let dateString = dayjs(unref(props.messageList)[index + 1].createTime).fromNow();
+      return dateString !== dayjs(unref(item).createTime).fromNow();
+    }
+    return false;
+  });
+
+  // 处理表情
+  function replaceEmoji(data) {
+    let newData = data;
+    if (typeof newData !== 'object') {
+      let reg = /\[(.+?)]/g; // [] 中括号
+      let zhEmojiName = newData.match(reg);
+      if (zhEmojiName) {
+        zhEmojiName.forEach((item) => {
+          let emojiFile = selEmojiFile(item);
+          newData = newData.replace(
+            item,
+            `<img class="chat-img" style="width: 24px;height: 24px;margin: 0 3px;" src="${sheep.$url.cdn(
+              '/static/img/chat/emoji/' + emojiFile,
+            )}"/>`,
+          );
+        });
+      }
+    }
+    return newData;
+  }
+
+  function selEmojiFile(name) {
+    for (let index in emojiList) {
+      if (emojiList[index].name === name) {
+        return emojiList[index].file;
+      }
+    }
+    return false;
+  }
+</script>
+
+<style scoped lang="scss">
+  .message-item {
+    margin-bottom: 33rpx;
+  }
+
+  .date-message,
+  .system-message {
+    width: fit-content;
+    border-radius: 12rpx;
+    padding: 8rpx 16rpx;
+    margin-bottom: 16rpx;
+    background-color: var(--ui-BG-3);
+    color: #999;
+    font-size: 24rpx;
+  }
+
+  .chat-avatar {
+    width: 70rpx;
+    height: 70rpx;
+    border-radius: 50%;
+  }
+
+  .send-status {
+    color: #333;
+    height: 80rpx;
+    margin-right: 8rpx;
+    display: flex;
+    align-items: center;
+
+    .loading {
+      width: 32rpx;
+      height: 32rpx;
+      -webkit-animation: rotating 2s linear infinite;
+      animation: rotating 2s linear infinite;
+
+      @-webkit-keyframes rotating {
+        0% {
+          transform: rotateZ(0);
+        }
+
+        100% {
+          transform: rotateZ(360deg);
+        }
+      }
+
+      @keyframes rotating {
+        0% {
+          transform: rotateZ(0);
+        }
+
+        100% {
+          transform: rotateZ(360deg);
+        }
+      }
+    }
+
+    .warning {
+      width: 32rpx;
+      height: 32rpx;
+      color: #ff3000;
+    }
+  }
+
+  .message-box {
+    max-width: 50%;
+    font-size: 16px;
+    line-height: 20px;
+    white-space: normal;
+    word-break: break-all;
+    word-wrap: break-word;
+    padding: 20rpx;
+    border-radius: 10rpx;
+    color: #fff;
+    background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
+
+    &.admin {
+      background: #fff;
+      color: #333;
+    }
+
+    :deep() {
+      .imgred {
+        width: 100%;
+      }
+
+      .imgred,
+      img {
+        width: 100%;
+      }
+    }
+  }
+
+  :deep() {
+    .goods,
+    .order {
+      max-width: 500rpx;
+    }
+  }
+
+  .message-img {
+    width: 100px;
+    height: 100px;
+    border-radius: 6rpx;
+  }
+
+  .template-wrap {
+    // width: 100%;
+    padding: 20rpx 24rpx;
+    background: #fff;
+    border-radius: 10rpx;
+
+    .title {
+      font-size: 26rpx;
+      font-weight: 500;
+      color: #333;
+      margin-bottom: 29rpx;
+    }
+
+    .item {
+      font-size: 24rpx;
+      color: var(--ui-BG-Main);
+      margin-bottom: 16rpx;
+
+      &:last-of-type {
+        margin-bottom: 0;
+      }
+    }
+  }
+
+  .error-img {
+    width: 400rpx;
+    height: 400rpx;
+  }
+</style>

+ 114 - 0
pages/chat/components/order.vue

@@ -0,0 +1,114 @@
+<template>
+  <view class="bg-white order-list-card-box ss-r-10 ss-m-t-14 ss-m-20"
+        :key="orderData.id">
+    <view class="order-card-header ss-flex ss-col-center ss-row-between ss-p-x-20">
+      <view class="order-no">订单号:{{ orderData.no }}</view>
+      <view class="order-state ss-font-26" :class="formatOrderColor(orderData)">
+        {{ formatOrderStatus(orderData) }}
+      </view>
+    </view>
+    <view class="border-bottom" v-for="item in orderData.items" :key="item.id">
+      <s-goods-item
+        :img="item.picUrl"
+        :title="item.spuName"
+        :skuText="item.properties.map((property) => property.valueName).join(' ')"
+        :price="item.price"
+        :num="item.count"
+      />
+    </view>
+    <view class="pay-box ss-m-t-30 ss-flex ss-row-right ss-p-r-20">
+      <view class="ss-flex ss-col-center">
+        <view class="discounts-title pay-color">共 {{ orderData.productCount }} 件商品,总金额:</view>
+        <view class="discounts-money pay-color">
+          ¥{{ fen2yuan(orderData.payPrice) }}
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup>
+  import { fen2yuan, formatOrderColor, formatOrderStatus } from '@/sheep/hooks/useGoods';
+
+  const props = defineProps({
+    orderData: {
+      type: Object,
+      default: {},
+    },
+  });
+</script>
+
+<style lang="scss" scoped>
+  .order-list-card-box {
+    .order-card-header {
+      height: 80rpx;
+
+      .order-no {
+        font-size: 26rpx;
+        font-weight: 500;
+      }
+
+      .order-state {}
+    }
+    .pay-box {
+      .discounts-title {
+        font-size: 24rpx;
+        line-height: normal;
+        color: #999999;
+      }
+
+      .discounts-money {
+        font-size: 24rpx;
+        line-height: normal;
+        color: #999;
+        font-family: OPPOSANS;
+      }
+
+      .pay-color {
+        color: #333;
+      }
+    }
+    .order-card-footer {
+      height: 100rpx;
+
+      .more-item-box {
+        padding: 20rpx;
+
+        .more-item {
+          height: 60rpx;
+
+          .title {
+            font-size: 26rpx;
+          }
+        }
+      }
+
+      .more-btn {
+        color: $dark-9;
+        font-size: 24rpx;
+      }
+
+      .content {
+        width: 154rpx;
+        color: #333333;
+        font-size: 26rpx;
+        font-weight: 500;
+      }
+    }
+  }
+  .warning-color {
+    color: #faad14;
+  }
+
+  .danger-color {
+    color: #ff3000;
+  }
+
+  .success-color {
+    color: #52c41a;
+  }
+
+  .info-color {
+    color: #999999;
+  }
+</style>

+ 151 - 0
pages/chat/components/select-popup.vue

@@ -0,0 +1,151 @@
+<template>
+  <su-popup :show="show" showClose round="10" backgroundColor="#eee" @close="emits('close')">
+    <view class="select-popup">
+      <view class="title">
+        <span>{{ mode == 'goods' ? '我的浏览' : '我的订单' }}</span>
+      </view>
+      <scroll-view
+        class="scroll-box"
+        scroll-y="true"
+        :scroll-with-animation="true"
+        :show-scrollbar="false"
+        @scrolltolower="loadmore"
+      >
+        <view
+          class="item"
+          v-for="item in state.pagination.data"
+          :key="item.id"
+          @tap="emits('select', { type: mode, data: item })"
+        >
+          <template v-if="mode == 'goods'">
+            <GoodsItem :goodsData="item" />
+          </template>
+          <template v-if="mode == 'order'">
+            <OrderItem :orderData="item" />
+          </template>
+        </view>
+        <uni-load-more :status="state.loadStatus" :content-text="{ contentdown: '上拉加载更多' }" />
+      </scroll-view>
+    </view>
+  </su-popup>
+</template>
+
+<script setup>
+  import { reactive, watch } from 'vue';
+  import _ from 'lodash-es';
+  import GoodsItem from './goods.vue';
+  import OrderItem from './order.vue';
+  import OrderApi from '@/sheep/api/trade/order';
+  import SpuHistoryApi from '@/sheep/api/product/history';
+
+  const emits = defineEmits(['select', 'close']);
+  const props = defineProps({
+    mode: {
+      type: String,
+      default: 'goods',
+    },
+    show: {
+      type: Boolean,
+      default: false,
+    },
+  });
+
+  watch(
+    () => props.mode,
+    () => {
+      state.pagination.data = [];
+      if (props.mode) {
+        getList(state.pagination.page);
+      }
+    },
+  );
+
+  const state = reactive({
+    loadStatus: '',
+    pagination: {
+      data: [],
+      current_page: 1,
+      total: 1,
+      last_page: 1,
+    },
+  });
+
+  async function getList(page, list_rows = 5) {
+    state.loadStatus = 'loading';
+    const res =
+      props.mode == 'goods'
+        ? await SpuHistoryApi.getBrowseHistoryPage({
+            page,
+            list_rows,
+          })
+        : await OrderApi.getOrderPage({
+            page,
+            list_rows,
+          });
+    let orderList = _.concat(state.pagination.data, res.data.list);
+    state.pagination = {
+      ...res.data,
+      data: orderList,
+    };
+    if (state.pagination.current_page < state.pagination.last_page) {
+      state.loadStatus = 'more';
+    } else {
+      state.loadStatus = 'noMore';
+    }
+  }
+
+  function loadmore() {
+    if (state.loadStatus !== 'noMore') {
+      getList(state.pagination.current_page + 1);
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .select-popup {
+    max-height: 600rpx;
+
+    .title {
+      height: 100rpx;
+      line-height: 100rpx;
+      padding: 0 26rpx;
+      background: #fff;
+      border-radius: 20rpx 20rpx 0 0;
+
+      span {
+        font-size: 32rpx;
+        position: relative;
+
+        &::after {
+          content: '';
+          display: block;
+          width: 100%;
+          height: 2px;
+          z-index: 1;
+          position: absolute;
+          left: 0;
+          bottom: -15px;
+          background: var(--ui-BG-Main);
+          pointer-events: none;
+        }
+      }
+    }
+
+    .scroll-box {
+      height: 500rpx;
+    }
+
+    .item {
+      background: #fff;
+      margin: 26rpx 26rpx 0;
+      border-radius: 20rpx;
+
+      :deep() {
+        .image {
+          width: 140rpx;
+          height: 140rpx;
+        }
+      }
+    }
+  }
+</style>

+ 166 - 0
pages/chat/components/toolsPopup.vue

@@ -0,0 +1,166 @@
+<template>
+  <su-popup
+    :show="showTools"
+    @close="handleClose"
+  >
+    <view class="ss-modal-box ss-flex-col">
+      <slot></slot>
+      <view class="content ss-flex ss-flex-1">
+        <template v-if="toolsMode === 'emoji'">
+          <swiper
+            class="emoji-swiper"
+            :indicator-dots="true"
+            circular
+            indicator-active-color="#7063D2"
+            indicator-color="rgba(235, 231, 255, 1)"
+            :autoplay="false"
+            :interval="3000"
+            :duration="1000"
+          >
+            <swiper-item v-for="emoji in emojiPage" :key="emoji">
+              <view class="ss-flex ss-flex-wrap">
+                <image
+                  v-for="item in emoji" :key="item"
+                  class="emoji-img"
+                  :src="sheep.$url.cdn(`/static/img/chat/emoji/${item.file}`)"
+                  @tap="onEmoji(item)"
+                >
+                </image>
+              </view>
+            </swiper-item>
+          </swiper>
+        </template>
+        <template v-else>
+          <view class="image">
+            <s-uploader
+              file-mediatype="image"
+              :imageStyles="{ width: 50, height: 50, border: false }"
+              @select="imageSelect({ type: 'image', data: $event })"
+            >
+              <image
+                class="icon"
+                :src="sheep.$url.static('/static/img/shop/chat/image.png')"
+                mode="aspectFill"
+              ></image>
+            </s-uploader>
+            <view>图片</view>
+          </view>
+          <view class="goods" @tap="onShowSelect('goods')">
+            <image
+              class="icon"
+              :src="sheep.$url.static('/static/img/shop/chat/goods.png')"
+              mode="aspectFill"
+            ></image>
+            <view>商品</view>
+          </view>
+          <view class="order" @tap="onShowSelect('order')">
+            <image
+              class="icon"
+              :src="sheep.$url.static('/static/img/shop/chat/order.png')"
+              mode="aspectFill"
+            ></image>
+            <view>订单</view>
+          </view>
+        </template>
+      </view>
+    </view>
+  </su-popup>
+</template>
+
+<script setup>
+  /**
+   * 聊天工具
+   */
+  import { emojiPage } from '@/pages/chat/util/emoji';
+  import sheep from '@/sheep';
+
+  const props = defineProps({
+    // 工具模式
+    toolsMode: {
+      type: String,
+      default: '',
+    },
+    // 控制工具菜单弹出
+    showTools: {
+      type: Boolean,
+      default: () => false,
+    },
+  });
+  const emits = defineEmits(['onEmoji', 'imageSelect', 'onShowSelect', 'close']);
+
+  // 关闭弹出工具菜单
+  function handleClose() {
+    emits('close');
+  }
+
+  // 选择表情
+  function onEmoji(emoji) {
+    emits('onEmoji', emoji);
+  }
+
+  // 选择图片
+  function imageSelect(val) {
+    emits('imageSelect', val);
+  }
+
+  // 选择商品或订单
+  function onShowSelect(mode) {
+    emits('onShowSelect', mode);
+  }
+</script>
+
+<style scoped lang="scss">
+  .content {
+    width: 100%;
+    align-content: space-around;
+    border-top: 1px solid #dfdfdf;
+    padding: 20rpx 0 0;
+
+    .emoji-swiper {
+      width: 100%;
+      height: 280rpx;
+      padding: 0 20rpx;
+
+      .emoji-img {
+        width: 50rpx;
+        height: 50rpx;
+        display: inline-block;
+        margin: 10rpx;
+      }
+    }
+
+    .image,
+    .goods,
+    .order {
+      width: 33.3%;
+      height: 280rpx;
+      text-align: center;
+      font-size: 24rpx;
+      color: #333;
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      justify-content: center;
+
+      .icon {
+        width: 50rpx;
+        height: 50rpx;
+        margin-bottom: 21rpx;
+      }
+    }
+
+    :deep() {
+      .uni-file-picker__container {
+        justify-content: center;
+      }
+
+      .file-picker__box {
+        display: none;
+
+        &:last-of-type {
+          display: flex;
+        }
+      }
+    }
+  }
+</style>

+ 187 - 0
pages/chat/index.vue

@@ -0,0 +1,187 @@
+<template>
+  <s-layout class="chat-wrap" :title="!isReconnecting ? '连接客服成功' : '会话重连中'" navbar="inner">
+    <!--  覆盖头部导航栏背景颜色  -->
+    <div class="page-bg" :style="{ height: sys_navBar + 'px' }"></div>
+    <!--  聊天区域  -->
+    <MessageList ref="messageListRef">
+      <template #bottom>
+        <message-input v-model="chat.msg" @on-tools="onTools" @send-message="onSendMessage"></message-input>
+      </template>
+    </MessageList>
+    <!--  聊天工具  -->
+    <tools-popup :show-tools="chat.showTools" :tools-mode="chat.toolsMode" @close="handleToolsClose"
+                 @on-emoji="onEmoji" @image-select="onSelect" @on-show-select="onShowSelect">
+      <message-input v-model="chat.msg" @on-tools="onTools" @send-message="onSendMessage"></message-input>
+    </tools-popup>
+    <!--  商品订单选择  -->
+    <SelectPopup
+      :mode="chat.selectMode"
+      :show="chat.showSelect"
+      @select="onSelect"
+      @close="chat.showSelect = false"
+    />
+  </s-layout>
+</template>
+
+<script setup>
+  import MessageList from '@/pages/chat/components/messageList.vue';
+  import { reactive, ref, toRefs } from 'vue';
+  import sheep from '@/sheep';
+  import ToolsPopup from '@/pages/chat/components/toolsPopup.vue';
+  import MessageInput from '@/pages/chat/components/messageInput.vue';
+  import SelectPopup from '@/pages/chat/components/select-popup.vue';
+  import { KeFuMessageContentTypeEnum, WebSocketMessageTypeConstants } from '@/pages/chat/util/constants';
+  import FileApi from '@/sheep/api/infra/file';
+  import KeFuApi from '@/sheep/api/promotion/kefu';
+  import { useWebSocket } from '@/sheep/hooks/useWebSocket';
+
+  const sys_navBar = sheep.$platform.navbar;
+
+  const chat = reactive({
+    msg: '',
+    scrollInto: '',
+    showTools: false,
+    toolsMode: '',
+    showSelect: false,
+    selectMode: '',
+  });
+
+  // 发送消息
+  async function onSendMessage() {
+    if (!chat.msg) return;
+    try {
+      const data = {
+        contentType: KeFuMessageContentTypeEnum.TEXT,
+        content: chat.msg,
+      };
+      await KeFuApi.sendKefuMessage(data);
+      await messageListRef.value.refreshMessageList();
+      chat.msg = '';
+    } finally {
+      chat.showTools = false;
+    }
+  }
+
+  const messageListRef = ref();
+
+  //======================= 聊天工具相关 start =======================
+
+  function handleToolsClose() {
+    chat.showTools = false;
+    chat.toolsMode = '';
+  }
+
+  function onEmoji(item) {
+    chat.msg += item.name;
+  }
+
+  // 点击工具栏开关
+  function onTools(mode) {
+    if (isReconnecting.value) {
+      sheep.$helper.toast('您已掉线!请返回重试');
+      return;
+    }
+
+    if (!chat.toolsMode || chat.toolsMode === mode) {
+      chat.showTools = !chat.showTools;
+    }
+    chat.toolsMode = mode;
+    if (!chat.showTools) {
+      chat.toolsMode = '';
+    }
+  }
+
+  function onShowSelect(mode) {
+    chat.showTools = false;
+    chat.showSelect = true;
+    chat.selectMode = mode;
+  }
+
+  async function onSelect({ type, data }) {
+    let msg;
+    switch (type) {
+      case 'image':
+        const res = await FileApi.uploadFile(data.tempFiles[0].path);
+        msg = {
+          contentType: KeFuMessageContentTypeEnum.IMAGE,
+          content: res.data,
+        };
+        break;
+      case 'goods':
+        msg = {
+          contentType: KeFuMessageContentTypeEnum.PRODUCT,
+          content: JSON.stringify(data),
+        };
+        break;
+      case 'order':
+        msg = {
+          contentType: KeFuMessageContentTypeEnum.ORDER,
+          content: JSON.stringify(data),
+        };
+        break;
+    }
+    if (msg) {
+      // 发送消息
+      // scrollBottom();
+      await KeFuApi.sendKefuMessage(msg);
+      await messageListRef.value.refreshMessageList();
+      chat.showTools = false;
+      chat.showSelect = false;
+      chat.selectMode = '';
+    }
+  }
+
+  //======================= 聊天工具相关 end =======================
+  const { options } = useWebSocket({
+    // 连接成功
+    onConnected: async () => {
+    },
+    // 收到消息
+    onMessage: async (data) => {
+      const type = data.type;
+      if (!type) {
+        console.error('未知的消息类型:' + data.value);
+        return;
+      }
+      // 2.2 消息类型:KEFU_MESSAGE_TYPE
+      if (type === WebSocketMessageTypeConstants.KEFU_MESSAGE_TYPE) {
+        // 刷新消息列表
+        await messageListRef.value.refreshMessageList(JSON.parse(data.content));
+        return;
+      }
+      // 2.3 消息类型:KEFU_MESSAGE_ADMIN_READ
+      if (type === WebSocketMessageTypeConstants.KEFU_MESSAGE_ADMIN_READ) {
+        console.log('管理员已读消息');
+      }
+    },
+  });
+  const isReconnecting = toRefs(options).isReconnecting; // 重连状态
+</script>
+
+<style scoped lang="scss">
+  .chat-wrap {
+
+    .page-bg {
+      width: 100%;
+      position: absolute;
+      top: 0;
+      left: 0;
+      background-color: var(--ui-BG-Main);
+      z-index: 1;
+    }
+
+    .status {
+      position: relative;
+      box-sizing: border-box;
+      z-index: 3;
+      height: 70rpx;
+      padding: 0 30rpx;
+      background: var(--ui-BG-Main-opacity-1);
+      display: flex;
+      align-items: center;
+      font-size: 30rpx;
+      font-weight: 400;
+      color: var(--ui-BG-Main);
+    }
+  }
+</style>

+ 19 - 0
pages/chat/util/constants.js

@@ -0,0 +1,19 @@
+export const KeFuMessageContentTypeEnum = {
+  TEXT: 1, // 文本消息
+  IMAGE: 2, // 图片消息
+  VOICE: 3, // 语音消息
+  VIDEO: 4, // 视频消息
+  SYSTEM: 5, // 系统消息
+  // ========== 商城特殊消息 ==========
+  PRODUCT: 10,//  商品消息
+  ORDER: 11,//  订单消息"
+};
+export const UserTypeEnum = {
+  MEMBER: 1, // 会员 面向 c 端,普通用户
+  ADMIN: 2, // 管理员 面向 b 端,管理后台
+};
+// Promotion 的 WebSocket 消息类型枚举类
+export const WebSocketMessageTypeConstants = {
+  KEFU_MESSAGE_TYPE: 'kefu_message_type', // 客服消息类型
+  KEFU_MESSAGE_ADMIN_READ: 'kefu_message_read_status_change' // 客服消息管理员已读
+}

+ 58 - 0
pages/chat/util/emoji.js

@@ -0,0 +1,58 @@
+export const emojiList = [
+  { name: '[笑掉牙]', file: 'xiaodiaoya.png' },
+  { name: '[可爱]', file: 'keai.png' },
+  { name: '[冷酷]', file: 'lengku.png' },
+  { name: '[闭嘴]', file: 'bizui.png' },
+  { name: '[生气]', file: 'shengqi.png' },
+  { name: '[惊恐]', file: 'jingkong.png' },
+  { name: '[瞌睡]', file: 'keshui.png' },
+  { name: '[大笑]', file: 'daxiao.png' },
+  { name: '[爱心]', file: 'aixin.png' },
+  { name: '[坏笑]', file: 'huaixiao.png' },
+  { name: '[飞吻]', file: 'feiwen.png' },
+  { name: '[疑问]', file: 'yiwen.png' },
+  { name: '[开心]', file: 'kaixin.png' },
+  { name: '[发呆]', file: 'fadai.png' },
+  { name: '[流泪]', file: 'liulei.png' },
+  { name: '[汗颜]', file: 'hanyan.png' },
+  { name: '[惊悚]', file: 'jingshu.png' },
+  { name: '[困~]', file: 'kun.png' },
+  { name: '[心碎]', file: 'xinsui.png' },
+  { name: '[天使]', file: 'tianshi.png' },
+  { name: '[晕]', file: 'yun.png' },
+  { name: '[啊]', file: 'a.png' },
+  { name: '[愤怒]', file: 'fennu.png' },
+  { name: '[睡着]', file: 'shuizhuo.png' },
+  { name: '[面无表情]', file: 'mianwubiaoqing.png' },
+  { name: '[难过]', file: 'nanguo.png' },
+  { name: '[犯困]', file: 'fankun.png' },
+  { name: '[好吃]', file: 'haochi.png' },
+  { name: '[呕吐]', file: 'outu.png' },
+  { name: '[龇牙]', file: 'ziya.png' },
+  { name: '[懵比]', file: 'mengbi.png' },
+  { name: '[白眼]', file: 'baiyan.png' },
+  { name: '[饿死]', file: 'esi.png' },
+  { name: '[凶]', file: 'xiong.png' },
+  { name: '[感冒]', file: 'ganmao.png' },
+  { name: '[流汗]', file: 'liuhan.png' },
+  { name: '[笑哭]', file: 'xiaoku.png' },
+  { name: '[流口水]', file: 'liukoushui.png' },
+  { name: '[尴尬]', file: 'ganga.png' },
+  { name: '[惊讶]', file: 'jingya.png' },
+  { name: '[大惊]', file: 'dajing.png' },
+  { name: '[不好意思]', file: 'buhaoyisi.png' },
+  { name: '[大闹]', file: 'danao.png' },
+  { name: '[不可思议]', file: 'bukesiyi.png' },
+  { name: '[爱你]', file: 'aini.png' },
+  { name: '[红心]', file: 'hongxin.png' },
+  { name: '[点赞]', file: 'dianzan.png' },
+  { name: '[恶魔]', file: 'emo.png' },
+];
+
+export let emojiPage = {};
+emojiList.forEach((item, index) => {
+  if (!emojiPage[Math.floor(index / 30) + 1]) {
+    emojiPage[Math.floor(index / 30) + 1] = [];
+  }
+  emojiPage[Math.floor(index / 30) + 1].push(item);
+});

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 160 - 0
pages/commission/commission-ranking.vue


+ 125 - 0
pages/commission/components/account-info.vue

@@ -0,0 +1,125 @@
+<!-- 分销账户:展示基本统计信息 -->
+<template>
+  <view class="account-card">
+    <view class="account-card-box">
+      <view class="ss-flex ss-row-between card-box-header">
+        <view class="ss-flex">
+          <view class="header-title ss-m-r-16">账户信息</view>
+          <button
+            class="ss-reset-button look-btn ss-flex"
+            @tap="state.showMoney = !state.showMoney"
+          >
+            <uni-icons
+              :type="state.showMoney ? 'eye-filled' : 'eye-slash-filled'"
+              color="#A57A55"
+              size="20"
+            />
+          </button>
+        </view>
+        <view class="ss-flex" @tap="sheep.$router.go('/pages/commission/wallet')">
+          <view class="header-title ss-m-r-4">查看明细</view>
+          <text class="cicon-play-arrow" />
+        </view>
+      </view>
+
+      <!-- 收益 -->
+      <view class="card-content ss-flex">
+        <view class="ss-flex-1 ss-flex-col ss-col-center">
+          <view class="item-title">当前佣金(元)</view>
+          <view class="item-detail">
+            {{ state.showMoney ? fen2yuan(state.summary.brokeragePrice || 0) : '***' }}
+          </view>
+        </view>
+        <view class="ss-flex-1 ss-flex-col ss-col-center">
+          <view class="item-title">昨天的佣金(元)</view>
+          <view class="item-detail">
+            {{ state.showMoney ? fen2yuan(state.summary.yesterdayPrice || 0) : '***' }}
+          </view>
+        </view>
+        <view class="ss-flex-1 ss-flex-col ss-col-center">
+          <view class="item-title">累计已提(元)</view>
+          <view class="item-detail">
+            {{ state.showMoney ? fen2yuan(state.summary.withdrawPrice || 0) : '***' }}
+          </view>
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup>
+  import sheep from '@/sheep';
+  import { computed, reactive, onMounted } from 'vue';
+  import BrokerageApi from '@/sheep/api/trade/brokerage';
+  import { fen2yuan } from '@/sheep/hooks/useGoods';
+
+  const userInfo = computed(() => sheep.$store('user').userInfo);
+
+  const state = reactive({
+    showMoney: false,
+    summary: {},
+  });
+
+  onMounted(async () => {
+    let { code, data } = await BrokerageApi.getBrokerageUserSummary();
+    if (code === 0) {
+      state.summary = data || {}
+    }
+  });
+</script>
+
+<style lang="scss" scoped>
+  .account-card {
+    width: 694rpx;
+    margin: 0 auto;
+    padding: 2rpx;
+    background: linear-gradient(180deg, #ffffff 0.88%, #fff9ec 100%);
+    border-radius: 12rpx;
+    z-index: 3;
+    position: relative;
+
+    .account-card-box {
+      background: #ffefd6;
+
+      .card-box-header {
+        padding: 0 30rpx;
+        height: 72rpx;
+        box-shadow: 0px 2px 6px #f2debe;
+
+        .header-title {
+          font-size: 24rpx;
+          font-weight: 500;
+          color: #a17545;
+          line-height: 30rpx;
+        }
+
+        .cicon-play-arrow {
+          color: #a17545;
+          font-size: 24rpx;
+          line-height: 30rpx;
+        }
+      }
+
+      .card-content {
+        height: 190rpx;
+        background: #fdfae9;
+
+        .item-title {
+          font-size: 24rpx;
+          font-weight: 500;
+          color: #cba67e;
+          line-height: 30rpx;
+          margin-bottom: 24rpx;
+        }
+
+        .item-detail {
+          font-size: 36rpx;
+          font-family: OPPOSANS;
+          font-weight: bold;
+          color: #692e04;
+          line-height: 30rpx;
+        }
+      }
+    }
+  }
+</style>

+ 160 - 0
pages/commission/components/account-type-select.vue

@@ -0,0 +1,160 @@
+<!-- 提现方式的 select 组件 -->
+<template>
+  <su-popup :show="show" class="ss-checkout-counter-wrap" @close="hideModal">
+    <view class="ss-modal-box bg-white ss-flex-col">
+      <view class="modal-header ss-flex-col ss-col-left">
+        <text class="modal-title ss-m-b-20">选择提现方式</text>
+      </view>
+      <view class="modal-content ss-flex-1 ss-p-b-100">
+        <radio-group @change="onChange">
+          <label
+            class="container-list ss-p-l-34 ss-p-r-24 ss-flex ss-col-center ss-row-center"
+            v-for="(item, index) in typeList"
+            :key="index"
+          >
+            <view class="container-icon ss-flex ss-m-r-20">
+              <image :src="sheep.$url.static(item.icon)" />
+            </view>
+            <view class="ss-flex-1">{{ item.title }}</view>
+            <radio
+              :value="item.value"
+              color="var(--ui-BG-Main)"
+              :checked="item.value === state.currentValue"
+              :disabled="!methods.includes(parseInt(item.value))"
+            />
+          </label>
+        </radio-group>
+      </view>
+      <view class="modal-footer ss-flex ss-row-center ss-col-center">
+        <button class="ss-reset-button save-btn" @tap="onConfirm">确定</button>
+      </view>
+    </view>
+  </su-popup>
+</template>
+
+<script setup>
+  import { reactive } from 'vue';
+  import sheep from '@/sheep';
+
+  const props = defineProps({
+    modelValue: {
+      type: Object,
+      default() {},
+    },
+    show: {
+      type: Boolean,
+      default: false,
+    },
+    methods: { // 开启的提现方式
+      type: Array,
+      default: [],
+    },
+  });
+  const emits = defineEmits(['update:modelValue', 'change', 'close']);
+  const state = reactive({
+    currentValue: '',
+  });
+
+  const typeList = [
+    {
+      // icon: '/static/img/shop/pay/wechat.png', // TODO 芋艿:后续给个 icon
+      title: '钱包余额',
+      value: '1',
+    },
+    {
+      icon: '/static/img/shop/pay/bank.png',
+      title: '银行卡转账',
+      value: '2',
+    },
+    {
+      icon: '/static/img/shop/pay/wechat.png',
+      title: '微信零钱',
+      value: '3',
+    },
+    {
+      icon: '/static/img/shop/pay/alipay.png',
+      title: '支付宝账户',
+      value: '4',
+    }
+  ];
+
+  function onChange(e) {
+    state.currentValue = e.detail.value;
+  }
+
+  const onConfirm = async () => {
+    if (state.currentValue === '') {
+      sheep.$helper.toast('请选择提现方式');
+      return;
+    }
+    // 赋值
+    emits('update:modelValue', {
+      type: state.currentValue
+    });
+    // 关闭弹窗
+    emits('close');
+  };
+
+  const hideModal = () => {
+    emits('close');
+  };
+</script>
+
+<style lang="scss" scoped>
+  .ss-modal-box {
+    border-radius: 30rpx 30rpx 0 0;
+    max-height: 1000rpx;
+
+    .modal-header {
+      position: relative;
+      padding: 60rpx 40rpx 40rpx;
+
+      .modal-title {
+        font-size: 32rpx;
+        font-weight: bold;
+      }
+
+      .close-icon {
+        position: absolute;
+        top: 10rpx;
+        right: 20rpx;
+        font-size: 46rpx;
+        opacity: 0.2;
+      }
+    }
+
+    .modal-content {
+      overflow-y: auto;
+
+      .container-list {
+        height: 96rpx;
+        border-bottom: 2rpx solid rgba(#dfdfdf, 0.5);
+        font-size: 28rpx;
+        font-weight: 500;
+        color: #333333;
+
+        .container-icon {
+          width: 36rpx;
+          height: 36rpx;
+        }
+      }
+    }
+
+    .modal-footer {
+      height: 120rpx;
+
+      .save-btn {
+        width: 710rpx;
+        height: 80rpx;
+        border-radius: 40rpx;
+        background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
+        color: $white;
+      }
+    }
+  }
+
+  image {
+    width: 100%;
+    height: 100%;
+  }
+</style>

+ 101 - 0
pages/commission/components/commission-auth.vue

@@ -0,0 +1,101 @@
+<!-- 分销权限弹窗:再没有权限时,进行提示  -->
+<template>
+  <su-popup
+    :show="state.show"
+    type="center"
+    round="10"
+    @close="state.show = false"
+    :isMaskClick="false"
+    maskBackgroundColor="rgba(0, 0, 0, 0.7)"
+  >
+    <view class="notice-box">
+      <view class="img-wrap">
+        <image
+          class="notice-img"
+          :src="sheep.$url.static('/static/img/shop/commission/forbidden.png')"
+          mode="aspectFill"
+        />
+      </view>
+      <view class="notice-title"> 抱歉!您没有分销权限 </view>
+      <view class="notice-detail"> 该功能暂不可用 </view>
+      <button
+        class="ss-reset-button notice-btn ui-Shadow-Main ui-BG-Main-Gradient"
+        @tap="sheep.$router.back()"
+      >
+        知道了
+      </button>
+      <button class="ss-reset-button back-btn" @tap="sheep.$router.back()"> 返回 </button>
+    </view>
+  </su-popup>
+</template>
+
+<script setup>
+  import { onShow } from '@dcloudio/uni-app';
+  import sheep from '@/sheep';
+  import { reactive } from 'vue';
+  import BrokerageApi from '@/sheep/api/trade/brokerage';
+
+  const state = reactive({
+    show: false,
+  });
+
+  onShow(async () => {
+    // 读取是否有分销权限
+    const { code, data } = await BrokerageApi.getBrokerageUser();
+    if (code === 0 && !data?.brokerageEnabled) {
+      state.show = true;
+    }
+  });
+</script>
+
+<style lang="scss" scoped>
+  .notice-box {
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    background-color: #fff;
+    width: 612rpx;
+    min-height: 658rpx;
+    background: #ffffff;
+    padding: 30rpx;
+    border-radius: 20rpx;
+    .img-wrap {
+      margin-bottom: 50rpx;
+      .notice-img {
+        width: 180rpx;
+        height: 170rpx;
+      }
+    }
+    .notice-title {
+      font-size: 35rpx;
+      font-weight: bold;
+      color: #333;
+      margin-bottom: 28rpx;
+    }
+    .notice-detail {
+      font-size: 28rpx;
+      font-weight: 400;
+      color: #999999;
+      line-height: 36rpx;
+      margin-bottom: 50rpx;
+    }
+    .notice-btn {
+      width: 492rpx;
+      line-height: 70rpx;
+      border-radius: 35rpx;
+      font-size: 28rpx;
+      font-weight: 500;
+      color: #ffffff;
+      margin-bottom: 10rpx;
+    }
+    .back-btn {
+      width: 492rpx;
+      line-height: 70rpx;
+      font-size: 28rpx;
+      font-weight: 500;
+      color: var(--ui-BG-Main-gradient);
+      background: none;
+    }
+  }
+</style>

+ 114 - 0
pages/commission/components/commission-info.vue

@@ -0,0 +1,114 @@
+<!-- 分销商信息  -->
+<template>
+	<!-- 用户资料 -->
+	<view class="user-card ss-flex ss-col-bottom">
+		<view class="card-top ss-flex ss-row-between">
+			<view class="ss-flex">
+				<view class="head-img-box">
+					<image class="head-img" :src="sheep.$url.cdn(userInfo.avatar)" mode="aspectFill"></image>
+				</view>
+				<view class="ss-flex-col">
+					<view class="user-name">{{ userInfo.nickname }}</view>
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script setup>
+	import sheep from '@/sheep';
+	import { computed, reactive } from 'vue';
+
+	const userInfo = computed(() => sheep.$store('user').userInfo);
+	const headerBg = sheep.$url.css('/static/img/shop/commission/background.png');
+
+	const state = reactive({
+		showMoney: false,
+	});
+
+</script>
+
+<style lang="scss" scoped>
+	// 用户资料卡片
+	.user-card {
+		width: 700rpx;
+		height: 192rpx;
+		margin: 0 auto;
+		margin-top: -88rpx;
+		padding-top: 88rpx;
+		background: v-bind(headerBg) no-repeat;
+		background-size: 100% 100%;
+
+		.head-img-box {
+			margin-right: 20rpx;
+			width: 100rpx;
+			height: 100rpx;
+			border-radius: 50%;
+			position: relative;
+			background: #fce0ad;
+
+			.head-img {
+				width: 92rpx;
+				height: 92rpx;
+				border-radius: 50%;
+				position: absolute;
+				top: 50%;
+				left: 50%;
+				transform: translate(-50%, -50%);
+			}
+		}
+
+		.card-top {
+			box-sizing: border-box;
+			padding-bottom: 34rpx;
+
+			.user-name {
+				font-size: 32rpx;
+				font-weight: bold;
+				color: #692e04;
+				line-height: 30rpx;
+				margin-bottom: 20rpx;
+			}
+
+			.log-btn {
+				width: 84rpx;
+				height: 42rpx;
+				border: 2rpx solid rgba(#ffffff, 0.33);
+				border-radius: 21rpx;
+				font-size: 22rpx;
+				font-weight: 400;
+				color: #ffffff;
+				margin-bottom: 20rpx;
+			}
+
+			.look-btn {
+				color: #fff;
+				width: 40rpx;
+				height: 40rpx;
+			}
+		}
+
+		.user-info-box {
+			.tag-box {
+				background: #ff6000;
+				border-radius: 18rpx;
+				line-height: 36rpx;
+
+				.tag-img {
+					width: 36rpx;
+					height: 36rpx;
+					border-radius: 50%;
+					margin-left: -2rpx;
+				}
+
+				.tag-title {
+					font-size: 24rpx;
+					padding: 0 10rpx;
+					font-weight: 500;
+					line-height: 36rpx;
+					color: #fff;
+				}
+			}
+		}
+	}
+</style>

+ 181 - 0
pages/commission/components/commission-log.vue

@@ -0,0 +1,181 @@
+<!-- 分销首页:明细列表  -->
+<template>
+  <view class="distribution-log-wrap">
+    <view class="header-box">
+      <image class="header-bg" :src="sheep.$url.static('/static/img/shop/commission/title2.png')" />
+      <view class="ss-flex header-title">
+        <view class="title">实时动态</view>
+        <text class="cicon-forward" />
+      </view>
+    </view>
+    <scroll-view
+      scroll-y="true"
+      @scrolltolower="loadmore"
+      class="scroll-box log-scroll"
+      scroll-with-animation="true"
+    >
+      <view v-if="state.pagination.list">
+        <view
+          class="log-item-box ss-flex ss-row-between"
+          v-for="item in state.pagination.list"
+          :key="item.id"
+        >
+          <view class="log-item-wrap">
+            <view class="log-item ss-flex ss-ellipsis-1 ss-col-center">
+              <view class="ss-flex ss-col-center">
+                <image
+                  class="log-img"
+                  :src="sheep.$url.static('/static/img/shop/avatar/notice.png')"
+                  mode="aspectFill"
+                />
+              </view>
+              <view class="log-text ss-ellipsis-1">
+                {{ item.title }} {{ fen2yuan(item.price) }} 元
+              </view>
+            </view>
+          </view>
+          <text class="log-time">{{ dayjs(item.createTime).fromNow() }}</text>
+        </view>
+      </view>
+
+      <!-- 加载更多 -->
+      <uni-load-more
+        v-if="state.pagination.total > 0"
+        :status="state.loadStatus"
+        color="#333333"
+        @tap="loadmore"
+      />
+    </scroll-view>
+  </view>
+</template>
+
+<script setup>
+  import sheep from '@/sheep';
+  import { reactive } from 'vue';
+  import _ from 'lodash-es';
+  import dayjs from 'dayjs';
+  import BrokerageApi from '@/sheep/api/trade/brokerage';
+  import { fen2yuan } from '../../../sheep/hooks/useGoods';
+
+  const state = reactive({
+    loadStatus: '',
+    pagination: {
+      list: [],
+      total: 0,
+      pageNo: 1,
+      pageSize: 8,
+    },
+  });
+
+  async function getLog() {
+    state.loadStatus = 'loading';
+    const { code, data } = await BrokerageApi.getBrokerageRecordPage({
+      pageNo: state.pagination.pageNo,
+      pageSize: state.pagination.pageSize,
+    });
+    if (code !== 0) {
+      return;
+    }
+    state.pagination.list = _.concat(state.pagination.list, data.list);
+    state.pagination.total = data.total;
+    state.loadStatus = state.pagination.list.length < state.pagination.total ? 'more' : 'noMore';
+  }
+
+  getLog();
+
+  // 加载更多
+  function loadmore() {
+    if (state.loadStatus === 'noMore') {
+      return;
+    }
+    state.pagination.pageNo++;
+    getLog();
+  }
+</script>
+
+<style lang="scss" scoped>
+  .distribution-log-wrap {
+    width: 690rpx;
+    margin: 0 auto;
+    margin-bottom: 20rpx;
+    border-radius: 12rpx;
+    z-index: 3;
+    position: relative;
+
+    .header-box {
+      width: 690rpx;
+      height: 76rpx;
+      position: relative;
+
+      .header-bg {
+        width: 690rpx;
+        height: 76rpx;
+      }
+
+      .header-title {
+        position: absolute;
+        left: 20rpx;
+        top: 24rpx;
+      }
+
+      .title {
+        font-size: 28rpx;
+        font-weight: 500;
+        color: #ffffff;
+        line-height: 30rpx;
+      }
+
+      .cicon-forward {
+        font-size: 30rpx;
+        font-weight: 400;
+        color: #ffffff;
+        line-height: 30rpx;
+      }
+    }
+
+    .log-scroll {
+      height: 600rpx;
+      background: #fdfae9;
+      padding: 10rpx 20rpx 0;
+      box-sizing: border-box;
+      border-radius: 0 0 12rpx 12rpx;
+
+      .log-item-box {
+        margin-bottom: 20rpx;
+
+        .log-time {
+          // margin-left: 30rpx;
+          text-align: right;
+          font-size: 24rpx;
+          font-family: OPPOSANS;
+          font-weight: 400;
+          color: #c4c4c4;
+        }
+      }
+
+      .loadmore-wrap {
+        // line-height: 80rpx;
+      }
+
+      .log-item {
+        // background: rgba(#ffffff, 0.2);
+        border-radius: 24rpx;
+        padding: 6rpx 20rpx 6rpx 12rpx;
+
+        .log-img {
+          width: 40rpx;
+          height: 40rpx;
+          border-radius: 50%;
+          margin-right: 10rpx;
+        }
+
+        .log-text {
+          max-width: 480rpx;
+          font-size: 24rpx;
+          font-weight: 500;
+          color: #333333;
+        }
+      }
+    }
+  }
+</style>

+ 145 - 0
pages/commission/components/commission-menu.vue

@@ -0,0 +1,145 @@
+<!-- 分销:商菜单栏 -->
+<template>
+  <view class="menu-box ss-flex-col">
+    <view class="header-box">
+      <image class="header-bg" :src="sheep.$url.static('/static/img/shop/commission/title1.png')" />
+      <view class="ss-flex header-title">
+        <view class="title">功能专区</view>
+        <text class="cicon-forward"></text>
+      </view>
+    </view>
+    <view class="menu-list ss-flex ss-flex-wrap">
+      <view
+        v-for="(item, index) in state.menuList"
+        :key="index"
+        class="item-box ss-flex-col ss-col-center"
+        @tap="sheep.$router.go(item.path)"
+      >
+        <image
+          class="menu-icon ss-m-b-10"
+          :src="sheep.$url.static(item.img)"
+          mode="aspectFill"
+        ></image>
+        <view>{{ item.title }}</view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup>
+  import sheep from '@/sheep';
+  import { reactive } from 'vue';
+
+  const state = reactive({
+    menuList: [
+      {
+        img: '/static/img/shop/commission/commission_icon1.png',
+        title: '我的团队',
+        path: '/pages/commission/team',
+      },
+      {
+        img: '/static/img/shop/commission/commission_icon2.png',
+        title: '佣金明细',
+        path: '/pages/commission/wallet',
+      },
+      {
+        img: '/static/img/shop/commission/commission_icon3.png',
+        title: '分销订单',
+        path: '/pages/commission/order',
+      },
+      {
+        img: '/static/img/shop/commission/commission_icon4.png',
+        title: '推广商品',
+        path: '/pages/commission/goods',
+      },
+      // {
+      //   img: '/static/img/shop/commission/commission_icon5.png',
+      //   title: '我的资料',
+      //   path: '/pages/commission/apply',
+      //   isAgentFrom: true,
+      // },
+      {
+        img: '/static/img/shop/commission/commission_icon7.png',
+        title: '邀请海报',
+        path: 'action:showShareModal',
+      },
+      {
+        img: '/static/img/shop/commission/commission_icon8.png',
+        title: '推广排行',
+        path: '/pages/commission/promoter',
+      },
+      {
+        img: '/static/img/shop/commission/commission_icon9.png',
+        title: '佣金排行',
+        path: '/pages/commission/commission-ranking',
+      },
+    ],
+  });
+</script>
+
+<style lang="scss" scoped>
+  .menu-box {
+    margin: 0 auto;
+    width: 690rpx;
+    margin-bottom: 20rpx;
+    margin-top: 20rpx;
+    border-radius: 12rpx;
+    z-index: 3;
+    position: relative;
+  }
+
+  .header-box {
+    width: 690rpx;
+    height: 76rpx;
+    position: relative;
+
+    .header-bg {
+      width: 690rpx;
+      height: 76rpx;
+    }
+
+    .header-title {
+      position: absolute;
+      left: 20rpx;
+      top: 24rpx;
+    }
+
+    .title {
+      font-size: 28rpx;
+      font-weight: 500;
+      color: #ffffff;
+      line-height: 30rpx;
+    }
+
+    .cicon-forward {
+      font-size: 30rpx;
+      font-weight: 400;
+      color: #ffffff;
+      line-height: 30rpx;
+    }
+  }
+
+  .menu-list {
+    padding: 50rpx 0 10rpx 0;
+    background: #fdfae9;
+    border-radius: 0 0 12rpx 12rpx;
+  }
+
+  .item-box {
+    width: 25%;
+    margin-bottom: 40rpx;
+  }
+
+  .menu-icon {
+    width: 68rpx;
+    height: 68rpx;
+    background: #ffffff;
+    border-radius: 50%;
+  }
+
+  .menu-title {
+    font-size: 26rpx;
+    font-weight: 500;
+    color: #ffffff;
+  }
+</style>

+ 166 - 0
pages/commission/goods.vue

@@ -0,0 +1,166 @@
+<!-- 分销商品列表  -->
+<template>
+  <s-layout title="推广商品" :onShareAppMessage="state.shareInfo">
+    <view class="goods-item ss-m-20" v-for="item in state.pagination.list" :key="item.id">
+      <s-goods-item
+        size="lg"
+        :img="item.picUrl"
+        :title="item.name"
+        :subTitle="item.introduction"
+        :price="item.price"
+        :originPrice="item.marketPrice"
+        priceColor="#333"
+        @tap="sheep.$router.go('/pages/goods/index', { id: item.id })"
+      >
+        <template #rightBottom>
+          <view class="ss-flex ss-row-between">
+            <view class="commission-num" v-if="item.brokerageMinPrice === undefined"
+              >预计佣金:计算中</view
+            >
+            <view
+              class="commission-num"
+              v-else-if="item.brokerageMinPrice === item.brokerageMaxPrice"
+            >
+              预计佣金:{{ fen2yuan(item.brokerageMinPrice) }}
+            </view>
+            <view class="commission-num" v-else>
+              预计佣金:{{ fen2yuan(item.brokerageMinPrice) }} ~
+              {{ fen2yuan(item.brokerageMaxPrice) }}
+            </view>
+            <button
+              class="ss-reset-button share-btn ui-BG-Main-Gradient"
+              @tap.stop="onShareGoods(item)"
+            >
+              分享赚
+            </button>
+          </view>
+        </template>
+      </s-goods-item>
+    </view>
+    <s-empty
+      v-if="state.pagination.total === 0"
+      icon="/static/goods-empty.png"
+      text="暂无推广商品"
+    />
+    <!-- 加载更多 -->
+    <uni-load-more
+      v-if="state.pagination.total > 0"
+      :status="state.loadStatus"
+      :content-text="{
+        contentdown: '上拉加载更多',
+      }"
+      @tap="loadMore"
+    />
+  </s-layout>
+</template>
+
+<script setup>
+  import sheep from '@/sheep';
+  import $share from '@/sheep/platform/share';
+  import { onLoad, onReachBottom } from '@dcloudio/uni-app';
+  import { reactive } from 'vue';
+  import _ from 'lodash-es';
+  import { showShareModal } from '@/sheep/hooks/useModal';
+  import SpuApi from '@/sheep/api/product/spu';
+  import BrokerageApi from '@/sheep/api/trade/brokerage';
+  import { fen2yuan } from '@/sheep/hooks/useGoods';
+
+  const state = reactive({
+    pagination: {
+      list: [],
+      total: 0,
+      pageNo: 1,
+      pageSize: 8,
+    },
+    loadStatus: '',
+    shareInfo: {},
+  });
+
+  // TODO @puhui999:【分享】接入
+  function onShareGoods(goodsInfo) {
+    state.shareInfo = $share.getShareInfo(
+      {
+        title: goodsInfo.title,
+        image: sheep.$url.cdn(goodsInfo.image),
+        desc: goodsInfo.subtitle,
+        params: {
+          page: '2',
+          query: goodsInfo.id,
+        },
+      },
+      {
+        type: 'goods', // 商品海报
+        title: goodsInfo.title, // 商品标题
+        image: sheep.$url.cdn(goodsInfo.image), // 商品主图
+        price: goodsInfo.price[0], // 商品价格
+        original_price: goodsInfo.original_price, // 商品原价
+      },
+    );
+    showShareModal();
+  }
+
+  async function getGoodsList() {
+    state.loadStatus = 'loading';
+    let { code, data } = await SpuApi.getSpuPage({
+      pageSize: state.pagination.pageSize,
+      pageNo: state.pagination.pageNo,
+    });
+
+    if (code !== 0) {
+      state.loadStatus = 'error'; // 处理错误状态
+      return;
+    }
+
+    // 使用 Promise.all 来等待所有佣金请求完成
+    await Promise.all(
+      data.list.map(async (item) => {
+        try {
+          const res = await BrokerageApi.getProductBrokeragePrice(item.id);
+          item.brokerageMinPrice = res.data.brokerageMinPrice;
+          item.brokerageMaxPrice = res.data.brokerageMaxPrice;
+        } catch (error) {
+          console.error(`获取商品【${item.name}】的佣金时出错:`, error);
+        }
+      }),
+    );
+
+    // 在所有请求完成后合并列表和更新状态
+    state.pagination.list = _.concat(state.pagination.list, data.list);
+    state.pagination.total = data.total;
+    state.loadStatus = state.pagination.list.length < state.pagination.total ? 'more' : 'noMore';
+  }
+
+  onLoad(() => {
+    getGoodsList();
+  });
+
+  // 加载更多
+  function loadMore() {
+    if (state.loadStatus === 'noMore') {
+      return;
+    }
+    state.pagination.pageNo++;
+    getGoodsList();
+  }
+
+  // 上拉加载更多
+  onReachBottom(() => {
+    loadMore();
+  });
+</script>
+
+<style lang="scss" scoped>
+  .goods-item {
+    .commission-num {
+      font-size: 24rpx;
+      font-weight: 500;
+      color: $red;
+    }
+
+    .share-btn {
+      width: 120rpx;
+      height: 50rpx;
+      border-radius: 25rpx;
+    }
+  }
+</style>

+ 46 - 0
pages/commission/index.vue

@@ -0,0 +1,46 @@
+<!-- 分销中心  -->
+<template>
+	<s-layout navbar="inner" class="index-wrap" title="分销中心" :bgStyle="bgStyle" :onShareAppMessage="shareInfo">
+		<!-- 分销商信息 -->
+		<commission-info />
+		<!-- 账户信息 -->
+		<account-info />
+		<!-- 菜单栏 -->
+		<commission-menu />
+		<!-- 分销记录 -->
+		<commission-log />
+
+		<!-- 权限弹窗 -->
+		<commission-auth />
+	</s-layout>
+</template>
+
+<script setup>
+  import { computed } from 'vue';
+	import commissionInfo from './components/commission-info.vue';
+	import accountInfo from './components/account-info.vue';
+	import commissionLog from './components/commission-log.vue';
+	import commissionMenu from './components/commission-menu.vue';
+	import commissionAuth from './components/commission-auth.vue';
+  import sheep from '@/sheep';
+
+  const shareInfo = computed(() => {
+    return sheep.$platform.share.getShareInfo({
+      params: {
+        page: '6',
+      },
+    }, {
+      type: 'user',
+    });
+  });
+
+	const bgStyle = {
+		color: '#F7D598',
+	};
+</script>
+
+<style lang="scss" scoped>
+	:deep(.page-main) {
+		background-size: 100% 100% !important;
+	}
+</style>

+ 328 - 0
pages/commission/order.vue

@@ -0,0 +1,328 @@
+<!-- 分销 - 订单明细 -->
+<template>
+  <s-layout title="分销订单" :class="state.scrollTop ? 'order-warp' : ''" navbar="inner">
+    <view
+      class="header-box"
+      :style="[
+        {
+          marginTop: '-' + Number(statusBarHeight + 88) + 'rpx',
+          paddingTop: Number(statusBarHeight + 108) + 'rpx',
+        },
+      ]"
+    >
+      <!-- 团队数据总览 -->
+      <view class="team-data-box ss-flex ss-col-center ss-row-between" style="width: 100%">
+        <view class="data-card" style="width: 100%">
+          <view class="total-item" style="width: 100%">
+            <view class="item-title" style="text-align: center">累计推广订单(单)</view>
+            <view class="total-num" style="text-align: center">
+              {{ state.totals }}
+            </view>
+          </view>
+        </view>
+      </view>
+    </view>
+
+    <!-- tab -->
+    <su-sticky bgColor="#fff">
+      <su-tabs
+        :list="tabMaps"
+        :scrollable="false"
+        :current="state.currentTab"
+        @change="onTabsChange"
+      >
+      </su-tabs>
+    </su-sticky>
+
+    <!-- 订单 -->
+    <view class="order-box">
+      <view class="order-item" v-for="item in state.pagination.list" :key="item">
+        <view class="order-header">
+          <view class="no-box ss-flex ss-col-center ss-row-between">
+            <text class="order-code">订单编号:{{ item.bizId }}</text>
+            <text class="order-state">
+              {{ item.status === 0 ? '待结算' : item.status === 1 ? '已结算' : '已取消' }}
+              ( 佣金 {{ fen2yuan(item.price) }} 元 )
+            </text>
+          </view>
+          <view class="order-from ss-flex ss-col-center ss-row-between">
+            <view class="from-user ss-flex ss-col-center">
+              <text>{{ item.title }}</text>
+            </view>
+            <view class="order-time">
+              {{ sheep.$helper.timeFormat(item.createTime, 'yyyy-mm-dd hh:MM:ss') }}
+            </view>
+          </view>
+        </view>
+      </view>
+      <!-- 数据为空 -->
+      <s-empty v-if="state.pagination.total === 0" icon="/static/order-empty.png" text="暂无订单" />
+      <!-- 加载更多 -->
+      <uni-load-more
+        v-if="state.pagination.total > 0"
+        :status="state.loadStatus"
+        :content-text="{
+          contentdown: '上拉加载更多',
+        }"
+        @tap="loadMore"
+      />
+    </view>
+  </s-layout>
+</template>
+
+<script setup>
+  import sheep from '@/sheep';
+  import { onLoad, onReachBottom } from '@dcloudio/uni-app';
+  import { reactive } from 'vue';
+  import _ from 'lodash-es';
+  import { onPageScroll } from '@dcloudio/uni-app';
+  import { resetPagination } from '@/sheep/util';
+  import BrokerageApi from '@/sheep/api/trade/brokerage';
+  import { fen2yuan } from '../../sheep/hooks/useGoods';
+
+  const statusBarHeight = sheep.$platform.device.statusBarHeight * 2;
+  const headerBg = sheep.$url.css('/static/img/shop/user/withdraw_bg.png');
+
+  onPageScroll((e) => {
+    state.scrollTop = e.scrollTop <= 100;
+  });
+
+  const state = reactive({
+    totals: 0, // 累计推广订单(单)
+    scrollTop: false,
+
+    currentTab: 0,
+    loadStatus: '',
+    pagination: {
+      list: [],
+      total: 0,
+      pageNo: 1,
+      pageSize: 8,
+    },
+  });
+
+  const tabMaps = [
+    {
+      name: '全部',
+      value: 'all',
+    },
+    {
+      name: '待结算',
+      value: '0', // 待结算
+    },
+    {
+      name: '已结算',
+      value: '1', // 已结算
+    },
+  ];
+
+  // 切换选项卡
+  function onTabsChange(e) {
+    resetPagination(state.pagination);
+    state.currentTab = e.index;
+    getOrderList();
+  }
+
+  // 获取订单列表
+  async function getOrderList() {
+    state.loadStatus = 'loading';
+    let { code, data } = await BrokerageApi.getBrokerageRecordPage({
+      pageSize: state.pagination.pageSize,
+      pageNo: state.pagination.pageNo,
+      bizType: 1, // 获得推广佣金
+      status: state.currentTab > 0 ? state.currentTab : undefined,
+    });
+    if (code !== 0) {
+      return;
+    }
+    state.pagination.list = _.concat(state.pagination.list, data.list);
+    state.pagination.total = data.total;
+    state.loadStatus = state.pagination.list.length < state.pagination.total ? 'more' : 'noMore';
+    if (state.currentTab === 0) {
+      state.totals = data.total;
+    }
+  }
+
+  onLoad(() => {
+    getOrderList();
+  });
+
+  // 加载更多
+  function loadMore() {
+    if (state.loadStatus === 'noMore') {
+      return;
+    }
+    state.pagination.pageNo++;
+    getOrderList();
+  }
+
+  // 上拉加载更多
+  onReachBottom(() => {
+    loadMore();
+  });
+</script>
+
+<style lang="scss" scoped>
+  .header-box {
+    box-sizing: border-box;
+    padding: 0 20rpx 20rpx 20rpx;
+    width: 750rpx;
+    background: v-bind(headerBg) no-repeat,
+      linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
+    background-size: 750rpx 100%;
+
+    // 团队信息总览
+    .team-data-box {
+      .data-card {
+        width: 305rpx;
+        background: #ffffff;
+        border-radius: 20rpx;
+        padding: 20rpx;
+
+        .total-item {
+          margin-bottom: 30rpx;
+
+          .item-title {
+            font-size: 24rpx;
+            font-weight: 500;
+            color: #999999;
+            line-height: normal;
+            margin-bottom: 20rpx;
+          }
+
+          .total-num {
+            font-size: 38rpx;
+            font-weight: 500;
+            color: #333333;
+            font-family: OPPOSANS;
+          }
+        }
+
+        .category-num {
+          font-size: 26rpx;
+          font-weight: 500;
+          color: #333333;
+          font-family: OPPOSANS;
+        }
+      }
+    }
+
+    // 直推
+    .direct-box {
+      margin-top: 20rpx;
+
+      .direct-item {
+        width: 340rpx;
+        background: #ffffff;
+        border-radius: 20rpx;
+        padding: 20rpx;
+        box-sizing: border-box;
+
+        .item-title {
+          font-size: 22rpx;
+          font-weight: 500;
+          color: #999999;
+          margin-bottom: 6rpx;
+        }
+
+        .item-value {
+          font-size: 38rpx;
+          font-weight: 500;
+          color: #333333;
+          font-family: OPPOSANS;
+        }
+      }
+    }
+  }
+
+  // 订单
+  .order-box {
+    .order-item {
+      background: #ffffff;
+      border-radius: 10rpx;
+      margin: 20rpx;
+
+      .order-footer {
+        padding: 20rpx;
+        font-size: 24rpx;
+        color: #999;
+      }
+
+      .order-header {
+        .no-box {
+          padding: 20rpx;
+
+          .order-code {
+            font-size: 26rpx;
+            font-weight: 500;
+            color: #333333;
+          }
+
+          .order-state {
+            font-size: 26rpx;
+            font-weight: 500;
+            color: var(--ui-BG-Main);
+          }
+        }
+
+        .order-from {
+          padding: 20rpx;
+
+          .from-user {
+            font-size: 24rpx;
+            font-weight: 400;
+            color: #666666;
+
+            .user-avatar {
+              width: 26rpx;
+              height: 26rpx;
+              border-radius: 50%;
+              margin-right: 8rpx;
+            }
+
+            .user-name {
+              font-size: 24rpx;
+              font-weight: 400;
+              color: #999999;
+            }
+          }
+
+          .order-time {
+            font-size: 24rpx;
+            font-weight: 400;
+            color: #999999;
+          }
+        }
+      }
+
+      .commission-box {
+        .name {
+          font-size: 24rpx;
+          font-weight: 400;
+          color: #999999;
+        }
+      }
+
+      .commission-num {
+        font-size: 30rpx;
+        font-weight: 500;
+        color: $red;
+        font-family: OPPOSANS;
+
+        &::before {
+          content: '¥';
+          font-size: 22rpx;
+        }
+      }
+
+      .order-status {
+        line-height: 30rpx;
+        padding: 0 10rpx;
+        border-radius: 30rpx;
+        margin-left: 20rpx;
+        font-size: 24rpx;
+        color: var(--ui-BG-Main);
+      }
+    }
+  }
+</style>

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 158 - 0
pages/commission/promoter.vue


+ 602 - 0
pages/commission/team.vue

@@ -0,0 +1,602 @@
+<!-- 页面 TODO 芋艿:该页面的实现代码需要优化,包括 js 和 css,以及相关的样式设计 -->
+<template>
+  <s-layout title="我的团队" :class="state.scrollTop ? 'team-wrap' : ''" navbar="inner">
+  <view
+	    class="header-box"
+	    :style="[
+	      {
+	        marginTop: '-' + Number(statusBarHeight + 88) + 'rpx',
+	        paddingTop: Number(statusBarHeight + 108) + 'rpx',
+	      },
+	    ]"
+	  >
+	    <!-- 推广数据总览 -->
+	    <view class="team-data-box ss-flex ss-col-center ss-row-between" style="width: 100%">
+	      <view class="data-card" style="width: 100%">
+	        <view class="total-item" style="width: 100%">
+	          <view class="item-title" style="text-align: center">推广人数</view>
+	          <view class="total-num" style="text-align: center">
+	            {{ state.summary.firstBrokerageUserCount + state.summary.secondBrokerageUserCount || 0 }}
+	          </view>
+	        </view>
+	      </view>
+	    </view>
+	  </view>
+    <view class="promoter-list">
+      <!--<view
+        class="promoterHeader bg-color"
+        style="backgroundcolor: #e93323 !important; height: 218rpx; color: #fff"
+      >
+        <view class="headerCon acea-row row-between" style="padding: 28px 29px 0 29px">
+          <view>
+            <view class="name" style="color: #fff">推广人数</view>
+            <view>
+              <text class="num" style="color: #fff">
+                {{
+                  state.summary.firstBrokerageUserCount + state.summary.secondBrokerageUserCount ||
+                  0
+                }}
+              </text>
+              人
+            </view>
+          </view>
+          <view class="iconfont icon-tuandui" />
+        </view>
+      </view>-->
+      <view style="padding: 0 20rpx">
+        <view class="nav acea-row row-around l1" style="margin-top:20rpx;">
+          <view :class="state.level == 1 ? 'item on' : 'item'" @click="setType(1)">
+            一级({{ state.summary.firstBrokerageUserCount || 0 }})
+          </view>
+          <view :class="state.level == 2 ? 'item on' : 'item'" @click="setType(2)">
+            二级({{ state.summary.secondBrokerageUserCount || 0 }})
+          </view>
+        </view>
+        <view
+          class="search acea-row row-between-wrapper"
+          style="display: flex; height: 100rpx; align-items: center"
+        >
+          <view class="input">
+            <input
+              placeholder="点击搜索会员名称"
+              v-model="state.nickname"
+              confirm-type="search"
+              name="search"
+              @confirm="submitForm"
+            />
+          </view>
+          <image
+            src="/static/images/search.png"
+            mode=""
+            style="width: 60rpx; height: 64rpx"
+            @click="submitForm"
+          />
+        </view>
+        <view class="list">
+          <view class="sortNav acea-row row-middle" style="display: flex; align-items: center">
+            <view
+              class="sortItem"
+              @click="setSort('userCount', 'asc')"
+              v-if="sort === 'userCountDESC'"
+            >
+              团队排序
+              <!-- TODO 芋艿:看看怎么从项目里拿出去 -->
+              <image src="/static/images/sort1.png" />
+            </view>
+            <view
+              class="sortItem"
+              @click="setSort('userCount', 'desc')"
+              v-else-if="sort === 'userCountASC'"
+            >
+              团队排序
+              <image src="/static/images/sort3.png" />
+            </view>
+            <view class="sortItem" @click="setSort('userCount', 'desc')" v-else>
+              团队排序
+              <image src="/static/images/sort2.png" />
+            </view>
+            <view class="sortItem" @click="setSort('price', 'asc')" v-if="sort === 'priceDESC'">
+              金额排序
+              <image src="/static/images/sort1.png" />
+            </view>
+            <view
+              class="sortItem"
+              @click="setSort('price', 'desc')"
+              v-else-if="sort === 'priceASC'"
+            >
+              金额排序
+              <image src="/static/images/sort3.png" />
+            </view>
+            <view class="sortItem" @click="setSort('price', 'desc')" v-else>
+              金额排序
+              <image src="/static/images/sort2.png" />
+            </view>
+            <view
+              class="sortItem"
+              @click="setSort('orderCount', 'asc')"
+              v-if="sort === 'orderCountDESC'"
+            >
+              订单排序
+              <image src="/static/images/sort1.png" />
+            </view>
+            <view
+              class="sortItem"
+              @click="setSort('orderCount', 'desc')"
+              v-else-if="sort === 'orderCountASC'"
+            >
+              订单排序
+              <image src="/static/images/sort3.png" />
+            </view>
+            <view class="sortItem" @click="setSort('orderCount', 'desc')" v-else>
+              订单排序
+              <image src="/static/images/sort2.png" />
+            </view>
+          </view>
+          <block v-for="(item, index) in state.pagination.list" :key="index">
+            <view class="item acea-row row-between-wrapper" style="display: flex">
+              <view
+                class="picTxt acea-row row-between-wrapper"
+                style="display: flex; align-items: center"
+              >
+                <view class="pictrue">
+                  <image :src="item.avatar" />
+                </view>
+                <view class="text">
+                  <view class="name line1">{{ item.nickname }}</view>
+                  <view>
+                    加入时间:
+                    {{ sheep.$helper.timeFormat(item.brokerageTime, 'yyyy-mm-dd hh:MM:ss') }}
+                  </view>
+                </view>
+              </view>
+              <view
+                class="right"
+                style="
+                  justify-content: center;
+                  flex-direction: column;
+                  display: flex;
+                  margin-left: auto;
+                "
+              >
+                <view>
+                  <text class="num font-color">{{ item.brokerageUserCount || 0 }} </text>人
+                </view>
+                <view>
+                  <text class="num">{{ item.orderCount || 0 }}</text
+                  >单</view
+                >
+                <view>
+                  <text class="num">{{ item.brokeragePrice || 0 }}</text
+                  >元
+                </view>
+              </view>
+            </view>
+          </block>
+          <block v-if="state.pagination.list.length === 0">
+            <view style="text-align: center;margin-top:30rpx;">暂无推广人数</view>
+          </block>
+        </view>
+      </view>
+    </view>
+    <!-- <home></home> -->
+
+    <!-- 		<view class="header-box" :style="[
+        {
+          marginTop: '-' + Number(statusBarHeight + 88) + 'rpx',
+          paddingTop: Number(statusBarHeight + 108) + 'rpx',
+        },
+      ]">
+			<view v-if="userInfo.parent_user" class="referrer-box ss-flex ss-col-center">
+				推荐人:
+				<image class="referrer-avatar ss-m-r-10" :src="sheep.$url.cdn(userInfo.parent_user.avatar)"
+					mode="aspectFill">
+				</image>
+				{{ userInfo.parent_user.nickname }}
+			</view>
+			<view class="team-data-box ss-flex ss-col-center ss-row-between">
+				<view class="data-card">
+					<view class="total-item">
+						<view class="item-title">团队总人数(人)</view>
+						<view class="total-num">
+							{{ (state.summary.firstBrokerageUserCount+ state.summary.secondBrokerageUserCount)|| 0 }}
+						</view>
+					</view>
+					<view class="category-item ss-flex">
+						<view class="ss-flex-1">
+							<view class="item-title">一级成员</view>
+							<view class="category-num">{{ state.summary.firstBrokerageUserCount || 0 }}</view>
+						</view>
+						<view class="ss-flex-1">
+							<view class="item-title">二级成员</view>
+							<view class="category-num">{{ state.summary.secondBrokerageUserCount || 0 }}</view>
+						</view>
+					</view>
+				</view>
+				<view class="data-card">
+					<view class="total-item">
+						<view class="item-title">团队分销商人数(人)</view>
+						<view class="total-num">{{ agentInfo?.child_agent_count_all || 0 }}</view>
+					</view>
+					<view class="category-item ss-flex">
+						<view class="ss-flex-1">
+							<view class="item-title">一级分销商</view>
+							<view class="category-num">{{ agentInfo?.child_agent_count_1 || 0 }}</view>
+						</view>
+						<view class="ss-flex-1">
+							<view class="item-title">二级分销商</view>
+							<view class="category-num">{{ agentInfo?.child_agent_count_2 || 0 }}</view>
+						</view>
+					</view>
+				</view>
+			</view>
+		</view>
+		<view class="list-box">
+			<uni-list :border="false">
+				<uni-list-chat v-for="item in state.pagination.data" :key="item.id" :avatar-circle="true"
+					:title="item.nickname" :avatar="sheep.$url.cdn(item.avatar)"
+					:note="filterUserNum(item.agent?.child_user_count_1)">
+					<view class="chat-custom-right">
+						<view v-if="item.avatar" class="tag-box ss-flex ss-col-center">
+							<image class="tag-img" :src="sheep.$url.cdn(item.avatar)" mode="aspectFill">
+							</image>
+							<text class="tag-title">{{ item.nickname }}</text>
+						</view>
+						<text
+							class="time-text">{{ sheep.$helper.timeFormat(item.brokerageTime, 'yyyy-mm-dd hh:MM:ss') }}</text>
+					</view>
+				</uni-list-chat>
+			</uni-list>
+		</view>
+		<s-empty v-if="state.pagination.total === 0" icon="/static/data-empty.png" text="暂无团队信息">
+		</s-empty> -->
+  </s-layout>
+</template>
+
+<script setup>
+  import sheep from '@/sheep';
+  import { onLoad, onReachBottom } from '@dcloudio/uni-app';
+  import { computed, reactive, ref } from 'vue';
+  import _ from 'lodash-es';
+  import { onPageScroll } from '@dcloudio/uni-app';
+  import BrokerageApi from '@/sheep/api/trade/brokerage';
+
+  const statusBarHeight = sheep.$platform.device.statusBarHeight * 2;
+  // const agentInfo = computed(() => sheep.$store('user').agentInfo);
+  const userInfo = computed(() => sheep.$store('user').userInfo);
+  const headerBg = sheep.$url.css('/static/img/shop/user/withdraw_bg.png');
+
+  onPageScroll((e) => {
+    state.scrollTop = e.scrollTop <= 100;
+  });
+
+  let sort = ref();
+  const state = reactive({
+    summary: {},
+    pagination: {
+      pageNo: 1,
+      pageSize: 8,
+      list: [],
+      total: 0,
+    },
+    loadStatus: '',
+    // ↓ 新 ui 逻辑
+    level: 1,
+    nickname: ref(''),
+    sortKey: '',
+    isAsc: '',
+  });
+
+  function filterUserNum(num) {
+    if (_.isNil(num)) {
+      return '';
+    }
+    return `下级团队${num}人`;
+  }
+
+  function submitForm() {
+    state.pagination.list = [];
+    getTeamList();
+  }
+
+  async function getTeamList() {
+    state.loadStatus = 'loading';
+    let { code, data } = await BrokerageApi.getBrokerageUserChildSummaryPage({
+      pageNo: state.pagination.pageNo,
+      pageSize: state.pagination.pageSize,
+      level: state.level,
+      'sortingField.order': state.isAsc,
+      'sortingField.field': state.sortKey,
+      nickname: state.nickname,
+    });
+    if (code !== 0) {
+      return;
+    }
+    state.pagination.list = _.concat(state.pagination.list, data.list);
+    state.pagination.total = data.total;
+    state.loadStatus = state.pagination.list.length < state.pagination.total ? 'more' : 'noMore';
+  }
+
+  function setType(e) {
+    state.pagination.list = [];
+    state.level = e + '';
+    getTeamList();
+  }
+
+  function setSort(sortKey, isAsc) {
+    state.pagination.list = [];
+    sort = sortKey + isAsc.toUpperCase();
+    state.isAsc = isAsc;
+    state.sortKey = sortKey;
+    getTeamList();
+  }
+
+  onLoad(async () => {
+    await getTeamList();
+    // 概要数据
+    let { data } = await BrokerageApi.getBrokerageUserSummary();
+    state.summary = data;
+  });
+
+  // 加载更多
+  function loadMore() {
+    if (state.loadStatus === 'noMore') {
+      return;
+    }
+    state.pagination.pageNo++;
+    getTeamList();
+  }
+
+  // 上拉加载更多
+  onReachBottom(() => {
+    loadMore();
+  });
+</script>
+
+<style lang="scss" scoped>
+  .l1 {
+    background-color: #fff;
+    height: 86rpx;
+    line-height: 86rpx;
+    font-size: 28rpx;
+    color: #282828;
+    border-bottom: 1rpx solid #eee;
+    border-top-left-radius: 14rpx;
+    border-top-right-radius: 14rpx;
+    display: flex;
+    justify-content: space-around;
+  }
+
+  .header-box {
+    box-sizing: border-box;
+    padding: 0 20rpx 20rpx 20rpx;
+    width: 750rpx;
+    z-index: 3;
+    position: relative;
+    background: v-bind(headerBg) no-repeat,
+      linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
+    background-size: 750rpx 100%;
+
+    // 团队信息总览
+    .team-data-box {
+      .data-card {
+        width: 305rpx;
+        background: #ffffff;
+        border-radius: 20rpx;
+        padding: 20rpx;
+
+        .item-title {
+          font-size: 22rpx;
+          font-weight: 500;
+          color: #999999;
+          line-height: 30rpx;
+          margin-bottom: 10rpx;
+        }
+
+        .total-item {
+          margin-bottom: 30rpx;
+        }
+
+        .total-num {
+          font-size: 38rpx;
+          font-weight: 500;
+          color: #333333;
+          font-family: OPPOSANS;
+        }
+
+        .category-num {
+          font-size: 26rpx;
+          font-weight: 500;
+          color: #333333;
+          font-family: OPPOSANS;
+        }
+      }
+    }
+  }
+
+  .list-box {
+    z-index: 3;
+    position: relative;
+  }
+
+  .chat-custom-right {
+    .time-text {
+      font-size: 22rpx;
+      font-weight: 400;
+      color: #999999;
+    }
+
+    .tag-box {
+      background: rgba(0, 0, 0, 0.2);
+      border-radius: 21rpx;
+      line-height: 30rpx;
+      padding: 5rpx 10rpx;
+      width: 140rpx;
+
+      .tag-img {
+        width: 34rpx;
+        height: 34rpx;
+        margin-right: 6rpx;
+        border-radius: 50%;
+      }
+
+      .tag-title {
+        font-size: 18rpx;
+        font-weight: 500;
+        color: rgba(255, 255, 255, 1);
+        line-height: 20rpx;
+      }
+    }
+  }
+
+  // 推荐人
+  .referrer-box {
+    font-size: 28rpx;
+    font-weight: 500;
+    color: #ffffff;
+    padding: 20rpx;
+
+    .referrer-avatar {
+      width: 34rpx;
+      height: 34rpx;
+      border-radius: 50%;
+    }
+  }
+
+  .promoter-list .nav {
+    background-color: #fff;
+    height: 86rpx;
+    line-height: 86rpx;
+    font-size: 28rpx;
+    color: #282828;
+    border-bottom: 1rpx solid #eee;
+    border-top-left-radius: 14rpx;
+    border-top-right-radius: 14rpx;
+    margin-top: -30rpx;
+  }
+
+  .promoter-list .nav .item.on {
+    border-bottom: 5rpx solid;
+    // $theme-color
+    color: var(--ui-BG-Main);
+    // $theme-color
+  }
+
+  .promoter-list .search {
+    width: 100%;
+    background-color: #fff;
+    height: 100rpx;
+    padding: 0 24rpx;
+    box-sizing: border-box;
+    border-bottom-left-radius: 14rpx;
+    border-bottom-right-radius: 14rpx;
+  }
+
+  .promoter-list .search .input {
+    width: 592rpx;
+    height: 60rpx;
+    border-radius: 50rpx;
+    background-color: #f5f5f5;
+    text-align: center;
+    position: relative;
+  }
+
+  .promoter-list .search .input input {
+    height: 100%;
+    font-size: 26rpx;
+    width: 610rpx;
+    text-align: center;
+  }
+
+  .promoter-list .search .input .placeholder {
+    color: #bbb;
+  }
+
+  .promoter-list .search .input .iconfont {
+    position: absolute;
+    right: 28rpx;
+    color: #999;
+    font-size: 28rpx;
+    top: 50%;
+    transform: translateY(-50%);
+  }
+
+  .promoter-list .search .iconfont {
+    font-size: 32rpx;
+    color: #515151;
+    height: 60rpx;
+    line-height: 60rpx;
+  }
+
+  .promoter-list .list {
+    margin-top: 20rpx;
+  }
+
+  .promoter-list .list .sortNav {
+    background-color: #fff;
+    height: 76rpx;
+    border-bottom: 1rpx solid #eee;
+    color: #333;
+    font-size: 28rpx;
+    border-top-left-radius: 14rpx;
+    border-top-right-radius: 14rpx;
+  }
+
+  .promoter-list .list .sortNav .sortItem {
+    text-align: center;
+    flex: 1;
+  }
+
+  .promoter-list .list .sortNav .sortItem image {
+    width: 24rpx;
+    height: 24rpx;
+    margin-left: 6rpx;
+    vertical-align: -3rpx;
+  }
+
+  .promoter-list .list .item {
+    background-color: #fff;
+    border-bottom: 1rpx solid #eee;
+    height: 152rpx;
+    padding: 0 24rpx;
+    font-size: 24rpx;
+    color: #666;
+  }
+
+  .promoter-list .list .item .picTxt .pictrue {
+    width: 106rpx;
+    height: 106rpx;
+    border-radius: 50%;
+  }
+
+  .promoter-list .list .item .picTxt .pictrue image {
+    width: 100%;
+    height: 100%;
+    border-radius: 50%;
+    border: 3rpx solid #fff;
+    box-shadow: 0 0 10rpx #aaa;
+    box-sizing: border-box;
+  }
+
+  .promoter-list .list .item .picTxt .text {
+    // width: 304rpx;
+    font-size: 24rpx;
+    color: #666;
+    margin-left: 14rpx;
+  }
+
+  .promoter-list .list .item .picTxt .text .name {
+    font-size: 28rpx;
+    color: #333;
+    margin-bottom: 13rpx;
+  }
+
+  .promoter-list .list .item .right {
+    text-align: right;
+    font-size: 22rpx;
+    color: #333;
+  }
+
+  .promoter-list .list .item .right .num {
+    margin-right: 7rpx;
+  }
+</style>

+ 518 - 0
pages/commission/wallet.vue

@@ -0,0 +1,518 @@
+<!-- 分销 - 佣金明细 -->
+<template>
+  <s-layout class="wallet-wrap" title="佣金">
+    <!-- 钱包卡片 -->
+    <view class="header-box ss-flex ss-row-center ss-col-center">
+      <view class="card-box ui-BG-Main ui-Shadow-Main">
+        <view class="card-head ss-flex ss-col-center">
+          <view class="card-title ss-m-r-10">当前佣金(元)</view>
+          <view
+            @tap="state.showMoney = !state.showMoney"
+            class="ss-eye-icon"
+            :class="state.showMoney ? 'cicon-eye' : 'cicon-eye-off'"
+          />
+        </view>
+        <view class="ss-flex ss-row-between ss-col-center ss-m-t-30">
+          <view class="money-num">{{
+            state.showMoney ? fen2yuan(state.summary.withdrawPrice || 0) : '*****'
+          }}</view>
+          <view class="ss-flex">
+            <view class="ss-m-r-20">
+              <button
+                class="ss-reset-button withdraw-btn"
+                @tap="sheep.$router.go('/pages/commission/withdraw')"
+              >
+                提现
+              </button>
+            </view>
+            <button class="ss-reset-button balance-btn ss-m-l-20" @tap="state.showModal = true">
+              转余额
+            </button>
+          </view>
+        </view>
+
+        <view class="ss-flex">
+          <view class="loading-money">
+            <view class="loading-money-title">冻结佣金</view>
+            <view class="loading-money-num">
+              {{ state.showMoney ? fen2yuan(state.summary.frozenPrice || 0) : '*****' }}
+            </view>
+          </view>
+          <view class="loading-money ss-m-l-100">
+            <view class="loading-money-title">可提现佣金</view>
+            <view class="loading-money-num">
+              {{ state.showMoney ? fen2yuan(state.summary.brokeragePrice || 0) : '*****' }}
+            </view>
+          </view>
+        </view>
+      </view>
+    </view>
+
+    <su-sticky>
+      <!-- 统计 -->
+      <view class="filter-box ss-p-x-30 ss-flex ss-col-center ss-row-between">
+        <uni-datetime-picker
+          v-model="state.date"
+          type="daterange"
+          @change="onChangeTime"
+          :end="state.today"
+        >
+          <button class="ss-reset-button date-btn">
+            <text>{{ dateFilterText }}</text>
+            <text class="cicon-drop-down ss-seldate-icon" />
+          </button>
+        </uni-datetime-picker>
+
+        <view class="total-box">
+          <!-- TODO 芋艿:这里暂时不考虑做 -->
+          <!-- <view class="ss-m-b-10">总收入¥{{ state.pagination.income.toFixed(2) }}</view> -->
+          <!-- <view>总支出¥{{ (-state.pagination.expense).toFixed(2) }}</view> -->
+        </view>
+      </view>
+      <su-tabs
+        :list="tabMaps"
+        @change="onChangeTab"
+        :scrollable="false"
+        :current="state.currentTab"
+      />
+    </su-sticky>
+    <s-empty
+      v-if="state.pagination.total === 0"
+      icon="/static/data-empty.png"
+      text="暂无数据"
+    ></s-empty>
+
+    <!-- 转余额弹框 -->
+    <su-popup
+      :show="state.showModal"
+      type="bottom"
+      round="20"
+      @close="state.showModal = false"
+      showClose
+    >
+      <view class="ss-p-x-20 ss-p-y-30">
+        <view class="model-title ss-m-b-30 ss-m-l-20">转余额</view>
+        <view class="model-subtitle ss-m-b-100 ss-m-l-20">将您的佣金转到余额中继续消费</view>
+        <view class="input-box ss-flex ss-col-center border-bottom ss-m-b-70 ss-m-x-20">
+          <view class="unit">¥</view>
+          <uni-easyinput
+            :inputBorder="false"
+            class="ss-flex-1 ss-p-l-10"
+            v-model="state.price"
+            type="number"
+            placeholder="请输入金额"
+          />
+        </view>
+        <button
+          class="ss-reset-button model-btn ui-BG-Main-Gradient ui-Shadow-Main"
+          @tap="onConfirm"
+        >
+          确定
+        </button>
+      </view>
+    </su-popup>
+
+    <!-- 钱包记录 -->
+    <view v-if="state.pagination.total > 0">
+      <view
+        class="wallet-list ss-flex border-bottom"
+        v-for="item in state.pagination.list"
+        :key="item.id"
+      >
+        <view class="list-content">
+          <view class="title-box ss-flex ss-row-between ss-m-b-20">
+            <text class="title ss-line-1">{{ item.title }}</text>
+            <view class="money">
+              <text v-if="item.price >= 0" class="add">+{{ fen2yuan(item.price) }}</text>
+              <text v-else class="minus">{{ fen2yuan(item.price) }}</text>
+            </view>
+          </view>
+          <text class="time">{{
+            sheep.$helper.timeFormat(item.createTime, 'yyyy-mm-dd hh:MM:ss')
+          }}</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- <u-gap></u-gap> -->
+    <uni-load-more
+      v-if="state.pagination.total > 0"
+      :status="state.loadStatus"
+      :content-text="{
+        contentdown: '上拉加载更多',
+      }"
+    />
+  </s-layout>
+</template>
+
+<script setup>
+  import { computed, reactive } from 'vue';
+  import { onLoad, onReachBottom } from '@dcloudio/uni-app';
+  import sheep from '@/sheep';
+  import dayjs from 'dayjs';
+  import _ from 'lodash-es';
+  import BrokerageApi from '@/sheep/api/trade/brokerage';
+  import { fen2yuan } from '@/sheep/hooks/useGoods';
+  import { resetPagination } from '@/sheep/util';
+
+  const headerBg = sheep.$url.css('/static/img/shop/user/wallet_card_bg.png');
+
+  const state = reactive({
+    showMoney: false,
+    summary: {}, // 分销信息
+
+    today: '',
+    date: [],
+    currentTab: 0,
+    pagination: {
+      list: [],
+      total: 0,
+      pageNo: 1,
+      pageSize: 8,
+    },
+    loadStatus: '',
+
+    price: undefined,
+    showModal: false,
+  });
+
+  const tabMaps = [
+    {
+      name: '分佣',
+      value: '1', // BrokerageRecordBizTypeEnum.ORDER
+    },
+    {
+      name: '提现',
+      value: '2', // BrokerageRecordBizTypeEnum.WITHDRAW
+    },
+  ];
+
+  const dateFilterText = computed(() => {
+    if (state.date[0] === state.date[1]) {
+      return state.date[0];
+    } else {
+      return state.date.join('~');
+    }
+  });
+
+  async function getLogList() {
+    state.loadStatus = 'loading';
+    let { code, data } = await BrokerageApi.getBrokerageRecordPage({
+      pageSize: state.pagination.pageSize,
+      pageNo: state.pagination.pageNo,
+      bizType: tabMaps[state.currentTab].value,
+      'createTime[0]': state.date[0] + ' 00:00:00',
+      'createTime[1]': state.date[1] + ' 23:59:59',
+    });
+    if (code !== 0) {
+      return;
+    }
+    state.pagination.list = _.concat(state.pagination.list, data.list);
+    state.pagination.total = data.total;
+    state.loadStatus = state.pagination.list.length < state.pagination.total ? 'more' : 'noMore';
+  }
+
+  function onChangeTab(e) {
+    resetPagination(state.pagination);
+    state.currentTab = e.index;
+    getLogList();
+  }
+
+  function onChangeTime(e) {
+    state.date[0] = e[0];
+    state.date[1] = e[e.length - 1];
+    resetPagination(state.pagination);
+    getLogList();
+  }
+
+  // 确认操作(转账到余额)
+  async function onConfirm() {
+    if (state.price <= 0) {
+      sheep.$helper.toast('请输入正确的金额');
+      return;
+    }
+    uni.showModal({
+      title: '提示',
+      content: '确认把您的佣金转入到余额钱包中?',
+      success: async function (res) {
+        if (!res.confirm) {
+          return;
+        }
+        const { code } = await BrokerageApi.createBrokerageWithdraw({
+          type: 1, // 钱包
+          price: state.price * 100,
+        });
+        if (code === 0) {
+          state.showModal = false;
+          await getAgentInfo();
+          onChangeTab({
+            index: 1,
+          });
+        }
+      },
+    });
+  }
+
+  async function getAgentInfo() {
+    const { code, data } = await BrokerageApi.getBrokerageUserSummary();
+    if (code !== 0) {
+      return;
+    }
+    state.summary = data;
+  }
+
+  onLoad(async (options) => {
+    state.today = dayjs().format('YYYY-MM-DD');
+    state.date = [state.today, state.today];
+    if (options.type === 2) {
+      // 切换到“提现” tab 下
+      state.currentTab = 1;
+    }
+    getLogList();
+    getAgentInfo();
+  });
+
+  onReachBottom(() => {
+    if (state.loadStatus === 'noMore') {
+      return;
+    }
+    state.pagination.pageNo++;
+    getLogList();
+  });
+</script>
+
+<style lang="scss" scoped>
+  // 钱包
+  .header-box {
+    background-color: $white;
+    padding: 30rpx;
+
+    .card-box {
+      width: 100%;
+      min-height: 300rpx;
+      padding: 40rpx;
+      background-size: 100% 100%;
+      border-radius: 30rpx;
+      overflow: hidden;
+      position: relative;
+      z-index: 1;
+      box-sizing: border-box;
+
+      &::after {
+        content: '';
+        display: block;
+        width: 100%;
+        height: 100%;
+        z-index: 2;
+        position: absolute;
+        top: 0;
+        left: 0;
+        background: v-bind(headerBg) no-repeat;
+        pointer-events: none;
+      }
+
+      .card-head {
+        color: $white;
+        font-size: 24rpx;
+      }
+
+      .ss-eye-icon {
+        font-size: 40rpx;
+        color: $white;
+      }
+
+      .money-num {
+        font-size: 40rpx;
+        line-height: normal;
+        font-weight: 500;
+        color: $white;
+        font-family: OPPOSANS;
+      }
+
+      .reduce-num {
+        font-size: 26rpx;
+        font-weight: 400;
+        color: $white;
+      }
+
+      .withdraw-btn {
+        width: 120rpx;
+        height: 60rpx;
+        line-height: 60rpx;
+        border-radius: 30px;
+        font-size: 24rpx;
+        font-weight: 500;
+        background-color: $white;
+        color: var(--ui-BG-Main);
+      }
+
+      .balance-btn {
+        width: 120rpx;
+        height: 60rpx;
+        line-height: 60rpx;
+        border-radius: 30px;
+        font-size: 24rpx;
+        font-weight: 500;
+        color: $white;
+        border: 1px solid $white;
+      }
+    }
+  }
+
+  .loading-money {
+    margin-top: 56rpx;
+
+    .loading-money-title {
+      font-size: 24rpx;
+      font-weight: 400;
+      color: #ffffff;
+      line-height: normal;
+      margin-bottom: 30rpx;
+    }
+
+    .loading-money-num {
+      font-size: 30rpx;
+      font-family: OPPOSANS;
+      font-weight: 500;
+      color: #fefefe;
+    }
+  }
+
+  // 筛选
+
+  .filter-box {
+    height: 120rpx;
+    padding: 0 30rpx;
+    background-color: $bg-page;
+
+    .total-box {
+      font-size: 24rpx;
+      font-weight: 500;
+      color: $dark-9;
+    }
+
+    .date-btn {
+      background-color: $white;
+      line-height: 54rpx;
+      border-radius: 27rpx;
+      padding: 0 20rpx;
+      font-size: 24rpx;
+      font-weight: 500;
+      color: $dark-6;
+
+      .ss-seldate-icon {
+        font-size: 50rpx;
+        color: $dark-9;
+      }
+    }
+  }
+
+  // tab
+  .wallet-tab-card {
+    .tab-item {
+      height: 80rpx;
+      position: relative;
+
+      .tab-title {
+        font-size: 30rpx;
+      }
+
+      .cur-tab-title {
+        font-weight: $font-weight-bold;
+      }
+
+      .tab-line {
+        width: 60rpx;
+        height: 6rpx;
+        border-radius: 6rpx;
+        position: absolute;
+        left: 50%;
+        transform: translateX(-50%);
+        bottom: 2rpx;
+        background-color: var(--ui-BG-Main);
+      }
+    }
+  }
+
+  // 钱包记录
+  .wallet-list {
+    padding: 30rpx;
+    background-color: #ffff;
+
+    .head-img {
+      width: 70rpx;
+      height: 70rpx;
+      border-radius: 50%;
+      background: $gray-c;
+    }
+
+    .list-content {
+      justify-content: space-between;
+      align-items: flex-start;
+      flex: 1;
+
+      .title {
+        font-size: 28rpx;
+        color: $dark-3;
+        width: 400rpx;
+      }
+
+      .time {
+        color: $gray-c;
+        font-size: 22rpx;
+      }
+    }
+
+    .money {
+      font-size: 28rpx;
+      font-weight: bold;
+      font-family: OPPOSANS;
+
+      .add {
+        color: var(--ui-BG-Main);
+      }
+
+      .minus {
+        color: $dark-3;
+      }
+    }
+  }
+
+  .model-title {
+    font-size: 36rpx;
+    font-weight: bold;
+    color: #333333;
+  }
+
+  .model-subtitle {
+    font-size: 26rpx;
+    color: #c2c7cf;
+  }
+
+  .model-btn {
+    width: 100%;
+    height: 80rpx;
+    border-radius: 40rpx;
+    font-size: 28rpx;
+    font-weight: 500;
+    color: #ffffff;
+    line-height: normal;
+  }
+
+  .input-box {
+    height: 100rpx;
+
+    .unit {
+      font-size: 48rpx;
+      color: #333;
+      font-weight: 500;
+      line-height: normal;
+    }
+
+    .uni-easyinput__placeholder-class {
+      font-size: 30rpx;
+      height: 40rpx;
+      line-height: normal;
+    }
+  }
+</style>

+ 463 - 0
pages/commission/withdraw.vue

@@ -0,0 +1,463 @@
+<!-- 分佣提现 -->
+<template>
+  <s-layout title="申请提现" class="withdraw-wrap" navbar="inner">
+    <view class="page-bg"></view>
+    <view
+      class="wallet-num-box ss-flex ss-col-center ss-row-between"
+      :style="[
+        {
+          marginTop: '-' + Number(statusBarHeight + 88) + 'rpx',
+          paddingTop: Number(statusBarHeight + 108) + 'rpx',
+        },
+      ]"
+    >
+      <view class="">
+        <view class="num-title">可提现金额(元)</view>
+        <view class="wallet-num">{{ fen2yuan(state.brokerageInfo.brokeragePrice) }}</view>
+      </view>
+      <button
+        class="ss-reset-button log-btn"
+        @tap="sheep.$router.go('/pages/commission/wallet', { type: 2 })"
+      >
+        提现记录
+      </button>
+    </view>
+    <!-- 提现输入卡片-->
+    <view class="draw-card">
+      <view class="bank-box ss-flex ss-col-center ss-row-between ss-m-b-30">
+        <view class="name">提现至</view>
+        <view class="bank-list ss-flex ss-col-center" @tap="onAccountSelect(true)">
+          <view v-if="!state.accountInfo.type" class="empty-text">请选择提现方式</view>
+          <view v-if="state.accountInfo.type === '1'" class="empty-text">钱包余额</view>
+          <view v-if="state.accountInfo.type === '2'" class="empty-text">银行卡转账</view>
+          <view v-if="state.accountInfo.type === '3'" class="empty-text">微信零钱</view>
+          <view v-if="state.accountInfo.type === '4'" class="empty-text">支付宝账户</view>
+          <text class="cicon-forward" />
+        </view>
+      </view>
+      <!-- 提现金额 -->
+      <view class="card-title">提现金额</view>
+      <view class="input-box ss-flex ss-col-center border-bottom">
+        <view class="unit">¥</view>
+        <uni-easyinput
+          :inputBorder="false"
+          class="ss-flex-1 ss-p-l-10"
+          v-model="state.accountInfo.price"
+          type="number"
+          placeholder="请输入提现金额"
+        />
+      </view>
+      <!-- 提现账号 -->
+      <view class="card-title" v-show="['2', '3', '4'].includes(state.accountInfo.type)">
+        提现账号
+      </view>
+      <view
+        class="input-box ss-flex ss-col-center border-bottom"
+        v-show="['2', '3', '4'].includes(state.accountInfo.type)"
+      >
+        <view class="unit" />
+        <uni-easyinput
+          :inputBorder="false"
+          class="ss-flex-1 ss-p-l-10"
+          v-model="state.accountInfo.accountNo"
+          placeholder="请输入提现账号"
+        />
+      </view>
+      <!-- 收款码 -->
+      <view class="card-title" v-show="['3', '4'].includes(state.accountInfo.type)">收款码</view>
+      <view
+        class="input-box ss-flex ss-col-center"
+        v-show="['3', '4'].includes(state.accountInfo.type)"
+      >
+        <view class="unit" />
+        <view class="upload-img">
+          <s-uploader
+            v-model:url="state.accountInfo.accountQrCodeUrl"
+            fileMediatype="image"
+            limit="1"
+            mode="grid"
+            :imageStyles="{ width: '168rpx', height: '168rpx' }"
+          />
+        </view>
+      </view>
+      <!-- 持卡人姓名 -->
+      <view class="card-title" v-show="state.accountInfo.type === '2'">持卡人</view>
+      <view
+        class="input-box ss-flex ss-col-center border-bottom"
+        v-show="state.accountInfo.type === '2'"
+      >
+        <view class="unit" />
+        <uni-easyinput
+          :inputBorder="false"
+          class="ss-flex-1 ss-p-l-10"
+          v-model="state.accountInfo.name"
+          placeholder="请输入持卡人姓名"
+        />
+      </view>
+      <!-- 提现银行 -->
+      <view class="card-title" v-show="state.accountInfo.type === '2'">提现银行</view>
+      <view
+        class="input-box ss-flex ss-col-center border-bottom"
+        v-show="state.accountInfo.type === '2'"
+      >
+        <view class="unit" />
+        <!--银行改为下拉选择-->
+        <picker
+          @change="bankChange"
+          :value="state.bankListSelectedIndex"
+          :range="state.bankList"
+          range-key="label"
+          style="width: 100%"
+        >
+          <uni-easyinput
+            :inputBorder="false"
+            :value="state.accountInfo.bankName"
+            placeholder="请选择银行"
+            suffixIcon="right"
+            disabled
+            :styles="{ disableColor: '#fff', borderColor: '#fff', color: '#333!important' }"
+          />
+        </picker>
+      </view>
+      <!-- 开户地址 -->
+      <view class="card-title" v-show="state.accountInfo.type === '2'">开户地址</view>
+      <view
+        class="input-box ss-flex ss-col-center border-bottom"
+        v-show="state.accountInfo.type === '2'"
+      >
+        <view class="unit" />
+        <uni-easyinput
+          :inputBorder="false"
+          class="ss-flex-1 ss-p-l-10"
+          v-model="state.accountInfo.bankAddress"
+          placeholder="请输入开户地址"
+        />
+      </view>
+      <button class="ss-reset-button save-btn ui-BG-Main-Gradient ui-Shadow-Main" @tap="onConfirm">
+        确认提现
+      </button>
+    </view>
+
+    <!-- 提现说明 -->
+    <view class="draw-notice">
+      <view class="title ss-m-b-30">提现说明</view>
+      <view class="draw-list"> 最低提现金额 {{ fen2yuan(state.minPrice) }} 元 </view>
+      <view class="draw-list">
+        冻结佣金:<text>¥{{ fen2yuan(state.brokerageInfo.frozenPrice) }}</text>
+        (每笔佣金的冻结期为 {{ state.frozenDays }} 天,到期后可提现)
+      </view>
+    </view>
+
+    <!-- 选择提现账户 -->
+    <account-type-select
+      :show="state.accountSelect"
+      @close="onAccountSelect(false)"
+      round="10"
+      v-model="state.accountInfo"
+      :methods="state.withdrawTypes"
+    />
+  </s-layout>
+</template>
+
+<script setup>
+  import { computed, reactive, onBeforeMount } from 'vue';
+  import sheep from '@/sheep';
+  import accountTypeSelect from './components/account-type-select.vue';
+  import { fen2yuan } from '@/sheep/hooks/useGoods';
+  import TradeConfigApi from '@/sheep/api/trade/config';
+  import BrokerageApi from '@/sheep/api/trade/brokerage';
+  import DictApi from '@/sheep/api/system/dict';
+
+  const headerBg = sheep.$url.css('/static/img/shop/user/withdraw_bg.png');
+  const statusBarHeight = sheep.$platform.device.statusBarHeight * 2;
+
+  const userStore = sheep.$store('user');
+  const userInfo = computed(() => userStore.userInfo);
+  const state = reactive({
+    accountInfo: {
+      // 提现表单
+      type: undefined,
+      accountNo: undefined,
+      accountQrCodeUrl: undefined,
+      name: undefined,
+      bankName: undefined,
+      bankAddress: undefined,
+    },
+
+    accountSelect: false,
+
+    brokerageInfo: {}, // 分销信息
+
+    frozenDays: 0, // 冻结天数
+    minPrice: 0, // 最低提现金额
+    withdrawTypes: [], // 提现方式
+    bankList: [], // 银行字典数据
+    bankListSelectedIndex: '', // 选中银行 bankList 的 index
+  });
+
+  // 打开提现方式的弹窗
+  const onAccountSelect = (e) => {
+    state.accountSelect = e;
+  };
+
+  // 提交提现
+  const onConfirm = async () => {
+    // 参数校验
+    //debugger;
+    if (
+      !state.accountInfo.price ||
+      state.accountInfo.price > state.brokerageInfo.price ||
+      state.accountInfo.price <= 0
+    ) {
+      sheep.$helper.toast('请输入正确的提现金额');
+      return;
+    }
+    if (!state.accountInfo.type) {
+      sheep.$helper.toast('请选择提现方式');
+      return;
+    }
+    // 提交请求
+    let { code } = await BrokerageApi.createBrokerageWithdraw({
+      ...state.accountInfo,
+      price: state.accountInfo.price * 100,
+    });
+    if (code !== 0) {
+      return;
+    }
+    // 提示
+    uni.showModal({
+      title: '操作成功',
+      content: '您的提现申请已成功提交',
+      cancelText: '继续提现',
+      confirmText: '查看记录',
+      success: (res) => {
+        if (res.confirm) {
+          sheep.$router.go('/pages/commission/wallet', { type: 2 });
+          return;
+        }
+        getBrokerageUser();
+        state.accountInfo = {};
+      },
+    });
+  };
+
+  // 获得分销配置
+  async function getWithdrawRules() {
+    let { code, data } = await TradeConfigApi.getTradeConfig();
+    if (code !== 0) {
+      return;
+    }
+    if (data) {
+      state.minPrice = data.brokerageWithdrawMinPrice || 0;
+      state.frozenDays = data.brokerageFrozenDays || 0;
+      state.withdrawTypes = data.brokerageWithdrawTypes;
+    }
+  }
+
+  // 获得分销信息
+  async function getBrokerageUser() {
+    const { data, code } = await BrokerageApi.getBrokerageUser();
+    if (code === 0) {
+      state.brokerageInfo = data;
+    }
+  }
+
+  // 获取提现银行配置字典
+  async function getDictDataListByType() {
+    let { code, data } = await DictApi.getDictDataListByType('brokerage_bank_name');
+    if (code !== 0) {
+      return;
+    }
+    if (data && data.length > 0) {
+      state.bankList = data;
+    }
+  }
+
+  // 银行选择
+  function bankChange(e) {
+    const value = e.detail.value;
+    state.bankListSelectedIndex = value;
+    state.accountInfo.bankName = state.bankList[value].label;
+  }
+
+  onBeforeMount(() => {
+    getWithdrawRules();
+    getBrokerageUser();
+    getDictDataListByType(); //获取银行字典数据
+  });
+</script>
+
+<style lang="scss" scoped>
+  :deep() {
+    .uni-input-input {
+      font-family: OPPOSANS !important;
+    }
+  }
+
+  .wallet-num-box {
+    padding: 0 40rpx 80rpx;
+    background: var(--ui-BG-Main) v-bind(headerBg) center/750rpx 100% no-repeat;
+    border-radius: 0 0 5% 5%;
+
+    .num-title {
+      font-size: 26rpx;
+      font-weight: 500;
+      color: $white;
+      margin-bottom: 20rpx;
+    }
+
+    .wallet-num {
+      font-size: 60rpx;
+      font-weight: 500;
+      color: $white;
+      font-family: OPPOSANS;
+    }
+
+    .log-btn {
+      width: 170rpx;
+      height: 60rpx;
+      line-height: 60rpx;
+      border: 1rpx solid $white;
+      border-radius: 30rpx;
+      padding: 0;
+      font-size: 26rpx;
+      font-weight: 500;
+      color: $white;
+    }
+  }
+
+  // 提现输入卡片
+  .draw-card {
+    background-color: $white;
+    border-radius: 20rpx;
+    width: 690rpx;
+    min-height: 560rpx;
+    margin: -60rpx 30rpx 30rpx 30rpx;
+    padding: 30rpx;
+    position: relative;
+    z-index: 3;
+    box-sizing: border-box;
+
+    .card-title {
+      font-size: 30rpx;
+      font-weight: 500;
+      margin-bottom: 30rpx;
+    }
+
+    .bank-box {
+      .name {
+        font-size: 28rpx;
+        font-weight: 500;
+      }
+
+      .bank-list {
+        .empty-text {
+          font-size: 28rpx;
+          font-weight: 400;
+          color: $dark-9;
+        }
+
+        .cicon-forward {
+          color: $dark-9;
+        }
+      }
+
+      .input-box {
+        width: 624rpx;
+        height: 100rpx;
+        margin-bottom: 40rpx;
+
+        .unit {
+          font-size: 48rpx;
+          color: #333;
+          font-weight: 500;
+        }
+
+        .uni-easyinput__placeholder-class {
+          font-size: 30rpx;
+          height: 36rpx;
+        }
+
+        :deep(.uni-easyinput__content-input) {
+          font-size: 48rpx;
+        }
+      }
+
+      .save-btn {
+        width: 616rpx;
+        height: 86rpx;
+        line-height: 86rpx;
+        border-radius: 40rpx;
+        margin-top: 80rpx;
+      }
+    }
+
+    .bind-box {
+      .placeholder-text {
+        font-size: 26rpx;
+        color: $dark-9;
+      }
+
+      .add-btn {
+        width: 100rpx;
+        height: 50rpx;
+        border-radius: 25rpx;
+        line-height: 50rpx;
+        font-size: 22rpx;
+        color: var(--ui-BG-Main);
+        background-color: var(--ui-BG-Main-light);
+      }
+    }
+
+    .input-box {
+      width: 624rpx;
+      height: 100rpx;
+      margin-bottom: 40rpx;
+
+      .unit {
+        font-size: 48rpx;
+        color: #333;
+        font-weight: 500;
+      }
+
+      .uni-easyinput__placeholder-class {
+        font-size: 30rpx;
+      }
+
+      :deep(.uni-easyinput__content-input) {
+        font-size: 48rpx;
+      }
+    }
+
+    .save-btn {
+      width: 616rpx;
+      height: 86rpx;
+      line-height: 86rpx;
+      border-radius: 40rpx;
+      margin-top: 80rpx;
+    }
+  }
+
+  // 提现说明
+  .draw-notice {
+    width: 684rpx;
+    background: #ffffff;
+    border: 2rpx solid #fffaee;
+    border-radius: 20rpx;
+    margin: 20rpx 32rpx 0 32rpx;
+    padding: 30rpx;
+    box-sizing: border-box;
+
+    .title {
+      font-weight: 500;
+      color: #333333;
+      font-size: 30rpx;
+    }
+
+    .draw-list {
+      font-size: 24rpx;
+      color: #999999;
+      line-height: 46rpx;
+    }
+  }
+</style>

+ 390 - 0
pages/coupon/detail.vue

@@ -0,0 +1,390 @@
+<!-- 优惠券详情  -->
+<template>
+  <s-layout title="优惠券详情">
+    <view class="bg-white">
+      <!-- 详情卡片 -->
+      <view class="detail-wrap ss-p-20">
+        <view class="detail-box">
+          <view class="tag-box ss-flex ss-col-center ss-row-center">
+            <image
+              class="tag-image"
+              :src="sheep.$url.static('/static/img/shop/app/coupon_icon.png')"
+              mode="aspectFit"
+            />
+          </view>
+          <view class="top ss-flex-col ss-col-center">
+            <view class="title ss-m-t-50 ss-m-b-20 ss-m-x-20">{{ state.coupon.name }}</view>
+            <view class="subtitle ss-m-b-50">
+              满 {{ fen2yuan(state.coupon.usePrice) }} 元,
+              {{
+                state.coupon.discountType === 1
+                  ? '减 ' + fen2yuan(state.coupon.discountPrice) + ' 元'
+                  : '打 ' + state.coupon.discountPercent / 10.0 + ' 折'
+              }}
+            </view>
+            <button
+              class="ss-reset-button ss-m-b-30"
+              :class="
+                state.coupon.canTake || state.coupon.status === 1
+                  ? 'use-btn' // 优惠劵模版(可领取)、优惠劵(可使用)
+                  : 'disable-btn'
+              "
+              :disabled="!state.coupon.canTake"
+              @click="getCoupon"
+            >
+              <text v-if="state.id > 0">{{ state.coupon.canTake ? '立即领取' : '已领取' }}</text>
+              <text v-else>
+                {{
+                  state.coupon.status === 1
+                    ? '可使用'
+                    : state.coupon.status === 2
+                    ? '已使用'
+                    : '已过期'
+                }}
+              </text>
+            </button>
+            <view class="time ss-m-y-30" v-if="state.coupon.validityType === 2">
+              有效期:领取后 {{ state.coupon.fixedEndTerm }} 天内可用
+            </view>
+            <view class="time ss-m-y-30" v-else>
+              有效期: {{ sheep.$helper.timeFormat(state.coupon.validStartTime, 'yyyy-mm-dd') }} 至
+              {{ sheep.$helper.timeFormat(state.coupon.validEndTime, 'yyyy-mm-dd') }}
+            </view>
+            <view class="coupon-line ss-m-t-14"></view>
+          </view>
+          <view class="bottom">
+            <view class="type ss-flex ss-col-center ss-row-between ss-p-x-30">
+              <view>优惠券类型</view>
+              <view>{{ state.coupon.discountType === 1 ? '满减券' : '折扣券' }}</view>
+            </view>
+            <uni-collapse>
+              <uni-collapse-item title="优惠券说明" v-if="state.coupon.description">
+                <view class="content ss-p-b-20">
+                  <text class="des ss-p-l-30">{{ state.coupon.description }}</text>
+                </view>
+              </uni-collapse-item>
+            </uni-collapse>
+          </view>
+        </view>
+      </view>
+
+      <!-- 适用商品 -->
+      <view
+        class="all-user ss-flex ss-row-center ss-col-center"
+        v-if="state.coupon.productScope === 1"
+      >
+        全场通用
+      </view>
+
+      <su-sticky v-else bgColor="#fff">
+        <view class="goods-title ss-p-20">
+          {{ state.coupon.productScope === 2 ? '指定商品可用' : '指定分类可用' }}
+        </view>
+        <su-tabs
+          :scrollable="true"
+          :list="state.tabMaps"
+          @change="onTabsChange"
+          :current="state.currentTab"
+          v-if="state.coupon.productScope === 3"
+        />
+      </su-sticky>
+      <!-- 指定商品 -->
+      <view v-if="state.coupon.productScope === 2">
+        <view v-for="(item, index) in state.pagination.list" :key="index">
+          <s-goods-column
+            class="ss-m-20"
+            size="lg"
+            :data="item"
+            @click="sheep.$router.go('/pages/goods/index', { id: item.id })"
+            :goodsFields="{
+              title: { show: true },
+              subtitle: { show: true },
+              price: { show: true },
+              original_price: { show: true },
+              sales: { show: true },
+              stock: { show: false },
+            }"
+          />
+        </view>
+      </view>
+      <!-- 指定分类 -->
+      <view v-if="state.coupon.productScope === 3">
+        <view v-for="(item, index) in state.pagination.list" :key="index">
+          <s-goods-column
+            class="ss-m-20"
+            size="lg"
+            :data="item"
+            @click="sheep.$router.go('/pages/goods/index', { id: item.id })"
+            :goodsFields="{
+              title: { show: true },
+              subtitle: { show: true },
+              price: { show: true },
+              original_price: { show: true },
+              sales: { show: true },
+              stock: { show: false },
+            }"
+          ></s-goods-column>
+        </view>
+      </view>
+      <uni-load-more
+        v-if="state.pagination.total > 0 && state.coupon.productScope === 3"
+        :status="state.loadStatus"
+        :content-text="{
+          contentdown: '上拉加载更多',
+        }"
+        @tap="loadMore"
+      />
+      <s-empty
+        v-if="state.coupon.productScope === 3 && state.pagination.total === 0"
+        paddingTop="0"
+        icon="/static/soldout-empty.png"
+        text="暂无商品"
+      />
+    </view>
+  </s-layout>
+</template>
+
+<script setup>
+  import sheep from '@/sheep';
+  import { onLoad, onReachBottom } from '@dcloudio/uni-app';
+  import { reactive } from 'vue';
+  import _ from 'lodash-es';
+  import CouponApi from '@/sheep/api/promotion/coupon';
+  import { fen2yuan } from '@/sheep/hooks/useGoods';
+  import SpuApi from '@/sheep/api/product/spu';
+  import CategoryApi from '@/sheep/api/product/category';
+  import { resetPagination } from '@/sheep/util';
+
+  const state = reactive({
+    id: 0, // 优惠劵模版编号 templateId
+    couponId: 0, // 用户优惠劵编号 couponId
+    coupon: {}, // 优惠劵信息
+
+    pagination: {
+      list: [],
+      total: 0,
+      pageNo: 1,
+      pageSize: 8,
+    },
+    categoryId: 0, // 选中的商品分类编号
+    tabMaps: [], // 指定分类时,每个分类构成一个 tab
+    currentTab: 0, // 选中的 tabMaps 下标
+    loadStatus: '',
+  });
+
+  function onTabsChange(e) {
+    resetPagination(state.pagination);
+    state.currentTab = e.index;
+    state.categoryId = e.value;
+    getGoodsListByCategory();
+  }
+
+  async function getGoodsListByCategory() {
+    state.loadStatus = 'loading';
+    const { code, data } = await SpuApi.getSpuPage({
+      categoryId: state.categoryId,
+      pageNo: state.pagination.pageNo,
+      pageSize: state.pagination.pageSize,
+    });
+    if (code !== 0) {
+      return;
+    }
+    state.pagination.list = _.concat(state.pagination.list, data.list);
+    state.pagination.total = data.total;
+    state.loadStatus = state.pagination.list.length < state.pagination.total ? 'more' : 'noMore';
+  }
+
+  // 获得商品列表,指定商品范围
+  async function getGoodsListById() {
+    const { data, code } = await SpuApi.getSpuListByIds(state.coupon.productScopeValues.join(','));
+    if (code !== 0) {
+      return;
+    }
+    state.pagination.list = data;
+  }
+
+  // 获得分类列表
+  async function getCategoryList() {
+    const { data, code } = await CategoryApi.getCategoryListByIds(
+      state.coupon.productScopeValues.join(','),
+    );
+    if (code !== 0) {
+      return;
+    }
+    state.tabMaps = data.map((category) => ({ name: category.name, value: category.id }));
+    // 加载第一个分类的商品列表
+    if (state.tabMaps.length > 0) {
+      state.categoryId = state.tabMaps[0].value;
+      await getGoodsListByCategory();
+    }
+  }
+
+  // 领取优惠劵
+  async function getCoupon() {
+    const { code } = await CouponApi.takeCoupon(state.id);
+    if (code !== 0) {
+      return;
+    }
+    uni.showToast({
+      title: '领取成功',
+    });
+    setTimeout(() => {
+      getCouponContent();
+    }, 1000);
+  }
+
+  // 加载优惠劵信息
+  async function getCouponContent() {
+    const { code, data } =
+      state.id > 0
+        ? await CouponApi.getCouponTemplate(state.id)
+        : await CouponApi.getCoupon(state.couponId);
+    if (code !== 0) {
+      return;
+    }
+    state.coupon = data;
+    // 不同指定范围,加载不同数据
+    if (state.coupon.productScope === 2) {
+      await getGoodsListById();
+    } else if (state.coupon.productScope === 3) {
+      await getCategoryList();
+    }
+  }
+
+  // 加载更多
+  function loadMore() {
+    if (state.loadStatus === 'noMore') {
+      return;
+    }
+    state.pagination.pageNo++;
+    getGoodsListByCategory();
+  }
+
+  onLoad((options) => {
+    state.id = options.id;
+    state.couponId = options.couponId;
+    getCouponContent(state.id, state.couponId);
+  });
+
+  // 上拉加载更多
+  onReachBottom(() => {
+    loadMore();
+  });
+</script>
+
+<style lang="scss" scoped>
+  .goods-title {
+    font-size: 34rpx;
+    font-weight: bold;
+    color: #333333;
+  }
+
+  .detail-wrap {
+    background: linear-gradient(
+      180deg,
+      var(--ui-BG-Main),
+      var(--ui-BG-Main-gradient),
+      var(--ui-BG-Main),
+      #fff
+    );
+  }
+
+  .detail-box {
+    // background-color: var(--ui-BG);
+    border-radius: 6rpx;
+    position: relative;
+    margin-top: 100rpx;
+    .tag-box {
+      width: 140rpx;
+      height: 140rpx;
+      background: var(--ui-BG);
+      border-radius: 50%;
+      position: absolute;
+      top: -70rpx;
+      left: 50%;
+      z-index: 6;
+      transform: translateX(-50%);
+
+      .tag-image {
+        width: 104rpx;
+        height: 104rpx;
+        border-radius: 50%;
+      }
+    }
+
+    .top {
+      background-color: #fff;
+      border-radius: 20rpx 20rpx 0 0;
+      -webkit-mask: radial-gradient(circle at 16rpx 100%, #0000 16rpx, red 0) -16rpx;
+      padding: 110rpx 0 0 0;
+      position: relative;
+      z-index: 5;
+
+      .title {
+        font-size: 40rpx;
+        color: #333;
+        font-weight: bold;
+      }
+
+      .subtitle {
+        font-size: 28rpx;
+        color: #333333;
+      }
+
+      .use-btn {
+        width: 386rpx;
+        height: 80rpx;
+        line-height: 80rpx;
+        background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
+        border-radius: 40rpx;
+        color: $white;
+      }
+
+      .disable-btn {
+        width: 386rpx;
+        height: 80rpx;
+        line-height: 80rpx;
+        background: #e5e5e5;
+        border-radius: 40rpx;
+        color: $white;
+      }
+
+      .time {
+        font-size: 26rpx;
+        font-weight: 400;
+        color: #999999;
+      }
+
+      .coupon-line {
+        width: 95%;
+        border-bottom: 2rpx solid #eeeeee;
+      }
+    }
+
+    .bottom {
+      background-color: #fff;
+      border-radius: 0 0 20rpx 20rpx;
+      -webkit-mask: radial-gradient(circle at 16rpx 0%, #0000 16rpx, red 0) -16rpx;
+      padding: 40rpx 30rpx;
+
+      .type {
+        height: 96rpx;
+        border-bottom: 2rpx solid #eeeeee;
+      }
+    }
+
+    .des {
+      font-size: 24rpx;
+      font-weight: 400;
+      color: #666666;
+    }
+  }
+
+  .all-user {
+    width: 100%;
+    height: 300rpx;
+    font-size: 34rpx;
+    font-weight: bold;
+    color: #333333;
+  }
+</style>

+ 222 - 0
pages/coupon/list.vue

@@ -0,0 +1,222 @@
+<!-- 优惠券中心  -->
+<template>
+  <s-layout title="优惠券" :bgStyle="{ color: '#f2f2f2' }">
+    <su-sticky bgColor="#fff">
+      <su-tabs
+        :list="tabMaps"
+        :scrollable="false"
+        @change="onTabsChange"
+        :current="state.currentTab"
+      />
+    </su-sticky>
+    <s-empty
+      v-if="state.pagination.total === 0"
+      icon="/static/coupon-empty.png"
+      text="暂无优惠券"
+    />
+    <!-- 情况一:领劵中心 -->
+    <template v-if="state.currentTab === 0">
+      <view v-for="item in state.pagination.list" :key="item.id">
+        <s-coupon-list
+          :data="item"
+          @tap="sheep.$router.go('/pages/coupon/detail', { id: item.id })"
+        >
+          <template #default>
+            <button
+              class="ss-reset-button card-btn ss-flex ss-row-center ss-col-center"
+              :class="!item.canTake ? 'border-btn' : ''"
+              @click.stop="getBuy(item.id)"
+              :disabled="!item.canTake"
+            >
+              {{ item.canTake ? '立即领取' : '已领取' }}
+            </button>
+          </template>
+        </s-coupon-list>
+      </view>
+    </template>
+    <!-- 情况二:我的优惠劵 -->
+    <template v-else>
+      <view v-for="item in state.pagination.list" :key="item.id">
+        <s-coupon-list
+          :data="item"
+          type="user"
+          @tap="sheep.$router.go('/pages/coupon/detail', { couponId: item.id })"
+        >
+          <template #default>
+            <button
+              class="ss-reset-button card-btn ss-flex ss-row-center ss-col-center"
+              :class="item.status !== 1 ? 'disabled-btn' : ''"
+              :disabled="item.status !== 1"
+              @click.stop="sheep.$router.go('/pages/coupon/detail', { couponId: item.id })"
+            >
+              {{ item.status === 1 ? '立即使用' : item.status === 2 ? '已使用' : '已过期' }}
+            </button>
+          </template>
+        </s-coupon-list>
+      </view>
+    </template>
+
+    <uni-load-more
+      v-if="state.pagination.total > 0"
+      :status="state.loadStatus"
+      :content-text="{
+        contentdown: '上拉加载更多',
+      }"
+      @tap="loadMore"
+    />
+  </s-layout>
+</template>
+
+<script setup>
+  import sheep from '@/sheep';
+  import { onLoad, onReachBottom } from '@dcloudio/uni-app';
+  import { reactive } from 'vue';
+  import _ from 'lodash-es';
+  import { resetPagination } from '@/sheep/util';
+  import CouponApi from '@/sheep/api/promotion/coupon';
+
+  // 数据
+  const state = reactive({
+    currentTab: 0, // 当前 tab
+    type: '1',
+    pagination: {
+      list: [],
+      total: 0,
+      pageNo: 1,
+      pageSize: 5,
+    },
+    loadStatus: '',
+  });
+
+  const tabMaps = [
+    {
+      name: '领券中心',
+      value: 'all',
+    },
+    {
+      name: '已领取',
+      value: '1',
+    },
+    {
+      name: '已使用',
+      value: '2',
+    },
+    {
+      name: '已失效',
+      value: '3',
+    },
+  ];
+
+  function onTabsChange(e) {
+    state.currentTab = e.index;
+    state.type = e.value;
+    resetPagination(state.pagination);
+    if (state.currentTab === 0) {
+      getData();
+    } else {
+      getCoupon();
+    }
+  }
+
+  // 获得优惠劵模版列表
+  async function getData() {
+    state.loadStatus = 'loading';
+    const { data, code } = await CouponApi.getCouponTemplatePage({
+      pageNo: state.pagination.pageNo,
+      pageSize: state.pagination.pageSize,
+    });
+    if (code !== 0) {
+      return;
+    }
+    state.pagination.list = _.concat(state.pagination.list, data.list);
+    state.pagination.total = data.total;
+    state.loadStatus = state.pagination.list.length < state.pagination.total ? 'more' : 'noMore';
+  }
+
+  // 获得我的优惠劵
+  async function getCoupon() {
+    state.loadStatus = 'loading';
+    const { data, code } = await CouponApi.getCouponPage({
+      pageNo: state.pagination.pageNo,
+      pageSize: state.pagination.pageSize,
+      status: state.type,
+    });
+    if (code !== 0) {
+      return;
+    }
+    state.pagination.list = _.concat(state.pagination.list, data.list);
+    state.pagination.total = data.total;
+    state.loadStatus = state.pagination.list.length < state.pagination.total ? 'more' : 'noMore';
+  }
+
+  // 领取优惠劵
+  async function getBuy(id) {
+    const { code } = await CouponApi.takeCoupon(id);
+    if (code !== 0) {
+      return;
+    }
+    uni.showToast({
+      title: '领取成功',
+    });
+    setTimeout(() => {
+      resetPagination(state.pagination);
+      getData();
+    }, 1000);
+  }
+
+  // 加载更多
+  function loadMore() {
+    if (state.loadStatus === 'noMore') {
+      return;
+    }
+    state.pagination.pageNo++;
+    if (state.currentTab === 0) {
+      getData();
+    } else {
+      getCoupon();
+    }
+  }
+
+  onLoad((Option) => {
+    // 领劵中心
+    if (Option.type === 'all' || !Option.type) {
+      getData();
+      // 我的优惠劵
+    } else {
+      Option.type === 'geted'
+        ? (state.currentTab = 1)
+        : Option.type === 'used'
+        ? (state.currentTab = 2)
+        : (state.currentTab = 3);
+      state.type = state.currentTab;
+      getCoupon();
+    }
+  });
+
+  onReachBottom(() => {
+    loadMore();
+  });
+</script>
+<style lang="scss" scoped>
+  .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;
+  }
+
+  .border-btn {
+    background: linear-gradient(90deg, var(--ui-BG-Main-opacity-4), var(--ui-BG-Main-light));
+    color: #fff !important;
+  }
+
+  .disabled-btn {
+    background: #cccccc;
+    background-color: #cccccc !important;
+    color: #fff !important;
+  }
+</style>

+ 190 - 0
pages/goods/comment/add.vue

@@ -0,0 +1,190 @@
+<!-- 评价  -->
+<template>
+  <s-layout title="评价">
+    <view>
+      <view v-for="(item, index) in state.orderInfo.items" :key="item.id">
+        <view>
+          <view class="commont-from-wrap">
+            <!-- 评价商品 -->
+            <s-goods-item
+              :img="item.picUrl"
+              :title="item.spuName"
+              :skuText="item.properties.map((property) => property.valueName).join(' ')"
+              :price="item.payPrice"
+              :num="item.count"
+            />
+          </view>
+
+          <view class="form-item">
+            <!-- 评分 -->
+            <view class="star-box ss-flex ss-col-center">
+              <view class="star-title ss-m-r-40">商品质量</view>
+              <uni-rate v-model="state.commentList[index].descriptionScores" />
+            </view>
+            <view class="star-box ss-flex ss-col-center">
+              <view class="star-title ss-m-r-40">服务态度</view>
+              <uni-rate v-model="state.commentList[index].benefitScores" />
+            </view>
+            <!-- 评价 -->
+            <view class="area-box">
+              <uni-easyinput
+                :inputBorder="false"
+                type="textarea"
+                maxlength="120"
+                autoHeight
+                v-model="state.commentList[index].content"
+                placeholder="宝贝满足你的期待吗?说说你的使用心得,分享给想买的他们吧~"
+              />
+              <view class="img-box">
+                <s-uploader
+                  v-model:url="state.commentList[index].images"
+                  fileMediatype="image"
+                  limit="9"
+                  mode="grid"
+                  :imageStyles="{ width: '168rpx', height: '168rpx' }"
+                  @success="(payload) => uploadSuccess(payload, index)"
+                />
+              </view>
+            </view>
+            <view class="checkbox-container">
+              <checkbox-group @change="(event) => toggleAnonymous(index, event)">
+                <label>
+                  <checkbox value="anonymousChecked" />
+                  匿名评论
+                </label>
+              </checkbox-group>
+            </view>
+          </view>
+        </view>
+      </view>
+    </view>
+    <su-fixed bottom placeholder>
+      <view class="foot_box ss-flex ss-row-center ss-col-center">
+        <button class="ss-reset-button post-btn ui-BG-Main-Gradient ui-Shadow-Main" @tap="onSubmit">
+          发布
+        </button>
+      </view>
+    </su-fixed>
+  </s-layout>
+</template>
+
+<script setup>
+  import sheep from '@/sheep';
+  import { onLoad } from '@dcloudio/uni-app';
+  import { reactive, ref } from 'vue';
+  import OrderApi from '@/sheep/api/trade/order';
+
+  const state = reactive({
+    orderInfo: {},
+    commentList: [],
+    id: null,
+  });
+
+  /**
+   * 切换是否匿名
+   *
+   * @param commentIndex  当前评论下标
+   * @param event 复选框事件
+   */
+  function toggleAnonymous(commentIndex, event) {
+    state.commentList[commentIndex].anonymous = event.detail.value[0] === 'anonymousChecked';
+  }
+
+  /**
+   * 发布评论
+   *
+   * @returns {Promise<void>}
+   */
+  async function onSubmit() {
+    // 顺序提交评论
+    for (const comment of state.commentList) {
+      await OrderApi.createOrderItemComment(comment);
+    }
+    // 都评论好,返回
+    sheep.$router.back();
+  }
+
+  /**
+   * 图片添加到表单
+   *
+   * @param payload 上传成功后的回调数据
+   * @param commentIndex  当前评论的下标
+   */
+  function uploadSuccess(payload, commentIndex) {
+    state.commentList[commentIndex].picUrls = state.commentList[commentIndex].images;
+  }
+
+  onLoad(async (options) => {
+    if (!options.id) {
+      sheep.$helper.toast(`缺少订单信息,请检查`);
+      return;
+    }
+    state.id = options.id;
+
+    const { code, data } = await OrderApi.getOrderDetail(state.id);
+    if (code !== 0) {
+      sheep.$helper.toast('无待评价订单');
+      return;
+    }
+    // 处理评论
+    data.items.forEach((item) => {
+      state.commentList.push({
+        anonymous: false,
+        orderItemId: item.id,
+        descriptionScores: 5,
+        benefitScores: 5,
+        content: '',
+        picUrls: [],
+      });
+    });
+    state.orderInfo = data;
+  });
+</script>
+
+<style lang="scss" scoped>
+  // 评价商品
+  .goods-card {
+    margin: 10rpx 0;
+    padding: 20rpx;
+    background: #fff;
+  }
+
+  // 评论,选择图片
+  .form-item {
+    background: #fff;
+
+    .star-box {
+      height: 100rpx;
+      padding: 0 25rpx;
+    }
+
+    .star-title {
+      font-weight: 600;
+    }
+  }
+
+  .area-box {
+    width: 690rpx;
+    min-height: 306rpx;
+    background: rgba(249, 250, 251, 1);
+    border-radius: 20rpx;
+    padding: 28rpx;
+    margin: auto;
+
+    .img-box {
+      margin-top: 20rpx;
+    }
+  }
+
+  .checkbox-container {
+    padding: 10rpx;
+  }
+
+  .post-btn {
+    width: 690rpx;
+    line-height: 80rpx;
+    border-radius: 40rpx;
+    color: rgba(#fff, 0.9);
+    margin-bottom: 20rpx;
+  }
+</style>

+ 168 - 0
pages/goods/comment/list.vue

@@ -0,0 +1,168 @@
+<!-- 商品评论的分页 -->
+<template>
+  <s-layout title="全部评论">
+    <su-tabs
+      :list="state.type"
+      :scrollable="false"
+      @change="onTabsChange"
+      :current="state.currentTab"
+    />
+    <!-- 评论列表 -->
+    <view class="ss-m-t-20">
+      <view class="list-item" v-for="item in state.pagination.list" :key="item">
+        <comment-item :item="item" />
+      </view>
+    </view>
+    <s-empty v-if="state.pagination.total === 0" text="暂无数据" icon="/static/data-empty.png" />
+    <!-- 下拉 -->
+    <uni-load-more
+      icon-type="auto"
+      v-if="state.pagination.total > 0"
+      :status="state.loadStatus"
+      :content-text="{
+        contentdown: '上拉加载更多',
+      }"
+      @tap="loadMore"
+    />
+  </s-layout>
+</template>
+
+<script setup>
+  import CommentApi from '@/sheep/api/product/comment';
+  import { onLoad, onReachBottom } from '@dcloudio/uni-app';
+  import { reactive } from 'vue';
+  import _ from 'lodash-es';
+  import commentItem from '../components/detail/comment-item.vue';
+
+  const state = reactive({
+    id: 0, // 商品 SPU 编号
+    type: [
+      { type: 0, name: '全部' },
+      { type: 1, name: '好评' },
+      { type: 2, name: '中评' },
+      { type: 3, name: '差评' },
+    ],
+    currentTab: 0, // 选中的 TAB
+    loadStatus: '',
+    pagination: {
+      list: [],
+      total: 0,
+      pageNo: 1,
+      pageSize: 8,
+    },
+  });
+
+  // 切换选项卡
+  function onTabsChange(e) {
+    state.currentTab = e.index;
+    // 加载列表
+    state.pagination.pageNo = 1;
+    state.pagination.list = [];
+    state.pagination.total = 0;
+    getList();
+  }
+
+  async function getList() {
+    // 加载列表
+    state.loadStatus = 'loading';
+    let res = await CommentApi.getCommentPage(
+      state.id,
+      state.pagination.pageNo,
+      state.pagination.pageSize,
+      state.type[state.currentTab].type,
+    );
+    if (res.code !== 0) {
+      return;
+    }
+    // 合并列表
+    state.pagination.list = _.concat(state.pagination.list, res.data.list);
+    state.pagination.total = res.data.total;
+    state.loadStatus = state.pagination.list.length < state.pagination.total ? 'more' : 'noMore';
+  }
+
+  // 加载更多
+  function loadMore() {
+    if (state.loadStatus === 'noMore') {
+      return;
+    }
+    state.pagination.pageNo++;
+    getList();
+  }
+
+  onLoad((options) => {
+    state.id = options.id;
+    getList();
+  });
+
+  // 上拉加载更多
+  onReachBottom(() => {
+    loadMore();
+  });
+</script>
+
+<style lang="scss" scoped>
+  .list-item {
+    padding: 32rpx 30rpx 20rpx 20rpx;
+    background: #fff;
+
+    .avatar {
+      width: 52rpx;
+      height: 52rpx;
+      border-radius: 50%;
+    }
+
+    .nickname {
+      font-size: 26rpx;
+      font-weight: 500;
+      color: #999999;
+    }
+
+    .create-time {
+      font-size: 24rpx;
+      font-weight: 500;
+      color: #c4c4c4;
+    }
+
+    .content-title {
+      font-size: 26rpx;
+      font-weight: 400;
+      color: #666666;
+      line-height: 42rpx;
+    }
+
+    .content-img {
+      width: 174rpx;
+      height: 174rpx;
+    }
+
+    .cicon-info-o {
+      font-size: 26rpx;
+      color: #c4c4c4;
+    }
+
+    .foot-title {
+      font-size: 24rpx;
+      font-weight: 500;
+      color: #999999;
+    }
+  }
+
+  .btn-box {
+    width: 100%;
+    height: 120rpx;
+    background: #fff;
+    border-top: 2rpx solid #eee;
+  }
+
+  .tab-btn {
+    width: 130rpx;
+    height: 62rpx;
+    background: #eeeeee;
+    border-radius: 31rpx;
+    font-size: 28rpx;
+    font-weight: 400;
+    color: #999999;
+    border: 1px solid #e5e5e5;
+    margin-right: 10rpx;
+  }
+</style>

+ 94 - 0
pages/goods/components/detail/comment-item.vue

@@ -0,0 +1,94 @@
+<!-- 商品评论项 -->
+<template>
+  <view>
+    <!-- 用户评论 -->
+    <view class="user ss-flex ss-m-b-14">
+      <view class="ss-m-r-20 ss-flex">
+        <image class="avatar" :src="item.userAvatar"></image>
+      </view>
+      <view class="nickname ss-m-r-20">{{ item.userNickname }}</view>
+      <view class="">
+        <uni-rate :readonly="true" v-model="item.scores" size="18" />
+      </view>
+    </view>
+    <view class="content"> {{ item.content }} </view>
+    <view class="ss-m-t-24" v-if="item.picUrls?.length">
+      <scroll-view class="scroll-box" scroll-x scroll-anchoring>
+        <view class="ss-flex">
+          <view v-for="(picUrl, index) in item.picUrls" :key="picUrl" class="ss-m-r-10">
+            <su-image
+              class="content-img"
+              isPreview
+              :previewList="item.picUrls"
+              :current="index"
+              :src="picUrl"
+              :height="120"
+              :width="120"
+              mode="aspectFill"
+            />
+          </view>
+        </view>
+      </scroll-view>
+    </view>
+    <!-- 商家回复 -->
+    <view class="ss-m-t-20 reply-box" v-if="item.replyTime">
+      <view class="reply-title">商家回复:</view>
+      <view class="reply-content">{{ item.replyContent }}</view>
+    </view>
+  </view>
+</template>
+
+<script setup>
+  const props = defineProps({
+    item: {
+      type: Object,
+      default() {},
+    },
+  });
+</script>
+
+<style lang="scss" scoped>
+  .avatar {
+    width: 52rpx;
+    height: 52rpx;
+    border-radius: 50%;
+  }
+
+  .nickname {
+    font-size: 26rpx;
+    font-weight: 500;
+    color: #999999;
+  }
+
+  .content {
+    width: 636rpx;
+    font-size: 26rpx;
+    font-weight: 400;
+    color: #333333;
+  }
+
+  .reply-box {
+    position: relative;
+    background: #f8f8f8;
+    border-radius: 8rpx;
+    padding: 16rpx;
+  }
+
+  .reply-title {
+    position: absolute;
+    left: 16rpx;
+    top: 16rpx;
+    font-weight: 400;
+    font-size: 26rpx;
+    line-height: 40rpx;
+    color: #333333;
+  }
+
+  .reply-content {
+    text-indent: 128rpx;
+    font-weight: 400;
+    font-size: 26rpx;
+    line-height: 40rpx;
+    color: #333333;
+  }
+</style>

+ 97 - 0
pages/goods/components/detail/detail-activity-tip.vue

@@ -0,0 +1,97 @@
+<template>
+  <su-fixed bottom placeholder :val="44">
+    <view>
+      <view v-for="activity in props.activityList" :key="activity.id">
+        <view
+          class="activity-box ss-p-x-38 ss-flex ss-row-between ss-col-center"
+          :class="activity.type === 1 ? 'seckill-box' : 'groupon-box'"
+        >
+          <view class="activity-title ss-flex">
+            <view class="ss-m-r-16">
+              <image
+                v-if="activity.type === 1"
+                :src="sheep.$url.static('/static/img/shop/goods/seckill-icon.png')"
+                class="activity-icon"
+              />
+              <image
+                v-else-if="activity.type === 3"
+                :src="sheep.$url.static('/static/img/shop/goods/groupon-icon.png')"
+                class="activity-icon"
+              />
+            </view>
+            <view>该商品正在参与{{ activity.name }}活动</view>
+          </view>
+          <button class="ss-reset-button activity-go" @tap="onActivity(activity)"> GO </button>
+        </view>
+      </view>
+    </view>
+  </su-fixed>
+</template>
+
+<script setup>
+  import sheep from '@/sheep';
+
+  const seckillBg = sheep.$url.css('/static/img/shop/goods/seckill-tip-bg.png');
+  const grouponBg = sheep.$url.css('/static/img/shop/goods/groupon-tip-bg.png');
+
+  const props = defineProps({
+    activityList: {
+      type: Array,
+      default() {
+        return [];
+      }
+    }
+  });
+
+  function onActivity(activity) {
+    const type = activity.type;
+    const typePath = type === 1 ? 'seckill' :
+      type === 2 ? 'TODO 拼团' : 'groupon';
+    sheep.$router.go(`/pages/goods/${typePath}`, {
+      id: activity.id,
+    });
+  }
+</script>
+
+<style lang="scss" scoped>
+  .activity-box {
+    width: 100%;
+    height: 80rpx;
+    box-sizing: border-box;
+    margin-bottom: 10rpx;
+
+    .activity-title {
+      font-size: 26rpx;
+      font-weight: 500;
+      color: #ffffff;
+      line-height: 42rpx;
+
+      .activity-icon {
+        width: 38rpx;
+        height: 38rpx;
+      }
+    }
+
+    .activity-go {
+      width: 70rpx;
+      height: 32rpx;
+      background: #ffffff;
+      border-radius: 16rpx;
+      font-weight: 500;
+      color: #ff6000;
+      font-size: 24rpx;
+      line-height: normal;
+    }
+  }
+
+  //秒杀卡片
+  .seckill-box {
+    background: v-bind(seckillBg) no-repeat;
+    background-size: 100% 100%;
+  }
+
+  .groupon-box {
+    background: v-bind(grouponBg) no-repeat;
+    background-size: 100% 100%;
+  }
+</style>

+ 31 - 0
pages/goods/components/detail/detail-cell-sku.vue

@@ -0,0 +1,31 @@
+<template>
+  <!-- SKU 选择的提示框 -->
+  <detail-cell label="选择" :value="value" />
+</template>
+
+<script setup>
+  import { computed } from 'vue';
+  import detailCell from './detail-cell.vue';
+
+  const props = defineProps({
+    modelValue: {
+      type: Array,
+      default() {
+        return [];
+      },
+    },
+    sku: {
+      type: Object
+    }
+  });
+  const value = computed(() => {
+    if (!props.sku?.id) {
+      return '请选择商品规格';
+    }
+    let str = '';
+    props.sku.properties.forEach(property => {
+      str += property.propertyName + ':' + property.valueName + ' ';
+    });
+    return str;
+  });
+</script>

+ 60 - 0
pages/goods/components/detail/detail-cell.vue

@@ -0,0 +1,60 @@
+<!-- 商品详情:cell 组件 -->
+<template>
+  <view class="detail-cell-wrap ss-flex ss-col-center ss-row-between" @tap="onClick">
+    <view class="label-text">{{ label }}</view>
+    <view class="cell-content ss-line-1 ss-flex-1">{{ value }}</view>
+    <button class="ss-reset-button">
+      <text class="_icon-forward right-forwrad-icon"></text>
+    </button>
+  </view>
+</template>
+
+<script setup>
+  /**
+   * 详情 cell
+   *
+   */
+  const props = defineProps({
+    label: {
+      type: String,
+      default: '',
+    },
+    value: {
+      type: String,
+      default: '',
+    },
+  });
+
+  const emits = defineEmits(['click']);
+
+  // 点击
+  const onClick = () => {
+    emits('click');
+  };
+</script>
+
+<style lang="scss" scoped>
+  .detail-cell-wrap {
+    padding: 10rpx 20rpx;
+    // min-height: 60rpx;
+
+    .label-text {
+      font-size: 28rpx;
+      font-weight: 500;
+      color: $dark-9;
+      margin-right: 38rpx;
+    }
+
+    .cell-content {
+      font-size: 28rpx;
+      font-weight: 500;
+      color: $dark-6;
+    }
+
+    .right-forwrad-icon {
+      font-size: 28rpx;
+      font-weight: 500;
+      color: $dark-9;
+    }
+  }
+</style>

+ 106 - 0
pages/goods/components/detail/detail-comment-card.vue

@@ -0,0 +1,106 @@
+<!-- 商品评论的卡片 -->
+<template>
+  <view class="detail-comment-card bg-white">
+    <view class="card-header ss-flex ss-col-center ss-row-between ss-p-b-30">
+      <view class="ss-flex ss-col-center">
+        <view class="line"></view>
+        <view class="title ss-m-l-20 ss-m-r-10">评价</view>
+        <view class="des">({{ state.total }})</view>
+      </view>
+      <view
+        class="ss-flex ss-col-center"
+        @tap="sheep.$router.go('/pages/goods/comment/list', { id: goodsId })"
+        v-if="state.commentList.length > 0"
+      >
+        <button class="ss-reset-button more-btn">查看全部</button>
+        <text class="cicon-forward" />
+      </view>
+    </view>
+    <!-- 评论列表 -->
+    <view class="card-content">
+      <view class="comment-box ss-p-y-30" v-for="item in state.commentList" :key="item.id">
+        <comment-item :item="item" />
+      </view>
+      <s-empty
+        v-if="state.commentList.length === 0"
+        paddingTop="0"
+        icon="/static/comment-empty.png"
+        text="期待您的第一个评价"
+      />
+    </view>
+  </view>
+</template>
+
+<script setup>
+  import { reactive, onBeforeMount } from 'vue';
+  import sheep from '@/sheep';
+  import CommentApi from '@/sheep/api/product/comment';
+  import commentItem from './comment-item.vue';
+
+  const props = defineProps({
+    goodsId: {
+      type: [Number, String],
+      default: 0,
+    },
+  });
+
+  const state = reactive({
+    commentList: [], // 评论列表,只展示最近的 3 条
+    total: 0, // 总评论数
+  });
+
+  async function getComment(id) {
+    const { data } = await CommentApi.getCommentPage(id, 1, 3, 0);
+    state.commentList = data.list;
+    state.total = data.total;
+  }
+
+  onBeforeMount(() => {
+    getComment(props.goodsId);
+  });
+</script>
+
+<style lang="scss" scoped>
+  .detail-comment-card {
+    margin: 0 20rpx 20rpx 20rpx;
+    padding: 20rpx 20rpx 0 20rpx;
+    .card-header {
+      .line {
+        width: 6rpx;
+        height: 30rpx;
+        background: linear-gradient(180deg, var(--ui-BG-Main) 0%, var(--ui-BG-Main-gradient) 100%);
+        border-radius: 3rpx;
+      }
+
+      .title {
+        font-size: 30rpx;
+        font-weight: bold;
+        line-height: normal;
+      }
+
+      .des {
+        font-size: 24rpx;
+        color: $dark-9;
+      }
+
+      .more-btn {
+        font-size: 24rpx;
+        color: var(--ui-BG-Main);
+        line-height: normal;
+      }
+
+      .cicon-forward {
+        font-size: 24rpx;
+        line-height: normal;
+        color: var(--ui-BG-Main);
+        margin-top: 4rpx;
+      }
+    }
+  }
+  .comment-box {
+    border-bottom: 2rpx solid #eeeeee;
+    &:last-child {
+      border: none;
+    }
+  }
+</style>

+ 52 - 0
pages/goods/components/detail/detail-content-card.vue

@@ -0,0 +1,52 @@
+<!-- 商品详情:描述卡片 -->
+<template>
+  <view class="detail-content-card bg-white ss-m-x-20 ss-p-t-20">
+    <view class="card-header ss-flex ss-col-center ss-m-b-30 ss-m-l-20">
+      <view class="line"></view>
+      <view class="title ss-m-l-20 ss-m-r-20">详情</view>
+    </view>
+    <view class="card-content">
+      <mp-html :content="content" />
+    </view>
+  </view>
+</template>
+
+<script setup>
+  import sheep from '@/sheep';
+  const { safeAreaInsets } = sheep.$platform.device;
+
+  const props = defineProps({
+    content: {
+      type: String,
+      default: '',
+    },
+  });
+</script>
+
+<style lang="scss" scoped>
+  .detail-content-card {
+    .card-header {
+      .line {
+        width: 6rpx;
+        height: 30rpx;
+        background: linear-gradient(180deg, var(--ui-BG-Main) 0%, var(--ui-BG-Main-gradient) 100%);
+        border-radius: 3rpx;
+      }
+
+      .title {
+        font-size: 30rpx;
+        font-weight: bold;
+      }
+
+      .des {
+        font-size: 24rpx;
+        color: $dark-9;
+      }
+
+      .more-btn {
+        font-size: 24rpx;
+        color: var(--ui-BG-Main);
+      }
+    }
+  }
+</style>

+ 256 - 0
pages/goods/components/detail/detail-navbar.vue

@@ -0,0 +1,256 @@
+<!-- 商品详情:商品/评价/详情的 nav -->
+<template>
+  <su-fixed alway :bgStyles="{ background: '#fff' }" :val="0" noNav opacity :placeholder="false">
+    <su-status-bar />
+    <view
+      class="ui-bar ss-flex ss-col-center ss-row-between ss-p-x-20"
+      :style="[{ height: sys_navBar - sys_statusBar + 'px' }]"
+    >
+      <!-- 左 -->
+      <view class="icon-box ss-flex">
+        <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 class="detail-tab-card ss-flex-1" :style="[{ opacity: state.tabOpacityVal }]">
+        <view class="tab-box ss-flex ss-col-center ss-row-around">
+          <view
+            class="tab-item ss-flex-1 ss-flex ss-row-center ss-col-center"
+            v-for="item in state.tabList"
+            :key="item.value"
+            @tap="onTab(item)"
+          >
+            <view class="tab-title" :class="state.curTab === item.value ? 'cur-tab-title' : ''">
+              {{ item.label }}
+            </view>
+            <view v-show="state.curTab === item.value" class="tab-line"></view>
+          </view>
+        </view>
+      </view>
+      <!-- #ifdef MP -->
+      <view :style="[capsuleStyle]"></view>
+      <!-- #endif -->
+    </view>
+  </su-fixed>
+</template>
+
+<script setup>
+  import { reactive } from 'vue';
+  import { onPageScroll } from '@dcloudio/uni-app';
+  import sheep from '@/sheep';
+  import throttle from '@/sheep/helper/throttle.js';
+  import { showMenuTools, closeMenuTools } from '@/sheep/hooks/useModal';
+
+  const sys_statusBar = sheep.$platform.device.statusBarHeight;
+  const sys_navBar = sheep.$platform.navbar;
+  const capsuleStyle = {
+    width: sheep.$platform.capsule.width + 'px',
+    height: sheep.$platform.capsule.height + 'px',
+  };
+
+  const state = reactive({
+    tabOpacityVal: 0,
+    curTab: 'goods',
+    tabList: [
+      {
+        label: '商品',
+        value: 'goods',
+        to: 'detail-swiper-selector',
+      },
+      {
+        label: '评价',
+        value: 'comment',
+        to: 'detail-comment-selector',
+      },
+      {
+        label: '详情',
+        value: 'detail',
+        to: 'detail-content-selector',
+      },
+    ],
+  });
+  const emits = defineEmits(['clickLeft']);
+  const hasHistory = sheep.$router.hasHistory();
+
+  function onClickLeft() {
+    if (hasHistory) {
+      sheep.$router.back();
+    } else {
+      sheep.$router.go('/pages/index/index');
+    }
+    emits('clickLeft');
+  }
+
+  function onClickRight() {
+    showMenuTools();
+  }
+
+  let commentCard = {
+    top: 0,
+    bottom: 0,
+  };
+
+  function getCommentCardNode() {
+    return new Promise((res, rej) => {
+      uni.createSelectorQuery()
+        .select('.detail-comment-selector')
+        .boundingClientRect((data) => {
+          if (data) {
+            commentCard.top = data.top;
+            commentCard.bottom = data.top + data.height;
+            res(data);
+          } else {
+            res(null);
+          }
+        })
+        .exec();
+    });
+  }
+
+  function onTab(tab) {
+    let scrollTop = 0;
+    if (tab.value === 'comment') {
+      scrollTop = commentCard.top - sys_navBar + 1;
+    } else if (tab.value === 'detail') {
+      scrollTop = commentCard.bottom - sys_navBar + 1;
+    }
+    uni.pageScrollTo({
+      scrollTop,
+      duration: 200,
+    });
+  }
+
+  onPageScroll((e) => {
+    state.tabOpacityVal = e.scrollTop > sheep.$platform.navbar ? 1 : e.scrollTop * 0.01;
+    if (commentCard.top === 0) {
+      throttle(() => {
+        getCommentCardNode();
+      }, 50);
+    }
+
+    if (e.scrollTop < commentCard.top - sys_navBar) {
+      state.curTab = 'goods';
+    } else if (
+      e.scrollTop >= commentCard.top - sys_navBar &&
+      e.scrollTop <= commentCard.bottom - sys_navBar
+    ) {
+      state.curTab = 'comment';
+    } else {
+      state.curTab = 'detail';
+    }
+  });
+</script>
+
+<style lang="scss" scoped>
+  .icon-box {
+    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;
+    border: 1px solid rgba(#fff, 0.4);
+    .line {
+      width: 2rpx;
+      height: 24rpx;
+      background: #e5e5e7;
+    }
+    .sicon-back {
+      font-size: 32rpx;
+      color: #000;
+    }
+    .sicon-home {
+      font-size: 32rpx;
+      color: #000;
+    }
+    .sicon-more {
+      font-size: 32rpx;
+      color: #000;
+    }
+    .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;
+      }
+    }
+  }
+  .left-box {
+    position: relative;
+    width: 60rpx;
+    height: 60rpx;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    .circle {
+      position: absolute;
+      left: 0;
+      top: 0;
+      width: 60rpx;
+      height: 60rpx;
+      background: rgba(#fff, 0.6);
+      border: 1rpx solid #ebebeb;
+      border-radius: 50%;
+      box-sizing: border-box;
+      z-index: -1;
+    }
+  }
+  .right {
+    position: relative;
+    width: 60rpx;
+    height: 60rpx;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    .circle {
+      position: absolute;
+      left: 0;
+      top: 0;
+      width: 60rpx;
+      height: 60rpx;
+      background: rgba(#ffffff, 0.6);
+      border: 1rpx solid #ebebeb;
+      box-sizing: border-box;
+      border-radius: 50%;
+      z-index: -1;
+    }
+  }
+  .detail-tab-card {
+    width: 50%;
+    .tab-item {
+      height: 80rpx;
+      position: relative;
+      z-index: 11;
+
+      .tab-title {
+        font-size: 30rpx;
+      }
+
+      .cur-tab-title {
+        font-weight: $font-weight-bold;
+      }
+
+      .tab-line {
+        width: 60rpx;
+        height: 6rpx;
+        border-radius: 6rpx;
+        position: absolute;
+        left: 50%;
+        transform: translateX(-50%);
+        bottom: 10rpx;
+        background-color: var(--ui-BG-Main);
+        z-index: 12;
+      }
+    }
+  }
+</style>

+ 40 - 0
pages/goods/components/detail/detail-progress.vue

@@ -0,0 +1,40 @@
+<!-- 秒杀商品:抢购进度 -->
+<template>
+  <view class="ss-flex ss-col-center">
+    <view class="progress-title ss-m-r-10"> 已抢{{ percent }}% </view>
+    <view class="progress-box ss-flex ss-col-center">
+      <view class="progerss-active" :style="{ width: percent < 10 ? '10%' : percent + '%' }">
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup>
+  const props = defineProps({
+    percent: {
+      type: Number,
+      default: 0,
+    },
+  });
+</script>
+
+<style lang="scss" scoped>
+  .progress-title {
+    font-size: 20rpx;
+    font-weight: 500;
+    color: #ffffff;
+  }
+
+  .progress-box {
+    width: 168rpx;
+    height: 18rpx;
+    background: #f6f6f6;
+    border-radius: 9rpx;
+  }
+
+  .progerss-active {
+    height: 24rpx;
+    background: linear-gradient(86deg, #f60600, #d00500);
+    border-radius: 12rpx;
+  }
+</style>

+ 177 - 0
pages/goods/components/detail/detail-skeleton.vue

@@ -0,0 +1,177 @@
+<template>
+  <view
+    class="skeleton-wrap"
+    :class="['theme-' + sys.mode, 'main-' + sys.theme, 'font-' + sys.fontSize]"
+  >
+    <view class="skeleton-banner"></view>
+    <view class="container-box">
+      <view class="container-box-strip title ss-m-b-58"></view>
+      <view class="container-box-strip ss-m-b-20"></view>
+      <view class="container-box-strip ss-m-b-20"></view>
+      <view class="container-box-strip w-364"></view>
+    </view>
+    <view class="container-box">
+      <view class="ss-flex ss-row-between ss-m-b-34">
+        <view class="container-box-strip w-380"></view>
+        <view class="circle"></view>
+      </view>
+      <view class="ss-flex ss-row-between ss-m-b-34">
+        <view class="container-box-strip w-556"></view>
+        <view class="circle"></view>
+      </view>
+      <view class="ss-flex ss-row-between">
+        <view class="container-box-strip w-556"></view>
+        <view class="circle"></view>
+      </view>
+    </view>
+    <view class="container-box">
+      <view class="container-box-strip w-198 ss-m-b-42"></view>
+      <view class="ss-flex">
+        <view class="circle ss-m-r-12"></view>
+        <view class="container-box-strip w-252"></view>
+      </view>
+    </view>
+    <su-fixed bottom placeholder bg="bg-white">
+      <view class="ui-tabbar-box">
+        <view class="foot ss-flex ss-col-center">
+          <view class="ss-m-r-54 ss-m-l-32">
+            <view class="rec ss-m-b-8"></view>
+            <view class="oval"></view>
+          </view>
+          <view class="ss-m-r-54">
+            <view class="rec ss-m-b-8"></view>
+            <view class="oval"></view>
+          </view>
+          <view class="ss-m-r-50">
+            <view class="rec ss-m-b-8"></view>
+            <view class="oval"></view>
+          </view>
+          <button class="ss-reset-button add-btn ui-Shadow-Main"></button>
+          <button class="ss-reset-button buy-btn ui-Shadow-Main"></button>
+        </view>
+      </view>
+    </su-fixed>
+  </view>
+</template>
+
+<script setup>
+  import { computed } from 'vue';
+  import sheep from '@/sheep';
+
+  const sys = computed(() => sheep.$store('sys'));
+</script>
+
+<style lang="scss" scoped>
+  @keyframes loading {
+    0% {
+      opacity: 0.5;
+    }
+
+    50% {
+      opacity: 1;
+    }
+
+    100% {
+      opacity: 0.5;
+    }
+  }
+
+  .skeleton-wrap {
+    width: 100%;
+    height: 100vh;
+    position: relative;
+
+    .skeleton-banner {
+      width: 100%;
+      height: calc(100vh - 882rpx);
+      background: #f4f4f4;
+    }
+
+    .container-box {
+      padding: 24rpx 18rpx;
+      margin: 14rpx 20rpx;
+      background: var(--ui-BG);
+      animation: loading 1.4s ease infinite;
+
+      .container-box-strip {
+        height: 40rpx;
+        background: #f3f3f1;
+        border-radius: 20rpx;
+      }
+
+      .title {
+        width: 470rpx;
+        height: 50rpx;
+        border-radius: 25rpx;
+      }
+
+      .w-364 {
+        width: 364rpx;
+      }
+
+      .w-380 {
+        width: 380rpx;
+      }
+
+      .w-556 {
+        width: 556rpx;
+      }
+
+      .w-198 {
+        width: 198rpx;
+      }
+
+      .w-252 {
+        width: 252rpx;
+      }
+
+      .circle {
+        width: 40rpx;
+        height: 40rpx;
+        background: #f3f3f1;
+        border-radius: 50%;
+      }
+    }
+    .ui-tabbar-box {
+      box-shadow: 0px -6px 10px 0px rgba(51, 51, 51, 0.2);
+    }
+
+    .foot {
+      height: 100rpx;
+      background: var(--ui-BG);
+      .rec {
+        width: 38rpx;
+        height: 38rpx;
+        background: #f3f3f1;
+        border-radius: 8rpx;
+      }
+
+      .oval {
+        width: 38rpx;
+        height: 22rpx;
+        background: #f3f3f1;
+        border-radius: 11rpx;
+      }
+      .add-btn {
+        width: 214rpx;
+        height: 72rpx;
+        font-weight: 500;
+        font-size: 28rpx;
+        border-radius: 40rpx 0 0 40rpx;
+        background-color: var(--ui-BG-Main-light);
+        color: var(--ui-BG-Main);
+      }
+
+      .buy-btn {
+        width: 214rpx;
+        height: 72rpx;
+        font-weight: 500;
+        font-size: 28rpx;
+
+        border-radius: 0 40rpx 40rpx 0;
+        background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
+        color: $white;
+      }
+    }
+  }
+</style>

+ 169 - 0
pages/goods/components/detail/detail-tabbar.vue

@@ -0,0 +1,169 @@
+<!-- 商品详情的底部导航 -->
+<template>
+  <su-fixed bottom placeholder bg="bg-white">
+    <view class="ui-tabbar-box">
+      <view class="ui-tabbar ss-flex ss-col-center ss-row-between">
+        <view
+          v-if="collectIcon"
+          class="detail-tabbar-item ss-flex ss-flex-col ss-row-center ss-col-center"
+          @tap="onFavorite"
+        >
+          <block v-if="modelValue.favorite">
+            <image
+              class="item-icon"
+              :src="sheep.$url.static('/static/img/shop/goods/collect_1.gif')"
+              mode="aspectFit"
+            />
+            <view class="item-title">已收藏</view>
+          </block>
+          <block v-else>
+            <image
+              class="item-icon"
+              :src="sheep.$url.static('/static/img/shop/goods/collect_0.png')"
+              mode="aspectFit"
+            />
+            <view class="item-title">收藏</view>
+          </block>
+        </view>
+        <view
+          v-if="serviceIcon"
+          class="detail-tabbar-item ss-flex ss-flex-col ss-row-center ss-col-center"
+          @tap="onChat"
+        >
+          <image
+            class="item-icon"
+            :src="sheep.$url.static('/static/img/shop/goods/message.png')"
+            mode="aspectFit"
+          />
+          <view class="item-title">客服</view>
+        </view>
+        <view
+          v-if="shareIcon"
+          class="detail-tabbar-item ss-flex ss-flex-col ss-row-center ss-col-center"
+          @tap="showShareModal"
+        >
+          <image
+            class="item-icon"
+            :src="sheep.$url.static('/static/img/shop/goods/share.png')"
+            mode="aspectFit"
+          />
+          <view class="item-title">分享</view>
+        </view>
+        <slot></slot>
+      </view>
+    </view>
+  </su-fixed>
+</template>
+
+<script setup>
+  /**
+   *
+   * 底部导航
+   *
+   * @property {String} bg 			 			- 背景颜色Class
+   * @property {String} ui 			 			- 自定义样式Class
+   * @property {Boolean} noFixed 		 			- 是否定位
+   * @property {Boolean} topRadius 		 		- 上圆角
+   */
+  import { reactive } from 'vue';
+  import sheep from '@/sheep';
+  import { showShareModal } from '@/sheep/hooks/useModal';
+  import FavoriteApi from '@/sheep/api/product/favorite';
+
+  // 数据
+  const state = reactive({});
+
+  // 接收参数
+  const props = defineProps({
+    modelValue: {
+      type: Object,
+      default() {},
+    },
+    bg: {
+      type: String,
+      default: 'bg-white',
+    },
+    bgStyles: {
+      type: Object,
+      default() {},
+    },
+    ui: {
+      type: String,
+      default: '',
+    },
+
+    noFixed: {
+      type: Boolean,
+      default: false,
+    },
+    topRadius: {
+      type: Number,
+      default: 0,
+    },
+    collectIcon: {
+      type: Boolean,
+      default: true,
+    },
+    serviceIcon: {
+      type: Boolean,
+      default: true,
+    },
+    shareIcon: {
+      type: Boolean,
+      default: true,
+    },
+  });
+
+  async function onFavorite() {
+    // 情况一:取消收藏
+    if (props.modelValue.favorite) {
+      const { code } = await FavoriteApi.deleteFavorite(props.modelValue.id);
+      if (code !== 0) {
+        return
+      }
+      sheep.$helper.toast('取消收藏');
+      props.modelValue.favorite = false;
+    // 情况二:添加收藏
+    } else {
+      const { code } = await FavoriteApi.createFavorite(props.modelValue.id);
+      if (code !== 0) {
+        return
+      }
+      sheep.$helper.toast('收藏成功');
+      props.modelValue.favorite = true;
+    }
+  }
+
+  const onChat = () => {
+    sheep.$router.go('/pages/chat/index', {
+      id: props.modelValue.id,
+    });
+  };
+</script>
+
+<style lang="scss" scoped>
+  .ui-tabbar-box {
+    box-shadow: 0px -6px 10px 0px rgba(51, 51, 51, 0.2);
+  }
+  .ui-tabbar {
+    display: flex;
+    height: 50px;
+    background: #fff;
+
+    .detail-tabbar-item {
+      width: 100rpx;
+
+      .item-icon {
+        width: 40rpx;
+        height: 40rpx;
+      }
+
+      .item-title {
+        font-size: 20rpx;
+        font-weight: 500;
+        line-height: 20rpx;
+        margin-top: 12rpx;
+      }
+    }
+  }
+</style>

+ 141 - 0
pages/goods/components/groupon/groupon-card-list.vue

@@ -0,0 +1,141 @@
+<!-- 拼团活动参团记录卡片 -->
+<template>
+  <view v-if="state.list.length > 0" class="groupon-list detail-card ss-p-x-20">
+    <view class="join-activity ss-flex ss-row-between ss-m-t-30">
+      <!-- todo: 接口缺少总数 -->
+      <view class="">已有{{ state.list.length }}人参与活动</view>
+      <text class="cicon-forward"></text>
+    </view>
+    <view
+      v-for="(record, index) in state.list"
+      @tap="sheep.$router.go('/pages/activity/groupon/detail', { id: record.id })"
+      :key="index"
+      class="ss-m-t-40 ss-flex ss-row-between border-bottom ss-p-b-30"
+    >
+      <view class="ss-flex ss-col-center">
+        <image :src="sheep.$url.cdn(record.avatar)" class="user-avatar"></image>
+        <view class="user-nickname ss-m-l-20 ss-line-1">{{ record.nickname }}</view>
+      </view>
+      <view class="ss-flex ss-col-center">
+        <view class="ss-flex-col ss-col-bottom ss-m-r-20">
+          <view class="title ss-flex ss-m-b-14">
+            还差
+            <view class="num">{{ record.userSize - record.userCount }}人</view>
+            成团
+          </view>
+          <view class="end-time">{{ endTime(record.expireTime) }}</view>
+        </view>
+        <view class="">
+          <button class="ss-reset-button go-btn" @tap.stop="onJoinGroupon(record)"> 去参团 </button>
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup>
+  import { onMounted, reactive } from 'vue';
+  import sheep from '@/sheep';
+  import { useDurationTime } from '@/sheep/hooks/useGoods';
+  import CombinationApi from "@/sheep/api/promotion/combination";
+
+  const props = defineProps({
+    modelValue: {
+      type: Object,
+      default() {},
+    },
+  });
+  const state = reactive({
+    list: [],
+  });
+
+  // 去参团
+  const emits = defineEmits(['join']);
+  function onJoinGroupon(record) {
+    emits('join', record);
+  }
+
+  // 结束时间或状态
+  function endTime(time) {
+    const durationTime = useDurationTime(time);
+
+    if (durationTime.ms <= 0) {
+      return '该团已解散';
+    }
+
+    let timeText = '剩余 ';
+    timeText += `${durationTime.h}时`;
+    timeText += `${durationTime.m}分`;
+    timeText += `${durationTime.s}秒`;
+    return timeText;
+  }
+
+  // 初始化
+  onMounted(async () => {
+    // 查询参团记录
+    // status = 0 表示未成团
+    const { data } = await CombinationApi.getHeadCombinationRecordList(props.modelValue.id, 0 , 10);
+    state.list = data;
+  });
+</script>
+
+<style lang="scss" scoped>
+  .detail-card {
+    background-color: $white;
+    margin: 14rpx 20rpx;
+    border-radius: 10rpx;
+    overflow: hidden;
+  }
+  .groupon-list {
+    .join-activity {
+      font-size: 28rpx;
+      font-weight: 500;
+      color: #999999;
+
+      .cicon-forward {
+        font-weight: 400;
+      }
+    }
+
+    .user-avatar {
+      width: 60rpx;
+      height: 60rpx;
+      background: #ececec;
+      border-radius: 60rpx;
+    }
+
+    .user-nickname {
+      font-size: 28rpx;
+      font-weight: 500;
+      color: #333333;
+      width: 160rpx;
+    }
+
+    .title {
+      font-size: 24rpx;
+      font-weight: 500;
+      color: #666666;
+
+      .num {
+        color: #ff6000;
+      }
+    }
+
+    .end-time {
+      font-size: 24rpx;
+      font-weight: 500;
+      color: #999999;
+    }
+
+    .go-btn {
+      width: 140rpx;
+      height: 60rpx;
+      background: linear-gradient(90deg, #ff6000 0%, #fe832a 100%);
+      border-radius: 30rpx;
+      color: #fff;
+      font-weight: 500;
+      font-size: 26rpx;
+      line-height: normal;
+    }
+  }
+</style>

+ 103 - 0
pages/goods/components/list/list-goods-card.vue

@@ -0,0 +1,103 @@
+<!-- 页面;暂时没用到  -->
+<template>
+  <view class="list-goods-card ss-flex-col" @tap="onClick">
+    <view class="md-img-box">
+      <image class="goods-img md-img-box" :src="sheep.$url.cdn(img)" mode="aspectFill"></image>
+    </view>
+    <view class="md-goods-content ss-flex-col ss-row-around">
+      <view class="md-goods-title ss-line-2 ss-m-x-20 ss-m-t-6 ss-m-b-16">{{ title }}</view>
+      <view class="md-goods-subtitle ss-line-1 ss-p-y-10 ss-p-20">{{ subTitle }}</view>
+      <view class="ss-flex ss-col-center ss-row-between ss-m-b-16 ss-m-x-20">
+        <view class="md-goods-price text-price">{{ price }}</view>
+        <view class="goods-origin-price text-price">{{ originPrice }}</view>
+        <view class="sales-text">已售{{ sales }}件</view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup>
+  import sheep from '@/sheep';
+  import { onLoad } from '@dcloudio/uni-app';
+  import { computed, reactive } from 'vue';
+
+  const props = defineProps({
+    img: {
+      type: String,
+      default: '',
+    },
+    subTitle: {
+      type: String,
+      default: '',
+    },
+    title: {
+      type: String,
+      default: '',
+    },
+    price: {
+      type: [String, Number],
+      default: '',
+    },
+    originPrice: {
+      type: [String, Number],
+      default: '',
+    },
+    sales: {
+      type: [String, Number],
+      default: '',
+    },
+  });
+  const emits = defineEmits(['click']);
+  const onClick = () => {
+    emits('click');
+  };
+</script>
+
+<style lang="scss" scoped>
+  .goods-img {
+    width: 100%;
+    height: 100%;
+    background-color: #f5f5f5;
+  }
+
+  .sales-text {
+    font-size: 20rpx;
+    color: #c4c4c4;
+  }
+
+  .goods-origin-price {
+    font-size: 20rpx;
+    color: #c4c4c4;
+    text-decoration: line-through;
+  }
+
+  .list-goods-card {
+    overflow: hidden;
+    width: 344rpx;
+    position: relative;
+    z-index: 1;
+    background-color: $white;
+    box-shadow: 0 0 20rpx 4rpx rgba(199, 199, 199, 0.22);
+    border-radius: 20rpx;
+
+    .md-img-box {
+      width: 344rpx;
+      height: 344rpx;
+    }
+
+    .md-goods-title {
+      font-size: 26rpx;
+      color: #333;
+    }
+    .md-goods-subtitle {
+      background-color: var(--ui-BG-Main-tag);
+      color: var(--ui-BG-Main);
+      font-size: 20rpx;
+    }
+
+    .md-goods-price {
+      font-size: 30rpx;
+      color: $red;
+    }
+  }
+</style>

+ 93 - 0
pages/goods/components/list/list-navbar.vue

@@ -0,0 +1,93 @@
+<!-- 页面;暂时没用到  -->
+<template>
+  <su-fixed
+    alway
+    :bgStyles="{ background: '#fff' }"
+    :val="0"
+    noNav
+    :opacity="false"
+    placeholder
+    index="10090"
+  >
+    <su-status-bar />
+    <view
+      class="ui-bar ss-flex ss-col-center ss-row-between ss-p-x-20"
+      :style="[{ height: sys_navBar - sys_statusBar + 'px' }]"
+    >
+      <!-- 左 -->
+      <view class="left-box">
+        <text
+          class="_icon-back back-icon"
+          @tap="toBack"
+          :style="[{ color: state.iconColor }]"
+        ></text>
+      </view>
+      <!-- 中 -->
+      <uni-search-bar
+        class="ss-flex-1"
+        radius="33"
+        :placeholder="placeholder"
+        cancelButton="none"
+        :focus="true"
+        v-model="state.searchVal"
+        @confirm="onSearch"
+      />
+      <!-- 右 -->
+      <view class="right">
+        <text class="sicon-more" :style="[{ color: state.iconColor }]" @tap="showMenuTools" />
+      </view>
+      <!-- #ifdef MP -->
+      <view :style="[capsuleStyle]"></view>
+      <!-- #endif -->
+    </view>
+  </su-fixed>
+</template>
+
+<script setup>
+  import { reactive } from 'vue';
+  import sheep from '@/sheep';
+  import { showMenuTools } from '@/sheep/hooks/useModal';
+
+  const sys_statusBar = sheep.$platform.device.statusBarHeight;
+  const sys_navBar = sheep.$platform.navbar;
+  const capsuleStyle = {
+    width: sheep.$platform.capsule.width + 'px',
+    height: sheep.$platform.capsule.height + 'px',
+    margin: '0 ' + (sheep.$platform.device.windowWidth - sheep.$platform.capsule.right) + 'px',
+  };
+
+  const state = reactive({
+    iconColor: '#000',
+    searchVal: '',
+  });
+
+  const props = defineProps({
+    placeholder: {
+      type: String,
+      default: '搜索内容',
+    },
+  });
+
+  const emits = defineEmits(['searchConfirm']);
+
+  // 返回
+  const toBack = () => {
+    sheep.$router.back();
+  };
+
+  // 搜索
+  const onSearch = () => {
+    emits('searchConfirm', state.searchVal);
+  };
+
+  const onTab = (item) => {};
+</script>
+
+<style lang="scss" scoped>
+  .back-icon {
+    font-size: 40rpx;
+  }
+  .sicon-more {
+    font-size: 48rpx;
+  }
+</style>

+ 550 - 0
pages/goods/groupon.vue

@@ -0,0 +1,550 @@
+<!-- 拼团商品详情 -->
+<template>
+  <s-layout :onShareAppMessage="shareInfo" navbar="goods">
+    <!-- 标题栏 -->
+    <detailNavbar />
+    <!-- 骨架屏 -->
+    <detailSkeleton v-if="state.skeletonLoading" />
+    <!-- 下架/售罄提醒 -->
+    <s-empty
+      v-else-if="
+        state.goodsInfo === null ||
+        state.activity.status !== 0 ||
+        state.activity.endTime < new Date().getTime()
+      "
+      text="活动不存在或已结束"
+      icon="/static/soldout-empty.png"
+      showAction
+      actionText="返回上一页"
+      @clickAction="sheep.$router.back()"
+    />
+    <block v-else>
+      <view class="detail-swiper-selector">
+        <!-- 商品图轮播 -->
+        <su-swiper
+          class="ss-m-b-14"
+          isPreview
+          :list="state.goodsSwiper"
+          dotStyle="tag"
+          imageMode="widthFix"
+          dotCur="bg-mask-40"
+          :seizeHeight="750"
+        />
+
+        <!-- 价格+标题 -->
+        <view class="title-card detail-card ss-m-y-14 ss-m-x-20 ss-p-x-20 ss-p-y-34">
+          <view class="ss-flex ss-row-between ss-m-b-60">
+            <view>
+              <view class="price-box ss-flex ss-col-bottom ss-m-b-18">
+                <view class="price-text ss-m-r-16">
+                  {{ fen2yuan(state.activity.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">
+                    <view class="groupon-tag">
+                      <image
+                        :src="sheep.$url.static('/static/img/shop/goods/groupon-tag.png')"
+                      ></image>
+                    </view>
+                  </view>
+                  <view class="tig-title">拼团价</view>
+                </view>
+              </view>
+              <view class="ss-flex ss-row-between">
+                <view class="origin-price ss-flex ss-col-center" v-if="state.goodsInfo.price">
+                  单买价:
+                  <view class="origin-price-text">
+                    {{ fen2yuan(state.goodsInfo.marketPrice) }}
+                  </view>
+                </view>
+              </view>
+            </view>
+
+            <view class="countdown-box" v-if="endTime.ms > 0">
+              <view class="countdown-title ss-m-b-20">距结束仅剩</view>
+              <view class="ss-flex countdown-time">
+                <view class="ss-flex countdown-h">{{ endTime.h }}</view>
+                <view class="ss-m-x-4">:</view>
+                <view class="countdown-num ss-flex ss-row-center">{{ endTime.m }}</view>
+                <view class="ss-m-x-4">:</view>
+                <view class="countdown-num ss-flex ss-row-center">{{ endTime.s }}</view>
+              </view>
+            </view>
+            <view class="countdown-title" v-else> 活动已结束 </view>
+          </view>
+
+          <view class="title-text ss-line-2 ss-m-b-6">{{ state.goodsInfo.name }}</view>
+          <view class="subtitle-text ss-line-1">{{ state.goodsInfo.introduction }}</view>
+        </view>
+
+        <!-- 功能卡片 -->
+        <view class="detail-cell-card detail-card ss-flex-col">
+          <!-- 规格 -->
+          <detail-cell-sku :sku="state.selectedSku" @tap="state.showSelectSku = true" />
+        </view>
+
+        <!-- 参团列表 -->
+        <groupon-card-list v-model="state.activity" @join="onJoinGroupon" />
+
+        <!-- 规格与数量弹框 -->
+        <s-select-groupon-sku
+          :show="state.showSelectSku"
+          :goodsInfo="state.goodsInfo"
+          :grouponAction="state.grouponAction"
+          :grouponNum="state.grouponNum"
+          @buy="onBuy"
+          @change="onSkuChange"
+          @close="onSkuClose"
+        />
+      </view>
+
+      <!-- 评价 -->
+      <detail-comment-card class="detail-comment-selector" :goodsId="state.goodsId" />
+      <!-- 详情 -->
+      <detail-content-card class="detail-content-selector" :content="state.goodsInfo.description" />
+
+      <!-- 商品tabbar -->
+      <detail-tabbar v-model="state.goodsInfo">
+        <view class="buy-box ss-flex ss-col-center ss-p-r-20">
+          <button
+            class="ss-reset-button origin-price-btn ss-flex-col"
+            @tap="sheep.$router.go('/pages/goods/index', { id: state.goodsInfo.id })"
+          >
+            <view class="btn-price">{{ fen2yuan(state.goodsInfo.marketPrice) }}</view>
+            <view>原价购买</view>
+          </button>
+          <button
+            class="ss-reset-button btn-tox ss-flex-col"
+            @tap="onCreateGroupon"
+            :class="
+              state.activity.status === 0 && state.goodsInfo.stock !== 0
+                ? 'check-btn-box'
+                : 'disabled-btn-box'
+            "
+            :disabled="state.goodsInfo.stock === 0 || state.activity.status !== 0"
+          >
+            <view class="btn-price">{{
+              fen2yuan(
+                state.selectedSku.price * state.selectedSku.count ||
+                  state.activity.price * state.selectedSku.count ||
+                  state.goodsInfo.price * state.selectedSku.count ||
+                  state.goodsInfo.price,
+              )
+            }}</view>
+            <view v-if="state.activity.startTime > new Date().getTime()">未开始</view>
+            <view v-else-if="state.activity.endTime <= new Date().getTime()">已结束</view>
+            <view v-else>
+              <view v-if="state.goodsInfo.stock === 0">已售罄</view>
+              <view v-else>立即开团</view>
+            </view>
+          </button>
+        </view>
+      </detail-tabbar>
+    </block>
+  </s-layout>
+</template>
+
+<script setup>
+  import { reactive, computed } from 'vue';
+  import { onLoad, onPageScroll } from '@dcloudio/uni-app';
+  import sheep from '@/sheep';
+  import { isEmpty } from 'lodash-es';
+  import detailNavbar from './components/detail/detail-navbar.vue';
+  import detailCellSku from './components/detail/detail-cell-sku.vue';
+  import detailTabbar from './components/detail/detail-tabbar.vue';
+  import detailSkeleton from './components/detail/detail-skeleton.vue';
+  import detailCommentCard from './components/detail/detail-comment-card.vue';
+  import detailContentCard from './components/detail/detail-content-card.vue';
+  import grouponCardList from './components/groupon/groupon-card-list.vue';
+  import { useDurationTime, formatGoodsSwiper, fen2yuan } from '@/sheep/hooks/useGoods';
+  import CombinationApi from '@/sheep/api/promotion/combination';
+  import SpuApi from '@/sheep/api/product/spu';
+
+  const headerBg = sheep.$url.css('/static/img/shop/goods/groupon-bg.png');
+  const btnBg = sheep.$url.css('/static/img/shop/goods/groupon-btn.png');
+  const disabledBtnBg = sheep.$url.css('/static/img/shop/goods/activity-btn-disabled.png');
+  const grouponBg = sheep.$url.css('/static/img/shop/goods/groupon-tip-bg.png');
+
+  onPageScroll(() => {});
+  const state = reactive({
+    skeletonLoading: true, // 骨架屏
+    goodsId: 0, // 商品ID
+    goodsInfo: {}, // 商品信息
+    goodsSwiper: [], // 商品轮播图
+    showSelectSku: false, // 显示规格弹框
+    selectedSku: {}, // 选中的规格属性
+    activity: {}, // 团购活动
+    grouponId: 0, // 团购ID
+    grouponNum: 0, // 团购人数
+    grouponAction: 'create', // 团购操作
+    combinationHeadId: null, // 拼团团长编号
+  });
+
+  // 倒计时
+  const endTime = computed(() => {
+    return useDurationTime(state.activity.endTime);
+  });
+
+  // 规格变更
+  function onSkuChange(e) {
+    state.selectedSku = e;
+  }
+
+  function onSkuClose() {
+    state.showSelectSku = false;
+  }
+
+  // 发起拼团
+  function onCreateGroupon() {
+    state.grouponAction = 'create';
+    state.grouponId = 0;
+    state.showSelectSku = true;
+  }
+
+  /**
+   * 去参团
+   *
+   * @param record 团长的团购记录
+   */
+  function onJoinGroupon(record) {
+    state.grouponAction = 'join';
+    state.grouponId = record.activityId;
+    state.combinationHeadId = record.id;
+    state.grouponNum = record.userSize;
+    state.showSelectSku = true;
+  }
+
+  // 立即购买
+  function onBuy(sku) {
+    sheep.$router.go('/pages/order/confirm', {
+      data: JSON.stringify({
+        order_type: 'goods',
+        combinationActivityId: state.activity.id,
+        combinationHeadId: state.combinationHeadId,
+        items: [
+          {
+            skuId: sku.id,
+            count: sku.count,
+          },
+        ],
+      }),
+    });
+  }
+
+  // 分享信息
+  const shareInfo = computed(() => {
+    if (isEmpty(state.activity)) return {};
+    return sheep.$platform.share.getShareInfo(
+      {
+        title: state.activity.name,
+        image: sheep.$url.cdn(state.goodsInfo.picUrl),
+        params: {
+          page: '3',
+          query: state.activity.id,
+        },
+      },
+      {
+        type: 'goods', // 商品海报
+        title: state.activity.name, // 商品标题
+        image: sheep.$url.cdn(state.goodsInfo.picUrl), // 商品主图
+        price: fen2yuan(state.goodsInfo.price), // 商品价格
+        marketPrice: fen2yuan(state.goodsInfo.marketPrice), // 商品原价
+      },
+    );
+  });
+
+  onLoad(async (options) => {
+    // 非法参数
+    if (!options.id) {
+      state.goodsInfo = null;
+      return;
+    }
+    state.grouponId = options.id;
+    // 加载活动信息
+    const { code, data: activity } = await CombinationApi.getCombinationActivity(state.grouponId);
+    state.activity = activity;
+    // 加载商品信息
+    const { data: spu } = await SpuApi.getSpuDetail(activity.spuId);
+    state.goodsId = spu.id;
+
+    // 默认显示最低价
+    spu.price = activity.products.reduce((min, product) => {
+      return Math.min(min, product.combinationPrice || Infinity);
+    }, Infinity);
+
+    // 价格、库存使用活动的
+    spu.skus.forEach((sku) => {
+      const product = activity.products.find((product) => product.skuId === sku.id);
+      if (product) {
+        sku.price = product.combinationPrice;
+      } else {
+        // 找不到可能是没配置,则不能发起秒杀
+        sku.stock = 0;
+      }
+    });
+
+    // 关闭骨架屏
+    state.skeletonLoading = false;
+    if (code === 0) {
+      state.goodsInfo = spu;
+      state.grouponNum = activity.userSize;
+      state.goodsSwiper = formatGoodsSwiper(state.goodsInfo.sliderPicUrls);
+    } else {
+      // 未找到商品
+      state.goodsInfo = null;
+    }
+  });
+</script>
+
+<style lang="scss" scoped>
+  .detail-card {
+    background-color: $white;
+    margin: 14rpx 20rpx;
+    border-radius: 10rpx;
+    overflow: hidden;
+  }
+
+  // 价格标题卡片
+  .title-card {
+    width: 710rpx;
+    box-sizing: border-box;
+    // height: 320rpx;
+    background-size: 100% 100%;
+    border-radius: 10rpx;
+    background-image: v-bind(headerBg);
+    background-repeat: no-repeat;
+
+    .price-box {
+      .price-text {
+        font-size: 30rpx;
+        font-weight: 500;
+        color: #fff;
+        line-height: normal;
+        font-family: OPPOSANS;
+
+        &::before {
+          content: '¥';
+          font-size: 30rpx;
+        }
+      }
+    }
+    .origin-price {
+      font-size: 24rpx;
+      font-weight: 400;
+      color: #fff;
+      opacity: 0.7;
+
+      .origin-price-text {
+        text-decoration: line-through;
+
+        font-family: OPPOSANS;
+
+        &::before {
+          content: '¥';
+        }
+      }
+    }
+
+    .tig {
+      border: 2rpx solid #ffffff;
+      border-radius: 4rpx;
+      width: 126rpx;
+      height: 38rpx;
+
+      .tig-icon {
+        margin-left: -2rpx;
+        width: 40rpx;
+        height: 40rpx;
+        background: #ffffff;
+        border-radius: 4rpx 0 0 4rpx;
+
+        .groupon-tag {
+          width: 32rpx;
+          height: 32rpx;
+        }
+      }
+
+      .tig-title {
+        font-size: 24rpx;
+        font-weight: 500;
+        line-height: normal;
+        color: #ffffff;
+        width: 86rpx;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+      }
+    }
+
+    .countdown-title {
+      font-size: 26rpx;
+      font-weight: 500;
+      color: #ffffff;
+    }
+
+    .countdown-time {
+      font-size: 26rpx;
+      font-weight: 500;
+      color: #ffffff;
+      .countdown-h {
+        font-size: 24rpx;
+        font-family: OPPOSANS;
+        font-weight: 500;
+        color: #ffffff;
+        padding: 0 4rpx;
+        height: 40rpx;
+        background: rgba(#000000, 0.1);
+        border-radius: 6rpx;
+      }
+      .countdown-num {
+        font-size: 24rpx;
+        font-family: OPPOSANS;
+        font-weight: 500;
+        color: #ffffff;
+        width: 40rpx;
+        height: 40rpx;
+        background: rgba(#000000, 0.1);
+        border-radius: 6rpx;
+      }
+    }
+
+    .title-text {
+      font-size: 30rpx;
+      font-weight: bold;
+      line-height: 42rpx;
+      color: #fff;
+    }
+
+    .subtitle-text {
+      font-size: 26rpx;
+      font-weight: 400;
+      color: #ffffff;
+      line-height: 42rpx;
+      opacity: 0.9;
+    }
+  }
+
+  // 购买
+  .buy-box {
+    .disabled-btn-box[disabled] {
+      background-color: transparent;
+    }
+    .check-btn-box {
+      width: 248rpx;
+      height: 80rpx;
+      font-size: 24rpx;
+      font-weight: 600;
+      margin-left: -36rpx;
+      background-image: v-bind(btnBg);
+      background-repeat: no-repeat;
+      background-size: 100% 100%;
+      color: #ffffff;
+      line-height: normal;
+      border-radius: 0px 40rpx 40rpx 0px;
+    }
+    .disabled-btn-box {
+      width: 248rpx;
+      height: 80rpx;
+      font-size: 24rpx;
+      font-weight: 600;
+      margin-left: -36rpx;
+      background-image: v-bind(disabledBtnBg);
+      background-repeat: no-repeat;
+      background-size: 100% 100%;
+      color: #999999;
+      line-height: normal;
+      border-radius: 0px 40rpx 40rpx 0px;
+    }
+
+    .origin-price-btn {
+      width: 236rpx;
+      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-title {
+        font-size: 28rpx;
+      }
+    }
+    .btn-price {
+      font-family: OPPOSANS;
+
+      &::before {
+        content: '¥';
+      }
+    }
+    .more-item-box {
+      .more-item {
+        width: 156rpx;
+        height: 58rpx;
+        font-size: 26rpx;
+        font-weight: 500;
+        color: #999999;
+        border-radius: 10rpx;
+      }
+      .more-item-hover {
+        background: rgba(#ffefe5, 0.32);
+        color: #ff6000;
+      }
+    }
+  }
+
+  .groupon-box {
+    background: v-bind(grouponBg) no-repeat;
+    background-size: 100% 100%;
+  }
+
+  //活动卡片
+  .activity-box {
+    width: 100%;
+    height: 80rpx;
+    box-sizing: border-box;
+    margin-bottom: 10rpx;
+
+    .activity-title {
+      font-size: 26rpx;
+      font-weight: 500;
+      color: #ffffff;
+      line-height: 42rpx;
+
+      .activity-icon {
+        width: 38rpx;
+        height: 38rpx;
+      }
+    }
+
+    .activity-go {
+      width: 70rpx;
+      height: 32rpx;
+      background: #ffffff;
+      border-radius: 16rpx;
+      font-weight: 500;
+      color: #ff6000;
+      font-size: 24rpx;
+      line-height: normal;
+    }
+  }
+
+  .model-box {
+    .title {
+      font-size: 36rpx;
+      font-weight: bold;
+      color: #333333;
+    }
+
+    .subtitle {
+      font-size: 26rpx;
+      font-weight: 500;
+      color: #333333;
+    }
+  }
+
+  image {
+    width: 100%;
+    height: 100%;
+  }
+</style>

+ 669 - 0
pages/goods/index.vue

@@ -0,0 +1,669 @@
+<template>
+  <view>
+    <s-layout :onShareAppMessage="shareInfo" navbar="goods">
+      <!-- 标题栏 -->
+      <detailNavbar />
+
+      <!-- 骨架屏 -->
+      <detailSkeleton v-if="state.skeletonLoading" />
+      <!-- 下架/售罄提醒 -->
+      <s-empty
+        v-else-if="state.goodsInfo === null"
+        text="商品不存在或已下架"
+        icon="/static/soldout-empty.png"
+        showAction
+        actionText="再逛逛"
+        actionUrl="/pages/goods/list"
+      />
+      <block v-else>
+        <view class="detail-swiper-selector">
+          <!-- 商品轮播图  -->
+          <su-swiper
+            class="ss-m-b-14"
+            isPreview
+            :list="formatGoodsSwiper(state.goodsInfo.sliderPicUrls)"
+            otStyle="tag"
+            imageMode="widthFix"
+            dotCur="bg-mask-40"
+            :seizeHeight="750"
+          />
+          <!-- 限时折扣/会员价的优惠信息 -->
+          <view
+            class="discount"
+            v-if="
+              state.settlementSku && state.settlementSku.id && state.settlementSku.promotionPrice
+            "
+          >
+            <image class="disImg" :src="sheep.$url.static('/static/img/shop/goods/dis.png')" />
+            <view class="discountCont">
+              <view class="disContT">
+                <view class="disContT1">
+                  <view class="disContT1P">
+                    ¥{{ fen2yuan(state.settlementSku.promotionPrice) }}
+                  </view>
+                  <view class="disContT1End">
+                    直降¥
+                    {{ fen2yuan(state.settlementSku.price - state.settlementSku.promotionPrice) }}
+                  </view>
+                </view>
+                <view class="disContT2" v-if="state.settlementSku.promotionType === 4">
+                  限时折扣
+                </view>
+                <view class="disContT2" v-else-if="state.settlementSku.promotionType === 6">
+                  会员折扣
+                </view>
+              </view>
+              <view class="disContB">
+                <view class="disContB1">
+                  价格:¥{{ fen2yuan(state.settlementSku.price) }} 丨 剩余:
+                  {{ state.settlementSku.stock }}
+                </view>
+                <view class="disContB2" v-if="state.settlementSku.promotionEndTime > 0">
+                  距结束仅剩
+                  <countDown
+                    :tipText="' '"
+                    :bgColor="bgColor"
+                    :dayText="':'"
+                    :hourText="':'"
+                    :minuteText="':'"
+                    :secondText="' '"
+                    :datatime="state.settlementSku.promotionEndTime / 1000"
+                    :isDay="false"
+                  />
+                </view>
+              </view>
+            </view>
+          </view>
+          <!-- 价格+标题 -->
+          <view class="title-card detail-card ss-p-y-30 ss-p-x-20">
+            <!-- 没有限时折扣/会员价的优惠信息时,展示的价格信息 -->
+            <view
+              class="ss-flex ss-row-between ss-col-center ss-m-b-26"
+              v-if="!state.settlementSku.promotionPrice"
+            >
+              <view class="price-box ss-flex ss-col-bottom">
+                <view class="price-text ss-m-r-16">
+                  {{ fen2yuan(state.selectedSku.price || state.goodsInfo.price) }}
+                </view>
+                <view class="origin-price-text" v-if="state.goodsInfo.marketPrice > state.goodsInfo.price">
+                  {{ fen2yuan(state.selectedSku.marketPrice || state.goodsInfo.marketPrice) }}
+                </view>
+              </view>
+              <view class="sales-text">
+                {{ formatSales('exact', state.goodsInfo.salesCount) }}
+              </view>
+            </view>
+            <view class="discounts-box ss-flex ss-row-between ss-m-b-28">
+              <!-- 查看优惠劵的描述 -->
+              <view
+                class="tag ss-m-r-10"
+                v-for="coupon in state.couponInfo.slice(0, 1)"
+                :key="coupon.id"
+                @tap="onOpenActivity"
+              >
+                [劵]满{{ fen2yuanSimple(coupon.usePrice) }}元{{
+                  coupon.discountType === 1
+                    ? '减' + fen2yuanSimple(coupon.discountPrice) + '元'
+                    : '打' + formatDiscountPercent(coupon.discountPercent) + '折'
+                }}
+              </view>
+              <!-- 查看满减送的描述 -->
+              <div class="tag-content">
+                <view class="tag-box ss-flex">
+                  <!-- 最多打印 3 条,所以需要扣除优惠劵已打印的 -->
+                  <view
+                    v-for="item in getRewardActivityRuleItemDescriptions(
+                      state.rewardActivity,
+                    ).slice(0, 3 - state.couponInfo.slice(0, 1).length)"
+                    :key="item"
+                    class="tag ss-m-r-10"
+                    @tap="onOpenActivity"
+                  >
+                    <text>{{ item }}</text>
+                  </view>
+                </view>
+              </div>
+              <!-- 领取优惠劵的按钮 -->
+              <view
+                class="get-coupon-box ss-flex ss-col-center ss-m-l-20"
+                @tap="onOpenActivity"
+                v-if="state.couponInfo.length"
+              >
+                <view class="discounts-title ss-m-r-8">领券</view>
+                <text class="cicon-forward"></text>
+              </view>
+            </view>
+            <view class="title-text ss-line-2 ss-m-b-6">{{ state.goodsInfo.name }}</view>
+            <view class="subtitle-text ss-line-1">{{ state.goodsInfo.introduction }}</view>
+          </view>
+
+          <!-- 功能卡片 -->
+          <view class="detail-cell-card detail-card ss-flex-col">
+            <detail-cell-sku
+              v-model="state.selectedSku.goods_sku_text"
+              :sku="state.selectedSku"
+              @tap="state.showSelectSku = true"
+            />
+          </view>
+
+          <!-- 规格与数量弹框 -->
+          <s-select-sku
+            :goodsInfo="state.goodsInfo"
+            :show="state.showSelectSku"
+            @addCart="onAddCart"
+            @buy="onBuy"
+            @change="onSkuChange"
+            @close="state.showSelectSku = false"
+          />
+        </view>
+
+        <!-- 评价 -->
+        <detail-comment-card class="detail-comment-selector" :goodsId="state.goodsId" />
+        <!-- 详情 -->
+        <detail-content-card
+          class="detail-content-selector"
+          :content="state.goodsInfo.description"
+        />
+
+        <!-- 活动跳转:拼团/秒杀/砍价活动 -->
+        <detail-activity-tip
+          v-if="state.activityList.length > 0"
+          :activity-list="state.activityList"
+        />
+
+        <!-- 详情 tabbar -->
+        <detail-tabbar v-model="state.goodsInfo">
+          <view class="buy-box ss-flex ss-col-center ss-p-r-20" v-if="state.goodsInfo.stock > 0">
+            <button
+              class="ss-reset-button add-btn ui-Shadow-Main"
+              @tap="state.showSelectSku = true"
+            >
+              加入购物车
+            </button>
+            <button
+              class="ss-reset-button buy-btn ui-Shadow-Main"
+              @tap="state.showSelectSku = true"
+            >
+              立即购买
+            </button>
+          </view>
+          <view class="buy-box ss-flex ss-col-center ss-p-r-20" v-else>
+            <button class="ss-reset-button disabled-btn" disabled> 已售罄 </button>
+          </view>
+        </detail-tabbar>
+
+        <!-- 满减送/限时折扣活动弹窗 -->
+        <s-activity-pop
+          v-model="state"
+          :show="state.showActivityModel"
+          @close="state.showActivityModel = false"
+          @get="onTakeCoupon"
+        />
+      </block>
+    </s-layout>
+  </view>
+</template>
+
+<script setup>
+  import { reactive, computed, ref, toRaw } from 'vue';
+  import { onLoad, onPageScroll } from '@dcloudio/uni-app';
+  import sheep from '@/sheep';
+  import CouponApi from '@/sheep/api/promotion/coupon';
+  import ActivityApi from '@/sheep/api/promotion/activity';
+  import FavoriteApi from '@/sheep/api/product/favorite';
+  import RewardActivityApi from '@/sheep/api/promotion/rewardActivity';
+  import {
+    formatSales,
+    formatGoodsSwiper,
+    fen2yuan,
+    fen2yuanSimple,
+    formatDiscountPercent,
+    getRewardActivityRuleItemDescriptions,
+  } from '@/sheep/hooks/useGoods';
+  import detailNavbar from './components/detail/detail-navbar.vue';
+  import detailCellSku from './components/detail/detail-cell-sku.vue';
+  import detailTabbar from './components/detail/detail-tabbar.vue';
+  import detailSkeleton from './components/detail/detail-skeleton.vue';
+  import detailCommentCard from './components/detail/detail-comment-card.vue';
+  import detailContentCard from './components/detail/detail-content-card.vue';
+  import detailActivityTip from './components/detail/detail-activity-tip.vue';
+  import { isEmpty } from 'lodash-es';
+  import SpuApi from '@/sheep/api/product/spu';
+
+  onPageScroll(() => {});
+  import countDown from '@/sheep/components/countDown/index.vue';
+  import OrderApi from '@/sheep/api/trade/order';
+  import activity from '@/sheep/api/promotion/activity';
+
+  const bgColor = {
+    bgColor: '#E93323',
+    Color: '#fff',
+    width: '44rpx',
+    timeTxtwidth: '16rpx',
+    isDay: true,
+  };
+  const isLogin = computed(() => sheep.$store('user').isLogin);
+  const state = reactive({
+    goodsId: 0,
+    skeletonLoading: true, // SPU 加载中
+    goodsInfo: {}, // SPU 信息
+    showSelectSku: false, // 是否展示 SKU 选择弹窗
+    selectedSku: {}, // 选中的 SKU
+    settlementSku: {}, // 结算的 SKU:由于 selectedSku 不进行默认选中,所以初始使用结算价格最低的 SKU 作为基础展示
+    showModel: false, // 是否展示 Coupon 优惠劵的弹窗
+    couponInfo: [], // 可领取的 Coupon 优惠劵的列表
+    showActivityModel: false, // 【满减送/限时折扣】是否展示 Activity 营销活动的弹窗
+    rewardActivity: {}, // 【满减送】活动
+    activityList: [], // 【秒杀/拼团/砍价】可参与的 Activity 营销活动的列表
+  });
+
+  // 规格变更
+  function onSkuChange(e) {
+    state.selectedSku = e;
+    state.settlementSku = e;
+  }
+
+  // 添加购物车
+  function onAddCart(e) {
+    if (!e.id) {
+      sheep.$helper.toast('请选择商品规格');
+      return;
+    }
+    sheep.$store('cart').add(e);
+  }
+
+  // 立即购买
+  function onBuy(e) {
+    if (!e.id) {
+      sheep.$helper.toast('请选择商品规格');
+      return;
+    }
+    sheep.$router.go('/pages/order/confirm', {
+      data: JSON.stringify({
+        items: [
+          {
+            skuId: e.id,
+            count: e.goods_num,
+            categoryId: state.goodsInfo.categoryId,
+          },
+        ],
+      }),
+    });
+  }
+
+  // 打开营销弹窗
+  function onOpenActivity() {
+    state.showActivityModel = true;
+  }
+
+  // 立即领取优惠劵
+  async function onTakeCoupon(id) {
+    const { code } = await CouponApi.takeCoupon(id);
+    if (code !== 0) {
+      return;
+    }
+    uni.showToast({
+      title: '领取成功',
+    });
+    setTimeout(() => {
+      getCoupon();
+    }, 1000);
+  }
+
+  const shareInfo = computed(() => {
+    if (isEmpty(state.goodsInfo)) return {};
+    return sheep.$platform.share.getShareInfo(
+      {
+        title: state.goodsInfo.name,
+        image: sheep.$url.cdn(state.goodsInfo.picUrl),
+        desc: state.goodsInfo.introduction,
+        params: {
+          page: '2',
+          query: state.goodsInfo.id,
+        },
+      },
+      {
+        type: 'goods', // 商品海报
+        title: state.goodsInfo.name, // 商品名称
+        image: sheep.$url.cdn(state.goodsInfo.picUrl), // 商品主图
+        price: fen2yuan(state.goodsInfo.price), // 商品价格
+        original_price: fen2yuan(state.goodsInfo.marketPrice), // 商品原价
+      },
+    );
+  });
+
+  async function getCoupon() {
+    const { code, data } = await CouponApi.getCouponTemplateList(state.goodsId, 2, 10);
+    if (code === 0) {
+      state.couponInfo = data;
+    }
+  }
+
+  async function getSettlementByIds(ids) {
+    let { data, code } = await OrderApi.getSettlementProduct(ids);
+    if (code !== 0 || data.length !== 1) {
+      return;
+    }
+    data = data[0];
+
+    // 补充 SKU 的价格信息
+    state.goodsInfo.skus.forEach((sku) => {
+      data.skus.forEach((item) => {
+        if (sku.id === item.id) {
+          sku.promotionType = item.promotionType;
+          sku.promotionPrice = item.promotionPrice;
+          sku.promotionId = item.promotionId;
+          sku.promotionEndTime = item.promotionEndTime;
+        }
+      });
+    });
+
+    // 选择有 promotionPrice 且最小的
+    state.settlementSku = state.goodsInfo.skus
+      .filter((sku) => sku.stock > 0 && sku.promotionPrice > 0)
+      .reduce((prev, curr) => (prev.promotionPrice < curr.promotionPrice ? prev : curr), []);
+
+    // 设置满减送活动
+    if (data.rewardActivity) {
+      state.rewardActivity = data.rewardActivity;
+      //获取活动时间
+      getActivityTime(state.rewardActivity.id);
+    }
+  }
+
+  //获取活动时间
+  async function getActivityTime(id) {
+    const { code, data } = await RewardActivityApi.getRewardActivity(id);
+    if (code === 0) {
+      // console.log('获取到的活动 数据', data)
+      state.rewardActivity.startTime = data.startTime;
+      state.rewardActivity.endTime = data.endTime;
+    }
+  }
+
+  onLoad((options) => {
+    // 非法参数
+    if (!options.id) {
+      state.goodsInfo = null;
+      return;
+    }
+    state.goodsId = options.id;
+    // 1. 加载商品信息
+    SpuApi.getSpuDetail(state.goodsId).then((res) => {
+      // 未找到商品
+      if (res.code !== 0 || !res.data) {
+        state.goodsInfo = null;
+        return;
+      }
+      // 加载到商品
+      state.skeletonLoading = false;
+      state.goodsInfo = res.data;
+      // 加载是否收藏
+      if (isLogin.value) {
+        FavoriteApi.isFavoriteExists(state.goodsId, 'goods').then((res) => {
+          if (res.code !== 0) {
+            return;
+          }
+          state.goodsInfo.favorite = res.data;
+        });
+      }
+    });
+
+    // 2. 加载优惠劵信息
+    getCoupon();
+
+    // 3. 加载营销活动信息
+    ActivityApi.getActivityListBySpuId(state.goodsId).then((res) => {
+      if (res.code !== 0) {
+        return;
+      }
+      state.activityList = res.data;
+    });
+    //获取结算信息
+    getSettlementByIds(state.goodsId);
+  });
+</script>
+
+<style lang="scss" scoped>
+  .detail-card {
+    background-color: #ffff;
+    margin: 14rpx 20rpx;
+    border-radius: 10rpx;
+    overflow: hidden;
+  }
+
+  // 价格标题卡片
+  .title-card {
+    .price-box {
+      .price-text {
+        font-size: 42rpx;
+        font-weight: 500;
+        color: #ff3000;
+        line-height: 30rpx;
+        font-family: OPPOSANS;
+
+        &::before {
+          content: '¥';
+          font-size: 30rpx;
+        }
+      }
+
+      .origin-price-text {
+        font-size: 26rpx;
+        font-weight: 400;
+        text-decoration: line-through;
+        color: $gray-c;
+        font-family: OPPOSANS;
+
+        &::before {
+          content: '¥';
+        }
+      }
+    }
+
+    .sales-text {
+      font-size: 26rpx;
+      font-weight: 500;
+      color: $gray-c;
+    }
+
+    .discounts-box {
+      .tag-content {
+        flex: 1;
+        min-width: 0;
+        white-space: nowrap;
+      }
+
+      .tag-box {
+        overflow: hidden;
+        text-overflow: ellipsis;
+      }
+
+      .tag {
+        flex-shrink: 0;
+        padding: 4rpx 10rpx;
+        font-size: 24rpx;
+        font-weight: 500;
+        border-radius: 4rpx;
+        color: var(--ui-BG-Main);
+        background: var(--ui-BG-Main-tag);
+      }
+
+      .discounts-title {
+        font-size: 24rpx;
+        font-weight: 500;
+        color: var(--ui-BG-Main);
+        line-height: normal;
+      }
+
+      .cicon-forward {
+        color: var(--ui-BG-Main);
+        font-size: 24rpx;
+        line-height: normal;
+        margin-top: 4rpx;
+      }
+    }
+
+    .title-text {
+      font-size: 30rpx;
+      font-weight: bold;
+      line-height: 42rpx;
+    }
+
+    .subtitle-text {
+      font-size: 26rpx;
+      font-weight: 400;
+      color: $dark-9;
+      line-height: 42rpx;
+    }
+  }
+
+  // 购买
+  .buy-box {
+    .add-btn {
+      width: 214rpx;
+      height: 72rpx;
+      font-weight: 500;
+      font-size: 28rpx;
+      border-radius: 40rpx 0 0 40rpx;
+      background-color: var(--ui-BG-Main-light);
+      color: var(--ui-BG-Main);
+    }
+
+    .buy-btn {
+      width: 214rpx;
+      height: 72rpx;
+      font-weight: 500;
+      font-size: 28rpx;
+
+      border-radius: 0 40rpx 40rpx 0;
+      background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
+      color: $white;
+    }
+
+    .disabled-btn {
+      width: 428rpx;
+      height: 72rpx;
+      border-radius: 40rpx;
+      background: #999999;
+      color: $white;
+    }
+  }
+
+  .model-box {
+    height: 60vh;
+
+    .model-content {
+      height: 56vh;
+    }
+
+    .title {
+      font-size: 36rpx;
+      font-weight: bold;
+      color: #333333;
+    }
+
+    .subtitle {
+      font-size: 26rpx;
+      font-weight: 500;
+      color: #333333;
+    }
+  }
+
+  // 限时折扣
+  .discount {
+    width: 750rpx;
+    height: 100rpx;
+    // background-color: red;
+    overflow: hidden;
+    position: relative;
+  }
+
+  .disImg {
+    width: 750rpx;
+    height: 100rpx;
+    position: absolute;
+    top: 0;
+    z-index: -1;
+  }
+
+  .discountCont {
+    width: 680rpx;
+    height: 90rpx;
+    margin: 10rpx auto 0 auto;
+    // background-color: gold;
+  }
+
+  .disContT {
+    width: 680rpx;
+    height: 50rpx;
+    display: flex;
+    justify-content: space-between;
+  }
+
+  .disContT1 {
+    width: 400rpx;
+    height: 50rpx;
+    // background-color: green;
+    display: flex;
+    justify-content: flex-start;
+    align-items: center;
+  }
+
+  .disContT2 {
+    width: 200rpx;
+    height: 50rpx;
+    line-height: 50rpx;
+    // background-color: gold;
+    font-size: 30rpx;
+    text-align: end;
+    color: white;
+    font-weight: bolder;
+    font-style: oblique 20deg;
+    letter-spacing: 0.1rem;
+  }
+
+  .disContT1P {
+    color: white;
+    font-weight: bold;
+    font-size: 28rpx;
+  }
+
+  .disContT1End {
+    // width: 180rpx;
+    padding: 0 10rpx;
+    height: 30rpx;
+    line-height: 28rpx;
+    text-align: center;
+    font-weight: bold;
+    background-color: white;
+    color: #ff3000;
+    font-size: 23rpx;
+    border-radius: 20rpx;
+    margin-left: 10rpx;
+  }
+
+  .disContB {
+    width: 680rpx;
+    height: 40rpx;
+    display: flex;
+    justify-content: space-between;
+    font-size: 20rpx;
+    color: white;
+    align-items: center;
+  }
+
+  .disContB1 {
+    width: 300rpx;
+    height: 40rpx;
+    line-height: 40rpx;
+  }
+
+  .disContB2 {
+    width: 300rpx;
+    height: 40rpx;
+    line-height: 40rpx;
+    display: flex;
+    justify-content: flex-end;
+  }
+</style>

+ 407 - 0
pages/goods/list.vue

@@ -0,0 +1,407 @@
+<template>
+  <s-layout
+    navbar="normal"
+    :leftWidth="0"
+    :rightWidth="0"
+    tools="search"
+    :defaultSearch="state.keyword"
+    @search="onSearch"
+  >
+    <!-- 筛选 -->
+    <su-sticky bgColor="#fff">
+      <view class="ss-flex">
+        <view class="ss-flex-1">
+          <su-tabs
+            :list="state.tabList"
+            :scrollable="false"
+            @change="onTabsChange"
+            :current="state.currentTab"
+          />
+        </view>
+        <view class="list-icon" @tap="state.iconStatus = !state.iconStatus">
+          <text v-if="state.iconStatus" class="sicon-goods-list" />
+          <text v-else class="sicon-goods-card" />
+        </view>
+      </view>
+    </su-sticky>
+
+    <!-- 弹窗 -->
+    <su-popup
+      :show="state.showFilter"
+      type="top"
+      round="10"
+      :space="sys_navBar + 38"
+      backgroundColor="#F6F6F6"
+      :zIndex="10"
+      @close="state.showFilter = false"
+    >
+      <view class="filter-list-box">
+        <view
+          class="filter-item"
+          v-for="(item, index) in state.tabList[state.currentTab].list"
+          :key="item.value"
+          :class="[{ 'filter-item-active': index === state.curFilter }]"
+          @tap="onFilterItem(index)"
+        >
+          {{ item.label }}
+        </view>
+      </view>
+    </su-popup>
+
+    <!-- 情况一:单列布局 -->
+    <view v-if="state.iconStatus && state.pagination.total > 0" class="goods-list ss-m-t-20">
+      <view
+        class="ss-p-l-20 ss-p-r-20 ss-m-b-20"
+        v-for="item in state.pagination.list"
+        :key="item.id"
+      >
+        <s-goods-column
+          class=""
+          size="lg"
+          :data="item"
+          :topRadius="10"
+          :bottomRadius="10"
+          @click="sheep.$router.go('/pages/goods/index', { id: item.id })"
+        />
+      </view>
+    </view>
+    <!-- 情况二:双列布局 -->
+    <view
+      v-if="!state.iconStatus && state.pagination.total > 0"
+      class="ss-flex ss-flex-wrap ss-p-x-20 ss-m-t-20 ss-col-top"
+    >
+      <view class="goods-list-box">
+        <view class="left-list" v-for="item in state.leftGoodsList" :key="item.id">
+          <s-goods-column
+            class="goods-md-box"
+            size="md"
+            :data="item"
+            :topRadius="10"
+            :bottomRadius="10"
+            @click="sheep.$router.go('/pages/goods/index', { id: item.id })"
+            @getHeight="mountMasonry($event, 'left')"
+          >
+            <template v-slot:cart>
+              <button class="ss-reset-button cart-btn" />
+            </template>
+          </s-goods-column>
+        </view>
+      </view>
+      <view class="goods-list-box">
+        <view class="right-list" v-for="item in state.rightGoodsList" :key="item.id">
+          <s-goods-column
+            class="goods-md-box"
+            size="md"
+            :topRadius="10"
+            :bottomRadius="10"
+            :data="item"
+            @click="sheep.$router.go('/pages/goods/index', { id: item.id })"
+            @getHeight="mountMasonry($event, 'right')"
+          >
+            <template v-slot:cart>
+              <button class="ss-reset-button cart-btn" />
+            </template>
+          </s-goods-column>
+        </view>
+      </view>
+    </view>
+    <uni-load-more
+      v-if="state.pagination.total > 0"
+      :status="state.loadStatus"
+      :content-text="{
+        contentdown: '上拉加载更多',
+      }"
+      @tap="loadMore"
+    />
+    <s-empty v-if="state.pagination.total === 0" icon="/static/soldout-empty.png" text="暂无商品" />
+  </s-layout>
+</template>
+
+<script setup>
+  import { reactive, ref } from 'vue';
+  import { onLoad, onReachBottom } from '@dcloudio/uni-app';
+  import sheep from '@/sheep';
+  import _ from 'lodash-es';
+  import { resetPagination } from '@/sheep/util';
+  import SpuApi from '@/sheep/api/product/spu';
+  import OrderApi from '@/sheep/api/trade/order';
+  import { appendSettlementProduct } from '@/sheep/hooks/useGoods';
+
+  const sys_navBar = sheep.$platform.navbar;
+  const emits = defineEmits(['close', 'change']);
+
+  const state = reactive({
+    pagination: {
+      list: [],
+      total: 0,
+      pageNo: 1,
+      pageSize: 6,
+    },
+    currentSort: undefined,
+    currentOrder: undefined,
+    currentTab: 0, // 当前选中的 tab
+    curFilter: 0, // 当前选中的 list 筛选项
+    showFilter: false,
+    iconStatus: false, // true - 单列布局;false - 双列布局
+    keyword: '',
+    categoryId: 0,
+    tabList: [
+      {
+        name: '综合推荐',
+        list: [
+          {
+            label: '综合推荐',
+          },
+          {
+            label: '价格升序',
+            sort: 'price',
+            order: true,
+          },
+          {
+            label: '价格降序',
+            sort: 'price',
+            order: false,
+          },
+        ],
+      },
+      {
+        name: '销量',
+        sort: 'salesCount',
+        order: false,
+      },
+      {
+        name: '新品优先',
+        value: 'createTime',
+        order: false,
+      },
+    ],
+    loadStatus: '',
+    leftGoodsList: [], // 双列布局 - 左侧商品
+    rightGoodsList: [], // 双列布局 - 右侧商品
+  });
+
+  // 加载瀑布流
+  let count = 0;
+  let leftHeight = 0;
+  let rightHeight = 0;
+
+  // 处理双列布局 leftGoodsList + rightGoodsList
+  function mountMasonry(height = 0, where = 'left') {
+    if (!state.pagination.list[count]) {
+      return;
+    }
+
+    if (where === 'left') {
+      leftHeight += height;
+    } else {
+      rightHeight += height;
+    }
+    if (leftHeight <= rightHeight) {
+      state.leftGoodsList.push(state.pagination.list[count]);
+    } else {
+      state.rightGoodsList.push(state.pagination.list[count]);
+    }
+    count++;
+  }
+
+  // 清空列表
+  function emptyList() {
+    resetPagination(state.pagination);
+    state.leftGoodsList = [];
+    state.rightGoodsList = [];
+    count = 0;
+    leftHeight = 0;
+    rightHeight = 0;
+  }
+
+  // 搜索
+  function onSearch(e) {
+    state.keyword = e;
+    emptyList();
+    getList(state.currentSort, state.currentOrder);
+  }
+
+  // 点击
+  function onTabsChange(e) {
+    // 如果点击的是【综合推荐】,则直接展开或者收起筛选项
+    if (state.tabList[e.index].list) {
+      state.currentTab = e.index;
+      state.showFilter = !state.showFilter;
+      return;
+    }
+    state.showFilter = false;
+
+    // 如果点击的是【销量】或者【新品优先】,则直接切换 tab
+    if (e.index === state.currentTab) {
+      return;
+    }
+
+    state.currentTab = e.index;
+    state.currentSort = e.sort;
+    state.currentOrder = e.order;
+    emptyList();
+    getList(e.sort, e.order);
+  }
+
+  // 点击 tab 的 list 筛选项
+  const onFilterItem = (val) => {
+    // 如果点击的是当前的筛选项,则直接收起筛选项,不要加载数据
+    // 这里选择 tabList[0] 的原因,是目前只有它有 list
+    if (
+      state.currentSort === state.tabList[0].list[val].sort &&
+      state.currentOrder === state.tabList[0].list[val].order
+    ) {
+      state.showFilter = false;
+      return;
+    }
+    state.showFilter = false;
+
+    // 设置筛选条件
+    state.curFilter = val;
+    state.tabList[0].name = state.tabList[0].list[val].label;
+    state.currentSort = state.tabList[0].list[val].sort;
+    state.currentOrder = state.tabList[0].list[val].order;
+    // 清空 + 加载数据
+    emptyList();
+    getList();
+  };
+
+  async function getList() {
+    state.loadStatus = 'loading';
+    const { code, data } = await SpuApi.getSpuPage({
+      pageNo: state.pagination.pageNo,
+      pageSize: state.pagination.pageSize,
+      sortField: state.currentSort,
+      sortAsc: state.currentOrder,
+      categoryId: state.categoryId,
+      keyword: state.keyword,
+    });
+    if (code !== 0) {
+      return;
+    }
+    // 拼接结算信息(营销)
+    await OrderApi.getSettlementProduct(data.list.map((item) => item.id).join(',')).then((res) => {
+      if (res.code !== 0) {
+        return;
+      }
+      appendSettlementProduct(data.list, res.data);
+    });
+    state.pagination.list = _.concat(state.pagination.list, data.list);
+    state.pagination.total = data.total;
+    state.loadStatus = state.pagination.list.length < state.pagination.total ? 'more' : 'noMore';
+    mountMasonry();
+  }
+
+  // 加载更多
+  function loadMore() {
+    if (state.loadStatus === 'noMore') {
+      return;
+    }
+    state.pagination.pageNo++;
+    getList(state.currentSort, state.currentOrder);
+  }
+
+  onLoad((options) => {
+    state.categoryId = options.categoryId;
+    state.keyword = options.keyword;
+    getList(state.currentSort, state.currentOrder);
+  });
+
+  // 上拉加载更多
+  onReachBottom(() => {
+    loadMore();
+  });
+</script>
+
+<style lang="scss" scoped>
+  .goods-list-box {
+    width: 50%;
+    box-sizing: border-box;
+
+    .left-list {
+      margin-right: 10rpx;
+      margin-bottom: 20rpx;
+    }
+
+    .right-list {
+      margin-left: 10rpx;
+      margin-bottom: 20rpx;
+    }
+  }
+
+  .goods-box {
+    &:nth-last-of-type(1) {
+      margin-bottom: 0 !important;
+    }
+
+    &:nth-child(2n) {
+      margin-right: 0;
+    }
+  }
+
+  .list-icon {
+    width: 80rpx;
+
+    .sicon-goods-card {
+      font-size: 40rpx;
+    }
+
+    .sicon-goods-list {
+      font-size: 40rpx;
+    }
+  }
+
+  .goods-card {
+    margin-left: 20rpx;
+  }
+
+  .list-filter-tabs {
+    background-color: #fff;
+  }
+
+  .filter-list-box {
+    padding: 28rpx 52rpx;
+
+    .filter-item {
+      font-size: 28rpx;
+      font-weight: 500;
+      color: #333333;
+      line-height: normal;
+      margin-bottom: 24rpx;
+
+      &:nth-last-child(1) {
+        margin-bottom: 0;
+      }
+    }
+
+    .filter-item-active {
+      color: var(--ui-BG-Main);
+    }
+  }
+
+  .tab-item {
+    height: 50px;
+    position: relative;
+    z-index: 11;
+
+    .tab-title {
+      font-size: 30rpx;
+    }
+
+    .cur-tab-title {
+      font-weight: $font-weight-bold;
+    }
+
+    .tab-line {
+      width: 60rpx;
+      height: 6rpx;
+      border-radius: 6rpx;
+      position: absolute;
+      left: 50%;
+      transform: translateX(-50%);
+      bottom: 10rpx;
+      background-color: var(--ui-BG-Main);
+      z-index: 12;
+    }
+  }
+</style>

+ 480 - 0
pages/goods/point.vue

@@ -0,0 +1,480 @@
+<!-- 秒杀商品详情 -->
+<template>
+  <s-layout :onShareAppMessage="shareInfo" navbar="goods">
+    <!-- 标题栏 -->
+    <detailNavbar />
+    <!-- 骨架屏 -->
+    <detailSkeleton v-if="state.skeletonLoading" />
+    <!-- 下架/售罄提醒 -->
+    <s-empty
+      v-else-if="state.goodsInfo === null || state.goodsInfo.activity_type !== PromotionActivityTypeEnum.POINT.type"
+      text="活动不存在或已结束"
+      icon="/static/soldout-empty.png"
+      showAction
+      actionText="再逛逛"
+      actionUrl="/pages/goods/list"
+    />
+    <block v-else>
+      <view class="detail-swiper-selector">
+        <!-- 商品图轮播 -->
+        <su-swiper
+          class="ss-m-b-14"
+          isPreview
+          :list="state.goodsSwiper"
+          dotStyle="tag"
+          imageMode="widthFix"
+          dotCur="bg-mask-40"
+          :seizeHeight="750"
+        />
+
+        <!-- 价格+标题 -->
+        <view class="title-card detail-card ss-p-y-40 ss-p-x-20">
+          <view class="ss-flex ss-row-between ss-col-center ss-m-b-18">
+            <view class="price-box ss-flex ss-col-bottom">
+              <image
+                :src="sheep.$url.static('/static/img/shop/goods/score1.svg')"
+                class="point-img"
+              ></image>
+              <text class="point-text ss-m-r-16">
+                {{ getShowPrice.point }}
+                {{ !getShowPrice.price || getShowPrice.price === 0 ? '' : `+¥${getShowPrice.price}` }}
+              </text>
+            </view>
+            <view class="sales-text">
+              {{ formatExchange(state.goodsInfo.sales_show_type, state.goodsInfo.sales) }}
+            </view>
+          </view>
+          <view class="origin-price-text ss-m-b-60" v-if="state.goodsInfo.marketPrice">
+            原价:¥{{ fen2yuan(state.selectedSku.marketPrice || state.goodsInfo.marketPrice) }}
+          </view>
+          <view class="title-text ss-line-2 ss-m-b-6">{{ state.goodsInfo.name || '' }}</view>
+          <view class="subtitle-text ss-line-1">{{ state.goodsInfo.introduction }}</view>
+        </view>
+
+        <!-- 功能卡片 -->
+        <view class="detail-cell-card detail-card ss-flex-col">
+          <detail-cell-sku :sku="state.selectedSku" @tap="state.showSelectSku = true" />
+        </view>
+        <!-- 规格与数量弹框 -->
+        <s-select-seckill-sku
+          v-model="state.goodsInfo"
+          :show="state.showSelectSku"
+          :single-limit-count="activity.singleLimitCount"
+          @buy="onBuy"
+          @change="onSkuChange"
+          @close="state.showSelectSku = false"
+        />
+      </view>
+
+      <!-- 评价 -->
+      <detail-comment-card class="detail-comment-selector" :goodsId="state.goodsInfo.id" />
+      <!-- 详情 -->
+      <detail-content-card class="detail-content-selector" :content="state.goodsInfo.description" />
+
+      <!-- 详情tabbar -->
+      <detail-tabbar v-model="state.goodsInfo">
+        <view class="buy-box ss-flex ss-col-center ss-p-r-20">
+          <button
+            class="ss-reset-button origin-price-btn ss-flex-col"
+            v-if="state.goodsInfo.marketPrice"
+            @tap="sheep.$router.go('/pages/goods/index', { id: state.goodsInfo.id })"
+          >
+            <view>
+              <view class="btn-price">{{ fen2yuan(state.goodsInfo.marketPrice) }}</view>
+              <view>原价购买</view>
+            </view>
+          </button>
+          <button
+            class="ss-reset-button btn-box ss-flex-col"
+            @tap="state.showSelectSku = true"
+            :class="
+             state.goodsInfo.stock != 0
+                ? 'check-btn-box'
+                : 'disabled-btn-box'
+            "
+            :disabled="state.goodsInfo.stock === 0"
+          >
+            <view class="price-box ss-flex">
+              <image
+                :src="sheep.$url.static('/static/img/shop/goods/score1.svg')"
+                style="width: 36rpx;height: 36rpx;margin: 0 4rpx;"
+              ></image>
+              <text class="point-text ss-m-r-16">
+                {{ getShowPrice.point }}
+                {{ !getShowPrice.price || getShowPrice.price === 0 ? '' : `+¥${getShowPrice.price}` }}
+              </text>
+            </view>
+            <view v-if="state.goodsInfo.stock === 0">已售罄</view>
+            <view v-else>立即兑换</view>
+          </button>
+        </view>
+      </detail-tabbar>
+    </block>
+  </s-layout>
+</template>
+
+<script setup>
+  import { computed, reactive, ref, unref } from 'vue';
+  import { onLoad, onPageScroll } from '@dcloudio/uni-app';
+  import sheep from '@/sheep';
+  import { isEmpty } from 'lodash-es';
+  import { fen2yuan, formatExchange, formatGoodsSwiper } from '@/sheep/hooks/useGoods';
+  import detailNavbar from './components/detail/detail-navbar.vue';
+  import detailCellSku from './components/detail/detail-cell-sku.vue';
+  import detailTabbar from './components/detail/detail-tabbar.vue';
+  import detailSkeleton from './components/detail/detail-skeleton.vue';
+  import detailCommentCard from './components/detail/detail-comment-card.vue';
+  import detailContentCard from './components/detail/detail-content-card.vue';
+  import SpuApi from '@/sheep/api/product/spu';
+  import { PromotionActivityTypeEnum } from '@/sheep/util/const';
+  import PointApi from '@/sheep/api/promotion/point';
+
+  const headerBg = sheep.$url.css('/static/img/shop/goods/score-bg.png');
+  const btnBg = sheep.$url.css('/static/img/shop/goods/seckill-btn.png');
+  const disabledBtnBg = sheep.$url.css('/static/img/shop/goods/activity-btn-disabled.png');
+  const seckillBg = sheep.$url.css('/static/img/shop/goods/seckill-tip-bg.png');
+  const grouponBg = sheep.$url.css('/static/img/shop/goods/groupon-tip-bg.png');
+
+  onPageScroll(() => {
+  });
+  const state = reactive({
+    skeletonLoading: true,
+    goodsInfo: {},
+    showSelectSku: false,
+    goodsSwiper: [],
+    selectedSku: {},
+    showModel: false,
+    total: 0,
+    price: '',
+  });
+
+  // 规格变更
+  function onSkuChange(e) {
+    state.selectedSku = e;
+  }
+
+  // 立即购买
+  function onBuy(sku) {
+    sheep.$router.go('/pages/order/confirm', {
+      data: JSON.stringify({
+        order_type: 'goods',
+        buy_type: 'point',
+        pointActivityId: activity.value.id,
+        items: [
+          {
+            skuId: sku.id,
+            count: sku.count,
+          },
+        ],
+      }),
+    });
+  }
+
+  // 分享信息
+  // TODO puhui999: 下次 fix
+  const shareInfo = computed(() => {
+    if (isEmpty(unref(activity))) return {};
+    return sheep.$platform.share.getShareInfo(
+      {
+        title: activity.value.name,
+        image: sheep.$url.cdn(state.goodsInfo.picUrl),
+        params: {
+          page: '4',
+          query: activity.value.id,
+        },
+      },
+      {
+        type: 'goods', // 商品海报
+        title: activity.value.name, // 商品标题
+        image: sheep.$url.cdn(state.goodsInfo.picUrl), // 商品主图
+        price: state.goodsInfo.price, // 商品价格
+        marketPrice: state.goodsInfo.marketPrice, // 商品原价
+      },
+    );
+  });
+
+  const activity = ref();
+
+  const getShowPrice = computed(() => {
+    if (!isEmpty(state.selectedSku)) {
+      const sku = state.selectedSku;
+      return {
+        point: sku.point,
+        price: !sku.pointPrice ? '' : fen2yuan(sku.pointPrice),
+      };
+    }
+    return {
+      point: activity.value.point,
+      price: !activity.value.price ? '' : fen2yuan(activity.value.price),
+    };
+  });
+
+  const getShowPriceText = computed(() => {
+    let priceText = `¥${fen2yuan(state.goodsInfo.price)}`;
+    if (!isEmpty(state.selectedSku)) {
+      const sku = state.selectedSku;
+      priceText = `${sku.point}${!sku.pointPrice ? '' : `+¥${fen2yuan(sku.pointPrice)}`}`;
+    }
+    return priceText;
+  });
+
+  // 查询活动
+  const getActivity = async (id) => {
+    const { data } = await PointApi.getPointActivity(id);
+    activity.value = data;
+    // 查询商品
+    await getSpu(data.spuId);
+  };
+
+  // 查询商品
+  const getSpu = async (id) => {
+    const { data } = await SpuApi.getSpuDetail(id);
+    data.activity_type = PromotionActivityTypeEnum.POINT.type;
+    state.goodsInfo = data;
+    state.goodsInfo.stock = Math.min(data.stock, activity.value.stock);
+    // 处理轮播图
+    state.goodsSwiper = formatGoodsSwiper(state.goodsInfo.sliderPicUrls);
+
+    // 价格、库存使用活动的
+    data.skus.forEach((sku) => {
+      const product = activity.value.products.find((product) => product.skuId === sku.id);
+      if (product) {
+        sku.point = product.point;
+        sku.pointPrice = product.price;
+        sku.stock = Math.min(sku.stock, product.stock);
+        // 设置限购数量
+        sku.limitCount = product.count;
+      } else {
+        // 找不到可能是没配置
+        sku.stock = 0;
+      }
+    });
+
+    state.skeletonLoading = false;
+  };
+
+  onLoad((options) => {
+    // 非法参数
+    if (!options.id) {
+      state.goodsInfo = null;
+      return;
+    }
+
+    // 查询活动
+    getActivity(options.id);
+  });
+</script>
+
+<style lang="scss" scoped>
+  .disabled-btn-box[disabled] {
+    background-color: transparent;
+  }
+
+  .detail-card {
+    background-color: $white;
+    margin: 14rpx 20rpx;
+    border-radius: 10rpx;
+    overflow: hidden;
+  }
+
+  // 价格标题卡片
+  .title-card {
+    width: 710rpx;
+    box-sizing: border-box;
+    background-size: 100% 100%;
+    border-radius: 10rpx;
+    background-image: v-bind(headerBg);
+    background-repeat: no-repeat;
+
+    .price-box {
+      .point-img {
+        width: 36rpx;
+        height: 36rpx;
+        margin: 0 4rpx;
+      }
+
+      .point-text {
+        font-size: 42rpx;
+        font-weight: 500;
+        color: #ff3000;
+        line-height: 36rpx;
+        font-family: OPPOSANS;
+      }
+
+      .price-text {
+        font-size: 42rpx;
+        font-weight: 500;
+        color: #ff3000;
+        line-height: 36rpx;
+        font-family: OPPOSANS;
+      }
+    }
+
+    .origin-price-text {
+      font-size: 26rpx;
+      font-weight: 400;
+      text-decoration: line-through;
+      color: $gray-c;
+      font-family: OPPOSANS;
+    }
+
+    .sales-text {
+      font-size: 26rpx;
+      font-weight: 500;
+      color: $gray-c;
+    }
+
+    .discounts-box {
+      .discounts-tag {
+        padding: 4rpx 10rpx;
+        font-size: 24rpx;
+        font-weight: 500;
+        border-radius: 4rpx;
+        color: var(--ui-BG-Main);
+        // background: rgba(#2aae67, 0.05);
+        background: var(--ui-BG-Main-tag);
+      }
+
+      .discounts-title {
+        font-size: 24rpx;
+        font-weight: 500;
+        color: var(--ui-BG-Main);
+        line-height: normal;
+      }
+
+      .cicon-forward {
+        color: var(--ui-BG-Main);
+        font-size: 24rpx;
+        line-height: normal;
+        margin-top: 4rpx;
+      }
+    }
+
+    .title-text {
+      font-size: 30rpx;
+      font-weight: bold;
+      line-height: 42rpx;
+    }
+
+    .subtitle-text {
+      font-size: 26rpx;
+      font-weight: 400;
+      color: $dark-9;
+      line-height: 42rpx;
+    }
+  }
+
+  // 购买
+  .buy-box {
+    .check-btn-box {
+      width: 248rpx;
+      height: 80rpx;
+      font-size: 24rpx;
+      font-weight: 600;
+      margin-left: -36rpx;
+      background-image: v-bind(btnBg);
+      background-repeat: no-repeat;
+      background-size: 100% 100%;
+      color: #ffffff;
+      line-height: normal;
+      border-radius: 0px 40rpx 40rpx 0px;
+    }
+
+    .disabled-btn-box {
+      width: 248rpx;
+      height: 80rpx;
+      font-size: 24rpx;
+      font-weight: 600;
+      margin-left: -36rpx;
+      background-image: v-bind(disabledBtnBg);
+      background-repeat: no-repeat;
+      background-size: 100% 100%;
+      color: #999999;
+      line-height: normal;
+      border-radius: 0px 40rpx 40rpx 0px;
+    }
+
+    .btn-price {
+      font-family: OPPOSANS;
+
+      &::before {
+        content: '¥';
+      }
+    }
+
+    .origin-price-btn {
+      width: 236rpx;
+      height: 80rpx;
+      background: rgba(#ff5651, 0.1);
+      color: #ff6000;
+      border-radius: 40rpx 0px 0px 40rpx;
+      line-height: normal;
+      font-size: 24rpx;
+      font-weight: 500;
+
+      .no-original {
+        font-size: 28rpx;
+      }
+
+      .btn-title {
+        font-size: 28rpx;
+      }
+    }
+  }
+
+  //秒杀卡片
+  .seckill-box {
+    background: v-bind(seckillBg) no-repeat;
+    background-size: 100% 100%;
+  }
+
+  .groupon-box {
+    background: v-bind(grouponBg) no-repeat;
+    background-size: 100% 100%;
+  }
+
+  //活动卡片
+  .activity-box {
+    width: 100%;
+    height: 80rpx;
+    box-sizing: border-box;
+    margin-bottom: 10rpx;
+
+    .activity-title {
+      font-size: 26rpx;
+      font-weight: 500;
+      color: #ffffff;
+      line-height: 42rpx;
+
+      .activity-icon {
+        width: 38rpx;
+        height: 38rpx;
+      }
+    }
+
+    .activity-go {
+      width: 70rpx;
+      height: 32rpx;
+      background: #ffffff;
+      border-radius: 16rpx;
+      font-weight: 500;
+      color: #ff6000;
+      font-size: 24rpx;
+      line-height: normal;
+    }
+  }
+
+  .model-box {
+    .title {
+      font-size: 36rpx;
+      font-weight: bold;
+      color: #333333;
+    }
+
+    .subtitle {
+      font-size: 26rpx;
+      font-weight: 500;
+      color: #333333;
+    }
+  }
+</style>

+ 564 - 0
pages/goods/seckill.vue

@@ -0,0 +1,564 @@
+<!-- 秒杀商品详情 -->
+<template>
+  <s-layout :onShareAppMessage="shareInfo" navbar="goods">
+    <!-- 标题栏 -->
+    <detailNavbar />
+    <!-- 骨架屏 -->
+    <detailSkeleton v-if="state.skeletonLoading" />
+    <!-- 下架/售罄提醒 -->
+    <s-empty
+      v-else-if="
+        state.goodsInfo === null || state.goodsInfo.activity_type !== 'seckill' || endTime.ms <= 0
+      "
+      text="活动不存在或已结束"
+      icon="/static/soldout-empty.png"
+      showAction
+      actionText="再逛逛"
+      actionUrl="/pages/goods/list"
+    />
+    <block v-else>
+      <view class="detail-swiper-selector">
+        <!-- 商品图轮播 -->
+        <su-swiper
+          class="ss-m-b-14"
+          isPreview
+          :list="state.goodsSwiper"
+          dotStyle="tag"
+          imageMode="widthFix"
+          dotCur="bg-mask-40"
+          :seizeHeight="750"
+        />
+
+        <!-- 价格+标题 -->
+        <view class="title-card ss-m-y-14 ss-m-x-20 ss-p-x-20 ss-p-y-34">
+          <view class="price-box ss-flex ss-row-between ss-m-b-18">
+            <view class="ss-flex">
+              <view class="price-text ss-m-r-16">
+                {{ 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>
+            <view class="countdown-box" v-if="endTime.ms > 0">
+              <view class="countdown-title ss-m-b-20">距结束仅剩</view>
+              <view class="ss-flex countdown-time">
+                <view class="ss-flex countdown-h">{{ endTime.h }}</view>
+                <view class="ss-m-x-4">:</view>
+                <view class="countdown-num ss-flex ss-row-center">{{ endTime.m }}</view>
+                <view class="ss-m-x-4">:</view>
+                <view class="countdown-num ss-flex ss-row-center">{{ endTime.s }}</view>
+              </view>
+            </view>
+            <view class="countdown-title" v-else> 活动已结束 </view>
+          </view>
+          <view class="ss-flex ss-row-between ss-m-b-60">
+            <view class="origin-price ss-flex ss-col-center" v-if="state.goodsInfo.marketPrice">
+              原价
+              <view class="origin-price-text">
+                {{ fen2yuan(state.selectedSku.marketPrice || state.goodsInfo.marketPrice) }}
+              </view>
+            </view>
+            <detail-progress :percent="state.percent" />
+          </view>
+
+          <view class="title-text ss-line-2 ss-m-b-6">{{ state.goodsInfo.name || '' }}</view>
+          <view class="subtitle-text ss-line-1">{{ state.goodsInfo.introduction }}</view>
+        </view>
+
+        <!-- 功能卡片 -->
+        <view class="detail-cell-card detail-card ss-flex-col">
+          <detail-cell-sku :sku="state.selectedSku" @tap="state.showSelectSku = true" />
+        </view>
+        <!-- 规格与数量弹框 -->
+        <s-select-seckill-sku
+          v-model="state.goodsInfo"
+          :show="state.showSelectSku"
+          :single-limit-count="activity.singleLimitCount"
+          @buy="onBuy"
+          @change="onSkuChange"
+          @close="state.showSelectSku = false"
+        />
+      </view>
+
+      <!-- 评价 -->
+      <detail-comment-card class="detail-comment-selector" :goodsId="state.goodsInfo.id" />
+      <!-- 详情 -->
+      <detail-content-card class="detail-content-selector" :content="state.goodsInfo.description" />
+
+      <!-- 详情tabbar -->
+      <detail-tabbar v-model="state.goodsInfo">
+        <!-- TODO: 缺货中 已售罄 判断 设计-->
+        <view class="buy-box ss-flex ss-col-center ss-p-r-20">
+          <button
+            class="ss-reset-button origin-price-btn ss-flex-col"
+            v-if="state.goodsInfo.marketPrice"
+            @tap="sheep.$router.go('/pages/goods/index', { id: state.goodsInfo.id })"
+          >
+            <view>
+              <view class="btn-price">{{ fen2yuan(state.goodsInfo.marketPrice) }}</view>
+              <view>原价购买</view>
+            </view>
+          </button>
+          <button v-else class="ss-reset-button origin-price-btn ss-flex-col">
+            <view
+              class="no-original"
+              :class="
+                state.goodsInfo.stock === 0 || timeStatusEnum !== TimeStatusEnum.STARTED ? '' : ''
+              "
+            >
+              秒杀价
+            </view>
+          </button>
+          <button
+            class="ss-reset-button btn-box ss-flex-col"
+            @tap="state.showSelectSku = true"
+            :class="
+              timeStatusEnum === TimeStatusEnum.STARTED && state.goodsInfo.stock != 0
+                ? 'check-btn-box'
+                : 'disabled-btn-box'
+            "
+            :disabled="state.goodsInfo.stock === 0 || timeStatusEnum !== TimeStatusEnum.STARTED"
+          >
+            <view class="btn-price">{{ fen2yuan(state.goodsInfo.price) }}</view>
+            <view v-if="timeStatusEnum === TimeStatusEnum.STARTED">
+              <view v-if="state.goodsInfo.stock === 0">已售罄</view>
+              <view v-else>立即秒杀</view>
+            </view>
+            <view v-else>{{ timeStatusEnum }}</view>
+          </button>
+        </view>
+      </detail-tabbar>
+    </block>
+  </s-layout>
+</template>
+
+<script setup>
+  import { reactive, computed, ref, unref } from 'vue';
+  import { onLoad, onPageScroll } from '@dcloudio/uni-app';
+  import sheep from '@/sheep';
+  import { isEmpty, min } from 'lodash-es';
+  import { useDurationTime, formatGoodsSwiper, fen2yuan } from '@/sheep/hooks/useGoods';
+  import detailNavbar from './components/detail/detail-navbar.vue';
+  import detailCellSku from './components/detail/detail-cell-sku.vue';
+  import detailTabbar from './components/detail/detail-tabbar.vue';
+  import detailSkeleton from './components/detail/detail-skeleton.vue';
+  import detailCommentCard from './components/detail/detail-comment-card.vue';
+  import detailContentCard from './components/detail/detail-content-card.vue';
+  import detailProgress from './components/detail/detail-progress.vue';
+  import SeckillApi from '@/sheep/api/promotion/seckill';
+  import SpuApi from '@/sheep/api/product/spu';
+  import { getTimeStatusEnum, TimeStatusEnum } from '@/sheep/util/const';
+
+  const headerBg = sheep.$url.css('/static/img/shop/goods/seckill-bg.png');
+  const btnBg = sheep.$url.css('/static/img/shop/goods/seckill-btn.png');
+  const disabledBtnBg = sheep.$url.css('/static/img/shop/goods/activity-btn-disabled.png');
+  const seckillBg = sheep.$url.css('/static/img/shop/goods/seckill-tip-bg.png');
+  const grouponBg = sheep.$url.css('/static/img/shop/goods/groupon-tip-bg.png');
+
+  onPageScroll(() => {});
+  const state = reactive({
+    skeletonLoading: true,
+    goodsInfo: {},
+    showSelectSku: false,
+    goodsSwiper: [],
+    selectedSku: {},
+    showModel: false,
+    total: 0,
+    percent: 0,
+    price: '',
+  });
+
+  const endTime = computed(() => {
+    return useDurationTime(activity.value.endTime);
+  });
+
+  // 规格变更
+  function onSkuChange(e) {
+    state.selectedSku = e;
+  }
+
+  // 立即购买
+  function onBuy(sku) {
+    sheep.$router.go('/pages/order/confirm', {
+      data: JSON.stringify({
+        order_type: 'goods',
+        buy_type: 'seckill',
+        seckillActivityId: activity.value.id,
+        items: [
+          {
+            skuId: sku.id,
+            count: sku.count,
+          },
+        ],
+      }),
+    });
+  }
+
+  // 分享信息
+  const shareInfo = computed(() => {
+    if (isEmpty(unref(activity))) return {};
+    return sheep.$platform.share.getShareInfo(
+      {
+        title: activity.value.name,
+        image: sheep.$url.cdn(state.goodsInfo.picUrl),
+        params: {
+          page: '4',
+          query: activity.value.id,
+        },
+      },
+      {
+        type: 'goods', // 商品海报
+        title: activity.value.name, // 商品标题
+        image: sheep.$url.cdn(state.goodsInfo.picUrl), // 商品主图
+        price: state.goodsInfo.price, // 商品价格
+        marketPrice: state.goodsInfo.marketPrice, // 商品原价
+      },
+    );
+  });
+
+  const activity = ref();
+  const timeStatusEnum = ref('');
+
+  // 查询活动
+  const getActivity = async (id) => {
+    const { data } = await SeckillApi.getSeckillActivity(id);
+    activity.value = data;
+    timeStatusEnum.value = getTimeStatusEnum(activity.value.startTime, activity.value.endTime);
+    state.percent = 100 - (data.stock / data.totalStock) * 100;
+    // 查询商品
+    await getSpu(data.spuId);
+  };
+
+  // 查询商品
+  const getSpu = async (id) => {
+    const { data } = await SpuApi.getSpuDetail(id);
+    data.activity_type = 'seckill';
+    state.goodsInfo = data;
+    // 处理轮播图
+    state.goodsSwiper = formatGoodsSwiper(state.goodsInfo.sliderPicUrls);
+
+    // 默认显示最低价
+    state.goodsInfo.price = min([
+      state.goodsInfo.price,
+      ...activity.value.products.map((spu) => spu.seckillPrice),
+    ]);
+
+    // 价格、库存使用活动的
+    data.skus.forEach((sku) => {
+      const product = activity.value.products.find((product) => product.skuId === sku.id);
+      if (product) {
+        sku.price = product.seckillPrice;
+        sku.stock = Math.min(sku.stock, product.stock);
+      } else {
+        // 找不到可能是没配置,则不能发起秒杀
+        sku.stock = 0;
+      }
+      // 设置限购数量
+      if (activity.value.totalLimitCount > 0 && activity.value.singleLimitCount > 0) {
+        sku.limitCount = Math.min(activity.value.totalLimitCount, activity.value.singleLimitCount);
+      } else if (activity.value.totalLimitCount > 0) {
+        sku.limitCount = activity.value.totalLimitCount;
+      } else if (activity.value.singleLimitCount > 0) {
+        sku.limitCount = activity.value.singleLimitCount;
+      }
+    });
+
+    state.skeletonLoading = false;
+  };
+
+  onLoad((options) => {
+    // 非法参数
+    if (!options.id) {
+      state.goodsInfo = null;
+      return;
+    }
+
+    // 查询活动
+    getActivity(options.id);
+  });
+</script>
+
+<style lang="scss" scoped>
+  .disabled-btn-box[disabled] {
+    background-color: transparent;
+  }
+
+  .detail-card {
+    background-color: $white;
+    margin: 14rpx 20rpx;
+    border-radius: 10rpx;
+    overflow: hidden;
+  }
+
+  // 价格标题卡片
+  .title-card {
+    width: 710rpx;
+    box-sizing: border-box;
+    // height: 320rpx;
+    background-size: 100% 100%;
+    border-radius: 10rpx;
+    background-image: v-bind(headerBg);
+    background-repeat: no-repeat;
+
+    .price-box {
+      .price-text {
+        font-size: 30rpx;
+        font-weight: 500;
+        color: #fff;
+        line-height: normal;
+        font-family: OPPOSANS;
+
+        &::before {
+          content: '¥';
+          font-size: 30rpx;
+        }
+      }
+    }
+
+    .origin-price {
+      font-size: 24rpx;
+      font-weight: 400;
+      color: #fff;
+      opacity: 0.7;
+
+      .origin-price-text {
+        text-decoration: line-through;
+
+        font-family: OPPOSANS;
+
+        &::before {
+          content: '¥';
+        }
+      }
+    }
+
+    .tig {
+      border: 2rpx solid #ffffff;
+      border-radius: 4rpx;
+      width: 126rpx;
+      height: 38rpx;
+
+      .tig-icon {
+        width: 40rpx;
+        height: 40rpx;
+        margin-left: -2rpx;
+        background: #ffffff;
+        border-radius: 4rpx 0 0 4rpx;
+
+        .cicon-alarm {
+          font-size: 32rpx;
+          color: #fc6e6f;
+        }
+      }
+
+      .tig-title {
+        width: 86rpx;
+        font-size: 24rpx;
+        font-weight: 500;
+        line-height: normal;
+        color: #ffffff;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+      }
+    }
+
+    .countdown-title {
+      font-size: 26rpx;
+      font-weight: 500;
+      color: #ffffff;
+    }
+
+    .countdown-time {
+      font-size: 26rpx;
+      font-weight: 500;
+      color: #ffffff;
+
+      .countdown-h {
+        font-size: 24rpx;
+        font-family: OPPOSANS;
+        font-weight: 500;
+        color: #ffffff;
+        padding: 0 4rpx;
+        height: 40rpx;
+        background: rgba(#000000, 0.1);
+        border-radius: 6rpx;
+      }
+
+      .countdown-num {
+        font-size: 24rpx;
+        font-family: OPPOSANS;
+        font-weight: 500;
+        color: #ffffff;
+        width: 40rpx;
+        height: 40rpx;
+        background: rgba(#000000, 0.1);
+        border-radius: 6rpx;
+      }
+    }
+
+    .discounts-box {
+      .discounts-tag {
+        padding: 4rpx 10rpx;
+        font-size: 24rpx;
+        font-weight: 500;
+        border-radius: 4rpx;
+        color: var(--ui-BG-Main);
+        // background: rgba(#2aae67, 0.05);
+        background: var(--ui-BG-Main-tag);
+      }
+
+      .discounts-title {
+        font-size: 24rpx;
+        font-weight: 500;
+        color: var(--ui-BG-Main);
+        line-height: normal;
+      }
+
+      .cicon-forward {
+        color: var(--ui-BG-Main);
+        font-size: 24rpx;
+        line-height: normal;
+        margin-top: 4rpx;
+      }
+    }
+
+    .title-text {
+      font-size: 30rpx;
+      font-weight: bold;
+      line-height: 42rpx;
+      color: #fff;
+    }
+
+    .subtitle-text {
+      font-size: 26rpx;
+      font-weight: 400;
+      color: #ffffff;
+      line-height: 42rpx;
+      opacity: 0.9;
+    }
+  }
+
+  // 购买
+  .buy-box {
+    .check-btn-box {
+      width: 248rpx;
+      height: 80rpx;
+      font-size: 24rpx;
+      font-weight: 600;
+      margin-left: -36rpx;
+      background-image: v-bind(btnBg);
+      background-repeat: no-repeat;
+      background-size: 100% 100%;
+      color: #ffffff;
+      line-height: normal;
+      border-radius: 0px 40rpx 40rpx 0px;
+    }
+
+    .disabled-btn-box {
+      width: 248rpx;
+      height: 80rpx;
+      font-size: 24rpx;
+      font-weight: 600;
+      margin-left: -36rpx;
+      background-image: v-bind(disabledBtnBg);
+      background-repeat: no-repeat;
+      background-size: 100% 100%;
+      color: #999999;
+      line-height: normal;
+      border-radius: 0px 40rpx 40rpx 0px;
+    }
+
+    .btn-price {
+      font-family: OPPOSANS;
+
+      &::before {
+        content: '¥';
+      }
+    }
+
+    .origin-price-btn {
+      width: 236rpx;
+      height: 80rpx;
+      background: rgba(#ff5651, 0.1);
+      color: #ff6000;
+      border-radius: 40rpx 0px 0px 40rpx;
+      line-height: normal;
+      font-size: 24rpx;
+      font-weight: 500;
+
+      .no-original {
+        font-size: 28rpx;
+      }
+
+      .btn-title {
+        font-size: 28rpx;
+      }
+    }
+  }
+
+  //秒杀卡片
+  .seckill-box {
+    background: v-bind(seckillBg) no-repeat;
+    background-size: 100% 100%;
+  }
+
+  .groupon-box {
+    background: v-bind(grouponBg) no-repeat;
+    background-size: 100% 100%;
+  }
+
+  //活动卡片
+  .activity-box {
+    width: 100%;
+    height: 80rpx;
+    box-sizing: border-box;
+    margin-bottom: 10rpx;
+
+    .activity-title {
+      font-size: 26rpx;
+      font-weight: 500;
+      color: #ffffff;
+      line-height: 42rpx;
+
+      .activity-icon {
+        width: 38rpx;
+        height: 38rpx;
+      }
+    }
+
+    .activity-go {
+      width: 70rpx;
+      height: 32rpx;
+      background: #ffffff;
+      border-radius: 16rpx;
+      font-weight: 500;
+      color: #ff6000;
+      font-size: 24rpx;
+      line-height: normal;
+    }
+  }
+
+  .model-box {
+    .title {
+      font-size: 36rpx;
+      font-weight: bold;
+      color: #333333;
+    }
+
+    .subtitle {
+      font-size: 26rpx;
+      font-weight: 500;
+      color: #333333;
+    }
+  }
+
+  image {
+    width: 100%;
+    height: 100%;
+  }
+</style>

+ 192 - 0
pages/index/cart.vue

@@ -0,0 +1,192 @@
+<template>
+	<s-layout title="购物车" tabbar="/pages/index/cart" :bgStyle="{ color: '#fff' }">
+		<s-empty v-if="state.list.length === 0" text="购物车空空如也,快去逛逛吧~" icon="/static/cart-empty.png" />
+
+		<!-- 头部 -->
+		<view class="cart-box ss-flex ss-flex-col ss-row-between" v-if="state.list.length">
+			<view class="cart-header ss-flex ss-col-center ss-row-between ss-p-x-30">
+				<view class="header-left ss-flex ss-col-center ss-font-26">
+					共
+					<text class="goods-number ui-TC-Main ss-flex">{{ state.list.length }}</text>
+					件商品
+				</view>
+				<view class="header-right">
+					<button v-if="state.editMode" class="ss-reset-button" @tap="state.editMode = false">
+						取消
+					</button>
+					<button v-else class="ss-reset-button ui-TC-Main" @tap="state.editMode = true">
+						编辑
+					</button>
+				</view>
+			</view>
+			<!-- 内容 -->
+			<view class="cart-content ss-flex-1 ss-p-x-30 ss-m-b-40">
+				<view class="goods-box ss-r-10 ss-m-b-14" v-for="item in state.list" :key="item.id">
+					<view class="ss-flex ss-col-center">
+						<label class="check-box ss-flex ss-col-center ss-p-l-10" @tap="onSelectSingle(item.id)">
+							<radio :checked="state.selectedIds.includes(item.id)" color="var(--ui-BG-Main)"
+								style="transform: scale(0.8)" @tap.stop="onSelectSingle(item.id)" />
+						</label>
+						<s-goods-item :title="item.spu.name" :img="item.spu.picUrl || item.goods.image"
+							:price="item.sku.price"
+							:skuText="item.sku.properties.length>1? item.sku.properties.reduce((items2,items)=>items2.valueName+' '+items.valueName):item.sku.properties[0].valueName"
+							priceColor="#FF3000" :titleWidth="400">
+							<template v-if="!state.editMode" v-slot:tool>
+								<su-number-box :min="0" :max="item.sku.stock" :step="1" v-model="item.count" @change="onNumberChange($event, item)" />
+							</template>
+						</s-goods-item>
+					</view>
+				</view>
+			</view>
+			<!-- 底部 -->
+			<su-fixed bottom :val="48" placeholder v-if="state.list.length > 0" :isInset="false">
+				<view class="cart-footer ss-flex ss-col-center ss-row-between ss-p-x-30 border-bottom">
+					<view class="footer-left ss-flex ss-col-center">
+						<label class="check-box ss-flex ss-col-center ss-p-r-30" @tap="onSelectAll">
+							<radio :checked="state.isAllSelected" color="var(--ui-BG-Main)"
+								style="transform: scale(0.8)" @tap.stop="onSelectAll" />
+							<view class="ss-m-l-8"> 全选 </view>
+						</label>
+						<text>合计:</text>
+						<view class="text-price price-text">
+							{{ fen2yuan(state.totalPriceSelected) }}
+						</view>
+					</view>
+					<view class="footer-right">
+						<button v-if="state.editMode" class="ss-reset-button ui-BG-Main-Gradient pay-btn ui-Shadow-Main"
+							@tap="onDelete">
+							删除
+						</button>
+						<button v-else class="ss-reset-button ui-BG-Main-Gradient pay-btn ui-Shadow-Main"
+							@tap="onConfirm">
+							去结算
+							{{ state.selectedIds?.length ? `(${state.selectedIds.length})` : '' }}
+						</button>
+					</view>
+				</view>
+			</su-fixed>
+		</view>
+	</s-layout>
+</template>
+
+<script setup>
+	import sheep from '@/sheep';
+	import { computed, reactive } from 'vue';
+  import { fen2yuan } from '../../sheep/hooks/useGoods';
+
+	const sys_navBar = sheep.$platform.navbar;
+	const cart = sheep.$store('cart');
+
+	const state = reactive({
+		editMode: false,
+		list: computed(() => cart.list),
+		selectedList: [],
+		selectedIds: computed(() => cart.selectedIds),
+		isAllSelected: computed(() => cart.isAllSelected),
+		totalPriceSelected: computed(() => cart.totalPriceSelected),
+	});
+
+	// 单选选中
+	function onSelectSingle(id) {
+		cart.selectSingle(id);
+	}
+
+  // 全选
+	function onSelectAll() {
+		cart.selectAll(!state.isAllSelected);
+	}
+
+	// 结算
+	function onConfirm() {
+		let items = []
+		let goods_list = [];
+		state.selectedList = state.list.filter((item) => state.selectedIds.includes(item.id));
+		state.selectedList.map((item) => {
+			// 此处前端做出修改
+			items.push({
+				skuId: item.sku.id,
+				count: item.count,
+				cartId: item.id,
+				categoryId: item.spu.categoryId
+			})
+			goods_list.push({
+				// goods_id: item.goods_id,
+				goods_id: item.spu.id,
+				// goods_num: item.goods_num,
+				goods_num: item.count,
+				// 商品价格id真没有
+				// goods_sku_price_id: item.goods_sku_price_id,
+			});
+		});
+		// return;
+		if (goods_list.length === 0) {
+			sheep.$helper.toast('请选择商品');
+			return;
+		}
+		sheep.$router.go('/pages/order/confirm', {
+			data: JSON.stringify({
+				items
+			}),
+		});
+	}
+
+	function onNumberChange(e, cartItem) {
+		if (e === 0) {
+			cart.delete(cartItem.id);
+			return;
+		}
+		if (cartItem.goods_num === e) return;
+		cartItem.goods_num = e;
+		cart.update({
+			goods_id: cartItem.id,
+			goods_num: e,
+			goods_sku_price_id: cartItem.goods_sku_price_id,
+		});
+	}
+	async function onDelete() {
+		cart.delete(state.selectedIds);
+	}
+</script>
+
+<style lang="scss" scoped>
+	:deep(.ui-fixed) {
+		height: 72rpx;
+	}
+
+	.cart-box {
+		width: 100%;
+
+		.cart-header {
+			height: 70rpx;
+			background-color: #f6f6f6;
+			width: 100%;
+			position: fixed;
+			left: 0;
+			top: v-bind('sys_navBar') rpx;
+			z-index: 1000;
+			box-sizing: border-box;
+		}
+
+		.cart-footer {
+			height: 100rpx;
+			background-color: #fff;
+
+			.pay-btn {
+				width: 180rpx;
+				height: 70rpx;
+				font-size: 28rpx;
+				line-height: 28rpx;
+				font-weight: 500;
+				border-radius: 40rpx;
+			}
+		}
+
+		.cart-content {
+			margin-top: 70rpx;
+
+			.goods-box {
+				background-color: #fff;
+			}
+		}
+	}
+</style>

+ 237 - 0
pages/index/category.vue

@@ -0,0 +1,237 @@
+<!-- 商品分类列表 -->
+<template>
+  <s-layout title="分类" tabbar="/pages/index/category" :bgStyle="{ color: '#fff' }">
+    <view class="s-category">
+      <view class="three-level-wrap ss-flex ss-col-top" :style="[{ height: pageHeight + 'px' }]">
+        <!-- 商品分类(左) -->
+        <scroll-view class="side-menu-wrap" scroll-y :style="[{ height: pageHeight + 'px' }]">
+          <view
+            class="menu-item ss-flex"
+            v-for="(item, index) in state.categoryList"
+            :key="item.id"
+            :class="[{ 'menu-item-active': index === state.activeMenu }]"
+            @tap="onMenu(index)"
+          >
+            <view class="menu-title ss-line-1">
+              {{ item.name }}
+            </view>
+          </view>
+        </scroll-view>
+        <!-- 商品分类(右) -->
+        <scroll-view
+          class="goods-list-box"
+          scroll-y
+          :style="[{ height: pageHeight + 'px' }]"
+          v-if="state.categoryList?.length"
+        >
+          <image
+            v-if="state.categoryList[state.activeMenu].picUrl"
+            class="banner-img"
+            :src="sheep.$url.cdn(state.categoryList[state.activeMenu].picUrl)"
+            mode="widthFix"
+          />
+          <first-one v-if="state.style === 'first_one'" :pagination="state.pagination" />
+          <first-two v-if="state.style === 'first_two'" :pagination="state.pagination" />
+          <second-one
+            v-if="state.style === 'second_one'"
+            :data="state.categoryList"
+            :activeMenu="state.activeMenu"
+          />
+          <uni-load-more
+            v-if="
+              (state.style === 'first_one' || state.style === 'first_two') &&
+              state.pagination.total > 0
+            "
+            :status="state.loadStatus"
+            :content-text="{
+              contentdown: '点击查看更多',
+            }"
+            @tap="loadMore"
+          />
+        </scroll-view>
+      </view>
+    </view>
+  </s-layout>
+</template>
+
+<script setup>
+  import secondOne from './components/second-one.vue';
+  import firstOne from './components/first-one.vue';
+  import firstTwo from './components/first-two.vue';
+  import sheep from '@/sheep';
+  import CategoryApi from '@/sheep/api/product/category';
+  import SpuApi from '@/sheep/api/product/spu';
+  import { onLoad, onReachBottom } from '@dcloudio/uni-app';
+  import { computed, reactive } from 'vue';
+  import _ from 'lodash-es';
+  import { handleTree } from '@/sheep/util';
+
+  const state = reactive({
+    style: 'second_one', // first_one(一级 - 样式一), first_two(二级 - 样式二), second_one(二级)
+    categoryList: [], // 商品分类树
+    activeMenu: 0, // 选中的一级菜单,在 categoryList 的下标
+
+    pagination: {
+      // 商品分页
+      list: [], // 商品列表
+      total: [], // 商品总数
+      pageNo: 1,
+      pageSize: 6,
+    },
+    loadStatus: '',
+  });
+
+  const { safeArea } = sheep.$platform.device;
+  const pageHeight = computed(() => safeArea.height - 44 - 50);
+
+  // 加载商品分类
+  async function getList() {
+    const { code, data } = await CategoryApi.getCategoryList();
+    if (code !== 0) {
+      return;
+    }
+    state.categoryList = handleTree(data);
+  }
+
+  // 选中菜单
+  const onMenu = (val) => {
+    state.activeMenu = val;
+    if (state.style === 'first_one' || state.style === 'first_two') {
+      state.pagination.pageNo = 1;
+      state.pagination.list = [];
+      state.pagination.total = 0;
+      getGoodsList();
+    }
+  };
+
+  // 加载商品列表
+  async function getGoodsList() {
+    // 加载列表
+    state.loadStatus = 'loading';
+    const res = await SpuApi.getSpuPage({
+      categoryId: state.categoryList[state.activeMenu].id,
+      pageNo: state.pagination.pageNo,
+      pageSize: state.pagination.pageSize,
+    });
+    if (res.code !== 0) {
+      return;
+    }
+    // 合并列表
+    state.pagination.list = _.concat(state.pagination.list, res.data.list);
+    state.pagination.total = res.data.total;
+    state.loadStatus = state.pagination.list.length < state.pagination.total ? 'more' : 'noMore';
+  }
+
+  // 加载更多商品
+  function loadMore() {
+    if (state.loadStatus === 'noMore') {
+      return;
+    }
+    state.pagination.pageNo++;
+    getGoodsList();
+  }
+
+  onLoad(async (params) => {
+    await getList();
+
+    // 首页点击分类的处理:查找满足条件的分类
+    const foundCategory = state.categoryList.find(category => category.id === params.id);
+    // 如果找到则调用 onMenu 自动勾选相应分类,否则调用 onMenu(0) 勾选第一个分类
+    onMenu(foundCategory ? state.categoryList.indexOf(foundCategory) : 0);
+  });
+
+  onReachBottom(() => {
+    loadMore();
+  });
+</script>
+
+<style lang="scss" scoped>
+  .s-category {
+    :deep() {
+      .side-menu-wrap {
+        width: 200rpx;
+        height: 100%;
+        padding-left: 12rpx;
+        background-color: #f6f6f6;
+
+        .menu-item {
+          width: 100%;
+          height: 88rpx;
+          position: relative;
+          transition: all linear 0.2s;
+
+          .menu-title {
+            line-height: 32rpx;
+            font-size: 30rpx;
+            font-weight: 400;
+            color: #333;
+            margin-left: 28rpx;
+            position: relative;
+            z-index: 0;
+
+            &::before {
+              content: '';
+              width: 64rpx;
+              height: 12rpx;
+              background: linear-gradient(
+                90deg,
+                var(--ui-BG-Main-gradient),
+                var(--ui-BG-Main-light)
+              ) !important;
+              position: absolute;
+              left: -64rpx;
+              bottom: 0;
+              z-index: -1;
+              transition: all linear 0.2s;
+            }
+          }
+
+          &.menu-item-active {
+            background-color: #fff;
+            border-radius: 20rpx 0 0 20rpx;
+
+            &::before {
+              content: '';
+              position: absolute;
+              right: 0;
+              bottom: -20rpx;
+              width: 20rpx;
+              height: 20rpx;
+              background: radial-gradient(circle at 0 100%, transparent 20rpx, #fff 0);
+            }
+
+            &::after {
+              content: '';
+              position: absolute;
+              top: -20rpx;
+              right: 0;
+              width: 20rpx;
+              height: 20rpx;
+              background: radial-gradient(circle at 0% 0%, transparent 20rpx, #fff 0);
+            }
+
+            .menu-title {
+              font-weight: 600;
+
+              &::before {
+                left: 0;
+              }
+            }
+          }
+        }
+      }
+
+      .goods-list-box {
+        background-color: #fff;
+        width: calc(100vw - 100px);
+        padding: 10px;
+      }
+
+      .banner-img {
+        width: calc(100vw - 130px);
+        border-radius: 5px;
+        margin-bottom: 20rpx;
+      }
+    }
+  }
+</style>

+ 26 - 0
pages/index/components/first-one.vue

@@ -0,0 +1,26 @@
+<!-- 分类展示:first-one 风格  -->
+<template>
+  <view class="ss-flex-col">
+    <view class="goods-box" v-for="item in pagination.list" :key="item.id">
+      <s-goods-column
+        size="sl"
+        :data="item"
+        @click="sheep.$router.go('/pages/goods/index', { id: item.id })"
+      />
+    </view>
+  </view>
+</template>
+
+<script setup>
+  import sheep from '@/sheep';
+
+  const props = defineProps({
+    pagination: Object,
+  });
+</script>
+
+<style lang="scss" scoped>
+  .goods-box {
+    width: 100%;
+  }
+</style>

+ 66 - 0
pages/index/components/first-two.vue

@@ -0,0 +1,66 @@
+<!-- 分类展示:first-two 风格  -->
+<template>
+  <view>
+    <view class="ss-flex flex-wrap">
+      <view class="goods-box" v-for="item in pagination?.list" :key="item.id">
+        <view @click="sheep.$router.go('/pages/goods/index', { id: item.id })">
+          <view class="goods-img">
+            <image class="goods-img" :src="item.picUrl" mode="aspectFit" />
+          </view>
+          <view class="goods-content">
+            <view class="goods-title ss-line-1 ss-m-b-28">{{ item.name }}</view>
+            <view class="goods-price">¥{{ fen2yuan(item.price) }}</view>
+          </view>
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup>
+  import sheep from '@/sheep';
+  import { fen2yuan } from '@/sheep/hooks/useGoods';
+
+  const props = defineProps({
+    pagination: Object,
+  });
+</script>
+
+<style lang="scss" scoped>
+  .goods-box {
+    width: calc((100% - 20rpx) / 2);
+    margin-bottom: 20rpx;
+
+    .goods-img {
+      width: 100%;
+      height: 246rpx;
+      border-radius: 10rpx 10rpx 0px 0px;
+    }
+
+    .goods-content {
+      width: 100%;
+      background: #ffffff;
+      box-shadow: 0px 0px 20rpx 4rpx rgba(199, 199, 199, 0.22);
+      padding: 20rpx 0 32rpx 16rpx;
+      box-sizing: border-box;
+      border-radius: 0 0 10rpx 10rpx;
+
+      .goods-title {
+        font-size: 26rpx;
+        font-weight: bold;
+        color: #333333;
+      }
+
+      .goods-price {
+        font-size: 24rpx;
+        font-family: OPPOSANS;
+        font-weight: 500;
+        color: #e1212b;
+      }
+    }
+
+    &:nth-child(2n + 1) {
+      margin-right: 20rpx;
+    }
+  }
+</style>

+ 80 - 0
pages/index/components/second-one.vue

@@ -0,0 +1,80 @@
+<!-- 分类展示:second-one 风格  -->
+<template>
+  <view>
+    <!-- 一级分类的名字 -->
+    <view class="title-box ss-flex ss-col-center ss-row-center ss-p-b-30">
+      <view class="title-line-left" />
+      <view class="title-text ss-p-x-20">{{ props.data[activeMenu].name }}</view>
+      <view class="title-line-right" />
+    </view>
+    <!-- 二级分类的名字 -->
+    <view class="goods-item-box ss-flex ss-flex-wrap ss-p-b-20">
+      <view
+        class="goods-item"
+        v-for="item in props.data[activeMenu].children"
+        :key="item.id"
+        @tap="
+          sheep.$router.go('/pages/goods/list', {
+            categoryId: item.id,
+          })
+        "
+      >
+        <image class="goods-img" :src="item.picUrl" mode="aspectFill" />
+        <view class="ss-p-10">
+          <view class="goods-title ss-line-1">{{ item.name }}</view>
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup>
+  import sheep from '@/sheep';
+
+  const props = defineProps({
+    data: {
+      type: Object,
+      default: () => ({}),
+    },
+    activeMenu: [Number, String],
+  });
+</script>
+
+<style lang="scss" scoped>
+  .title-box {
+    .title-line-left,
+    .title-line-right {
+      width: 15px;
+      height: 1px;
+      background: #d2d2d2;
+    }
+  }
+
+  .goods-item {
+    width: calc((100% - 20px) / 3);
+    margin-right: 10px;
+    margin-bottom: 10px;
+
+    &:nth-of-type(3n) {
+      margin-right: 0;
+    }
+
+    .goods-img {
+      width: calc((100vw - 140px) / 3);
+      height: calc((100vw - 140px) / 3);
+    }
+
+    .goods-title {
+      font-size: 26rpx;
+      font-weight: bold;
+      color: #333333;
+      line-height: 40rpx;
+      text-align: center;
+    }
+
+    .goods-price {
+      color: $red;
+      line-height: 40rpx;
+    }
+  }
+</style>

+ 88 - 0
pages/index/index.vue

@@ -0,0 +1,88 @@
+<!-- 首页,支持店铺装修 -->
+<template>
+	<view v-if="template">
+		<s-layout title="首页" navbar="custom" tabbar="/pages/index/index" :bgStyle="template.page"
+			:navbarStyle="template.navigationBar" onShareAppMessage>
+			<s-block v-for="(item, index) in template.components" :key="index" :styles="item.property.style">
+				<s-block-item :type="item.id" :data="item.property" :styles="item.property.style" />
+			</s-block>
+		</s-layout>
+	</view>
+</template>
+
+<script setup>
+	import {
+		computed
+	} from 'vue';
+	import {
+		onLoad,
+		onPageScroll,
+		onPullDownRefresh
+	} from '@dcloudio/uni-app';
+	import sheep from '@/sheep';
+	import $share from '@/sheep/platform/share';
+	// 隐藏原生tabBar
+	uni.hideTabBar();
+
+	const template = computed(() => sheep.$store('app').template?.home);
+	// 在此处拦截改变一下首页轮播图 此处先写死后期复活 放到启动函数里
+	// (async function() {
+		// console.log('原代码首页定制化数据',template)
+		// let {
+		// 	data
+		// } = await index2Api.decorate();
+		// console.log('首页导航配置化过高无法兼容',JSON.parse(data[1].value))
+		// 改变首页底部数据 但是没有通过数组id获取商品数据接口
+		// let {
+		// 	data: datas
+		// } = await index2Api.spids();
+		// template.value.data[9].data.goodsIds = datas.list.map(item => item.id);
+		// template.value.data[0].data.list = JSON.parse(data[0].value).map(item => {
+		// 	return {
+		// 		src: item.picUrl,
+		// 		url: item.url,
+		// 		title: item.name,
+		// 		type: "image"
+		// 	}
+		// })
+	// }())
+
+
+	onLoad((options) => {
+		// #ifdef MP
+		// 小程序识别二维码
+		if (options.scene) {
+			const sceneParams = decodeURIComponent(options.scene).split('=');
+      console.log("sceneParams=>",sceneParams);
+			options[sceneParams[0]] = sceneParams[1];
+		}
+		// #endif
+
+		// 预览模板
+		if (options.templateId) {
+			sheep.$store('app').init(options.templateId);
+		}
+
+		// 解析分享信息
+		if (options.spm) {
+			$share.decryptSpm(options.spm);
+		}
+
+		// 进入指定页面(完整页面路径)
+		if (options.page) {
+			sheep.$router.go(decodeURIComponent(options.page));
+		}
+	});
+
+	// 下拉刷新
+	onPullDownRefresh(() => {
+		sheep.$store('app').init();
+		setTimeout(function() {
+			uni.stopPullDownRefresh();
+		}, 800);
+	});
+
+	onPageScroll(() => {});
+</script>
+
+<style></style>

+ 39 - 0
pages/index/login.vue

@@ -0,0 +1,39 @@
+<!-- 微信公众号的登录回调页 -->
+<template>
+  <!-- 空登陆页 -->
+  <view />
+</template>
+
+<script setup>
+  import sheep from '@/sheep';
+  import { onLoad } from '@dcloudio/uni-app';
+
+  onLoad(async (options) => {
+    // #ifdef H5
+    // 将 search 参数赋值到 options 中,方便下面解析
+    new URLSearchParams(location.search).forEach((value, key) => {
+      options[key] = value;
+    });
+    // 执行登录 or 绑定,注意需要 await 绑定
+    const event = options.event;
+    const code = options.code;
+    const state = options.state;
+    if (event === 'login') { // 场景一:登录
+      await sheep.$platform.useProvider().login(code, state);
+    } else if (event === 'bind') { // 场景二:绑定
+      await sheep.$platform.useProvider().bind(code, state);
+    }
+
+    // 检测 H5 登录回调
+    let returnUrl = uni.getStorageSync('returnUrl');
+    if (returnUrl) {
+      uni.removeStorage({key:'returnUrl'});
+      location.replace(returnUrl);
+    } else {
+      uni.switchTab({
+        url: '/',
+      });
+    }
+    // #endif
+  });
+</script>

+ 51 - 0
pages/index/page.vue

@@ -0,0 +1,51 @@
+<!-- 自定义页面:支持装修 -->
+<template>
+  <s-layout
+    :title="state.name"
+    navbar="custom"
+    :bgStyle="state.page"
+    :navbarStyle="state.navigationBar"
+    onShareAppMessage
+    showLeftButton
+  >
+    <s-block v-for="(item, index) in state.components" :key="index" :styles="item.property.style">
+      <s-block-item :type="item.id" :data="item.property" :styles="item.property.style" />
+    </s-block>
+  </s-layout>
+</template>
+
+<script setup>
+  import { reactive } from 'vue';
+  import { onLoad, onPageScroll } from '@dcloudio/uni-app';
+  import DiyApi from '@/sheep/api/promotion/diy';
+
+  const state = reactive({
+    name: '',
+    components: [],
+    navigationBar: {},
+    page: {},
+  });
+  onLoad(async (options) => {
+    let id = options.id
+
+    // #ifdef MP
+    // 小程序预览自定义页面
+    if (options.scene) {
+      const sceneParams = decodeURIComponent(options.scene).split('=');
+      id = sceneParams[1];
+    }
+    // #endif
+
+    const { code, data } = await DiyApi.getDiyPage(id);
+    if (code === 0) {
+      state.name = data.name;
+      state.components = data.property?.components;
+      state.navigationBar = data.property?.navigationBar;
+      state.page = data.property?.page;
+    }
+  });
+
+  onPageScroll(() => {});
+</script>
+
+<style></style>

+ 119 - 0
pages/index/search.vue

@@ -0,0 +1,119 @@
+<!-- 搜索界面 -->
+<template>
+  <s-layout class="set-wrap" title="搜索" :bgStyle="{ color: '#FFF' }">
+    <view class="ss-p-x-24">
+      <view class="ss-flex ss-col-center">
+        <uni-search-bar
+          class="ss-flex-1"
+          radius="33"
+          placeholder="请输入关键字"
+          cancelButton="none"
+          :focus="true"
+          @confirm="onSearch($event.value)"
+        />
+      </view>
+      <view class="ss-flex ss-row-between ss-col-center">
+        <view class="serach-history">搜索历史</view>
+        <button class="clean-history ss-reset-button" @tap="onDelete"> 清除搜索历史 </button>
+      </view>
+      <view class="ss-flex ss-col-center ss-row-left ss-flex-wrap">
+        <button
+          class="history-btn ss-reset-button"
+          @tap="onSearch(item)"
+          v-for="(item, index) in state.historyList"
+          :key="index"
+        >
+          {{ item }}
+        </button>
+      </view>
+    </view>
+  </s-layout>
+</template>
+
+<script setup>
+  import { reactive } from 'vue';
+  import sheep from '@/sheep';
+  import { onLoad } from '@dcloudio/uni-app';
+
+  const state = reactive({
+    historyList: [],
+  });
+
+  // 搜索
+  function onSearch(keyword) {
+    if (!keyword) {
+      return;
+    }
+    saveSearchHistory(keyword);
+    // 前往商品列表(带搜索条件)
+    sheep.$router.go('/pages/goods/list', { keyword });
+  }
+
+  // 保存搜索历史
+  function saveSearchHistory(keyword) {
+    // 如果关键词在搜索历史中,则把此关键词先移除
+    if (state.historyList.includes(keyword)) {
+      state.historyList.splice(state.historyList.indexOf(keyword), 1);
+    }
+    // 置顶关键词
+    state.historyList.unshift(keyword);
+
+    // 最多保留 10 条记录
+    if (state.historyList.length >= 10) {
+      state.historyList.length = 10;
+    }
+    uni.setStorageSync('searchHistory', state.historyList);
+  }
+
+  function onDelete() {
+    uni.showModal({
+      title: '提示',
+      content: '确认清除搜索历史吗?',
+      success: function (res) {
+        if (res.confirm) {
+          state.historyTag = [];
+          uni.removeStorageSync('searchHistory');
+        }
+      },
+    });
+  }
+
+  onLoad(() => {
+    state.historyList = uni.getStorageSync('searchHistory') || [];
+  });
+</script>
+
+<style lang="scss" scoped>
+  .serach-title {
+    font-size: 30rpx;
+    font-weight: 500;
+    color: #333333;
+  }
+
+  .uni-searchbar {
+    padding-left: 0;
+  }
+
+  .serach-history {
+    font-weight: bold;
+    color: #333333;
+    font-size: 30rpx;
+  }
+
+  .clean-history {
+    font-weight: 500;
+    color: #999999;
+    font-size: 28rpx;
+  }
+
+  .history-btn {
+    padding: 0 38rpx;
+    height: 60rpx;
+    background: #f5f6f8;
+    border-radius: 30rpx;
+    font-size: 28rpx;
+    color: #333333;
+    max-width: 690rpx;
+    margin: 0 20rpx 20rpx 0;
+  }
+</style>

+ 42 - 0
pages/index/user.vue

@@ -0,0 +1,42 @@
+<!-- 个人中心:支持装修 -->
+<template>
+  <s-layout
+    title="我的"
+    tabbar="/pages/index/user"
+    navbar="custom"
+    :bgStyle="template.page"
+    :navbarStyle="template.navigationBar"
+    onShareAppMessage
+  >
+    <s-block v-for="(item, index) in template.components" :key="index" :styles="item.property.style">
+      <s-block-item :type="item.id" :data="item.property" :styles="item.property.style" />
+    </s-block>
+  </s-layout>
+</template>
+
+<script setup>
+  import { computed } from 'vue';
+  import { onShow, onPageScroll, onPullDownRefresh } from '@dcloudio/uni-app';
+  import sheep from '@/sheep';
+
+  // 隐藏原生tabBar
+  uni.hideTabBar();
+
+  const template = computed(() => sheep.$store('app').template.user);
+  const isLogin = computed(() => sheep.$store('user').isLogin);
+
+  onShow(() => {
+    sheep.$store('user').updateUserData();
+  });
+
+  onPullDownRefresh(() => {
+    sheep.$store('user').updateUserData();
+    setTimeout(function () {
+      uni.stopPullDownRefresh();
+    }, 800);
+  });
+
+  onPageScroll(() => {});
+</script>
+
+<style></style>

+ 278 - 0
pages/order/addressSelection.vue

@@ -0,0 +1,278 @@
+<!-- 下单界面,收货地址 or 自提门店的选择组件 -->
+<template>
+  <view class="allAddress" :style="state.isPickUp ? '' : 'padding-top:10rpx;'">
+    <view class="nav flex flex-wrap">
+      <view
+        class="item font-color"
+        :class="state.deliveryType === 1 ? 'on' : 'on2'"
+        @tap="switchDeliveryType(1)"
+        v-if="state.isPickUp"
+      />
+      <view
+        class="item font-color"
+        :class="state.deliveryType === 2 ? 'on' : 'on2'"
+        @tap="switchDeliveryType(2)"
+        v-if="state.isPickUp"
+      />
+    </view>
+    <!-- 情况一:收货地址的选择 -->
+    <view
+      class="address flex flex-wrap flex-center ss-row-between"
+      @tap="onSelectAddress"
+      v-if="state.deliveryType === 1"
+      :style="state.isPickUp ? '' : 'border-top-left-radius: 14rpx;border-top-right-radius: 14rpx;'"
+    >
+      <view class="addressCon" v-if="state.addressInfo.name">
+        <view class="name"
+          >{{ state.addressInfo.name }}
+          <text class="phone">{{ state.addressInfo.mobile }}</text>
+        </view>
+        <view class="flex flex-wrap">
+          <text class="default font-color" v-if="state.addressInfo.defaultStatus">[默认]</text>
+          <text class="line2"
+            >{{ state.addressInfo.areaName }} {{ state.addressInfo.detailAddress }}</text
+          >
+        </view>
+      </view>
+      <view class="addressCon" v-else>
+        <view class="setaddress">设置收货地址</view>
+      </view>
+      <view class="iconfont">
+        <view class="ss-rest-button">
+          <text class="_icon-forward" />
+        </view>
+      </view>
+    </view>
+    <!-- 情况二:门店的选择 -->
+    <view class="address flex flex-wrap flex-center ss-row-between" v-else @tap="onSelectAddress">
+      <view class="addressCon" v-if="state.pickUpInfo.name">
+        <view class="name"
+          >{{ state.pickUpInfo.name }}
+          <text class="phone">{{ state.pickUpInfo.phone }}</text>
+        </view>
+        <view class="line1">
+          {{ state.pickUpInfo.areaName }}{{ ', ' + state.pickUpInfo.detailAddress }}
+        </view>
+      </view>
+      <view class="addressCon" v-else>
+        <view class="setaddress">选择自提门店</view>
+      </view>
+      <view class="iconfont">
+        <view class="ss-rest-button">
+          <text class="_icon-forward" />
+        </view>
+      </view>
+    </view>
+    <view class="line">
+      <image :src="sheep.$url.static('/static/images/line.png', 'local')" />
+    </view>
+  </view>
+</template>
+
+<script setup>
+  import { computed } from 'vue';
+  import sheep from '@/sheep';
+  import { isEmpty } from 'lodash-es';
+
+  const props = defineProps({
+    modelValue: {
+      type: Object,
+      default() {},
+    },
+  });
+  const emits = defineEmits(['update:modelValue']);
+
+  // computed 解决父子组件双向数据同步
+  const state = computed({
+    get() {
+      return new Proxy(props.modelValue, {
+        set(obj, name, val) {
+          emits('update:modelValue', {
+            ...obj,
+            [name]: val,
+          });
+          return true;
+        },
+      });
+    },
+    set(val) {
+      emits('update:modelValue', val);
+    },
+  });
+
+  // 选择地址
+  function onSelectAddress() {
+    let emitName = 'SELECT_ADDRESS';
+    let addressPage = '/pages/user/address/list?type=select';
+    if (state.value.deliveryType === 2) {
+      emitName = 'SELECT_PICK_UP_INFO';
+      addressPage = '/pages/user/goods_details_store/index';
+    }
+    uni.$once(emitName, (e) => {
+      changeConsignee(e.addressInfo);
+    });
+    sheep.$router.go(addressPage);
+  }
+
+  // 更改收货人地址&计算订单信息
+  async function changeConsignee(addressInfo = {}) {
+    if (!isEmpty(addressInfo)) {
+      if (state.value.deliveryType === 1) {
+        state.value.addressInfo = addressInfo;
+      }
+      if (state.value.deliveryType === 2) {
+        state.value.pickUpInfo = addressInfo;
+      }
+    }
+  }
+
+  // 收货方式切换
+  const switchDeliveryType = (type) => {
+    state.value.deliveryType = type;
+  };
+</script>
+
+<style scoped lang="scss">
+  .allAddress .font-color {
+    color: #e93323 !important;
+  }
+  .line2 {
+    width: 504rpx;
+  }
+  .textR {
+    text-align: right;
+  }
+
+  .line {
+    width: 100%;
+    height: 3rpx;
+  }
+
+  .line image {
+    width: 100%;
+    height: 100%;
+    display: block;
+  }
+
+  .address {
+    padding: 28rpx;
+    background-color: #fff;
+    box-sizing: border-box;
+  }
+
+  .address .addressCon {
+    width: 596rpx;
+    font-size: 26rpx;
+    color: #666;
+  }
+
+  .address .addressCon .name {
+    font-size: 30rpx;
+    color: #282828;
+    font-weight: bold;
+    margin-bottom: 10rpx;
+  }
+
+  .address .addressCon .name .phone {
+    margin-left: 50rpx;
+  }
+
+  .address .addressCon .default {
+    margin-right: 12rpx;
+  }
+
+  .address .addressCon .setaddress {
+    color: #333;
+    font-size: 28rpx;
+  }
+
+  .address .iconfont {
+    font-size: 35rpx;
+    color: #707070;
+  }
+
+  .allAddress {
+    width: 100%;
+    background: linear-gradient(to bottom, #e93323 0%, #f5f5f5 100%);
+    // background-image: linear-gradient(to bottom, #e93323 0%, #f5f5f5 100%);
+    // background-image: -webkit-linear-gradient(to bottom, #e93323 0%, #f5f5f5 100%);
+    // background-image: -moz-linear-gradient(to bottom, #e93323 0%, #f5f5f5 100%);
+    //padding: 100rpx 30rpx 0 30rpx;
+    padding-top: 100rpx;
+    padding-bottom: 10rpx;
+  }
+
+  .allAddress .nav {
+    width: 690rpx;
+    margin: 0 auto;
+  }
+
+  .allAddress .nav .item {
+    width: 334rpx;
+  }
+
+  .allAddress .nav .item.on {
+    position: relative;
+    width: 230rpx;
+  }
+
+  .allAddress .nav .item.on::before {
+    position: absolute;
+    bottom: 0;
+    content: '快递配送';
+    font-size: 28rpx;
+    display: block;
+    height: 0;
+    width: 336rpx;
+    border-width: 0 20rpx 80rpx 0;
+    border-style: none solid solid;
+    border-color: transparent transparent #fff;
+    z-index: 2;
+    border-radius: 14rpx 36rpx 0 0;
+    text-align: center;
+    line-height: 80rpx;
+  }
+
+  .allAddress .nav .item:nth-of-type(2).on::before {
+    content: '到店自提';
+    border-width: 0 0 80rpx 20rpx;
+    border-radius: 36rpx 14rpx 0 0;
+  }
+
+  .allAddress .nav .item.on2 {
+    position: relative;
+  }
+
+  .allAddress .nav .item.on2::before {
+    position: absolute;
+    bottom: 0;
+    content: '到店自提';
+    font-size: 28rpx;
+    display: block;
+    height: 0;
+    width: 401rpx;
+    border-width: 0 0 60rpx 60rpx;
+    border-style: none solid solid;
+    border-color: transparent transparent #f7c1bd;
+    border-radius: 36rpx 14rpx 0 0;
+    text-align: center;
+    line-height: 60rpx;
+  }
+
+  .allAddress .nav .item:nth-of-type(1).on2::before {
+    content: '快递配送';
+    border-width: 0 60rpx 60rpx 0;
+    border-radius: 14rpx 36rpx 0 0;
+  }
+
+  .allAddress .address {
+    width: 690rpx;
+    max-height: 180rpx;
+    margin: 0 auto;
+  }
+
+  .allAddress .line {
+    width: 100%;
+    margin: 0 auto;
+  }
+</style>

+ 351 - 0
pages/order/aftersale/apply.vue

@@ -0,0 +1,351 @@
+<!-- 售后申请 -->
+<template>
+  <s-layout title="申请售后">
+    <!-- 售后商品 -->
+    <view class="goods-box">
+      <s-goods-item
+        :img="state.item.picUrl"
+        :title="state.item.spuName"
+        :skuText="state.item.properties?.map((property) => property.valueName).join(' ')"
+        :price="state.item.price"
+        :num="state.item.count"
+      />
+    </view>
+
+    <uni-forms ref="form" v-model="formData" :rules="rules" label-position="top">
+      <!-- 售后类型 -->
+      <view class="refund-item">
+        <view class="item-title ss-m-b-20">售后类型</view>
+        <view class="ss-flex-col">
+          <radio-group @change="onRefundChange">
+            <label
+              class="ss-flex ss-col-center ss-p-y-10"
+              v-for="(item, index) in state.wayList"
+              :key="index"
+            >
+              <radio
+                :checked="formData.type === item.value"
+                color="var(--ui-BG-Main)"
+                style="transform: scale(0.8)"
+                :value="item.value"
+              />
+              <view class="item-value ss-m-l-8">{{ item.text }}</view>
+            </label>
+          </radio-group>
+        </view>
+      </view>
+      <!-- 退款金额 -->
+      <view class="refund-item ss-flex ss-col-center ss-row-between" @tap="state.showModal = true">
+        <text class="item-title">退款金额</text>
+        <view class="ss-flex refund-cause ss-col-center">
+          <text class="ss-m-r-20">¥{{ fen2yuan(state.item.payPrice) }}</text>
+        </view>
+      </view>
+      <!-- 申请原因 -->
+      <view class="refund-item ss-flex ss-col-center ss-row-between" @tap="state.showModal = true">
+        <text class="item-title">申请原因</text>
+        <view class="ss-flex refund-cause ss-col-center">
+          <text class="ss-m-r-20" v-if="formData.applyReason">{{ formData.applyReason }}</text>
+          <text class="ss-m-r-20" v-else>请选择申请原因~</text>
+          <text class="cicon-forward" style="height: 28rpx"></text>
+        </view>
+      </view>
+
+      <!-- 留言 -->
+      <view class="refund-item">
+        <view class="item-title ss-m-b-20">相关描述</view>
+        <view class="describe-box">
+          <uni-easyinput
+            :inputBorder="false"
+            class="describe-content"
+            type="textarea"
+            maxlength="120"
+            autoHeight
+            v-model="formData.applyDescription"
+            placeholder="客官~请描述您遇到的问题,建议上传照片"
+          />
+          <!-- TODO 芋艿:上传的测试 -->
+          <view class="upload-img">
+            <s-uploader
+              v-model:url="formData.images"
+              fileMediatype="image"
+              limit="9"
+              mode="grid"
+              :imageStyles="{ width: '168rpx', height: '168rpx' }"
+            />
+          </view>
+        </view>
+      </view>
+    </uni-forms>
+
+    <!-- 底部按钮 -->
+    <su-fixed bottom placeholder>
+      <view class="foot-wrap">
+        <view class="foot_box ss-flex ss-col-center ss-row-between ss-p-x-30">
+          <button class="ss-reset-button contcat-btn" @tap="sheep.$router.go('/pages/chat/index')">
+            联系客服
+          </button>
+          <button class="ss-reset-button ui-BG-Main-Gradient sub-btn" @tap="submit">提交</button>
+        </view>
+      </view>
+    </su-fixed>
+
+    <!-- 申请原因弹窗 -->
+    <su-popup :show="state.showModal" round="10" :showClose="true" @close="state.showModal = false">
+      <view class="modal-box page_box">
+        <view class="modal-head item-title head_box ss-flex ss-row-center ss-col-center">
+          申请原因
+        </view>
+        <view class="modal-content content_box">
+          <radio-group @change="onChange">
+            <label class="radio ss-flex ss-col-center" v-for="item in state.reasonList" :key="item">
+              <view class="ss-flex-1 ss-p-20">{{ item }}</view>
+              <radio
+                :value="item"
+                color="var(--ui-BG-Main)"
+                :checked="item === state.currentValue"
+              />
+            </label>
+          </radio-group>
+        </view>
+        <view class="modal-foot foot_box ss-flex ss-row-center ss-col-center">
+          <button class="ss-reset-button close-btn ui-BG-Main-Gradient" @tap="onReason">
+            确定
+          </button>
+        </view>
+      </view>
+    </su-popup>
+  </s-layout>
+</template>
+
+<script setup>
+  import sheep from '@/sheep';
+  import { onLoad } from '@dcloudio/uni-app';
+  import { reactive, ref } from 'vue';
+  import OrderApi from '@/sheep/api/trade/order';
+  import TradeConfigApi from '@/sheep/api/trade/config';
+  import { fen2yuan } from '@/sheep/hooks/useGoods';
+  import AfterSaleApi from '@/sheep/api/trade/afterSale';
+  import { WxaSubscribeTemplate } from '@/sheep/util/const';
+
+  const form = ref(null);
+  const state = reactive({
+    orderId: 0, // 订单编号
+    itemId: 0, // 订单项编号
+    order: {}, // 订单
+    item: {}, // 订单项
+    config: {}, // 交易配置
+
+    // 售后类型
+    wayList: [
+      {
+        text: '仅退款',
+        value: '10',
+      },
+      {
+        text: '退款退货',
+        value: '20',
+      },
+    ],
+    reasonList: [], // 可选的申请原因数组
+    showModal: false, // 是否显示申请原因弹窗
+    currentValue: '', // 当前选择的售后原因
+  });
+  let formData = reactive({
+    way: '',
+    applyReason: '',
+    applyDescription: '',
+    images: [],
+  });
+  const rules = reactive({});
+
+  // 提交表单
+  async function submit() {
+    let data = {
+      orderItemId: state.itemId,
+      refundPrice: state.item.payPrice,
+      ...formData,
+    };
+    const { code } = await AfterSaleApi.createAfterSale(data);
+    if (code === 0) {
+      uni.showToast({
+        title: '申请成功',
+      });
+      sheep.$router.redirect('/pages/order/aftersale/list');
+    }
+  }
+
+  // 选择售后类型
+  function onRefundChange(e) {
+    formData.way = e.detail.value;
+    // 清理理由
+    state.reasonList =
+      formData.way === '10'
+        ? state.config.afterSaleRefundReasons || []
+        : state.config.afterSaleReturnReasons || [];
+    formData.applyReason = '';
+    state.currentValue = '';
+  }
+
+  // 选择申请原因
+  function onChange(e) {
+    state.currentValue = e.detail.value;
+  }
+
+  // 确定
+  function onReason() {
+    formData.applyReason = state.currentValue;
+    state.showModal = false;
+  }
+
+  onLoad(async (options) => {
+    // 解析参数
+    if (!options.orderId || !options.itemId) {
+      sheep.$helper.toast(`缺少订单信息,请检查`);
+      return;
+    }
+    state.orderId = options.orderId;
+    state.itemId = parseInt(options.itemId);
+
+    // 读取订单信息
+    const { code, data } = await OrderApi.getOrderDetail(state.orderId);
+    if (code !== 0) {
+      return;
+    }
+    state.order = data;
+    state.item = data.items.find((item) => item.id === state.itemId) || {};
+
+    // 设置选项
+    if (state.order.status === 10) {
+      state.wayList.splice(1, 1);
+    }
+
+    // 读取配置
+    state.config = (await TradeConfigApi.getTradeConfig()).data;
+  });
+</script>
+
+<style lang="scss" scoped>
+  .item-title {
+    font-size: 30rpx;
+    font-weight: bold;
+    color: rgba(51, 51, 51, 1);
+    // margin-bottom: 20rpx;
+  }
+
+  // 售后项目
+  .refund-item {
+    background-color: #fff;
+    border-bottom: 1rpx solid #f5f5f5;
+    padding: 30rpx;
+
+    &:last-child {
+      border: none;
+    }
+
+    // 留言
+    .describe-box {
+      width: 690rpx;
+      background: rgba(249, 250, 251, 1);
+      padding: 30rpx;
+      box-sizing: border-box;
+      border-radius: 20rpx;
+
+      .describe-content {
+        height: 200rpx;
+        font-size: 24rpx;
+        font-weight: 400;
+        color: #333;
+      }
+    }
+
+    // 联系方式
+    .input-box {
+      height: 84rpx;
+      background: rgba(249, 250, 251, 1);
+      border-radius: 20rpx;
+    }
+  }
+
+  .goods-box {
+    background: #fff;
+    padding: 20rpx;
+    margin-bottom: 20rpx;
+  }
+
+  .foot-wrap {
+    height: 100rpx;
+    width: 100%;
+  }
+
+  .foot_box {
+    height: 100rpx;
+    background-color: #fff;
+
+    .sub-btn {
+      width: 336rpx;
+      line-height: 74rpx;
+      border-radius: 38rpx;
+      color: rgba(#fff, 0.9);
+      font-size: 28rpx;
+    }
+
+    .contcat-btn {
+      width: 336rpx;
+      line-height: 74rpx;
+      background: rgba(238, 238, 238, 1);
+      border-radius: 38rpx;
+      font-size: 28rpx;
+      font-weight: 400;
+      color: rgba(51, 51, 51, 1);
+    }
+  }
+
+  .modal-box {
+    width: 750rpx;
+    // height: 680rpx;
+    border-radius: 30rpx 30rpx 0 0;
+    background: #fff;
+
+    .modal-head {
+      height: 100rpx;
+      font-size: 30rpx;
+    }
+
+    .modal-content {
+      font-size: 28rpx;
+    }
+
+    .modal-foot {
+      .close-btn {
+        width: 710rpx;
+        line-height: 80rpx;
+        border-radius: 40rpx;
+        color: rgba(#fff, 0.9);
+      }
+    }
+  }
+
+  .success-box {
+    width: 600rpx;
+    padding: 90rpx 0 64rpx 0;
+
+    .cicon-check-round {
+      font-size: 96rpx;
+      color: #04b750;
+    }
+
+    .success-title {
+      font-weight: 500;
+      color: #333333;
+      font-size: 32rpx;
+    }
+
+    .success-btn {
+      width: 492rpx;
+      height: 70rpx;
+      background: linear-gradient(90deg, var(--ui-BG-Main-gradient), var(--ui-BG-Main));
+      border-radius: 35rpx;
+    }
+  }
+</style>

+ 379 - 0
pages/order/aftersale/detail.vue

@@ -0,0 +1,379 @@
+<!-- 售后详情 -->
+<template>
+  <s-layout title="售后详情" :navbar="!isEmpty(state.info) && state.loading ? 'inner' : 'normal'">
+    <view class="content_box" v-if="!isEmpty(state.info) && state.loading">
+      <!-- 步骤条 -->
+      <view
+        class="steps-box ss-flex"
+        :style="[
+          {
+            marginTop: '-' + Number(statusBarHeight + 88) + 'rpx',
+            paddingTop: Number(statusBarHeight + 88) + 'rpx',
+          },
+        ]"
+      >
+        <view class="ss-flex">
+          <view class="steps-item" v-for="(item, index) in state.list" :key="index">
+            <view class="ss-flex">
+              <text
+                class="sicon-circleclose"
+                v-if="state.list.length - 1 === index && [61, 62, 63].includes(state.info.status)"
+              />
+              <text
+                class="sicon-circlecheck"
+                v-else
+                :class="state.active >= index ? 'activity-color' : 'info-color'"
+              />
+
+              <view
+                v-if="state.list.length - 1 !== index"
+                class="line"
+                :class="state.active >= index ? 'activity-bg' : 'info-bg'"
+              />
+            </view>
+            <view
+              class="steps-item-title"
+              :class="state.active >= index ? 'activity-color' : 'info-color'"
+            >
+              {{ item.title }}
+            </view>
+          </view>
+        </view>
+      </view>
+
+      <!-- 服务状态 -->
+      <view
+        class="status-box ss-flex ss-col-center ss-row-between ss-m-x-20"
+        @tap="sheep.$router.go('/pages/order/aftersale/log', { id: state.id })"
+      >
+        <view class="">
+          <view class="status-text">
+            {{ formatAfterSaleStatusDescription(state.info) }}
+          </view>
+          <view class="status-time">
+            {{ sheep.$helper.timeFormat(state.info.updateTime, 'yyyy-mm-dd hh:MM:ss') }}
+          </view>
+        </view>
+        <text class="ss-iconfont _icon-forward" style="color: #666" />
+      </view>
+
+      <!-- 退款金额 -->
+      <view class="aftersale-money ss-flex ss-col-center ss-row-between">
+        <view class="aftersale-money--title">退款总额</view>
+        <view class="aftersale-money--num">¥{{ fen2yuan(state.info.refundPrice) }}</view>
+      </view>
+      <!-- 服务商品 -->
+      <view class="order-shop">
+        <s-goods-item
+          :img="state.info.picUrl"
+          :title="state.info.spuName"
+          :titleWidth="480"
+          :skuText="state.info.properties.map((property) => property.valueName).join(' ')"
+          :num="state.info.count"
+        />
+      </view>
+
+      <!-- 服务内容 -->
+      <view class="aftersale-content">
+        <view class="aftersale-item ss-flex ss-col-center">
+          <view class="item-title">服务单号:</view>
+          <view class="item-content ss-m-r-16">{{ state.info.no }}</view>
+          <button class="ss-reset-button copy-btn" @tap="onCopy">复制</button>
+        </view>
+        <view class="aftersale-item ss-flex ss-col-center">
+          <view class="item-title">申请时间:</view>
+          <view class="item-content">
+            {{ sheep.$helper.timeFormat(state.info.createTime, 'yyyy-mm-dd hh:MM:ss') }}
+          </view>
+        </view>
+        <view class="aftersale-item ss-flex ss-col-center">
+          <view class="item-title">售后类型:</view>
+          <view class="item-content">{{ state.info.way === 10 ? '仅退款' : '退款退货' }}</view>
+        </view>
+        <view class="aftersale-item ss-flex ss-col-center">
+          <view class="item-title">申请原因:</view>
+          <view class="item-content">{{ state.info.applyReason }}</view>
+        </view>
+        <view class="aftersale-item ss-flex ss-col-center">
+          <view class="item-title">相关描述:</view>
+          <view class="item-content">{{ state.info.applyDescription }}</view>
+        </view>
+      </view>
+    </view>
+
+    <!-- 操作区 -->
+    <s-empty
+      v-if="isEmpty(state.info) && state.loading"
+      icon="/static/order-empty.png"
+      text="暂无该订单售后详情"
+    />
+    <su-fixed bottom placeholder bg="bg-white" v-if="!isEmpty(state.info)">
+      <view class="foot_box">
+        <button
+          class="ss-reset-button btn"
+          v-if="state.info.buttons?.includes('cancel')"
+          @tap="onApply(state.info.id)"
+        >
+          取消申请
+        </button>
+        <button
+          class="ss-reset-button btn"
+          v-if="state.info.buttons?.includes('delivery')"
+          @tap="sheep.$router.go('/pages/order/aftersale/return-delivery', { id: state.info.id })"
+        >
+          填写退货
+        </button>
+        <button
+          class="ss-reset-button contcat-btn btn"
+          @tap="sheep.$router.go('/pages/chat/index')"
+        >
+          联系客服
+        </button>
+      </view>
+    </su-fixed>
+  </s-layout>
+</template>
+
+<script setup>
+  import sheep from '@/sheep';
+  import { onLoad } from '@dcloudio/uni-app';
+  import { reactive } from 'vue';
+  import { isEmpty } from 'lodash-es';
+  import {
+    fen2yuan,
+    formatAfterSaleStatusDescription,
+    handleAfterSaleButtons,
+  } from '@/sheep/hooks/useGoods';
+  import AfterSaleApi from '@/sheep/api/trade/afterSale';
+
+  const statusBarHeight = sheep.$platform.device.statusBarHeight * 2;
+  const headerBg = sheep.$url.css('/static/img/shop/order/order_bg.png');
+  const state = reactive({
+    id: 0, // 售后编号
+    info: {}, // 收货信息
+    loading: false,
+    active: 0, // 在 list 的激活位置
+    list: [
+      {
+        title: '提交申请',
+      },
+      {
+        title: '处理中',
+      },
+      {
+        title: '完成',
+      },
+    ], // 时间轴
+  });
+
+  function onApply(id) {
+    uni.showModal({
+      title: '提示',
+      content: '确定要取消此申请吗?',
+      success: async function (res) {
+        if (!res.confirm) {
+          return;
+        }
+        const { code } = await AfterSaleApi.cancelAfterSale(id);
+        if (code === 0) {
+          await getDetail(id);
+        }
+      },
+    });
+  }
+
+  // 复制
+  const onCopy = () => {
+    sheep.$helper.copyText(state.info.no);
+  };
+
+  async function getDetail(id) {
+    state.loading = true;
+    const { code, data } = await AfterSaleApi.getAfterSale(id);
+    if (code !== 0) {
+      state.info = null;
+      return;
+    }
+    state.info = data;
+    handleAfterSaleButtons(state.info);
+
+    // 处理时间轴
+    if ([10].includes(state.info.status)) {
+      state.active = 0;
+    } else if ([20, 30].includes(state.info.status)) {
+      state.active = 1;
+    } else if ([40, 50].includes(state.info.status)) {
+      state.active = 2;
+    } else if ([61, 62, 63].includes(state.info.status)) {
+      state.active = 2;
+    }
+  }
+
+  onLoad((options) => {
+    if (!options.id) {
+      sheep.$helper.toast(`缺少订单信息,请检查`);
+      return;
+    }
+    state.id = options.id;
+    getDetail(options.id);
+  });
+</script>
+
+<style lang="scss" scoped>
+  // 步骤条
+  .steps-box {
+    width: 100%;
+    height: 190rpx;
+    background: v-bind(headerBg) no-repeat,
+      linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
+    background-size: 750rpx 100%;
+    padding-left: 72rpx;
+
+    .steps-item {
+      .sicon-circleclose {
+        font-size: 24rpx;
+        color: #fff;
+      }
+
+      .sicon-circlecheck {
+        font-size: 24rpx;
+      }
+
+      .steps-item-title {
+        font-size: 24rpx;
+        font-weight: 400;
+        margin-top: 16rpx;
+        margin-left: -36rpx;
+        width: 100rpx;
+        text-align: center;
+      }
+    }
+  }
+
+  .activity-color {
+    color: #fff;
+  }
+
+  .info-color {
+    color: rgba(#fff, 0.4);
+  }
+
+  .activity-bg {
+    background: #fff;
+  }
+
+  .info-bg {
+    background: rgba(#fff, 0.4);
+  }
+
+  .line {
+    width: 270rpx;
+    height: 4rpx;
+  }
+
+  // 服务状态
+  .status-box {
+    position: relative;
+    z-index: 3;
+    background-color: #fff;
+    border-radius: 20rpx 20rpx 0px 0px;
+    padding: 20rpx;
+    margin-top: -20rpx;
+
+    .status-text {
+      font-size: 28rpx;
+
+      font-weight: 500;
+      color: rgba(51, 51, 51, 1);
+      margin-bottom: 20rpx;
+    }
+
+    .status-time {
+      font-size: 24rpx;
+
+      font-weight: 400;
+      color: rgba(153, 153, 153, 1);
+    }
+  }
+
+  // 退款金额
+  .aftersale-money {
+    background-color: #fff;
+    height: 98rpx;
+    padding: 0 20rpx;
+    margin: 20rpx;
+
+    .aftersale-money--title {
+      font-size: 28rpx;
+
+      font-weight: 500;
+      color: rgba(51, 51, 51, 1);
+    }
+
+    .aftersale-money--num {
+      font-size: 28rpx;
+      font-family: OPPOSANS;
+      font-weight: 500;
+      color: #ff3000;
+    }
+  }
+
+  // order-shop
+  .order-shop {
+    padding: 20rpx;
+    background-color: #fff;
+    margin: 0 20rpx 20rpx 20rpx;
+  }
+
+  // 服务内容
+  .aftersale-content {
+    background-color: #fff;
+    padding: 20rpx;
+    margin: 0 20rpx;
+
+    .aftersale-item {
+      height: 60rpx;
+
+      .copy-btn {
+        background: #eeeeee;
+        color: #333;
+        border-radius: 20rpx;
+        width: 75rpx;
+        height: 40rpx;
+        font-size: 22rpx;
+      }
+
+      .item-title {
+        color: #999;
+        font-size: 28rpx;
+      }
+
+      .item-content {
+        color: #333;
+        font-size: 28rpx;
+      }
+    }
+  }
+
+  // 底部功能
+  .foot_box {
+    height: 100rpx;
+    background-color: #fff;
+    display: flex;
+    align-items: center;
+    justify-content: flex-end;
+
+    .btn {
+      width: 160rpx;
+      line-height: 60rpx;
+      background: rgba(238, 238, 238, 1);
+      border-radius: 30rpx;
+      padding: 0;
+      margin-right: 20rpx;
+      font-size: 26rpx;
+
+      font-weight: 400;
+      color: rgba(51, 51, 51, 1);
+    }
+  }
+</style>

+ 210 - 0
pages/order/aftersale/list.vue

@@ -0,0 +1,210 @@
+<!-- 售后列表 -->
+<template>
+  <s-layout title="售后列表">
+    <!-- tab -->
+    <su-sticky bgColor="#fff">
+      <su-tabs
+        :list="tabMaps"
+        :scrollable="false"
+        @change="onTabsChange"
+        :current="state.currentTab"
+      />
+    </su-sticky>
+    <s-empty v-if="state.pagination.total === 0" icon="/static/data-empty.png" text="暂无数据" />
+    <!-- 列表 -->
+    <view v-if="state.pagination.total > 0">
+      <view
+        class="list-box ss-m-y-20"
+        v-for="order in state.pagination.list"
+        :key="order.id"
+        @tap="sheep.$router.go('/pages/order/aftersale/detail', { id: order.id })"
+      >
+        <view class="order-head ss-flex ss-col-center ss-row-between">
+          <text class="no">服务单号:{{ order.no }}</text>
+          <text class="state">{{ formatAfterSaleStatus(order) }}</text>
+        </view>
+        <s-goods-item
+          :img="order.picUrl"
+          :title="order.spuName"
+          :skuText="order.properties.map((property) => property.valueName).join(' ')"
+          :price="order.refundPrice"
+        />
+        <view class="apply-box ss-flex ss-col-center ss-row-between border-bottom ss-p-x-20">
+          <view class="ss-flex ss-col-center">
+            <view class="title ss-m-r-20">{{ order.way === 10 ? '仅退款' : '退款退货' }}</view>
+            <view class="value">{{ formatAfterSaleStatusDescription(order) }}</view>
+          </view>
+          <text class="_icon-forward"></text>
+        </view>
+        <view class="tool-btn-box ss-flex ss-col-center ss-row-right ss-p-r-20">
+          <!-- TODO 功能缺失:填写退货信息 -->
+          <view>
+            <button
+              class="ss-reset-button tool-btn"
+              @tap.stop="onApply(order.id)"
+              v-if="order?.buttons.includes('cancel')"
+              >取消申请</button
+            >
+          </view>
+        </view>
+      </view>
+    </view>
+    <uni-load-more
+      v-if="state.pagination.total > 0"
+      :status="state.loadStatus"
+      :content-text="{
+        contentdown: '上拉加载更多',
+      }"
+      @tap="loadMore"
+    />
+  </s-layout>
+</template>
+
+<script setup>
+  import sheep from '@/sheep';
+  import { onLoad, onReachBottom } from '@dcloudio/uni-app';
+  import { reactive } from 'vue';
+  import _ from 'lodash-es';
+  import {
+    formatAfterSaleStatus,
+    formatAfterSaleStatusDescription,
+    handleAfterSaleButtons,
+  } from '@/sheep/hooks/useGoods';
+  import AfterSaleApi from '@/sheep/api/trade/afterSale';
+  import { resetPagination } from '@/sheep/util';
+
+  const state = reactive({
+    currentTab: 0,
+    showApply: false,
+    pagination: {
+      list: [],
+      total: 0,
+      pageNo: 1,
+      pageSize: 10,
+    },
+    loadStatus: '',
+  });
+
+  // TODO 芋艿:优化点,增加筛选
+  const tabMaps = [
+    {
+      name: '全部',
+      value: 'all',
+    },
+    // {
+    //   name: '申请中',
+    //   value: 'nooper',
+    // },
+    // {
+    //   name: '处理中',
+    //   value: 'ing',
+    // },
+    // {
+    //   name: '已完成',
+    //   value: 'completed',
+    // },
+    // {
+    //   name: '已拒绝',
+    //   value: 'refuse',
+    // },
+  ];
+
+  // 切换选项卡
+  function onTabsChange(e) {
+    resetPagination(state.pagination);
+    state.currentTab = e.index;
+    getOrderList();
+  }
+
+  // 获取售后列表
+  async function getOrderList() {
+    state.loadStatus = 'loading';
+    let { data, code } = await AfterSaleApi.getAfterSalePage({
+      // type: tabMaps[state.currentTab].value,
+      pageNo: state.pagination.pageNo,
+      pageSize: state.pagination.pageSize,
+    });
+    if (code !== 0) {
+      return;
+    }
+    data.list.forEach((order) => handleAfterSaleButtons(order));
+    state.pagination.list = _.concat(state.pagination.list, data.list);
+    state.pagination.total = data.total;
+    state.loadStatus = state.pagination.list.length < state.pagination.total ? 'more' : 'noMore';
+  }
+
+  function onApply(orderId) {
+    uni.showModal({
+      title: '提示',
+      content: '确定要取消此申请吗?',
+      success: async function (res) {
+        if (!res.confirm) {
+          return;
+        }
+        const { code } = await AfterSaleApi.cancelAfterSale(orderId);
+        if (code === 0) {
+          resetPagination(state.pagination);
+          await getOrderList();
+        }
+      },
+    });
+  }
+
+  onLoad(async (options) => {
+    if (options.type) {
+      state.currentTab = options.type;
+    }
+    await getOrderList();
+  });
+
+  // 加载更多
+  function loadMore() {
+    if (state.loadStatus === 'noMore') {
+      return;
+    }
+    state.pagination.pageNo++;
+    getOrderList();
+  }
+
+  // 上拉加载更多
+  onReachBottom(() => {
+    loadMore();
+  });
+</script>
+
+<style lang="scss" scoped>
+  .list-box {
+    background-color: #fff;
+
+    .order-head {
+      padding: 0 25rpx;
+      height: 77rpx;
+    }
+
+    .apply-box {
+      height: 82rpx;
+
+      .title {
+        font-size: 24rpx;
+      }
+
+      .value {
+        font-size: 22rpx;
+        color: $dark-6;
+      }
+    }
+
+    .tool-btn-box {
+      height: 100rpx;
+
+      .tool-btn {
+        width: 160rpx;
+        height: 60rpx;
+        background: #f6f6f6;
+        border-radius: 30rpx;
+        font-size: 26rpx;
+        font-weight: 400;
+      }
+    }
+  }
+</style>

+ 77 - 0
pages/order/aftersale/log-item.vue

@@ -0,0 +1,77 @@
+<!-- 售后日志的每一项展示 -->
+<template>
+  <view class="log-item ss-flex">
+    <view class="log-icon ss-flex-col ss-col-center ss-m-r-20">
+      <text class="cicon-title" :class="index === 0 ? 'activity-color' : ''" />
+      <view v-if="data.length - 1 !== index" class="line" />
+    </view>
+    <view>
+      <view class="text">{{ item.content }}</view>
+      <view class="date">
+        {{ sheep.$helper.timeFormat(item.createTime, 'yyyy-mm-dd hh:MM:ss') }}
+      </view>
+    </view>
+  </view>
+</template>
+<script setup>
+  import sheep from '@/sheep';
+
+  const props = defineProps({
+    item: {
+      type: Object, // 当前日志
+      default() {},
+    },
+    index: {
+      type: Number, // item 在 data 的下标
+      default: 0,
+    },
+    data: {
+      type: Object, // 日志列表
+      default() {},
+    },
+  });
+</script>
+<style lang="scss" scoped>
+  .log-item {
+    align-items: stretch;
+  }
+  .log-icon {
+    height: inherit;
+    .cicon-title {
+      font-size: 30rpx;
+      color: #dfdfdf;
+    }
+    .activity-color {
+      color: #60bd45;
+    }
+    .line {
+      width: 1px;
+      height: 100%;
+      background: #dfdfdf;
+    }
+  }
+  .text {
+    font-size: 28rpx;
+    font-weight: 500;
+    color: #333333;
+  }
+  .richtext {
+    font-size: 24rpx;
+    font-weight: 500;
+    color: #999999;
+    margin: 20rpx 0 0 0;
+  }
+  .content-img {
+    margin-top: 20rpx;
+    width: 200rpx;
+    height: 200rpx;
+  }
+  .date {
+    margin-top: 20rpx;
+    font-size: 24rpx;
+    font-family: OPPOSANS;
+    font-weight: 400;
+    color: #999999;
+    margin-bottom: 40rpx;
+  }
+</style>

+ 38 - 0
pages/order/aftersale/log.vue

@@ -0,0 +1,38 @@
+<!-- 售后日志列表  -->
+<template>
+  <s-layout title="售后进度">
+    <view class="log-box">
+      <view v-for="(item, index) in state.list" :key="item.id">
+        <log-item :item="item" :index="index" :data="state.list" />
+      </view>
+    </view>
+  </s-layout>
+</template>
+
+<script setup>
+  import { onLoad } from '@dcloudio/uni-app';
+  import { reactive } from 'vue';
+  import logItem from './log-item.vue';
+  import AfterSaleApi from '@/sheep/api/trade/afterSale';
+
+  const state = reactive({
+    list: [],
+  });
+
+  async function getDetail(id) {
+    const { data } = await AfterSaleApi.getAfterSaleLogList(id);
+    state.list = data;
+  }
+
+  onLoad((options) => {
+    state.aftersaleId = options.id;
+    getDetail(options.id);
+  });
+</script>
+
+<style lang="scss" scoped>
+  .log-box {
+    padding: 24rpx 24rpx 24rpx 40rpx;
+    background-color: #fff;
+  }
+</style>

+ 195 - 0
pages/order/aftersale/return-delivery.vue

@@ -0,0 +1,195 @@
+<template>
+	<s-layout title="退货物流">
+		<view>
+			<form @submit="subRefund" report-submit='true'>
+				<view class='apply-return'>
+					<view class='list borRadius14'>
+						<view class='item acea-row row-between-wrapper' style="display: flex;align-items: center;">
+							<view>物流公司</view>
+							<view v-if="state.expresses.length>0" style="flex:1">
+								<picker mode='selector' class='num' @change="bindPickerChange" :value="state.expressIndex"
+									:range="state.expresses" range-key="name">
+									<view class="picker acea-row row-between-wrapper" style="display: flex;justify-content: space-between;">
+										<view class='reason'>{{ state.expresses[state.expressIndex].name }}</view>
+										<text class='iconfont _icon-forward' />
+									</view>
+								</picker>
+							</view>
+						</view>
+						<view class='item textarea acea-row row-between' style="display: flex;align-items: center;">
+							<view>物流单号</view>
+							<input placeholder='请填写物流单号' class='num' name="logisticsNo"
+                     placeholder-class='placeholder' />
+						</view>
+						<button class='returnBnt bg-color ss-reset-button ui-BG-Main-Gradient sub-btn'
+							form-type="submit"
+							style="background: linear-gradient(90deg,var(--ui-BG-Main),var(--ui-BG-Main-gradient))!important">提交</button>
+					</view>
+				</view>
+			</form>
+		</view>
+	</s-layout>
+</template>
+
+<script setup>
+	import { onLoad } from '@dcloudio/uni-app';
+	import { reactive } from 'vue';
+  import sheep from '@/sheep';
+  import AfterSaleApi from '@/sheep/api/trade/afterSale';
+  import DeliveryApi from '@/sheep/api/trade/delivery';
+
+  const state = reactive({
+    id: 0, // 售后编号
+		expressIndex: 0, // 选中的 expresses 下标
+		expresses: [], // 可选的快递列表
+	})
+
+	function bindPickerChange(e) {
+		state.expressIndex = e.detail.value;
+	}
+
+	async function subRefund(e) {
+    let data = {
+      id: state.id,
+      logisticsId: state.expresses[state.expressIndex].id,
+      logisticsNo: e.detail.value.logisticsNo,
+    };
+    const { code } = await AfterSaleApi.deliveryAfterSale(data);
+    if (code !== 0) {
+      return;
+    }
+    uni.showToast({
+      title: '填写退货成功',
+    });
+    sheep.$router.go('/pages/order/aftersale/detail', { id: state.id });
+	}
+
+  // 获得快递物流列表
+	async function getExpressList() {
+    const { code, data } = await DeliveryApi.getDeliveryExpressList();
+    if (code !== 0) {
+      return;
+    }
+    state.expresses = data;
+	}
+
+	onLoad(options => {
+    if (!options.id) {
+      sheep.$helper.toast(`缺少订单信息,请检查`);
+      return
+    }
+    state.id = options.id;
+    // 获得快递物流列表
+    getExpressList();
+	})
+</script>
+<style lang="scss" scoped>
+	.apply-return {
+		padding: 20rpx 30rpx 70rpx 30rpx;
+	}
+
+	.apply-return .list {
+		background-color: #fff;
+		margin-top: 18rpx;
+		padding: 0 24rpx 70rpx 24rpx;
+	}
+
+	.apply-return .list .item {
+		min-height: 90rpx;
+		border-bottom: 1rpx solid #eee;
+		font-size: 30rpx;
+		color: #333;
+	}
+
+	.apply-return .list .item .num {
+		color: #282828;
+		margin-left: 27rpx;
+		// width: 227rpx;
+		// text-align: right;
+	}
+
+	.apply-return .list .item .num .picker .reason {
+		width: 385rpx;
+	}
+
+	.apply-return .list .item .num .picker .iconfont {
+		color: #666;
+		font-size: 30rpx;
+		margin-top: 2rpx;
+	}
+
+	.apply-return .list .item.textarea {
+		padding: 24rpx 0;
+	}
+
+	.apply-return .list .item textarea {
+		height: 100rpx;
+		font-size: 30rpx;
+	}
+
+	.apply-return .list .item .placeholder {
+		color: #bbb;
+	}
+
+	.apply-return .list .item .title {
+		height: 95rpx;
+		width: 100%;
+	}
+
+	.apply-return .list .item .title .tip {
+		font-size: 30rpx;
+		color: #bbb;
+	}
+
+	.apply-return .list .item .upload {
+		padding-bottom: 36rpx;
+	}
+
+	.apply-return .list .item .upload .pictrue {
+		border-radius: 14rpx;
+		margin: 22rpx 23rpx 0 0;
+		width: 156rpx;
+		height: 156rpx;
+		position: relative;
+		font-size: 24rpx;
+		color: #bbb;
+	}
+
+	.apply-return .list .item .upload .pictrue:nth-of-type(4n) {
+		margin-right: 0;
+	}
+
+	.apply-return .list .item .upload .pictrue image {
+		width: 100%;
+		height: 100%;
+		border-radius: 14rpx;
+	}
+
+	.apply-return .list .item .upload .pictrue .icon-guanbi1 {
+		position: absolute;
+		font-size: 45rpx;
+		top: -10rpx;
+		right: -10rpx;
+	}
+
+	.apply-return .list .item .upload .pictrue .icon-icon25201 {
+		color: #bfbfbf;
+		font-size: 50rpx;
+	}
+
+	.apply-return .list .item .upload .pictrue:nth-last-child(1) {
+		border: 1rpx solid #ddd;
+		box-sizing: border-box;
+	}
+
+	.apply-return .returnBnt {
+		font-size: 32rpx;
+		color: #fff;
+		width: 100%;
+		height: 86rpx;
+		border-radius: 50rpx;
+		text-align: center;
+		line-height: 86rpx;
+		margin: 43rpx auto;
+	}
+</style>

+ 507 - 0
pages/order/confirm.vue

@@ -0,0 +1,507 @@
+<template>
+  <s-layout title="确认订单">
+    <!-- 头部地址选择【配送地址】【自提地址】 -->
+    <AddressSelection v-model="addressState" />
+
+    <!-- 商品信息 -->
+    <view class="order-card-box ss-m-b-14">
+      <s-goods-item
+        v-for="item in state.orderInfo.items"
+        :key="item.skuId"
+        :img="item.picUrl"
+        :title="item.spuName"
+        :skuText="item.properties.map((property) => property.valueName).join(' ')"
+        :price="item.price"
+        :num="item.count"
+        marginBottom="10"
+      />
+      <view class="order-item ss-flex ss-col-center ss-row-between ss-p-x-20 bg-white ss-r-10">
+        <view class="item-title">订单备注</view>
+        <view class="ss-flex ss-col-center">
+          <uni-easyinput
+            maxlength="20"
+            placeholder="建议留言前先与商家沟通"
+            v-model="state.orderPayload.remark"
+            :inputBorder="false"
+            :clearable="false"
+          />
+        </view>
+      </view>
+    </view>
+
+    <!-- 价格信息 -->
+    <view class="bg-white total-card-box ss-p-20 ss-m-b-14 ss-r-10">
+      <view class="total-box-content border-bottom">
+        <view class="order-item ss-flex ss-col-center ss-row-between">
+          <view class="item-title">商品金额</view>
+          <view class="ss-flex ss-col-center">
+            <text class="item-value ss-m-r-24">
+              ¥{{ fen2yuan(state.orderInfo.price.totalPrice) }}
+            </text>
+          </view>
+        </view>
+        <view
+          v-if="state.orderPayload.pointActivityId"
+          class="order-item ss-flex ss-col-center ss-row-between"
+        >
+          <view class="item-title">兑换积分</view>
+          <view class="ss-flex ss-col-center">
+            <image
+              :src="sheep.$url.static('/static/img/shop/goods/score1.svg')"
+              class="score-img"
+            />
+            <text class="item-value ss-m-r-24">
+              {{ state.orderInfo.usePoint }}
+            </text>
+          </view>
+        </view>
+        <view
+          class="order-item ss-flex ss-col-center ss-row-between"
+          v-if="state.orderInfo.type === 0 || state.orderPayload.pointActivityId"
+        >
+          <view class="item-title">积分抵扣</view>
+          <view class="ss-flex ss-col-center">
+            {{ state.pointStatus || state.orderPayload.pointActivityId ? '剩余积分' : '当前积分' }}
+            <image
+              :src="sheep.$url.static('/static/img/shop/goods/score1.svg')"
+              class="score-img"
+            />
+            <text class="item-value ss-m-r-24">
+              {{
+                state.pointStatus || state.orderPayload.pointActivityId
+                  ? state.orderInfo.totalPoint - state.orderInfo.usePoint
+                  : state.orderInfo.totalPoint || 0
+              }}
+            </text>
+            <checkbox-group @change="changeIntegral" v-if="!state.orderPayload.pointActivityId">
+              <checkbox
+                :checked="state.pointStatus"
+                :disabled="!state.orderInfo.totalPoint || state.orderInfo.totalPoint <= 0"
+              />
+            </checkbox-group>
+          </view>
+        </view>
+        <!-- 快递配置时,信息的展示 -->
+        <view
+          class="order-item ss-flex ss-col-center ss-row-between"
+          v-if="addressState.deliveryType === 1"
+        >
+          <view class="item-title">运费</view>
+          <view class="ss-flex ss-col-center">
+            <text class="item-value ss-m-r-24" v-if="state.orderInfo.price.deliveryPrice > 0">
+              +¥{{ fen2yuan(state.orderInfo.price.deliveryPrice) }}
+            </text>
+            <view class="item-value ss-m-r-24" v-else>免运费</view>
+          </view>
+        </view>
+        <!-- 门店自提时,需要填写姓名和手机号 -->
+        <view
+          class="order-item ss-flex ss-col-center ss-row-between"
+          v-if="addressState.deliveryType === 2"
+        >
+          <view class="item-title">联系人</view>
+          <view class="ss-flex ss-col-center">
+            <uni-easyinput
+              maxlength="20"
+              placeholder="请填写您的联系姓名"
+              v-model="addressState.receiverName"
+              :inputBorder="false"
+              :clearable="false"
+            />
+          </view>
+        </view>
+        <view
+          class="order-item ss-flex ss-col-center ss-row-between"
+          v-if="addressState.deliveryType === 2"
+        >
+          <view class="item-title">联系电话</view>
+          <view class="ss-flex ss-col-center">
+            <uni-easyinput
+              maxlength="20"
+              placeholder="请填写您的联系电话"
+              v-model="addressState.receiverMobile"
+              :inputBorder="false"
+              :clearable="false"
+            />
+          </view>
+        </view>
+        <!-- 优惠劵:只有 type = 0 普通订单(非拼团、秒杀、砍价),才可以使用优惠劵 -->
+        <view
+          class="order-item ss-flex ss-col-center ss-row-between"
+          v-if="state.orderInfo.type === 0"
+        >
+          <view class="item-title">优惠券</view>
+          <view class="ss-flex ss-col-center" @tap="state.showCoupon = true">
+            <text class="item-value text-red" v-if="state.orderPayload.couponId > 0">
+              -¥{{ fen2yuan(state.orderInfo.price.couponPrice) }}
+            </text>
+            <text
+              class="item-value"
+              :class="
+                state.couponInfo.filter((coupon) => coupon.match).length > 0
+                  ? 'text-red'
+                  : 'text-disabled'
+              "
+              v-else
+            >
+              {{
+                state.couponInfo.filter((coupon) => coupon.match).length > 0
+                  ? state.couponInfo.filter((coupon) => coupon.match).length + ' 张可用'
+                  : '暂无可用优惠券'
+              }}
+            </text>
+            <text class="_icon-forward item-icon" />
+          </view>
+        </view>
+        <view
+          class="order-item ss-flex ss-col-center ss-row-between"
+          v-if="state.orderInfo.price.discountPrice > 0"
+        >
+          <view class="item-title">活动优惠</view>
+          <view class="ss-flex ss-col-center" @tap="state.showDiscount = true">
+            <text class="item-value text-red">
+              -¥{{ fen2yuan(state.orderInfo.price.discountPrice) }}
+            </text>
+            <text class="_icon-forward item-icon" />
+          </view>
+        </view>
+        <view
+          class="order-item ss-flex ss-col-center ss-row-between"
+          v-if="state.orderInfo.price.vipPrice > 0"
+        >
+          <view class="item-title">会员优惠</view>
+          <view class="ss-flex ss-col-center">
+            <text class="item-value text-red">
+              -¥{{ fen2yuan(state.orderInfo.price.vipPrice) }}
+            </text>
+          </view>
+        </view>
+      </view>
+      <view class="total-box-footer ss-font-28 ss-flex ss-row-right ss-col-center ss-m-r-28">
+        <view class="total-num ss-m-r-20">
+          共{{ state.orderInfo.items.reduce((acc, item) => acc + item.count, 0) }}件
+        </view>
+        <view>合计:</view>
+        <view class="total-num text-red"> ¥{{ fen2yuan(state.orderInfo.price.payPrice) }}</view>
+      </view>
+    </view>
+
+    <!-- 选择优惠券弹框 -->
+    <s-coupon-select
+      v-model="state.couponInfo"
+      :show="state.showCoupon"
+      @confirm="onSelectCoupon"
+      @close="state.showCoupon = false"
+    />
+
+    <!-- 满额折扣弹框 TODO @puhui999:【折扣】后续要把优惠信息打进去 -->
+    <s-discount-list
+      v-model="state.orderInfo"
+      :show="state.showDiscount"
+      @close="state.showDiscount = false"
+    />
+
+    <!-- 底部 -->
+    <su-fixed bottom :opacity="false" bg="bg-white" placeholder :noFixed="false" :index="200">
+      <view class="footer-box border-top ss-flex ss-row-between ss-p-x-20 ss-col-center">
+        <view class="total-box-footer ss-flex ss-col-center">
+          <view class="total-num ss-font-30 text-red">
+            ¥{{ fen2yuan(state.orderInfo.price.payPrice) }}
+          </view>
+        </view>
+        <button
+          class="ss-reset-button ui-BG-Main-Gradient ss-r-40 submit-btn ui-Shadow-Main"
+          @tap="onConfirm"
+        >
+          提交订单
+        </button>
+      </view>
+    </su-fixed>
+  </s-layout>
+</template>
+
+<script setup>
+  import { reactive, ref, watch } from 'vue';
+  import { onLoad } from '@dcloudio/uni-app';
+  import AddressSelection from '@/pages/order/addressSelection.vue';
+  import sheep from '@/sheep';
+  import OrderApi from '@/sheep/api/trade/order';
+  import TradeConfigApi from '@/sheep/api/trade/config';
+  import { fen2yuan } from '@/sheep/hooks/useGoods';
+
+  const state = reactive({
+    orderPayload: {},
+    orderInfo: {
+      items: [], // 商品项列表
+      price: {}, // 价格信息
+    },
+    showCoupon: false, // 是否展示优惠劵
+    couponInfo: [], // 优惠劵列表
+    showDiscount: false, // 是否展示营销活动
+    // ========== 积分 ==========
+    pointStatus: false, //是否使用积分
+  });
+
+  const addressState = ref({
+    addressInfo: {}, // 选择的收货地址
+    deliveryType: 1, // 收货方式:1-快递配送,2-门店自提
+    isPickUp: true, // 门店自提是否开启
+    pickUpInfo: {}, // 选择的自提门店信息
+    receiverName: '', // 收件人名称
+    receiverMobile: '', // 收件人手机
+  });
+
+  // ========== 积分 ==========
+  /**
+   * 使用积分抵扣
+   */
+  const changeIntegral = async () => {
+    state.pointStatus = !state.pointStatus;
+    await getOrderInfo();
+  };
+
+  // 选择优惠券
+  async function onSelectCoupon(couponId) {
+    state.orderPayload.couponId = couponId;
+    await getOrderInfo();
+    state.showCoupon = false;
+  }
+
+  // 提交订单
+  function onConfirm() {
+    if (addressState.value.deliveryType === 1 && !addressState.value.addressInfo.id) {
+      sheep.$helper.toast('请选择收货地址');
+      return;
+    }
+    if (addressState.value.deliveryType === 2) {
+      if (!addressState.value.pickUpInfo.id) {
+        sheep.$helper.toast('请选择自提门店地址');
+        return;
+      }
+      if (addressState.value.receiverName === '' || addressState.value.receiverMobile === '') {
+        sheep.$helper.toast('请填写联系人或联系人电话');
+        return;
+      }
+      if (!/^[\u4e00-\u9fa5\w]{2,16}$/.test(addressState.value.receiverName)) {
+        sheep.$helper.toast('请填写您的真实姓名');
+        return;
+      }
+      if (!/^1(3|4|5|7|8|9|6)\d{9}$/.test(addressState.value.receiverMobile)) {
+        sheep.$helper.toast('请填写正确的手机号');
+        return;
+      }
+    }
+    submitOrder();
+  }
+
+  // 创建订单&跳转
+  async function submitOrder() {
+    const { code, data } = await OrderApi.createOrder({
+      items: state.orderPayload.items,
+      couponId: state.orderPayload.couponId,
+      remark: state.orderPayload.remark,
+      deliveryType: addressState.value.deliveryType,
+      addressId: addressState.value.addressInfo.id, // 收件地址编号
+      pickUpStoreId: addressState.value.pickUpInfo.id, //自提门店编号
+      receiverName: addressState.value.receiverName, // 选择门店自提时,该字段为联系人名
+      receiverMobile: addressState.value.receiverMobile, // 选择门店自提时,该字段为联系人手机
+      pointStatus: state.pointStatus,
+      combinationActivityId: state.orderPayload.combinationActivityId,
+      combinationHeadId: state.orderPayload.combinationHeadId,
+      seckillActivityId: state.orderPayload.seckillActivityId,
+      pointActivityId: state.orderPayload.pointActivityId,
+    });
+    if (code !== 0) {
+      return;
+    }
+    // 更新购物车列表,如果来自购物车
+    if (state.orderPayload.items[0].cartId > 0) {
+      sheep.$store('cart').getList();
+    }
+
+    // 跳转到支付页面
+    if (data.payOrderId && data.payOrderId > 0) {
+      sheep.$router.redirect('/pages/pay/index', {
+        id: data.payOrderId,
+      });
+    } else {
+      sheep.$router.redirect('/pages/order/detail', {
+        id: data.id,
+      });
+    }
+  }
+
+  // 检查库存 & 计算订单价格
+  async function getOrderInfo() {
+    // 计算价格
+    const { data, code } = await OrderApi.settlementOrder({
+      items: state.orderPayload.items,
+      couponId: state.orderPayload.couponId,
+      deliveryType: addressState.value.deliveryType,
+      addressId: addressState.value.addressInfo.id, // 收件地址编号
+      pickUpStoreId: addressState.value.pickUpInfo.id, //自提门店编号
+      receiverName: addressState.value.receiverName, // 选择门店自提时,该字段为联系人名
+      receiverMobile: addressState.value.receiverMobile, // 选择门店自提时,该字段为联系人手机
+      pointStatus: state.pointStatus,
+      combinationActivityId: state.orderPayload.combinationActivityId,
+      combinationHeadId: state.orderPayload.combinationHeadId,
+      seckillActivityId: state.orderPayload.seckillActivityId,
+      pointActivityId: state.orderPayload.pointActivityId,
+    });
+    if (code !== 0) {
+      return;
+    }
+    state.orderInfo = data;
+    state.couponInfo = data.coupons || [];
+    // 设置收货地址
+    if (state.orderInfo.address) {
+      addressState.value.addressInfo = state.orderInfo.address;
+    }
+  }
+
+  onLoad(async (options) => {
+    if (!options.data) {
+      sheep.$helper.toast('参数不正确,请检查!');
+      return;
+    }
+    state.orderPayload = JSON.parse(options.data);
+    await getOrderInfo();
+    // 获取交易配置
+    const { data, code } = await TradeConfigApi.getTradeConfig();
+    if (code === 0) {
+      addressState.value.isPickUp = data.deliveryPickUpEnabled;
+    }
+  });
+
+  // 使用 watch 监听地址和配送方式的变化
+  watch(addressState, async (newAddress, oldAddress) => {
+    // 如果收货地址或配送方式有变化,则重新计算价格
+    if (
+      newAddress.addressInfo.id !== oldAddress.addressInfo.id ||
+      newAddress.deliveryType !== oldAddress.deliveryType
+    ) {
+      await getOrderInfo();
+    }
+  });
+</script>
+
+<style lang="scss" scoped>
+  :deep() {
+    .uni-input-wrapper {
+      width: 320rpx;
+    }
+
+    .uni-easyinput__content-input {
+      font-size: 28rpx;
+      height: 72rpx;
+      text-align: right !important;
+      padding-right: 0 !important;
+
+      .uni-input-input {
+        font-weight: 500;
+        color: #333333;
+        font-size: 26rpx;
+        height: 32rpx;
+        margin-top: 4rpx;
+      }
+    }
+
+    .uni-easyinput__content {
+      display: flex !important;
+      align-items: center !important;
+      justify-content: right !important;
+    }
+  }
+
+  .score-img {
+    width: 36rpx;
+    height: 36rpx;
+    margin: 0 4rpx;
+  }
+
+  .order-item {
+    height: 80rpx;
+
+    .item-title {
+      font-size: 28rpx;
+      font-weight: 400;
+    }
+
+    .item-value {
+      font-size: 28rpx;
+      font-weight: 500;
+      font-family: OPPOSANS;
+    }
+
+    .text-disabled {
+      color: #bbbbbb;
+    }
+
+    .item-icon {
+      color: $dark-9;
+    }
+
+    .remark-input {
+      text-align: right;
+    }
+
+    .item-placeholder {
+      color: $dark-9;
+      font-size: 26rpx;
+      text-align: right;
+    }
+  }
+
+  .total-box-footer {
+    height: 90rpx;
+
+    .total-num {
+      color: #333333;
+      font-family: OPPOSANS;
+    }
+  }
+
+  .footer-box {
+    height: 100rpx;
+
+    .submit-btn {
+      width: 240rpx;
+      height: 70rpx;
+      font-size: 28rpx;
+      font-weight: 500;
+
+      .goto-pay-text {
+        line-height: 28rpx;
+      }
+    }
+
+    .cancel-btn {
+      width: 240rpx;
+      height: 80rpx;
+      font-size: 26rpx;
+      background-color: #e5e5e5;
+      color: $dark-9;
+    }
+  }
+
+  .title {
+    font-size: 36rpx;
+    font-weight: bold;
+    color: #333333;
+  }
+
+  .subtitle {
+    font-size: 28rpx;
+    color: #999999;
+  }
+
+  .cicon-checkbox {
+    font-size: 36rpx;
+    color: var(--ui-BG-Main);
+  }
+
+  .cicon-box {
+    font-size: 36rpx;
+    color: #999999;
+  }
+</style>

+ 673 - 0
pages/order/detail.vue

@@ -0,0 +1,673 @@
+<!-- 订单详情 -->
+<template>
+  <s-layout title="订单详情" class="index-wrap" navbar="inner">
+    <!-- 订单状态 TODO -->
+    <view
+      class="state-box ss-flex-col ss-col-center ss-row-right"
+      :style="[
+        {
+          marginTop: '-' + Number(statusBarHeight + 88) + 'rpx',
+          paddingTop: Number(statusBarHeight + 88) + 'rpx',
+        },
+      ]"
+    >
+      <view class="ss-flex ss-m-t-32 ss-m-b-20">
+        <image
+          v-if="
+            state.orderInfo.status_code == 'unpaid' ||
+            state.orderInfo.status === 10 || // 待发货
+            state.orderInfo.status_code == 'nocomment'
+          "
+          class="state-img"
+          :src="sheep.$url.static('/static/img/shop/order/order_loading.png')"
+        >
+        </image>
+        <image
+          v-if="
+            state.orderInfo.status_code == 'completed' ||
+            state.orderInfo.status_code == 'refund_agree'
+          "
+          class="state-img"
+          :src="sheep.$url.static('/static/img/shop/order/order_success.png')"
+        >
+        </image>
+        <image
+          v-if="state.orderInfo.status_code == 'cancel' || state.orderInfo.status_code == 'closed'"
+          class="state-img"
+          :src="sheep.$url.static('/static/img/shop/order/order_close.png')"
+        >
+        </image>
+        <image
+          v-if="state.orderInfo.status_code == 'noget'"
+          class="state-img"
+          :src="sheep.$url.static('/static/img/shop/order/order_express.png')"
+        >
+        </image>
+        <view class="ss-font-30">{{ formatOrderStatus(state.orderInfo) }}</view>
+      </view>
+      <view class="ss-font-26 ss-m-x-20 ss-m-b-70">
+        {{ formatOrderStatusDescription(state.orderInfo) }}
+      </view>
+    </view>
+
+    <!-- 收货地址 -->
+    <view class="order-address-box" v-if="state.orderInfo.receiverAreaId > 0">
+      <view class="ss-flex ss-col-center">
+        <text class="address-username">
+          {{ state.orderInfo.receiverName }}
+        </text>
+        <text class="address-phone">{{ state.orderInfo.receiverMobile }}</text>
+      </view>
+      <view class="address-detail">
+        {{ state.orderInfo.receiverAreaName }} {{ state.orderInfo.receiverDetailAddress }}
+      </view>
+    </view>
+
+    <view
+      class="detail-goods"
+      :style="[{ marginTop: state.orderInfo.receiverAreaId > 0 ? '0' : '-40rpx' }]"
+    >
+      <!-- 订单信 -->
+      <view class="order-list" v-for="item in state.orderInfo.items" :key="item.goods_id">
+        <view class="order-card">
+          <s-goods-item
+            @tap="onGoodsDetail(item.spuId)"
+            :img="item.picUrl"
+            :title="item.spuName"
+            :skuText="item.properties.map((property) => property.valueName).join(' ')"
+            :price="item.price"
+            :num="item.count"
+          >
+            <template #tool>
+              <view class="ss-flex">
+                <button
+                  class="ss-reset-button apply-btn"
+                  v-if="[10, 20, 30].includes(state.orderInfo.status) && item.afterSaleStatus === 0"
+                  @tap.stop="
+                    sheep.$router.go('/pages/order/aftersale/apply', {
+                      orderId: state.orderInfo.id,
+                      itemId: item.id,
+                    })
+                  "
+                >
+                  申请售后
+                </button>
+                <button
+                  class="ss-reset-button apply-btn"
+                  v-if="item.afterSaleStatus === 10"
+                  @tap.stop="
+                    sheep.$router.go('/pages/order/aftersale/detail', {
+                      id: item.afterSaleId,
+                    })
+                  "
+                >
+                  退款中
+                </button>
+                <button
+                  class="ss-reset-button apply-btn"
+                  v-if="item.afterSaleStatus === 20"
+                  @tap.stop="
+                    sheep.$router.go('/pages/order/aftersale/detail', {
+                      id: item.afterSaleId,
+                    })
+                  "
+                >
+                  退款成功
+                </button>
+              </view>
+            </template>
+            <template #priceSuffix>
+              <button class="ss-reset-button tag-btn" v-if="item.status_text">
+                {{ item.status_text }}
+              </button>
+            </template>
+          </s-goods-item>
+        </view>
+      </view>
+    </view>
+
+    <!--  自提核销  -->
+    <PickUpVerify
+      :order-info="state.orderInfo"
+      :systemStore="systemStore"
+      ref="pickUpVerifyRef"
+    ></PickUpVerify>
+
+    <!-- 订单信息 -->
+    <view class="notice-box">
+      <view class="notice-box__content">
+        <view class="notice-item--center">
+          <view class="ss-flex ss-flex-1">
+            <text class="title">订单编号:</text>
+            <text class="detail">{{ state.orderInfo.no }}</text>
+          </view>
+          <button class="ss-reset-button copy-btn" @tap="onCopy">复制</button>
+        </view>
+        <view class="notice-item">
+          <text class="title">下单时间:</text>
+          <text class="detail">
+            {{ sheep.$helper.timeFormat(state.orderInfo.createTime, 'yyyy-mm-dd hh:MM:ss') }}
+          </text>
+        </view>
+        <view class="notice-item" v-if="state.orderInfo.payTime">
+          <text class="title">支付时间:</text>
+          <text class="detail">
+            {{ sheep.$helper.timeFormat(state.orderInfo.payTime, 'yyyy-mm-dd hh:MM:ss') }}
+          </text>
+        </view>
+        <view class="notice-item">
+          <text class="title">支付方式:</text>
+          <text class="detail">{{ state.orderInfo.payChannelName || '-' }}</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 价格信息 -->
+    <view class="order-price-box">
+      <view class="notice-item ss-flex ss-row-between">
+        <text class="title">商品总额</text>
+        <view class="ss-flex">
+          <text class="detail">¥{{ fen2yuan(state.orderInfo.totalPrice) }}</text>
+        </view>
+      </view>
+      <view class="notice-item ss-flex ss-row-between">
+        <text class="title">运费</text>
+        <text class="detail">¥{{ fen2yuan(state.orderInfo.deliveryPrice) }}</text>
+      </view>
+      <view class="notice-item ss-flex ss-row-between" v-if="state.orderInfo.couponPrice > 0">
+        <text class="title">优惠劵金额</text>
+        <text class="detail">-¥{{ fen2yuan(state.orderInfo.couponPrice) }}</text>
+      </view>
+      <view class="notice-item ss-flex ss-row-between" v-if="state.orderInfo.pointPrice > 0">
+        <text class="title">积分抵扣</text>
+        <text class="detail">-¥{{ fen2yuan(state.orderInfo.pointPrice) }}</text>
+      </view>
+      <view class="notice-item ss-flex ss-row-between" v-if="state.orderInfo.discountPrice > 0">
+        <text class="title">活动优惠</text>
+        <text class="detail">¥{{ fen2yuan(state.orderInfo.discountPrice) }}</text>
+      </view>
+      <view class="notice-item ss-flex ss-row-between" v-if="state.orderInfo.vipPrice > 0">
+        <text class="title">会员优惠</text>
+        <text class="detail">-¥{{ fen2yuan(state.orderInfo.vipPrice) }}</text>
+      </view>
+      <view class="notice-item all-rpice-item ss-flex ss-m-t-20">
+        <text class="title">{{ state.orderInfo.payStatus ? '已付款' : '需付款' }}</text>
+        <text class="detail all-price">¥{{ fen2yuan(state.orderInfo.payPrice) }}</text>
+      </view>
+      <view
+        class="notice-item all-rpice-item ss-flex ss-m-t-20"
+        v-if="state.orderInfo.refundPrice > 0"
+      >
+        <text class="title">已退款</text>
+        <text class="detail all-price">¥{{ fen2yuan(state.orderInfo.refundPrice) }}</text>
+      </view>
+    </view>
+
+    <!-- 底部按钮 -->
+    <!-- TODO: 查看物流、等待成团、评价完后返回页面没刷新页面 -->
+    <su-fixed bottom placeholder bg="bg-white" v-if="state.orderInfo.buttons?.length">
+      <view class="footer-box ss-flex ss-col-center ss-row-right">
+        <button
+          class="ss-reset-button cancel-btn"
+          v-if="state.orderInfo.buttons?.includes('cancel')"
+          @tap="onCancel(state.orderInfo.id)"
+        >
+          取消订单
+        </button>
+        <button
+          class="ss-reset-button pay-btn ui-BG-Main-Gradient"
+          v-if="state.orderInfo.buttons?.includes('pay')"
+          @tap="onPay(state.orderInfo.payOrderId)"
+        >
+          继续支付
+        </button>
+        <button
+          class="ss-reset-button cancel-btn"
+          v-if="state.orderInfo.buttons?.includes('combination')"
+          @tap="
+            sheep.$router.go('/pages/activity/groupon/detail', {
+              id: state.orderInfo.combinationRecordId,
+            })
+          "
+        >
+          拼团详情
+        </button>
+        <button
+          class="ss-reset-button cancel-btn"
+          v-if="state.orderInfo.buttons?.includes('express')"
+          @tap="onExpress(state.orderInfo.id)"
+        >
+          查看物流
+        </button>
+        <button
+          class="ss-reset-button cancel-btn"
+          v-if="state.orderInfo.buttons?.includes('confirm')"
+          @tap="onConfirm(state.orderInfo.id)"
+        >
+          确认收货
+        </button>
+        <button
+          class="ss-reset-button cancel-btn"
+          v-if="state.orderInfo.buttons?.includes('comment')"
+          @tap="onComment(state.orderInfo.id)"
+        >
+          评价
+        </button>
+      </view>
+    </su-fixed>
+  </s-layout>
+</template>
+
+<script setup>
+  import sheep from '@/sheep';
+  import { onLoad, onShow } from '@dcloudio/uni-app';
+  import { reactive, ref } from 'vue';
+  import { isEmpty } from 'lodash-es';
+  import {
+    fen2yuan,
+    formatOrderStatus,
+    formatOrderStatusDescription,
+    handleOrderButtons,
+  } from '@/sheep/hooks/useGoods';
+  import OrderApi from '@/sheep/api/trade/order';
+  import DeliveryApi from '@/sheep/api/trade/delivery';
+  import PickUpVerify from '@/pages/order/pickUpVerify.vue';
+
+  const statusBarHeight = sheep.$platform.device.statusBarHeight * 2;
+  const headerBg = sheep.$url.css('/static/img/shop/order/order_bg.png');
+
+  const state = reactive({
+    orderInfo: {},
+    merchantTradeNo: '', // 商户订单号
+    comeinType: '', // 进入订单详情的来源类型
+  });
+
+  // ========== 门店自提(核销) ==========
+  const systemStore = ref({}); // 门店信息
+
+  // 复制
+  const onCopy = () => {
+    sheep.$helper.copyText(state.orderInfo.no);
+  };
+
+  // 去支付
+  function onPay(payOrderId) {
+    sheep.$router.go('/pages/pay/index', {
+      id: payOrderId,
+    });
+  }
+
+  // 查看商品
+  function onGoodsDetail(id) {
+    sheep.$router.go('/pages/goods/index', {
+      id,
+    });
+  }
+
+  // 取消订单
+  async function onCancel(orderId) {
+    uni.showModal({
+      title: '提示',
+      content: '确定要取消订单吗?',
+      success: async function (res) {
+        if (!res.confirm) {
+          return;
+        }
+        const { code } = await OrderApi.cancelOrder(orderId);
+        if (code === 0) {
+          await getOrderDetail(orderId);
+        }
+      },
+    });
+  }
+
+  // 查看物流
+  async function onExpress(id) {
+    sheep.$router.go('/pages/order/express/log', {
+      id,
+    });
+  }
+
+  // 确认收货
+  async function onConfirm(orderId, ignore = false) {
+    // 需开启确认收货组件
+    // todo: 芋艿:待接入微信
+    // 1.怎么检测是否开启了发货组件功能?如果没有开启的话就不能在这里return出去
+    // 2.如果开启了走mpConfirm方法,需要在App.vue的show方法中拿到确认收货结果
+    let isOpenBusinessView = true;
+    if (
+      sheep.$platform.name === 'WechatMiniProgram' &&
+      !isEmpty(state.orderInfo.wechat_extra_data) &&
+      isOpenBusinessView &&
+      !ignore
+    ) {
+      mpConfirm(orderId);
+      return;
+    }
+
+    uni.showModal({
+      title: '提示',
+      content: '确认收货吗?',
+      success: async function (res) {
+        if (!res.confirm) {
+          return;
+        }
+        // 正常的确认收货流程
+        const { code } = await OrderApi.receiveOrder(orderId);
+        if (code === 0) {
+          await getOrderDetail(orderId);
+        }
+      },
+    });
+  }
+
+  // #ifdef MP-WEIXIN
+  // 小程序确认收货组件
+  function mpConfirm(orderId) {
+    if (!wx.openBusinessView) {
+      sheep.$helper.toast(`请升级微信版本`);
+      return;
+    }
+    wx.openBusinessView({
+      businessType: 'weappOrderConfirm',
+      extraData: {
+        merchant_trade_no: state.orderInfo.wechat_extra_data.merchant_trade_no,
+        transaction_id: state.orderInfo.wechat_extra_data.transaction_id,
+      },
+      success(response) {
+        console.log('success:', response);
+        if (response.errMsg === 'openBusinessView:ok') {
+          if (response.extraData.status === 'success') {
+            onConfirm(orderId, true);
+          }
+        }
+      },
+      fail(error) {
+        console.log('error:', error);
+      },
+      complete(result) {
+        console.log('result:', result);
+      },
+    });
+  }
+
+  // #endif
+
+  // 评价
+  function onComment(id) {
+    sheep.$router.go('/pages/goods/comment/add', {
+      id,
+    });
+  }
+
+  const pickUpVerifyRef = ref();
+
+  async function getOrderDetail(id) {
+    // 对详情数据进行适配
+    let res;
+    if (state.comeinType === 'wechat') {
+      // TODO 芋艿:微信场景下
+      res = await OrderApi.getOrderDetail(id, {
+        merchant_trade_no: state.merchantTradeNo,
+      });
+    } else {
+      res = await OrderApi.getOrderDetail(id);
+    }
+    if (res.code === 0) {
+      state.orderInfo = res.data;
+      handleOrderButtons(state.orderInfo);
+      // 配送方式:门店自提
+      if (res.data.pickUpStoreId) {
+        const { data } = await DeliveryApi.getDeliveryPickUpStore(res.data.pickUpStoreId);
+        systemStore.value = data || {};
+      }
+      if (state.orderInfo.deliveryType === 2 && state.orderInfo.payStatus) {
+        pickUpVerifyRef.value && pickUpVerifyRef.value.markCode(res.data.pickUpVerifyCode);
+      }
+    } else {
+      sheep.$router.back();
+    }
+  }
+
+  onShow(async () => {
+    //onShow中获取订单列表,保证跳转后页面为最新状态
+    await getOrderDetail(state.orderInfo.id);
+  })
+
+  onLoad(async (options) => {
+    let id = 0;
+    if (options.id) {
+      id = options.id;
+    }
+    // TODO 芋艿:下面两个变量,后续接入
+    state.comeinType = options.comein_type;
+    if (state.comeinType === 'wechat') {
+      state.merchantTradeNo = options.merchant_trade_no;
+    }
+    state.orderInfo.id = id
+  });
+</script>
+
+<style lang="scss" scoped>
+  .score-img {
+    width: 36rpx;
+    height: 36rpx;
+    margin: 0 4rpx;
+  }
+
+  .apply-btn {
+    width: 140rpx;
+    height: 50rpx;
+    border-radius: 25rpx;
+    font-size: 24rpx;
+    border: 2rpx solid #dcdcdc;
+    line-height: normal;
+    margin-left: 16rpx;
+  }
+
+  .state-box {
+    color: rgba(#fff, 0.9);
+    width: 100%;
+    background: v-bind(headerBg) no-repeat,
+      linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
+    background-size: 750rpx 100%;
+    box-sizing: border-box;
+
+    .state-img {
+      width: 60rpx;
+      height: 60rpx;
+      margin-right: 20rpx;
+    }
+  }
+
+  .order-address-box {
+    background-color: #fff;
+    border-radius: 10rpx;
+    margin: -50rpx 20rpx 16rpx 20rpx;
+    padding: 44rpx 34rpx 42rpx 20rpx;
+    font-size: 30rpx;
+    box-sizing: border-box;
+    font-weight: 500;
+    color: rgba(51, 51, 51, 1);
+
+    .address-username {
+      margin-right: 20rpx;
+    }
+
+    .address-detail {
+      font-size: 26rpx;
+      font-weight: 500;
+      color: rgba(153, 153, 153, 1);
+      margin-top: 20rpx;
+    }
+  }
+
+  .detail-goods {
+    border-radius: 10rpx;
+    margin: 0 20rpx 20rpx 20rpx;
+
+    .order-list {
+      margin-bottom: 20rpx;
+      background-color: #fff;
+
+      .order-card {
+        padding: 20rpx 0;
+
+        .order-sku {
+          font-size: 24rpx;
+
+          font-weight: 400;
+          color: rgba(153, 153, 153, 1);
+          width: 450rpx;
+          margin-bottom: 20rpx;
+
+          .order-num {
+            margin-right: 10rpx;
+          }
+        }
+
+        .tag-btn {
+          margin-left: 16rpx;
+          font-size: 24rpx;
+          height: 36rpx;
+          color: var(--ui-BG-Main);
+          border: 2rpx solid var(--ui-BG-Main);
+          border-radius: 14rpx;
+          padding: 0 4rpx;
+        }
+      }
+    }
+  }
+
+  // 订单信息。
+  .notice-box {
+    background: #fff;
+    border-radius: 10rpx;
+    margin: 0 20rpx 20rpx 20rpx;
+
+    .notice-box__head {
+      font-size: 30rpx;
+
+      font-weight: 500;
+      color: rgba(51, 51, 51, 1);
+      line-height: 80rpx;
+      border-bottom: 1rpx solid #dfdfdf;
+      padding: 0 25rpx;
+    }
+
+    .notice-box__content {
+      padding: 20rpx;
+
+      .self-pickup-box {
+        width: 100%;
+
+        .self-pickup--img {
+          width: 200rpx;
+          height: 200rpx;
+          margin: 40rpx 0;
+        }
+      }
+    }
+
+    .notice-item,
+    .notice-item--center {
+      display: flex;
+      align-items: center;
+      line-height: normal;
+      margin-bottom: 24rpx;
+
+      .title {
+        font-size: 28rpx;
+        color: #999;
+      }
+
+      .detail {
+        font-size: 28rpx;
+        color: #333;
+        flex: 1;
+      }
+    }
+  }
+
+  .copy-btn {
+    width: 100rpx;
+    line-height: 50rpx;
+    border-radius: 25rpx;
+    padding: 0;
+    background: rgba(238, 238, 238, 1);
+    font-size: 22rpx;
+    font-weight: 400;
+    color: rgba(51, 51, 51, 1);
+  }
+
+  // 订单价格信息
+  .order-price-box {
+    background-color: #fff;
+    border-radius: 10rpx;
+    padding: 20rpx;
+    margin: 0 20rpx 20rpx 20rpx;
+
+    .notice-item {
+      line-height: 70rpx;
+
+      .title {
+        font-size: 28rpx;
+        color: #999;
+      }
+
+      .detail {
+        font-size: 28rpx;
+        color: #333;
+        font-family: OPPOSANS;
+      }
+    }
+
+    .all-rpice-item {
+      justify-content: flex-end;
+      align-items: center;
+
+      .title {
+        font-size: 26rpx;
+        font-weight: 500;
+        color: #333333;
+        line-height: normal;
+      }
+
+      .all-price {
+        font-size: 26rpx;
+        font-family: OPPOSANS;
+        line-height: normal;
+        color: $red;
+      }
+    }
+  }
+
+  // 底部
+  .footer-box {
+    height: 100rpx;
+    width: 100%;
+    box-sizing: border-box;
+    border-radius: 10rpx;
+    padding-right: 20rpx;
+
+    .cancel-btn {
+      width: 160rpx;
+      height: 60rpx;
+      background: #eeeeee;
+      border-radius: 30rpx;
+      margin-right: 20rpx;
+      font-size: 26rpx;
+      font-weight: 400;
+      color: #333333;
+    }
+
+    .pay-btn {
+      width: 160rpx;
+      height: 60rpx;
+      font-size: 26rpx;
+      border-radius: 30rpx;
+      font-weight: 500;
+      color: #fff;
+    }
+  }
+</style>

+ 162 - 0
pages/order/express/log.vue

@@ -0,0 +1,162 @@
+<!-- 物流追踪 -->
+<template>
+  <s-layout title="物流追踪">
+    <view class="log-wrap">
+      <!-- 商品信息 -->
+      <view class="log-card ss-flex ss-m-20 ss-r-10" v-if="goodsImages.length > 0">
+        <uni-swiper-dot :info="goodsImages" :current="state.current" mode="round">
+          <swiper class="swiper-box">
+            <swiper-item v-for="(item, index) in goodsImages" :key="index">
+              <image class="log-card-img" :src="sheep.$url.static(item.image)" />
+            </swiper-item>
+          </swiper>
+        </uni-swiper-dot>
+        <view class="log-card-msg">
+          <!-- TODO 芋艿:【物流】优化点:展示状态 -->
+          <!--          <view class="ss-flex ss-m-b-8">-->
+          <!--            <view>物流状态:</view>-->
+          <!--            <view class="warning-color">{{ state.info.status_text }}</view>-->
+          <!--          </view>-->
+          <view class="ss-m-b-8">快递单号:{{ state.info.logisticsNo }}</view>
+          <view>快递公司:{{ state.info.logisticsName }}</view>
+        </view>
+      </view>
+
+      <!-- 物流轨迹 -->
+      <view class="log-content ss-m-20 ss-r-10">
+        <view
+          class="log-content-box ss-flex"
+          v-for="(item, index) in state.tracks"
+          :key="item.title"
+        >
+          <view class="log-icon ss-flex-col ss-col-center ss-m-r-20">
+            <text class="cicon-title" />
+            <view v-if="state.tracks.length - 1 !== index" class="line" />
+          </view>
+          <view class="log-content-msg">
+            <!-- TODO 芋艿:【物流】优化点:展示状态 -->
+            <!--            <view class="log-msg-title ss-m-b-20">-->
+            <!--              {{ item.status_text }}-->
+            <!--            </view>-->
+            <view class="log-msg-desc ss-m-b-16">{{ item.content }}</view>
+            <view class="log-msg-date ss-m-b-40">
+              {{ sheep.$helper.timeFormat(item.time, 'yyyy-mm-dd hh:MM:ss') }}
+            </view>
+          </view>
+        </view>
+      </view>
+    </view>
+  </s-layout>
+</template>
+
+<script setup>
+  import sheep from '@/sheep';
+  import { onLoad } from '@dcloudio/uni-app';
+  import { computed, reactive } from 'vue';
+  import OrderApi from '@/sheep/api/trade/order';
+
+  const state = reactive({
+    info: [],
+    tracks: [],
+  });
+
+  const goodsImages = computed(() => {
+    let array = [];
+    if (state.info.items) {
+      state.info.items.forEach((item) => {
+        array.push({
+          image: item.picUrl,
+        });
+      });
+    }
+    return array;
+  });
+
+  async function getExpressDetail(id) {
+    const { data } = await OrderApi.getOrderExpressTrackList(id);
+    state.tracks = data.reverse();
+  }
+
+  async function getOrderDetail(id) {
+    const { data } = await OrderApi.getOrderDetail(id);
+    state.info = data;
+  }
+
+  onLoad((options) => {
+    getExpressDetail(options.id);
+    getOrderDetail(options.id);
+  });
+</script>
+
+<style lang="scss" scoped>
+  .swiper-box {
+    width: 200rpx;
+    height: 200rpx;
+  }
+  .log-card {
+    border-top: 2rpx solid rgba(#dfdfdf, 0.5);
+    padding: 20rpx;
+    background: #fff;
+    margin-bottom: 20rpx;
+    .log-card-img {
+      width: 200rpx;
+      height: 200rpx;
+      margin-right: 20rpx;
+    }
+    .log-card-msg {
+      font-size: 28rpx;
+      font-weight: 500;
+      width: 490rpx;
+      color: #333333;
+      .warning-color {
+        color: #999;
+      }
+    }
+  }
+  .log-content {
+    padding: 34rpx 20rpx 0rpx 20rpx;
+    background: #fff;
+    .log-content-box {
+      align-items: stretch;
+    }
+    .log-icon {
+      height: inherit;
+      .cicon-title {
+        color: #ccc;
+        font-size: 40rpx;
+      }
+      .activity-color {
+        color: #f0c785;
+        font-size: 40rpx;
+      }
+      .info-color {
+        color: #ccc;
+        font-size: 40rpx;
+      }
+      .line {
+        width: 1px;
+        height: 100%;
+        background: #d8d8d8;
+      }
+    }
+
+    .log-content-msg {
+      .log-msg-title {
+        font-size: 28rpx;
+        font-weight: bold;
+        color: #333333;
+      }
+      .log-msg-desc {
+        font-size: 24rpx;
+        font-weight: 400;
+        color: #333333;
+        line-height: 36rpx;
+      }
+      .log-msg-date {
+        font-size: 24rpx;
+        font-weight: 500;
+        color: #999999;
+      }
+    }
+  }
+</style>

+ 504 - 0
pages/order/list.vue

@@ -0,0 +1,504 @@
+<!-- 订单列表 -->
+<template>
+  <s-layout title="我的订单">
+    <su-sticky bgColor="#fff">
+      <su-tabs
+        :list="tabMaps"
+        :scrollable="false"
+        @change="onTabsChange"
+        :current="state.currentTab"
+      />
+    </su-sticky>
+    <s-empty v-if="state.pagination.total === 0" icon="/static/order-empty.png" text="暂无订单" />
+    <view v-if="state.pagination.total > 0">
+      <view
+        class="bg-white order-list-card-box ss-r-10 ss-m-t-14 ss-m-20"
+        v-for="order in state.pagination.list"
+        :key="order.id"
+        @tap="onOrderDetail(order.id)"
+      >
+        <view class="order-card-header ss-flex ss-col-center ss-row-between ss-p-x-20">
+          <view class="order-no">订单号:{{ order.no }}</view>
+          <view class="order-state ss-font-26" :class="formatOrderColor(order)">
+            {{ formatOrderStatus(order) }}
+          </view>
+        </view>
+        <view class="border-bottom" v-for="item in order.items" :key="item.id">
+          <s-goods-item
+            :img="item.picUrl"
+            :title="item.spuName"
+            :skuText="item.properties.map((property) => property.valueName).join(' ')"
+            :price="item.price"
+            :num="item.count"
+          />
+        </view>
+        <view class="pay-box ss-m-t-30 ss-flex ss-row-right ss-p-r-20">
+          <view class="ss-flex ss-col-center">
+            <view class="discounts-title pay-color"
+              >共 {{ order.productCount }} 件商品,总金额:</view
+            >
+            <view class="discounts-money pay-color"> ¥{{ fen2yuan(order.payPrice) }} </view>
+          </view>
+        </view>
+        <view
+          class="order-card-footer ss-flex ss-col-center ss-p-x-20"
+          :class="order.buttons.length > 3 ? 'ss-row-between' : 'ss-row-right'"
+        >
+          <view class="ss-flex ss-col-center">
+            <button
+              v-if="order.buttons.includes('combination')"
+              class="tool-btn ss-reset-button"
+              @tap.stop="onOrderGroupon(order)"
+            >
+              拼团详情
+            </button>
+            <button
+              v-if="order.buttons.length === 0"
+              class="tool-btn ss-reset-button"
+              @tap.stop="onOrderDetail(order.id)"
+            >
+              查看详情
+            </button>
+            <button
+              v-if="order.buttons.includes('confirm')"
+              class="tool-btn ss-reset-button"
+              @tap.stop="onConfirm(order)"
+            >
+              确认收货
+            </button>
+            <button
+              v-if="order.buttons.includes('express')"
+              class="tool-btn ss-reset-button"
+              @tap.stop="onExpress(order.id)"
+            >
+              查看物流
+            </button>
+            <button
+              v-if="order.buttons.includes('cancel')"
+              class="tool-btn ss-reset-button"
+              @tap.stop="onCancel(order.id)"
+            >
+              取消订单
+            </button>
+            <button
+              v-if="order.buttons.includes('comment')"
+              class="tool-btn ss-reset-button"
+              @tap.stop="onComment(order.id)"
+            >
+              评价
+            </button>
+            <button
+              v-if="order.buttons.includes('delete')"
+              class="delete-btn ss-reset-button"
+              @tap.stop="onDelete(order.id)"
+            >
+              删除订单
+            </button>
+            <button
+              v-if="order.buttons.includes('pay')"
+              class="tool-btn ss-reset-button ui-BG-Main-Gradient"
+              @tap.stop="onPay(order.payOrderId)"
+            >
+              继续支付
+            </button>
+          </view>
+        </view>
+      </view>
+    </view>
+
+    <!-- 加载更多 -->
+    <uni-load-more
+      v-if="state.pagination.total > 0"
+      :status="state.loadStatus"
+      :content-text="{
+        contentdown: '上拉加载更多',
+      }"
+      @tap="loadMore"
+    />
+  </s-layout>
+</template>
+
+<script setup>
+  import { reactive } from 'vue';
+  import { onLoad, onReachBottom, onPullDownRefresh } from '@dcloudio/uni-app';
+  import {
+    fen2yuan,
+    formatOrderColor,
+    formatOrderStatus,
+    handleOrderButtons,
+  } from '@/sheep/hooks/useGoods';
+  import sheep from '@/sheep';
+  import _ from 'lodash-es';
+  import { isEmpty } from 'lodash-es';
+  import OrderApi from '@/sheep/api/trade/order';
+  import { resetPagination } from '@/sheep/util';
+
+  // 数据
+  const state = reactive({
+    currentTab: 0, // 选中的 tabMaps 下标
+    pagination: {
+      list: [],
+      total: 0,
+      pageNo: 1,
+      pageSize: 5,
+    },
+    loadStatus: '',
+  });
+
+  const tabMaps = [
+    {
+      name: '全部',
+    },
+    {
+      name: '待付款',
+      value: 0,
+    },
+    {
+      name: '待发货',
+      value: 10,
+    },
+    {
+      name: '待收货',
+      value: 20,
+    },
+    {
+      name: '待评价',
+      value: 30,
+    },
+  ];
+
+  // 切换选项卡
+  function onTabsChange(e) {
+    if (state.currentTab === e.index) {
+      return;
+    }
+    // 重头加载代码
+    resetPagination(state.pagination);
+    state.currentTab = e.index;
+    getOrderList();
+  }
+
+  // 订单详情
+  function onOrderDetail(id) {
+    sheep.$router.go('/pages/order/detail', {
+      id,
+    });
+  }
+
+  // 跳转拼团记录的详情
+  function onOrderGroupon(order) {
+    sheep.$router.go('/pages/activity/groupon/detail', {
+      id: order.combinationRecordId,
+    });
+  }
+
+  // 继续支付
+  function onPay(payOrderId) {
+    sheep.$router.go('/pages/pay/index', {
+      id: payOrderId,
+    });
+  }
+
+  // 评价
+  function onComment(id) {
+    sheep.$router.go('/pages/goods/comment/add', {
+      id,
+    });
+  }
+
+  // 确认收货 TODO 芋艿:待测试
+  async function onConfirm(order, ignore = false) {
+    // 需开启确认收货组件
+    // todo: 芋艿:需要后续接入微信收货组件
+    // 1.怎么检测是否开启了发货组件功能?如果没有开启的话就不能在这里return出去
+    // 2.如果开启了走mpConfirm方法,需要在App.vue的show方法中拿到确认收货结果
+    let isOpenBusinessView = true;
+    if (
+      sheep.$platform.name === 'WechatMiniProgram' &&
+      !isEmpty(order.wechat_extra_data) &&
+      isOpenBusinessView &&
+      !ignore
+    ) {
+      mpConfirm(order);
+      return;
+    }
+
+    uni.showModal({
+      title: '提示',
+      content: '确认收货吗?',
+      success: async function (res) {
+        if (!res.confirm) {
+          return;
+        }
+        // 正常的确认收货流程
+        const { code } = await OrderApi.receiveOrder(order.id);
+        if (code === 0) {
+          resetPagination(state.pagination);
+          await getOrderList();
+        }
+      },
+    });
+  }
+
+  // #ifdef MP-WEIXIN
+  // 小程序确认收货组件 TODO 芋艿:后续再接入
+  function mpConfirm(order) {
+    if (!wx.openBusinessView) {
+      sheep.$helper.toast(`请升级微信版本`);
+      return;
+    }
+    wx.openBusinessView({
+      businessType: 'weappOrderConfirm',
+      extraData: {
+        merchant_id: '1481069012',
+        merchant_trade_no: order.wechat_extra_data.merchant_trade_no,
+        transaction_id: order.wechat_extra_data.transaction_id,
+      },
+      success(response) {
+        console.log('success:', response);
+        if (response.errMsg === 'openBusinessView:ok') {
+          if (response.extraData.status === 'success') {
+            onConfirm(order, true);
+          }
+        }
+      },
+      fail(error) {
+        console.log('error:', error);
+      },
+      complete(result) {
+        console.log('result:', result);
+      },
+    });
+  }
+  // #endif
+
+  // 查看物流
+  async function onExpress(id) {
+    sheep.$router.go('/pages/order/express/log', {
+      id,
+    });
+  }
+
+  // 取消订单
+  async function onCancel(orderId) {
+    uni.showModal({
+      title: '提示',
+      content: '确定要取消订单吗?',
+      success: async function (res) {
+        if (!res.confirm) {
+          return;
+        }
+        const { code } = await OrderApi.cancelOrder(orderId);
+        if (code === 0) {
+          // 修改数据的状态
+          let index = state.pagination.list.findIndex((order) => order.id === orderId);
+          const orderInfo = state.pagination.list[index];
+          orderInfo.status = 40;
+          handleOrderButtons(orderInfo);
+        }
+      },
+    });
+  }
+
+  // 删除订单
+  function onDelete(orderId) {
+    uni.showModal({
+      title: '提示',
+      content: '确定要删除订单吗?',
+      success: async function (res) {
+        if (res.confirm) {
+          const { code } = await OrderApi.deleteOrder(orderId);
+          if (code === 0) {
+            // 删除数据
+            let index = state.pagination.list.findIndex((order) => order.id === orderId);
+            state.pagination.list.splice(index, 1);
+          }
+        }
+      },
+    });
+  }
+
+  // 获取订单列表
+  async function getOrderList() {
+    state.loadStatus = 'loading';
+    let { code, data } = await OrderApi.getOrderPage({
+      pageNo: state.pagination.pageNo,
+      pageSize: state.pagination.pageSize,
+      status: tabMaps[state.currentTab].value,
+      commentStatus: tabMaps[state.currentTab].value === 30 ? false : null,
+    });
+    if (code !== 0) {
+      return;
+    }
+    data.list.forEach((order) => handleOrderButtons(order));
+    state.pagination.list = _.concat(state.pagination.list, data.list);
+    state.pagination.total = data.total;
+    state.loadStatus = state.pagination.list.length < state.pagination.total ? 'more' : 'noMore';
+  }
+
+  onLoad(async (options) => {
+    if (options.type) {
+      state.currentTab = options.type;
+    }
+    await getOrderList();
+  });
+
+  // 加载更多
+  function loadMore() {
+    if (state.loadStatus === 'noMore') {
+      return;
+    }
+    state.pagination.pageNo++;
+    getOrderList();
+  }
+
+  // 上拉加载更多
+  onReachBottom(() => {
+    loadMore();
+  });
+
+  // 下拉刷新
+  onPullDownRefresh(() => {
+    resetPagination(state.pagination);
+    getOrderList();
+    setTimeout(function () {
+      uni.stopPullDownRefresh();
+    }, 800);
+  });
+</script>
+
+<style lang="scss" scoped>
+  .score-img {
+    width: 36rpx;
+    height: 36rpx;
+    margin: 0 4rpx;
+  }
+
+  .tool-btn {
+    width: 160rpx;
+    height: 60rpx;
+    background: #f6f6f6;
+    font-size: 26rpx;
+    border-radius: 30rpx;
+    margin-right: 10rpx;
+
+    &:last-of-type {
+      margin-right: 0;
+    }
+  }
+
+  .delete-btn {
+    width: 160rpx;
+    height: 56rpx;
+    color: #ff3000;
+    background: #fee;
+    border-radius: 28rpx;
+    font-size: 26rpx;
+    margin-right: 10rpx;
+    line-height: normal;
+
+    &:last-of-type {
+      margin-right: 0;
+    }
+  }
+
+  .apply-btn {
+    width: 140rpx;
+    height: 50rpx;
+    border-radius: 25rpx;
+    font-size: 24rpx;
+    border: 2rpx solid #dcdcdc;
+    line-height: normal;
+    margin-left: 16rpx;
+  }
+
+  .swiper-box {
+    flex: 1;
+
+    .swiper-item {
+      height: 100%;
+      width: 100%;
+    }
+  }
+
+  .order-list-card-box {
+    .order-card-header {
+      height: 80rpx;
+
+      .order-no {
+        font-size: 26rpx;
+        font-weight: 500;
+      }
+
+      .order-state {
+      }
+    }
+
+    .pay-box {
+      .discounts-title {
+        font-size: 24rpx;
+        line-height: normal;
+        color: #999999;
+      }
+
+      .discounts-money {
+        font-size: 24rpx;
+        line-height: normal;
+        color: #999;
+        font-family: OPPOSANS;
+      }
+
+      .pay-color {
+        color: #333;
+      }
+    }
+
+    .order-card-footer {
+      height: 100rpx;
+
+      .more-item-box {
+        padding: 20rpx;
+
+        .more-item {
+          height: 60rpx;
+
+          .title {
+            font-size: 26rpx;
+          }
+        }
+      }
+
+      .more-btn {
+        color: $dark-9;
+        font-size: 24rpx;
+      }
+
+      .content {
+        width: 154rpx;
+        color: #333333;
+        font-size: 26rpx;
+        font-weight: 500;
+      }
+    }
+  }
+
+  :deep(.uni-tooltip-popup) {
+    background: var(--ui-BG);
+  }
+
+  .warning-color {
+    color: #faad14;
+  }
+
+  .danger-color {
+    color: #ff3000;
+  }
+
+  .success-color {
+    color: #52c41a;
+  }
+
+  .info-color {
+    color: #999999;
+  }
+</style>

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio