Harper před 1 rokem
revize
92779307cc
100 změnil soubory, kde provedl 18366 přidání a 0 odebrání
  1. 12 0
      .gitignore
  2. binární
      .image/common/mall-feature.png
  3. binární
      .image/common/mall-preview.png
  4. binární
      .image/common/project-vs.png
  5. binární
      .image/common/ruoyi-vue-pro-architecture.png
  6. binární
      .image/common/ruoyi-vue-pro-biz.png
  7. binární
      .image/common/yudao-cloud-architecture.png
  8. binární
      .image/common/yudao-roadmap.png
  9. binární
      .image/mall/会员详情.png
  10. binární
      .image/mall/商品详情.png
  11. binární
      .image/mall/店铺装修.png
  12. binární
      .image/mall/营销中心.png
  13. binární
      .image/mall/订单详情.png
  14. 6 0
      .prettierignore
  15. 10 0
      .prettierrc
  16. 39 0
      App.vue
  17. 21 0
      LICENSE
  18. 56 0
      README.md
  19. 3 0
      androidPrivacy.json
  20. 17 0
      index.html
  21. 9 0
      jsconfig.json
  22. 15 0
      main.js
  23. 225 0
      manifest.json
  24. 104 0
      package.json
  25. 677 0
      pages.json
  26. 480 0
      pages/activity/bargain/detail.vue
  27. 370 0
      pages/activity/bargain/list.vue
  28. 508 0
      pages/activity/groupon/detail.vue
  29. 225 0
      pages/activity/groupon/list.vue
  30. 239 0
      pages/activity/groupon/order.vue
  31. 206 0
      pages/activity/index.vue
  32. 385 0
      pages/activity/seckill/list.vue
  33. 401 0
      pages/app/sign.vue
  34. 63 0
      pages/chat/components/goods.vue
  35. 122 0
      pages/chat/components/order.vue
  36. 152 0
      pages/chat/components/select-popup.vue
  37. 58 0
      pages/chat/emoji.js
  38. 870 0
      pages/chat/index.vue
  39. 821 0
      pages/chat/socket.js
  40. 160 0
      pages/commission/commission-ranking.vue
  41. 125 0
      pages/commission/components/account-info.vue
  42. 160 0
      pages/commission/components/account-type-select.vue
  43. 101 0
      pages/commission/components/commission-auth.vue
  44. 113 0
      pages/commission/components/commission-info.vue
  45. 165 0
      pages/commission/components/commission-log.vue
  46. 138 0
      pages/commission/components/commission-menu.vue
  47. 150 0
      pages/commission/goods.vue
  48. 37 0
      pages/commission/index.vue
  49. 331 0
      pages/commission/order.vue
  50. 158 0
      pages/commission/promoter.vue
  51. 581 0
      pages/commission/team.vue
  52. 470 0
      pages/commission/wallet.vue
  53. 427 0
      pages/commission/withdraw.vue
  54. 378 0
      pages/coupon/detail.vue
  55. 218 0
      pages/coupon/list.vue
  56. 145 0
      pages/goods/comment/add.vue
  57. 167 0
      pages/goods/comment/list.vue
  58. 94 0
      pages/goods/components/detail/comment-item.vue
  59. 100 0
      pages/goods/components/detail/detail-activity-tip.vue
  60. 31 0
      pages/goods/components/detail/detail-cell-sku.vue
  61. 60 0
      pages/goods/components/detail/detail-cell.vue
  62. 106 0
      pages/goods/components/detail/detail-comment-card.vue
  63. 52 0
      pages/goods/components/detail/detail-content-card.vue
  64. 256 0
      pages/goods/components/detail/detail-navbar.vue
  65. 40 0
      pages/goods/components/detail/detail-progress.vue
  66. 177 0
      pages/goods/components/detail/detail-skeleton.vue
  67. 169 0
      pages/goods/components/detail/detail-tabbar.vue
  68. 141 0
      pages/goods/components/groupon/groupon-card-list.vue
  69. 103 0
      pages/goods/components/list/list-goods-card.vue
  70. 93 0
      pages/goods/components/list/list-navbar.vue
  71. 532 0
      pages/goods/groupon.vue
  72. 413 0
      pages/goods/index.vue
  73. 362 0
      pages/goods/list.vue
  74. 555 0
      pages/goods/seckill.vue
  75. 200 0
      pages/index/cart.vue
  76. 236 0
      pages/index/category.vue
  77. 26 0
      pages/index/components/first-one.vue
  78. 66 0
      pages/index/components/first-two.vue
  79. 80 0
      pages/index/components/second-one.vue
  80. 87 0
      pages/index/index.vue
  81. 38 0
      pages/index/login.vue
  82. 51 0
      pages/index/page.vue
  83. 119 0
      pages/index/search.vue
  84. 42 0
      pages/index/user.vue
  85. 357 0
      pages/order/aftersale/apply.vue
  86. 342 0
      pages/order/aftersale/detail.vue
  87. 193 0
      pages/order/aftersale/list.vue
  88. 77 0
      pages/order/aftersale/log-item.vue
  89. 38 0
      pages/order/aftersale/log.vue
  90. 194 0
      pages/order/aftersale/return-delivery.vue
  91. 394 0
      pages/order/confirm.vue
  92. 626 0
      pages/order/detail.vue
  93. 162 0
      pages/order/express/log.vue
  94. 459 0
      pages/order/list.vue
  95. 288 0
      pages/pay/index.vue
  96. 165 0
      pages/pay/recharge-log.vue
  97. 259 0
      pages/pay/recharge.vue
  98. 287 0
      pages/pay/result.vue
  99. 60 0
      pages/public/error.vue
  100. 118 0
      pages/public/faq.vue

+ 12 - 0
.gitignore

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

binární
.image/common/mall-feature.png


binární
.image/common/mall-preview.png


binární
.image/common/project-vs.png


binární
.image/common/ruoyi-vue-pro-architecture.png


binární
.image/common/ruoyi-vue-pro-biz.png


binární
.image/common/yudao-cloud-architecture.png


binární
.image/common/yudao-roadmap.png


binární
.image/mall/会员详情.png


binární
.image/mall/商品详情.png


binární
.image/mall/店铺装修.png


binární
.image/mall/营销中心.png


binární
.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://github.com/YunaiV/ruoyi-vue-pro>
+
+![架构图](/.image/common/ruoyi-vue-pro-architecture.png)
+
+② Spring Cloud 微服务架构:<https://github.com/YunaiV/yudao-cloud>
+
+![架构图](/.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,
+  };
+}

+ 225 - 0
manifest.json

@@ -0,0 +1,225 @@
+{
+    "name" : "非繁科技",
+    "appid" : "__UNI__BAFF75B",
+    "description" : "基于 uni-app + Vue3 技术驱动的在线商城系统,内含诸多功能与丰富的活动,期待您的使用和反馈。",
+    "versionName" : "1.8.3",
+    "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.VIBRATE\"/>",
+                    "<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" : "wx63c280fe3248a3e7",
+        "setting" : {
+            "urlCheck" : false,
+            "minified" : true,
+            "postcss" : true
+        },
+        "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" : "hash",
+            "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"
+}

+ 104 - 0
package.json

@@ -0,0 +1,104 @@
+{
+  "id": "shopro",
+  "name": "shopro",
+  "displayName": "芋道商城",
+  "version": "1.0.1",
+  "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": {
+    "@hyoga/uni-socket.io": "^1.0.1",
+    "dayjs": "^1.11.7",
+    "lodash": "^4.17.21",
+    "luch-request": "^3.0.8",
+    "pinia": "^2.0.33",
+    "pinia-plugin-persist-uni": "^1.2.0",
+    "qs-canvas": "^1.0.11",
+    "weixin-js-sdk": "^1.6.0"
+  },
+  "devDependencies": {
+    "prettier": "^2.8.7",
+    "vconsole": "^3.15.0"
+  }
+}

+ 677 - 0
pages.json

@@ -0,0 +1,677 @@
+{
+	"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": "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": "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": "bargain/list",
+					"style": {
+						"navigationBarTitleText": "砍价列表"
+					},
+					"meta": {
+						"sync": true,
+						"title": "砍价列表",
+						"group": "营销活动"
+					}
+				},
+				{
+					"path": "bargain/detail",
+					"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"
+			}
+		]
+	}
+}

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 480 - 0
pages/activity/bargain/detail.vue


+ 370 - 0
pages/activity/bargain/list.vue

@@ -0,0 +1,370 @@
+<template>
+	<!-- TODO @科举:参考 groupon/list.vue 和 seckill/list.vue 界面,调整下头部,就是从 5 到 11 行的  -->
+	<s-layout navbar="inner" title='砍价列表'>
+		<view style='background-color: red;height:100vh;'>
+			<view class='bargain-list'>
+				<!-- #ifdef H5 -->
+				<view class='iconfont icon-xiangzuo' @tap='goBack' :style="'top:'+ (state.navH/2) +'rpx'"
+					v-if="state.returnShow">
+				</view>
+				<!-- #endif -->
+
+				<!-- 砍价记录的概要 -->
+				<view class='header'>
+					<view class="pic">
+						<view class='swipers'>
+							<swiper indicator-dots="true" autoplay="true" interval="2500" duration="500" vertical="true"
+								circular="true">
+								<block v-for="(item,index) in state.bargainSuccessList" :key='index'>
+									<swiper-item>
+										<view class="acea-row row-middle" style='display:flex'>
+											<image :src="item.avatar" class="mr9"></image>
+											<view class='mr9 nickName'>{{ item.nickname }}</view>
+											<text class='mr9'>拿了</text>
+											<view class='line1'>{{ item.activityName }}</view>
+										</view>
+									</swiper-item>
+								</block>
+							</swiper>
+						</view>
+					</view>
+					<view class="tit">已有{{ state.bargainTotal }}人砍成功</view>
+				</view>
+
+				<!-- 砍价活动列表 -->
+				<view class='list'>
+					<block v-for="(item,index) in state.bargainList" :key="index">
+						<view style='display:flex' class='item acea-row row-between-wrapper'
+							@tap="openSubscribe('/pages/activity/bargain/detail?id='+ item.id)">
+							<view class='pictrue'>
+								<image :src='item.picUrl'></image>
+							</view>
+							<view class='text acea-row row-column-around'>
+								<view class='name line2'>{{ item.name }}</view>
+								<view class="acea-row" style="margin-bottom: 14rpx;display:flex">
+									<s-count-down :tipText="' '" :bgColor="state.bgColor" :dayText="':'" :hourText="':'"
+										:minuteText="':'" :secondText="' '" :datatime="item.endTime / 1000"
+										:isDay="true" />
+									<text class="txt">后结束</text>
+								</view>
+								<view v-if="item.stock === 0">
+									<view style="font-size: 22rpx;"
+										@tap="openSubscribe('/pages/activity/goods_bargain_details/index?id='+ item.id +'&startBargainUid='+ uid)">
+										已售罄</view>
+								</view>
+								<view class='money font-color'>最低: ¥<text
+										class='price'>{{ fen2yuan(item.bargainMinPrice) }}</text></view>
+							</view>
+							<view v-if="item.stock > 0" class='cutBnt bg-color'>参与砍价</view>
+							<view v-if="item.stock === 0" class='cutBnt bg-color-hui'>已售罄</view>
+						</view>
+					</block>
+					<view class='loadingicon acea-row row-center-wrapper' v-if='state.bargainList.length > 0'
+						style='text-align: center;'>
+						<text class='loading iconfont icon-jiazai' :hidden='!loading'></text>{{state.loadTitle}}
+					</view>
+				</view>
+			</view>
+		</view>
+	</s-layout>
+</template>
+
+<script setup>
+	import {
+		reactive
+	} from 'vue';
+	import sheep from '@/sheep';
+	import _ from 'lodash';
+	import {
+		onLoad,
+		onReachBottom
+	} from '@dcloudio/uni-app';
+	import {
+		fen2yuan
+	} from '@/sheep/hooks/useGoods';
+	import BargainApi from '@/sheep/api/promotion/bargain'
+	const state = reactive({
+		navH: '',
+		returnShow: true,
+
+		// ========== 砍价记录概要的相关变量 ==========
+		bargainTotal: 0,
+		bargainSuccessList: [],
+
+		// ========== 砍价活动的相关变量 ==========
+		bargainList: [],
+		page: 1,
+		limit: 10,
+		loading: false,
+		loadend: false,
+		bgColor: {
+			'bgColor': '#E93323',
+			'Color': '#fff',
+			'width': '44rpx',
+			'timeTxtwidth': '16rpx',
+			'isDay': true
+		},
+		loadTitle: '加载更多',
+	});
+
+	async function getBargainHeader() {
+		let {
+			code,
+			data
+		} = await BargainApi.getBargainRecordSummary()
+		if (code == 0) {
+			state.bargainTotal = data.successUserCount;
+			state.bargainSuccessList = data.successList;
+		} else {
+			state.$util.Tips({
+				title: data
+			});
+		}
+	}
+
+	async function getBargainList() {
+		// TODO @科举:loading 和 loadTitle 改成现在这个项目的风格,包括组件使用 uni-load-more
+		if (state.loadend || state.loading) {
+			return;
+		}
+		state.loading = true;
+		state.loadTitle = '';
+		let {
+			data,
+			code
+		} = await BargainApi.getBargainActivityPage({
+			pageNo: state.page,
+			pageSize: state.limit
+		})
+		if (code == 0) {
+			const list = data.list;
+			const bargainList = _.concat(state.bargainList, list);
+			const loadend = list.length < state.limit;
+			state.loadend = loadend;
+			state.loading = false;
+			state.loadTitle = loadend ? '已全部加载' : '加载更多';
+			// this.$set(this, 'bargainList', bargainList);
+			state.bargainList = data.list
+			// this.$set(this, 'page', this.page + 1);
+			state.page = state.page + 1;
+
+		} else {
+			state.loading = false;
+			state.loadTitle = '加载更多';
+		}
+	}
+
+	function openSubscribe(e) {
+		console.log('跳转')
+		console.log(e)
+		// TODO @科举:参考 pages/pay/result.vue 页面的 subscribeMessage 方法,写订阅逻辑。
+		sheep.$router.go(e)
+		return;
+		let page = e;
+		// #ifndef MP
+		uni.navigateTo({
+			url: page
+		});
+		// #endif
+		// #ifdef MP
+		uni.showLoading({
+			title: '正在加载',
+		})
+		openBargainSubscribe().then(res => {
+			uni.hideLoading();
+
+		}).catch((err) => {
+			uni.hideLoading();
+		});
+		// #endif
+	}
+
+	onLoad(function() {
+		getBargainHeader();
+		getBargainList();
+	})
+
+	onReachBottom(() => {
+		getBargainList();
+	});
+</script>
+
+<style lang='scss' scoped>
+	page,
+	.page-app {
+		background-color: #e93323 !important;
+	}
+
+	.font-color {
+		color: red;
+	}
+
+	.mr9 {
+		margin-right: 9rpx;
+	}
+
+	.swipers {
+		height: 100%;
+		width: 76%;
+		margin: auto;
+		overflow: hidden;
+		font-size: 22rpx;
+		color: #fff;
+
+		image {
+			width: 24rpx;
+			height: 24rpx;
+			border-radius: 50%;
+			overflow: hidden;
+		}
+
+		swiper {
+			height: 100%;
+			width: 100%;
+			overflow: hidden;
+		}
+
+		.line1 {
+			width: 195rpx;
+		}
+	}
+
+	.bargain-list .icon-xiangzuo {
+		font-size: 40rpx;
+		color: #fff;
+		position: fixed;
+		left: 30rpx;
+		z-index: 99;
+		transform: translateY(-20%);
+		height: 100%
+	}
+
+	.bargain-list .header {
+		/* TODO 芋艿:此处原来采用base64 但是过长编辑到小程序卡死 目前采用网络地址 需解决 */
+		background-image: url('https://huizhizao-1314830814.cos.ap-shanghai.myqcloud.com/huizhizao-1314830814/bdc8a9210710b83bcd88a14703f440fc7091792706b5cb71b54361488a547298.png');
+		babackground-repeat: no-repeat;
+		background-size: 100% 100%;
+		width: 750rpx;
+		height: 420rpx;
+
+		.acea-row {
+			height: 50rpx;
+			line-height: 50rpx;
+			left: 50rpx;
+
+			.nickName {
+				width: 65rpx;
+				overflow: hidden;
+				white-space: nowrap;
+			}
+		}
+
+		.pic {
+			width: 478rpx;
+			height: 50rpx;
+			margin: 0 auto;
+			/* TODO 芋艿:此处原来是本地地址小程序不支持,需改为线上 */
+			background-image: url('https://huizhizao-1314830814.cos.ap-shanghai.myqcloud.com/huizhizao-1314830814/d111ac53e1390618f22fcc03e415bcd584b3f409ae52421aef95c2ab9b02aa30.png');
+			babackground-repeat: no-repeat;
+			background-size: 100% 100%;
+		}
+
+		.tit {
+			color: #FFFFFF;
+			font-size: 24rpx;
+			font-weight: 400;
+			text-align: center;
+			margin-top: 304rpx;
+		}
+	}
+
+	.bargain-list .list {
+		padding: 0 30rpx;
+	}
+
+	.bargain-list .list .item {
+		position: relative;
+		height: 250rpx;
+		background-color: #fff;
+		border-radius: 14rpx;
+		margin-bottom: 20rpx;
+		padding: 30rpx 25rpx;
+	}
+
+	.bargain-list .list .item .pictrue {
+		width: 190rpx;
+		height: 190rpx;
+	}
+
+	.bargain-list .list .item .pictrue image {
+		width: 100%;
+		height: 100%;
+		border-radius: 14rpx;
+	}
+
+	.bargain-list .list .item .text {
+		width: 432rpx;
+		font-size: 28rpx;
+		color: #333333;
+
+		.txt {
+			font-size: 22rpx;
+			margin-left: 4rpx;
+			color: #666666;
+			line-height: 36rpx;
+		}
+	}
+
+	.bargain-list .list .item .text .name {
+		width: 100%;
+		height: 68rpx;
+		line-height: 36rpx;
+		font-size: 28rpx;
+		margin-bottom: 26rpx;
+	}
+
+	.bargain-list .list .item .text .num {
+		font-size: 26rpx;
+		color: #999;
+	}
+
+	.bargain-list .list .item .text .num .iconfont {
+		font-size: 35rpx;
+		margin-right: 7rpx;
+	}
+
+	.bargain-list .list .item .text .money {
+		font-size: 24rpx;
+		font-weight: bold;
+	}
+
+	.bargain-list .list .item .text .money .price {
+		font-size: 38rpx;
+	}
+
+	.bargain-list .list .item .cutBnt {
+		position: absolute;
+		width: 162rpx;
+		height: 52rpx;
+		border-radius: 50rpx;
+		font-size: 24rpx;
+		color: #fff;
+		text-align: center;
+		line-height: 52rpx;
+		right: 24rpx;
+		bottom: 30rpx;
+		background: linear-gradient(90deg, #FF7931 0%, #E93323 100%);
+	}
+
+	.bargain-list .list .item .cutBnt .iconfont {
+		margin-right: 8rpx;
+		font-size: 30rpx;
+	}
+
+	.bargain-list .list .load {
+		font-size: 24rpx;
+		height: 85rpx;
+		text-align: center;
+		line-height: 85rpx;
+	}
+</style>

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

@@ -0,0 +1,508 @@
+<!-- 拼团订单的详情 -->
+<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>
+
+      <!-- TODO 芋艿:这里暂时没接入 -->
+      <view v-if="state.data.goods">
+        <s-select-groupon-sku
+          :show="state.showSelectSku"
+          :goodsInfo="state.data.goods"
+          :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 { useDurationTime } from '@/sheep/hooks/useGoods';
+  import { showShareModal } from '@/sheep/hooks/useModal';
+  import { isEmpty } from 'lodash';
+  import CombinationApi from "@/sheep/api/promotion/combination";
+
+  const headerBg = sheep.$url.css('/static/img/shop/user/withdraw_bg.png');
+  const statusBarHeight = sheep.$platform.device.statusBarHeight * 2;
+  const state = reactive({
+    data: {}, // 拼团详情
+    loading: true,
+    grouponAction: 'create',
+    showSelectSku: false,
+    grouponNum: 0,
+    number: 0,
+    activity: {},
+    combinationHeadId: null, // 拼团团长编号
+  });
+
+  // todo 芋艿:分享要再接下
+  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.id,
+        },
+      },
+      {
+        type: 'groupon', // 邀请拼团海报
+        title: state.data.headRecord.spuName, // 商品标题
+        image: sheep.$url.cdn(state.data.headRecord.picUrl), // 商品主图
+        price: state.data.goods?.price, // 商品价格
+        original_price: state.data.goods?.original_price, // 商品原价
+      },
+    );
+  });
+
+  // 订单详情
+  function onDetail(orderId) {
+    sheep.$router.go('/pages/order/detail', {
+      id: orderId,
+    });
+  }
+
+  // 去开团 TODO 芋艿:这里没接入
+  function onCreateGroupon() {
+    state.grouponAction = 'create';
+    state.grouponId = 0;
+    state.showSelectSku = true;
+  }
+
+  // 规格变更 TODO 芋艿:这里没接入
+  function onSkuChange(e) {
+    state.selectedSkuPrice = e;
+  }
+
+  // 立即参团 TODO 芋艿:这里没接入
+  function onJoinGroupon() {
+    state.grouponAction = 'join';
+    state.grouponId = state.data.activityId;
+    state.combinationHeadId = state.data.id;
+    state.grouponNum = state.data.num;
+    state.showSelectSku = true;
+  }
+
+  // 立即购买 TODO 芋艿:这里没接入
+  function onBuy(sku) {
+    sheep.$router.go('/pages/order/confirm', {
+      data: JSON.stringify({
+        order_type: 'goods',
+        combinationActivityId: state.data.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;
+    } 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';
+  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>

+ 206 - 0
pages/activity/index.vue

@@ -0,0 +1,206 @@
+<!-- 指定满减送的活动列表 -->
+<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">
+            {{ formatRewardActivityRule(state.activityInfo, item) }}
+          </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';
+  import RewardActivityApi from '@/sheep/api/promotion/rewardActivity';
+  import { formatRewardActivityRule } from '@/sheep/hooks/useGoods';
+  import SpuApi from '@/sheep/api/product/spu';
+
+  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;
+    }
+    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>

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

@@ -0,0 +1,385 @@
+<!-- 秒杀活动列表 -->
+<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)">
+          <!-- 活动起始时间 -->
+          <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"
+            @click="sheep.$router.go('/pages/goods/seckill', { id: activity.id })"
+          >
+            <!-- 抢购进度 -->
+            <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 }]">
+                <span v-if="activeTimeConfig?.status === TimeStatusEnum.WAIT_START">未开始</span>
+                <span v-else-if="activeTimeConfig?.status === TimeStatusEnum.STARTED">马上抢</span>
+                <span v-else>已结束</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')
+    // 判断时间段的状态
+    data.forEach((config, index) => {
+      const startTime = dayjs(`${today} ${config.startTime}`)
+      const endTime = dayjs(`${today} ${config.endTime}`)
+      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);
+    // 滚动到进行中的时间段
+    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) => {
+    activeTimeIndex.value = index
+
+    // 查询活动列表
+    activityPageParams.pageNo = 1
+    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({
+    id: 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>

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 401 - 0
pages/app/sign.vue


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

@@ -0,0 +1,63 @@
+<template>
+  <view class="goods ss-flex">
+    <image class="image" :src="sheep.$url.cdn(goodsData.image)" mode="aspectFill"> </image>
+    <view class="ss-flex-1">
+      <view class="title ss-line-2">
+        {{ goodsData.title }}
+      </view>
+      <view v-if="goodsData.subtitle" class="subtitle ss-line-1">
+        {{ goodsData.subtitle }}
+      </view>
+      <view class="price ss-m-t-8">
+        ¥{{ isArray(goodsData.price) ? goodsData.price[0] : goodsData.price }}
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup>
+  import sheep from '@/sheep';
+  import { isArray } from 'lodash';
+
+  const props = defineProps({
+    goodsData: {
+      type: Object,
+      default: {},
+    },
+  });
+</script>
+
+<style lang="scss" scoped>
+  .goods {
+    background: #fff;
+    padding: 20rpx;
+    border-radius: 12rpx;
+
+    .image {
+      width: 116rpx;
+      height: 116rpx;
+      flex-shrink: 0;
+      margin-right: 20rpx;
+    }
+
+    .title {
+      height: 64rpx;
+      line-height: 32rpx;
+      font-size: 26rpx;
+      font-weight: 500;
+      color: #333;
+    }
+
+    .subtitle {
+      font-size: 24rpx;
+      font-weight: 400;
+      color: #999;
+    }
+
+    .price {
+      font-size: 26rpx;
+      font-weight: 500;
+      color: #ff3000;
+    }
+  }
+</style>

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

@@ -0,0 +1,122 @@
+<template>
+  <view class="order">
+    <view class="top ss-flex ss-row-between">
+      <span>{{ orderData.order_sn }}</span>
+      <span>{{ orderData.create_time.split(' ')[1] }}</span>
+    </view>
+    <template v-if="from != 'msg'">
+      <view class="bottom ss-flex" v-for="item in orderData.items" :key="item">
+        <image class="image" :src="sheep.$url.cdn(item.goods_image)" mode="aspectFill"> </image>
+        <view class="ss-flex-1">
+          <view class="title ss-line-2">
+            {{ item.goods_title }}
+          </view>
+          <view v-if="item.goods_num" class="num ss-m-b-10"> 数量:{{ item.goods_num }} </view>
+          <view class="ss-flex ss-row-between ss-m-t-8">
+            <span class="price">¥{{ item.goods_price }}</span>
+            <span class="status">{{ orderData.status_text }}</span>
+          </view>
+        </view>
+      </view>
+    </template>
+    <template v-else>
+      <view class="bottom ss-flex" v-for="item in [orderData.items[0]]" :key="item">
+        <image class="image" :src="sheep.$url.cdn(item.goods_image)" mode="aspectFill"> </image>
+        <view class="ss-flex-1">
+          <view class="title title-1 ss-line-1">
+            {{ item.goods_title }}
+          </view>
+          <view class="order-total ss-flex ss-row-between ss-m-t-8">
+            <span>共{{ orderData.items.length }}件商品</span>
+            <span>合计 ¥{{ orderData.pay_fee }}</span>
+          </view>
+          <view class="ss-flex ss-row-right ss-m-t-8">
+            <span class="status">{{ orderData.status_text }}</span>
+          </view>
+        </view>
+      </view>
+    </template>
+  </view>
+</template>
+
+<script setup>
+  import sheep from '@/sheep';
+
+  const props = defineProps({
+    from: String,
+    orderData: {
+      type: Object,
+      default: {},
+    },
+  });
+</script>
+
+<style lang="scss" scoped>
+  .order {
+    background: #fff;
+    padding: 20rpx;
+    border-radius: 12rpx;
+
+    .top {
+      line-height: 40rpx;
+      font-size: 24rpx;
+      font-weight: 400;
+      color: #999;
+      border-bottom: 1px solid rgba(223, 223, 223, 0.5);
+      margin-bottom: 20rpx;
+    }
+
+    .bottom {
+      margin-bottom: 20rpx;
+
+      &:last-of-type {
+        margin-bottom: 0;
+      }
+
+      .image {
+        flex-shrink: 0;
+        width: 116rpx;
+        height: 116rpx;
+        margin-right: 20rpx;
+      }
+
+      .title {
+        height: 64rpx;
+        line-height: 32rpx;
+        font-size: 26rpx;
+        font-weight: 500;
+        color: #333;
+
+        &.title-1 {
+          height: 32rpx;
+          width: 300rpx;
+        }
+      }
+
+      .num {
+        font-size: 24rpx;
+        font-weight: 400;
+        color: #999;
+      }
+
+      .price {
+        font-size: 26rpx;
+        font-weight: 500;
+        color: #ff3000;
+      }
+
+      .status {
+        font-size: 24rpx;
+        font-weight: 500;
+        color: var(--ui-BG-Main);
+      }
+
+      .order-total {
+        line-height: 28rpx;
+        font-size: 24rpx;
+        font-weight: 400;
+        color: #999;
+      }
+    }
+  }
+</style>

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

@@ -0,0 +1,152 @@
+<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"
+          @tap="emits('select', { type: mode, data: item })"
+        >
+          <template v-if="mode == 'goods'">
+            <GoodsItem :goodsData="item.goods" />
+          </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 sheep from '@/sheep';
+  import _ from 'lodash';
+  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.data);
+    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>

+ 58 - 0
pages/chat/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);
+});

+ 870 - 0
pages/chat/index.vue

@@ -0,0 +1,870 @@
+<template>
+  <s-layout class="chat-wrap" title="客服" navbar="inner">
+    <div class="status">
+      {{ socketState.isConnect ? customerServiceInfo.title : '网络已断开,请检查网络后刷新重试' }}
+    </div>
+    <div class="page-bg" :style="{ height: sys_navBar + 'px' }"></div>
+    <view class="chat-box" :style="{ height: pageHeight + 'px' }">
+      <scroll-view
+        :style="{ height: pageHeight + 'px' }"
+        scroll-y="true"
+        :scroll-with-animation="false"
+        :enable-back-to-top="true"
+        :scroll-into-view="chat.scrollInto"
+      >
+        <button
+          class="loadmore-btn ss-reset-button"
+          v-if="
+            chatList.length &&
+            chatHistoryPagination.lastPage > 1 &&
+            loadingMap[chatHistoryPagination.loadStatus].title
+          "
+          @click="onLoadMore"
+        >
+          {{ loadingMap[chatHistoryPagination.loadStatus].title }}
+          <i
+            class="loadmore-icon sa-m-l-6"
+            :class="loadingMap[chatHistoryPagination.loadStatus].icon"
+          ></i>
+        </button>
+        <view class="message-item ss-flex-col" v-for="(item, index) in chatList" :key="index">
+          <view class="ss-flex ss-row-center ss-col-center">
+            <!-- 日期 -->
+            <view v-if="item.from !== 'system' && showTime(item, index)" class="date-message">
+              {{ formatTime(item.date) }}
+            </view>
+            <!-- 系统消息 -->
+            <view v-if="item.from === 'system'" class="system-message">
+              {{ item.content.text }}
+            </view>
+          </view>
+          <!-- 常见问题 -->
+          <view v-if="item.mode === 'template' && item.content.list.length" class="template-wrap">
+            <view class="title">猜你想问</view>
+            <view
+              class="item"
+              v-for="(item, index) in item.content.list"
+              :key="index"
+              @click="onTemplateList(item)"
+            >
+              * {{ item.title }}
+            </view>
+          </view>
+
+          <view
+            v-if="
+              (item.from === 'customer_service' && item.mode !== 'template') ||
+              item.from === 'customer'
+            "
+            class="ss-flex ss-col-top"
+            :class="[
+              item.from === 'customer_service'
+                ? `ss-row-left`
+                : item.from === 'customer'
+                ? `ss-row-right`
+                : '',
+            ]"
+          >
+            <!-- 客服头像 -->
+            <image
+              v-show="item.from === 'customer_service'"
+              class="chat-avatar ss-m-r-24"
+              :src="
+                sheep.$url.cdn(item?.sender?.avatar) ||
+                sheep.$url.static('/static/img/shop/chat/default.png')
+              "
+              mode="aspectFill"
+            ></image>
+
+            <!-- 发送状态 -->
+            <span
+              v-if="
+                item.from === 'customer' &&
+                index == chatData.chatList.length - 1 &&
+                chatData.isSendSucces !== 0
+              "
+              class="send-status"
+            >
+              <image
+                v-if="chatData.isSendSucces == -1"
+                class="loading"
+                :src="sheep.$url.static('/static/img/shop/chat/loading.png')"
+                mode="aspectFill"
+              ></image>
+              <!-- <image
+                v-if="chatData.isSendSucces == 1"
+                class="warning"
+                :src="sheep.$url.static('/static/img/shop/chat/warning.png')"
+                mode="aspectFill"
+                @click="onAgainSendMessage(item)"
+              ></image> -->
+            </span>
+
+            <!-- 内容 -->
+            <template v-if="item.mode === 'text'">
+              <view class="message-box" :class="[item.from]">
+                <div
+                  class="message-text ss-flex ss-flex-wrap"
+                  @click="onRichtext"
+                  v-html="replaceEmoji(item.content.text)"
+                ></div>
+              </view>
+            </template>
+            <template v-if="item.mode === 'image'">
+              <view class="message-box" :class="[item.from]" :style="{ width: '200rpx' }">
+                <su-image
+                  class="message-img"
+                  isPreview
+                  :previewList="[sheep.$url.cdn(item.content.url)]"
+                  :current="0"
+                  :src="sheep.$url.cdn(item.content.url)"
+                  :height="200"
+                  :width="200"
+                  mode="aspectFill"
+                ></su-image>
+              </view>
+            </template>
+            <template v-if="item.mode === 'goods'">
+              <GoodsItem
+                :goodsData="item.content.item"
+                @tap="
+                  sheep.$router.go('/pages/goods/index', {
+                    id: item.content.item.id,
+                  })
+                "
+              />
+            </template>
+            <template v-if="item.mode === 'order'">
+              <OrderItem
+                from="msg"
+                :orderData="item.content.item"
+                @tap="
+                  sheep.$router.go('/pages/order/detail', {
+                    id: item.content.item.id,
+                  })
+                "
+              />
+            </template>
+            <!-- user头像 -->
+            <image
+              v-show="item.from === 'customer'"
+              class="chat-avatar ss-m-l-24"
+              :src="sheep.$url.cdn(customerUserInfo.avatar)"
+              mode="aspectFill"
+            >
+            </image>
+          </view>
+        </view>
+        <view id="scrollBottom"></view>
+      </scroll-view>
+    </view>
+    <su-fixed bottom>
+      <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="chat.msg"
+            placeholder="请输入你要咨询的问题"
+          ></uni-easyinput>
+        </view>
+        <text class="sicon-basic bq" @tap.stop="onTools('emoji')"></text>
+        <text
+          v-if="!chat.msg"
+          class="sicon-edit"
+          :class="{ 'is-active': chat.toolsMode == 'tools' }"
+          @tap.stop="onTools('tools')"
+        ></text>
+        <button v-if="chat.msg" class="ss-reset-button send-btn" @tap="onSendMessage">
+          发送
+        </button>
+      </view>
+    </su-fixed>
+    <su-popup
+      :show="chat.showTools"
+      @close="
+        chat.showTools = false;
+        chat.toolsMode = '';
+      "
+    >
+      <view class="ss-modal-box ss-flex-col">
+        <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="chat.msg"
+              placeholder="请输入你要咨询的问题"
+            ></uni-easyinput>
+          </view>
+          <text class="sicon-basic bq" @tap.stop="onTools('emoji')"></text>
+          <text></text>
+          <text
+            v-if="!chat.msg"
+            class="sicon-edit"
+            :class="{ 'is-active': chat.toolsMode == 'tools' }"
+            @tap.stop="onTools('tools')"
+          ></text>
+          <button v-if="chat.msg" class="ss-reset-button send-btn" @tap="onSendMessage">
+            发送
+          </button>
+        </view>
+        <view class="content ss-flex ss-flex-1">
+          <template v-if="chat.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">
+                  <template v-for="item in emoji" :key="item">
+                    <image
+                      class="emoji-img"
+                      :src="sheep.$url.cdn(`/static/img/chat/emoji/${item.file}`)"
+                      @tap="onEmoji(item)"
+                    >
+                    </image>
+                  </template>
+                </view>
+              </swiper-item>
+            </swiper>
+          </template>
+          <template v-else>
+            <view class="image">
+              <s-uploader
+                file-mediatype="image"
+                :imageStyles="{ width: 50, height: 50, border: false }"
+                @select="onSelect({ 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>
+
+    <SelectPopup
+      :mode="chat.selectMode"
+      :show="chat.showSelect"
+      @select="onSelect"
+      @close="chat.showSelect = false"
+    />
+  </s-layout>
+</template>
+
+<script setup>
+  import sheep from '@/sheep';
+  import { computed, reactive, toRefs } from 'vue';
+  import { onLoad } from '@dcloudio/uni-app';
+  import { emojiList, emojiPage } from './emoji.js';
+  import SelectPopup from './components/select-popup.vue';
+  import GoodsItem from './components/goods.vue';
+  import OrderItem from './components/order.vue';
+  import { useChatWebSocket } from './socket';
+
+  const {
+    socketInit,
+    state: chatData,
+    socketSendMsg,
+    formatChatInput,
+    socketHistoryList,
+    onDrop,
+    onPaste,
+    getFocus,
+    // upload,
+    getUserToken,
+    // socketTest,
+    showTime,
+    formatTime,
+  } = useChatWebSocket();
+  const chatList = toRefs(chatData).chatList;
+  const customerServiceInfo = toRefs(chatData).customerServerInfo;
+  const chatHistoryPagination = toRefs(chatData).chatHistoryPagination;
+  const customerUserInfo = toRefs(chatData).customerUserInfo;
+  const socketState = toRefs(chatData).socketState;
+
+  const sys_navBar = sheep.$platform.navbar;
+  const chatConfig = computed(() => sheep.$store('app').chat);
+
+  const { screenHeight, safeAreaInsets, safeArea, screenWidth } = sheep.$platform.device;
+  const pageHeight = safeArea.height - 44 - 35 - 50;
+
+  const chatStatus = {
+    online: {
+      text: '在线',
+      colorVariate: '#46c55f',
+    },
+    offline: {
+      text: '离线',
+      colorVariate: '#b5b5b5',
+    },
+    busy: {
+      text: '忙碌',
+      colorVariate: '#ff0e1b',
+    },
+  };
+
+  // 加载更多
+  const loadingMap = {
+    loadmore: {
+      title: '查看更多',
+      icon: 'el-icon-d-arrow-left',
+    },
+    nomore: {
+      title: '没有更多了',
+      icon: '',
+    },
+    loading: {
+      title: '加载中... ',
+      icon: 'el-icon-loading',
+    },
+  };
+  const onLoadMore = () => {
+    chatHistoryPagination.value.page < chatHistoryPagination.value.lastPage && socketHistoryList();
+  };
+
+  const chat = reactive({
+    msg: '',
+    scrollInto: '',
+
+    showTools: false,
+    toolsMode: '',
+
+    showSelect: false,
+    selectMode: '',
+    chatStyle: {
+      mode: 'inner',
+      color: '#F8270F',
+      type: 'color',
+      alwaysShow: 1,
+      src: '',
+      list: {},
+    },
+  });
+
+  // 点击工具栏开关
+  function onTools(mode) {
+    if (!socketState.value.isConnect) {
+      sheep.$helper.toast(socketState.value.tip || '您已掉线!请返回重试');
+      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 { path, fullurl } = await sheep.$api.app.upload(data.tempFiles[0].path, 'default');
+        msg = {
+          from: 'customer',
+          mode: 'image',
+          date: new Date().getTime(),
+          content: {
+            url: fullurl,
+            path: path,
+          },
+        };
+        break;
+      case 'goods':
+        msg = {
+          from: 'customer',
+          mode: 'goods',
+          date: new Date().getTime(),
+          content: {
+            item: {
+              id: data.goods.id,
+              title: data.goods.title,
+              image: data.goods.image,
+              price: data.goods.price,
+              stock: data.goods.stock,
+            },
+          },
+        };
+        break;
+      case 'order':
+        msg = {
+          from: 'customer',
+          mode: 'order',
+          date: new Date().getTime(),
+          content: {
+            item: {
+              id: data.id,
+              order_sn: data.order_sn,
+              create_time: data.create_time,
+              pay_fee: data.pay_fee,
+              items: data.items.filter((item) => ({
+                goods_id: item.goods_id,
+                goods_title: item.goods_title,
+                goods_image: item.goods_image,
+                goods_price: item.goods_price,
+              })),
+              status_text: data.status_text,
+            },
+          },
+        };
+        break;
+    }
+    if (msg) {
+      socketSendMsg(msg, () => {
+        scrollBottom();
+      });
+      // scrollBottom();
+      chat.showTools = false;
+      chat.showSelect = false;
+      chat.selectMode = '';
+    }
+  }
+
+  function onAgainSendMessage(item) {
+    if (!socketState.value.isConnect) {
+      sheep.$helper.toast(socketState.value.tip || '您已掉线!请返回重试');
+      return;
+    }
+    if (!item) return;
+    const data = {
+      from: 'customer',
+      mode: 'text',
+      date: new Date().getTime(),
+      content: item.content,
+    };
+    socketSendMsg(data, () => {
+      scrollBottom();
+    });
+  }
+
+  function onSendMessage() {
+    if (!socketState.value.isConnect) {
+      sheep.$helper.toast(socketState.value.tip || '您已掉线!请返回重试');
+      return;
+    }
+    if (!chat.msg) return;
+    const data = {
+      from: 'customer',
+      mode: 'text',
+      date: new Date().getTime(),
+      content: {
+        text: chat.msg,
+      },
+    };
+    socketSendMsg(data, () => {
+      scrollBottom();
+    });
+    chat.showTools = false;
+    // scrollBottom();
+    setTimeout(() => {
+      chat.msg = '';
+    }, 100);
+  }
+
+  // 点击猜你想问
+  function onTemplateList(e) {
+    if (!socketState.value.isConnect) {
+      sheep.$helper.toast(socketState.value.tip || '您已掉线!请返回重试');
+      return;
+    }
+    const data = {
+      from: 'customer',
+      mode: 'text',
+      date: new Date().getTime(),
+      content: {
+        text: e.title,
+      },
+      customData: {
+        question_id: e.id,
+      },
+    };
+    socketSendMsg(data, () => {
+      scrollBottom();
+    });
+    // scrollBottom();
+  }
+
+  function onEmoji(item) {
+    chat.msg += item.name;
+  }
+
+  function selEmojiFile(name) {
+    for (let index in emojiList) {
+      if (emojiList[index].name === name) {
+        return emojiList[index].file;
+      }
+    }
+    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 scrollBottom() {
+    let timeout = null;
+    chat.scrollInto = '';
+    clearTimeout(timeout);
+    timeout = setTimeout(() => {
+      chat.scrollInto = 'scrollBottom';
+    }, 100);
+  }
+
+  onLoad(async () => {
+    const { error } = await getUserToken();
+    if (error === 0) {
+      socketInit(chatConfig.value, () => {
+        scrollBottom();
+      });
+    } else {
+      socketState.value.isConnect = false;
+    }
+  });
+</script>
+
+<style lang="scss" scoped>
+  .page-bg {
+    width: 100%;
+    position: absolute;
+    top: 0;
+    left: 0;
+    background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
+    background-size: 750rpx 100%;
+    z-index: 1;
+  }
+
+  .chat-wrap {
+    // :deep() {
+    //   .ui-navbar-box {
+    //     background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
+    //   }
+    // }
+
+    .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);
+    }
+
+    .chat-box {
+      padding: 0 20rpx 0;
+
+      .loadmore-btn {
+        width: 98%;
+        height: 40px;
+        font-size: 12px;
+        color: #8c8c8c;
+
+        .loadmore-icon {
+          transform: rotate(90deg);
+        }
+      }
+
+      .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;
+        // max-width: 500rpx;
+        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));
+
+        &.customer_service {
+          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;
+      }
+
+      #scrollBottom {
+        height: 120rpx;
+      }
+    }
+
+    .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;
+      }
+    }
+  }
+
+  .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>
+<style>
+  .chat-img {
+    width: 24px;
+    height: 24px;
+    margin: 0 3px;
+  }
+  .full-img {
+    object-fit: cover;
+    width: 100px;
+    height: 100px;
+    border-radius: 6px;
+  }
+</style>

+ 821 - 0
pages/chat/socket.js

@@ -0,0 +1,821 @@
+import { reactive, ref, unref } from 'vue';
+import sheep from '@/sheep';
+// import chat from '@/sheep/api/chat';
+import dayjs from 'dayjs';
+import io from '@hyoga/uni-socket.io';
+
+export function useChatWebSocket(socketConfig) {
+  let SocketIo = null;
+
+  // chat状态数据
+  const state = reactive({
+    chatDotNum: 0, //总状态红点
+    chatList: [], //会话信息
+    customerUserInfo: {}, //用户信息
+    customerServerInfo: {
+      //客服信息
+      title: '连接中...',
+      state: 'connecting',
+      avatar: null,
+      nickname: '',
+    },
+    socketState: {
+      isConnect: true, //是否连接成功
+      isConnecting: false, //重连中,不允许新的socket开启。
+      tip: '',
+    },
+    chatHistoryPagination: {
+      page: 0, //当前页
+      list_rows: 10, //每页条数
+      last_id: 0, //最后条ID
+      lastPage: 0, //总共多少页
+      loadStatus: 'loadmore', //loadmore-加载前的状态,loading-加载中的状态,nomore-没有更多的状态
+    },
+    templateChatList: [], //猜你想问
+
+    chatConfig: {}, // 配置信息
+
+    isSendSucces: -1, // 是否发送成功 -1=发送中|0=发送成功|1发送失败
+  });
+
+  /**
+   * 连接初始化
+   * @param {Object} config  - 配置信息
+   * @param {Function} callBack -回调函数,有新消息接入,保持底部
+   */
+  const socketInit = (config, callBack) => {
+    state.chatConfig = config;
+    if (SocketIo && SocketIo.connected) return; // 如果socket已经连接,返回false
+    if (state.socketState.isConnecting) return; // 重连中,返回false
+
+    // 启动初始化
+    SocketIo = io(config.chat_domain, {
+      reconnection: true, // 默认 true    是否断线重连
+      reconnectionAttempts: 5, // 默认无限次   断线尝试次数
+      reconnectionDelay: 1000, // 默认 1000,进行下一次重连的间隔。
+      reconnectionDelayMax: 5000, // 默认 5000, 重新连接等待的最长时间 默认 5000
+      randomizationFactor: 0.5, // 默认 0.5 [0-1],随机重连延迟时间
+      timeout: 20000, // 默认 20s
+      transports: ['websocket', 'polling'], // websocket | polling,
+      ...config,
+    });
+
+    // 监听连接
+    SocketIo.on('connect', async (res) => {
+      socketReset(callBack);
+      // socket连接
+      // 用户登录
+      // 顾客登录
+      console.log('socket:connect');
+    });
+    // 监听消息
+    SocketIo.on('message', (res) => {
+      if (res.error === 0) {
+        const { message, sender } = res.data;
+        state.chatList.push(formatMessage(res.data.message));
+
+        // 告诉父级页面
+        // window.parent.postMessage({
+        // 	chatDotNum: ++state.chatDotNum
+        // })
+        callBack && callBack();
+      }
+    });
+    // 监听客服接入成功
+    SocketIo.on('customer_service_access', (res) => {
+      if (res.error === 0) {
+        editCustomerServerInfo({
+          title: res.data.customer_service.name,
+          state: 'online',
+          avatar: res.data.customer_service.avatar,
+        });
+        state.chatList.push(formatMessage(res.data.message));
+        // callBack && callBack()
+      }
+    });
+    // 监听排队等待
+    SocketIo.on('waiting_queue', (res) => {
+      if (res.error === 0) {
+        editCustomerServerInfo({
+          title: res.data.title,
+          state: 'waiting',
+          avatar: '',
+        });
+        // callBack && callBack()
+      }
+    });
+    // 监听没有客服在线
+    SocketIo.on('no_customer_service', (res) => {
+      if (res.error === 0) {
+        editCustomerServerInfo({
+          title: '暂无客服在线...',
+          state: 'waiting',
+          avatar: '',
+        });
+      }
+      state.chatList.push(formatMessage(res.data.message));
+      // callBack && callBack()
+    });
+    // 监听客服上线
+    SocketIo.on('customer_service_online', (res) => {
+      if (res.error === 0) {
+        editCustomerServerInfo({
+          title: res.data.customer_service.name,
+          state: 'online',
+          avatar: res.data.customer_service.avatar,
+        });
+      }
+    });
+    // 监听客服下线
+    SocketIo.on('customer_service_offline', (res) => {
+      if (res.error === 0) {
+        editCustomerServerInfo({
+          title: res.data.customer_service.name,
+          state: 'offline',
+          avatar: res.data.customer_service.avatar,
+        });
+      }
+    });
+    // 监听客服忙碌
+    SocketIo.on('customer_service_busy', (res) => {
+      if (res.error === 0) {
+        editCustomerServerInfo({
+          title: res.data.customer_service.name,
+          state: 'busy',
+          avatar: res.data.customer_service.avatar,
+        });
+      }
+    });
+    // 监听客服断开链接
+    SocketIo.on('customer_service_break', (res) => {
+      if (res.error === 0) {
+        editCustomerServerInfo({
+          title: '客服服务结束',
+          state: 'offline',
+          avatar: '',
+        });
+        state.socketState.isConnect = false;
+        state.socketState.tip = '当前服务已结束';
+      }
+      state.chatList.push(formatMessage(res.data.message));
+      // callBack && callBack()
+    });
+    // 监听自定义错误 custom_error
+    SocketIo.on('custom_error', (error) => {
+      editCustomerServerInfo({
+        title: error.msg,
+        state: 'offline',
+        avatar: '',
+      });
+      console.log('custom_error:', error);
+    });
+    // 监听错误 error
+    SocketIo.on('error', (error) => {
+      console.log('error:', error);
+    });
+    // 重连失败 connect_error
+    SocketIo.on('connect_error', (error) => {
+      console.log('connect_error');
+    });
+    // 连接上,但无反应 connect_timeout
+    SocketIo.on('connect_timeout', (error) => {
+      console.log(error, 'connect_timeout');
+    });
+    // 服务进程销毁 disconnect
+    SocketIo.on('disconnect', (error) => {
+      console.log(error, 'disconnect');
+    });
+    // 服务重启重连上reconnect
+    SocketIo.on('reconnect', (error) => {
+      console.log(error, 'reconnect');
+    });
+    // 开始重连reconnect_attempt
+    SocketIo.on('reconnect_attempt', (error) => {
+      state.socketState.isConnect = false;
+      state.socketState.isConnecting = true;
+      editCustomerServerInfo({
+        title: `重连中,第${error}次尝试...`,
+        state: 'waiting',
+        avatar: '',
+      });
+      console.log(error, 'reconnect_attempt');
+    });
+    // 重新连接中reconnecting
+    SocketIo.on('reconnecting', (error) => {
+      console.log(error, 'reconnecting');
+    });
+    // 重新连接错误reconnect_error
+    SocketIo.on('reconnect_error', (error) => {
+      console.log('reconnect_error');
+    });
+    // 重新连接失败reconnect_failed
+    SocketIo.on('reconnect_failed', (error) => {
+      state.socketState.isConnecting = false;
+      editCustomerServerInfo({
+        title: `重连失败,请刷新重试~`,
+        state: 'waiting',
+        avatar: '',
+      });
+      console.log(error, 'reconnect_failed');
+
+      // setTimeout(() => {
+      state.isSendSucces = 1;
+      // }, 500)
+    });
+  };
+
+  // 重置socket
+  const socketReset = (callBack) => {
+    state.chatList = [];
+    state.chatHistoryList = [];
+    state.chatHistoryPagination = {
+      page: 0,
+      per_page: 10,
+      last_id: 0,
+      totalPage: 0,
+    };
+    socketConnection(callBack); // 连接
+  };
+
+  // 退出连接
+  const socketClose = () => {
+    SocketIo.emit('customer_logout', {}, (res) => {
+      console.log('socket:退出', res);
+    });
+  };
+
+  // 测试事件
+  const socketTest = () => {
+    SocketIo.emit('test', {}, (res) => {
+      console.log('test:test', res);
+    });
+  };
+
+  // 发送消息
+  const socketSendMsg = (data, sendMsgCallBack) => {
+    state.isSendSucces = -1;
+    state.chatList.push(data);
+    sendMsgCallBack && sendMsgCallBack();
+    SocketIo.emit(
+      'message',
+      {
+        message: formatInput(data),
+        ...data.customData,
+      },
+      (res) => {
+        // setTimeout(() => {
+        state.isSendSucces = res.error;
+        // }, 500)
+
+        // console.log(res, 'socket:send');
+        // sendMsgCallBack && sendMsgCallBack()
+      },
+    );
+  };
+
+  // 连接socket,存入sessionId
+  const socketConnection = (callBack) => {
+    SocketIo.emit(
+      'connection',
+      {
+        auth: 'user',
+        token: uni.getStorageSync('socketUserToken') || '',
+        session_id: uni.getStorageSync('socketSessionId') || '',
+      },
+      (res) => {
+        if (res.error === 0) {
+          socketCustomerLogin(callBack);
+          uni.setStorageSync('socketSessionId', res.data.session_id);
+          // uni.getStorageSync('socketUserToken') && socketLogin(uni.getStorageSync(
+          // 	'socketUserToken')) // 如果有用户token,绑定
+          state.customerUserInfo = res.data.chat_user;
+          state.socketState.isConnect = true;
+        } else {
+          editCustomerServerInfo({
+            title: `服务器异常!`,
+            state: 'waiting',
+            avatar: '',
+          });
+          state.socketState.isConnect = false;
+        }
+      },
+    );
+  };
+
+  // 用户id,获取token
+  const getUserToken = async (id) => {
+    const res = await chat.unifiedToken();
+    if (res.error === 0) {
+      uni.setStorageSync('socketUserToken', res.data.token);
+      // SocketIo && SocketIo.connected && socketLogin(res.data.token)
+    }
+    return res;
+  };
+
+  // 用户登录
+  const socketLogin = (token) => {
+    SocketIo.emit(
+      'login',
+      {
+        token: token,
+      },
+      (res) => {
+        console.log(res, 'socket:login');
+        state.customerUserInfo = res.data.chat_user;
+      },
+    );
+  };
+
+  // 顾客登录
+  const socketCustomerLogin = (callBack) => {
+    SocketIo.emit(
+      'customer_login',
+      {
+        room_id: state.chatConfig.room_id,
+      },
+      (res) => {
+        state.templateChatList = res.data.questions.length ? res.data.questions : [];
+        state.chatList.push({
+          from: 'customer_service', // 用户customer右 |  顾客customer_service左 | 系统system中间
+          mode: 'template', // goods,order,image,text,system
+          date: new Date().getTime(), //时间
+          content: {
+            //内容
+            list: state.templateChatList,
+          },
+        });
+        res.error === 0 && socketHistoryList(callBack);
+      },
+    );
+  };
+
+  // 获取历史消息
+  const socketHistoryList = (historyCallBack) => {
+    state.chatHistoryPagination.loadStatus = 'loading';
+    state.chatHistoryPagination.page += 1;
+    SocketIo.emit('messages', state.chatHistoryPagination, (res) => {
+      if (res.error === 0) {
+        state.chatHistoryPagination.total = res.data.messages.total;
+        state.chatHistoryPagination.lastPage = res.data.messages.last_page;
+        state.chatHistoryPagination.page = res.data.messages.current_page;
+        res.data.messages.data.forEach((item) => {
+          item.message_type && state.chatList.unshift(formatMessage(item));
+        });
+        state.chatHistoryPagination.loadStatus =
+          state.chatHistoryPagination.page < state.chatHistoryPagination.lastPage
+            ? 'loadmore'
+            : 'nomore';
+        if (state.chatHistoryPagination.last_id == 0) {
+          state.chatHistoryPagination.last_id = res.data.messages.data.length
+            ? res.data.messages.data[0].id
+            : 0;
+        }
+        state.chatHistoryPagination.page === 1 && historyCallBack && historyCallBack();
+      }
+
+      // 历史记录之后,猜你想问
+      // state.chatList.push({
+      // 	from: 'customer_service', // 用户customer右 |  顾客customer_service左 | 系统system中间
+      // 	mode: 'template', // goods,order,image,text,system
+      // 	date: new Date().getTime(), //时间
+      // 	content: { //内容
+      // 		list: state.templateChatList
+      // 	}
+      // })
+    });
+  };
+
+  // 修改客服信息
+  const editCustomerServerInfo = (data) => {
+    state.customerServerInfo = {
+      ...state.customerServerInfo,
+      ...data,
+    };
+  };
+
+  /**
+   * ================
+   * 工具函数 ↓
+   * ===============
+   */
+
+  /**
+   * 是否显示时间
+   * @param {*} item - 数据
+   * @param {*} index - 索引
+   */
+  const showTime = (item, index) => {
+    if (unref(state.chatList)[index + 1]) {
+      let dateString = dayjs(unref(state.chatList)[index + 1].date).fromNow();
+      if (dateString === dayjs(unref(item).date).fromNow()) {
+        return false;
+      } else {
+        dateString = dayjs(unref(item).date).fromNow();
+        return true;
+      }
+    }
+    return false;
+  };
+
+  /**
+   * 格式化时间
+   * @param {*} time - 时间戳
+   */
+  const formatTime = (time) => {
+    let diffTime = new Date().getTime() - time;
+    if (diffTime > 28 * 24 * 60 * 1000) {
+      return dayjs(time).format('MM/DD HH:mm');
+    }
+    if (diffTime > 360 * 28 * 24 * 60 * 1000) {
+      return dayjs(time).format('YYYY/MM/DD HH:mm');
+    }
+    return dayjs(time).fromNow();
+  };
+
+  /**
+   * 获取焦点
+   * @param {*} virtualNode - 节点信息 ref
+   */
+  const getFocus = (virtualNode) => {
+    if (window.getSelection) {
+      let chatInput = unref(virtualNode);
+      chatInput.focus();
+      let range = window.getSelection();
+      range.selectAllChildren(chatInput);
+      range.collapseToEnd();
+    } else if (document.selection) {
+      let range = document.selection.createRange();
+      range.moveToElementText(chatInput);
+      range.collapse(false);
+      range.select();
+    }
+  };
+
+  /**
+   * 文件上传
+   * @param {Blob} file -文件数据流
+   * @return {path,fullPath}
+   */
+
+  const upload = (name, file) => {
+    return new Promise((resolve, reject) => {
+      let data = new FormData();
+      data.append('file', file, name);
+      data.append('group', 'chat');
+      ajax({
+        url: '/upload',
+        method: 'post',
+        headers: {
+          'Content-Type': 'multipart/form-data',
+        },
+        data,
+        success: function (res) {
+          resolve(res);
+        },
+        error: function (err) {
+          reject(err);
+        },
+      });
+    });
+  };
+
+  /**
+   * 粘贴到输入框
+   * @param {*} e  - 粘贴内容
+   * @param {*} uploadHttp - 上传图片地址
+   */
+  const onPaste = async (e) => {
+    let paste = e.clipboardData || window.clipboardData;
+    let filesArr = Array.from(paste.files);
+    filesArr.forEach(async (child) => {
+      if (child && child.type.includes('image')) {
+        e.preventDefault(); //阻止默认
+        let file = child;
+        const img = await readImg(file);
+        const blob = await compressImg(img, file.type);
+        const { data } = await upload(file.name, blob);
+        let image = `<img class="full-url" src='${data.fullurl}'>`;
+        document.execCommand('insertHTML', false, image);
+      } else {
+        document.execCommand('insertHTML', false, paste.getData('text'));
+      }
+    });
+  };
+
+  /**
+   * 拖拽到输入框
+   * @param {*} e  - 粘贴内容
+   * @param {*} uploadHttp - 上传图片地址
+   */
+  const onDrop = async (e) => {
+    e.preventDefault(); //阻止默认
+    let filesArr = Array.from(e.dataTransfer.files);
+    filesArr.forEach(async (child) => {
+      if (child && child.type.includes('image')) {
+        let file = child;
+        const img = await readImg(file);
+        const blob = await compressImg(img, file.type);
+        const { data } = await upload(file.name, blob);
+        let image = `<img class="full-url" src='${data.fullurl}' >`;
+        document.execCommand('insertHTML', false, image);
+      } else {
+        ElMessage({
+          message: '禁止拖拽非图片资源',
+          type: 'warning',
+        });
+      }
+    });
+  };
+
+  /**
+   * 解析富文本输入框内容
+   * @param {*}  virtualNode -节点信息
+   * @param {Function} formatInputCallBack - cb 回调
+   */
+  const formatChatInput = (virtualNode, formatInputCallBack) => {
+    let res = '';
+    let elemArr = Array.from(virtualNode.childNodes);
+    elemArr.forEach((child, index) => {
+      if (child.nodeName === '#text') {
+        //如果为文本节点
+        res += child.nodeValue;
+        if (
+          //文本节点的后面是图片,并且不是emoji,分开发送。输入框中的图片和文本表情分开。
+          elemArr[index + 1] &&
+          elemArr[index + 1].nodeName === 'IMG' &&
+          elemArr[index + 1] &&
+          elemArr[index + 1].name !== 'emoji'
+        ) {
+          const data = {
+            from: 'customer',
+            mode: 'text',
+            date: new Date().getTime(),
+            content: {
+              text: filterXSS(res),
+            },
+          };
+          formatInputCallBack && formatInputCallBack(data);
+          res = '';
+        }
+      } else if (child.nodeName === 'BR') {
+        res += '<br/>';
+      } else if (child.nodeName === 'IMG') {
+        // 有emjio 和 一般图片
+        // 图片解析后直接发送,不跟文字表情一组
+        if (child.name !== 'emoji') {
+          let srcReg = /src=[\'\']?([^\'\']*)[\'\']?/i;
+          let src = child.outerHTML.match(srcReg);
+          const data = {
+            from: 'customer',
+            mode: 'image',
+            date: new Date().getTime(),
+            content: {
+              url: src[1],
+              path: src[1].replace(/http:\/\/[^\/]*/, ''),
+            },
+          };
+          formatInputCallBack && formatInputCallBack(data);
+        } else {
+          // 非表情图片跟文字一起发送
+          res += child.outerHTML;
+        }
+      } else if (child.nodeName === 'DIV') {
+        res += `<div style='width:200px; white-space: nowrap;'>${child.outerHTML}</div>`;
+      }
+    });
+    if (res) {
+      const data = {
+        from: 'customer',
+        mode: 'text',
+        date: new Date().getTime(),
+        content: {
+          text: filterXSS(res),
+        },
+      };
+      formatInputCallBack && formatInputCallBack(data);
+    }
+    unref(virtualNode).innerHTML = '';
+  };
+
+  /**
+   * 状态回调
+   * @param {*} res -接口返回数据
+   */
+  const callBackNotice = (res) => {
+    ElNotification({
+      title: 'socket',
+      message: res.msg,
+      showClose: true,
+      type: res.error === 0 ? 'success' : 'warning',
+      duration: 1200,
+    });
+  };
+
+  /**
+   * 格式化发送信息
+   * @param {Object} message
+   * @returns  obj - 消息对象
+   */
+  const formatInput = (message) => {
+    let obj = {};
+    switch (message.mode) {
+      case 'text':
+        obj = {
+          message_type: 'text',
+          message: message.content.text,
+        };
+        break;
+      case 'image':
+        obj = {
+          message_type: 'image',
+          message: message.content.path,
+        };
+        break;
+      case 'goods':
+        obj = {
+          message_type: 'goods',
+          message: message.content.item,
+        };
+        break;
+      case 'order':
+        obj = {
+          message_type: 'order',
+          message: message.content.item,
+        };
+        break;
+      default:
+        break;
+    }
+    return obj;
+  };
+  /**
+   * 格式化接收信息
+   * @param {*} message
+   * @returns obj - 消息对象
+   */
+  const formatMessage = (message) => {
+    let obj = {};
+    switch (message.message_type) {
+      case 'system':
+        obj = {
+          from: 'system', // 用户customer左 |  顾客customer_service右 | 系统system中间
+          mode: 'system', // goods,order,image,text,system
+          date: message.create_time * 1000, //时间
+          content: {
+            //内容
+            text: message.message,
+          },
+        };
+        break;
+      case 'text':
+        obj = {
+          from: message.sender_identify,
+          mode: message.message_type,
+          date: message.create_time * 1000, //时间
+          sender: message.sender,
+          content: {
+            text: message.message,
+            messageId: message.id,
+          },
+        };
+        break;
+      case 'image':
+        obj = {
+          from: message.sender_identify,
+          mode: message.message_type,
+          date: message.create_time * 1000, //时间
+          sender: message.sender,
+          content: {
+            url: sheep.$url.cdn(message.message),
+            messageId: message.id,
+          },
+        };
+        break;
+      case 'goods':
+        obj = {
+          from: message.sender_identify,
+          mode: message.message_type,
+          date: message.create_time * 1000, //时间
+          sender: message.sender,
+          content: {
+            item: message.message,
+            messageId: message.id,
+          },
+        };
+        break;
+      case 'order':
+        obj = {
+          from: message.sender_identify,
+          mode: message.message_type,
+          date: message.create_time * 1000, //时间
+          sender: message.sender,
+          content: {
+            item: message.message,
+            messageId: message.id,
+          },
+        };
+        break;
+      default:
+        break;
+    }
+    return obj;
+  };
+
+  /**
+   * file 转换为 img
+   * @param {*} file  - file 文件
+   * @returns  img   - img标签
+   */
+  const readImg = (file) => {
+    return new Promise((resolve, reject) => {
+      const img = new Image();
+      const reader = new FileReader();
+      reader.onload = function (e) {
+        img.src = e.target.result;
+      };
+      reader.onerror = function (e) {
+        reject(e);
+      };
+      reader.readAsDataURL(file);
+      img.onload = function () {
+        resolve(img);
+      };
+      img.onerror = function (e) {
+        reject(e);
+      };
+    });
+  };
+
+  /**
+   * 压缩图片
+   *@param img -被压缩的img对象
+   * @param type -压缩后转换的文件类型
+   * @param mx -触发压缩的图片最大宽度限制
+   * @param mh -触发压缩的图片最大高度限制
+   * @returns blob - 文件流
+   */
+  const compressImg = (img, type = 'image/jpeg', mx = 1000, mh = 1000, quality = 1) => {
+    return new Promise((resolve, reject) => {
+      const canvas = document.createElement('canvas');
+      const context = canvas.getContext('2d');
+      const { width: originWidth, height: originHeight } = img;
+      // 最大尺寸限制
+      const maxWidth = mx;
+      const maxHeight = mh;
+      // 目标尺寸
+      let targetWidth = originWidth;
+      let targetHeight = originHeight;
+      if (originWidth > maxWidth || originHeight > maxHeight) {
+        if (originWidth / originHeight > 1) {
+          // 宽图片
+          targetWidth = maxWidth;
+          targetHeight = Math.round(maxWidth * (originHeight / originWidth));
+        } else {
+          // 高图片
+          targetHeight = maxHeight;
+          targetWidth = Math.round(maxHeight * (originWidth / originHeight));
+        }
+      }
+      canvas.width = targetWidth;
+      canvas.height = targetHeight;
+      context.clearRect(0, 0, targetWidth, targetHeight);
+      // 图片绘制
+      context.drawImage(img, 0, 0, targetWidth, targetHeight);
+      canvas.toBlob(
+        function (blob) {
+          resolve(blob);
+        },
+        type,
+        quality,
+      );
+    });
+  };
+
+  return {
+    compressImg,
+    readImg,
+    formatMessage,
+    formatInput,
+    callBackNotice,
+
+    socketInit,
+    socketSendMsg,
+    socketClose,
+    socketHistoryList,
+
+    getFocus,
+    formatChatInput,
+    onDrop,
+    onPaste,
+    upload,
+
+    getUserToken,
+
+    state,
+
+    socketTest,
+
+    showTime,
+    formatTime,
+  };
+}

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 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/user/wallet/commission')">
+          <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/wechat.png',
+      title: '微信零钱',
+      value: '2',
+    },
+    {
+      icon: '/static/img/shop/pay/alipay.png',
+      title: '支付宝账户',
+      value: '3',
+    },
+    {
+      icon: '/static/img/shop/pay/bank.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>

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

@@ -0,0 +1,113 @@
+<!-- 分销商信息  -->
+<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: 690rpx;
+		height: 192rpx;
+		margin: -88rpx 20rpx 0 20rpx;
+		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>

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

@@ -0,0 +1,165 @@
+<!-- 分销首页:明细列表  -->
+<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';
+	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: 1,
+		},
+	});
+
+	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>

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

@@ -0,0 +1,138 @@
+<!-- 分销:商菜单栏 -->
+<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,
+			// },
+			// todo @芋艿:邀请海报需要登录后的个人数据
+			{
+				img: '/static/img/shop/commission/commission_icon7.png',
+				title: '邀请海报',
+				path: 'action:showShareModal',
+			},
+      // TODO @芋艿:缺少 icon
+      {
+				// img: '/static/img/shop/commission/commission_icon7.png',
+				title: '推广排行',
+				path: '/pages/commission/promoter',
+			},
+      {
+				// img: '/static/img/shop/commission/commission_icon7.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>

+ 150 - 0
pages/commission/goods.vue

@@ -0,0 +1,150 @@
+<!-- 分销商品列表  -->
+<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';
+  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: 1,
+    },
+    loadStatus: '',
+    shareInfo: {},
+  });
+
+  // TODO 芋艿:分享的接入
+  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) {
+      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';
+    // 补充分佣金额
+    data.list.forEach((item) => {
+      BrokerageApi.getProductBrokeragePrice(item.id).then((res) => {
+        item.brokerageMinPrice = res.data.brokerageMinPrice;
+        item.brokerageMaxPrice = res.data.brokerageMaxPrice;
+      });
+    });
+  }
+
+  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>

+ 37 - 0
pages/commission/index.vue

@@ -0,0 +1,37 @@
+<!-- 分销中心  -->
+<template>
+	<s-layout navbar="inner" class="index-wrap" title="分销中心" :bgStyle="bgStyle" onShareAppMessage>
+		<!-- 分销商信息 -->
+		<commission-info />
+		<!-- 账户信息 -->
+		<account-info />
+		<!-- 菜单栏 -->
+		<commission-menu />
+		<!-- 分销记录 -->
+		<commission-log />
+
+		<!-- 权限弹窗 -->
+		<commission-auth />
+	</s-layout>
+</template>
+
+<script setup>
+	import { reactive } 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';
+
+	const state = reactive({});
+
+	const bgStyle = {
+		color: '#F7D598',
+	};
+</script>
+
+<style lang="scss" scoped>
+	:deep(.page-main) {
+		background-size: 100% 100% !important;
+	}
+</style>

+ 331 - 0
pages/commission/order.vue

@@ -0,0 +1,331 @@
+<!-- 分销 - 订单明细 -->
+<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';
+  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: 1,
+    },
+  });
+
+  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.pageSize,
+      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>

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 158 - 0
pages/commission/promoter.vue


+ 581 - 0
pages/commission/team.vue

@@ -0,0 +1,581 @@
+<!-- 页面 TODO 芋艿:该页面的实现代码需要优化,包括 js 和 css,以及相关的样式设计 -->
+<template>
+  <s-layout title="我的团队" :class="state.scrollTop ? 'team-wrap' : ''" navbar="inner">
+    <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 30rpx">
+        <view class="nav acea-row row-around l1">
+          <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">暂无推广人数</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';
+  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: red;
+    // $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>

+ 470 - 0
pages/commission/wallet.vue

@@ -0,0 +1,470 @@
+<!-- 分销 - 佣金明细 -->
+<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';
+  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: 1,
+    },
+		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>

+ 427 - 0
pages/commission/withdraw.vue

@@ -0,0 +1,427 @@
+<!-- 分佣提现 -->
+<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="['2', '3'].includes(state.accountInfo.type)">收款码</view>
+      <view
+        class="input-box ss-flex ss-col-center"
+        v-show="['2', '3'].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 === '4'">持卡人</view>
+      <view
+        class="input-box ss-flex ss-col-center border-bottom"
+        v-show="state.accountInfo.type === '4'"
+      >
+        <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 === '4'">提现银行</view>
+      <view
+        class="input-box ss-flex ss-col-center border-bottom"
+        v-show="state.accountInfo.type === '4'"
+      >
+        <view class="unit" />
+        <uni-easyinput
+          :inputBorder="false"
+          class="ss-flex-1 ss-p-l-10"
+          v-model="state.accountInfo.bankName"
+          placeholder="请输入提现银行"
+        />
+      </view>
+      <!-- 开户地址 -->
+      <view class="card-title" v-show="state.accountInfo.type === '4'">开户地址</view>
+      <view
+        class="input-box ss-flex ss-col-center border-bottom"
+        v-show="state.accountInfo.type === '4'"
+      >
+        <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';
+
+  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: [], // 提现方式
+  });
+
+  // 打开提现方式的弹窗
+  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;
+    }
+  }
+
+  onBeforeMount(() => {
+    getWithdrawRules();
+    getBrokerageUser()
+  })
+</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>

+ 378 - 0
pages/coupon/detail.vue

@@ -0,0 +1,378 @@
+<!-- 优惠券详情  -->
+<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>
+            <!-- TODO 芋艿:可优化,增加优惠劵的描述 -->
+            <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';
+  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: 1,
+    },
+    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>

+ 218 - 0
pages/coupon/list.vue

@@ -0,0 +1,218 @@
+<!-- 优惠券中心  -->
+<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';
+  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',
+    },
+  ];
+
+  // TODO yunai:
+  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>

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

@@ -0,0 +1,145 @@
+<!-- 评价  -->
+<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="宝贝满足你的期待吗?说说你的使用心得,分享给想买的他们吧~" />
+              <!-- TODO 芋艿:文件上传 -->
+							<view class="img-box">
+								<s-uploader v-model:url="state.commentList[index].images" fileMediatype="image"
+									limit="9" mode="grid" :imageStyles="{ width: '168rpx', height: '168rpx' }" />
+							</view>
+						</view>
+					</view>
+				</view>
+			</view>
+		</view>
+    <!-- TODO 芋艿:是否匿名 -->
+
+		<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 } from 'vue';
+  import OrderApi from '@/sheep/api/trade/order';
+
+	const state = reactive({
+		orderInfo: {},
+		commentList: [],
+		id: null
+	});
+
+	async function onSubmit() {
+    // 顺序提交评论
+    for (const comment of state.commentList) {
+      await OrderApi.createOrderItemComment(comment);
+    }
+    // 都评论好,返回
+    sheep.$router.back();
+	}
+
+	onLoad(async (options) => {
+    if (!options.id) {
+      sheep.$helper.toast(`缺少订单信息,请检查`);
+      return
+    }
+		state.id = options.id;
+
+		const { code, data } = await OrderApi.getOrder(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;
+		}
+	}
+
+	.post-btn {
+		width: 690rpx;
+		line-height: 80rpx;
+		border-radius: 40rpx;
+		color: rgba(#fff, 0.9);
+		margin-bottom: 20rpx;
+	}
+</style>

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

@@ -0,0 +1,167 @@
+<!-- 商品评论的分页 -->
+<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
+      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';
+  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: 1,
+    },
+  });
+
+  // 切换选项卡
+  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>

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

@@ -0,0 +1,100 @@
+<template>
+  <su-fixed bottom placeholder :val="44">
+    <view>
+      <view v-for="activity in props.activityList" :key="activity.id">
+        <!-- TODO 芋艿:拼团 -->
+        <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"
+              />
+              <!-- TODO 芋艿:拼团 -->
+              <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';
+
+  // TODO 芋艿:这里要迁移下;
+  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>

+ 532 - 0
pages/goods/groupon.vue

@@ -0,0 +1,532 @@
+<!-- 拼团商品详情 -->
+<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.price) }}
+                  </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.selectedSkuPrice" @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 -->
+      <!-- TODO: 已售罄、预热 判断 设计-->
+      <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.activity.price || 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';
+  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,   // 显示规格弹框
+    selectedSkuPrice: {},   // 选中的规格价格
+    activity: {},           // 团购活动
+    grouponId: 0,           // 团购ID
+    grouponNum: 0,          // 团购人数
+    grouponAction: 'create',  // 团购操作
+    combinationHeadId: null, // 拼团团长编号
+  });
+
+  // 倒计时
+  const endTime = computed(() => {
+    return useDurationTime(state.activity.endTime);
+  });
+
+  // 规格变更
+  function onSkuChange(e) {
+    state.selectedSkuPrice = 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,
+          },
+        ],
+      }),
+    });
+  }
+
+  // 分享信息
+  // TODO @芋艿:分享的接入
+  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;
+    activity.products.forEach(product => {
+      spu.price = Math.min(spu.price, product.combinationPrice); // 设置 SPU 的最低价格
+    });
+    // 关闭骨架屏
+    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>

+ 413 - 0
pages/goods/index.vue

@@ -0,0 +1,413 @@
+<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="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-26">
+							<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 > 0">
+									{{ 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">
+							<!-- 满减送/限时折扣活动的提示 -->
+							<div class="tag-content">
+								<view class="tag-box ss-flex">
+									<view class="tag ss-m-r-10" v-for="promos in state.activityInfo"
+                        :key="promos.id" @tap="onActivity">
+										{{ promos.name }}
+									</view>
+								</view>
+							</div>
+
+							<!-- 优惠劵 -->
+							<view class="get-coupon-box ss-flex ss-col-center ss-m-l-20" @tap="state.showModel = true"
+								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-coupon-get v-model="state.couponInfo" :show="state.showModel" @close="state.showModel = false"
+                      @get="onGet" />
+
+				<!-- 满减送/限时折扣活动弹窗 -->
+				<s-activity-pop v-model="state.activityInfo" :show="state.showActivityModel"
+                        @close="state.showActivityModel = false" />
+			</block>
+		</s-layout>
+	</view>
+</template>
+
+<script setup>
+	import {
+		reactive,
+		computed
+	} 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 { formatSales, 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 detailActivityTip from './components/detail/detail-activity-tip.vue';
+	import { isEmpty } from 'lodash';
+  import SpuApi from '@/sheep/api/product/spu';
+
+	onPageScroll(() => {});
+
+	const state = reactive({
+		goodsId: 0,
+		skeletonLoading: true, // SPU 加载中
+		goodsInfo: {}, // SPU 信息
+		showSelectSku: false, // 是否展示 SKU 选择弹窗
+		selectedSku: {}, // 选中的 SKU
+		showModel: false, // 是否展示 Coupon 优惠劵的弹窗
+		couponInfo: [], // 可领取的 Coupon 优惠劵的列表
+		showActivityModel: false, // 【满减送/限时折扣】是否展示 Activity 营销活动的弹窗
+		activityInfo: [], // 【满减送/限时折扣】可参与的 Activity 营销活动的列表
+		activityList: [], // 【秒杀/拼团/砍价】可参与的 Activity 营销活动的列表
+	});
+
+	// 规格变更
+	function onSkuChange(e) {
+		state.selectedSku = e;
+	}
+
+	// 添加购物车
+	function onAddCart(e) {
+    if (!e.id) {
+      sheep.$helper.toast('请选择商品规格');
+      return;
+    }
+		sheep.$store('cart').add(e);
+	}
+
+	// 立即购买
+	function onBuy(e) {
+    if (!state.selectedSku.id) {
+      sheep.$helper.toast('请选择商品规格');
+      return;
+    }
+    sheep.$router.go('/pages/order/confirm', {
+      data: JSON.stringify({
+        items: [{
+          skuId: e.id,
+          count: e.goods_num
+        }],
+        // TODO 芋艿:后续清理掉这 2 参数
+        deliveryType: 1,
+        pointStatus: false,
+      }),
+    });
+	}
+
+	// 营销活动
+	function onActivity() {
+		state.showActivityModel = true;
+	}
+
+	// 立即领取
+	async function onGet(id) {
+    const { code } = await CouponApi.takeCoupon(id);
+    if (code !== 0) {
+      return;
+    }
+    uni.showToast({
+      title: '领取成功',
+    });
+    setTimeout(() => {
+      getCoupon();
+    }, 1000);
+	}
+
+	//  TODO 芋艿:待测试
+	const shareInfo = computed(() => {
+		if (isEmpty(state.goodsInfo)) return {};
+		return sheep.$platform.share.getShareInfo({
+			title: state.goodsInfo.name,
+			image: sheep.$url.cdn(state.goodsInfo.image),
+			desc: state.goodsInfo.subtitle,
+			params: {
+				page: '2',
+				query: state.goodsInfo.id,
+			},
+		}, {
+			type: 'goods', // 商品海报
+			title: state.goodsInfo.name, // 商品标题
+			// image: sheep.$url.cdn(state.goodsInfo.image), // 商品主图
+			image: sheep.$url.cdn(state.goodsInfo.picUrl), // 商品主图
+			price: fen2yuan(state.goodsInfo.price), // 商品价格
+			original_price: fen2yuan(state.goodsInfo.maretPrice), // 商品原价
+		}, );
+	});
+
+  async function getCoupon() {
+    const { code, data } = await CouponApi.getCouponTemplateList(state.goodsId, 2, 10);
+    if (code === 0) {
+      state.couponInfo = data;
+    }
+  }
+
+	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;
+
+      // 加载是否收藏
+      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;
+			}
+      res.data.forEach(activity => {
+        if ([1, 2, 3].includes(activity.type)) { // 情况一:拼团/秒杀/砍价
+          state.activityList.push(activity);
+        } else if (activity.type === 5) { // 情况二:满减送
+          state.activityInfo.push(activity);
+        } else { // 情况三:限时折扣 TODO 芋艿
+          console.log('待实现!优先级不高');
+        }
+      })
+		});
+	});
+</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;
+		}
+	}
+</style>

+ 362 - 0
pages/goods/list.vue

@@ -0,0 +1,362 @@
+<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 } from 'vue';
+	import { onLoad, onReachBottom } from '@dcloudio/uni-app';
+	import sheep from '@/sheep';
+	import _ from 'lodash';
+  import { resetPagination } from '@/sheep/util';
+  import SpuApi from '@/sheep/api/product/spu';
+
+	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;
+    }
+    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>

+ 555 - 0
pages/goods/seckill.vue

@@ -0,0 +1,555 @@
+<!-- 秒杀商品详情 -->
+<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'"
+      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} from 'vue';
+  import { onLoad, onPageScroll } from '@dcloudio/uni-app';
+  import sheep from '@/sheep';
+  import {isEmpty, min} from 'lodash';
+  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,
+          },
+        ],
+      }),
+    });
+  }
+
+  // 分享信息 TODO 芋艿:待接入
+  const shareInfo = computed(() => {
+    if (isEmpty(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.startTime, activity.endTime)
+
+    // 查询商品
+    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>

+ 200 - 0
pages/index/cart.vue

@@ -0,0 +1,200 @@
+<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/100"
+							: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)"></su-number-box>
+							</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">
+							{{ 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,
+		unref
+	} from 'vue';
+
+	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) {
+		console.log('单选')
+		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) => {
+			console.log(item, '便利');
+			// 此处前端做出修改
+			items.push({
+				skuId: item.sku.id,
+				count: item.count,
+				cartId: item.id,
+			})
+			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({
+				// order_type: 'goods',
+				// goods_list,
+				items,
+				// from: 'cart',
+				deliveryType: 1,
+				pointStatus: false,
+			}),
+		});
+	}
+
+	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>

+ 236 - 0
pages/index/category.vue

@@ -0,0 +1,236 @@
+<!-- 商品分类列表 -->
+<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';
+  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 () => {
+    await getList();
+    // 如果是 first 风格,需要加载商品分页
+    if (state.style === 'first_one' || state.style === 'first_two') {
+      onMenu(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.title }}</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>

+ 87 - 0
pages/index/index.vue

@@ -0,0 +1,87 @@
+<!-- 首页,支持店铺装修 -->
+<template>
+	<view v-if="template">
+		<s-layout title="首页" navbar="custom" tabbar="/pages/index/index" :bgStyle="template.page"
+			:navbarStyle="template.style?.navbar" 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('=');
+			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>

+ 38 - 0
pages/index/login.vue

@@ -0,0 +1,38 @@
+<!-- 微信公众号的登录回调页 -->
+<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;
+    });
+    const event = options.event;
+    const code = options.code;
+    const state = options.state;
+    if (event === 'login') { // 场景一:登录
+      const res = await sheep.$platform.useProvider().login(code, state);
+    } else if (event === 'bind') { // 场景二:绑定
+      sheep.$platform.useProvider().bind(code, state);
+    }
+
+    // 检测 H5 登录回调
+    let returnUrl = uni.getStorageSync('returnUrl');
+    if (returnUrl) {
+      uni.removeStorage('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.style?.navbar"
+    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>

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

@@ -0,0 +1,357 @@
+<!-- 售后申请 -->
+<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';
+
+  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: '' // 当前选择的售后原因
+  });
+  const formData = reactive({
+    way: '',
+    applyReason: '',
+    applyDescription: '',
+    images: [],
+  });
+  const rules = reactive({});
+
+  // 提交表单
+  async function submit() {
+    // #ifdef MP
+    sheep.$platform.useProvider('wechat').subscribeMessage('order_aftersale_change');
+    // #endif
+    let data = {
+      orderItemId: state.itemId,
+      refundPrice: state.item.payPrice,
+      ...formData,
+    };
+    const { code } = await AfterSaleApi.createAfterSale(data);
+    if (code === 0) {
+      uni.showToast({
+        title: '申请成功',
+      });
+      sheep.$router.go('/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.getOrder(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>

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

@@ -0,0 +1,342 @@
+<!-- 售后详情 -->
+<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';
+  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>

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

@@ -0,0 +1,193 @@
+<!-- 售后列表 -->
+<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';
+  import { formatAfterSaleStatus, formatAfterSaleStatusDescription, handleAfterSaleButtons } from '@/sheep/hooks/useGoods';
+  import AfterSaleApi from '@/sheep/api/trade/afterSale';
+
+	const paginationNull = {
+		list: [],
+    total: 0,
+    pageNo: 1,
+    pageSize: 10
+	};
+
+	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) {
+		state.pagination = paginationNull
+		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) {
+          state.pagination = paginationNull
+          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>

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

@@ -0,0 +1,194 @@
+<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>
+							<picker mode='selector' class='num' @change="bindPickerChange" :value="state.expressIndex"
+                      :range="state.expresses" range-key="name">
+								<view class="picker acea-row row-between-wrapper">
+									<view class='reason'>{{ state.expresses[state.expressIndex].name }}</view>
+                  <!-- TODO 芋艿:这里样式有问题,少了 > 按钮 -->
+									<text class='iconfont icon-jiantou' />
+								</view>
+							</picker>
+						</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>

+ 394 - 0
pages/order/confirm.vue

@@ -0,0 +1,394 @@
+<template>
+  <s-layout title="确认订单">
+    <!-- TODO:这个判断先删除 v-if="state.orderInfo.need_address === 1" -->
+    <view class="bg-white address-box ss-m-b-14 ss-r-b-10" @tap="onSelectAddress">
+      <s-address-item :item="state.addressInfo" :hasBorderBottom="false">
+        <view class="ss-rest-button">
+          <text class="_icon-forward" />
+        </view>
+      </s-address-item>
+    </view>
+
+    <!-- 商品信息 -->
+    <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>
+        <!-- TODO 芋艿:接入积分 -->
+        <view
+          class="order-item ss-flex ss-col-center ss-row-between"
+          v-if="state.orderPayload.order_type === 'score'"
+        >
+          <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.score_amount }}</text>
+          </view>
+        </view>
+        <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.deliveryPrice) }}
+            </text>
+          </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.length > 0 ? 'text-red' : 'text-disabled'"
+              v-else
+            >
+              {{
+                state.couponInfo.length > 0 ? state.couponInfo.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" TODO 芋艿:后续要把优惠信息打进去 -->
+            <text class="item-value text-red">
+              -¥{{ fen2yuan(state.orderInfo.price.discountPrice) }}
+            </text>
+            <text class="_icon-forward item-icon" />
+          </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 芋艿:后续要把优惠信息打进去 -->
+    <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 } from 'vue';
+  import { onLoad } from '@dcloudio/uni-app';
+  import sheep from '@/sheep';
+  import { isEmpty } from 'lodash';
+  import OrderApi from '@/sheep/api/trade/order';
+  import CouponApi from '@/sheep/api/promotion/coupon';
+  import { fen2yuan } from '@/sheep/hooks/useGoods';
+
+  const state = reactive({
+    orderPayload: {},
+    orderInfo: {
+      items: [], // 商品项列表
+      price: {}, // 价格信息
+    },
+    addressInfo: {}, // 选择的收货地址
+    showCoupon: false, // 是否展示优惠劵
+    couponInfo: [], // 优惠劵列表
+    showDiscount: false, // 是否展示营销活动
+  });
+
+  // 选择地址
+  function onSelectAddress() {
+    uni.$once('SELECT_ADDRESS', (e) => {
+      changeConsignee(e.addressInfo);
+    });
+    sheep.$router.go('/pages/user/address/list');
+  }
+
+  // 更改收货人地址&计算订单信息
+  async function changeConsignee(addressInfo = {}) {
+    if (!isEmpty(addressInfo)) {
+      state.addressInfo = addressInfo;
+    }
+    await getOrderInfo();
+  }
+
+  // 选择优惠券
+  async function onSelectCoupon(couponId) {
+    state.orderPayload.couponId = couponId || 0;
+    await getOrderInfo();
+    state.showCoupon = false;
+  }
+
+  // 提交订单
+  function onConfirm() {
+    if (!state.addressInfo.id) {
+      sheep.$helper.toast('请选择收货地址');
+      return;
+    }
+    submitOrder();
+  }
+
+  // 创建订单&跳转
+  async function submitOrder() {
+    const { code, data } = await OrderApi.createOrder({
+      items: state.orderPayload.items,
+      couponId: state.orderPayload.couponId,
+      addressId: state.addressInfo.id,
+      deliveryType: 1, // TODO 芋艿:需要支持【门店自提】
+      pointStatus: false, // TODO 芋艿:需要支持【积分选择】
+      combinationActivityId: state.orderPayload.combinationActivityId,
+      combinationHeadId: state.orderPayload.combinationHeadId,
+      seckillActivityId: state.orderPayload.seckillActivityId
+    });
+    if (code !== 0) {
+      return;
+    }
+    // 更新购物车列表,如果来自购物车
+    if (state.orderPayload.items[0].cartId > 0) {
+      sheep.$store('cart').getList();
+    }
+    // 跳转到支付页面
+    sheep.$router.redirect('/pages/pay/index', {
+      id: data.payOrderId,
+    });
+  }
+
+  // 检查库存 & 计算订单价格
+  async function getOrderInfo() {
+    // 计算价格
+    const { data, code } = await OrderApi.settlementOrder({
+      items: state.orderPayload.items,
+      couponId: state.orderPayload.couponId,
+      addressId: state.addressInfo.id,
+      deliveryType: 1, // TODO 芋艿:需要支持【门店自提】
+      pointStatus: false, // TODO 芋艿:需要支持【积分选择】
+      combinationActivityId: state.orderPayload.combinationActivityId,
+      combinationHeadId: state.orderPayload.combinationHeadId,
+      seckillActivityId: state.orderPayload.seckillActivityId
+    });
+    if (code !== 0) {
+      return;
+    }
+    state.orderInfo = data;
+    // 设置收货地址
+    if (state.orderInfo.address) {
+      state.addressInfo = state.orderInfo.address;
+    }
+  }
+
+  // 获取可用优惠券
+  async function getCoupons() {
+    const { code, data } = await CouponApi.getMatchCouponList(
+      state.orderInfo.price.payPrice,
+      state.orderInfo.items.map((item) => item.spuId),
+      state.orderPayload.items.map((item) => item.skuId),
+      state.orderPayload.items.map((item) => item.categoryId),
+    );
+    if (code === 0) {
+      state.couponInfo = data;
+    }
+  }
+
+  onLoad(async (options) => {
+    if (!options.data) {
+      sheep.$helper.toast('参数不正确,请检查!');
+      return;
+    }
+    state.orderPayload = JSON.parse(options.data);
+    await getOrderInfo();
+    await getCoupons();
+  });
+</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>

+ 626 - 0
pages/order/detail.vue

@@ -0,0 +1,626 @@
+<!-- 订单详情 -->
+<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' }]"
+    >
+      <!-- 订单信息 TODO 芋艿: -->
+      <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.skuId)"
+            :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>
+
+    <!-- 订单信息 -->
+    <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>
+      <!-- TODO 芋艿:优惠劵抵扣、积分抵扣 -->
+      <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 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>
+        <!-- TODO 芋艿:拼团接入 -->
+        <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.ext.groupon_id,
+            })
+          "
+        >
+          拼团详情
+        </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 } from '@dcloudio/uni-app';
+  import { reactive } from 'vue';
+  import { isEmpty } from 'lodash';
+  import {
+    fen2yuan,
+    formatOrderStatus,
+    formatOrderStatusDescription,
+    handleOrderButtons,
+  } from '@/sheep/hooks/useGoods';
+  import OrderApi from '@/sheep/api/trade/order';
+
+  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 onCopy = () => {
+    sheep.$helper.copyText(state.orderInfo.sn);
+  };
+
+  // 去支付
+  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,
+    });
+  }
+
+  // 确认收货 TODO 芋艿:待测试
+  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;
+    }
+
+    // 正常的确认收货流程
+    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
+    });
+  }
+
+  async function getOrderDetail(id) {
+    // 对详情数据进行适配
+    let res;
+    if (state.comeinType === 'wechat') {
+      // TODO 芋艿:微信场景下
+      res = await OrderApi.getOrder(id, {
+        merchant_trade_no: state.merchantTradeNo,
+      });
+    } else {
+      res = await OrderApi.getOrder(id);
+    }
+    if (res.code === 0) {
+      state.orderInfo = res.data;
+      handleOrderButtons(state.orderInfo);
+    } else {
+      sheep.$router.back();
+    }
+  }
+
+  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;
+    }
+    await getOrderDetail(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.getOrder(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>

+ 459 - 0
pages/order/list.vue

@@ -0,0 +1,459 @@
+<!-- 订单列表 -->
+<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';
+	import {
+		isEmpty
+	} from 'lodash';
+  import OrderApi from '@/sheep/api/trade/order';
+
+	const paginationNull = {
+		list: [],
+    total: 0,
+		pageNo: 1,
+    pageSize: 5,
+	};
+
+	// 数据
+	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;
+    }
+    // 重头加载代码
+		state.pagination = paginationNull;
+		state.currentTab = e.index;
+		getOrderList();
+	}
+
+	// 订单详情
+	function onOrderDetail(id) {
+		sheep.$router.go('/pages/order/detail', {
+			id,
+		});
+	}
+
+	// 分享拼团 TODO 芋艿:待测试
+	function onOrderGroupon(order) {
+		sheep.$router.go('/pages/activity/groupon/detail', {
+			id: order.ext.groupon_id,
+		});
+	}
+
+	// 继续支付
+	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;
+		}
+
+		// 正常的确认收货流程
+		const { code } = await OrderApi.receiveOrder(order.id);
+		if (code === 0) {
+			state.pagination = paginationNull;
+			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(() => {
+		state.pagination = paginationNull;
+		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>

+ 288 - 0
pages/pay/index.vue

@@ -0,0 +1,288 @@
+<!-- 收银台 -->
+<template>
+  <s-layout title="收银台">
+    <view class="bg-white ss-modal-box ss-flex-col">
+      <!-- 订单信息 -->
+      <view class="modal-header ss-flex-col ss-col-center ss-row-center">
+        <view class="money-box ss-m-b-20">
+          <text class="money-text">{{ fen2yuan(state.orderInfo.price) }}</text>
+        </view>
+        <view class="time-text">
+          <text>{{ payDescText }}</text>
+        </view>
+      </view>
+
+      <!-- 支付方式 -->
+      <view class="modal-content ss-flex-1">
+        <view class="pay-title ss-p-l-30 ss-m-y-30">选择支付方式</view>
+        <radio-group @change="onTapPay">
+          <label class="pay-type-item" v-for="item in state.payMethods" :key="item.title">
+            <view
+              class="pay-item ss-flex ss-col-center ss-row-between ss-p-x-30 border-bottom"
+              :class="{ 'disabled-pay-item': item.disabled }"
+            >
+              <view class="ss-flex ss-col-center">
+                <image
+                  class="pay-icon"
+                  v-if="item.disabled"
+                  :src="sheep.$url.static('/static/img/shop/pay/cod_disabled.png')"
+                  mode="aspectFit"
+                />
+                <image
+                  class="pay-icon"
+                  v-else
+                  :src="sheep.$url.static(item.icon)"
+                  mode="aspectFit"
+                />
+                <text class="pay-title">{{ item.title }}</text>
+              </view>
+              <view class="check-box ss-flex ss-col-center ss-p-l-10">
+                <view class="userInfo-money ss-m-r-10" v-if="item.value === 'wallet'">
+                  余额: {{ fen2yuan(userWallet.balance) }}元
+                </view>
+                <radio
+                  :value="item.value"
+                  color="var(--ui-BG-Main)"
+                  style="transform: scale(0.8)"
+                  :disabled="item.disabled"
+                  :checked="state.payment === item.value"
+                />
+              </view>
+            </view>
+          </label>
+        </radio-group>
+      </view>
+
+      <!-- 工具 -->
+      <view class="modal-footer ss-flex ss-row-center ss-col-center ss-m-t-80 ss-m-b-40">
+        <button v-if="state.payStatus === 0" class="ss-reset-button past-due-btn">
+          检测支付环境中
+        </button>
+        <button v-else-if="state.payStatus === -1" class="ss-reset-button past-due-btn" disabled>
+          支付已过期
+        </button>
+        <button
+          v-else
+          class="ss-reset-button save-btn"
+          @tap="onPay"
+          :disabled="state.payStatus !== 1"
+          :class="{ 'disabled-btn': state.payStatus !== 1 }"
+        >
+          立即支付
+        </button>
+      </view>
+    </view>
+  </s-layout>
+</template>
+<script setup>
+  import { computed, reactive } from 'vue';
+  import { onLoad } from '@dcloudio/uni-app';
+  import sheep from '@/sheep';
+  import { fen2yuan, useDurationTime } from '@/sheep/hooks/useGoods';
+  import PayOrderApi from '@/sheep/api/pay/order';
+  import PayChannelApi from '@/sheep/api/pay/channel';
+  import { getPayMethods } from '@/sheep/platform/pay';
+
+  const userWallet = computed(() => sheep.$store('user').userWallet);
+
+  // 检测支付环境
+  const state = reactive({
+    orderType: 'goods', // 订单类型; goods - 商品订单, recharge - 充值订单
+    orderInfo: {}, // 支付单信息
+    payStatus: 0, // 0=检测支付环境, -2=未查询到支付单信息, -1=支付已过期, 1=待支付,2=订单已支付
+    payMethods: [], // 可选的支付方式
+    payment: '', // 选中的支付方式
+  });
+
+  const onPay = () => {
+    if (state.payment === '') {
+      sheep.$helper.toast('请选择支付方式');
+      return;
+    }
+    if (state.payment === 'wallet') {
+      uni.showModal({
+        title: '提示',
+        content: '确定要支付吗?',
+        success: function (res) {
+          if (res.confirm) {
+            sheep.$platform.pay(state.payment, state.orderType, state.orderInfo.id);
+          }
+        },
+      });
+    } else {
+      sheep.$platform.pay(state.payment, state.orderType, state.orderInfo.id);
+    }
+  };
+
+  // 支付文案提示
+  const payDescText = computed(() => {
+    if (state.payStatus === 2) {
+      return '该订单已支付';
+    }
+    if (state.payStatus === 1) {
+      const time = useDurationTime(state.orderInfo.expireTime);
+      if (time.ms <= 0) {
+        state.payStatus = -1;
+        return '';
+      }
+      return `剩余支付时间 ${time.h}:${time.m}:${time.s} `;
+    }
+    if (state.payStatus === -2) {
+      return '未查询到支付单信息';
+    }
+    return '';
+  });
+
+  // 状态转换:payOrder.status => payStatus
+  function checkPayStatus() {
+    if (state.orderInfo.status === 10
+      || state.orderInfo.status === 20 ) { // 支付成功
+      state.payStatus = 2;
+      return;
+    }
+    if (state.orderInfo.status === 30) { // 支付关闭
+      state.payStatus = -1;
+      return;
+    }
+    state.payStatus = 1; // 待支付
+  }
+
+  // 切换支付方式
+  function onTapPay(e) {
+    state.payment = e.detail.value;
+  }
+
+  // 设置支付订单信息
+  async function setOrder(id) {
+    // 获得支付订单信息
+    const { data, code } = await PayOrderApi.getOrder(id);
+    if (code !== 0 || !data) {
+      state.payStatus = -2;
+      return;
+    }
+    state.orderInfo = data;
+    // 获得支付方式
+    await setPayMethods();
+    // 设置支付状态
+    checkPayStatus();
+  }
+
+  // 获得支付方式
+  async function setPayMethods() {
+    const { data, code } = await PayChannelApi.getEnableChannelCodeList(state.orderInfo.appId)
+    if (code !== 0) {
+      return
+    }
+    state.payMethods = getPayMethods(data)
+  }
+
+  onLoad((options) => {
+    if (sheep.$platform.name === 'WechatOfficialAccount'
+      && sheep.$platform.os === 'ios'
+      && !sheep.$platform.landingPage.includes('pages/pay/index')) {
+      location.reload();
+      return;
+    }
+    // 获得支付订单信息
+    let id = options.id;
+    if (options.orderType) {
+      state.orderType = options.orderType;
+    }
+    setOrder(id);
+    // 刷新钱包的缓存
+    sheep.$store('user').getWallet();
+  });
+</script>
+
+<style lang="scss" scoped>
+  .pay-icon {
+    width: 36rpx;
+    height: 36rpx;
+    margin-right: 26rpx;
+  }
+
+  .ss-modal-box {
+    // max-height: 1000rpx;
+
+    .modal-header {
+      position: relative;
+      padding: 60rpx 20rpx 40rpx;
+
+
+      .money-text {
+        color: $red;
+        font-size: 46rpx;
+        font-weight: bold;
+        font-family: OPPOSANS;
+
+        &::before {
+          content: '¥';
+          font-size: 30rpx;
+        }
+      }
+
+      .time-text {
+        font-size: 26rpx;
+        color: $gray-b;
+      }
+
+      .close-icon {
+        position: absolute;
+        top: 10rpx;
+        right: 20rpx;
+        font-size: 46rpx;
+        opacity: 0.2;
+      }
+    }
+
+    .modal-content {
+      overflow-y: auto;
+
+      .pay-title {
+        font-size: 26rpx;
+        font-weight: 500;
+        color: #333333;
+      }
+
+      .pay-tip {
+        font-size: 26rpx;
+        color: #bbbbbb;
+      }
+
+      .pay-item {
+        height: 86rpx;
+      }
+      .disabled-pay-item {
+        .pay-title {
+          color: #999999;
+        }
+      }
+
+      .userInfo-money {
+        font-size: 26rpx;
+        color: #bbbbbb;
+        line-height: normal;
+      }
+    }
+
+    .save-btn {
+      width: 710rpx;
+      height: 80rpx;
+      border-radius: 40rpx;
+      background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
+      color: $white;
+    }
+    .disabled-btn {
+      background: #e5e5e5;
+      color: #999999;
+    }
+
+    .past-due-btn {
+      width: 710rpx;
+      height: 80rpx;
+      border-radius: 40rpx;
+      background-color: #999;
+      color: #fff;
+    }
+  }
+</style>

+ 165 - 0
pages/pay/recharge-log.vue

@@ -0,0 +1,165 @@
+<!-- 充值记录 -->
+<template>
+  <s-layout class="widthdraw-log-wrap" title="充值记录">
+    <!-- 记录卡片 -->
+    <view class="wallet-log-box ss-p-b-30">
+      <view class="log-list" v-for="item in state.pagination.list" :key="item">
+        <view class="head ss-flex ss-col-center ss-row-between">
+          <view class="title">充值金额</view>
+          <view class="num" :class="item.refundStatus === 10 ? 'danger-color' : 'success-color'">
+            {{ fen2yuan(item.payPrice) }} 元
+            <text v-if="item.bonusPrice > 0">(赠送 {{ fen2yuan(item.bonusPrice)}} 元)</text>
+          </view>
+        </view>
+        <view class="status-box item ss-flex ss-col-center ss-row-between">
+          <view class="item-title">支付状态</view>
+          <view
+            class="status-text"
+            :class="item.refundStatus === 10 ? 'danger-color' : 'success-color'"
+          >
+            {{ item.refundStatus === 10 ? '已退款' : '已支付' }}
+          </view>
+        </view>
+        <view class="time-box item ss-flex ss-col-center ss-row-between">
+          <text class="item-title">充值渠道</text>
+          <view class="time ss-ellipsis-1">{{ item.payChannelName }}</view>
+        </view>
+        <view class="time-box item ss-flex ss-col-center ss-row-between">
+          <text class="item-title">充值单号</text>
+          <view class="time"> {{ item.payOrderChannelOrderNo }} </view>
+        </view>
+        <view class="time-box item ss-flex ss-col-center ss-row-between">
+          <text class="item-title">充值时间</text>
+          <view class="time"> {{ sheep.$helper.timeFormat(item.payTime, 'yyyy-mm-dd hh:MM:ss') }}</view>
+        </view>
+      </view>
+    </view>
+    <s-empty
+      v-if="state.pagination.total === 0"
+      icon="/static/comment-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 { reactive } from 'vue';
+  import { onLoad, onReachBottom } from '@dcloudio/uni-app';
+  import _ from 'lodash';
+  import PayWalletApi from '@/sheep/api/pay/wallet';
+  import sheep from '@/sheep';
+  import { fen2yuan } from '../../sheep/hooks/useGoods';
+
+  const state = reactive({
+    pagination: {
+      list: [],
+      total: 0,
+      pageNo: 1,
+      pageSize: 5,
+    },
+    loadStatus: '',
+  });
+
+  async function getLogList(page = 1, list_rows = 5) {
+    const { code, data } = await PayWalletApi.getWalletRechargePage({
+      pageNo: page,
+      pageSize: list_rows,
+    });
+    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 loadMore() {
+    if (state.loadStatus === 'noMore') {
+      return;
+    }
+    state.pagination.pageNo++;
+    getLogList();
+  }
+
+  onLoad(() => {
+    getLogList();
+  });
+
+  onReachBottom(() => {
+    loadMore();
+  });
+</script>
+
+<style lang="scss" scoped>
+  // 记录卡片
+  .log-list {
+    min-height: 213rpx;
+    background: $white;
+    margin-bottom: 10rpx;
+    padding-bottom: 10rpx;
+
+    .head {
+      padding: 0 35rpx;
+      height: 80rpx;
+      border-bottom: 1rpx solid $gray-e;
+      margin-bottom: 20rpx;
+
+      .title {
+        font-size: 28rpx;
+        font-weight: 500;
+        color: $dark-3;
+      }
+
+      .num {
+        font-size: 28rpx;
+        font-weight: 500;
+      }
+    }
+
+    .item {
+      padding: 0 30rpx 10rpx;
+
+      .item-icon {
+        color: $gray-d;
+        font-size: 36rpx;
+        margin-right: 8rpx;
+      }
+
+      .item-title {
+        width: 180rpx;
+        font-size: 24rpx;
+        font-weight: 400;
+        color: #666666;
+      }
+
+      .status-text {
+        font-size: 24rpx;
+        font-weight: 500;
+      }
+
+      .time {
+        font-size: 24rpx;
+        font-weight: 400;
+        color: #c0c0c0;
+      }
+    }
+  }
+  .warning-color {
+    color: #faad14;
+  }
+  .danger-color {
+    color: #ff4d4f;
+  }
+  .success-color {
+    color: #67c23a;
+  }
+</style>

+ 259 - 0
pages/pay/recharge.vue

@@ -0,0 +1,259 @@
+<!-- 充值界面 -->
+<template>
+	<s-layout title="充值" class="withdraw-wrap" navbar="inner">
+		<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(userWallet.balance) }}</view>
+			</view>
+			<button class="ss-reset-button log-btn" @tap="sheep.$router.go('/pages/pay/recharge-log')">
+        充值记录
+      </button>
+		</view>
+		<view class="recharge-box">
+			<view class="recharge-card-box">
+				<view class="input-label ss-m-b-50">充值金额</view>
+				<view class="input-box ss-flex border-bottom ss-p-b-20">
+					<view class="unit">¥</view>
+					<uni-easyinput v-model="state.recharge_money" type="digit" placeholder="请输入充值金额"
+                         :inputBorder="false" />
+				</view>
+				<view class="face-value-box ss-flex ss-flex-wrap ss-m-y-40">
+					<button class="ss-reset-button face-value-btn" v-for="item in state.packageList" :key="item.money"
+						:class="[{ 'btn-active': state.recharge_money === fen2yuan(item.payPrice) }]"
+						@tap="onCard(item.payPrice)">
+						<text class="face-value-title">{{ fen2yuan(item.payPrice) }}</text>
+						<view v-if="item.bonusPrice" class="face-value-tag">
+							送 {{ fen2yuan(item.bonusPrice) }} 元
+            </view>
+					</button>
+				</view>
+				<button class="ss-reset-button save-btn ui-BG-Main-Gradient ss-m-t-60 ui-Shadow-Main" @tap="onConfirm">
+					确认充值
+				</button>
+			</view>
+		</view>
+	</s-layout>
+</template>
+
+<script setup>
+	import { computed, reactive } from 'vue';
+	import sheep from '@/sheep';
+	import { onLoad } from '@dcloudio/uni-app';
+  import { fen2yuan } from '@/sheep/hooks/useGoods';
+  import PayWalletApi from '@/sheep/api/pay/wallet';
+
+	const userWallet = computed(() => sheep.$store('user').userWallet);
+	const statusBarHeight = sheep.$platform.device.statusBarHeight * 2;
+	const headerBg = sheep.$url.css('/static/img/shop/user/withdraw_bg.png');
+
+	const state = reactive({
+		recharge_money: '', // 输入的充值金额
+    packageList: [],
+	});
+
+	// 点击卡片,选择充值金额
+	function onCard(e) {
+		state.recharge_money = fen2yuan(e);
+	}
+
+  // 获得钱包充值套餐列表
+	async function getRechargeTabs() {
+    const { code, data } = await PayWalletApi.getWalletRechargePackageList();
+    if (code !== 0) {
+      return;
+    }
+    state.packageList = data;
+	}
+
+  // 发起支付
+	async function onConfirm() {
+		const { code, data } = await PayWalletApi.createWalletRecharge({
+			packageId: state.packageList.find((item) => fen2yuan(item.payPrice) === state.recharge_money)?.id,
+			payPrice: state.recharge_money * 100
+		});
+		if (code !== 0) {
+			return;
+		}
+    // #ifdef MP
+    sheep.$platform.useProvider('wechat').subscribeMessage('money_change');
+    // #endif
+    sheep.$router.go('/pages/pay/index', {
+      id: data.payOrderId,
+      orderType: 'recharge'
+    });
+	}
+
+	onLoad(() => {
+		getRechargeTabs();
+	});
+</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;
+		}
+	}
+
+	.recharge-box {
+		position: relative;
+		padding: 0 30rpx;
+		margin-top: -60rpx;
+	}
+
+	.save-btn {
+		width: 620rpx;
+		height: 86rpx;
+		border-radius: 44rpx;
+		font-size: 30rpx;
+	}
+
+	.recharge-card-box {
+		width: 690rpx;
+		background: var(--ui-BG);
+		border-radius: 20rpx;
+		padding: 30rpx;
+		box-sizing: border-box;
+
+		.input-label {
+			font-size: 30rpx;
+			font-weight: 500;
+			color: #333;
+		}
+
+		.unit {
+			display: flex;
+			align-items: center;
+			font-size: 48rpx;
+			font-weight: 500;
+		}
+
+		.uni-easyinput__placeholder-class {
+			font-size: 30rpx;
+			height: 60rpx;
+			display: flex;
+			align-items: center;
+		}
+
+		:deep(.uni-easyinput__content-input) {
+			font-size: 48rpx;
+		}
+
+		.face-value-btn {
+			width: 200rpx;
+			height: 144rpx;
+			border: 1px solid var(--ui-BG-Main);
+			border-radius: 10rpx;
+			position: relative;
+			z-index: 1;
+			margin-bottom: 15rpx;
+			margin-right: 15rpx;
+
+			&:nth-of-type(3n) {
+				margin-right: 0;
+			}
+
+			.face-value-title {
+				font-size: 36rpx;
+				font-weight: 500;
+				color: var(--ui-BG-Main);
+				font-family: OPPOSANS;
+
+				&::after {
+					content: '元';
+					font-size: 24rpx;
+					margin-left: 6rpx;
+				}
+			}
+
+			.face-value-tag {
+				position: absolute;
+				z-index: 2;
+				height: 40rpx;
+				line-height: 40rpx;
+				background: var(--ui-BG-Main);
+				opacity: 0.8;
+				border-radius: 10rpx 0 20rpx 0;
+				top: 0;
+				left: -2rpx;
+				padding: 0 16rpx;
+				font-size: 22rpx;
+				color: $white;
+				font-family: OPPOSANS;
+			}
+
+			&::before {
+				position: absolute;
+				content: ' ';
+				width: 100%;
+				height: 100%;
+				background: var(--ui-BG-Main);
+				opacity: 0.1;
+				z-index: 0;
+				left: 0;
+				top: 0;
+			}
+		}
+
+		.btn-active {
+			z-index: 1;
+
+			&::before {
+				content: '';
+				background: var(--ui-BG-Main);
+				opacity: 1;
+			}
+
+			.face-value-title {
+				color: $white;
+				position: relative;
+				z-index: 1;
+				font-family: OPPOSANS;
+			}
+
+			.face-value-tag {
+				background: $white;
+				color: var(--ui-BG-Main);
+				font-family: OPPOSANS;
+			}
+		}
+	}
+</style>

+ 287 - 0
pages/pay/result.vue

@@ -0,0 +1,287 @@
+<!-- 支付结果页面 -->
+<template>
+  <s-layout title="支付结果" :bgStyle="{ color: '#FFF' }">
+    <view class="pay-result-box ss-flex-col ss-row-center ss-col-center">
+      <!-- 信息展示 -->
+      <view class="pay-waiting ss-m-b-30" v-if="payResult === 'waiting'" />
+      <image
+        class="pay-img ss-m-b-30"
+        v-if="payResult === 'success'"
+        :src="sheep.$url.static('/static/img/shop/order/order_pay_success.gif')"
+      />
+      <image
+        class="pay-img ss-m-b-30"
+        v-if="['failed', 'closed'].includes(payResult)"
+        :src="sheep.$url.static('/static/img/shop/order/order_paty_fail.gif')"
+      />
+      <view class="tip-text ss-m-b-30" v-if="payResult === 'success'">支付成功</view>
+      <view class="tip-text ss-m-b-30" v-if="payResult === 'failed'">支付失败</view>
+      <view class="tip-text ss-m-b-30" v-if="payResult === 'closed'">该订单已关闭</view>
+      <view class="tip-text ss-m-b-30" v-if="payResult === 'waiting'">检测支付结果...</view>
+      <view class="pay-total-num ss-flex" v-if="payResult === 'success'">
+        <view>¥{{ fen2yuan(state.orderInfo.price) }}</view>
+      </view>
+
+      <!-- 操作区 -->
+      <view class="btn-box ss-flex ss-row-center ss-m-t-50">
+        <button class="back-btn ss-reset-button" @tap="sheep.$router.go('/pages/index/index')">
+          返回首页
+        </button>
+        <button
+          class="check-btn ss-reset-button"
+          v-if="payResult === 'failed'"
+          @tap="
+            sheep.$router.redirect('/pages/pay/index', { id: state.id, orderType: state.orderType })
+          "
+        >
+          重新支付
+        </button>
+        <button class="check-btn ss-reset-button" v-if="payResult === 'success'" @tap="onOrder">
+          查看订单
+        </button>
+        <!-- TODO 芋艿:拼团接入 -->
+        <button
+          class="check-btn ss-reset-button"
+          v-if="payResult === 'success' && state.tradeOrder.type === 3"
+          @tap="sheep.$router.redirect('/pages/activity/groupon/order')"
+        >
+          我的拼团
+        </button>
+      </view>
+
+      <!-- TODO 芋艿:订阅 -->
+      <!-- #ifdef MP -->
+      <view class="subscribe-box ss-flex ss-m-t-44">
+        <image class="subscribe-img" :src="sheep.$url.static('/static/img/shop/order/cargo.png')" />
+        <view class="subscribe-title ss-m-r-48 ss-m-l-16">获取实时发货信息与订单状态</view>
+        <view class="subscribe-start" @tap="subscribeMessage">立即订阅</view>
+      </view>
+      <!-- #endif -->
+    </view>
+  </s-layout>
+</template>
+
+<script setup>
+  import { onLoad, onHide, onShow } from '@dcloudio/uni-app';
+  import { reactive, computed } from 'vue';
+  import { isEmpty } from 'lodash';
+  import sheep from '@/sheep';
+  import PayOrderApi from '@/sheep/api/pay/order';
+  import { fen2yuan } from '../../sheep/hooks/useGoods';
+  import OrderApi from '@/sheep/api/trade/order';
+
+  const state = reactive({
+    id: 0, // 支付单号
+    orderType: 'goods', // 订单类型
+    result: 'unpaid', // 支付状态
+    orderInfo: {}, // 支付订单信息
+    tradeOrder: {}, // 商品订单信息,只有在 orderType 为 goods 才会请求。目的:【我的拼团】按钮的展示
+    counter: 0, // 获取结果次数
+  });
+
+  // 支付结果 result => payResult
+  const payResult = computed(() => {
+    if (state.result === 'unpaid') {
+      return 'waiting';
+    }
+    if (state.result === 'paid') {
+      return 'success';
+    }
+    if (state.result === 'failed') {
+      return 'failed';
+    }
+    if (state.result === 'closed') {
+      return 'closed';
+    }
+  });
+
+  // 获得订单信息
+  async function getOrderInfo(id) {
+    state.counter++;
+    // 1. 加载订单信息
+    const { data, code } = await PayOrderApi.getOrder(id);
+    if (code === 0) {
+      state.orderInfo = data;
+      if (!state.orderInfo || state.orderInfo.status === 30) {
+        // 支付关闭
+        state.result = 'closed';
+        return;
+      }
+      if (state.orderInfo.status !== 0) {
+        // 非待支付,可能是已支付,可能是已退款
+        state.result = 'paid';
+        // #ifdef MP
+        subscribeMessage();
+        // #endif
+        // 特殊:获得商品订单信息
+        if (state.orderType === 'goods') {
+          const { data, code } = await OrderApi.getOrder(state.orderInfo.merchantOrderId);
+          if (code === 0) {
+            state.tradeOrder = data;
+          }
+        }
+        return;
+      }
+    }
+    // 2.1 情况三一:未支付,且轮询次数小于三次,则继续轮询
+    if (state.counter < 3 && state.result === 'unpaid') {
+      setTimeout(() => {
+        getOrderInfo(id);
+      }, 1500);
+    }
+    // 2.2 情况二:超过三次检测才判断为支付失败
+    if (state.counter >= 3) {
+      state.result = 'failed';
+    }
+  }
+
+  function onOrder() {
+    // TODO 芋艿:待测试
+    if (state.orderType === 'recharge') {
+      sheep.$router.redirect('/pages/pay/recharge-log');
+    } else {
+      sheep.$router.redirect('/pages/order/list');
+    }
+  }
+
+  // TODO 芋艿:待测试
+  // #ifdef MP
+  function subscribeMessage() {
+    let event = ['order_dispatched'];
+    if (state.tradeOrder.type === 3) {
+      event.push('groupon_finish');
+      event.push('groupon_fail');
+    }
+    sheep.$platform.useProvider('wechat').subscribeMessage(event);
+  }
+  // #endif
+
+  onLoad(async (options) => {
+    // 支付订单号
+    if (options.id) {
+      state.id = options.id;
+    }
+    // 订单类型
+    if (options.orderType) {
+      state.orderType = options.orderType;
+    }
+
+    // 支付结果传值过来是失败,则直接显示失败界面
+    if (options.payState === 'fail') {
+      state.result = 'failed';
+    } else {
+      // 轮询三次检测订单支付结果
+      await getOrderInfo(state.id);
+    }
+  });
+
+  onShow(() => {
+    if (isEmpty(state.orderInfo)) {
+      return;
+    }
+    getOrderInfo(state.id);
+  });
+
+  onHide(() => {
+    state.result = 'unpaid';
+    state.counter = 0;
+  });
+</script>
+
+<style lang="scss" scoped>
+  @keyframes rotation {
+    0% {
+      transform: rotate(0deg);
+    }
+
+    100% {
+      transform: rotate(360deg);
+    }
+  }
+
+  .score-img {
+    width: 36rpx;
+    height: 36rpx;
+    margin: 0 4rpx;
+  }
+
+  .pay-result-box {
+    padding: 60rpx 0;
+
+    .pay-waiting {
+      margin-top: 20rpx;
+      width: 60rpx;
+      height: 60rpx;
+      border: 10rpx solid rgb(233, 231, 231);
+      border-bottom-color: rgb(204, 204, 204);
+      border-radius: 50%;
+      display: inline-block;
+      // -webkit-animation: rotation 1s linear infinite;
+      animation: rotation 1s linear infinite;
+    }
+
+    .pay-img {
+      width: 130rpx;
+      height: 130rpx;
+    }
+
+    .tip-text {
+      font-size: 30rpx;
+      font-weight: bold;
+      color: #333333;
+    }
+
+    .pay-total-num {
+      font-size: 36rpx;
+      font-weight: 500;
+      color: #333333;
+      font-family: OPPOSANS;
+    }
+
+    .btn-box {
+      width: 100%;
+
+      .back-btn {
+        width: 190rpx;
+        height: 70rpx;
+        font-size: 28rpx;
+        border: 2rpx solid #dfdfdf;
+        border-radius: 35rpx;
+        font-weight: 400;
+        color: #595959;
+      }
+
+      .check-btn {
+        width: 190rpx;
+        height: 70rpx;
+        font-size: 28rpx;
+        border: 2rpx solid #dfdfdf;
+        border-radius: 35rpx;
+        font-weight: 400;
+        color: #595959;
+        margin-left: 32rpx;
+      }
+    }
+
+    .subscribe-box {
+      .subscribe-img {
+        width: 44rpx;
+        height: 44rpx;
+      }
+
+      .subscribe-title {
+        font-weight: 500;
+        font-size: 32rpx;
+        line-height: 36rpx;
+        color: #434343;
+      }
+
+      .subscribe-start {
+        color: var(--ui-BG-Main);
+        font-weight: 700;
+        font-size: 32rpx;
+        line-height: 36rpx;
+      }
+    }
+  }
+</style>

+ 60 - 0
pages/public/error.vue

@@ -0,0 +1,60 @@
+<!-- 错误界面 -->
+<template>
+  <view class="error-page">
+    <s-empty
+      v-if="errCode === 'NetworkError'"
+      icon="/static/internet-empty.png"
+      text="网络连接失败"
+      showAction
+      actionText="重新连接"
+      @clickAction="onReconnect"
+      buttonColor="#ff3000"
+    />
+    <s-empty
+      v-else-if="errCode === 'TemplateError'"
+      icon="/static/internet-empty.png"
+      text="未找到模板"
+      showAction
+      actionText="重新加载"
+      @clickAction="onReconnect"
+      buttonColor="#ff3000"
+    />
+    <s-empty
+      v-else-if="errCode !== ''"
+      icon="/static/internet-empty.png"
+      :text="errMsg"
+      showAction
+      actionText="重新加载"
+      @clickAction="onReconnect"
+      buttonColor="#ff3000"
+    />
+  </view>
+</template>
+
+<script setup>
+  import { onLoad } from '@dcloudio/uni-app';
+  import { ref } from 'vue';
+  import { ShoproInit } from '@/sheep';
+
+  const errCode = ref('');
+  const errMsg = ref('');
+
+  onLoad((options) => {
+    errCode.value = options.errCode;
+    errMsg.value = options.errMsg;
+  });
+
+  // 重新连接
+  async function onReconnect() {
+    uni.reLaunch({
+      url: '/pages/index/index',
+    });
+    await ShoproInit();
+  }
+</script>
+
+<style lang="scss" scoped>
+  .error-page {
+    width: 100%;
+  }
+</style>

+ 118 - 0
pages/public/faq.vue

@@ -0,0 +1,118 @@
+<!-- FAQ 常见问题 -->
+<template>
+  <s-layout class="set-wrap" title="常见问题" :bgStyle="{ color: '#FFF' }">
+    <uni-collapse>
+      <uni-collapse-item v-for="(item, index) in state.list" :key="item">
+        <template v-slot:title>
+          <view class="ss-flex ss-col-center header">
+            <view class="ss-m-l-20 ss-m-r-20 icon">
+              <view class="rectangle">
+                <view class="num ss-flex ss-row-center ss-col-center">
+                  {{ index + 1 < 10 ? '0' + (index + 1) : index + 1 }}
+                </view>
+              </view>
+              <view class="triangle"> </view>
+            </view>
+            <view class="title ss-m-t-36 ss-m-b-36">
+              {{ item.title }}
+            </view>
+          </view>
+        </template>
+        <view class="content ss-p-l-78 ss-p-r-40 ss-p-b-50 ss-p-t-20">
+          <text class="text">{{ item.content }}</text>
+        </view>
+      </uni-collapse-item>
+    </uni-collapse>
+    <s-empty
+      v-if="state.list.length === 0 && !state.loading"
+      text="暂无常见问题"
+      icon="/static/collect-empty.png"
+    />
+  </s-layout>
+</template>
+
+<script setup>
+  import { onLoad } from '@dcloudio/uni-app';
+  import { reactive } from 'vue';
+  import sheep from '@/sheep';
+
+  const state = reactive({
+    list: [],
+    loading: true,
+  });
+
+  async function getFaqList() {
+    const { error, data } = await sheep.$api.data.faq();
+    if (error === 0) {
+      state.list = data;
+      state.loading = false;
+    }
+  }
+  onLoad(() => {
+    // TODO 芋艿:目前简单做,使用营销文章,作为 faq
+    if (true) {
+      sheep.$router.go('/pages/public/richtext', {
+        title: '常见问题',
+      })
+      return;
+    }
+    getFaqList();
+  });
+</script>
+
+<style lang="scss" scoped>
+  .header {
+    .title {
+      font-size: 28rpx;
+      font-weight: 500;
+      color: #333333;
+      line-height: 30rpx;
+      max-width: 688rpx;
+    }
+
+    .icon {
+      position: relative;
+      width: 40rpx;
+      height: 40rpx;
+
+      .rectangle {
+        position: absolute;
+        left: 0;
+        top: 0;
+        width: 40rpx;
+        height: 36rpx;
+        background: var(--ui-BG-Main);
+        border-radius: 4px;
+
+        .num {
+          width: 100%;
+          height: 100%;
+          font-size: 24rpx;
+          font-weight: 500;
+          color: var(--ui-BG);
+          line-height: 32rpx;
+        }
+      }
+
+      .triangle {
+        width: 0;
+        height: 0;
+        border-left: 4rpx solid transparent;
+        border-right: 4rpx solid transparent;
+        border-top: 8rpx solid var(--ui-BG-Main);
+        position: absolute;
+        left: 16rpx;
+        bottom: -4rpx;
+      }
+    }
+  }
+
+  .content {
+    border-bottom: 1rpx solid #dfdfdf;
+
+    .text {
+      font-size: 26rpx;
+      color: #666666;
+    }
+  }
+</style>

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů