瀏覽代碼

初始化

落日晚风 1 年之前
父節點
當前提交
98baaab9bc
共有 100 個文件被更改,包括 20337 次插入0 次删除
  1. 12 0
      .gitignore
  2. 6 0
      .prettierignore
  3. 10 0
      .prettierrc
  4. 46 0
      App.vue
  5. 21 0
      LICENSE
  6. 190 0
      README.md
  7. 3 0
      androidPrivacy.json
  8. 17 0
      index.html
  9. 9 0
      jsconfig.json
  10. 15 0
      main.js
  11. 240 0
      manifest.json
  12. 105 0
      package.json
  13. 763 0
      pages.json
  14. 507 0
      pages/activity/groupon/detail.vue
  15. 254 0
      pages/activity/groupon/list.vue
  16. 298 0
      pages/activity/groupon/order.vue
  17. 191 0
      pages/activity/index.vue
  18. 249 0
      pages/activity/seckill/list.vue
  19. 77 0
      pages/app/score-shop.vue
  20. 512 0
      pages/app/sign.vue
  21. 63 0
      pages/chat/components/goods.vue
  22. 122 0
      pages/chat/components/order.vue
  23. 151 0
      pages/chat/components/select-popup.vue
  24. 58 0
      pages/chat/emoji.js
  25. 870 0
      pages/chat/index.vue
  26. 821 0
      pages/chat/socket.js
  27. 290 0
      pages/commission/apply.vue
  28. 108 0
      pages/commission/components/account-info.vue
  29. 184 0
      pages/commission/components/commission-auth.vue
  30. 173 0
      pages/commission/components/commission-condition.vue
  31. 126 0
      pages/commission/components/commission-info.vue
  32. 184 0
      pages/commission/components/commission-log.vue
  33. 153 0
      pages/commission/components/commission-menu.vue
  34. 137 0
      pages/commission/goods.vue
  35. 61 0
      pages/commission/index.vue
  36. 417 0
      pages/commission/order.vue
  37. 173 0
      pages/commission/share-log.vue
  38. 257 0
      pages/commission/team.vue
  39. 372 0
      pages/coupon/detail.vue
  40. 261 0
      pages/coupon/list.vue
  41. 209 0
      pages/goods/comment/add.vue
  42. 167 0
      pages/goods/comment/list.vue
  43. 94 0
      pages/goods/components/detail/comment-item.vue
  44. 101 0
      pages/goods/components/detail/detail-activity-tip.vue
  45. 115 0
      pages/goods/components/detail/detail-cell-params.vue
  46. 121 0
      pages/goods/components/detail/detail-cell-service.vue
  47. 31 0
      pages/goods/components/detail/detail-cell-sku.vue
  48. 60 0
      pages/goods/components/detail/detail-cell.vue
  49. 106 0
      pages/goods/components/detail/detail-comment-card.vue
  50. 51 0
      pages/goods/components/detail/detail-content-card.vue
  51. 255 0
      pages/goods/components/detail/detail-navbar.vue
  52. 39 0
      pages/goods/components/detail/detail-progress.vue
  53. 177 0
      pages/goods/components/detail/detail-skeleton.vue
  54. 171 0
      pages/goods/components/detail/detail-tabbar.vue
  55. 137 0
      pages/goods/components/groupon/groupon-card-list.vue
  56. 103 0
      pages/goods/components/list/list-goods-card.vue
  57. 92 0
      pages/goods/components/list/list-navbar.vue
  58. 597 0
      pages/goods/groupon.vue
  59. 384 0
      pages/goods/index.vue
  60. 383 0
      pages/goods/list.vue
  61. 368 0
      pages/goods/score.vue
  62. 534 0
      pages/goods/seckill.vue
  63. 200 0
      pages/index/cart.vue
  64. 236 0
      pages/index/category.vue
  65. 26 0
      pages/index/components/first-one.vue
  66. 66 0
      pages/index/components/first-two.vue
  67. 80 0
      pages/index/components/second-one.vue
  68. 90 0
      pages/index/index.vue
  69. 39 0
      pages/index/login.vue
  70. 52 0
      pages/index/page.vue
  71. 113 0
      pages/index/search.vue
  72. 41 0
      pages/index/user.vue
  73. 318 0
      pages/order/aftersale/apply.vue
  74. 355 0
      pages/order/aftersale/detail.vue
  75. 238 0
      pages/order/aftersale/list.vue
  76. 99 0
      pages/order/aftersale/log-item.vue
  77. 54 0
      pages/order/aftersale/log.vue
  78. 414 0
      pages/order/confirm.vue
  79. 692 0
      pages/order/detail.vue
  80. 84 0
      pages/order/dispatch/content.vue
  81. 104 0
      pages/order/express/list.vue
  82. 174 0
      pages/order/express/log.vue
  83. 329 0
      pages/order/invoice.vue
  84. 586 0
      pages/order/list.vue
  85. 237 0
      pages/pay/components/account-info-modal.vue
  86. 178 0
      pages/pay/components/account-type-select.vue
  87. 356 0
      pages/pay/index.vue
  88. 171 0
      pages/pay/recharge-log.vue
  89. 250 0
      pages/pay/recharge.vue
  90. 285 0
      pages/pay/result.vue
  91. 187 0
      pages/pay/withdraw-log.vue
  92. 380 0
      pages/pay/withdraw.vue
  93. 57 0
      pages/public/error.vue
  94. 110 0
      pages/public/faq.vue
  95. 226 0
      pages/public/feedback.vue
  96. 47 0
      pages/public/richtext.vue
  97. 239 0
      pages/public/setting.vue
  98. 15 0
      pages/public/webview.vue
  99. 261 0
      pages/user/address/edit.vue
  100. 147 0
      pages/user/address/list.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

+ 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"
+}

+ 46 - 0
App.vue

@@ -0,0 +1,46 @@
+<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.

+ 190 - 0
README.md

@@ -0,0 +1,190 @@
+## 简介
+
+![title](https://file.sheepjs.com/www/preview/dcloud/01.png)
+
+<div align="center">
+
+[![star](https://gitee.com/sheepjs/shopro-uniapp/badge/star.svg)](https://gitee.com/sheepjs/shopro-uniapp.git)
+[![fork](https://gitee.com/sheepjs/shopro-uniapp/badge/fork.svg?theme=gvp)](https://gitee.com/sheepjs/shopro-uniapp.git)
+[![version](https://img.shields.io/badge/Shopro-V1.5-brightgreen)](https://gitee.com/sheepjs/shopro-uniapp.git)
+[![license](http://img.shields.io/badge/license-MIT-orange)](https://gitee.com/sheepjs/shopro-uniapp.git)
+
+[官方网站](https://www.shopro.top/) | [H5 演示](http://shopro.sheepjs.com/) | [管理系统](https://shopro.sheepjs.com/admin/) | [问题反馈](https://gitee.com/sheepjs/shopro-uniapp/issues)
+
+</div>
+
+## 特性
+
+![features](https://file.sheepjs.com/www/preview/dcloud/02.png) 
+
+- **支持主题色+自定义头部导航+自定义底部导航**
+- **内含沉浸式头部、通用头部导航示例,支持后端自定义配置底部导航背景和样式**
+- **店铺装修组件(轮播、标题栏、优惠券、商品组、宫格导航、列表导航+广告魔方+富文本、搜索栏等众多组件)**
+- **内置微信公众号分享 jssdk+微信小程序分享卡片+微信 App 分享+海报分享统一封装**
+- **内置微信公众号登录+微信小程序手机号登录+微信 App 开放平台登录+账号密码登录+iOS 登录统一封装**
+- **内置余额支付+微信公众号 jssdk 支付+微信小程序支付+微信 App 支付+支付宝网页支付+支付宝 App 支付统一封装**
+- **支持第三方 cdn 图片资源地址,并支持阿里云、腾讯云、七牛云图片缩放参数**
+- **严格适配多终端场景并支持 App 审核上架**
+
+
+## 技术栈
+
+- **前端技术栈:uni-app、ES6、Vue3、Vite、Pinia;**
+## 安装
+
+```bash
+# 1.克隆项目
+$ git clone https://gitee.com/sheepjs/shopro-uniapp.git
+```
+
+```bash
+# 2.拷贝env示例配置文件 重命名为.env
+$ cd shopro-uniapp
+$ cp env .env 
+```
+
+```bash
+# 3.安装依赖 (需安装nodejs环境, 使用npm国内镜像)
+$ npm install --registry=https://registry.npmmirror.com
+```
+
+```bash
+# 4.使用HbuilderX 运行...
+```
+
+## 体验
+
+![系统架构](https://file.sheepjs.com/www/preview/dcloud/04.png)
+
+客户端演示地址:[https://shopro.sheepjs.com](https://shopro.sheepjs.com)
+
+演示账号: shopro
+
+演示密码: a123456
+
+管理端演示地址:[https://shopro.sheepjs.com/admin/](https://shopro.sheepjs.com/admin/)
+
+演示账号: shopro
+
+演示密码: 123456
+
+_(注意:演示环境已屏蔽管理权限和相关操作)_
+
+
+## 项目结构
+
+```
+├── pages                   // 页面
+│      ├── index            // 入口页面
+│      ├── user             // 用户相关
+│      ├── public           // 公共页面
+│      ├── activity         // 活动页面
+│      ├── app              // 积分、签到页面
+│      ├── chat             // 客服页面
+│      ├── commission       // 分销页面
+│      ├── coupon           // 优惠券页面
+│      ├── goods            // 商品页面
+│      ├── order            // 订单页面
+│      ├── pay              // 支付页面
+├── sheep                   // 底层依赖/工具库
+│      ├── api              // 服务端接口
+│      ├── components       // 自定义功能组件
+│      ├── config           // 配置文件
+│      ├── helper           // 助手函数
+│      ├── hooks            // vue-hooks
+│      ├── libs             // 自定义依赖
+│      ├── platform         // 第三方平台登录、分享、支付
+│      ├── request          // 请求类库
+│      ├── router           // 自定义路由跳转
+│      ├── scss             // 主样式库
+│      ├── store            // pinia状态管理模块
+│      ├── ui               // 自定义UI组件
+│      ├── url              // cdn图片地址格式化
+│      ├── validate         // 通用验证器
+│      ├── index.js         // Shopro入口文件
+├── uni_modules             // dcloud第三方插件
+
+```
+
+
+## 更新
+
+### 近期计划
+
+- [ ] Typescript 重构;
+
+### V1.8.3 更新简介 2023/10/25
+1. 对接微信小程序发货管理
+2. 修复路由模式为history时,微信公众号使用微信登录时跳转白屏bug
+
+### V1.8.2 更新简介 2023/09/4
+1. 添加 图片热区组件
+2. 添加 商品评论商家回复功能
+3. 优化 购物车性能
+4. 优化 搜索组件
+5. 优化 动态添加直播组件
+6. 优化 轮播图组件
+7. 优化 微信小程序订阅消息提醒时机
+8. 优化 移动小程序端客服bug
+9. 优化 h5支付拉起微信或者支付宝客户端时,支付单查询过早的问题
+10. 优化 标题栏组件
+11. 优化 二级分类组件
+12. 优化 规格弹框,手动输入数量无法改变数量问题
+13. 优化 绑定手机号
+14. 重构 瀑布流商品
+15. 重构 小程序快捷登录
+16. 海报图片协议转换,自动识别https协议
+17. 升级依赖版本
+
+### V1.8.1 更新简介 2023/03/18
+
+1. 优化搜索组件
+
+2. 添加多端直播组件,动态加载直播插件
+
+3. 添加多种配送方式(货到付款、手动发货)
+
+4. 添加发货内容详情展示
+
+5. 优化`radio`点击效果bug
+
+6. 商品轮播图添加视频播放
+
+6. 修复部分页面样式显示问题
+
+
+### V1.8.0 更新简介 2023/02/07
+
+1. 引入`luch-request`,替换`libs`中的`request`
+
+2. 兼容`HbulderX`版本更新小程序端`v-bind`无法使用多层对象的问题
+
+3. 优化分页数据相关页面代码
+
+4. 富文本渲染组件使用`mp-html`替换原`su-parse`
+
+5. 修复阶梯拼团弹框点击规格自动关闭问题
+
+6. 自定义页面头部添加返回按钮及快捷菜单
+
+7. 优化筛选时间可以任意选择时间问题(改为只能筛选当天及以前)
+
+8. 修复部分页面样式显示问题
+
+### V1.7.1 更新简介 2022/12/09
+
+1. 更新插件市场忽略文件问题
+
+2. 更改客服聊天图片样式问题
+
+### V1.5 更新简介 2022/12/07
+
+- [x] 服务保障icon 变形问题;
+- [x] 确认订单 可用优惠券逻辑修改;
+- [x] `su-image`组件中`customStyle`添加`width`属性;
+
+---
+
+**<p align="center">如果您觉得我们的开源项目很有帮助,请点击 :star: Star(https://gitee.com/sheepjs/shopro-uniapp.git) 支持 SheepJS 开源团队:heart:</p>**
+
+---

+ 3 - 0
androidPrivacy.json

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

+ 17 - 0
index.html

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

+ 9 - 0
jsconfig.json

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

+ 15 - 0
main.js

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

+ 240 - 0
manifest.json

@@ -0,0 +1,240 @@
+{
+  "name": "星品",
+  "appid": "__UNI__082C0BA",
+  "description": "Shopro是由SheepJS团队开发,使用Uniapp+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": "wx43051b2afa4ed3d0",
+    "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"
+}

+ 105 - 0
package.json

@@ -0,0 +1,105 @@
+{
+  "id": "shopro",
+  "name": "shopro",
+  "displayName": "星品购",
+  "version": "1.0.1",
+  "description": "Shopro-B2C商城,一套代码,同时发行到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",
+    "shopro",
+    "商城模板"
+  ],
+  "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"
+  }
+}

+ 763 - 0
pages.json

@@ -0,0 +1,763 @@
+{
+  "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": "score",
+          "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": "invoice",
+          "style": {
+            "navigationBarTitleText": "发票详情"
+          },
+          "meta": {
+            "auth": true,
+            "title": "发票详情"
+          }
+        },
+        {
+          "path": "dispatch/content",
+          "style": {
+            "navigationBarTitleText": "发货内容"
+          },
+          "meta": {
+            "auth": true,
+            "title": "发货内容"
+          }
+        },
+        {
+          "path": "aftersale/apply",
+          "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": "物流轨迹"
+          }
+        },
+        {
+          "path": "express/list",
+          "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": "invoice/list",
+          "style": {
+            "navigationBarTitleText": "发票管理"
+          },
+          "meta": {
+            "auth": true,
+            "sync": true,
+            "title": "发票管理",
+            "group": "用户中心"
+          }
+        },
+        {
+          "path": "invoice/edit",
+          "style": {
+            "navigationBarTitleText": "编辑发票"
+          },
+          "meta": {
+            "auth": true,
+            "title": "编辑发票"
+          }
+        },
+        {
+          "path": "wallet/money",
+          "style": {
+            "navigationBarTitleText": "我的余额"
+          },
+          "meta": {
+            "auth": true,
+            "sync": true,
+            "title": "用户余额",
+            "group": "用户中心"
+          }
+        },
+        {
+          "path": "wallet/commission",
+          "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": "apply",
+          "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": "share-log",
+          "style": {
+            "navigationBarTitleText": "分享记录"
+          },
+          "meta": {
+            "auth": true,
+            "sync": true,
+            "title": "分享记录",
+            "group": "分销商城"
+          }
+        },
+        {
+          "path": "team",
+          "style": {
+            "navigationBarTitleText": "我的团队"
+          },
+          "meta": {
+            "auth": true,
+            "sync": true,
+            "title": "我的团队",
+            "group": "分销商城"
+          }
+        }
+      ]
+    },
+    {
+      "root": "pages/app",
+      "pages": [
+        {
+          "path": "sign",
+          "style": {
+            "navigationBarTitleText": "签到中心"
+          },
+          "meta": {
+            "auth": true,
+            "sync": true,
+            "title": "签到中心",
+            "group": "应用"
+          }
+        },
+        {
+          "path": "score-shop",
+          "style": {
+            "navigationBarTitleText": "积分商城"
+          },
+          "meta": {
+            "auth": false,
+            "sync": true,
+            "title": "积分商城",
+            "group": "应用"
+          }
+        }
+      ]
+    },
+    {
+      "root": "pages/public",
+      "pages": [
+        {
+          "path": "setting",
+          "style": {
+            "navigationBarTitleText": "系统设置"
+          },
+          "meta": {
+            "sync": true,
+            "title": "系统设置",
+            "group": "通用"
+          }
+        },
+        {
+          "path": "feedback",
+          "style": {
+            "navigationBarTitleText": "问题反馈"
+          },
+          "meta": {
+            "auth": true,
+            "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": "支付"
+          }
+        },
+        {
+          "path": "withdraw",
+          "style": {
+            "navigationBarTitleText": "申请提现"
+          },
+          "meta": {
+            "auth": true,
+            "sync": true,
+            "title": "申请提现",
+            "group": "支付"
+          }
+        },
+        {
+          "path": "withdraw-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": "营销活动"
+          }
+        }
+      ]
+    }
+  ],
+  "globalStyle": {
+    "navigationBarTextStyle": "black",
+    "navigationBarTitleText": "星品购",
+    "navigationBarBackgroundColor": "#FFFFFF",
+    "backgroundColor": "#FFFFFF",
+    "navigationStyle": "custom"
+  },
+  "tabBar": {
+    "list": [
+      {
+        "pagePath": "pages/index/index"
+      },
+      {
+        "pagePath": "pages/index/cart"
+      },
+      {
+        "pagePath": "pages/index/user"
+      }
+    ]
+  }
+}

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

@@ -0,0 +1,507 @@
+<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.goods"
+        :style="[
+          {
+            marginTop: '-' + Number(statusBarHeight + 88) + 'rpx',
+            paddingTop: Number(statusBarHeight + 108) + 'rpx',
+          },
+        ]"
+      >
+        <s-goods-item
+          class="goods-box"
+          :img="state.data.goods.image"
+          :title="state.data.goods.title"
+          :price="state.data.goods.price[0]"
+          priceColor="#E1212B"
+          @tap="
+            sheep.$router.go('/pages/goods/groupon', {
+              id: state.data.goods.id,
+              activity_id: state.data.goods.activity.id,
+            })
+          "
+          :style="[{ top: Number(statusBarHeight + 108) + 'rpx' }]"
+        >
+          <template #groupon>
+            <view class="ss-flex">
+              <view class="sales-title">{{ state.data.num }}人团</view>
+              <view class="num-title ss-m-l-20">已拼{{ state.data.goods.sales }}件</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.status === 'finish' || state.data.status === 'finish_fictitious'">
+          <view v-if="state.data.my">
+            <view class="countdown-title ss-flex">
+              <text class="cicon-check-round"></text>
+              恭喜您~拼团成功
+            </view>
+          </view>
+          <view v-else>
+            <view class="countdown-title ss-flex">
+              <text class="cicon-info"></text>
+              抱歉~该团已满员
+            </view>
+          </view>
+        </view>
+        <view v-if="state.data.status === 'invalid'">
+          <view class="countdown-title ss-flex">
+            <text class="cicon-info"></text>
+            {{ state.data.my ? '拼团超时,已自动退款' : '该团已解散' }}
+          </view>
+        </view>
+        <view v-if="state.data.status === 'ing'">
+          <!-- TODO: 拼团进行中+活动结束-->
+          <view v-if="state.data.activity_status === 'ended'">
+            <view class="countdown-title ss-flex">
+              <text class="cicon-info"></text>
+              拼团已结束,请关注下次活动
+            </view>
+          </view>
+
+          <view class="countdown-title ss-flex" v-if="state.data.activity_status === 'ing'">
+            还差
+            <view class="num">{{ state.data.num - state.data.current_num }}人</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"
+            v-for="item in state.data.groupon_logs"
+            :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.number" :key="item">
+            <image
+              :src="sheep.$url.static('/static/img/shop/avatar/unknown.png')"
+              class="avatar-img"
+            ></image>
+          </view>
+        </view>
+        <view
+          class="detail-cell-wrap ss-flex ss-col-center ss-row-between"
+          v-if="state.data.activity?.richtext_id > 0"
+          @tap="
+            sheep.$router.go('/pages/public/richtext', {
+              id: state.data.activity.richtext_id,
+              title: state.data.activity.richtext_title,
+            })
+          "
+        >
+          <view class="label-text">玩法</view>
+          <view class="ss-flex">
+            <view class="cell-content ss-line-1 ss-flex-1">
+              {{ state.data.activity?.richtext_title }}
+            </view>
+            <button class="ss-reset-button">
+              <text class="_icon-forward right-forwrad-icon"></text>
+            </button>
+          </view>
+        </view>
+      </view>
+      <view
+        v-if="
+          state.data.status == 'finish' ||
+          state.data.status == 'finish_fictitious' ||
+          state.data.status == 'invalid'
+        "
+        class="ss-m-t-40 ss-flex ss-row-center"
+      >
+        <button
+          class="ss-reset-button order-btn"
+          v-if="state.data.my"
+          @tap="onDetail(state.data.my.order_id)"
+        >
+          查看订单
+        </button>
+        <button class="ss-reset-button join-btn" v-else @tap="onCreateGroupon"> 我要开团 </button>
+      </view>
+      <view v-if="state.data.status === 'ing'" class="ss-m-t-40 ss-flex ss-row-center">
+        <view v-if="state.data.activity_status === 'ended'">
+          <button
+            class="ss-reset-button join-btn"
+            v-if="state.data.my"
+            @tap="onDetail(state.data.my.order_id)"
+          >
+            查看订单
+          </button>
+          <button
+            class="ss-reset-button disabled-btn"
+            v-else
+            disabled
+            @tap="onDetail(state.data.my.order_id)"
+          >
+            去参团
+          </button>
+        </view>
+        <view v-else class="ss-flex ss-row-center">
+          <view v-if="state.data.my">
+            <button
+              class="ss-reset-button join-btn"
+              :disabled="state.data.activity_status === 'ing' && endTime.ms <= 0"
+              @tap="onShare"
+            >
+              邀请好友来拼团
+            </button>
+          </view>
+          <view v-else>
+            <button
+              class="ss-reset-button join-btn"
+              :disabled="state.data.activity_status === 'ing' && endTime.ms <= 0"
+              @tap="onJoinGroupon()"
+            >
+              立即参团
+            </button>
+          </view>
+        </view>
+      </view>
+
+      <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-empty>
+  </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';
+
+  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,
+  });
+
+  const shareInfo = computed(() => {
+    if (isEmpty(state.data)) return {};
+    return sheep.$platform.share.getShareInfo(
+      {
+        title: state.data.goods.title,
+        image: sheep.$url.cdn(state.data.goods.image),
+        desc: state.data.goods.subtitle,
+        params: {
+          page: '5',
+          query: state.data.id,
+        },
+      },
+      {
+        type: 'groupon', // 邀请拼团海报
+        title: state.data.goods.title, // 商品标题
+        image: sheep.$url.cdn(state.data.goods.image), // 商品主图
+        price: state.data.goods.price[0], // 商品价格
+        original_price: state.data.goods.original_price, // 商品原价
+      },
+    );
+  });
+
+  // 订单详情
+  function onDetail(orderId) {
+    sheep.$router.go('/pages/order/detail', {
+      id: orderId,
+    });
+  }
+
+  //去开团
+  function onCreateGroupon() {
+    state.grouponAction = 'create';
+    state.grouponId = 0;
+    state.showSelectSku = true;
+  }
+
+  // 规格变更
+  function onSkuChange(e) {
+    state.selectedSkuPrice = e;
+  }
+
+  // 立即参团
+  function onJoinGroupon() {
+    state.grouponAction = 'join';
+    state.grouponId = state.data.id;
+    state.grouponNum = state.data.num;
+    state.showSelectSku = true;
+  }
+
+  // 立即购买
+  function onBuy(e) {
+    sheep.$router.go('/pages/order/confirm', {
+      data: JSON.stringify({
+        order_type: 'goods',
+        buy_type: 'groupon',
+        activity_id: state.data.activity.id,
+        groupon_id: state.grouponId,
+        groupon_num: state.grouponNum,
+        goods_list: [
+          {
+            goods_id: e.goods_id,
+            goods_num: e.goods_num,
+            goods_sku_price_id: e.id,
+          },
+        ],
+      }),
+    });
+  }
+
+  const endTime = computed(() => {
+    return useDurationTime(state.data.expire_time);
+  });
+
+  // 获取拼团团队详情
+  async function getGrouponDetail(id) {
+    const { error, data } = await sheep.$api.activity.grouponDetail(id);
+    if (error === 0) {
+      state.data = data;
+      let number = Number(state.data.num - state.data.current_num);
+      state.number = number > 0 ? number : 0;
+    } else {
+      state.data = null;
+    }
+    state.loading = false;
+  }
+
+  function onShare() {
+    showShareModal();
+  }
+
+  onLoad((options) => {
+    getGrouponDetail(options.id);
+  });
+</script>
+
+<style lang="scss" scoped>
+  .recharge-box {
+    position: relative;
+    margin-bottom: 120rpx;
+    background: v-bind(headerBg) center/750rpx 100%
+        no-repeat,
+      linear-gradient(115deg, #f44739 0%, #ff6600 100%);
+    border-radius: 0 0 5% 5%;
+    height: 100rpx;
+
+    .goods-box {
+      width: 710rpx;
+      border-radius: 20rpx;
+      position: absolute;
+      left: 20rpx;
+      box-sizing: border-box;
+    }
+
+    .sales-title {
+      height: 32rpx;
+      background: rgba(#ffe0e2, 0.29);
+      border-radius: 16rpx;
+      font-size: 24rpx;
+      font-weight: 400;
+      padding: 6rpx 20rpx;
+      color: #f7979c;
+    }
+
+    .num-title {
+      font-size: 24rpx;
+      font-weight: 400;
+      color: #999999;
+    }
+  }
+
+  .countdown-time {
+    font-size: 26rpx;
+    font-weight: 500;
+    color: #383a46;
+    .countdown-h {
+      font-size: 24rpx;
+      font-family: OPPOSANS;
+      font-weight: 500;
+      color: #ffffff;
+      padding: 0 4rpx;
+      margin-left: 16rpx;
+      height: 40rpx;
+      background: linear-gradient(90deg, #ff6000 0%, #fe832a 100%);
+      border-radius: 6rpx;
+    }
+    .countdown-num {
+      font-size: 24rpx;
+      font-family: OPPOSANS;
+      font-weight: 500;
+      color: #ffffff;
+      width: 40rpx;
+      height: 40rpx;
+      background: linear-gradient(90deg, #ff6000 0%, #fe832a 100%);
+      border-radius: 6rpx;
+    }
+  }
+
+  .countdown-box {
+    // height: 364rpx;
+    background: #ffffff;
+    border-radius: 10rpx;
+    box-sizing: border-box;
+
+    .countdown-title {
+      font-size: 28rpx;
+      font-weight: 500;
+      color: #333333;
+
+      .cicon-check-round {
+        color: #42b111;
+        margin-right: 24rpx;
+      }
+
+      .cicon-info {
+        color: #d71e08;
+        margin-right: 24rpx;
+      }
+
+      .num {
+        color: #ff6000;
+      }
+    }
+
+    .header-avatar {
+      width: 86rpx;
+      height: 86rpx;
+      background: #ececec;
+      border-radius: 50%;
+      border: 4rpx solid #edc36c;
+      position: relative;
+      box-sizing: border-box;
+
+      .avatar-img {
+        width: 100%;
+        height: 100%;
+        border-radius: 50%;
+      }
+
+      .header-tag {
+        width: 72rpx;
+        height: 36rpx;
+        font-size: 24rpx;
+        line-height: nor;
+        background: linear-gradient(132deg, #f3dfb1, #f3dfb1, #ecbe60);
+        border-radius: 16rpx;
+        position: absolute;
+        left: 4rpx;
+        top: -36rpx;
+      }
+    }
+    .default-avatar {
+      width: 86rpx;
+      height: 86rpx;
+      background: #ececec;
+      border-radius: 50%;
+      .avatar-img {
+        width: 100%;
+        height: 100%;
+        border-radius: 50%;
+      }
+    }
+
+    .user-avatar {
+      width: 86rpx;
+      height: 86rpx;
+      background: #ececec;
+      border-radius: 50%;
+    }
+  }
+  .order-btn {
+    width: 668rpx;
+    height: 70rpx;
+    border: 2rpx solid #dfdfdf;
+    border-radius: 35rpx;
+    color: #999999;
+    font-weight: 500;
+    font-size: 26rpx;
+    line-height: normal;
+  }
+
+  .disabled-btn {
+    width: 668rpx;
+    height: 70rpx;
+    background: #dddddd;
+    border-radius: 35rpx;
+    color: #999999;
+    font-weight: 500;
+    font-size: 28rpx;
+    line-height: normal;
+  }
+
+  .join-btn {
+    width: 668rpx;
+    height: 70rpx;
+    background: linear-gradient(90deg, #ff6000 0%, #fe832a 100%);
+    box-shadow: 0px 8rpx 6rpx 0px rgba(255, 104, 4, 0.22);
+    border-radius: 35rpx;
+    color: #fff;
+    font-weight: 500;
+    font-size: 28rpx;
+    line-height: normal;
+  }
+
+  .detail-cell-wrap {
+    width: 100%;
+    padding: 10rpx 20rpx;
+    box-sizing: border-box;
+    border-top: 2rpx solid #dfdfdf;
+    background-color: #fff;
+    // min-height: 60rpx;
+
+    .label-text {
+      font-size: 28rpx;
+      font-weight: 400;
+    }
+
+    .cell-content {
+      font-size: 28rpx;
+      font-weight: 500;
+      color: $dark-6;
+    }
+
+    .right-forwrad-icon {
+      font-size: 28rpx;
+      font-weight: 500;
+      color: $dark-9;
+    }
+  }
+</style>

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

@@ -0,0 +1,254 @@
+<template>
+  <s-layout navbar="inner" :bgStyle="{ color: '#FE832A' }">
+    <view
+      class="page-bg"
+      :style="[{ marginTop: '-' + Number(statusBarHeight + 88) + 'rpx' }]"
+    ></view>
+    <view class="list-content">
+      <view class="content-header ss-flex-col ss-col-center ss-row-center">
+        <view class="content-header-title ss-m-b-22 ss-flex ss-row-center">
+          <view>{{ state.activityInfo.title }}</view>
+          <!-- <view class="more">更多</view> -->
+        </view>
+        <view class="content-header-box ss-flex ss-row-center">
+          <view class="countdown-box ss-flex" v-if="endTime?.ms > 0 && state.activityInfo">
+            <view class="countdown-title ss-m-r-12">距结束</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="" v-if="endTime?.ms < 0 && state.activityInfo"> 活动已结束 </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="item in state.pagination.data" :key="item.id">
+          <s-goods-column
+            class=""
+            size="lg"
+            :data="item"
+            :grouponTag="true"
+            @click="
+              sheep.$router.go('/pages/goods/groupon', {
+                id: item.id,
+                activity_id: state.activityId,
+              })
+            "
+          >
+            <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, computed } from 'vue';
+  import { onLoad, onReachBottom } from '@dcloudio/uni-app';
+  import sheep from '@/sheep';
+  import _ from 'lodash';
+  import { useDurationTime } from '@/sheep/hooks/useGoods';
+
+  const { screenHeight, safeAreaInsets, screenWidth, safeArea } = sheep.$platform.device;
+  const sys_navBar = sheep.$platform.navbar;
+  const statusBarHeight = sheep.$platform.device.statusBarHeight * 2;
+  const pageHeight =
+    (safeArea.height + safeAreaInsets.bottom) * 2 + statusBarHeight - sys_navBar - 350;
+  const headerBg = sheep.$url.css('/static/img/shop/goods/groupon-header.png');
+
+  const state = reactive({
+    activityId: 0,
+    pagination: {
+      data: [],
+      current_page: 1,
+      total: 1,
+      last_page: 1,
+    },
+    loadStatus: '',
+    activityInfo: {},
+  });
+  // 倒计时
+  const endTime = computed(() => {
+    if (state.activityInfo.end_time) {
+      return useDurationTime(state.activityInfo.end_time);
+    }
+  });
+
+  async function getList(activityId, page = 1, list_rows = 4) {
+    state.loadStatus = 'loading';
+    const res = await sheep.$api.goods.activityList({
+      list_rows,
+      activity_id: activityId,
+      page,
+    });
+    if (res.error === 0) {
+      if (page >= 2) {
+        let couponList = _.concat(state.pagination.data, res.data.data);
+        state.pagination = {
+          ...res.data,
+          data: couponList,
+        };
+      } else {
+        state.pagination = res.data;
+      }
+      if (state.pagination.current_page < state.pagination.last_page) {
+        state.loadStatus = 'more';
+      } else {
+        state.loadStatus = 'noMore';
+      }
+    }
+  }
+  async function getActivity(id) {}
+  // 加载更多
+  function loadmore() {
+    if (state.loadStatus !== 'noMore') {
+      getList(state.activityId, state.pagination.current_page + 1);
+    }
+  }
+  // 上拉加载更多
+  onReachBottom(() => {
+    loadmore();
+  });
+  onLoad(async (options) => {
+    if (!options.id) {
+      state.activityInfo = null;
+      return;
+    }
+    state.activityId = options.id;
+    getList(state.activityId);
+    const { error, data } = await sheep.$api.activity.activity(options.id);
+    if (error === 0) {
+      state.activityInfo = data;
+    } else {
+      state.activityInfo = null;
+    }
+  });
+</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: 150rpx;
+      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;
+        }
+      }
+      .content-header-box {
+        width: 678rpx;
+        height: 64rpx;
+        background: rgba($color: #fff, $alpha: 0.66);
+        border-radius: 32px;
+        .num {
+          font-size: 24rpx;
+          font-family: OPPOSANS;
+          font-weight: 500;
+          color: #f51c11;
+          line-height: 30rpx;
+        }
+        .title {
+          font-size: 24rpx;
+          font-weight: 400;
+          font-family: OPPOSANS;
+          color: #333;
+          line-height: 30rpx;
+        }
+        .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: 50rpx;
+          line-height: 50rpx;
+          padding: 0 20rpx;
+          border-radius: 25rpx;
+          font-size: 24rpx;
+          color: #fff;
+          background: linear-gradient(90deg, #ff6600 0%, #fe832a 100%);
+        }
+      }
+    }
+  }
+</style>

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

@@ -0,0 +1,298 @@
+<!-- 页面 -->
+<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"> </s-empty>
+    <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="order in state.pagination.data"
+        :key="order.id"
+      >
+        <view class="order-card-header ss-flex ss-col-center ss-row-between ss-p-x-20">
+          <view class="order-no">订单号:{{ order.my.order.order_sn }}</view>
+          <view
+            class="ss-font-26"
+            :class="
+              order.status === 'ing'
+                ? 'warning-color'
+                : order.status === 'invalid'
+                ? 'danger-color'
+                : 'success-color'
+            "
+            >{{ order.status_text }}</view
+          >
+        </view>
+        <view class="border-bottom">
+          <s-goods-item
+            :img="order.goods.image"
+            :title="order.goods.title"
+            :price="order.goods.price[0]"
+            priceColor="#E1212B"
+            radius="20"
+          >
+            <template #groupon>
+              <view class="ss-flex">
+                <view class="sales-title"> {{ order.num }}人团 </view>
+                <!-- <view class="num-title ss-m-l-20"> 已拼{{ order.goods.sales }}件 </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: order.my.order_id })"
+          >
+            订单详情
+          </button>
+          <button
+            class="tool-btn ss-reset-button"
+            :class="{ 'ui-BG-Main-Gradient': order.status === 'ing' }"
+            @tap="sheep.$router.go('/pages/activity/groupon/detail', { id: order.id })"
+          >
+            {{ order.status === 'ing' ? '邀请拼团' : '拼团详情' }}
+          </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 { computed, reactive } from 'vue';
+  import { onLoad, onReachBottom, onPullDownRefresh } from '@dcloudio/uni-app';
+  import sheep from '@/sheep';
+  import _ from 'lodash';
+
+  // 数据
+  const state = reactive({
+    currentTab: 0,
+    pagination: {
+      data: [],
+      current_page: 1,
+      total: 1,
+      last_page: 1,
+    },
+    loadStatus: '',
+    deleteOrderId: 0,
+  });
+
+  const tabMaps = [
+    {
+      name: '全部',
+      value: 'all',
+    },
+    {
+      name: '进行中',
+      value: 'ing',
+    },
+    {
+      name: '拼团成功',
+      value: 'finish',
+    },
+    {
+      name: '拼团失败',
+      value: 'invalid',
+    },
+  ];
+
+  // 切换选项卡
+  function onTabsChange(e) {
+    state.pagination = {
+      data: [],
+      current_page: 1,
+      total: 1,
+      last_page: 1,
+    };
+    state.currentTab = e.index;
+    getGrouponList();
+  }
+
+  // 订单详情
+  function onDetail(orderSN) {
+    sheep.$router.go('/pages/order/detail', {
+      orderSN,
+    });
+  }
+
+  // 继续支付
+  function onPay(orderSN) {
+    sheep.$router.go('/pages/pay/index', {
+      orderSN,
+    });
+  }
+
+  // 评价
+  function onComment(orderSN) {
+    sheep.$router.go('/pages/order/comment/add', {
+      orderSN,
+    });
+  }
+
+  // 确认收货
+  async function onConfirm(orderId) {
+    const { error, data } = await sheep.$api.order.confirm(orderId);
+    if (error === 0) {
+      let index = state.pagination.data.findIndex((order) => order.id === orderId);
+      state.pagination.data[index] = data;
+    }
+  }
+
+  // 取消订单
+  async function onCancel(orderId) {
+    const { error, data } = await sheep.$api.order.cancel(orderId);
+    if (error === 0) {
+      let index = state.pagination.data.findIndex((order) => order.id === orderId);
+      state.pagination.data[index] = data;
+    }
+  }
+
+  // 获取订单列表
+  async function getGrouponList(page = 1, list_rows = 5) {
+    state.loadStatus = 'loading';
+    let res = await sheep.$api.activity.myGroupon({
+      type: tabMaps[state.currentTab].value,
+    });
+    if (res.error === 0) {
+      if (page >= 2) {
+        let orderList = _.concat(state.pagination.data, res.data.data);
+        state.pagination = {
+          ...res.data,
+          data: orderList,
+        };
+      } else {
+        state.pagination = res.data;
+      }
+
+      if (state.pagination.current_page < state.pagination.last_page) {
+        state.loadStatus = 'more';
+      } else {
+        state.loadStatus = 'noMore';
+      }
+    }
+  }
+
+  onLoad((options) => {
+    if (options.type) {
+      state.currentTab = options.type;
+    }
+    getGrouponList();
+  });
+
+  // 加载更多
+  function loadmore() {
+    if (state.loadStatus !== 'noMore') {
+      getGrouponList(state.pagination.current_page + 1);
+    }
+  }
+
+  // 上拉加载更多
+  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>

+ 191 - 0
pages/activity/index.vue

@@ -0,0 +1,191 @@
+<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">{{ state.activityInfo.type_text }}:</view>
+        <view class="ss-flex-1">
+          <view class="tip-content" v-for="item in state.activityInfo.texts" :key="item">
+            {{ 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"> </button>
+            </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';
+
+  const state = reactive({
+    pagination: {
+      data: [],
+      current_page: 1,
+      total: 1,
+      last_page: 1,
+    },
+    loadStatus: '',
+    leftGoodsList: [],
+    rightGoodsList: [],
+    activityId: 0,
+    activityInfo: {},
+  });
+  // 加载瀑布流
+  let count = 0;
+  let leftHeight = 0;
+  let rightHeight = 0;
+
+  function mountMasonry(height = 0, where = 'left') {
+    if (!state.pagination.data[count]) return;
+
+    if (where === 'left') {
+      leftHeight += height;
+    } else {
+      rightHeight += height;
+    }
+    if (leftHeight <= rightHeight) {
+      state.leftGoodsList.push(state.pagination.data[count]);
+    } else {
+      state.rightGoodsList.push(state.pagination.data[count]);
+    }
+    count++;
+  }
+  async function getList(activityId, page = 1, list_rows = 6) {
+    state.loadStatus = 'loading';
+    const res = await sheep.$api.goods.activityList({
+      list_rows,
+      activity_id: activityId,
+      page,
+    });
+    if (res.error === 0) {
+      if (page >= 2) {
+        let couponList = _.concat(state.pagination.data, res.data.data);
+        state.pagination = {
+          ...res.data,
+          data: couponList,
+        };
+      } else {
+        state.pagination = res.data;
+      }
+      mountMasonry();
+      if (state.pagination.current_page < state.pagination.last_page) {
+        state.loadStatus = 'more';
+      } else {
+        state.loadStatus = 'noMore';
+      }
+    }
+  }
+  async function getActivity(id) {
+    const { error, data } = await sheep.$api.activity.activity(id);
+    if (error === 0) {
+      state.activityInfo = data;
+    }
+  }
+  // 加载更多
+  function loadmore() {
+    if (state.loadStatus !== 'noMore') {
+      getList(state.activityId, state.pagination.current_page + 1);
+    }
+  }
+  // 上拉加载更多
+  onReachBottom(() => {
+    loadmore();
+  });
+  onLoad((options) => {
+    state.activityId = options.activityId;
+    getList(state.activityId);
+    getActivity(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>

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

@@ -0,0 +1,249 @@
+<template>
+  <s-layout navbar="inner" :bgStyle="{ color: 'rgb(245,28,19)' }">
+    <view
+      class="page-bg"
+      :style="[{ marginTop: '-' + Number(statusBarHeight + 88) + 'rpx' }]"
+    ></view>
+    <view class="list-content">
+      <view class="content-header ss-flex-col ss-col-center ss-row-center">
+        <view class="content-header-title ss-m-b-22 ss-flex ss-row-center">
+          <view>{{ state.activityInfo.title }}</view>
+          <!-- <view class="more">更多</view> -->
+        </view>
+        <view class="content-header-box ss-flex ss-row-center">
+          <view class="countdown-box ss-flex" v-if="endTime?.ms > 0 && state.activityInfo">
+            <view class="countdown-title ss-m-r-12">距结束</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="" v-if="endTime?.ms < 0 && state.activityInfo"> 活动已结束 </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="item in state.pagination.data" :key="item.id">
+          <s-goods-column
+            class=""
+            size="lg"
+            :data="item"
+            :seckillTag="true"
+            @click="
+              sheep.$router.go('/pages/goods/seckill', {
+                id: item.id,
+                activity_id: state.activityId,
+              })
+            "
+          >
+            <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, computed } from 'vue';
+  import { onLoad, onReachBottom } from '@dcloudio/uni-app';
+  import sheep from '@/sheep';
+  import _ from 'lodash';
+  import { useDurationTime } from '@/sheep/hooks/useGoods';
+
+  const { screenHeight, safeAreaInsets, screenWidth, safeArea } = sheep.$platform.device;
+  const sys_navBar = sheep.$platform.navbar;
+  const statusBarHeight = sheep.$platform.device.statusBarHeight * 2;
+  const pageHeight =
+    (safeArea.height + safeAreaInsets.bottom) * 2 + statusBarHeight - sys_navBar - 350;
+  const headerBg = sheep.$url.css('/static/img/shop/goods/seckill-header.png');
+
+  const state = reactive({
+    activityId: 0,
+    pagination: {
+      data: [],
+      current_page: 1,
+      total: 1,
+      last_page: 1,
+    },
+    loadStatus: '',
+    activityInfo: {},
+  });
+  // 倒计时
+  const endTime = computed(() => {
+    if (state.activityInfo.end_time) {
+      return useDurationTime(state.activityInfo.end_time);
+    }
+  });
+
+  async function getList(activityId, page = 1, list_rows = 4) {
+    state.loadStatus = 'loading';
+    const res = await sheep.$api.goods.activityList({
+      list_rows,
+      activity_id: activityId,
+      page,
+    });
+    if (res.error === 0) {
+        let couponList = _.concat(state.pagination.data, res.data.data);
+        state.pagination = {
+          ...res.data,
+          data: couponList,
+        };
+      if (state.pagination.current_page < state.pagination.last_page) {
+        state.loadStatus = 'more';
+      } else {
+        state.loadStatus = 'noMore';
+      }
+    }
+  }
+  async function getActivity(id) {}
+  // 加载更多
+  function loadmore() {
+    if (state.loadStatus !== 'noMore') {
+      getList(state.activityId, state.pagination.current_page + 1);
+    }
+  }
+  // 上拉加载更多
+  onReachBottom(() => {
+    loadmore();
+  });
+  onLoad(async (options) => {
+    if (!options.id) {
+      state.activityInfo = null;
+      return;
+    }
+    state.activityId = options.id;
+    getList(state.activityId);
+    const { error, data } = await sheep.$api.activity.activity(options.id);
+    if (error === 0) {
+      state.activityInfo = data;
+    } else {
+      state.activityInfo = null;
+    }
+  });
+</script>
+<style lang="scss" scoped>
+  .page-bg {
+    width: 100%;
+    height: 458rpx;
+    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: 150rpx;
+      background: linear-gradient(180deg, #fff4f7, #ffe6ec);
+      .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;
+        }
+      }
+      .content-header-box {
+        width: 678rpx;
+        height: 64rpx;
+        background: rgba($color: #fff, $alpha: 0.66);
+        border-radius: 32px;
+        .num {
+          font-size: 24rpx;
+          font-family: OPPOSANS;
+          font-weight: 500;
+          color: #f51c11;
+          line-height: 30rpx;
+        }
+        .title {
+          font-size: 24rpx;
+          font-weight: 400;
+          font-family: OPPOSANS;
+          color: #333;
+          line-height: 30rpx;
+        }
+        .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: 50rpx;
+          line-height: 50rpx;
+          padding: 0 20rpx;
+          border-radius: 25rpx;
+          font-size: 24rpx;
+          color: #fff;
+          background: linear-gradient(90deg, #ff6600 0%, #fe832a 100%);
+        }
+      }
+    }
+  }
+</style>

+ 77 - 0
pages/app/score-shop.vue

@@ -0,0 +1,77 @@
+<!-- 页面  -->
+<template>
+  <s-layout title="积分商城">
+    <view class="ss-p-20">
+      <view v-for="item in state.pagination.data" :key="item.id" class="ss-m-b-20">
+        <s-score-card
+          size="sl"
+          :data="item"
+          priceColor="#FF3000"
+          @tap="sheep.$router.go('/pages/goods/score', { id: item.id })"
+        ></s-score-card>
+      </view>
+    </view>
+    <s-empty
+      v-if="state.pagination.total === 0"
+      icon="/static/goods-empty.png"
+      text="暂无积分商品"
+    ></s-empty>
+    <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';
+
+  const state = reactive({
+    pagination: {
+      data: [],
+      current_page: 1,
+      total: 1,
+      last_page: 1,
+    },
+    loadStatus: '',
+  });
+  async function getData(page = 1, list_rows = 5) {
+    state.loadStatus = 'loading';
+    let res = await sheep.$api.app.scoreShop.list({
+      list_rows,
+      page,
+    });
+    if (res.error === 0) {
+        let couponlist = _.concat(state.pagination.data, res.data.data);
+        state.pagination = {
+          ...res.data,
+          data: couponlist,
+        };
+      if (state.pagination.current_page < state.pagination.last_page) {
+        state.loadStatus = 'more';
+      } else {
+        state.loadStatus = 'noMore';
+      }
+    }
+  }
+  // 加载更多
+  function loadmore() {
+    if (state.loadStatus !== 'noMore') {
+      getData(state.pagination.current_page + 1);
+    }
+  }
+
+  // 上拉加载更多
+  onReachBottom(() => {
+    loadmore();
+  });
+  onLoad(() => {
+    getData();
+  });
+</script>

+ 512 - 0
pages/app/sign.vue

@@ -0,0 +1,512 @@
+<!-- 页面  -->
+<template>
+  <s-layout title="签到有礼">
+    <view v-if="state.loading"></view>
+    <view class="sign-wrap" v-else-if="state.data && !state.loading">
+      <!-- 签到日历 -->
+      <view class="content-box calendar">
+        <view class="sign-everyday ss-flex ss-col-center ss-row-between ss-p-x-30">
+          <text class="sign-everyday-title">签到日历</text>
+          <view class="sign-num-box">
+            已连续签到
+            <text class="sign-num">{{ state.continue_days }}</text>
+            天
+          </view>
+        </view>
+
+        <!-- 切换年月 -->
+        <view class="bar ss-flex ss-col-center ss-row-center">
+          <view class="previous" @tap="handleCalendar(0)"><text class="cicon-back"></text></view>
+          <view class="date ss-m-x-20">{{ state.cur_year || '--' }} 年 {{ state.cur_month || '--' }} 月</view>
+          <view class="next" @tap="handleCalendar(1)"><text class="cicon-forward"></text></view>
+        </view>
+
+        <!-- 显示星期 -->
+        <view class="week ss-flex">
+          <view class="week-item ss-flex ss-row-center" v-for="(item, index) in state.weeks_ch" :key="index">
+            {{ item.title }}
+          </view>
+        </view>
+
+        <!-- 日历表 -->
+        <view class="myDateTable">
+          <view v-for="(item, j) in state.data.days" :key="j" class="dateCell ss-flex ss-row-center ss-col-center">
+            <!-- 空格 -->
+            <view class="ss-flex ss-row-center ss-col-center">
+              <text :decode="true">&nbsp;&nbsp;</text>
+            </view>
+            <view>
+              <!-- 已签到日期 -->
+              <view v-if="item.is_sign" class="is-sign ss-flex ss-row-center">
+                <view class="is-sign-num">{{ item.day < 10 ? '0' + item.day : item.day }}</view>
+                    <image class="is-sign-image" :src="sheep.$url.static('/static/img/shop/app/correct.png')">
+                    </image>
+                </view>
+                <!-- 未签到日期 -->
+                <view class="is-sign ss-flex ss-row-center" v-if="item.is_replenish == 1"
+                  @tap="onShowRetroactive(item.date)">
+                  <view class="cell-num">{{ item.day < 10 ? '0' + item.day : item.day }}</view>
+                      <text class="cicon-title"></text>
+                  </view>
+                  <view class="is-sign ss-flex ss-row-center" v-if="item.is_replenish == 0 && !item.is_sign">
+                    <view class="cell-num">{{ item.day < 10 ? '0' + item.day : item.day }}</view>
+                    </view>
+                  </view>
+                </view>
+
+                <!-- 签到按钮 -->
+                <view class="ss-flex ss-col-center ss-row-center sign-box ss-m-y-40">
+                  <button class="ss-reset-button sign-btn" v-if="state.isSign === 0" @tap="onSign">签到</button>
+                  <button class="ss-reset-button already-btn" v-if="state.isSign === 1" disabled>已签到</button>
+                </view>
+              </view>
+            </view>
+            <view class="bg-white ss-m-t-16 ss-p-t-30 ss-p-b-60 ss-p-x-40">
+              <view class="activity-title ss-m-b-30">签到说明</view>
+              <view class="activity-des">
+                1、每日签到固定 {{ state.data.rules.everyday }} 积分
+                <text v-if="state.data.rules.is_inc == '1'">
+                  ,次日递增奖励 {{ state.data.rules.inc_num }} 积分,直到
+                  {{ state.data.rules.until_day }} 天之后不再增加
+                </text>
+              </view>
+              <view class="activity-des" v-if="state.data.rules.discounts?.length > 0">
+                2、<text class="" v-for="i in state.data.rules.discounts" :key="i">
+                  连续签到 {{ i.full }} 天,奖励 {{ i.value }} 积分;
+                </text>
+              </view>
+              <view class="activity-des" v-if="state.data.rules.is_replenish == '1'">
+                {{ state.data.rules.discounts?.length > 0 ? '3' : '2' }}、用户在
+                {{ state.data.rules.replenish_limit }} 天内,可补签
+                {{ state.data.rules.replenish_days }} 天,每次补签消耗
+                {{ state.data.rules.replenish_num }}积分
+              </view>
+            </view>
+          </view>
+          <s-empty v-else-if="!state.data && !state.loading" icon="/static/data-empty.png" text="签到活动还未开始">
+          </s-empty>
+          <su-popup :show="state.showModel" type="center" round="10" :isMaskClick="false">
+            <view class="model-box ss-flex-col">
+              <view class="ss-m-t-56 ss-flex-col ss-col-center">
+                <text class="cicon-check-round"></text>
+                <view class="score-title">{{ state.signin.score }}积分</view>
+                <view class="model-title ss-flex ss-col-center ss-m-t-22 ss-m-b-30">
+                  已连续打卡{{ state.continue_days }}天
+                </view>
+              </view>
+              <view class="model-bg ss-flex-col ss-col-center ss-row-right">
+                <view class="title ss-m-b-64">签到成功</view>
+                <view class="ss-m-b-40">
+                  <button class="ss-reset-button confirm-btn" @tap="onConfirm">确认</button>
+                </view>
+              </view>
+            </view>
+          </su-popup>
+          <su-popup :show="state.showRetroactive" type="center" round="10" :isMaskClick="false">
+            <view class="model-box ss-flex-col">
+              <view class="ss-m-t-56 ss-flex-col ss-col-center">
+                <text class="cicon-check-round"></text>
+                <view class="score-title">消耗{{ state.data?.rules.replenish_num }}积分</view>
+                <view class="model-title ss-flex ss-col-center ss-m-t-22 ss-m-b-30">
+                  已连续打卡{{ state.continue_days }}天
+                </view>
+              </view>
+              <view class="model-bg ss-flex-col ss-col-center ss-row-right">
+                <view class="title ss-m-b-64">确认补签</view>
+                <view class="ss-m-b-40 ss-flex">
+                  <button class="ss-reset-button cancel-btn" @tap="state.showRetroactive = false">取消</button>
+                  <button class="ss-reset-button confirm-btn" @tap="onRetroactive">确认</button>
+                </view>
+              </view>
+            </view>
+          </su-popup>
+  </s-layout>
+</template>
+
+<script setup>
+import sheep from '@/sheep';
+import { onLoad, onReady } from '@dcloudio/uni-app';
+import { computed, reactive } from 'vue';
+
+const headerBg = sheep.$url.css('/static/img/shop/app/sign.png');
+
+const state = reactive({
+  data: {
+    days: [], //日历
+    rules: {}, //规则
+  },
+  cur_year: 0, //当前选的年
+  cur_month: 0, //当前选的月
+  cur_day: 0, //当前选择的天
+  weeks_ch: [
+    {
+      title: '日',
+      value: '0',
+    },
+    {
+      title: '一',
+      value: '1',
+    },
+    {
+      title: '二',
+      value: '2',
+    },
+    {
+      title: '三',
+      value: '3',
+    },
+    {
+      title: '四',
+      value: '4',
+    },
+    {
+      title: '五',
+      value: '5',
+    },
+    {
+      title: '六',
+      value: '6',
+    },
+  ], //星期
+  showModel: false, //签到弹框
+  continue_days: 0, //连续签到天数
+  signin: {}, // 签到
+  showRetroactive: false, //补签弹框
+  date: '', //补签选中日期
+  isSign: 0, //今天是否签到
+  loading: true,
+});
+async function onSign() {
+  const { error, data } = await sheep.$api.activity.signAdd();
+  if (error === 0) {
+    state.showModel = true;
+    state.signin = data;
+    // getData();
+  }
+}
+
+function onShowRetroactive(e) {
+  state.showRetroactive = true;
+  state.date = e;
+}
+//签到确认刷新页面
+function onConfirm() {
+  state.showModel = false;
+  getData();
+}
+//补签
+async function onRetroactive() {
+  const { error, data } = await sheep.$api.activity.replenish({
+    date: state.date,
+  });
+  if (error === 0) {
+    state.showRetroactive = false;
+    getData();
+  }
+}
+
+async function getData(mouth) {
+  const { error, data } = await sheep.$api.activity.signList(mouth);
+  if (error === 0) {
+    state.data = data;
+  } else {
+    state.data = null;
+  }
+  state.loading = false;
+  if (state.data) {
+    state.data.days.forEach((i, index) => {
+      if (index < i.week) {
+        index++;
+        var obj = {
+          day: null,
+          is_sign: false,
+        };
+        state.data.days.unshift(obj);
+      }
+      if (index == 1) {
+        let arr = i.date.split('-');
+        state.cur_year = arr[0];
+        state.cur_month = arr[1];
+      }
+    });
+    if (state.data.days[0].day == null) {
+      state.data.days.forEach((i, index) => {
+        if (i.current == 'today') {
+          state.isSign = i.is_sign;
+        }
+      });
+    }
+    state.continue_days = data.continue_days;
+  }
+}
+
+onReady(() => {
+  getData();
+});
+
+// 切换控制年月,上一个月,下一个月
+const handleCalendar = (type) => {
+  const cur_year = parseInt(state.cur_year);
+  const cur_month = parseInt(state.cur_month);
+  var newMonth;
+  var newYear = cur_year;
+  if (type === 0) {
+    //上个月
+    newMonth = cur_month - 1;
+    if (newMonth < 1) {
+      newYear = cur_year - 1;
+      newMonth = 12;
+    } else if (newMonth < 10) {
+      newMonth = '0' + newMonth;
+    }
+  } else {
+    newMonth = cur_month + 1;
+    if (newMonth > 12) {
+      newYear = cur_year + 1;
+      newMonth = 1;
+    } else if (newMonth < 10) {
+      newMonth = '0' + newMonth;
+    }
+  }
+  getData({
+    month: newYear + '-' + newMonth,
+  });
+};
+</script>
+
+<style lang="scss" scoped>
+.header-box {
+  border-top: 2rpx solid rgba(#dfdfdf, 0.5);
+}
+
+// 日历
+.calendar {
+  background: #fff;
+
+  .sign-everyday {
+    height: 100rpx;
+    background: rgba(255, 255, 255, 1);
+    border: 2rpx solid rgba(223, 223, 223, 0.4);
+
+    .sign-everyday-title {
+      font-size: 32rpx;
+      color: rgba(51, 51, 51, 1);
+      font-weight: 500;
+    }
+
+    .sign-num-box {
+      font-size: 26rpx;
+      font-weight: 500;
+      color: rgba(153, 153, 153, 1);
+
+      .sign-num {
+        font-size: 30rpx;
+        font-weight: 600;
+        color: #ff6000;
+        padding: 0 10rpx;
+        font-family: OPPOSANS;
+      }
+    }
+  }
+
+  // 年月日
+  .bar {
+    height: 100rpx;
+
+    .date {
+      font-size: 30rpx;
+      font-family: OPPOSANS;
+      font-weight: 500;
+      color: #333333;
+      line-height: normal;
+    }
+  }
+
+  .cicon-back {
+    margin-top: 6rpx;
+    font-size: 30rpx;
+    color: #c4c4c4;
+    line-height: normal;
+  }
+
+  .cicon-forward {
+    margin-top: 6rpx;
+    font-size: 30rpx;
+    color: #c4c4c4;
+    line-height: normal;
+  }
+
+  // 星期
+  .week {
+    .week-item {
+      font-size: 24rpx;
+      font-weight: 500;
+      color: rgba(153, 153, 153, 1);
+      flex: 1;
+    }
+  }
+
+  // 日历表
+  .myDateTable {
+    display: flex;
+    flex-wrap: wrap;
+
+    .dateCell {
+      width: calc(750rpx / 7);
+      height: 80rpx;
+      font-size: 26rpx;
+      font-weight: 400;
+      color: rgba(51, 51, 51, 1);
+    }
+  }
+}
+
+.is-sign {
+  width: 48rpx;
+  height: 48rpx;
+  position: relative;
+
+  .is-sign-num {
+    font-size: 24rpx;
+    font-family: OPPOSANS;
+    font-weight: 500;
+    line-height: normal;
+  }
+
+  .is-sign-image {
+    position: absolute;
+    left: 0;
+    top: 0;
+    width: 48rpx;
+    height: 48rpx;
+  }
+}
+
+.cell-num {
+  font-size: 24rpx;
+  font-family: OPPOSANS;
+  font-weight: 500;
+  color: #333333;
+  line-height: normal;
+}
+
+.cicon-title {
+  position: absolute;
+  right: -10rpx;
+  top: -6rpx;
+  font-size: 20rpx;
+  color: red;
+}
+
+// 签到按钮
+.sign-box {
+  height: 140rpx;
+  width: 100%;
+
+  .sign-btn {
+    width: 710rpx;
+    height: 80rpx;
+    border-radius: 35rpx;
+    font-size: 30rpx;
+    font-weight: 500;
+    box-shadow: 0 0.2em 0.5em rgba(#ff6000, 0.4);
+    background: linear-gradient(90deg, #ff6000, #fe832a);
+    color: #fff;
+  }
+
+  .already-btn {
+    width: 710rpx;
+    height: 80rpx;
+    border-radius: 35rpx;
+    font-size: 30rpx;
+    font-weight: 500;
+  }
+}
+
+.model-box {
+  width: 520rpx;
+  // height: 590rpx;
+  background: linear-gradient(177deg, #ff6000 0%, #fe832a 100%);
+  // background: linear-gradient(177deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
+  border-radius: 10rpx;
+
+  .cicon-check-round {
+    font-size: 70rpx;
+    color: #fff;
+  }
+
+  .score-title {
+    font-size: 34rpx;
+    font-family: OPPOSANS;
+    font-weight: 500;
+    color: #fcff00;
+  }
+
+  .model-title {
+    font-size: 28rpx;
+    font-weight: 500;
+    color: #ffffff;
+  }
+
+  .model-bg {
+    width: 520rpx;
+    height: 344rpx;
+    background-size: 100% 100%;
+    background-image: v-bind(headerBg);
+    background-repeat: no-repeat;
+    border-radius: 0 0 10rpx 10rpx;
+
+    .title {
+      font-size: 34rpx;
+      font-weight: bold;
+      // color: var(--ui-BG-Main);
+      color: #ff6000;
+    }
+
+    .subtitle {
+      font-size: 26rpx;
+      font-weight: 500;
+      color: #999999;
+    }
+
+    .cancel-btn {
+      width: 220rpx;
+      height: 70rpx;
+      border: 2rpx solid #ff6000;
+      border-radius: 35rpx;
+      font-size: 28rpx;
+      font-weight: 500;
+      color: #ff6000;
+      line-height: normal;
+      margin-right: 10rpx;
+    }
+
+    .confirm-btn {
+      width: 220rpx;
+      height: 70rpx;
+      background: linear-gradient(90deg, #ff6000, #fe832a);
+      box-shadow: 0 0.2em 0.5em rgba(#ff6000, 0.4);
+      border-radius: 35rpx;
+      font-size: 28rpx;
+      font-weight: 500;
+      color: #ffffff;
+      line-height: normal;
+    }
+  }
+}
+
+//签到说明
+.activity-title {
+  font-size: 32rpx;
+  font-weight: 500;
+  color: #333333;
+  line-height: normal;
+}
+
+.activity-des {
+  font-size: 26rpx;
+  font-weight: 500;
+  color: #666666;
+  line-height: 40rpx;
+}
+</style>

+ 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>

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

@@ -0,0 +1,151 @@
+<template>
+  <su-popup :show="show" showClose round="10" backgroundColor="#eee" @close="emits('close')">
+    <view class="select-popup">
+      <view class="title">
+        <span>{{ mode == 'goods' ? '我的浏览' : '我的订单' }}</span>
+      </view>
+      <scroll-view
+        class="scroll-box"
+        scroll-y="true"
+        :scroll-with-animation="true"
+        :show-scrollbar="false"
+        @scrolltolower="loadmore"
+      >
+        <view
+          class="item"
+          v-for="item in state.pagination.data"
+          :key="item"
+          @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 { onLoad } from '@dcloudio/uni-app';
+  import sheep from '@/sheep';
+  import _ from 'lodash';
+  import GoodsItem from './goods.vue';
+  import OrderItem from './order.vue';
+
+  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 sheep.$api.user.view.list({
+            page,
+            list_rows,
+          })
+        : await sheep.$api.order.list({
+            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,
+  };
+}

+ 290 - 0
pages/commission/apply.vue

@@ -0,0 +1,290 @@
+<!-- 申请分销商  -->
+<template>
+  <s-layout title="申请分销商" class="apply-wrap" navbar="inner">
+    <s-empty
+      v-if="state.error === 1"
+      paddingTop="0"
+      icon="/static/comment-empty.png"
+      text="未开启分销商申请"
+    ></s-empty>
+
+    <view v-if="state.error === 0" class="distribution-apply-wrap">
+      <view class="apply-header">
+        <view class="header-box ss-flex">
+          <image
+            class="bg-img"
+            :src="sheep.$url.cdn(state.background)"
+            mode="widthFix"
+            @load="onImgLoad"
+          ></image>
+          <view class="heaer-title">申请分销商</view>
+        </view>
+      </view>
+      <view class="apply-box bg-white" :style="{ marginTop: state.imgHeight + 'rpx' }">
+        <uni-forms
+          label-width="200"
+          :model="state.model"
+          :rules="state.rules"
+          border
+          class="form-box"
+        >
+          <view class="item-box">
+            <uni-forms-item
+              v-for="(item, index) in state.formList"
+              :key="index"
+              :label="item.name"
+              :required="true"
+              :label-position="item.type == 'image' ? 'top' : 'left'"
+            >
+              <uni-easyinput
+                v-if="item.type !== 'image'"
+                :inputBorder="false"
+                :type="item.type"
+                :styles="{ disableColor: '#fff' }"
+                placeholderStyle="color:#BBBBBB;font-size:28rpx;line-height:normal"
+                v-model="item.value"
+                :placeholder="`请填写${item.name}`"
+              />
+              <s-uploader
+                v-if="item.type === 'image'"
+                v-model:url="item.value"
+                fileMediatype="image"
+                limit="1"
+                mode="grid"
+                :imageStyles="{ width: '168rpx', height: '168rpx' }"
+                class="file-picker"
+              />
+            </uni-forms-item>
+          </view>
+        </uni-forms>
+        <label class="ss-flex ss-m-t-20" v-if="state.protocol?.status == 1" @tap="onChange">
+          <radio
+            :checked="state.isAgree"
+            color="var(--ui-BG-Main)"
+            style="transform: scale(0.6)"
+            @tap.stop="onChange"
+          />
+          <view class="agreement-text ss-flex">
+            <view class="ss-m-r-4">勾选代表同意</view>
+            <view
+              class="tcp-text"
+              @tap.stop="
+                sheep.$router.go('/pages/public/richtext', {
+                  id: state.protocol.id,
+                  title: state.protocol.title,
+                })
+              "
+            >
+              《{{ state.protocol.title }}》
+            </view>
+          </view>
+        </label>
+        <su-fixed bottom placeholder>
+          <view class="submit-box ss-flex ss-row-center ss-p-30">
+            <button class="submit-btn ss-reset-button ui-BG-Main ui-Shadow-Main" @tap="submit">
+              {{ submitText }}
+            </button>
+          </view>
+        </su-fixed>
+      </view>
+    </view>
+  </s-layout>
+</template>
+
+<script setup>
+  import sheep from '@/sheep';
+  import { onLoad } from '@dcloudio/uni-app';
+  import { computed, reactive } from 'vue';
+  import { isEmpty } from 'lodash';
+
+  const state = reactive({
+    error: -1,
+    status: '-',
+    config: {},
+    isAgree: false,
+    formList: [],
+    protocol: {},
+    applyInfo: [],
+    background: '',
+    imgHeight: 400,
+  });
+  const statusBarHeight = sheep.$platform.device.statusBarHeight * 2;
+
+  //勾选协议
+  function onChange() {
+    state.isAgree = !state.isAgree;
+  }
+
+  const submitText = computed(() => {
+    if (state.status === 'normal') return '修改信息';
+    if (state.status === 'needinfo') return '提交审核';
+    if (state.status === 'reject') return '重新提交';
+    return '';
+  });
+
+  async function getAgentForm() {
+    const { error, data } = await sheep.$api.commission.form();
+    state.error = error;
+    if (error === 0) {
+      state.status = data.status;
+      state.background = data.background;
+      state.formList = data.form;
+      state.applyInfo = data.applyInfo;
+      state.protocol = data.protocol;
+
+      if (data.protocol.status != 1) {
+        state.isAgree = true;
+      }
+      mergeFormList();
+    }
+  }
+  function onImgLoad(e) {
+    state.imgHeight = (e.detail.height / e.detail.width) * 750 - 88 - statusBarHeight;
+  }
+
+  async function submit() {
+    if (!state.isAgree) {
+      sheep.$helper.toast('请同意申请协议');
+      return;
+    }
+
+    const validate = state.formList.every((item) => {
+      if (isEmpty(item.value)) {
+        if (item.type !== 'image') {
+          sheep.$helper.toast(`请填写${item.name}`);
+        } else {
+          sheep.$helper.toast(`请上传${item.name}`);
+        }
+        return false;
+      }
+      return true;
+    });
+
+    if (!validate) {
+      return;
+    }
+
+    const { error } = await sheep.$api.commission.apply({
+      data: state.formList,
+    });
+    if (error === 0) {
+      sheep.$router.back();
+    }
+  }
+
+  onLoad(() => {
+    getAgentForm();
+  });
+
+  // 初始化formData
+  function mergeFormList() {
+    state.formList.forEach((form) => {
+      const apply = state.applyInfo.find(
+        (info) => info.type === form.type && info.name === form.name,
+      );
+      if (typeof apply !== 'undefined') form.value = apply.value;
+    });
+  }
+</script>
+
+<style lang="scss" scoped>
+  :deep() {
+    .uni-forms-item__label .label-text {
+      font-size: 28rpx !important;
+      color: #333333 !important;
+      line-height: normal !important;
+    }
+
+    .file-picker__progress {
+      height: 0 !important;
+    }
+
+    .uni-list-item__content-title {
+      font-size: 28rpx !important;
+      color: #333333 !important;
+      line-height: normal !important;
+    }
+
+    .uni-icons {
+      font-size: 40rpx !important;
+    }
+
+    .is-disabled {
+      color: #333333;
+    }
+  }
+
+  .distribution-apply-wrap {
+    // height: 100vh;
+    // width: 100vw;
+    // position: absolute;
+    // left: 0;
+    // top: 0;
+    // background-color: #fff;
+    // overflow-y: auto;
+
+    .submit-btn {
+      width: 690px;
+      height: 86rpx;
+      border-radius: 43rpx;
+    }
+    .apply-header {
+      position: absolute;
+      left: 0;
+      top: 0;
+    }
+    .header-box {
+      width: 100%;
+      position: relative;
+      .bg-img {
+        width: 750rpx;
+      }
+
+      .heaer-title {
+        position: absolute;
+        left: 30rpx;
+        top: 50%;
+        transform: translateY(-50%);
+        font-size: 50rpx;
+        font-weight: bold;
+        color: #ffffff;
+        z-index: 11;
+
+        &::before {
+          content: '';
+          width: 51rpx;
+          height: 8rpx;
+          background: #ffffff;
+          border-radius: 4rpx;
+          position: absolute;
+          z-index: 12;
+          bottom: -20rpx;
+        }
+      }
+    }
+
+    .apply-box {
+      padding: 0 40rpx;
+
+      .item-box {
+        border-bottom: 2rpx solid #eee;
+      }
+    }
+  }
+
+  .agreement-text {
+    font-size: 24rpx;
+    color: #c4c4c4;
+    line-height: normal;
+
+    .tcp-text {
+      color: var(--ui-BG-Main);
+    }
+  }
+
+  .card-image {
+    width: 140rpx;
+    height: 140rpx;
+    border-radius: 50%;
+  }
+</style>

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

@@ -0,0 +1,108 @@
+<!-- 账户  -->
+<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"
+            ></uni-icons>
+          </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"></text>
+        </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 ? agentInfo.total_income || '0.00' : '***' }}
+          </view>
+        </view>
+        <view class="ss-flex-1 ss-flex-col ss-col-center">
+          <view class="item-title">我的佣金(元)</view>
+          <view class="item-detail">
+            {{ state.showMoney ? userInfo.commission || '0.00' : '***' }}
+          </view>
+        </view>
+        <view class="ss-flex-1 ss-flex-col ss-col-center">
+          <view class="item-title">我的消费(元)</view>
+          <view class="item-detail">
+            {{ state.showMoney ? userInfo.total_consume || '0.00' : '***' }}
+          </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 agentInfo = computed(() => sheep.$store('user').agentInfo);
+
+  const state = reactive({
+    showMoney: false,
+  });
+</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>

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

@@ -0,0 +1,184 @@
+<!-- 页面  -->
+<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(state.event.image)"
+          mode="aspectFill"
+        ></image>
+      </view>
+      <view class="notice-title">{{ state.event.title }}</view>
+      <view class="notice-detail">{{ state.event.subtitle }}</view>
+      <button
+        class="ss-reset-button notice-btn ui-Shadow-Main ui-BG-Main-Gradient"
+        @tap="onTap(state.event.action)"
+      >
+        {{ state.event.button }}
+      </button>
+      <button class="ss-reset-button back-btn" @tap="sheep.$router.back()"> 返回 </button>
+    </view>
+  </su-popup>
+</template>
+
+<script setup>
+  import sheep from '@/sheep';
+  import { reactive, watch } from 'vue';
+
+  const props = defineProps({
+    error: {
+      type: Number,
+      default: 0,
+    },
+  });
+  const emits = defineEmits(['getAgentInfo']);
+  const state = reactive({
+    event: {},
+    show: false,
+  });
+
+  watch(
+    () => props.error,
+    (error) => {
+      if (error !== 0 && error !== 100) {
+        state.event = eventMap[error];
+        state.show = true;
+      }
+    },
+  );
+
+  async function onTap(eventName) {
+    switch (eventName) {
+      case 'back': // 返回
+        sheep.$router.back();
+        break;
+      case 'apply': // 需提交资料
+        sheep.$router.go('/pages/commission/apply');
+        break;
+      case 'reApply': // 直接重新申请
+        let { error } = await sheep.$api.commission.apply();
+        if (error === 0) {
+          emits('getAgentInfo');
+        }
+        break;
+    }
+  }
+  const eventMap = {
+    // 关闭
+    101: {
+      image: '/static/img/shop/commission/close.png',
+      title: '分销中心已关闭',
+      subtitle: '该功能暂不可用',
+      button: '知道了',
+      action: 'back',
+    },
+    // 禁用
+    102: {
+      image: '/static/img/shop/commission/forbidden.png',
+      title: '账户已被禁用',
+      subtitle: '该功能暂不可用',
+      button: '知道了',
+      action: 'back',
+    },
+    // 补充信息
+    103: {
+      image: '/static/img/shop/commission/apply.png',
+      title: '待完善信息',
+      subtitle: '请补充您的信息后提交审核',
+      button: '完善信息',
+      action: 'apply',
+    },
+    // 审核中
+    104: {
+      image: '/static/img/shop/commission/pending.png',
+      title: '正在审核中',
+      subtitle: '请耐心等候结果',
+      button: '知道了',
+      action: 'back',
+    },
+    // 重新提交
+    105: {
+      image: '/static/img/shop/commission/reject.png',
+      title: '抱歉!您的申请信息未通过',
+      subtitle: '请尝试修改后重新提交',
+      button: '重新申请',
+      action: 'apply',
+    },
+    // 直接重新申请
+    106: {
+      image: '/static/img/shop/commission/reject.png',
+      title: '抱歉!您的申请未通过',
+      subtitle: '请尝试重新申请',
+      button: '重新申请',
+      action: 'reApply',
+    },
+    // 冻结
+    107: {
+      image: '/static/img/shop/commission/freeze.png',
+      title: '抱歉!您的账户已被冻结',
+      subtitle: '如有疑问请联系客服',
+      button: '联系客服',
+      action: 'chat',
+    },
+  };
+</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>

+ 173 - 0
pages/commission/components/commission-condition.vue

@@ -0,0 +1,173 @@
+<template>
+  <su-popup
+    :show="state.show"
+    type="bottom"
+    round="10"
+    :isMaskClick="false"
+    :backgroundImage="sheep.$url.css('/static/img/shop/commission/become-agent.png')"
+    @close="show = false"
+    backgroundColor="var(--ui-BG-Main)"
+  >
+    <view class="model-box ss-flex ss-row-center">
+      <view class="content">
+        <scroll-view
+          class="scroll-box"
+          scroll-y="true"
+          :scroll-with-animation="true"
+          :show-scrollbar="false"
+        >
+          <view v-if="errorData.type === 'goods'">
+            <view class="item-box ss-m-b-20" v-for="item in errorData.value" :key="item.id">
+              <s-goods-item :title="item.title" :img="item.image" :price="item.price[0]" priceColor="#E1212B" @tap="sheep.$router.go('/pages/goods/index', { id: item.id })">
+                <template #groupon>
+                  <view class="item-box-subtitle">{{ item.subtitle }}</view>
+                </template>
+              </s-goods-item>
+            </view>
+          </view>
+
+          <s-goods-item
+            title="累计消费满"
+            price=""
+            :img="sheep.$url.static('/static/img/shop/commission/consume.png')"
+            v-else-if="errorData.type === 'consume'"
+          >
+            <template #groupon>
+              <view class="ss-flex">
+                <view class="progress-box ss-flex">
+                  <view
+                    class="progerss-active"
+                    :style="{
+                      width: state.percent < 10 ? '10%' : state.percent + '%',
+                    }"
+                  ></view>
+                </view>
+                <view class="progress-title ss-m-l-10">{{ errorData.value }}元</view>
+              </view>
+              <view class="progress-title ss-m-t-20">{{ userInfo.total_consume }}元</view>
+            </template>
+          </s-goods-item>
+        </scroll-view>
+        <view class="content-des" v-if="errorData.type === 'goods'"
+          >* 购买指定商品即可成为分销商</view
+        >
+        <view class="content-des" v-else-if="errorData.type === 'consume'"
+          >* 满足累计消费即可成为分销商</view
+        >
+      </view>
+      <button class="ss-reset-button go-btn ui-BG-Main-Gradient" @tap="sheep.$router.back()">
+        返回
+      </button>
+    </view>
+  </su-popup>
+</template>
+<script setup>
+  import sheep from '@/sheep';
+  import { computed, reactive, watch } from 'vue';
+  import { onLoad } from '@dcloudio/uni-app';
+
+  const props = defineProps({
+    error: {
+      type: Number,
+      default: 0,
+    },
+    errorData: {
+      type: Object,
+      default() {},
+    },
+  });
+
+  const userInfo = computed(() => sheep.$store('user').userInfo);
+
+  const state = reactive({
+    percent: computed(() => {
+      if (props.errorData.type !== 'consume') {
+        return 0;
+      }
+      let percent = (userInfo.value.total_consume / props.errorData.value) * 100;
+      return parseInt(percent);
+    }),
+    show: false,
+    money: '',
+  });
+
+  watch(
+    () => props.error,
+    (error) => {
+      if (error == 100) {
+        state.show = true;
+      }
+    },
+  );
+</script>
+<style lang="scss" scoped>
+  :deep() {
+    .ss-goods-item-warp {
+      background-color: #f8f8f8 !important;
+    }
+  }
+
+  .progress-title {
+    font-size: 20rpx;
+    font-weight: 500;
+    color: #666666;
+  }
+
+  .progress-box {
+    flex: 1;
+    height: 18rpx;
+    background: #e7e7e7;
+    border-radius: 9rpx;
+  }
+
+  .progerss-active {
+    height: 24rpx;
+    background: linear-gradient(90deg, #ff6000 0%, #fe832a 100%);
+    border-radius: 12rpx;
+  }
+
+  .model-box {
+    padding: 140rpx 40rpx 60rpx 40rpx;
+    height: 916rpx;
+    box-sizing: border-box;
+    position: relative;
+
+    .content {
+      height: 720rpx;
+      width: 612rpx;
+      padding-top: 30rpx;
+      // background-color: #fff;
+      box-sizing: border-box;
+
+      .content-des {
+        margin-top: 20rpx;
+        font-size: 24rpx;
+        font-weight: 500;
+        color: #999999;
+        text-align: center;
+      }
+    }
+
+    .scroll-box {
+      height: 620rpx;
+    }
+    .item-box-subtitle {
+      font-size: 24rpx;
+      font-weight: 500;
+      color: #999999;
+      line-height: normal;
+    }
+
+    .go-btn {
+      width: 600rpx;
+      height: 70rpx;
+      position: absolute;
+      left: 50%;
+      transform: translateX(-50%);
+      bottom: 120rpx;
+      border-radius: 35rpx;
+      font-size: 28rpx;
+      font-weight: 500;
+    }
+  }
+</style>

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

@@ -0,0 +1,126 @@
+<!-- 分销商信息  -->
+<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 class="user-info-box ss-flex">
+            <view class="tag-box ss-flex" v-if="agentInfo.level_info">
+              <image
+                v-if="agentInfo.level_info?.image"
+                class="tag-img"
+                :src="sheep.$url.cdn(agentInfo.level_info?.image)"
+                mode="aspectFill"
+              >
+              </image>
+              <text class="tag-title">{{ agentInfo.level_info?.name }}</text>
+            </view>
+            <view class="ss-iconfont uicon-arrow-right" style="color: #fff; font-size: 28rpx">
+            </view>
+          </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 agentInfo = computed(() => sheep.$store('user').agentInfo);
+  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>

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

@@ -0,0 +1,184 @@
+<!-- 分销明细  -->
+<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"></text>
+      </view>
+    </view>
+    <scroll-view
+      scroll-y="true"
+      @scrolltolower="loadmore"
+      class="scroll-box log-scroll"
+      scroll-with-animation="true"
+    >
+      <view v-if="state.pagination.data">
+        <view
+          class="log-item-box ss-flex ss-row-between"
+          v-for="item in state.pagination.data"
+          :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
+                  v-if="item.oper_type === 'user'"
+                  class="log-img"
+                  :src="sheep.$url.cdn(item.oper?.avatar)"
+                  mode="aspectFill"
+                ></image>
+                <image
+                  v-else-if="item.oper_type === 'admin'"
+                  class="log-img"
+                  :src="sheep.$url.static('/static/img/shop/avatar/default_user.png')"
+                  mode="aspectFill"
+                ></image>
+                <image
+                  v-else
+                  class="log-img"
+                  :src="sheep.$url.static('/static/img/shop/avatar/notice.png')"
+                  mode="aspectFill"
+                ></image>
+              </view>
+              <view class="log-text ss-ellipsis-1">{{ item.remark }}</view>
+            </view>
+          </view>
+          <text class="log-time">{{ dayjs(item.create_time).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 { computed, reactive } from 'vue';
+  import _ from 'lodash';
+  import dayjs from 'dayjs';
+
+  const state = reactive({
+    loadStatus: '',
+    pagination: {
+      data: [],
+      current_page: 1,
+      total: 1,
+      last_page: 1,
+    },
+  });
+
+  async function getLog(page = 1) {
+    const res = await sheep.$api.commission.log({
+      page,
+    });
+    if (res.error === 0) {
+      let list = _.concat(state.pagination.data, res.data.data);
+      state.pagination = {
+        ...res.data,
+        data: list,
+      };
+      if (state.pagination.current_page < state.pagination.last_page) {
+        state.loadStatus = 'more';
+      } else {
+        state.loadStatus = 'noMore';
+      }
+    }
+  }
+  getLog();
+
+  // 加载更多
+  function loadmore() {
+    if (state.loadStatus !== 'noMore') {
+      getLog(state.pagination.current_page + 1);
+    }
+  }
+</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>

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

@@ -0,0 +1,153 @@
+<!-- 分销商菜单栏 -->
+<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>
+
+    <!-- <uni-grid :column="4" :showBorder="false" :highlight="false">
+      <uni-grid-item
+        v-for="(item, index) in state.menuList"
+        :index="index"
+        :key="index"
+        @tap="sheep.$router.go(item.path)"
+      >
+        <view class="grid-item-box ss-flex ss-flex-col ss-row-center ss-col-center">
+          <image
+            class="menu-icon ss-m-b-10"
+            :src="sheep.$url.static(item.img)"
+            mode="aspectFill"
+          ></image>
+          <text class="menu-title">{{ item.title }}</text>
+        </view>
+      </uni-grid-item>
+    </uni-grid> -->
+  </view>
+</template>
+
+<script setup>
+  import sheep from '@/sheep';
+  import { onLoad } from '@dcloudio/uni-app';
+  import { computed, 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/user/wallet/commission',
+      },
+      {
+        img: '/static/img/shop/commission/commission_icon3.png',
+        title: '分销订单',
+        path: '/pages/commission/order',
+      },
+      {
+        img: '/static/img/shop/commission/commission_icon4.png',
+        title: '推广商品',
+        path: '/pages/commission/goods',
+      },
+      {
+        img: '/static/img/shop/commission/commission_icon5.png',
+        title: '我的资料',
+        path: '/pages/commission/apply',
+        isAgentFrom: true,
+      },
+      {
+        img: '/static/img/shop/commission/commission_icon7.png',
+        title: '邀请海报',
+        path: 'action:showShareModal',
+      },
+      {
+        img: '/static/img/shop/commission/commission_icon8.png',
+        title: '分享记录',
+        path: '/pages/commission/share-log',
+      },
+    ],
+  });
+</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>

+ 137 - 0
pages/commission/goods.vue

@@ -0,0 +1,137 @@
+<!-- 页面  -->
+<template>
+  <s-layout title="推广商品" :onShareAppMessage="state.shareInfo">
+    <view class="goods-item ss-m-20" v-for="item in state.pagination.data" :key="item.id">
+      <s-goods-item
+        size="lg"
+        :img="item.image"
+        :title="item.title"
+        :subTitle="item.subtitle"
+        :price="item.price[0]"
+        :originPrice="item.original_price"
+        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">预计佣金:¥{{ item.commission }}</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="暂无推广商品"
+    ></s-empty>
+    <!-- 加载更多 -->
+    <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 { computed, reactive } from 'vue';
+  import _ from 'lodash';
+  import { showShareModal } from '@/sheep/hooks/useModal';
+
+  const state = reactive({
+    pagination: {
+      data: [],
+      current_page: 1,
+      total: 1,
+      last_page: 1,
+    },
+    loadStatus: '',
+    shareInfo: {},
+  });
+
+  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(page = 1, list_rows = 8) {
+    state.loadStatus = 'loading';
+    let res = await sheep.$api.commission.goods({
+      list_rows,
+      page,
+    });
+    if (res.error === 0) {
+      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';
+      }
+    }
+  }
+
+  onLoad(async () => {
+    getGoodsList();
+  });
+
+  // 加载更多
+  function loadmore() {
+    if (state.loadStatus !== 'noMore') {
+      getGoodsList(state.pagination.current_page + 1);
+    }
+  }
+
+  // 上拉加载更多
+  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>

+ 61 - 0
pages/commission/index.vue

@@ -0,0 +1,61 @@
+<!-- 分销中心  -->
+<template>
+  <s-layout navbar="inner" class="index-wrap" title="分销中心" :bgStyle="bgStyle" onShareAppMessage>
+    <!-- 分销商信息 -->
+    <commission-info />
+    <!-- 账户信息 -->
+    <account-info />
+    <!-- 菜单栏 -->
+    <commission-menu />
+    <!-- 分销记录 -->
+    <commission-log />
+    <!-- 弹框 -->
+    <commission-condition :error="state.error" :errorData="state.errorData" />
+
+    <!-- 权限 -->
+    <commission-auth :error="state.error" @getAgentInfo="getAgentInfo" />
+  </s-layout>
+</template>
+
+<script setup>
+  import sheep from '@/sheep';
+  import { onShow } from '@dcloudio/uni-app';
+  import { computed, 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';
+  import commissionCondition from './components/commission-condition.vue';
+
+  const state = reactive({
+    error: 0,
+    errorData: {},
+    config: {
+      background: '/storage/default/20220704/29ac76a3c9d0d983200d612e45a052ca.png',
+    },
+  });
+
+  const agentInfo = computed(() => sheep.$store('user').agentInfo);
+
+  const bgStyle = {
+    color: '#F7D598',
+  };
+
+  async function getAgentInfo() {
+    const { error, data } = await sheep.$store('user').getAgentInfo();
+    if (error !== 0) {
+      state.error = error;
+      state.errorData = data;
+    }
+  }
+  onShow(() => {
+    getAgentInfo();
+  });
+</script>
+
+<style lang="scss" scoped>
+  :deep(.page-main) {
+    background-size: 100% 100% !important;
+  }
+</style>

+ 417 - 0
pages/commission/order.vue

@@ -0,0 +1,417 @@
+<!-- 分销订单  -->
+<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">
+        <view class="data-card">
+          <view class="total-item">
+            <view class="item-title">团队订单数量(单)</view>
+            <view class="total-num">
+              {{ state.agentInfo.child_order_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">
+                {{ state.agentInfo.child_order_count_1 || 0 }}
+              </view>
+            </view>
+            <view class="ss-flex-1">
+              <view class="item-title">二级订单</view>
+              <view class="category-num">
+                {{ state.agentInfo.child_order_count_2 || 0 }}
+              </view>
+            </view>
+          </view>
+        </view>
+        <view class="data-card">
+          <view class="total-item">
+            <view class="item-title">团队订单金额(元)</view>
+            <view class="total-num">
+              {{ state.agentInfo.child_order_money_all || '0.00' }}
+            </view>
+          </view>
+          <view class="category-item ss-flex">
+            <view class="ss-flex-1">
+              <view class="item-title">一级订单</view>
+              <view class="category-num">
+                {{ state.agentInfo.child_order_money_1 || '0.00' }}
+              </view>
+            </view>
+            <view class="ss-flex-1">
+              <view class="item-title">二级订单</view>
+              <view class="category-num">
+                {{ state.agentInfo.child_order_money_2 || '0.00' }}
+              </view>
+            </view>
+          </view>
+        </view>
+      </view>
+      <!-- 自购 -->
+      <view class="direct-box ss-flex ss-row-between">
+        <view class="direct-item">
+          <view class="item-title">自购分销订单数量(单)</view>
+          <view class="item-value">
+            {{ state.agentInfo.child_order_count_0 || 0 }}
+          </view>
+        </view>
+        <view class="direct-item">
+          <view class="item-title">自购分销订单金额(元)</view>
+          <view class="item-value">
+            {{ state.agentInfo.child_order_money_0 || '0.00' }}
+          </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.data" :key="item">
+        <view class="order-header">
+          <view class="no-box ss-flex ss-col-center ss-row-between">
+            <text class="order-code">订单编号:{{ item.order.order_sn }}</text>
+            <text class="order-state">{{ item.order_item.status_text }}</text>
+          </view>
+          <view class="order-from ss-flex ss-col-center ss-row-between">
+            <view class="from-user ss-flex ss-col-center">
+              <text>下单人:</text>
+              <image class="user-avatar" :src="sheep.$url.cdn(item.buyer.avatar)" mode="aspectFill">
+              </image>
+              <text class="user-name">{{ item.buyer.nickname }}</text>
+            </view>
+            <view class="order-time">{{ item.create_time }}</view>
+          </view>
+        </view>
+        <s-goods-item
+          class="border-bottom"
+          :img="item.order_item.goods_image"
+          :title="item.order_item.goods_title"
+          :skuText="item.order_item.goods_sku_text"
+          :price="item.order_item.goods_price"
+          :num="item.order_item.goods_num"
+        >
+          <template #rightBottom>
+            <view class="ss-flex commission-box ss-row-between ss-m-t-10">
+              <view class="ss-flex">
+                <text class="name">佣金:</text>
+                <text class="commission-num">{{ item.rewards[0]?.commission }}</text>
+              </view>
+              <view class="order-status">
+                {{ item.commission_order_status_text }}
+              </view>
+            </view>
+          </template>
+        </s-goods-item>
+      </view>
+      <!-- 数据为空 -->
+      <s-empty v-if="state.pagination.total === 0" icon="/static/order-empty.png" text="暂无订单">
+      </s-empty>
+      <!-- 加载更多 -->
+      <uni-load-more
+        v-if="state.pagination.total > 0"
+        :status="state.loadStatus"
+        :content-text="{
+          contentdown: '上拉加载更多',
+        }"
+        @tap="loadmore"
+      />
+    </view>
+    <!-- </view> -->
+  </s-layout>
+</template>
+
+<script setup>
+  import sheep from '@/sheep';
+  import { onLoad, onReachBottom } from '@dcloudio/uni-app';
+  import { computed, reactive } from 'vue';
+  import _ from 'lodash';
+  import { onPageScroll } from '@dcloudio/uni-app';
+
+  const statusBarHeight = sheep.$platform.device.statusBarHeight * 2;
+  const headerBg = sheep.$url.css('/static/img/shop/user/withdraw_bg.png');
+  onPageScroll((e) => {
+    if (e.scrollTop > 100) {
+      state.scrollTop = false;
+    } else {
+      state.scrollTop = true;
+    }
+  });
+
+  const state = reactive({
+    pagination: {
+      data: [],
+      current_page: 1,
+      total: 1,
+      last_page: 1,
+    },
+    loadStatus: '',
+    currentTab: 0,
+    agentInfo: {},
+    scrollTop: false,
+  });
+
+  const tabMaps = [
+    {
+      name: '全部',
+      value: 'all',
+    },
+    // {
+    // 	name: '不计入',
+    // 	value: 'no'
+    // },
+    {
+      name: '已计入',
+      value: 'yes',
+    },
+    {
+      name: '已扣除',
+      value: 'back',
+    },
+    {
+      name: '已取消',
+      value: 'cancel',
+    },
+  ];
+  // 切换选项卡
+  function onTabsChange(e) {
+    state.pagination = {
+      data: [],
+      current_page: 1,
+      total: 1,
+      last_page: 1,
+    };
+    state.currentTab = e.index;
+    getOrderList();
+  }
+
+  // 获取订单列表
+  async function getOrderList(page = 1, list_rows = 5) {
+    state.loadStatus = 'loading';
+    let res = await sheep.$api.commission.order({
+      type: tabMaps[state.currentTab].value,
+      list_rows,
+      page,
+    });
+    if (res.error === 0) {
+      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';
+      }
+    }
+  }
+
+  async function getAgentInfo() {
+    const { error, data, msg } = await sheep.$api.commission.agent();
+    if (error === 0) {
+      state.agentInfo = data;
+    }
+  }
+
+  onLoad(() => {
+    getAgentInfo();
+    getOrderList();
+  });
+
+  // 加载更多
+  function loadmore() {
+    if (state.loadStatus !== 'noMore') {
+      getOrderList(state.pagination.current_page + 1);
+    }
+  }
+
+  // 上拉加载更多
+  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>

+ 173 - 0
pages/commission/share-log.vue

@@ -0,0 +1,173 @@
+<!-- 分销记录  -->
+<template>
+  <s-layout title="分享记录">
+    <view class="distraction-share-wrap">
+      <view class="share-log-box">
+        <!-- 分享记录列表 -->
+        <view class="log-list ss-flex" v-for="item in state.pagination.data" :key="item.id">
+          <view class="log-avatar-wrap">
+            <image
+              class="log-avatar"
+              :src="sheep.$url.cdn(item.user?.avatar)"
+              mode="aspectFill"
+            ></image>
+          </view>
+
+          <view class="item-right">
+            <view class="name">{{ item.user?.nickname }}</view>
+            <view class="content ss-flex">
+              <view v-if="item.ext?.image" class="content-img-wrap">
+                <image class="content-img" :src="sheep.$url.cdn(item.ext?.image)" mode="aspectFill">
+                </image>
+              </view>
+
+              <view v-if="item.ext?.memo" class="content-text">
+                {{ item.ext?.memo }}
+              </view>
+            </view>
+            <view class="item-bottom ss-flex ss-row-between">
+              <view class="from-text"></view>
+              <view class="time">{{ dayjs(item.create_time).fromNow() }}</view>
+            </view>
+          </view>
+        </view>
+        <s-empty
+          v-if="state.pagination.total === 0"
+          icon="/static/data-empty.png"
+          text="暂无分享记录"
+        >
+        </s-empty>
+        <!-- 加载更多 -->
+        <uni-load-more
+          v-if="state.pagination.total > 0"
+          :status="state.loadStatus"
+          :content-text="{
+            contentdown: '上拉加载更多',
+          }"
+          @tap="loadmore"
+        />
+      </view>
+    </view>
+  </s-layout>
+</template>
+
+<script setup>
+  import sheep from '@/sheep';
+  import { onLoad } from '@dcloudio/uni-app';
+  import { computed, reactive } from 'vue';
+  import _ from 'lodash';
+  import dayjs from 'dayjs';
+
+  const state = reactive({
+    pagination: {
+      data: [],
+      current_page: 1,
+      total: 1,
+      last_page: 1,
+    },
+    loadStatus: '',
+  });
+
+  async function getShareLog(page = 1, list_rows = 8) {
+    state.loadStatus = 'loading';
+    let res = await sheep.$api.user.share.list({
+      list_rows,
+      page,
+    });
+    if (res.error === 0) {
+      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') {
+      getShareLog(state.pagination.current_page + 1);
+    }
+  }
+  onLoad(async () => {
+    getShareLog();
+  });
+</script>
+
+<style lang="scss" scoped>
+  .share-log-box {
+    // 分享记录列表
+    .log-list {
+      background-color: #fff;
+      padding: 30rpx;
+      margin: 10rpx 0;
+      align-items: flex-start;
+
+      .log-avatar-wrap {
+        margin-right: 14rpx;
+
+        .log-avatar {
+          width: 40rpx;
+          height: 40rpx;
+          border-radius: 50%;
+        }
+      }
+
+      .item-right {
+        flex: 1;
+
+        .name {
+          font-size: 26rpx;
+          font-weight: 500;
+          color: #7f7aa5;
+          margin-bottom: 30rpx;
+        }
+
+        .content {
+          background: rgba(241, 241, 241, 0.46);
+          border-radius: 2rpx;
+          padding: 10rpx;
+          margin-bottom: 20rpx;
+
+          .content-img-wrap {
+            margin-right: 16rpx;
+            width: 80rpx;
+            height: 80rpx;
+
+            .content-img {
+              width: 80rpx;
+              height: 80rpx;
+              border-radius: 6rpx;
+            }
+          }
+
+          .content-text {
+            font-size: 24rpx;
+            font-weight: 500;
+            color: #333333;
+          }
+        }
+
+        .item-bottom {
+          width: 100%;
+
+          .time {
+            font-size: 22rpx;
+            font-weight: 500;
+            color: #c8c8c8;
+          }
+
+          .from-text {
+            font-size: 22rpx;
+            font-weight: 500;
+            color: #c8c8c8;
+          }
+        }
+      }
+    }
+  }
+</style>

+ 257 - 0
pages/commission/team.vue

@@ -0,0 +1,257 @@
+<!-- 页面  -->
+<template>
+  <s-layout title="我的团队" :class="state.scrollTop ? 'team-wrap' : ''" navbar="inner">
+    <view
+      class="header-box"
+      :style="[
+        {
+          marginTop: '-' + Number(statusBarHeight + 88) + 'rpx',
+          paddingTop: Number(statusBarHeight + 108) + 'rpx',
+        },
+      ]"
+    >
+      <!-- 推荐人 -->
+      <view 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">{{ agentInfo.child_user_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_user_count_1 || 0 }}</view>
+            </view>
+            <view class="ss-flex-1">
+              <view class="item-title">二级成员</view>
+              <view class="category-num">{{ agentInfo.child_user_count_2 || 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.agent?.level_info" class="tag-box ss-flex ss-col-center">
+              <image
+                class="tag-img"
+                :src="sheep.$url.cdn(item.agent.level_info.image)"
+                mode="aspectFill"
+              >
+              </image>
+              <text class="tag-title">{{ item.agent.level_info.name }}</text>
+            </view>
+
+            <text class="time-text">{{ item.create_time }}</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 } from 'vue';
+  import _ from 'lodash';
+  import { onPageScroll } from '@dcloudio/uni-app';
+
+  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) => {
+    if (e.scrollTop > 100) {
+      state.scrollTop = false;
+    } else {
+      state.scrollTop = true;
+    }
+  });
+  const state = reactive({
+    pagination: {
+      data: [],
+      current_page: 1,
+      total: 1,
+      last_page: 1,
+    },
+    loadStatus: '',
+  });
+
+  function filterUserNum(num) {
+    if (_.isNil(num)) {
+      return '';
+    }
+    return `下级团队${num}人`;
+  }
+
+  async function getTeamList(page = 1, list_rows = 8) {
+    state.loadStatus = 'loading';
+    let res = await sheep.$api.commission.team({
+      list_rows,
+      page,
+    });
+    if (res.error === 0) {
+        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';
+      }
+    }
+  }
+
+  onLoad(async () => {
+    getTeamList();
+  });
+
+  // 加载更多
+  function loadmore() {
+    if (state.loadStatus !== 'noMore') {
+      getTeamList(state.pagination.current_page + 1);
+    }
+  }
+
+  // 上拉加载更多
+  onReachBottom(() => {
+    loadmore();
+  });
+</script>
+
+<style lang="scss" scoped>
+  .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%;
+    }
+  }
+</style>

+ 372 - 0
pages/coupon/detail.vue

@@ -0,0 +1,372 @@
+<!-- 优惠券详情  -->
+<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"
+            ></image>
+          </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.list.name }}</view>
+            <view class="subtitle ss-m-b-50">{{ state.list.amount_text }}</view>
+            <button
+              class="ss-reset-button ss-m-b-30"
+              :class="
+                state.list.get_status == 'can_get' || state.list.get_status == 'can_use'
+                  ? 'use-btn'
+                  : 'disable-btn'
+              "
+              :disabled="
+                (state.list.get_status != 'can_get' && state.list.get_status != 'can_use') ||
+                state.userCouponId
+              "
+              @click="getCoupon"
+            >
+              {{ state.list.get_status_text }}
+            </button>
+            <view
+              class="time ss-m-y-30"
+              v-if="
+                state.list.get_status == 'can_get' ||
+                state.list.get_status == 'cannot_get' ||
+                state.list.get_status == 'get_over'
+              "
+            >
+              领取时间:{{ state.list.get_start_time }}至{{ state.list.get_end_time }}
+            </view>
+            <view class="time ss-m-y-30" v-else>
+              有效期:{{ state.list.use_start_time }}至{{ state.list.use_end_time }}
+            </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.list.type_text }}</view>
+            </view>
+            <uni-collapse>
+              <uni-collapse-item title="优惠券说明" v-if="state.list.description">
+                <view class="content ss-p-b-20">
+                  <text class="des ss-p-l-30">{{ state.list.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.list.use_scope == 'all_use'"
+      >
+        {{ state.list.use_scope_text }}
+      </view>
+
+      <su-sticky v-else bgColor="#fff">
+        <view class="goods-title ss-p-20">{{ state.list.use_scope_text }}</view>
+        <su-tabs
+          :scrollable="true"
+          :list="state.tabMaps"
+          @change="onTabsChange"
+          :current="state.currentTab"
+          v-if="state.list.use_scope == 'category'"
+        ></su-tabs>
+      </su-sticky>
+      <view v-if="state.list.use_scope == 'goods' || state.list.use_scope == 'disabled_goods'">
+        <view v-for="(item, index) in state.list.items_value" :key="index">
+          <s-goods-column
+            class="ss-m-20"
+            size="lg"
+            :data="item"
+            :titleColor="props.goodsFieldsStyle?.title?.color"
+            :subTitleColor="props.goodsFieldsStyle?.subtitle?.color"
+            @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 },
+            }"
+            :buttonShow="state.list.use_scope != 'disabled_goods'"
+          ></s-goods-column>
+        </view>
+      </view>
+      <view v-if="state.list.use_scope == 'category'">
+        <view v-for="(item, index) in state.pagination.data" :key="index">
+          <s-goods-column
+            class="ss-m-20"
+            size="lg"
+            :data="item"
+            :titleColor="props.goodsFieldsStyle?.title?.color"
+            :subTitleColor="props.goodsFieldsStyle?.subtitle?.color"
+            @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 },
+            }"
+            :buttonShow="state.list.use_scope != 'disabled_goods'"
+          ></s-goods-column>
+        </view>
+      </view>
+      <uni-load-more
+        v-if="state.pagination.total > 0 && state.list.use_scope == 'category'"
+        :status="state.loadStatus"
+        :content-text="{
+          contentdown: '上拉加载更多',
+        }"
+        @tap="loadmore"
+      />
+      <s-empty
+        v-if="state.list.use_scope == 'category' && state.pagination.total === 0"
+        paddingTop="0"
+        icon="/static/soldout-empty.png"
+        text="暂无商品"
+      >
+      </s-empty>
+    </view>
+  </s-layout>
+</template>
+
+<script setup>
+  import sheep from '@/sheep';
+  import { onLoad, onReachBottom } from '@dcloudio/uni-app';
+  import { reactive } from 'vue';
+  import _ from 'lodash';
+
+  const pagination = {
+    data: [],
+    current_page: 1,
+    total: 1,
+    last_page: 1,
+  };
+  const state = reactive({
+    list: {},
+    couponId: 0,
+    userCouponId: 0,
+    pagination: {
+      data: [],
+      current_page: 1,
+      total: 1,
+      last_page: 1,
+    },
+    tabMaps: [],
+    loadStatus: '',
+    categoryId: 0,
+  });
+
+  // 接收参数
+  const props = defineProps({
+    includes: {
+      type: Array,
+      default() {
+        return [];
+      },
+    },
+    list: {
+      type: Array,
+      default: () => [],
+    },
+    goodsFieldsStyle: {
+      type: Object,
+      default() {},
+    },
+    buyData: {
+      type: Object,
+      default() {},
+    },
+  });
+
+  function onTabsChange(e) {
+    state.pagination = pagination;
+    state.currentTab = e.index;
+    state.categoryId = e.value;
+    getGoodsList(state.categoryId);
+  }
+  async function getGoodsList(categoryId, page = 1, list_rows = 5) {
+    state.loadStatus = 'loading';
+    const res = await sheep.$api.goods.list({
+      category_id: categoryId,
+      list_rows,
+      page,
+      is_category_deep: false,
+    });
+    if (res.error === 0) {
+      let couponlist = _.concat(state.pagination.data, res.data.data);
+      state.pagination = {
+        ...res.data,
+        data: couponlist,
+      };
+      if (state.pagination.current_page < state.pagination.last_page) {
+        state.loadStatus = 'more';
+      } else {
+        state.loadStatus = 'noMore';
+      }
+    }
+  }
+  async function getCoupon() {
+    const { error, msg } = await sheep.$api.coupon.get(state.couponId);
+    if (error === 0) {
+      uni.showToast({
+        title: msg,
+      });
+      setTimeout(() => {
+        getCouponContent(state.couponId, state.userCouponId);
+      }, 1000);
+    }
+  }
+  async function getCouponContent(id, c) {
+    const { data } = await sheep.$api.coupon.detail(id, c);
+    state.list = data;
+    data.items_value.forEach((i) => {
+      state.tabMaps.push({ name: i.name, value: i.id });
+    });
+    state.pagination = pagination;
+    if (state.list.use_scope == 'category') {
+      getGoodsList(state.tabMaps[0].value);
+    }
+  }
+  // 加载更多
+  function loadmore() {
+    if (state.loadStatus !== 'noMore') {
+      getGoodsList(state.categoryId, state.pagination.current_page + 1);
+    }
+  }
+  onLoad((options) => {
+    state.couponId = options.id;
+    state.userCouponId = options.user_coupon_id;
+    getCouponContent(state.couponId, state.userCouponId);
+  });
+  // 上拉加载更多
+  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>

+ 261 - 0
pages/coupon/list.vue

@@ -0,0 +1,261 @@
+<!-- 优惠券中心  -->
+<template>
+	<s-layout title="优惠券" :bgStyle="{ color: '#f2f2f2' }">
+		<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/coupon-empty.png" text="暂无优惠券"></s-empty>
+		<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.get_status != 'can_get' ? 'border-btn' : ''" @click.stop="getBuy(item.id)"
+							:disabled="item.get_status != 'can_get'">
+							<!-- {{ item.status_text }} -->
+							{{item.status_text|| '立即使用' }}
+						</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', {
+					              id: item.id,
+					            })
+					          " -->
+					<template #default>
+						<button class="ss-reset-button card-btn ss-flex ss-row-center ss-col-center" :class="
+                item.status == 'can_get' || item.status == 'can_use'
+                  ? ''
+                  : item.status == 'used' || item.status == 'expired'
+                  ? 'disabled-btn'
+                  : 'border-btn'
+              " :disabled="item.status != 'can_get' && item.status != 'can_use'" @click.stop="
+                sheep.$router.go('/pages/coupon/detail', {
+                  id: item.coupon_id,
+                  user_coupon_id: item.id,
+                })
+              ">
+							<!-- {{ item.status_text }} -->
+							{{item.status_text|| '立即使用' }}
+						</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 {
+		computed,
+		reactive
+	} from 'vue';
+	import _ from 'lodash';
+
+	const pagination = {
+		data: [],
+		current_page: 1,
+		total: 1,
+		last_page: 1,
+	};
+	// 数据
+	const state = reactive({
+		currentTab: 0,
+		pagination: {
+			data: [],
+			current_page: 1,
+			total: 1,
+			last_page: 1,
+		},
+		loadStatus: '',
+		type: '1',
+	});
+
+	const tabMaps = [
+		// {
+		//   name: '领券中心',
+		//   value: 'all',
+		// },
+		{
+			name: '已领取',
+			value: '1',
+		},
+		{
+			name: '已使用',
+			value: '2',
+		},
+		{
+			name: '已失效',
+			value: '3',
+		},
+	];
+
+	function onTabsChange(e) {
+		state.pagination = pagination
+		state.currentTab = e.index;
+		state.type = e.value;
+		// if (state.currentTab == 0) {
+		// 	getData();
+		// } else {
+		getCoupon();
+		// }
+	}
+	async function getData(page = 1, list_rows = 5) {
+		state.loadStatus = 'loading';
+		const res = await sheep.$api.coupon.list({
+			list_rows,
+			page
+		});
+		if (res.error === 0) {
+			let couponlist = _.concat(state.pagination.data, res.data.data);
+			state.pagination = {
+				...res.data,
+				data: couponlist,
+			};
+			if (state.pagination.current_page < state.pagination.last_page) {
+				state.loadStatus = 'more';
+			} else {
+				state.loadStatus = 'noMore';
+			}
+		}
+	}
+
+	async function getCoupon(page = 1, list_rows = 5) {
+		state.loadStatus = 'loading';
+		let res = await sheep.$api.coupon.userCoupon({
+			status: state.type,
+			pageSize: list_rows,
+			pageNo: page
+		});
+		if (res.code === 0) {
+			// 拦截修改数据
+			let obj = {
+				1: '可用',
+				2: '已用',
+				3: '过期'
+			}
+			res.data.list = res.data.list.map(item => {
+				return {
+					...item,
+					enough: (item.usePrice / 100).toFixed(2),
+					amount: (item.discountPrice / 100).toFixed(2),
+					use_start_time: sheep.$helper.timeFormat(item.validStartTime, 'yyyy-mm-dd hh:MM:ss'),
+					use_end_time: sheep.$helper.timeFormat(item.validEndTime, 'yyyy-mm-dd hh:MM:ss'),
+					status_text: obj[item.status]
+				}
+			});
+			if (page >= 2) {
+				let couponlist = _.concat(state.pagination.data, res.data.list);
+
+				state.pagination = {
+					...res.data,
+					data: couponlist,
+				};
+				console.log(state.pagination, '拿到的优惠券数据');
+			} else {
+				state.pagination = res.data;
+				console.log(state.pagination, '拿到的优惠券数据');
+			}
+			// if (state.pagination.current_page < state.pagination.last_page) {
+			// 	state.loadStatus = 'more';
+			// } else {
+			// 	state.loadStatus = 'noMore';
+			// }
+		}
+	}
+	async function getBuy(id) {
+		const {
+			error,
+			msg
+		} = await sheep.$api.coupon.get(id);
+		if (error === 0) {
+			uni.showToast({
+				title: msg,
+			});
+			setTimeout(() => {
+				state.pagination = pagination
+				getData();
+			}, 1000);
+		}
+	}
+
+	// 加载更多
+	function loadmore() {
+		if (state.loadStatus !== 'noMore') {
+			if (state.currentTab == 0) {
+				getData(state.pagination.current_page + 1);
+			} else {
+				getCoupon(state.pagination.current_page + 1);
+			}
+		}
+	}
+	onLoad((Option) => {
+		// if (Option.type === 'all' || !Option.type) {
+		// 	getData();
+		// } else {
+		// state.type = Option.type;
+		// Option.type === 'geted' ?
+		// 	() :
+		// 	Option.type === 'used' ?
+		// 	(state.currentTab = 1 && state.type = 2) :
+		// 	(state.currentTab = 2 && state.type = 3);
+
+		if (Option.type == 'geted') {
+			state.currentTab = 0
+			state.type = 1
+		} else if (Option.type == 'used') {
+			state.currentTab = 1
+			state.type = 2
+		} else {
+			state.currentTab = 2
+			state.type = 3
+		}
+		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>

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

@@ -0,0 +1,209 @@
+<!-- 评价  -->
+<template>
+	<s-layout title="评价">
+		<view>
+			<view v-for="(item, index) in state.orderInfo.items" :key="item.id">
+				<view v-if="item.btns.includes('comment')">
+					<view class="commont-from-wrap">
+						<!-- 评价商品 -->
+						<s-goods-item :img="item.goods_image" :title="item.goods_title" :skuText="item.goods_sku_text"
+							:price="item.goods_price" :num="item.goods_num"></s-goods-item>
+					</view>
+
+					<view class="form-item">
+						<!-- 评分 -->
+						<view class="star-box ss-flex ss-col-center">
+							<view class="star-title ss-m-r-40">
+								<!-- {{ rateMap[state.commentList[index].level] }} -->
+								商品质量
+							</view>
+							<uni-rate v-model="state.commentList[index].level" />
+						</view>
+						<view class="star-box ss-flex ss-col-center">
+							<view class="star-title ss-m-r-40">
+								<!-- {{ rateMap[state.commentList[index].level] }} -->
+								服务态度
+							</view>
+							<uni-rate v-model="state.commentList[index].level2" />
+						</view>
+						<!-- 评价 -->
+						<view class="area-box">
+							<uni-easyinput :inputBorder="false" type="textarea" maxlength="120" autoHeight
+								v-model="state.commentList[index].content"
+								placeholder="宝贝满足你的期待吗?说说你的使用心得,分享给想买的他们吧~"></uni-easyinput>
+
+							<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>
+
+		<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 {
+		computed,
+		reactive
+	} from 'vue';
+
+	const state = reactive({
+		orderInfo: {},
+		commentList: [],
+		orderId: null
+	});
+
+	const rateMap = {
+		1: '糟糕',
+		2: '差评',
+		3: '一般',
+		4: '良好',
+		5: '好评',
+	};
+
+	async function onSubmit() {
+		// 对接商品评价
+		// console.log(state.orderInfo);
+		// return;
+		let obj = {
+			anonymous: false,
+			benefitScores: state.commentList[0].level2,
+			content: state.commentList[0].content,
+			descriptionScores: state.commentList[0].level,
+			orderItemId: state.commentList[0].item_id,
+			picUrls: 'https://t7.baidu.com/it/u=2531125946,3055766435&fm=193&f=GIF'
+		}
+		const {
+			code
+		} = await sheep.$api.order.comment(obj);
+		if (code === 0) {
+			sheep.$router.back();
+		}
+	}
+
+	onLoad(async (options) => {
+		let id = '';
+		if (options.orderSN) {
+			id = options.orderSN;
+		}
+		if (options.id) {
+			id = options.id;
+		}
+		if (options.orderId) {
+			state.orderId = options.orderId
+		}
+
+		const res = await sheep.$api.order.detail(id);
+		if (res.code === 0) {
+			let obj = {
+				10: ['待发货', '等待买家付款', ["apply_refund"]],
+				30: ['待评价', '等待买家评价', ["express", "comment"]]
+			}
+
+			res.data.status_text = obj[res.data.status][0];
+			res.data.status_desc = obj[res.data.status][1];
+			res.data.btns = obj[res.data.status][2];
+			res.data.address = {
+				province_name: res.data.receiverAreaName.split(' ')[0],
+				district_name: res.data.receiverAreaName.split(' ')[2],
+				city_name: res.data.receiverAreaName.split(' ')[1],
+				address: res.data.receiverDetailAddress,
+				consignee: res.data.receiverName,
+				mobile: res.data.receiverMobile,
+			}
+			res.data.pay_fee = res.data.payPrice / 100
+			res.data.create_time = sheep.$helper.timeFormat(res.data.createTime, 'yyyy-mm-dd hh:MM:ss')
+			res.data.order_sn = res.data.no
+			res.data.id = res.data.id
+			res.data.goods_amount = res.data.totalPrice / 100
+			res.data.dispatch_amount = res.data.deliveryPrice / 100
+			res.data.pay_types_text = res.data.payChannelName.split(',')
+			res.data.items = res.data.items.map(ite => {
+				return {
+					...ite,
+					btns: obj[res.data.status][2],
+					goods_title: ite.spuName,
+					goods_num: ite.count,
+					goods_price: ite.price / 100,
+					goods_image: ite.picUrl,
+					goods_sku_text: ite.properties.reduce((it0, it1) => it0 + it1.valueName + ' ', '')
+				}
+			})
+			if (res.data.btns.includes('comment')) {
+				state.orderInfo = res.data;
+				state.orderInfo.items.forEach((item) => {
+					if (item.btns.includes('comment')) {
+						state.commentList.push({
+							item_id: item.id,
+							level: 5,
+							content: '',
+							images: [],
+						});
+					}
+				});
+				console.log(state.orderInfo.items, '循环')
+				return;
+			}
+		}
+		sheep.$helper.toast('无待评价订单');
+	});
+</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>

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

@@ -0,0 +1,101 @@
+<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.spuId,
+      activity_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>

+ 115 - 0
pages/goods/components/detail/detail-cell-params.vue

@@ -0,0 +1,115 @@
+<template>
+  <view>
+    <detail-cell
+      v-if="modelValue.length > 0"
+      label="参数"
+      :value="state.paramsTitle"
+      @click="state.show = true"
+    ></detail-cell>
+    <su-popup :show="state.show" round="10" :showClose="true" @close="close">
+      <view class="ss-modal-box bg-white">
+        <view class="modal-header">产品参数</view>
+        <scroll-view
+          class="modal-content ss-p-t-50"
+          scroll-y="true"
+          :scroll-with-animation="true"
+          :show-scrollbar="false"
+          @touchmove.stop
+        >
+          <view class="sale-item ss-flex ss-col-top" v-for="item in modelValue" :key="item.title">
+            <view class="item-title">{{ item.title }}</view>
+            <view class="item-value">{{ item.content }}</view>
+          </view>
+        </scroll-view>
+        <view class="modal-footer ss-flex ss-row-center ss-m-b-20">
+          <button class="ss-reset-button save-btn ui-Shadow-Main" @tap="state.show = false"
+            >确定</button
+          >
+        </view>
+      </view>
+    </su-popup>
+  </view>
+</template>
+
+<script setup>
+  import { reactive, computed } from 'vue';
+
+  import detailCell from './detail-cell.vue';
+  const props = defineProps({
+    modelValue: {
+      type: Object,
+      default() {
+        return [];
+      },
+    },
+  });
+  const state = reactive({
+    show: false,
+    paramsTitle: computed(() => {
+      let titleArray = [];
+      props.modelValue.map((item) => {
+        titleArray.push(item.title);
+      });
+      return titleArray.join(' · ');
+    }),
+  });
+
+  function close() {
+    state.show = false;
+  }
+</script>
+
+<style lang="scss" scoped>
+  .ss-modal-box {
+    border-radius: 30rpx 30rpx 0 0;
+    max-height: 730rpx;
+
+    .modal-header {
+      position: relative;
+      padding: 30rpx 20rpx 40rpx;
+      font-size: 36rpx;
+      font-weight: bold;
+    }
+
+    .modal-content {
+      padding: 0 30rpx;
+      max-height: 500rpx;
+      box-sizing: border-box;
+
+      .sale-item {
+        margin-bottom: 24rpx;
+        padding-bottom: 24rpx;
+        border-bottom: 2rpx solid rgba(#dfdfdf, 0.4);
+
+        .item-title {
+          font-size: 28rpx;
+          font-weight: 500;
+          line-height: 42rpx;
+          width: 120rpx;
+          white-space: normal;
+        }
+
+        .item-value {
+          font-size: 26rpx;
+          font-weight: 400;
+          color: $dark-9;
+          line-height: 42rpx;
+          flex: 1;
+          margin-left: 20rpx;
+        }
+      }
+    }
+
+    .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;
+      }
+    }
+  }
+</style>

+ 121 - 0
pages/goods/components/detail/detail-cell-service.vue

@@ -0,0 +1,121 @@
+<template>
+  <view>
+    <detail-cell
+      v-if="modelValue.length > 0"
+      label="保障"
+      :value="state.paramsTitle"
+      @click="state.show = true"
+    >
+    </detail-cell>
+    <su-popup :show="state.show" round="10" :showClose="true" @close="state.show = false">
+      <view class="ss-modal-box">
+        <view class="modal-header">服务保障</view>
+        <scroll-view
+          class="modal-content"
+          scroll-y="true"
+          :scroll-with-animation="true"
+          :show-scrollbar="false"
+          @touchmove.stop
+        >
+          <view class="sale-item ss-flex ss-col-top" v-for="item in modelValue" :key="item.id">
+            <image
+              class="title-icon ss-m-r-14"
+              :src="sheep.$url.cdn(item.image)"
+              mode="aspectFill"
+            ></image>
+            <view class="title-box">
+              <view class="item-title ss-m-b-20">{{ item.name }}</view>
+              <view class="item-value">{{ item.description }}</view>
+            </view>
+          </view>
+        </scroll-view>
+        <view class="modal-footer ss-flex ss-row-center ss-m-b-20">
+          <button class="ss-reset-button save-btn ui-Shadow-Main" @tap="state.show = false"
+            >确定</button
+          >
+        </view>
+      </view>
+    </su-popup>
+  </view>
+</template>
+
+<script setup>
+  import { reactive, computed } from 'vue';
+  import sheep from '@/sheep';
+  import detailCell from './detail-cell.vue';
+  const props = defineProps({
+    modelValue: {
+      type: Object,
+      default() {},
+    },
+  });
+
+  const state = reactive({
+    show: false,
+    paramsTitle: computed(() => {
+      let nameArray = [];
+      props.modelValue.map((item) => {
+        nameArray.push(item.name);
+      });
+      return nameArray.join(' · ');
+    }),
+  });
+</script>
+
+<style lang="scss" scoped>
+  .ss-modal-box {
+    border-radius: 30rpx 30rpx 0 0;
+    max-height: 730rpx;
+
+    .modal-header {
+      position: relative;
+      padding: 30rpx 20rpx 40rpx;
+      font-size: 36rpx;
+      font-weight: bold;
+    }
+
+    .modal-content {
+      padding: 0 30rpx;
+      max-height: 500rpx;
+      box-sizing: border-box;
+
+      .sale-item {
+        margin-bottom: 44rpx;
+
+        .title-icon {
+          width: 36rpx;
+          height: 36rpx;
+        }
+		.title-box{
+			flex: 1;
+		}
+
+        .item-title {
+          font-size: 28rpx;
+          font-weight: 500;
+          line-height: normal;
+        }
+
+        .item-value {
+          font-size: 26rpx;
+          font-weight: 400;
+          color: $dark-9;
+          line-height: 42rpx;
+        }
+      }
+    }
+
+    .modal-footer {
+      height: 120rpx;
+      background-color: #fff;
+
+      .save-btn {
+        width: 710rpx;
+        height: 80rpx;
+        border-radius: 40rpx;
+        background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
+        color: $white;
+      }
+    }
+  }
+</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 @@
+<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>

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

@@ -0,0 +1,51 @@
+<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"></mp-html>
+    </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>

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

@@ -0,0 +1,255 @@
+<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>

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

@@ -0,0 +1,39 @@
+<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>

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

@@ -0,0 +1,171 @@
+<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"
+            ></image>
+            <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"
+            ></image>
+            <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"
+          ></image>
+          <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"
+          ></image>
+          <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 { computed, reactive } from 'vue';
+  import sheep from '@/sheep';
+  import { showShareModal } from '@/sheep/hooks/useModal';
+
+  // 数据
+  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,
+    },
+  });
+  const elStyles = computed(() => {
+    return {
+      'border-top-left-radius': props.topRadius + 'rpx',
+      'border-top-right-radius': props.topRadius + 'rpx',
+      overflow: 'hidden',
+    };
+  });
+
+  const tabbarheight = (e) => {
+    uni.setStorageSync('tabbar', e);
+  };
+  async function onFavorite() {
+    const { error } = await sheep.$api.user.favorite.do(props.modelValue.id);
+    if (error === 0) {
+      if (props.modelValue.favorite) {
+        props.modelValue.favorite = 0;
+      } else {
+        props.modelValue.favorite = 1;
+      }
+    }
+  }
+
+  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>

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

@@ -0,0 +1,137 @@
+<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">
+      <view class="">已有{{ modelValue.sales }}人参与活动</view>
+      <text class="cicon-forward"></text>
+    </view>
+    <view
+      v-for="(item, index) in state.list"
+      @tap="sheep.$router.go('/pages/activity/groupon/detail', { id: item.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(item.leader.avatar)" class="user-avatar"></image>
+        <view class="user-nickname ss-m-l-20 ss-line-1">{{ item.leader.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">{{ item.num - item.current_num }}人</view>
+            成团
+          </view>
+          <view class="end-time">{{ endTime(item.expire_time) }}</view>
+        </view>
+        <view class="">
+          <button class="ss-reset-button go-btn" @tap.stop="onJoinGroupon(item)"> 去参团 </button>
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup>
+  import { onMounted, reactive } from 'vue';
+  import sheep from '@/sheep';
+  import { useDurationTime } from '@/sheep/hooks/useGoods';
+
+  const props = defineProps({
+    modelValue: {
+      type: Object,
+      default() {},
+    },
+  });
+  const state = reactive({
+    list: [],
+  });
+  const emits = defineEmits(['join']);
+
+  function onJoinGroupon(groupon) {
+    emits('join', groupon);
+  }
+
+  // 倒计时
+  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 () => {
+    const { data } = await sheep.$api.activity.getGrouponList({
+      goods_id: props.modelValue.id,
+      activity_id: props.modelValue.activity.id,
+    });
+    state.list = data.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>

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

@@ -0,0 +1,92 @@
+<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>

+ 597 - 0
pages/goods/groupon.vue

@@ -0,0 +1,597 @@
+<template>
+  <s-layout :onShareAppMessage="shareInfo" navbar="goods">
+    <!-- 标题栏 -->
+    <detailNavbar />
+    <!-- 骨架屏 -->
+    <detailSkeleton v-if="state.skeletonLoading" />
+    <!-- 空置页 -->
+    <s-empty
+      v-else-if="
+        state.goodsInfo === null ||
+        !['groupon', 'groupon_ladder'].includes(state.goodsInfo.activity_type)
+      "
+      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">
+                  {{ goodsPrice }}
+                </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.original_price"
+                >
+                  单买价:
+                  <view class="origin-price-text">
+                    {{ state.goodsInfo.original_goods_price[0] || state.goodsInfo.original_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.title }}</view>
+          <view class="subtitle-text ss-line-1">{{ state.goodsInfo.subtitle }}</view>
+        </view>
+
+        <!-- 功能卡片 -->
+        <view class="detail-cell-card detail-card ss-flex-col">
+          <!-- 规格 -->
+          <detail-cell-sku
+            v-model="state.selectedSkuPrice.goods_sku_text"
+            :skus="state.goodsInfo.skus"
+            @tap="state.showSelectSku = true"
+          />
+          <!-- 服务 -->
+          <detail-cell-service v-model="state.goodsInfo.service" />
+          <!-- 参数 -->
+          <detail-cell-params v-model="state.goodsInfo.params" />
+          <!-- 玩法 -->
+          <detail-cell
+            v-if="state.goodsInfo.activity.richtext_id > 0"
+            label="玩法"
+            :value="state.goodsInfo.activity.richtext_title"
+            @click="
+              sheep.$router.go('/pages/public/richtext', {
+                id: state.goodsInfo.activity.richtext_id,
+                title: state.goodsInfo.activity.richtext_title,
+              })
+            "
+          />
+        </view>
+
+        <!-- 参团列表 -->
+        <groupon-card-list
+          v-if="state.goodsInfo.activity.rules.is_team_card === '1'"
+          v-model="state.goodsInfo"
+          @join="onJoinGroupon"
+        />
+
+        <!-- 规格与数量弹框 -->
+        <s-select-groupon-sku
+          :show="state.showSelectSku"
+          :goodsInfo="state.goodsInfo"
+          :grouponAction="state.grouponAction"
+          :grouponNum="state.grouponNum"
+          @buy="onBuy"
+          @ladder="onLadder"
+          @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.content" />
+
+      <!-- 商品tabbar -->
+      <!-- TODO: 已售罄、预热 判断 设计-->
+      <detail-tabbar v-model="state.goodsInfo">
+        <view class="buy-box ss-flex ss-col-center ss-p-r-20">
+          <button
+            v-if="state.goodsInfo.activity.rules.is_alone == 1"
+            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">{{
+              state.goodsInfo.original_goods_price[0] || state.goodsInfo.original_price
+            }}</view>
+            <view>原价购买</view>
+          </button>
+          <button v-else class="ss-reset-button origin-price-btn ss-flex-col">
+            <view class="btn-title">{{
+              state.grouponNum == 0 ? '阶梯团' : state.grouponNum + '人团'
+            }}</view>
+          </button>
+          <button
+            class="ss-reset-button btn-tox ss-flex-col"
+            @tap="onCreateGroupon"
+            :class="
+              state.goodsInfo.activity.status === 'ing' && state.goodsInfo.stock != 0
+                ? 'check-btn-box'
+                : 'disabled-btn-box'
+            "
+            :disabled="state.goodsInfo.stock === 0 || state.goodsInfo.activity.status != 'ing'"
+          >
+            <view class="btn-price">{{ goodsPrice }}</view>
+            <view v-if="state.goodsInfo.activity.status === 'ing'">
+              <view v-if="state.goodsInfo.stock === 0">已售罄</view>
+              <view v-else>立即开团</view>
+            </view>
+            <view v-else>{{ state.goodsInfo.activity.status_text }}</view>
+          </button>
+        </view>
+      </detail-tabbar>
+    </block>
+    <!-- 轮播  -->
+  </s-layout>
+</template>
+
+<script setup>
+  import { reactive, getCurrentInstance, computed, ref } 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 detailCell from './components/detail/detail-cell.vue';
+  import detailCellSku from './components/detail/detail-cell-sku.vue';
+  import detailCellService from './components/detail/detail-cell-service.vue';
+  import detailCellParams from './components/detail/detail-cell-params.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, formatPrice, formatGoodsSwiper } from '@/sheep/hooks/useGoods';
+
+
+  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 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,  // 骨架屏
+    goodsId: 0,             // 商品ID
+    goodsInfo: {},          // 商品信息
+    goodsSwiper: [],        // 商品轮播图
+    showSelectSku: false,   // 显示规格弹框
+    selectedSkuPrice: {},   // 选中的规格价格
+    grouponId: 0,           // 团购ID
+    grouponType: '',        // 团购类型
+    grouponNum: 0,          // 团购人数
+    grouponAction: 'create',  // 团购操作  
+  });
+
+  // 商品主价格
+  const goodsPrice = computed(() => {
+    if (isEmpty(state.selectedSkuPrice)) {
+      return formatPrice(state.goodsInfo.price);
+    }
+	if(state.grouponNum === 0 && state.grouponType === 'groupon_ladder') {
+		return formatPrice(state.goodsInfo.price)
+	}
+    if (state.grouponType === 'groupon') {
+      return state.selectedSkuPrice.groupon_price;
+    }
+    if (state.grouponType === 'groupon_ladder') {
+      return state.selectedSkuPrice.ladder_price;
+    }
+    return '';
+  });
+
+  // 倒计时
+  const endTime = computed(() => {
+    return useDurationTime(state.goodsInfo.activity.end_time);
+  });
+
+  // 规格变更
+  function onSkuChange(e) {
+    state.selectedSkuPrice = e;
+  }
+
+  // 阶梯变更
+  function onLadder(e) {
+    state.showSelectSku = false;
+    state.grouponNum = e
+    setTimeout(() => {
+      state.showSelectSku = true;
+    }, 80);
+  }
+
+  function onSkuClose() {
+    state.showSelectSku = false;
+  }
+
+  // 发起拼团
+  function onCreateGroupon() {
+    state.grouponAction = 'create';
+    state.grouponId = 0;
+    state.showSelectSku = true;
+  }
+
+  // 点击参团
+  function onJoinGroupon(groupon) {
+    state.grouponAction = 'join';
+    state.grouponId = groupon.id;
+    state.grouponNum = groupon.num;
+    state.showSelectSku = true;
+  }
+
+  // 立即购买
+  function onBuy(e) {
+    sheep.$router.go('/pages/order/confirm', {
+      data: JSON.stringify({
+        order_type: 'goods',
+        buy_type: 'groupon',
+        activity_id: state.goodsInfo.activity.id,
+        groupon_id: state.grouponId,
+        groupon_num: state.grouponNum,
+        goods_list: [
+          {
+            goods_id: e.goods_id,
+            goods_num: e.goods_num,
+            goods_sku_price_id: e.id,
+          },
+        ],
+      }),
+    });
+  }
+
+  const shareInfo = computed(() => {
+    if (isEmpty(state.goodsInfo?.activity)) return {};
+    return sheep.$platform.share.getShareInfo(
+      {
+        title: state.goodsInfo.title,
+        image: sheep.$url.cdn(state.goodsInfo.image),
+        params: {
+          page: '3',
+          query: state.goodsInfo.id + ',' + state.goodsInfo.activity.id,
+        },
+      },
+      {
+        type: 'goods', // 商品海报
+        title: state.goodsInfo.title, // 商品标题
+        image: sheep.$url.cdn(state.goodsInfo.image), // 商品主图
+        price: state.goodsInfo.price[0], // 商品价格
+        original_price: state.goodsInfo.original_price, // 商品原价
+      },
+    );
+  });
+
+  onLoad(async (options) => {
+    // 非法参数
+    if (!options.id) {
+      state.goodsInfo = null;
+      return;
+    }
+    state.goodsId = options.id;
+    // 加载商品信息
+    const { error, data } = await sheep.$api.goods.detail(options.id, {
+      activity_id: options.activity_id,
+    });
+    // 关闭骨架屏
+    state.skeletonLoading = false;
+    if (error === 0) {
+      state.goodsInfo = data;
+      state.grouponType = state.goodsInfo.activity_type;
+      if (state.grouponType === 'groupon') {
+        state.grouponNum = state.goodsInfo.activity.rules.team_num;
+      }
+      state.goodsSwiper = formatGoodsSwiper(state.goodsInfo.images);
+    } 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;
+      }
+    }
+  }
+
+  //秒杀卡片
+  .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>

+ 384 - 0
pages/goods/index.vue

@@ -0,0 +1,384 @@
+<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)"
+                     dotStyle="tag" imageMode="widthFix" dotCur="bg-mask-40" :seizeHeight="750" />
+
+					<!-- 价格+标题 -->
+					<view class="title-card detail-card ss-p-y-40 ss-p-x-20">
+						<view class="ss-flex ss-row-between ss-col-center ss-m-b-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">
+              <!-- 满减送/限时折扣活动的提示 TODO 芋艿:promos 未写 -->
+              <div class="tag-content">
+								<view class="tag-box ss-flex">
+									<view class="tag ss-m-r-10" v-for="promos in state.goodsInfo.promos" :key="promos.id" @tap="onActivity">
+										{{ promos.title }}
+									</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" />
+            <!-- TODO 芋艿:可能暂时不考虑使用 -->
+						<detail-cell-service v-if="state.goodsInfo.service" v-model="state.goodsInfo.service" />
+						<detail-cell-params v-if="state.goodsInfo.params" v-model="state.goodsInfo.params" />
+					</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 { 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 detailCellService from './components/detail/detail-cell-service.vue';
+	import detailCellParams from './components/detail/detail-cell-params.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';
+
+	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;
+	}
+
+	// 添加购物车  TODO 芋艿:待测试
+	function onAddCart(e) {
+		sheep.$store('cart').add(e);
+	}
+
+	// 立即购买  TODO 芋艿:待测试
+	function onBuy(e) {
+		sheep.$router.go('/pages/order/confirm', {
+			data: JSON.stringify({
+				order_type: 'goods',
+				goods_list: [{
+					goods_id: e.goods_id,
+					goods_num: e.goods_num,
+					goods_sku_price_id: e.id,
+				}, ],
+			}),
+		});
+	}
+
+	// 营销活动  TODO 芋艿:待测试
+	function onActivity() {
+		state.activityInfo = state.goodsInfo.promos;
+		state.showActivityModel = true;
+	}
+
+	// 立即领取  TODO 芋艿:待测试
+	async function onGet(id) {
+		const {
+			error,
+			msg
+		} = await sheep.$api.coupon.get(id);
+		if (error === 0) {
+			uni.showToast({
+				title: msg,
+			});
+			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), // 商品主图
+			price: state.goodsInfo.price[0], // 商品价格
+			original_price: state.goodsInfo.original_price, // 商品原价
+		}, );
+	});
+
+	onLoad(async (options) => {
+		// 非法参数
+		if (!options.id) {
+			state.goodsInfo = null;
+			return;
+		}
+		state.goodsId = options.id;
+		// 1. 加载商品信息
+		sheep.$api.goods.detail(state.goodsId).then((res) => {
+      // 未找到商品
+      if (res.code !== 0 || !res.data) {
+        state.goodsInfo = null;
+        return;
+      }
+			// 加载到商品
+			state.skeletonLoading = false;
+      state.goodsInfo = res.data;
+		});
+
+    // 2. 加载优惠劵信息
+    CouponApi.getCouponTemplateList(state.goodsId,2, 10).then((res) => {
+      if (res.code !== 0) {
+        return;
+      }
+      state.couponInfo = res.data;
+    });
+
+    // 3. 加载营销活动信息
+    ActivityApi.getActivityListBySpuId(state.goodsId).then((res) => {
+      if (res.code !== 0) {
+        return;
+      }
+      state.activityList = res.data;
+    });
+	});
+</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>

+ 383 - 0
pages/goods/list.vue

@@ -0,0 +1,383 @@
+<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"
+          ></su-tabs>
+        </view>
+        <view class="list-icon" @tap="state.iconStatus = !state.iconStatus">
+          <text v-if="state.iconStatus" class="sicon-goods-list"></text>
+          <text v-else class="sicon-goods-card"></text>
+        </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.data"
+        :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 })"
+        ></s-goods-column>
+      </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"> </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"
+            :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"> </button>
+            </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-empty>
+  </s-layout>
+</template>
+
+<script setup>
+  import { reactive } from 'vue';
+  import { onLoad, onReachBottom } from '@dcloudio/uni-app';
+  import sheep from '@/sheep';
+  import _ from 'lodash';
+
+  const sys_navBar = sheep.$platform.navbar;
+  const emits = defineEmits(['close', 'change']);
+
+  const pagination = {
+    data: [],
+    current_page: 1,
+    total: 1,
+    last_page: 1,
+  };
+  const state = reactive({
+    pagination: {
+      data: [],
+      current_page: 1,
+      total: 1,
+      last_page: 1,
+    },
+    // currentSort: 'weigh',
+    // currentOrder: 'desc',
+    currentTab: 0,
+    filterParams: {},
+    curFilter: 0,
+    showFilter: false,
+    iconStatus: false,
+    categoryId: 0,
+    tabList: [
+      {
+        name: '综合推荐',
+        // value: '',
+        list: [
+          {
+            label: '综合推荐',
+            // sort: '',
+            // order: true,
+          },
+          {
+            label: '价格升序',
+            sort: 'price',
+            order: true,
+          },
+          {
+            label: '价格降序',
+            sort: 'price',
+            order: false,
+          },
+        ],
+      },
+      {
+        name: '销量',
+        // value: 'salesCount',
+      },
+      {
+        name: '新品优先',
+        // value: 'create_time',
+      },
+    ],
+    loadStatus: '',
+    keyword: '',
+    leftGoodsList: [],
+    rightGoodsList: [],
+  });
+
+  // 加载瀑布流
+  let count = 0;
+  let leftHeight = 0;
+  let rightHeight = 0;
+
+  function mountMasonry(height = 0, where = 'left') {
+    if (!state.pagination.data[count]) return;
+
+    if (where === 'left') {
+      leftHeight += height;
+    } else {
+      rightHeight += height;
+    }
+    if (leftHeight <= rightHeight) {
+      state.leftGoodsList.push(state.pagination.data[count]);
+    } else {
+      state.rightGoodsList.push(state.pagination.data[count]);
+    }
+    count++;
+  }
+
+  function emptyList() {
+    state.pagination = pagination
+    state.leftGoodsList = [];
+    state.rightGoodsList = [];
+    count = 0;
+    leftHeight = 0;
+    rightHeight = 0;
+  }
+  //搜索
+  function onSearch(e) {
+    state.keyword = e;
+    emptyList();
+    getList(state.currentSort, state.currentOrder, state.categoryId, e);
+  }
+
+  // 点击
+  function onTabsChange(e) {
+    if (state.tabList[e.index].list) {
+      state.currentTab = e.index;
+      state.showFilter = !state.showFilter;
+      return;
+    } else {
+      state.showFilter = false;
+    }
+    if (e.index === state.currentTab) {
+      return;
+    } else {
+      state.currentTab = e.index;
+    }
+    emptyList();
+
+    getList(e.value, state.currentOrder, state.categoryId, state.keyword);
+  }
+
+  // 点击筛选项
+  const onFilterItem = (val) => {
+	  console.log(val)
+    if (
+      state.currentSort === state.tabList[0].list[val].sort &&
+      state.currentOrder === state.tabList[0].list[val].order
+    ) {
+      state.showFilter = false;
+      return;
+    }
+    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(state.currentSort, state.currentOrder, state.categoryId, state.keyword);
+    state.showFilter = false;
+  };
+
+  async function getList(Sort, Order, categoryId, keyword, page = 1, list_rows = 6) {
+    state.loadStatus = 'loading';
+    const res = await sheep.$api.goods.list({
+      sortField: Sort,
+      sortAsc: Order,
+      category_id: !keyword ? categoryId : '',
+      pageSize:list_rows,
+      keyword: keyword,
+      pageNo:page,
+    });
+    if (res.code === 0) {
+        let couponList = _.concat(state.pagination.data, res.data.list);
+        state.pagination = {
+          ...res.data,
+          data: couponList,
+        };
+      mountMasonry();
+      if (state.pagination.current_page < state.pagination.last_page) {
+        state.loadStatus = 'more';
+      } else {
+        state.loadStatus = 'noMore';
+      }
+    }
+  }
+  // 加载更多
+  function loadmore() {
+    if (state.loadStatus !== 'noMore') {
+      getList(
+        state.currentSort,
+        state.currentOrder,
+        state.categoryId,
+        state.keyword,
+        state.pagination.current_page + 1,
+      );
+    }
+  }
+  onLoad((options) => {
+    state.categoryId = options.categoryId;
+    state.keyword = options.keyword;
+    getList(state.currentSort, state.currentOrder, state.categoryId, state.keyword);
+  });
+  // 上拉加载更多
+  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>

+ 368 - 0
pages/goods/score.vue

@@ -0,0 +1,368 @@
+<template>
+  <view>
+    <s-layout :onShareAppMessage="state.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>
+        <!-- 商品轮播图  -->
+        <su-swiper
+          class="ss-m-b-14 detail-swiper-selector"
+          isPreview
+          :list="state.goodsSwiper"
+          dotStyle="tag"
+          imageMode="widthFix"
+          dotCur="bg-mask-40"
+          :seizeHeight="750"
+        />
+
+        <!-- 价格+标题 -->
+        <view class="title-card detail-card ss-p-y-40 ss-p-x-20">
+          <view class="ss-flex ss-row-between ss-col-center ss-m-b-18">
+            <view class="price-box ss-flex ss-col-bottom">
+              <view v-if="goodsPrice.price > 0" class="price-text"> ¥{{ goodsPrice.price }} </view>
+              <text v-if="goodsPrice.price > 0 && goodsPrice.score > 0">+</text>
+              <image
+                :src="sheep.$url.static('/static/img/shop/goods/score1.svg')"
+                class="score-img"
+              ></image>
+              <view class="score-text ss-m-r-16">
+                {{ goodsPrice.score }}
+              </view>
+            </view>
+            <view class="sales-text">
+              {{ formatExchange(state.goodsInfo.sales_show_type, state.goodsInfo.sales) }}
+            </view>
+          </view>
+          <view class="origin-price-text ss-m-b-60" v-if="state.goodsInfo.original_price">
+            原价:¥{{ state.selectedSkuPrice.original_price || state.goodsInfo.original_price }}
+          </view>
+          <view class="title-text ss-line-2 ss-m-b-6">{{ state.goodsInfo.title }}</view>
+          <view class="subtitle-text ss-line-1">{{ state.goodsInfo.subtitle }}</view>
+        </view>
+
+        <!-- 功能卡片 -->
+        <view class="detail-cell-card detail-card ss-flex-col">
+          <detail-cell-sku
+            v-model="state.selectedSkuPrice.goods_sku_text"
+            :skus="state.goodsInfo.skus"
+            @tap="state.showSelectSku = true"
+          />
+          <detail-cell-service v-model="state.goodsInfo.service" />
+          <detail-cell-params v-model="state.goodsInfo.params" />
+        </view>
+        <!-- 规格与数量弹框 -->
+        <s-select-sku
+          :goodsInfo="state.goodsInfo"
+          :show="state.showSelectSku"
+          :isScore="true"
+          @addCart="onAddCart"
+          @buy="onBuy"
+          @change="onSkuChange"
+          @close="state.showSelectSku = false"
+        />
+
+        <!-- 评价 -->
+        <view class="detail-comment-selector">
+          <detail-comment-card :goodsId="state.goodsId" />
+        </view>
+
+        <!-- 详情 -->
+        <view class="detail-content-selector"></view>
+        <detail-content-card :content="state.goodsInfo.content" />
+
+        <!-- 详情tabbar -->
+        <detail-tabbar v-model="state.goodsInfo" :shareIcon="false" :collectIcon="false">
+          <!-- TODO: 缺货中 已售罄 判断 设计-->
+          <view class="buy-box ss-flex ss-col-center ss-p-r-20" v-if="state.goodsInfo.stock > 0">
+            <button class="ss-reset-button buy-btn" @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>
+      </block>
+    </s-layout>
+  </view>
+</template>
+
+<script setup>
+  import { reactive, computed } from 'vue';
+  import { onLoad, onPageScroll } from '@dcloudio/uni-app';
+  import sheep from '@/sheep';
+  import { isEmpty } from 'lodash';
+  import { formatExchange, formatGoodsSwiper } from '@/sheep/hooks/useGoods';
+  import detailNavbar from './components/detail/detail-navbar.vue';
+  import detailCellSku from './components/detail/detail-cell-sku.vue';
+  import detailCellService from './components/detail/detail-cell-service.vue';
+  import detailCellParams from './components/detail/detail-cell-params.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';
+
+  const headerBg = sheep.$url.css('/static/img/shop/goods/score-bg.png');
+  const seckillBg = sheep.$url.css('/static/img/shop/goods/seckill-tip-bg.png');
+  const grouponBg = sheep.$url.css('/static/img/shop/goods/seckill-tip-bg.png');
+
+  onPageScroll(() => {});
+
+  const state = reactive({
+    goodsId: 0,
+    skeletonLoading: true,
+    goodsInfo: {},
+    showSelectSku: false,
+    goodsSwiper: [],
+    selectedSkuPrice: {},
+    shareInfo: {},
+    showModel: false,
+    total: 0,
+    couponInfo: [],
+  });
+
+  const goodsPrice = computed(() => {
+    let price, score;
+    if (isEmpty(state.selectedSkuPrice)) {
+      price = state.goodsInfo.price[0];
+      score = state.goodsInfo.score || 0;
+    } else {
+      price = state.selectedSkuPrice.price;
+      score = state.selectedSkuPrice.score || 0;
+    }
+    return { price, score };
+  });
+
+  // 规格变更
+  function onSkuChange(e) {
+    state.selectedSkuPrice = e;
+  }
+  // 格式化价格
+  function formatPrice(e) {
+    if (Number(e[0]) > 0) {
+      return e.length === 1 ? e[0] : e.join('~');
+    } else {
+      return '';
+    }
+  }
+  // 添加购物车
+  function onAddCart(e) {
+    sheep.$store('cart').add(e);
+  }
+  // 立即购买
+  function onBuy(e) {
+    sheep.$router.go('/pages/order/confirm', {
+      data: JSON.stringify({
+        order_type: 'score',
+        goods_list: [
+          {
+            goods_id: e.goods_id,
+            goods_num: e.goods_num,
+            goods_sku_price_id: e.id,
+          },
+        ],
+      }),
+    });
+  }
+
+  onLoad((options) => {
+    // 非法参数
+    if (!options.id) {
+      state.goodsInfo = null;
+      return;
+    }
+    state.goodsId = options.id;
+    // 加载商品信息
+    sheep.$api.app.scoreShop.detail(state.goodsId).then((res) => {
+      state.skeletonLoading = false;
+      if (res.error === 0) {
+        state.goodsInfo = res.data;
+        state.goodsSwiper = formatGoodsSwiper(state.goodsInfo.images);
+      } else {
+        // 未找到商品
+        state.goodsInfo = null;
+      }
+    });
+  });
+</script>
+
+<style lang="scss" scoped>
+  .detail-card {
+    background-color: #ffff;
+    margin: 14rpx 20rpx;
+    border-radius: 10rpx;
+    overflow: hidden;
+  }
+
+  // 价格标题卡片
+  .title-card {
+    width: 710rpx;
+    box-sizing: border-box;
+    background-size: 100% 100%;
+    border-radius: 10rpx;
+    background-image: v-bind(headerBg);
+    background-repeat: no-repeat;
+    .price-box {
+      .score-img {
+        width: 36rpx;
+        height: 36rpx;
+        margin: 0 4rpx;
+      }
+      .score-text {
+        font-size: 42rpx;
+        font-weight: 500;
+        color: #ff3000;
+        line-height: 36rpx;
+        font-family: OPPOSANS;
+      }
+      .price-text {
+        font-size: 42rpx;
+        font-weight: 500;
+        color: #ff3000;
+        line-height: 36rpx;
+        font-family: OPPOSANS;
+      }
+    }
+    .origin-price-text {
+      font-size: 26rpx;
+      font-weight: 400;
+      text-decoration: line-through;
+      color: $gray-c;
+      font-family: OPPOSANS;
+    }
+
+    .sales-text {
+      font-size: 26rpx;
+      font-weight: 500;
+      color: $gray-c;
+    }
+
+    .discounts-box {
+      .discounts-tag {
+        padding: 4rpx 10rpx;
+        font-size: 24rpx;
+        font-weight: 500;
+        border-radius: 4rpx;
+        color: var(--ui-BG-Main);
+        // background: rgba(#2aae67, 0.05);
+        background: var(--ui-BG-Main-tag);
+      }
+
+      .discounts-title {
+        font-size: 24rpx;
+        font-weight: 500;
+        color: var(--ui-BG-Main);
+        line-height: normal;
+      }
+
+      .cicon-forward {
+        color: var(--ui-BG-Main);
+        font-size: 24rpx;
+        line-height: normal;
+        margin-top: 4rpx;
+      }
+    }
+
+    .title-text {
+      font-size: 30rpx;
+      font-weight: bold;
+      line-height: 42rpx;
+    }
+
+    .subtitle-text {
+      font-size: 26rpx;
+      font-weight: 400;
+      color: $dark-9;
+      line-height: 42rpx;
+    }
+  }
+
+  // 购买
+  .buy-box {
+    .buy-btn {
+      width: 630rpx;
+      height: 80rpx;
+      border-radius: 40rpx;
+      background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
+      color: $white;
+    }
+    .disabled-btn {
+      width: 630rpx;
+      height: 80rpx;
+      border-radius: 40rpx;
+      background: #999999;
+      color: $white;
+    }
+  }
+
+  //秒杀卡片
+  .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 {
+    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>

+ 534 - 0
pages/goods/seckill.vue

@@ -0,0 +1,534 @@
+<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">
+                {{ state.selectedSkuPrice.price || formatPrice(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.original_price">
+              原价
+              <view class="origin-price-text">
+                {{ state.selectedSkuPrice.original_price || state.goodsInfo.original_price }}
+              </view>
+            </view>
+            <detail-progress :percent="state.percent" />
+          </view>
+
+          <view class="title-text ss-line-2 ss-m-b-6">{{ state.goodsInfo.title }}</view>
+          <view class="subtitle-text ss-line-1">{{ state.goodsInfo.subtitle }}</view>
+        </view>
+
+        <!-- 功能卡片 -->
+        <view class="detail-cell-card detail-card ss-flex-col">
+          <detail-cell-sku
+            v-model="state.selectedSkuPrice.goods_sku_text"
+            :skus="state.goodsInfo.skus"
+            @tap="state.showSelectSku = true"
+          />
+          <detail-cell-service v-model="state.goodsInfo.service" />
+          <detail-cell-params v-model="state.goodsInfo.params" />
+        </view>
+        <!-- 规格与数量弹框 -->
+        <s-select-seckill-sku
+          v-model="state.goodsInfo"
+          :show="state.showSelectSku"
+          @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.content" />
+
+      <!-- 详情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.original_price"
+            @tap="sheep.$router.go('/pages/goods/index', { id: state.goodsInfo.id })"
+          >
+            <view>
+              <view class="btn-price">{{ state.goodsInfo.original_price }}</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 || state.goodsInfo.activity.status != 'ing' ? '' : ''
+              "
+              >秒杀价</view
+            >
+          </button>
+          <button
+            class="ss-reset-button btn-box ss-flex-col"
+            @tap="state.showSelectSku = true"
+            :class="
+              state.goodsInfo.activity.status === 'ing' && state.goodsInfo.stock != 0
+                ? 'check-btn-box'
+                : 'disabled-btn-box'
+            "
+            :disabled="state.goodsInfo.stock === 0 || state.goodsInfo.activity.status != 'ing'"
+          >
+            <view class="btn-price">{{ state.goodsInfo.price[0] }}</view>
+            <view v-if="state.goodsInfo.activity.status === 'ing'">
+              <view v-if="state.goodsInfo.stock === 0">已售罄</view>
+              <view v-else>立即秒杀</view>
+            </view>
+            <view v-else>{{ state.goodsInfo.activity.status_text }}</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 { useDurationTime, formatGoodsSwiper, formatPrice } from '@/sheep/hooks/useGoods';
+  import detailNavbar from './components/detail/detail-navbar.vue';
+  import detailCellSku from './components/detail/detail-cell-sku.vue';
+  import detailCellService from './components/detail/detail-cell-service.vue';
+  import detailCellParams from './components/detail/detail-cell-params.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';
+
+  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({
+    goodsId: 0,
+    skeletonLoading: true,
+    goodsInfo: {},
+    showSelectSku: false,
+    goodsSwiper: [],
+    selectedSkuPrice: {},
+    showModel: false,
+    total: 0,
+    percent: 0,
+    price: '',
+  });
+
+  // 倒计时
+  const endTime = computed(() => {
+    return useDurationTime(state.goodsInfo.activity.end_time);
+  });
+
+  // 规格变更
+  function onSkuChange(e) {
+    state.selectedSkuPrice = e;
+  }
+
+  // 立即购买
+  function onBuy(e) {
+    sheep.$router.go('/pages/order/confirm', {
+      data: JSON.stringify({
+        order_type: 'goods',
+        buy_type: 'seckill',
+        activity_id: state.goodsInfo.activity.id,
+        goods_list: [
+          {
+            goods_id: e.goods_id,
+            goods_num: e.goods_num,
+            goods_sku_price_id: e.id,
+          },
+        ],
+      }),
+    });
+  }
+
+  const shareInfo = computed(() => {
+    if (isEmpty(state.goodsInfo?.activity)) return {};
+    return sheep.$platform.share.getShareInfo(
+      {
+        title: state.goodsInfo.title,
+        image: sheep.$url.cdn(state.goodsInfo.image),
+        params: {
+          page: '4',
+          query: state.goodsInfo.id + ',' + state.goodsInfo.activity.id,
+        },
+      },
+      {
+        type: 'goods', // 商品海报
+        title: state.goodsInfo.title, // 商品标题
+        image: sheep.$url.cdn(state.goodsInfo.image), // 商品主图
+        price: state.goodsInfo.price[0], // 商品价格
+        original_price: state.goodsInfo.original_price, // 商品原价
+      },
+    );
+  });
+
+  onLoad((options) => {
+    // 非法参数
+    if (!options.id) {
+      state.goodsInfo = null;
+      return;
+    }
+    state.goodsId = options.id;
+    // 加载商品信息
+    sheep.$api.goods
+      .detail(options.id, {
+        activity_id: options.activity_id,
+      })
+      .then((res) => {
+        state.skeletonLoading = false;
+        if (res.error === 0) {
+          state.goodsInfo = res.data;
+          state.percent =
+            state.goodsInfo.stock + state.goodsInfo.sales > 0
+              ? (
+                  (state.goodsInfo.sales / (state.goodsInfo.sales + state.goodsInfo.stock)) *
+                  100
+                ).toFixed(2)
+              : 0;
+          state.percent = Number(state.percent);
+          state.goodsSwiper = formatGoodsSwiper(state.goodsInfo.images);
+        } else {
+          // 未找到商品
+          state.goodsInfo = null;
+        }
+      });
+  });
+</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>

+ 90 - 0
pages/index/index.vue

@@ -0,0 +1,90 @@
+<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';
+  import index2Api from '@/sheep/api/index2';
+	// 隐藏原生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));
+		}
+
+		// TODO 芋艿:测试接口的调用
+		sheep.$api.app.test();
+	});
+
+	// 下拉刷新
+	onPullDownRefresh(() => {
+		sheep.$store('app').init();
+		setTimeout(function() {
+			uni.stopPullDownRefresh();
+		}, 800);
+	});
+
+	onPageScroll(() => {});
+</script>
+
+<style></style>

+ 39 - 0
pages/index/login.vue

@@ -0,0 +1,39 @@
+<template>
+  <!-- 空登陆页 -->
+  <view></view>
+</template>
+
+<script setup>
+  import { isEmpty } from 'lodash';
+  import sheep from '@/sheep';
+  import { onLoad, onShow } from '@dcloudio/uni-app';
+
+  onLoad(async (options) => {
+    // #ifdef H5
+    let event = '';
+    if (options.login_code) {
+      event = 'login';
+      const { error } = await sheep.$platform.useProvider().login(options.login_code);
+      if (error === 0) {
+        sheep.$store('user').getInfo();
+      }
+    }
+    if (options.bind_code) {
+      event = 'bind';
+      const { error } = await sheep.$platform.useProvider().bind(options.bind_code);
+    }
+
+    // 检测H5登录回调
+    let returnUrl = uni.getStorageSync('returnUrl');
+    if (returnUrl) {
+      uni.removeStorage('returnUrl');
+      location.replace(returnUrl);
+    } else {
+      uni.switchTab({
+        url: '/',
+      });
+    }
+
+    // #endif
+  });
+</script>

+ 52 - 0
pages/index/page.vue

@@ -0,0 +1,52 @@
+<template>
+  <s-layout
+    :title="page.name"
+    navbar="custom"
+    :bgStyle="page.style?.background"
+    :navbarStyle="page.style?.navbar"
+    onShareAppMessage
+    showLeftButton
+  >
+    <s-block v-for="(item, index) in page.list" :key="index" :styles="item.style">
+      <s-block-item :type="item.type" :data="item.data" :styles="item.style" />
+    </s-block>
+  </s-layout>
+</template>
+
+<script setup>
+  import { computed, reactive } from 'vue';
+  import sheep from '@/sheep';
+  import { onLoad, onPageScroll } from '@dcloudio/uni-app';
+
+  const page = reactive({
+    name: '',
+    list: [],
+    style: {},
+  });
+  onLoad(async (options) => {
+    let id;
+
+    if (options.id) {
+      id = options.id;
+    }
+
+    // #ifdef MP
+    // 小程序预览自定义页面
+    if (options.scene) {
+      const sceneParams = decodeURIComponent(options.scene).split('=');
+      id = sceneParams[1];
+    }
+    // #endif
+
+    const { error, data } = await sheep.$api.app.page(id);
+    if (error === 0) {
+      page.name = data.name;
+      page.list = data.diypage?.page?.data;
+      page.style = data.diypage?.page?.style;
+    }
+  });
+
+  onPageScroll(() => {});
+</script>
+
+<style></style>

+ 113 - 0
pages/index/search.vue

@@ -0,0 +1,113 @@
+<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>

+ 41 - 0
pages/index/user.vue

@@ -0,0 +1,41 @@
+<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>

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

@@ -0,0 +1,318 @@
+<!-- 订单详情 -->
+<template>
+  <s-layout title="申请售后">
+    <!-- 售后商品 -->
+    <view class="goods-box">
+      <s-goods-item :img="state.goodsItem.goods_image" :title="state.goodsItem.goods_title"
+        :skuText="state.goodsItem.goods_sku_text" :price="state.goodsItem.goods_price"
+        :num="state.goodsItem.goods_num"></s-goods-item>
+    </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.refundTypeList" :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" v-if="formData.reason">{{ formData.reason }}</text>
+          <text class="ss-m-r-20" v-else>请选择申请原因~</text>
+          <!-- <text class="ss-iconfont _icon-forward" style="color: #666"></text> -->
+          <text class="cicon-forward" style="height: 28rpx"></text>
+        </view>
+      </view>
+      <view class="refund-item u-m-b-20">
+        <view class="item-title ss-m-b-20">联系方式</view>
+        <view class="input-box u-flex">
+          <uni-easyinput :inputBorder="false" type="number" v-model="formData.mobile" placeholder="请输入您的联系电话"
+            paddingLeft="10" />
+        </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.content" placeholder="客官~请描述您遇到的问题,建议上传照片"></uni-easyinput>
+          <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.refundReasonList" :key="item.value">
+              <view class="ss-flex-1 ss-p-20">{{ item.title }}</view>
+              <radio :value="item.value" color="var(--ui-BG-Main)" :checked="item.value === 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, unref } from 'vue';
+const form = ref(null);
+const state = reactive({
+  showModal: false,
+  currentValue: 0,
+  goodsItem: {},
+  reasonText: '',
+  //售后类型
+  refundTypeList: [
+    {
+      text: '仅退款',
+      value: 'refund',
+    },
+    {
+      text: '退/换货',
+      value: 'return',
+    },
+    {
+      text: '其他',
+      value: 'other',
+    },
+  ],
+  refundReasonList: [
+    {
+      value: '1',
+      title: '卖家发错货了',
+    },
+    {
+      value: '2',
+      title: '退运费',
+    },
+    {
+      value: '3',
+      title: '大小/重量与商品描述不符',
+    },
+    {
+      value: '4',
+      title: '生产日期/保质期与商品描述不符',
+    },
+    {
+      value: '5',
+      title: '质量问题',
+    },
+    {
+      value: '6',
+      title: '我不想要了',
+    },
+  ],
+});
+const formData = reactive({
+  type: '',
+  reason: '',
+  mobile: '',
+  content: '',
+  images: [],
+});
+const rules = reactive({});
+
+// 提交表单
+async function submit() {
+  // #ifdef MP
+  sheep.$platform.useProvider('wechat').subscribeMessage('order_aftersale_change');
+  // #endif
+  let data = {
+    ...formData,
+    order_id: state.goodsItem.order_id,
+    order_item_id: state.goodsItem.id,
+  };
+  const res = await sheep.$api.order.aftersale.apply(data);
+  if (res.error === 0) {
+    uni.showToast({
+      title: res.msg,
+    });
+    sheep.$router.go('/pages/order/aftersale/list');
+  }
+}
+
+//选择售后类型
+function onRefundChange(e) {
+  formData.type = e.detail.value;
+}
+
+//选择申请原因
+function onChange(e) {
+  state.currentValue = e.detail.value;
+  state.refundReasonList.forEach((item) => {
+    if (item.value === e.detail.value) {
+      state.reasonText = item.title;
+    }
+  });
+}
+//确定
+function onReason() {
+  formData.reason = state.reasonText;
+  state.showModal = false;
+}
+
+function onTitle(e, title) {
+  state.currentValue = e;
+  state.reasonText = title;
+}
+onLoad((options) => {
+  state.goodsItem = JSON.parse(options.item);
+});
+</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>

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

@@ -0,0 +1,355 @@
+<!-- 售后详情 -->
+<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',
+          },
+        ]">
+				<!-- <uni-steps :options="state.list" :active="state.active" active-color="#fff" /> -->
+				<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 && state.info.aftersale_status === -2) ||
+                  (state.list.length - 1 == index && state.info.aftersale_status === -1)
+                "></text>
+							<text class="sicon-circlecheck" v-else
+								:class="state.active >= index ? 'activity-color' : 'info-color'"></text>
+
+							<view v-if="state.list.length - 1 != index" class="line"
+								:class="state.active >= index ? 'activity-bg' : 'info-bg'"></view>
+						</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.aftersaleId })">
+				<view class="">
+					<view class="status-text">{{ state.info.aftersale_status_desc }}</view>
+					<view class="status-time">{{ state.info.update_time }}</view>
+				</view>
+				<text class="ss-iconfont _icon-forward" style="color: #666"></text>
+			</view> -->
+
+			<!-- 退款金额 -->
+			<view class="aftersale-money ss-flex ss-col-center ss-row-between">
+				<view class="aftersale-money--title">退款总额</view>
+				<view class="aftersale-money--num">¥{{ state.info.refundPrice/100 }}</view>
+			</view>
+			<!-- 服务商品 -->
+			<view class="order-shop">
+				<!-- 		<s-goods-item :title="state.info.goods_title" :price="state.info.goods_price"
+					:img="state.info.goods_image" priceColor="#333333" :titleWidth="480"
+					:skuText="state.info.goods_sku_text" :num="state.info.goods_num"></s-goods-item> -->
+				<s-goods-item :img=" state.info.picUrl" :title=" state.info.spuName" priceColor="#333333"
+					:titleWidth="480" :skuText=" state.info.properties.reduce((a,b)=>a+b.valueName+' ','')"
+					:price=" state.info.refundPrice/100" :num=" state.info.count"></s-goods-item>
+			</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">{{ status2[state.info.way] }}</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.btns?.includes('cancel')"
+					@tap="onApply(state.info.id)">取消申请</button>
+				<button class="ss-reset-button btn" v-if="state.info.btns?.includes('delete')"
+					@tap="onDelete(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';
+
+	const statusBarHeight = sheep.$platform.device.statusBarHeight * 2;
+	const headerBg = sheep.$url.css('/static/img/shop/order/order_bg.png');
+	const state = reactive({
+		active: 0,
+		aftersaleId: 0,
+		info: {},
+		list: [{
+				title: '提交申请',
+			},
+			{
+				title: '处理中',
+			},
+		],
+		loading: false,
+	});
+
+	const status2 = {
+		10: '仅退款',
+		20: '退货退款'
+	}
+
+	function onApply(orderId) {
+		uni.showModal({
+			title: '提示',
+			content: '确定要取消此申请吗?',
+			success: async function(res) {
+				if (res.confirm) {
+					const {
+						error
+					} = await sheep.$api.order.aftersale.cancel(orderId);
+					if (error === 0) {
+						getDetail(state.aftersaleId);
+					}
+				}
+			},
+		});
+	}
+
+	function onDelete(orderId) {
+		uni.showModal({
+			title: '提示',
+			content: '确定要删除吗?',
+			success: async function(res) {
+				if (res.confirm) {
+					const {
+						error
+					} = await sheep.$api.order.aftersale.delete(orderId);
+					if (error === 0) {
+						sheep.$router.back();
+					}
+				}
+			},
+		});
+	}
+	const onCopy = () => {
+		sheep.$helper.copyText(state.info.aftersale_sn);
+	};
+	async function getDetail(id) {
+		const {
+			code,
+			data
+		} = await sheep.$api.order.aftersale.detail(id);
+		state.loading = true;
+		if (code === 0) {
+			state.info = data;
+			if (state.info.aftersale_status === -2 || state.info.aftersale_status === -1) {
+				state.list.push({
+					title: state.info.aftersale_status_text
+				});
+				state.active = 2;
+			} else {
+				state.list.push({
+					title: '完成'
+				});
+				state.active = state.info.aftersale_status;
+			}
+		} else {
+			state.info = null;
+		}
+	}
+	onLoad((options) => {
+		state.aftersaleId = 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>

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

@@ -0,0 +1,238 @@
+<!-- 售后列表 -->
+<template>
+	<s-layout title="售后列表">
+		<!-- tab -->
+		<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/data-empty.png" text="暂无数据">
+		</s-empty>
+		<!-- 列表 -->
+		<view v-if="state.pagination.total > 0">
+			<view class="list-box ss-m-y-20" v-for="order in state.pagination.data" :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">{{ status[order.status] }}</text>
+				</view>
+				<s-goods-item :img="order.picUrl" :title="order.spuName"
+					:skuText="order.properties.reduce((a,b)=>a+b.valueName+' ','')" :price="order.refundPrice/100"
+					:num="order.count"></s-goods-item>
+				<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">{{ status2[order.way] }}</view>
+						<!-- <view class="value">{{ order.aftersale_status_desc }}</view> -->
+						<view class="value">{{ order.applyReason }}</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">
+					<view>
+						<button class="ss-reset-button tool-btn" @tap.stop="onApply(order.id)"
+							v-if="order.btns.includes('cancel')">取消申请</button>
+					</view>
+					<view>
+						<button class="ss-reset-button tool-btn" @tap.stop="onDelete(order.id)"
+							v-if="order.btns.includes('delete')">删除</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 {
+		computed,
+		reactive
+	} from 'vue';
+	import _ from 'lodash';
+
+	const pagination = {
+		data: [],
+		current_page: 1,
+		total: 1,
+		last_page: 1,
+	};
+	const state = reactive({
+		currentTab: 0,
+		showApply: false,
+		pagination: {
+			data: [],
+			current_page: 1,
+			total: 1,
+			last_page: 1,
+		},
+		loadStatus: '',
+	});
+	// 字典需要登录 尚未接入 先用固定值代替
+	const status = {
+		10: '申请售后',
+		20: '商品待退货',
+		30: '商家待收货',
+		40: '等待退款',
+		50: '退款成功',
+		61: '买家取消',
+		62: '商家拒绝',
+		63: '商家拒收货'
+	}
+	const status2 = {
+		10: '仅退款',
+		20: '退货退款'
+	}
+	const tabMaps = [{
+			name: '全部',
+			value: 'all',
+		},
+		// {
+		//   name: '申请中',
+		//   value: 'nooper',
+		// },
+		// {
+		//   name: '处理中',
+		//   value: 'ing',
+		// },
+		// {
+		//   name: '已完成',
+		//   value: 'completed',
+		// },
+		// {
+		//   name: '已拒绝',
+		//   value: 'refuse',
+		// },
+	];
+	// 切换选项卡
+	function onTabsChange(e) {
+		state.pagination = pagination
+		state.currentTab = e.index;
+		getOrderList();
+	}
+
+	// 获取售后列表
+	async function getOrderList(page = 1, list_rows = 5) {
+		pagination.current_page = page;
+		state.loadStatus = 'loading';
+		let res = await sheep.$api.order.aftersale.list({
+			// type: tabMaps[state.currentTab].value,
+			pageSize: list_rows,
+			pageNo: page,
+		});
+		console.log(res, '未处理前售后列表数据')
+		if (res.code === 0) {
+			let orderList = _.concat(state.pagination.data, res.data.list);
+
+			state.pagination = {
+				total: res.data.total,
+				...res.data,
+				data: orderList,
+			};
+			console.log(state.pagination, '售后订单数据')
+			// if (state.pagination.current_page < state.pagination.last_page) {
+			state.loadStatus = 'more';
+			// } else {
+			// state.loadStatus = 'noMore';
+			// }
+		}
+	}
+
+	function onApply(orderId) {
+		uni.showModal({
+			title: '提示',
+			content: '确定要取消此申请吗?',
+			success: async function(res) {
+				if (res.confirm) {
+					const {
+						error
+					} = await sheep.$api.order.aftersale.cancel(orderId);
+					if (error === 0) {
+						state.pagination = pagination
+						getOrderList();
+					}
+				}
+			},
+		});
+	}
+
+	function onDelete(orderId) {
+		uni.showModal({
+			title: '提示',
+			content: '确定要删除吗?',
+			success: async function(res) {
+				if (res.confirm) {
+					const {
+						error
+					} = await sheep.$api.order.aftersale.delete(orderId);
+					if (error === 0) {
+						state.pagination = pagination
+						getOrderList();
+					}
+				}
+			},
+		});
+	}
+
+	onLoad(async (options) => {
+		if (options.type) {
+			state.currentTab = options.type;
+		}
+		getOrderList();
+	});
+
+	// 加载更多
+	function loadmore() {
+		// if (state.loadStatus !== 'noMore') {
+		getOrderList(pagination.current_page + 1);
+		// }
+	}
+
+	// 上拉加载更多
+	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>

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

@@ -0,0 +1,99 @@
+<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' : ''"></text>
+      <view v-if="data.length - 1 != index" class="line"></view>
+    </view>
+    <view>
+      <view class="text">{{ item.log_type_text }}</view>
+      <mp-html class="richtext" :content="item.content"></mp-html>
+      <view class="" v-if="item.images?.length">
+        <scroll-view class="scroll-box" scroll-x scroll-anchoring>
+          <view class="ss-flex">
+            <view v-for="i in item.images" :key="i" class="ss-m-r-20">
+              <su-image
+                class="content-img"
+                isPreview
+                :previewList="state.commentImages"
+                :current="index"
+                :src="i"
+                :height="120"
+                :width="120"
+                mode="aspectFit"
+              ></su-image>
+            </view>
+          </view>
+        </scroll-view>
+      </view>
+      <view class="date">{{ item.create_time }}</view>
+    </view>
+  </view>
+</template>
+<script setup>
+  import sheep from '@/sheep';
+  import { reactive } from 'vue';
+  const props = defineProps({
+    item: {
+      type: Object,
+      default() {},
+    },
+    index: {
+      type: Number,
+      default: 0,
+    },
+    data: {
+      type: Object,
+      default() {},
+    },
+  });
+  const state = reactive({
+    commentImages: [],
+  });
+  props.item.images?.forEach((i) => {
+    state.commentImages.push(sheep.$url.cdn(i));
+  });
+</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>

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

@@ -0,0 +1,54 @@
+<!-- 售后进度  -->
+<template>
+  <s-layout title="售后进度">
+    <view class="log-box">
+      <view  v-for="(item, index) in state.info" :key="item.title">
+        <log-item :item="item" :index="index" :data="state.info"></log-item>
+      </view>
+    </view>
+  </s-layout>
+</template>
+
+<script setup>
+  import sheep from '@/sheep';
+  import { onLoad } from '@dcloudio/uni-app';
+  import { computed, reactive } from 'vue';
+  import logItem from './log-item.vue';
+
+  const state = reactive({
+    active: 1,
+    list: [
+      {
+        title: '买家下单',
+        desc: '2018-11-11',
+      },
+      {
+        title: '卖家发货',
+        desc: '2018-11-12',
+      },
+      {
+        title: '买家签收',
+        desc: '2018-11-13',
+      },
+      {
+        title: '交易完成',
+        desc: '2018-11-14',
+      },
+    ],
+  });
+  async function getDetail(id) {
+    const { data } = await sheep.$api.order.aftersale.detail(id);
+    state.info = data.logs;
+  }
+  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>

+ 414 - 0
pages/order/confirm.vue

@@ -0,0 +1,414 @@
+<template>
+	<s-layout title="确认订单">
+		<!-- 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"></text></view>
+			</s-address-item>
+		</view>
+		<view class="order-card-box ss-m-b-14">
+			<s-goods-item v-for="item in state.orderInfo.goods_list" :key="item.goods_id"
+				:img="item.current_sku_price.image || item.goods.image" :title="item.goods.title"
+				:skuText="item.current_sku_price?.goods_sku_text" :price="item.current_sku_price.price"
+				:num="item.goods_num" marginBottom="10">
+				<template #top>
+					<view class="order-item ss-flex ss-col-center ss-row-between ss-p-x-20 bg-white">
+						<view class="item-title">配送方式</view>
+						<view class="ss-flex ss-col-center">
+							<text class="item-value">{{ item.dispatch_type_text }}</text>
+						</view>
+					</view>
+				</template>
+			</s-goods-item>
+
+			<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"></uni-easyinput>
+				</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">¥{{ state.orderInfo.goods_amount }}</text>
+					</view>
+				</view>
+				<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"></image>
+						<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">+¥{{ state.orderInfo.dispatch_amount }}</text>
+					</view>
+				</view>
+				<view class="order-item ss-flex ss-col-center ss-row-between"
+					v-if="state.orderPayload.order_type != 'score'">
+					<!-- <view v-if="state.orderInfo.coupon_discount_fee > 0" class="order-item ss-flex ss-col-center ss-row-between"> -->
+					<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.coupon_id">-¥{{ state.orderInfo.coupon_discount_fee }}</text>
+						<text class="item-value"
+							:class="state.couponInfo.can_use?.length > 0 ? 'text-red' : 'text-disabled'" v-else>{{
+                state.couponInfo.can_use?.length > 0
+                  ? state.couponInfo.can_use?.length + '张可用'
+                  : '暂无可用优惠券'
+              }}</text>
+
+						<text class="_icon-forward item-icon"></text>
+					</view>
+				</view>
+				<view class="order-item ss-flex ss-col-center ss-row-between"
+					v-if="state.orderInfo.promo_infos?.length">
+					<!-- <view v-if="state.orderInfo.promo_discount_fee > 0" class="order-item ss-flex ss-col-center ss-row-between"> -->
+					<view class="item-title">活动优惠</view>
+					<view class="ss-flex ss-col-center" @tap="state.showDiscount = true">
+						<text class="item-value text-red"> -¥{{ state.orderInfo.promo_discount_fee }} </text>
+						<text class="_icon-forward item-icon"></text>
+					</view>
+				</view>
+			</view>
+			<view class="total-box-footer ss-font-28 ss-flex ss-row-right ss-col-center ss-m-r-28">
+				<view class="total-num ss-m-r-20">共{{ state.totalNumber }}件</view>
+				<view>合计:</view>
+				<view class="total-num text-red"> ¥{{ state.orderInfo.pay_fee }} </view>
+				<view class="ss-flex" v-if="state.orderPayload.order_type === 'score'">
+					<view class="total-num ss-font-30 text-red ss-m-l-4"> + </view>
+					<image :src="sheep.$url.static('/static/img/shop/goods/score1.svg')" class="score-img"></image>
+					<view class="total-num ss-font-30 text-red">{{ state.orderInfo.score_amount }}</view>
+				</view>
+			</view>
+		</view>
+		<!-- 发票 -->
+		<view class="bg-white ss-p-20 ss-r-20">
+			<view class="order-item ss-flex ss-col-center ss-row-between">
+				<view class="item-title">发票申请</view>
+				<view class="ss-flex ss-col-center" @tap="onSelectInvoice">
+					<text class="item-value">{{ state.invoiceInfo.name || '无需开具发票' }}</text>
+					<text class="_icon-forward item-icon"></text>
+				</view>
+			</view>
+		</view>
+		<!-- 选择优惠券弹框 -->
+		<s-coupon-select v-model="state.couponInfo" :show="state.showCoupon" @confirm="onSelectCoupon"
+			@close="state.showCoupon = false" />
+		<!-- 满额折扣弹框  -->
+		<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"> ¥{{ state.orderInfo.pay_fee }} </view>
+					<view v-if="state.orderPayload.order_type === 'score'" class="ss-flex">
+						<view class="total-num ss-font-30 text-red ss-m-l-4">+</view>
+						<image :src="sheep.$url.static('/static/img/shop/goods/score1.svg')" class="score-img"></image>
+						<view class="total-num ss-font-30 text-red">{{ state.orderInfo.score_amount }}</view>
+					</view>
+				</view>
+
+				<button class="ss-reset-button ui-BG-Main-Gradient ss-r-40 submit-btn ui-Shadow-Main" @tap="onConfirm">
+					{{ exchangeNow ? '立即兑换' : '提交订单' }}
+				</button>
+			</view>
+		</su-fixed>
+	</s-layout>
+</template>
+
+<script setup>
+	import {
+		reactive,
+		computed
+	} from 'vue';
+	import {
+		onLoad,
+		onPageScroll,
+		onShow
+	} from '@dcloudio/uni-app';
+	import sheep from '@/sheep';
+	import {
+		isEmpty
+	} from 'lodash';
+
+	const state = reactive({
+		orderPayload: {},
+		orderInfo: {},
+		addressInfo: {},
+		invoiceInfo: {},
+		totalNumber: 0,
+		showCoupon: false,
+		couponInfo: [],
+		showDiscount: false,
+	});
+
+	// 立即兑换(立即兑换无需跳转收银台)
+	const exchangeNow = computed(
+		() => state.orderPayload.order_type === 'score' && state.orderInfo.pay_fee == 0,
+	);
+
+	// 选择地址
+	function onSelectAddress() {
+		uni.$once('SELECT_ADDRESS', (e) => {
+			changeConsignee(e.addressInfo);
+		});
+		sheep.$router.go('/pages/user/address/list');
+	}
+
+	// 更改收货人地址&计算订单信息
+	async function changeConsignee(addressInfo = {}) {
+		if (isEmpty(addressInfo)) {
+			const {
+				code,
+				data
+			} = await sheep.$api.user.address.default();
+			console.log(data, '默认收货地址');
+			if (code === 0 && !isEmpty(data)) {
+				console.log('执行赋值')
+				addressInfo = data;
+			}
+		}
+		if (!isEmpty(addressInfo)) {
+			state.addressInfo = addressInfo;
+			state.orderPayload.address_id = state.addressInfo.id;
+		}
+		getOrderInfo();
+	}
+
+	// 选择优惠券
+	async function onSelectCoupon(e) {
+		state.orderPayload.coupon_id = e || 0;
+		getOrderInfo();
+		state.showCoupon = false;
+	}
+
+	// 选择发票信息
+	function onSelectInvoice() {
+		uni.$once('SELECT_INVOICE', (e) => {
+			state.invoiceInfo = e.invoiceInfo;
+			state.orderPayload.invoice_id = e.invoiceInfo.id || 0;
+		});
+		sheep.$router.go('/pages/user/invoice/list');
+	}
+
+	// 提交订单/立即兑换
+	function onConfirm() {
+		if (!state.orderPayload.address_id && state.orderInfo.need_address === 1) {
+			sheep.$helper.toast('请选择收货地址');
+			return;
+		}
+
+		if (exchangeNow.value) {
+			uni.showModal({
+				title: '提示',
+				content: '确定使用积分立即兑换?',
+				cancelText: '再想想',
+				success: async function(res) {
+					if (res.confirm) {
+						submitOrder();
+					}
+				},
+			});
+		} else {
+			submitOrder();
+		}
+	}
+
+	// 创建订单&跳转
+	async function submitOrder() {
+		const {
+			error,
+			data
+		} = await sheep.$api.order.create(state.orderPayload);
+		if (error === 0) {
+			// 更新购物车列表
+			if (state.orderPayload.from === 'cart') {
+				sheep.$store('cart').getList();
+			}
+			if (exchangeNow.value) {
+				sheep.$router.redirect('/pages/pay/result', {
+					orderSN: data.order_sn,
+				});
+			} else {
+				sheep.$router.redirect('/pages/pay/index', {
+					orderSN: data.order_sn,
+				});
+			}
+		}
+	}
+
+	// 检查库存&计算订单价格
+	async function getOrderInfo() {
+		console.log(state.orderPayload, '计算价格传参')
+		// let {code, data} = await sheep.$api.order.calc(state.orderPayload);
+		// let data = await sheep.$api.order.calc(state.orderPayload);
+		console.log(state.orderPayload.items)
+		let data = await sheep.$api.order.calc({
+			deliveryType: 1,
+			pointStatus: false,
+			items: state.orderPayload.items
+		});
+		console.log(data, '修改后的获取订单详细数据')
+		return;
+		if (error === 0) {
+			state.totalNumber = 0;
+			state.orderInfo = data;
+			state.orderInfo.goods_list.forEach((item) => {
+				state.totalNumber += item.goods_num;
+			});
+		}
+	}
+
+	// 获取可用优惠券
+	async function getCoupons() {
+		const {
+			error,
+			data
+		} = await sheep.$api.order.coupons(state.orderPayload);
+		if (error === 0) {
+			state.couponInfo = data;
+		}
+	}
+
+	onLoad(async (options) => {
+		console.log(options)
+		if (options.data) {
+			state.orderPayload = JSON.parse(options.data);
+			changeConsignee();
+			if (state.orderPayload.order_type !== 'score') {
+				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>

+ 692 - 0
pages/order/detail.vue

@@ -0,0 +1,692 @@
+<!-- 订单详情 -->
+<template>
+	<s-layout title="订单详情" class="index-wrap" navbar="inner">
+		<!-- 订单状态 -->
+		<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_code == 'nosend' ||
+            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">{{ state.orderInfo.status_text }}</view>
+			</view>
+			<view class="ss-font-26 ss-m-x-20 ss-m-b-70">{{ state.orderInfo.status_desc }}</view>
+		</view>
+
+		<!-- 收货地址 -->
+		<view class="order-address-box" v-if="state.orderInfo.address">
+			<view class="ss-flex ss-col-center">
+				<text class="address-username">
+					{{ state.orderInfo.address.consignee }}
+				</text>
+				<text class="address-phone">{{ state.orderInfo.address.mobile }}</text>
+			</view>
+			<view class="address-detail">{{ addressText }}</view>
+		</view>
+
+		<view class="detail-goods" :style="[{ marginTop: state.orderInfo.address ? '0' : '-40rpx' }]">
+			<!-- 订单信息 -->
+			<view class="order-list" v-for="item in state.orderInfo.items" :key="item.goods_id">
+				<view class="order-card">
+					<s-goods-item @tap="onGoodsDetail(item.goods_id)" :img="item.goods_image" :title="item.goods_title"
+						:skuText="item.goods_sku_text" :price="item.goods_price" :score="state.orderInfo.score_amount"
+						:num="item.goods_num">
+						<!-- 						<template #top>
+							<view class="order-item ss-flex ss-col-center ss-row-between ss-p-x-20 bg-white">
+								<view class="item-title">配送方式</view>
+								<view class="ss-flex ss-col-center">
+									<text class="item-value ss-m-r-20">{{ item.dispatch_type_text }}</text>
+									<button class="ss-reset-button copy-btn" @tap="onDetail(item)" v-if="
+                      (item.dispatch_type === 'autosend' || item.dispatch_type === 'custom') &&
+                      item.dispatch_status !== 0
+                    ">详情</button>
+								</view>
+							</view>
+						</template>
+						<template #tool>
+							<view class="ss-flex">
+								<button class="ss-reset-button apply-btn" v-if="item.btns.includes('aftersale')"
+									@tap.stop="
+                    sheep.$router.go('/pages/order/aftersale/apply', {
+                      item: JSON.stringify(item),
+                    })
+                  ">
+									申请售后
+								</button>
+								<button class="ss-reset-button apply-btn" v-if="item.btns.includes('re_aftersale')"
+									@tap.stop="
+                    sheep.$router.go('/pages/order/aftersale/apply', {
+                      item: JSON.stringify(item),
+                    })
+                  ">
+									重新售后
+								</button>
+
+								<button class="ss-reset-button apply-btn" v-if="item.btns.includes('aftersale_info')"
+									@tap.stop="
+                    sheep.$router.go('/pages/order/aftersale/detail', {
+                      id: item.ext.aftersale_id,
+                    })
+                  ">
+									售后详情
+								</button>
+								<button class="ss-reset-button apply-btn" v-if="item.btns.includes('buy_again')"
+									@tap.stop="
+                    sheep.$router.go('/pages/goods/index', {
+                      id: item.goods_id,
+                    })
+                  ">
+									再次购买
+								</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.order_sn }}</text>
+					</view>
+					<button class="ss-reset-button copy-btn" @tap="onCopy">复制</button>
+				</view>
+				<view class="notice-item">
+					<text class="title">下单时间:</text>
+					<text class="detail">{{ state.orderInfo.create_time }}</text>
+				</view>
+				<view class="notice-item" v-if="state.orderInfo.paid_time">
+					<text class="title">支付时间:</text>
+					<text class="detail">{{ state.orderInfo.paid_time || '-' }}</text>
+				</view>
+				<view class="notice-item">
+					<text class="title">支付方式:</text>
+					<text class="detail">{{ state.orderInfo.pay_types_text?.join(',') || '-' }}</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"
+						v-if="Number(state.orderInfo.goods_amount) > 0">¥{{ state.orderInfo.goods_amount }}</text>
+					<view v-if="state.orderInfo.score_amount && Number(state.orderInfo.goods_amount) > 0"
+						class="detail">+</view>
+					<view class="price-text ss-flex ss-col-center" v-if="state.orderInfo.score_amount">
+						<image :src="sheep.$url.static('/static/img/shop/goods/score1.svg')" class="score-img"></image>
+						<view class="detail">{{ state.orderInfo.score_amount }}</view>
+					</view>
+				</view>
+			</view>
+			<view class="notice-item ss-flex ss-row-between">
+				<text class="title">运费</text>
+				<text class="detail">¥{{ state.orderInfo.dispatch_amount }}</text>
+			</view>
+			<view class="notice-item ss-flex ss-row-between" v-if="state.orderInfo.total_discount_fee > 0">
+				<text class="title">优惠金额</text>
+				<text class="detail">¥{{ state.orderInfo.total_discount_fee }}</text>
+			</view>
+			<view class="notice-item all-rpice-item ss-flex ss-m-t-20">
+				<text class="title">{{
+          ['paid', 'completed'].includes(state.orderInfo.status) ? '已付款' : '需付款'
+        }}</text>
+				<text class="detail all-price"
+					v-if="Number(state.orderInfo.pay_fee) > 0">¥{{ state.orderInfo.pay_fee }}</text>
+				<view v-if="
+            state.orderInfo.score_amount &&
+            Number(state.orderInfo.pay_fee) > 0 &&
+            ['paid', 'completed'].includes(state.orderInfo.status)
+          " class="detail all-price">+</view>
+				<view class="price-text ss-flex ss-col-center" v-if="
+            state.orderInfo.score_amount && ['paid', 'completed'].includes(state.orderInfo.status)
+          ">
+					<image :src="sheep.$url.static('/static/img/shop/goods/score1.svg')" class="score-img"></image>
+					<view class="detail all-price">{{ state.orderInfo.score_amount }}</view>
+				</view>
+			</view>
+			<view class="notice-item all-rpice-item ss-flex ss-m-t-20" v-if="refundFee > 0">
+				<text class="title">已退款</text>
+				<text class="detail all-price">¥{{ refundFee.toFixed(2) }}</text>
+			</view>
+		</view>
+
+		<!-- 底部按钮 -->
+		<!-- TODO: 查看物流、等待成团、评价完后返回页面没刷新页面 -->
+		<su-fixed bottom placeholder bg="bg-white" v-if="state.orderInfo.btns?.length">
+			<view class="footer-box ss-flex ss-col-center ss-row-right">
+				<button class="ss-reset-button cancel-btn" v-if="state.orderInfo.btns?.includes('cancel')"
+					@tap="onCancel(state.orderInfo.id)">取消订单</button>
+				<button class="ss-reset-button pay-btn ui-BG-Main-Gradient" v-if="state.orderInfo.btns?.includes('pay')"
+					@tap="onPay(state.orderInfo.order_sn)">继续支付</button>
+				<button class="ss-reset-button cancel-btn" v-if="state.orderInfo.btns?.includes('apply_refund')"
+					@tap="onRefund(state.orderInfo.id)">申请退款</button>
+				<button class="ss-reset-button cancel-btn" v-if="state.orderInfo.btns?.includes('groupon')" @tap="
+            sheep.$router.go('/pages/activity/groupon/detail', {
+              id: state.orderInfo.ext.groupon_id,
+            })
+          ">
+					{{ state.orderInfo.status_code === 'groupon_ing' ? '邀请拼团' : '拼团详情' }}
+				</button>
+				<button class="ss-reset-button cancel-btn" v-if="state.orderInfo.btns?.includes('express')"
+					@tap="onExpress(state.orderInfo.id)">查看物流</button>
+				<button class="ss-reset-button cancel-btn" v-if="state.orderInfo.btns?.includes('confirm')"
+					@tap="onConfirm(state.orderInfo.id)">确认收货</button>
+				<button class="ss-reset-button cancel-btn" v-if="state.orderInfo.btns?.includes('comment')"
+					@tap="onComment(state.orderInfo.id,state.orderInfo)">评价晒单</button>
+				<button v-if="state.orderInfo.btns?.includes('invoice')" class="ss-reset-button cancel-btn"
+					@tap.stop="onOrderInvoice(state.orderInfo.invoice?.id)">
+					查看发票
+				</button>
+				<button v-if="state.orderInfo.btns?.includes('re_apply_refund')" class="ss-reset-button cancel-btn"
+					@tap.stop="onRefund(state.orderInfo.id)">
+					重新退款
+				</button>
+			</view>
+		</su-fixed>
+	</s-layout>
+</template>
+
+<script setup>
+	import sheep from '@/sheep';
+	import {
+		onLoad
+	} from '@dcloudio/uni-app';
+	import {
+		computed,
+		reactive
+	} from 'vue';
+	import {
+		isEmpty
+	} from 'lodash';
+
+	const statusBarHeight = sheep.$platform.device.statusBarHeight * 2;
+	const headerBg = sheep.$url.css('/static/img/shop/order/order_bg.png');
+	const tradeManaged = computed(() => sheep.$store('app').has_wechat_trade_managed);
+
+	const state = reactive({
+		orderInfo: {},
+		merchantTradeNo: '', // 商户订单号
+		comeinType: '', // 进入订单详情的来源类型
+	});
+
+	const addressText = computed(() => {
+		let data = state.orderInfo.address;
+		if (data) {
+			return `${data.province_name} ${data.city_name} ${data.district_name} ${data.address}`;
+		}
+		return '';
+	});
+
+	// 复制
+	const onCopy = () => {
+		sheep.$helper.copyText(state.orderInfo.order_sn);
+	};
+	//退款总额
+	const refundFee = computed(() => {
+		let refundFee = 0;
+		state.orderInfo.items?.forEach((i) => {
+			refundFee += Number(i.refund_fee);
+		});
+		return refundFee;
+	});
+	// 去支付
+	function onPay(orderSN) {
+		sheep.$router.go('/pages/pay/index', {
+			orderSN,
+		});
+	}
+
+	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) {
+					const {
+						error,
+						data
+					} = await sheep.$api.order.cancel(orderId);
+					if (error === 0) {
+						getOrderDetail(data.order_sn);
+					}
+				}
+			},
+		});
+	}
+
+	// 申请退款
+	async function onRefund(orderId) {
+		uni.showModal({
+			title: '提示',
+			content: '确定要申请退款吗?',
+			success: async function(res) {
+				if (res.confirm) {
+					const {
+						error,
+						data
+					} = await sheep.$api.order.applyRefund(orderId);
+					if (error === 0) {
+						getOrderDetail(data.order_sn);
+					}
+				}
+			},
+		});
+	}
+
+	// 查看物流
+	async function onExpress(orderId) {
+		sheep.$router.go('/pages/order/express/list', {
+			orderId,
+		});
+	}
+
+	//确认收货
+	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 {
+			error,
+			data
+		} = await sheep.$api.order.confirm(orderId);
+		if (error === 0) {
+			getOrderDetail(data.order_sn);
+		}
+	}
+
+	// #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 onOrderInvoice(invoiceId) {
+		sheep.$router.go('/pages/order/invoice', {
+			invoiceId,
+		});
+	}
+
+	// 配送方式详情
+	function onDetail(item) {
+		sheep.$router.go('/pages/order/dispatch/content', {
+			id: item.order_id,
+			item_id: item.id,
+		});
+	}
+
+	// 评价
+	function onComment(orderSN, orderId) {
+		console.log(orderId);
+		// return;
+		uni.$once('SELECT_INVOICE', (e) => {
+			state.invoiceInfo = e.invoiceInfo;
+		});
+		sheep.$router.go('/pages/goods/comment/add', {
+			orderSN,
+			orderId
+		});
+	}
+	async function getOrderDetail(id) {
+		// 对详情数据进行适配
+		let res = {};
+		if (state.comeinType === 'wechat') {
+			res = await sheep.$api.order.detail(id, {
+				merchant_trade_no: state.merchantTradeNo,
+			});
+		} else {
+			res = await sheep.$api.order.detail(id);
+		}
+		console.log(res, '我的订单详情数据');
+		if (res.code === 0) {
+			let obj = {
+				10: ['待发货', '等待买家付款', ["apply_refund"]],
+				30: ['待评价', '等待买家评价', ["express", "comment"]]
+			}
+			res.data.status_text = obj[res.data.status][0];
+			res.data.status_desc = obj[res.data.status][1];
+			res.data.btns = obj[res.data.status][2];
+			res.data.address = {
+				province_name: res.data.receiverAreaName.split(' ')[0],
+				district_name: res.data.receiverAreaName.split(' ')[2],
+				city_name: res.data.receiverAreaName.split(' ')[1],
+				address: res.data.receiverDetailAddress,
+				consignee: res.data.receiverName,
+				mobile: res.data.receiverMobile,
+			}
+			res.data.pay_fee = res.data.payPrice / 100
+			res.data.create_time = sheep.$helper.timeFormat(res.data.createTime, 'yyyy-mm-dd hh:MM:ss')
+			res.data.order_sn = res.data.no
+			res.data.goods_amount = res.data.totalPrice / 100
+			res.data.dispatch_amount = res.data.deliveryPrice / 100
+			res.data.pay_types_text = res.data.payChannelName.split(',')
+			res.data.items = res.data.items.map(ite => {
+
+				return {
+					...ite,
+					goods_title: ite.spuName,
+					goods_num: ite.count,
+					goods_price: ite.price / 100,
+					goods_image: ite.picUrl,
+					goods_sku_text: ite.properties.reduce((it0, it1) => it0 + it1.valueName + ' ', '')
+				}
+			})
+			state.orderInfo = res.data;
+			console.log(state.orderInfo, '修改后数据')
+		} else {
+			sheep.$router.back();
+		}
+	}
+
+	onLoad(async (options) => {
+		let id = 0;
+		if (options.orderSN) {
+			id = options.orderSN;
+		}
+		if (options.id) {
+			id = options.id;
+		}
+		state.comeinType = options.comein_type;
+		if (state.comeinType === 'wechat') {
+			state.merchantTradeNo = options.merchant_trade_no;
+		}
+		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>

+ 84 - 0
pages/order/dispatch/content.vue

@@ -0,0 +1,84 @@
+<template>
+  <s-layout title="发货内容">
+    <view class="order-card ss-m-x-20 ss-r-20">
+      <s-goods-item
+        :img="state.data.goods_image"
+        :title="state.data.goods_title"
+        :skuText="state.data.goods_sku_text"
+        :price="state.data.goods_price"
+        :num="state.data.goods_num"
+        radius="20"
+      >
+        <template #priceSuffix>
+          <button class="ss-reset-button tag-btn" v-if="state.data.status_text">
+            {{ state.data.status_text }}
+          </button>
+        </template>
+      </s-goods-item>
+    </view>
+    <view class="bg-white ss-p-20 ss-m-x-20 ss-r-20">
+      <view class="title ss-m-b-26">发货信息</view>
+      <view v-if="state.data.ext?.dispatch_content_type === 'params'">
+        <view class="desc ss-m-b-20" v-for="item in state.data.ext.dispatch_content" :key="item">
+          {{ item.title }}: {{ item.content }}
+        </view>
+      </view>
+      <view class="desc" v-else>{{ state.data.ext?.dispatch_content }}</view>
+    </view>
+  </s-layout>
+</template>
+<script setup>
+  import { onLoad } from '@dcloudio/uni-app';
+  import { reactive } from 'vue';
+  import sheep from '@/sheep';
+
+  const state = reactive({
+    data: [],
+  });
+  async function getDetail(id, item_id) {
+    const { error, data } = await sheep.$api.order.itemDetail(id,item_id);
+    if (error === 0) {
+      state.data = data;
+    }
+  }
+  onLoad((options) => {
+    getDetail(options.id, options.item_id);
+  });
+</script>
+<style lang="scss" scoped>
+  .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;
+    }
+  }
+  .title {
+    font-size: 28rpx;
+    font-weight: bold;
+    color: #333333;
+  }
+  .desc {
+    font-size: 26rpx;
+    font-weight: 400;
+    color: #333333;
+  }
+</style>

+ 104 - 0
pages/order/express/list.vue

@@ -0,0 +1,104 @@
+<!-- 物流包裹-->
+<template>
+  <s-layout title="物流包裹">
+    <view class="express-wrap">
+      <su-sticky bgColor="#FFE2B6">
+        <view class="header ss-flex ss-p-l-24">{{ state.list.length }}个包裹已派送</view>
+      </su-sticky>
+      <view
+        class="express-box"
+        v-for="item in state.list"
+        :key="item.type"
+        @tap="sheep.$router.go('/pages/order/express/log', { id: item.id, orderId: state.orderId })"
+      >
+        <view class="express-box-header ss-flex ss-row-between">
+          <view class="express-box-header-type">{{ item.status_text }}</view>
+          <view class="express-box-header-num">{{
+            item.express_name + ' : ' + item.express_no
+          }}</view>
+        </view>
+        <view class="express-box-content">
+          <view class="content-address">{{ item.logs[0]?.content }}</view>
+          <view class="" v-if="item.items?.length">
+            <scroll-view class="scroll-box" scroll-x scroll-anchoring>
+              <view class="ss-flex">
+                <view v-for="i in item.items" :key="i" class="ss-m-r-20"
+                  ><image class="content-img" :src="sheep.$url.static(i.goods_image)" />
+                </view>
+              </view>
+            </scroll-view>
+          </view>
+        </view>
+        <view class="express-box-foot">共{{ item.items.length }}件商品</view>
+      </view>
+    </view>
+  </s-layout>
+</template>
+
+<script setup>
+  import sheep from '@/sheep';
+  import { onLoad } from '@dcloudio/uni-app';
+  import { computed, reactive } from 'vue';
+  const state = reactive({
+    list: [],
+    orderId: '',
+  });
+  async function getExpressList(id) {
+    const { data } = await sheep.$api.order.express(id, '');
+    state.list = data;
+  }
+  onLoad((Option) => {
+    state.orderId = Option.orderId;
+    getExpressList(state.orderId);
+  });
+</script>
+
+<style lang="scss" scoped>
+  .header {
+    height: 84rpx;
+    font-size: 30rpx;
+    font-weight: 500;
+    color: #a8700d;
+  }
+  .express-box {
+    background: #fff;
+    padding-bottom: 30rpx;
+    box-sizing: border-box;
+    margin-bottom: 20rpx;
+    .express-box-header {
+      height: 76rpx;
+      padding: 0 24rpx;
+      border-bottom: 2rpx solid rgba(#dfdfdf, 0.5);
+      .express-box-header-type {
+        font-size: 26rpx;
+        font-weight: 500;
+        color: #999;
+      }
+      .express-box-header-num {
+        font-size: 26rpx;
+        font-weight: 400;
+        color: #999999;
+      }
+    }
+    .express-box-content {
+      padding: 20rpx 24rpx;
+      .content-address {
+        font-size: 28rpx;
+        font-weight: 400;
+        color: #333333;
+        line-height: normal;
+        margin-bottom: 20rpx;
+      }
+      .content-img {
+        width: 180rpx;
+        height: 180rpx;
+      }
+    }
+    .express-box-foot {
+      padding: 0 24rpx;
+      font-size: 24rpx;
+      font-weight: 400;
+      color: #999999;
+    }
+  }
+</style>

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

@@ -0,0 +1,174 @@
+<!-- 物流追踪 -->
+<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" @change="change">
+            <swiper-item v-for="(item, index) in goodsImages" :key="index">
+              <image class="log-card-img" :src="sheep.$url.static(item.image)"></image>
+            </swiper-item>
+          </swiper>
+        </uni-swiper-dot>
+
+        <view class="log-card-msg">
+          <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.express_no }}</view>
+          <view>快递公司:{{ state.info.express_name }}</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.info.logs"
+          :key="item.title"
+        >
+          <view class="log-icon ss-flex-col ss-col-center ss-m-r-20">
+            <text
+              v-if="state.info.logs[index].status === state.info.logs[index - 1]?.status"
+              class="cicon-title"
+            ></text>
+            <text
+              v-if="state.info.logs[index].status != state.info.logs[index - 1]?.status"
+              :class="[
+                index === 0 ? 'activity-color' : 'info-color',
+                item.status === 'transport'
+                  ? 'sicon-transport'
+                  : item.status === 'delivery'
+                  ? 'sicon-delivery'
+                  : item.status === 'collect'
+                  ? 'sicon-a-collectmaterials'
+                  : item.status === 'fail' || item.status === 'back' || item.status === 'refuse'
+                  ? 'sicon-circleclose'
+                  : item.status === 'signfor'
+                  ? 'sicon-circlecheck'
+                  : 'sicon-warning-outline',
+              ]"
+            ></text>
+            <view v-if="state.info.logs.length - 1 != index" class="line"></view>
+          </view>
+          <view class="log-content-msg">
+            <view
+              v-if="
+                item.status_text &&
+                state.info.logs[index].status != state.info.logs[index - 1]?.status
+              "
+              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">{{ item.change_date }}</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';
+
+  const state = reactive({
+    info: [],
+    current: 0,
+  });
+  const goodsImages = computed(() => {
+    let array = [];
+    if (state.info.items) {
+      state.info.items.forEach((item) => {
+        array.push({
+          image: item.goods_image,
+        });
+      });
+    }
+    return array;
+  });
+  function change(e) {
+    state.current = e.detail.current;
+  }
+  async function getExpressdetail(id, orderId) {
+    const { data } = await sheep.$api.order.express(id, orderId);
+    state.info = data;
+  }
+  onLoad((Option) => {
+    getExpressdetail(Option.id, Option.orderId);
+  });
+</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>

+ 329 - 0
pages/order/invoice.vue

@@ -0,0 +1,329 @@
+<!-- 订单详情 -->
+<template>
+  <s-layout title="发票详情" class="invoice-wrap" navbar="inner">
+    <view
+      class="invoice-heard ss-flex-col ss-row-right ss-col-center"
+      :style="[
+        {
+          marginTop: '-' + Number(statusBarHeight + 88) + 'rpx',
+          paddingTop: Number(statusBarHeight + 88) + 'rpx',
+        },
+      ]"
+    >
+      <view class="ss-flex ss-m-t-32 ss-m-b-32">
+        <text
+          class="sicon-warning-line"
+          v-if="state.data.status === 'waiting' || state.data.status === 'unpaid'"
+        ></text>
+        <text class="sicon-check-line" v-if="state.data.status === 'finish'"></text>
+        <view class="invoice-heard-title">{{ state.data.status_text }}</view>
+      </view>
+      <view class="ss-flex ss-m-b-52">
+        <view class="ss-m-r-20 invoice-heard-desc">预计可开发票金额:</view>
+        <view class="invoice-heard-price">¥{{ state.data.amount }}</view>
+      </view>
+    </view>
+    <view class="invoice-content ss-flex-col ss-col-center">
+      <view class="ss-m-t-50 ss-m-b-42 invoice-content-title">增值税电子普通发票</view>
+      <view class="ss-flex ss-m-b-64">
+        <view v-for="(item, index) in state.info" :key="item.title">
+          <view class="log-icon ss-flex">
+            <text class="sicon-circlecheck" v-if="statusNum >= index"></text>
+            <text class="sicon-unchecked" v-else></text>
+            <view
+              v-if="state.info.length - 1 != index"
+              class="line"
+              :class="statusNum >= index ? 'activity-color' : ''"
+            ></view>
+          </view>
+          <view class="log-title">{{ item.title }}</view>
+        </view>
+      </view>
+      <view class="invoice-content-list ss-flex ss-row-between ss-col-top">
+        <view class="">
+          <view class="ss-flex">
+            <view class="list-title">发票类型</view>
+            <view class="list-desc">{{ state.data.type_text }}</view>
+          </view>
+          <view class="ss-flex">
+            <view class="list-title">发票抬头</view>
+            <view class="list-desc">{{ state.data.name }}</view>
+          </view>
+          <view class="ss-flex" v-if="state.data.type === 'company'">
+            <view class="list-title">发票税号</view>
+            <view class="list-desc">{{ state.data.tax_no }}</view>
+          </view>
+          <view class="ss-flex" v-if="state.data.status === 'finish'">
+            <view class="list-title">实开金额</view>
+            <view class="list-desc">¥{{ state.data.invoice_amount }}</view>
+          </view>
+          <view class="ss-flex" v-if="state.data.status === 'finish'">
+            <view class="list-title">开票时间</view>
+            <view class="list-desc">{{ state.data.finish_time }}</view>
+          </view>
+          <view class="ss-flex">
+            <view class="list-title">申请时间</view>
+            <view class="list-desc">{{ state.data.create_time }}</view>
+          </view>
+        </view>
+        <view
+          class="invoice-content-img ss-flex-col ss-col-center"
+          v-if="state.data.status === 'finish'"
+        >
+          <su-image
+            class="invoice-img"
+            isPreview
+            :previewList="state.jointImage"
+            :current="0"
+            :src="sheep.$url.static('/static/img/shop/order/invoice_thumb.png')"
+            :height="110"
+            mode="scaleToFill"
+            v-if="state.jointImage[0].substr(-4) != '.pdf'"
+          ></su-image>
+          <!-- TODO: 发票为多个pdf时 -->
+          <view v-if="state.jointImage[0].substr(-4) == '.pdf'" @tap="onInvoice">
+            <image
+              :src="sheep.$url.static('/static/img/shop/order/invoice_thumb.png')"
+              class="invoice-img"
+            ></image>
+          </view>
+          <view class="invoice-img-num">共{{ state.numImage }}张</view>
+          <view class="invoice-img-title">点击预览发票</view>
+        </view>
+      </view>
+    </view>
+    <view class="invoice-order ss-m-t-20">
+      <view class="goods-box" v-for="item in state.data.order_items" :key="item.id">
+        <s-goods-item
+          :img="item.goods_image"
+          :title="item.goods_title"
+          :skuText="item.goods_sku_text"
+          :price="item.goods_price"
+          :num="item.goods_num"
+        />
+      </view>
+      <view class="invoice-order-list">
+        <view class="ss-flex">
+          <view class="list-title">订单状态</view>
+          <view class="list-desc">{{ state.data.order?.status_text }}</view>
+        </view>
+        <view class="ss-flex">
+          <view class="list-title">订单编号</view>
+          <view class="list-desc">{{ state.data.order?.order_sn }}</view>
+        </view>
+        <view class="ss-flex">
+          <view class="list-title">下单时间</view>
+          <view class="list-desc">{{ state.data.order?.create_time }}</view>
+        </view>
+      </view>
+    </view>
+  </s-layout>
+</template>
+
+<script setup>
+  import sheep from '@/sheep';
+  import { onLoad } from '@dcloudio/uni-app';
+  import { computed, reactive } from 'vue';
+
+  const statusBarHeight = sheep.$platform.device.statusBarHeight * 2;
+  const headerBg = sheep.$url.css('/static/img/shop/order/invoice_bg.png');
+  const state = reactive({
+    info: [
+      {
+        title: '订单提交',
+      },
+      {
+        title: '等待开票',
+      },
+      {
+        title: '开票完成',
+      },
+    ],
+    data: {},
+    jointImage: [],
+    numImage: 0,
+  });
+  const statusNum = computed(() => {
+    if (state.data.status === 'finish') {
+      return 2;
+    } else if (state.data.status === 'waiting') {
+      return 1;
+    } else {
+      return 0;
+    }
+  });
+  function onInvoice() {
+    // #ifdef H5
+    window.open(state.jointImage);
+    // #endif
+    // #ifdef MP || APP-PLUS
+    uni.downloadFile({
+      url: state.jointImage[0],
+      success: function (res) {
+        var filePath = res.tempFilePath;
+        uni.openDocument({
+          filePath: filePath,
+          showMenu: true,
+          success: function (res) {
+            console.log('打开文档成功');
+          },
+        });
+      },
+    });
+    // #endif
+  }
+  async function getInvoiceDetail(id) {
+    const { data } = await sheep.$api.order.invoice(id);
+    state.data = data;
+    state.data.download_urls?.forEach((i, index) => {
+      state.numImage = index + 1;
+      if (i.substr(-4) != '.pdf') {
+        state.jointImage.push(sheep.$url.static(i));
+      } else {
+        state.jointImage.push(sheep.$url.static(i));
+      }
+    });
+  }
+  onLoad((options) => {
+    getInvoiceDetail(options.invoiceId);
+  });
+</script>
+
+<style lang="scss" scoped>
+  .invoice-heard {
+    width: 100%;
+    box-sizing: border-box;
+    background: v-bind(headerBg) no-repeat,
+      linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
+    background-size: 750rpx 100%;
+    .sicon-warning-line {
+      color: #fff;
+      font-size: 34rpx;
+    }
+    .sicon-check-line {
+      color: #fff;
+      font-size: 34rpx;
+    }
+    .invoice-heard-title {
+      font-size: 34rpx;
+      font-weight: 500;
+      color: #ffffff;
+      margin-left: 8rpx;
+      line-height: normal;
+    }
+    .invoice-heard-desc {
+      font-size: 24rpx;
+      font-weight: 500;
+      color: #ffffff;
+    }
+    .invoice-heard-price {
+      font-size: 28rpx;
+      font-family: OPPOSANS;
+      font-weight: 500;
+      color: #ffffff;
+    }
+  }
+  .invoice-content {
+    width: 100%;
+    position: relative;
+    z-index: 3;
+    background: #ffffff;
+    border-radius: 20rpx;
+    margin-top: -16rpx;
+    .invoice-content-title {
+      font-size: 30rpx;
+      font-weight: 500;
+      color: #333333;
+    }
+    .log-icon {
+      .sicon-unchecked {
+        color: #c2bec2;
+        font-size: 44rpx;
+      }
+      .sicon-circlecheck {
+        color: #e60a00;
+        font-size: 44rpx;
+      }
+      .line {
+        width: 158rpx;
+        height: 6rpx;
+        background: #f2f2f2;
+        border: 2rpx solid #ffffff;
+      }
+      .activity-color {
+        background: #e60a00;
+      }
+    }
+    .log-title {
+      font-size: 26rpx;
+      font-weight: 500;
+      color: #333333;
+      margin-left: -26rpx;
+      margin-top: 30rpx;
+    }
+    .invoice-content-list {
+      width: 100%;
+      padding: 0 46rpx 0 30rpx;
+      box-sizing: border-box;
+    }
+    .list-title {
+      font-size: 26rpx;
+      font-weight: 500;
+      color: #999999;
+      margin-right: 44rpx;
+      margin-bottom: 36rpx;
+    }
+    .list-desc {
+      font-size: 26rpx;
+      font-weight: 500;
+      color: #333333;
+      margin-bottom: 36rpx;
+    }
+    .invoice-img {
+      width: 200rpx;
+      height: 110rpx;
+    }
+    .invoice-img-num {
+      width: 216rpx;
+      height: 40rpx;
+      background: rgba(#000000, 0.45);
+      font-size: 24rpx;
+      font-weight: 500;
+      color: #ffffff;
+      text-align: center;
+      margin-top: -30rpx;
+      z-index: 1;
+    }
+    .invoice-img-title {
+      font-size: 24rpx;
+      font-weight: 500;
+      color: #999999;
+    }
+  }
+  .invoice-order {
+    width: 100%;
+    padding-top: 30rpx;
+    box-sizing: border-box;
+    background: #fff;
+    border-radius: 20rpx;
+  }
+  .goods-box {
+    border-bottom: 2rpx solid #dfdfdf;
+  }
+  .invoice-order-list {
+    padding: 40rpx 24rpx 0 24rpx;
+    .list-title {
+      font-size: 26rpx;
+      font-weight: 500;
+      color: #999999;
+      margin-right: 44rpx;
+      margin-bottom: 36rpx;
+    }
+    .list-desc {
+      font-size: 26rpx;
+      font-weight: 500;
+      color: #333333;
+      margin-bottom: 36rpx;
+    }
+  }
+</style>

+ 586 - 0
pages/order/list.vue

@@ -0,0 +1,586 @@
+<!-- 页面 -->
+<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/order-empty.png" text="暂无订单"></s-empty>
+		<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.data"
+				: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.status_code)">{{
+            order.status
+          }}</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.length>1? item.properties.reduce((items2,items)=>items2.valueName+' '+items.valueName):item.properties[0].valueName"
+						:price="item.price/100" :score="order.score_amount" :num="item.count">
+						<template #tool>
+							<view class="ss-flex">
+								<!-- <button class="ss-reset-button apply-btn" v-if="item.btns.includes('aftersale')"
+									@tap.stop="
+                    sheep.$router.go('/pages/order/aftersale/apply', {
+                      item: JSON.stringify(item),
+                    })
+                  ">
+									申请售后
+								</button>
+								<button class="ss-reset-button apply-btn" v-if="item.btns.includes('re_aftersale')"
+									@tap.stop="
+                    sheep.$router.go('/pages/order/aftersale/apply', {
+                      item: JSON.stringify(item),
+                    })
+                  ">
+									重新售后
+								</button>
+
+								<button class="ss-reset-button apply-btn" v-if="item.btns.includes('aftersale_info')"
+									@tap.stop="
+                    sheep.$router.go('/pages/order/aftersale/detail', {
+                      id: item.ext.aftersale_id,
+                    })
+                  ">
+									售后详情
+								</button>
+								<button class="ss-reset-button apply-btn" v-if="item.btns.includes('buy_again')"
+									@tap.stop="
+                    sheep.$router.go('/pages/goods/index', {
+                      id: item.goods_id,
+                    })
+                  ">
+									再次购买
+								</button> -->
+							</view>
+						</template>
+					</s-goods-item>
+				</view>
+				<view class="pay-box ss-m-t-30 ss-flex ss-row-right ss-p-r-20">
+					<!-- <view v-if="order.total_discount_fee > 0" class="ss-flex ss-col-center ss-m-r-8">
+						<view class="discounts-title">优惠:¥</view>
+						<view class="discounts-money">{{ order.total_discount_fee }}</view>
+					</view> -->
+					<!-- 	<view class="ss-flex ss-col-center ss-m-r-8">
+						<view class="discounts-title">运费:¥</view>
+						<view class="discounts-money">{{ order.dispatch_amount }}</view>
+					</view> -->
+					<view class="ss-flex ss-col-center">
+						<view class="discounts-title pay-color">共{{count}}件商品,总金额:</view>
+						<view class="discounts-money pay-color" v-if="Number(order.payPrice) > 0">
+							¥{{ order.payPrice/100 }}</view>
+						<view v-if="order.score_amount && Number(order.payPrice) > 0">+</view>
+						<view class="discounts-money pay-color ss-flex ss-col-center" v-if="order.score_amount">
+							<image :src="sheep.$url.static('/static/img/shop/goods/score1.svg')" class="score-img">
+							</image>
+							<view>{{ order.score_amount }}</view>
+						</view>
+					</view>
+				</view>
+				<!-- :class="order.btns.length > 3 ? 'ss-row-between' : 'ss-row-right'" -->
+				<view class="order-card-footer ss-flex ss-col-center ss-p-x-20">
+					<!-- <su-popover>
+            <button class="more-btn ss-reset-button" @click.stop>更多</button>
+            <template #content>
+              <view class="more-item-box">
+                <view class="more-item ss-flex ss-col-center ss-reset-button">
+                  <view class="item-title">删除订单</view>
+                </view>
+                <view class="more-item ss-flex ss-col-center ss-reset-button">
+                  <view class="item-title">查看发票</view>
+                </view>
+                <view class="more-item ss-flex ss-col-center ss-reset-button">
+                  <view class="item-title">评价晒单</view>
+                </view>
+              </view>
+            </template>
+          </su-popover> -->
+					<view class="ss-flex ss-col-center">
+						<!-- 				<button v-if="order.btns.includes('groupon')" class="tool-btn ss-reset-button"
+							@tap.stop="onOrderGroupon(order)">
+							{{ order.status_code === 'groupon_ing' ? '邀请拼团' : '拼团详情' }}
+						</button>
+						<button v-if="order.btns.includes('invoice')" class="tool-btn ss-reset-button"
+							@tap.stop="onOrderInvoice(order.invoice?.id)">
+							查看发票
+						</button>
+						<button v-if="order.btns.length === 0" class="tool-btn ss-reset-button"
+							@tap.stop="onOrderDetail(order.order_sn)">
+							查看详情
+						</button>
+
+						<button v-if="order.btns.includes('confirm')" class="tool-btn ss-reset-button"
+							@tap.stop="onConfirm(order)">
+							确认收货
+						</button>
+
+						<button v-if="order.btns.includes('express')" class="tool-btn ss-reset-button"
+							@tap.stop="onExpress(order.id)">
+							查看物流
+						</button>
+
+						<button v-if="order.btns.includes('apply_refund')" class="tool-btn ss-reset-button"
+							@tap.stop="onRefund(order.id)">
+							申请退款
+						</button>
+						<button v-if="order.btns.includes('re_apply_refund')" class="tool-btn ss-reset-button"
+							@tap.stop="onRefund(order.id)">
+							重新退款
+						</button>
+
+						<button v-if="order.btns.includes('cancel')" class="tool-btn ss-reset-button"
+							@tap.stop="onCancel(order.id)">
+							取消订单
+						</button>
+
+						<button v-if="order.btns.includes('comment')" class="tool-btn ss-reset-button"
+							@tap.stop="onComment(order.order_sn)">
+							评价晒单
+						</button>
+
+						<button v-if="order.btns.includes('delete')" class="delete-btn ss-reset-button"
+							@tap.stop="onDelete(order.id)">
+							删除订单
+						</button>
+
+						<button v-if="order.btns.includes('pay')" class="tool-btn ss-reset-button ui-BG-Main-Gradient"
+							@tap.stop="onPay(order.order_sn)">
+							继续支付
+						</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 {
+		computed,
+		reactive
+	} from 'vue';
+	import {
+		onLoad,
+		onReachBottom,
+		onPullDownRefresh
+	} from '@dcloudio/uni-app';
+	import {
+		formatOrderColor
+	} from '@/sheep/hooks/useGoods';
+	import sheep from '@/sheep';
+	import _ from 'lodash';
+	import {
+		isEmpty
+	} from 'lodash';
+
+	const pagination = {
+		data: [],
+		current_page: 1,
+		total: 1,
+		last_page: 1,
+	};
+	// 数据
+	const state = reactive({
+		currentTab: 0,
+		pagination: {
+			data: [],
+			current_page: 1,
+			total: 1,
+			last_page: 1,
+		},
+		loadStatus: '',
+		deleteOrderId: 0,
+		error: 0,
+	});
+
+	const tabMaps = [{
+			name: '全部',
+			// value: 'all',
+		},
+		{
+			name: '待付款',
+			value: 0,
+		},
+		{
+			name: '待发货',
+			value: 10,
+		},
+		{
+			name: '待收货',
+			value: 20,
+		},
+		{
+			name: '待评价',
+			value: 30,
+		},
+	];
+
+	// 切换选项卡
+	function onTabsChange(e) {
+		if (state.currentTab === e.index) return;
+
+		state.pagination = pagination;
+		state.currentTab = e.index;
+
+		getOrderList();
+	}
+
+	// 订单详情
+	function onOrderDetail(orderSN) {
+		sheep.$router.go('/pages/order/detail', {
+			orderSN,
+		});
+	}
+
+	// 分享拼团
+	function onOrderGroupon(order) {
+		sheep.$router.go('/pages/activity/groupon/detail', {
+			id: order.ext.groupon_id,
+		});
+	}
+
+	// 查看发票
+	function onOrderInvoice(invoiceId) {
+		sheep.$router.go('/pages/order/invoice', {
+			invoiceId,
+		});
+	}
+
+	// 继续支付
+	function onPay(orderSN) {
+		sheep.$router.go('/pages/pay/index', {
+			orderSN,
+		});
+	}
+
+	// 评价
+	function onComment(orderSN) {
+		sheep.$router.go('/pages/goods/comment/add', {
+			orderSN,
+		});
+	}
+
+	// 确认收货
+	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 {
+			error
+		} = await sheep.$api.order.confirm(order.id);
+		if (error === 0) {
+			state.pagination = pagination;
+			getOrderList();
+		}
+	}
+
+	// #ifdef MP-WEIXIN
+	// 小程序确认收货组件
+	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(orderId) {
+		sheep.$router.go('/pages/order/express/list', {
+			orderId,
+		});
+	}
+
+	// 取消订单
+	async function onCancel(orderId) {
+		uni.showModal({
+			title: '提示',
+			content: '确定要取消订单吗?',
+			success: async function(res) {
+				if (res.confirm) {
+					const {
+						error,
+						data
+					} = await sheep.$api.order.cancel(orderId);
+					if (error === 0) {
+						let index = state.pagination.data.findIndex((order) => order.id === orderId);
+						state.pagination.data[index] = data;
+					}
+				}
+			},
+		});
+	}
+
+	// 删除订单
+	function onDelete(orderId) {
+		uni.showModal({
+			title: '提示',
+			content: '确定要删除订单吗?',
+			success: async function(res) {
+				if (res.confirm) {
+					const {
+						error,
+						data
+					} = await sheep.$api.order.delete(orderId);
+					if (error === 0) {
+						let index = state.pagination.data.findIndex((order) => order.id === orderId);
+						state.pagination.data.splice(index, 1);
+					}
+				}
+			},
+		});
+	}
+
+	// 申请退款
+	async function onRefund(orderId) {
+		uni.showModal({
+			title: '提示',
+			content: '确定要申请退款吗?',
+			success: async function(res) {
+				if (res.confirm) {
+					// #ifdef MP
+					sheep.$platform.useProvider('wechat').subscribeMessage('order_refund');
+					// #endif
+					const {
+						error,
+						data
+					} = await sheep.$api.order.applyRefund(orderId);
+					if (error === 0) {
+						let index = state.pagination.data.findIndex((order) => order.id === orderId);
+						state.pagination.data[index] = data;
+					}
+				}
+			},
+		});
+	}
+
+	// 获取订单列表
+	async function getOrderList(page = 1, list_rows = 5) {
+		state.loadStatus = 'loading';
+		let res = await sheep.$api.order.list({
+			status: tabMaps[state.currentTab].value,
+			pageSize: list_rows,
+			pageNo: page,
+			commentStatus: tabMaps[state.currentTab].value == 30 ? false : null
+		});
+		state.error = res.code;
+		if (res.code === 0) {
+			let orderList = _.concat(state.pagination.data, res.data.list);
+			state.pagination = {
+				...res.data,
+				data: orderList,
+			};
+			console.log(state.pagination)
+			if (state.pagination.data.length < state.pagination.total) {
+				state.loadStatus = 'more';
+			} else {
+				state.loadStatus = 'noMore';
+			}
+		}
+	}
+
+	onLoad(async (options) => {
+		if (options.type) {
+			state.currentTab = options.type;
+		}
+		getOrderList();
+	});
+
+	// 加载更多
+	function loadmore() {
+		if (state.loadStatus !== 'noMore') {
+			getOrderList(parseInt((state.pagination.data.length / 5) + 1));
+		}
+	}
+
+	// 上拉加载更多
+	onReachBottom(() => {
+		loadmore();
+	});
+
+	//下拉刷新
+	onPullDownRefresh(() => {
+		state.pagination = pagination;
+		getOrderList();
+		setTimeout(function() {
+			uni.stopPullDownRefresh();
+		}, 800);
+	});
+</script>
+
+<style lang="scss" scoped>
+	.score-img {
+		width: 36rpx;
+		height: 36rpx;
+		margin: 0 4rpx;
+	}
+
+	.tool-btn {
+		width: 160rpx;
+		height: 60rpx;
+		background: #f6f6f6;
+		font-size: 26rpx;
+		border-radius: 30rpx;
+		margin-right: 10rpx;
+
+		&:last-of-type {
+			margin-right: 0;
+		}
+	}
+
+	.delete-btn {
+		width: 160rpx;
+		height: 56rpx;
+		color: #ff3000;
+		background: #fee;
+		border-radius: 28rpx;
+		font-size: 26rpx;
+		margin-right: 10rpx;
+		line-height: normal;
+
+		&:last-of-type {
+			margin-right: 0;
+		}
+	}
+
+	.apply-btn {
+		width: 140rpx;
+		height: 50rpx;
+		border-radius: 25rpx;
+		font-size: 24rpx;
+		border: 2rpx solid #dcdcdc;
+		line-height: normal;
+		margin-left: 16rpx;
+	}
+
+	.swiper-box {
+		flex: 1;
+
+		.swiper-item {
+			height: 100%;
+			width: 100%;
+		}
+	}
+
+	.order-list-card-box {
+		.order-card-header {
+			height: 80rpx;
+
+			.order-no {
+				font-size: 26rpx;
+				font-weight: 500;
+			}
+
+			.order-state {}
+		}
+
+		.pay-box {
+			.discounts-title {
+				font-size: 24rpx;
+				line-height: normal;
+				color: #999999;
+			}
+
+			.discounts-money {
+				font-size: 24rpx;
+				line-height: normal;
+				color: #999;
+				font-family: OPPOSANS;
+			}
+
+			.pay-color {
+				color: #333;
+			}
+		}
+
+		.order-card-footer {
+			height: 100rpx;
+
+			.more-item-box {
+				padding: 20rpx;
+
+				.more-item {
+					height: 60rpx;
+
+					.title {
+						font-size: 26rpx;
+					}
+				}
+			}
+
+			.more-btn {
+				color: $dark-9;
+				font-size: 24rpx;
+			}
+
+			.content {
+				width: 154rpx;
+				color: #333333;
+				font-size: 26rpx;
+				font-weight: 500;
+			}
+		}
+	}
+
+	:deep(.uni-tooltip-popup) {
+		background: var(--ui-BG);
+	}
+
+	.warning-color {
+		color: #faad14;
+	}
+
+	.danger-color {
+		color: #ff3000;
+	}
+
+	.success-color {
+		color: #52c41a;
+	}
+
+	.info-color {
+		color: #999999;
+	}
+</style>

+ 237 - 0
pages/pay/components/account-info-modal.vue

@@ -0,0 +1,237 @@
+<template>
+  <su-popup :show="show" class="add-bank-wrap" @close="hideModal">
+    <view class="ss-modal-box bg-white ss-flex-col">
+      <view class="modal-header ss-flex-col ss-col-left">
+        <text v-if="props.modelValue.type === 'bank'" class="modal-title ss-m-b-20">
+          绑定银行卡
+        </text>
+        <text v-if="props.modelValue.type === 'wechat'" class="modal-title ss-m-b-20">
+          绑定微信
+        </text>
+        <text v-if="props.modelValue.type === 'alipay'" class="modal-title ss-m-b-20">
+          绑定支付宝
+        </text>
+      </view>
+      <view class="modal-content ss-flex-1 ss-p-b-100">
+        <block v-if="props.modelValue.type === 'bank'">
+          <uni-forms
+            ref="form"
+            :model="state.bank.model"
+            :rules="state.bank.rules"
+            validateTrigger="bind"
+            labelWidth="160"
+            labelAlign="center"
+            border
+            :labelStyle="{ fontWeight: 'bold' }"
+          >
+            <uni-forms-item name="account_name" label="持卡人">
+              <uni-easyinput
+                :inputBorder="false"
+                placeholder="请输入持卡人"
+                v-model="state.bank.model.account_name"
+              />
+            </uni-forms-item>
+            <uni-forms-item name="account_header" label="开户行">
+              <uni-easyinput
+                :inputBorder="false"
+                placeholder="请输入开户行"
+                v-model="state.bank.model.account_header"
+              />
+            </uni-forms-item>
+            <uni-forms-item name="account_no" label="银行卡号">
+              <uni-easyinput
+                type="number"
+                :inputBorder="false"
+                placeholder="请输入银行卡号"
+                v-model="state.bank.model.account_no"
+              />
+            </uni-forms-item>
+          </uni-forms>
+        </block>
+
+        <block v-if="props.modelValue.type === 'wechat'">
+          <uni-forms
+            ref="form"
+            :model="state.wechat.model"
+            :rules="state.wechat.rules"
+            validateTrigger="bind"
+            labelWidth="160"
+            labelAlign="center"
+            border
+            :labelStyle="{ fontWeight: 'bold' }"
+          >
+            <uni-forms-item name="account_name" label="真实姓名">
+              <uni-easyinput
+                :inputBorder="false"
+                placeholder="请输入您的真实姓名"
+                v-model="state.wechat.model.account_name"
+              />
+            </uni-forms-item>
+          </uni-forms>
+        </block>
+
+        <block v-if="props.modelValue.type === 'alipay'">
+          <uni-forms
+            ref="form"
+            :model="state.alipay.model"
+            :rules="state.alipay.rules"
+            validateTrigger="bind"
+            labelWidth="160"
+            labelAlign="center"
+            border
+            :labelStyle="{ fontWeight: 'bold' }"
+          >
+            <uni-forms-item name="account_name" label="真实姓名">
+              <uni-easyinput
+                :inputBorder="false"
+                placeholder="请输入您的真实姓名"
+                v-model="state.alipay.model.account_name"
+              />
+            </uni-forms-item>
+            <uni-forms-item name="account_no" label="支付宝">
+              <uni-easyinput
+                :inputBorder="false"
+                placeholder="请输入支付宝 邮箱/手机号"
+                v-model="state.alipay.model.account_no"
+              />
+            </uni-forms-item>
+          </uni-forms>
+        </block>
+      </view>
+      <view class="modal-footer ss-flex ss-row-center ss-col-center">
+        <button class="ss-reset-button save-btn" @tap="onSave">保存</button>
+      </view>
+    </view>
+  </su-popup>
+</template>
+
+<script setup>
+  import { ref, reactive, unref, watchPostEffect, watch } from 'vue';
+  import sheep from '@/sheep';
+  import { realName, bankName, bankCode, alipayAccount } from '@/sheep/validate/form';
+
+  const form = ref(null);
+  const props = defineProps({
+    modelValue: {
+      type: Object,
+      default() {},
+    },
+    show: {
+      type: Boolean,
+      default: false,
+    },
+  });
+  watch(
+    () => props.modelValue,
+    (newValue, oldValue) => {
+      setModelValue(newValue);
+    },
+  );
+
+  function setModelValue(modelValue) {
+    Object.keys(state[modelValue.type].model).forEach((key) => {
+      state[modelValue.type].model[key] = modelValue[key];
+    });
+  }
+
+  const emits = defineEmits(['update:modelValue', 'close']);
+  // 数据
+  const state = reactive({
+    bank: {
+      model: {
+        account_name: '',
+        account_header: '',
+        account_no: '',
+      },
+      rules: {
+        account_name: realName,
+        account_header: bankName,
+        account_no: bankCode,
+      },
+    },
+    alipay: {
+      model: {
+        account_name: '',
+        account_no: '',
+      },
+      rules: {
+        account_name: realName,
+        account_no: alipayAccount,
+      },
+    },
+    wechat: {
+      model: {
+        account_name: '',
+      },
+      rules: {
+        account_name: realName,
+      },
+    },
+  });
+
+  setModelValue(props.modelValue);
+
+  const hideModal = () => {
+    emits('close');
+  };
+  const onSave = async () => {
+    const validate = await unref(form)
+      .validate()
+      .catch((error) => {
+        'error: ', error;
+      });
+    if (!validate) return;
+    let data = {
+      type: props.modelValue.type,
+      account_header: state[props.modelValue.type].model.account_header,
+      account_name: state[props.modelValue.type].model.account_name,
+      account_no: state[props.modelValue.type].model.account_no,
+    };
+    let res = await sheep.$api.user.account.save(data);
+    if (res.error === 0) {
+      emits('update:modelValue', res.data);
+      hideModal();
+    }
+  };
+</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;
+    }
+
+    .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;
+      }
+    }
+  }
+</style>

+ 178 - 0
pages/pay/components/account-type-select.vue

@@ -0,0 +1,178 @@
+<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(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, onBeforeMount, nextTick } 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',
+      title: '微信零钱',
+      value: 'wechat',
+    },
+    {
+      icon: '/static/img/shop/pay/alipay.png',
+      title: '支付宝账户',
+      value: 'alipay',
+    },
+    {
+      icon: '/static/img/shop/pay/bank.png',
+      title: '银行卡转账',
+      value: 'bank',
+    },
+  ];
+  const getWalletAccountInfo = async () => {
+    return new Promise(async (resolve, reject) => {
+      let res = await sheep.$api.user.account.info({
+        type: state.currentValue,
+      });
+      if (res.error === 0) {
+        if (!props.methods.includes(res.data.type)) {
+          return;
+        }
+        state.currentValue = res.data.type;
+        emits('update:modelValue', {
+          type: res.data.type,
+          account_header: res.data.account_header,
+          account_name: res.data.account_name,
+          account_no: res.data.account_no,
+        });
+      } else {
+        emits('update:modelValue', {
+          type: state.currentValue,
+        });
+      }
+      resolve();
+    });
+  };
+
+  function onChange(e) {
+    state.currentValue = e.detail.value;
+  }
+
+  const onConfirm = async () => {
+    if (state.currentValue === '') {
+      sheep.$helper.toast('请选择提现方式');
+      return;
+    }
+    await getWalletAccountInfo();
+    emits('close');
+  };
+
+  const hideModal = () => {
+    emits('close');
+  };
+
+  onBeforeMount(async () => {
+    await getWalletAccountInfo();
+  });
+</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>

+ 356 - 0
pages/pay/index.vue

@@ -0,0 +1,356 @@
+<!-- 收银台 -->
+<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">{{ state.orderInfo.pay_fee }}</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 }"
+              v-if="allowedPayment.includes(item.value)"
+            >
+              <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>
+                <image
+                  class="pay-icon"
+                  v-else
+                  :src="sheep.$url.static(item.icon)"
+                  mode="aspectFit"
+                ></image>
+                <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 == 'money'">
+                  余额: {{ userInfo.money }}元
+                </view>
+                <view
+                  class="userInfo-money ss-m-r-10"
+                  v-if="item.value == 'offline' && item.disabled"
+                >
+                  部分商品不支持
+                </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 { useDurationTime } from '@/sheep/hooks/useGoods';
+
+  const userInfo = computed(() => sheep.$store('user').userInfo);
+
+  // 检测支付环境
+  const state = reactive({
+    orderType: 'goods',
+    payment: '',
+    orderInfo: {},
+    payStatus: 0, // 0=检测支付环境, -2=未查询到支付单信息, -1=支付已过期, 1=待支付,2=订单已支付
+    payMethods: [],
+  });
+
+  const allowedPayment = computed(() => {
+    if(state.orderType === 'recharge') {
+      return sheep.$store('app').platform.recharge_payment
+    }
+    return sheep.$store('app').platform.payment
+    });
+
+  const payMethods = [
+    {
+      icon: '/static/img/shop/pay/wechat.png',
+      title: '微信支付',
+      value: 'wechat',
+      disabled: false,
+    },
+    {
+      icon: '/static/img/shop/pay/alipay.png',
+      title: '支付宝支付',
+      value: 'alipay',
+      disabled: false,
+    },
+    {
+      icon: '/static/img/shop/pay/wallet.png',
+      title: '余额支付',
+      value: 'money',
+      disabled: false,
+    },
+    {
+      icon: '/static/img/shop/pay/apple.png',
+      title: 'Apple Pay',
+      value: 'apple',
+      disabled: false,
+    },
+    {
+      icon: '/static/img/shop/pay/cod.png',
+      title: '货到付款',
+      value: 'offline',
+      disabled: false,
+    },
+  ];
+
+  const onPay = () => {
+    if (state.payment === '') {
+      sheep.$helper.toast('请选择支付方式');
+      return;
+    }
+    if (state.payment === 'money') {
+      uni.showModal({
+        title: '提示',
+        content: '确定要支付吗?',
+        success: function (res) {
+          if (res.confirm) {
+            sheep.$platform.pay(state.payment, state.orderType, state.orderInfo.order_sn);
+          }
+        },
+      });
+    } else if (state.payment === 'offline') {
+      uni.showModal({
+        title: '提示',
+        content: '确定要下单吗?',
+        success: function (res) {
+          if (res.confirm) {
+            sheep.$platform.pay(state.payment, state.orderType, state.orderInfo.order_sn);
+          }
+        },
+      });
+    } else {
+      sheep.$platform.pay(state.payment, state.orderType, state.orderInfo.order_sn);
+    }
+  };
+
+  const payDescText = computed(() => {
+    if (state.payStatus === 2) {
+      return '该订单已支付';
+    }
+    if (state.payStatus === 1 && state.orderInfo.ext.expired_time !== 0) {
+      const time = useDurationTime(state.orderInfo.ext.expired_time);
+      if (time.ms <= 0) {
+        state.payStatus = -1;
+        return '';
+      }
+      return `剩余支付时间 ${time.h}:${time.m}:${time.s} `;
+    }
+    if (state.payStatus === -2) {
+      return '未查询到支付单信息';
+    }
+
+    return '';
+  });
+
+  function checkPayStatus() {
+    if (state.orderInfo.status === 'unpaid') {
+      state.payStatus = 1;
+      return;
+    }
+    if (state.orderInfo.status === 'closed') {
+      state.payStatus = -1;
+      return;
+    }
+    state.payStatus = 2;
+  }
+
+  function onTapPay(e) {
+    state.payment = e.detail.value;
+  }
+
+  async function setRechargeOrder(id) {
+    const { data, error } = await sheep.$api.trade.order(id);
+    if (error === 0) {
+      state.orderInfo = data;
+      state.payMethods = payMethods;
+      checkPayStatus();
+    } else {
+      state.payStatus = -2;
+    }
+  }
+
+  async function setGoodsOrder(id) {
+    const { data, error } = await sheep.$api.order.detail(id);
+    if (error === 0) {
+      state.orderInfo = data;
+      if (state.orderInfo.ext.offline_status === 'none') {
+        payMethods.forEach((item, index, array) => {
+          if (item.value === 'offline') {
+            array.splice(index, 1);
+          }
+        });
+      } else if (state.orderInfo.ext.offline_status === 'disabled') {
+        payMethods.forEach((item) => {
+          if (item.value === 'offline') {
+            item.disabled = true;
+          }
+        });
+      }
+      state.payMethods = payMethods;
+      checkPayStatus();
+    } else {
+      state.payStatus = -2;
+    }
+  }
+
+  onLoad((options) => {
+    if (
+      sheep.$platform.name === 'WechatOfficialAccount' &&
+      sheep.$platform.os === 'ios' &&
+      !sheep.$platform.landingPage.includes('pages/pay/index')
+    ) {
+      location.reload();
+      return;
+    }
+    let id = '';
+    if (options.orderSN) {
+      id = options.orderSN;
+    }
+    if (options.id) {
+      id = options.id;
+    }
+    if (options.type === 'recharge') {
+      state.orderType = 'recharge';
+      // 充值订单
+      setRechargeOrder(id);
+    } else {
+      state.orderType = 'goods';
+      // 商品订单
+      setGoodsOrder(id);
+    }
+  });
+</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>

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

@@ -0,0 +1,171 @@
+<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.data" :key="item">
+        <view class="head ss-flex ss-col-center ss-row-between">
+          <view class="title">充值金额</view>
+          <view
+            class="num"
+            :class="
+              item.status === -1
+                ? 'danger-color'
+                : item.status === 2
+                ? 'success-color'
+                : 'warning-color'
+            "
+            >{{ item.pay_fee }}元</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.status === -1
+                ? 'danger-color'
+                : item.status === 2
+                ? 'success-color'
+                : 'warning-color'
+            "
+            >{{ item.status_text }}</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.platform_text }}</view>
+        </view>
+        <view class="time-box item ss-flex ss-col-center ss-row-between">
+          <text class="item-title">充值单号</text>
+          <view class="time"> {{ item.order_sn }} </view>
+        </view>
+        <view class="time-box item ss-flex ss-col-center ss-row-between">
+          <text class="item-title">充值时间</text>
+          <view class="time"> {{ item.paid_time }}</view>
+        </view>
+      </view>
+    </view>
+    <s-empty
+      v-if="state.pagination.total === 0"
+      icon="/static/comment-empty.png"
+      text="暂无充值记录"
+    ></s-empty>
+    <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 sheep from '@/sheep';
+  import { onLoad, onReachBottom } from '@dcloudio/uni-app';
+  import _ from 'lodash';
+  const state = reactive({
+    currentTab: 0,
+    pagination: {
+      data: [],
+      current_page: 1,
+      total: 1,
+      last_page: 1,
+    },
+    loadStatus: '',
+  });
+  async function getLogList(page = 1, list_rows = 5) {
+    const res = await sheep.$api.trade.orderLog({ type: 'recharge', list_rows, page });
+    if (res.error === 0) {
+      let logList = _.concat(state.pagination.data, res.data.data);
+      state.pagination = {
+        ...res.data,
+        data: logList,
+      };
+      if (state.pagination.current_page < state.pagination.last_page) {
+        state.loadStatus = 'more';
+      } else {
+        state.loadStatus = 'noMore';
+      }
+    }
+  }
+  // 加载更多
+  function loadmore() {
+    if (state.loadStatus !== 'noMore') {
+      getLogList(state.pagination.current_page + 1);
+    }
+  }
+  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>

+ 250 - 0
pages/pay/recharge.vue

@@ -0,0 +1,250 @@
+<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">{{ userInfo.money }}</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" v-if="state.data.status">
+        <view class="input-label ss-m-b-50">充值金额</view>
+        <view class="input-box ss-flex border-bottom ss-p-b-20" v-if="state.data.custom_status">
+          <view class="unit">¥</view>
+          <uni-easyinput v-model="state.recharge_money" type="digit" placeholder="请输入充值金额" :inputBorder="false">
+          </uni-easyinput>
+        </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.faceValueList" :key="item.money"
+            :class="[{ 'btn-active': state.recharge_money == parseFloat(item.money) }]" @tap="onCard(item.money)">
+            <text class="face-value-title">{{ item.money }}</text>
+            <view v-if="item.gift" class="face-value-tag">
+              送{{ item.gift }}{{ state.data.gift_type == 'money' ? '元' : '积分' }}</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 class="" v-if="state.data.status === 0"> 关闭充值 </view>
+    </view>
+  </s-layout>
+</template>
+
+<script setup>
+import { computed, reactive } from 'vue';
+import sheep from '@/sheep';
+import { onLoad } from '@dcloudio/uni-app';
+
+const userInfo = computed(() => sheep.$store('user').userInfo);
+const statusBarHeight = sheep.$platform.device.statusBarHeight * 2;
+const headerBg = sheep.$url.css('/static/img/shop/user/withdraw_bg.png');
+
+const state = reactive({
+  recharge_money: '',
+  data: {},
+  faceValueList: [],
+});
+// 点击卡片
+
+function onCard(e) {
+  state.recharge_money = e;
+}
+async function getRechargeTabs() {
+  const res = await sheep.$api.trade.rechargeRules();
+  if (res.error === 0) {
+    state.data = res.data;
+    state.data.status = res.data.status;
+    state.faceValueList = res.data.quick_amounts;
+  }
+}
+
+function onChange(e) {
+  state.data.gift_type = e.detail.value;
+}
+
+async function onConfirm() {
+  const { error, data } = await sheep.$api.trade.recharge({
+    recharge_money: state.recharge_money,
+  });
+  if (error === 0) {
+    // #ifdef MP
+    sheep.$platform.useProvider('wechat').subscribeMessage('money_change');
+    // #endif
+    sheep.$router.go('/pages/pay/index', { orderSN: data.order_sn, type: '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>

+ 285 - 0
pages/pay/result.vue

@@ -0,0 +1,285 @@
+<!-- 支付结果页面 -->
+<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'"> </view>
+      <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>
+      <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')"
+      ></image>
+      <view class="tip-text ss-m-b-30" v-if="payResult == 'success'">{{
+        state.orderInfo.pay_mode === 'offline' ? '下单成功' : '支付成功'
+      }}</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 v-if="Number(state.orderInfo.pay_fee) > 0">¥{{ state.orderInfo.pay_fee }}</view>
+        <view v-if="state.orderInfo.score_amount && Number(state.orderInfo.pay_fee) > 0">+</view>
+        <view class="price-text ss-flex ss-col-center" v-if="state.orderInfo.score_amount">
+          <image
+            :src="sheep.$url.static('/static/img/shop/goods/score1.svg')"
+            class="score-img"
+          ></image>
+          <view>{{ state.orderInfo.score_amount }}</view>
+        </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', { orderSN: state.orderId })"
+        >
+          重新支付
+        </button>
+        <button class="check-btn ss-reset-button" v-if="payResult === 'success'" @tap="onOrder">
+          查看订单
+        </button>
+        <button
+          class="check-btn ss-reset-button"
+          v-if="
+            payResult === 'success' &&
+            ['groupon', 'groupon_ladder'].includes(state.orderInfo.activity_type)
+          "
+          @tap="sheep.$router.redirect('/pages/activity/groupon/order')"
+        >
+          我的拼团
+        </button>
+      </view>
+      <!-- #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')"
+        ></image>
+        <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';
+
+  const state = reactive({
+    orderId: 0,
+    orderType: 'goods',
+    result: 'unpaid', // 支付状态
+    orderInfo: {}, // 订单详情
+    counter: 0, // 获取结果次数
+  });
+
+  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(orderId) {
+    let checkPayResult;
+    state.counter++;
+    if (state.orderType === 'recharge') {
+      checkPayResult = sheep.$api.trade.order;
+    } else {
+      checkPayResult = sheep.$api.order.detail;
+    }
+    const { data, error } = await checkPayResult(orderId);
+    if (error === 0) {
+      state.orderInfo = data;
+      if (state.orderInfo.status === 'closed') {
+        state.result = 'closed';
+        return;
+      }
+      if (state.orderInfo.status !== 'unpaid') {
+        state.result = 'paid';
+        // #ifdef MP
+        subscribeMessage();
+        // #endif
+        return;
+      }
+    }
+    if (state.counter < 3 && state.result === 'unpaid') {
+      setTimeout(() => {
+        getOrderInfo(orderId);
+      }, 1500);
+    }
+    // 超过三次检测才判断为支付失败
+    if (state.counter >= 3) {
+      state.result = 'failed';
+    }
+  }
+
+  function onOrder() {
+    if ((state.orderType === 'recharge')) {
+      sheep.$router.redirect('/pages/pay/recharge-log');
+    } else {
+      sheep.$router.redirect('/pages/order/list');
+    }
+  }
+
+  // #ifdef MP
+  function subscribeMessage() {
+    let event = ['order_dispatched'];
+    if (['groupon', 'groupon_ladder'].includes(state.orderInfo.activity_type)) {
+      event.push('groupon_finish');
+      event.push('groupon_fail');
+    }
+    sheep.$platform.useProvider('wechat').subscribeMessage(event);
+  }
+  // #endif
+
+  onLoad(async (options) => {
+    let id = '';
+    // 支付订单号
+    if (options.orderSN) {
+      id = options.orderSN;
+    }
+    if (options.id) {
+      id = options.id;
+    }
+    state.orderId = id;
+
+    if (options.orderType === 'recharge') {
+      state.orderType = 'recharge';
+    }
+
+    // 支付结果传值过来是失败,则直接显示失败界面
+    if (options.payState === 'fail') {
+      state.result = 'failed';
+    } else {
+      // 轮询三次检测订单支付结果
+      getOrderInfo(state.orderId);
+    }
+  });
+
+  onShow(() => {
+    if(isEmpty(state.orderInfo)) return;
+    getOrderInfo(state.orderId);
+  })
+
+  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>

+ 187 - 0
pages/pay/withdraw-log.vue

@@ -0,0 +1,187 @@
+<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.data" :key="item">
+        <view class="head ss-flex ss-col-center ss-row-between">
+          <view class="title">{{
+            item.withdraw_type === 'bank'
+              ? '提现至银行卡'
+              : item.withdraw_type === 'alipay'
+              ? '提现至支付宝'
+              : '提现至微信'
+          }}</view>
+          <view
+            class="num"
+            :class="
+              item.status === -1
+                ? 'danger-color'
+                : item.status === 2
+                ? 'success-color'
+                : 'warning-color'
+            "
+            >{{ item.amount }}元</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.status === -1
+                ? 'danger-color'
+                : item.status === 2
+                ? 'success-color'
+                : 'warning-color'
+            "
+            >{{ item.status_text }}</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" v-if="item.withdraw_type === 'bank'"
+            >{{ item.withdraw_info_hidden.开户行 }}[{{ item.withdraw_info_hidden.银行卡号 }}]</view
+          >
+          <view class="time ss-ellipsis-1" v-if="item.withdraw_type === 'alipay'">
+            支付宝[{{ item.withdraw_info_hidden.支付宝账户 }}]
+          </view>
+          <view class="time ss-ellipsis-1" v-if="item.withdraw_type === 'wechat'">微信零钱</view>
+        </view>
+        <view class="time-box item ss-flex ss-col-center ss-row-between">
+          <text class="item-title">提现单号</text>
+          <view class="time"> {{ item.withdraw_sn }} </view>
+        </view>
+        <view class="time-box item ss-flex ss-col-center ss-row-between">
+          <text class="item-title">手续费</text>
+          <view class="time">{{ item.charge_fee }}元</view>
+        </view>
+        <view class="time-box item ss-flex ss-col-center ss-row-between">
+          <text class="item-title">申请时间</text>
+          <view class="time"> {{ item.create_time }}</view>
+        </view>
+      </view>
+    </view>
+    <s-empty
+      v-if="state.pagination.total === 0"
+      icon="/static/comment-empty.png"
+      text="暂无提现记录"
+    ></s-empty>
+    <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 sheep from '@/sheep';
+  import { onLoad, onReachBottom } from '@dcloudio/uni-app';
+  import _ from 'lodash';
+  const state = reactive({
+    currentTab: 0,
+    pagination: {
+      data: [],
+      current_page: 1,
+      total: 1,
+      last_page: 1,
+    },
+    loadStatus: '',
+  });
+  async function getList(page = 1, list_rows = 6) {
+    const res = await sheep.$api.pay.withdraw.list({ list_rows, page });
+    if (res.error === 0) {
+      let logList = _.concat(state.pagination.data, res.data.data);
+      state.pagination = {
+        ...res.data,
+        data: logList,
+      };
+      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);
+    }
+  }
+  onLoad(() => {
+    getList();
+  });
+  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>

+ 380 - 0
pages/pay/withdraw.vue

@@ -0,0 +1,380 @@
+<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">{{ userInfo.commission }}</view>
+      </view>
+      <button class="ss-reset-button log-btn" @tap="sheep.$router.go('/pages/pay/withdraw-log')"
+        >提现记录</button
+      >
+    </view>
+    <!-- 提现输入卡片-->
+    <view class="draw-card">
+      <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.amount"
+          type="number"
+          placeholder="请输入提现金额"
+        />
+      </view>
+      <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 === 'wechat'" class="empty-text">微信零钱</view>
+          <view v-if="state.accountInfo.type === 'alipay'" class="empty-text">支付宝账户</view>
+          <view v-if="state.accountInfo.type === 'bank'" class="empty-text">银行卡转账</view>
+          <text class="cicon-forward"></text>
+        </view>
+      </view>
+      <view class="bind-box ss-flex ss-col-center ss-row-between" v-if="state.accountInfo.type">
+        <view class="placeholder-text" v-if="state.accountInfo.account_name">
+          {{ state.accountInfo.account_header }}|{{ state.accountInfo.account_name }}
+        </view>
+        <view class="placeholder-text" v-else>暂无提现账户</view>
+        <button class="add-btn ss-reset-button" @tap="onAccountEdit(true)">
+          {{ state.accountInfo.account_name ? '修改' : '添加' }}
+        </button>
+      </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" v-for="(rule, index) in state.rulesList" :key="index">
+        {{ index + 1 }}.{{ rule }}
+      </view>
+    </view>
+
+    <!-- 选择提现账户 -->
+    <account-type-select
+      :show="state.accountSelect"
+      @close="onAccountSelect(false)"
+      round="10"
+      v-model="state.accountInfo"
+      :methods="state.rules.methods"
+    />
+    <!-- 编辑账户信息 -->
+    <account-info-modal
+      v-if="state.accountInfo.type"
+      v-model="state.accountInfo"
+      :show="state.accountEdit"
+      @close="onAccountEdit(false)"
+      round="10"
+    />
+  </s-layout>
+</template>
+
+<script setup>
+  import { computed, reactive, onBeforeMount } from 'vue';
+  import sheep from '@/sheep';
+  import accountTypeSelect from './components/account-type-select.vue';
+  import accountInfoModal from './components/account-info-modal.vue';
+  import { onPageScroll } from '@dcloudio/uni-app';
+  const headerBg = sheep.$url.css('/static/img/shop/user/withdraw_bg.png');
+  onPageScroll(() => {});
+  const statusBarHeight = sheep.$platform.device.statusBarHeight * 2;
+  function filterRules(rules) {
+    let list = [];
+    let str1 = '';
+    if (rules.min_amount > 0) {
+      str1 += `最少 ${rules.min_amount}元; `;
+    }
+    if (rules.max_amount > 0) {
+      str1 += `最多 ${rules.max_amount}元;`;
+    }
+    if (str1 !== '') {
+      list.push('单次提现金额 ' + str1);
+    }
+
+    if (rules.max_num > 0) {
+      list.push(`每${rules.num_unit === 'day' ? '天' : '月'}最多可提现 ${rules.max_num} 次;`);
+    }
+
+    if (rules.charge_rate_format > 0) {
+      list.push(`每次收取提现手续费 ${rules.charge_rate_format}%;`);
+    }
+    list.push(
+      `提现申请后将${rules.auto_arrival ? '自动' : '审核后'}到账, 到账结果请查收对应渠道服务通知;`,
+    );
+    list.push('如有疑问请及时联系客服.');
+
+    return list;
+  }
+
+  const userStore = sheep.$store('user');
+  const userInfo = computed(() => userStore.userInfo);
+  const state = reactive({
+    amount: '',
+    type: '',
+    accountInfo: {},
+    accountSelect: false,
+    accountEdit: false,
+    rules: {
+      min_amount: 0,
+      max_amount: 0,
+      max_num: 0,
+      num_unit: 0,
+      charge_rate_format: 0,
+      charge_rate: 0,
+      methods: [],
+    },
+    rulesList: [],
+  });
+
+  const onAccountEdit = (e) => {
+    state.accountEdit = e;
+  };
+
+  const onAccountSelect = (e) => {
+    state.accountSelect = e;
+  };
+
+  const onConfirm = async () => {
+    let payload = {
+      money: state.amount,
+      ...state.accountInfo,
+    };
+
+    if (payload.money > userInfo.commission || payload.money <= 0) {
+      sheep.$helper.toast('请输入正确的提现金额');
+      return;
+    }
+
+    if (!payload.type) {
+      sheep.$helper.toast('请选择提现方式');
+      return;
+    }
+
+    if (!payload.account_name || !payload.account_header || !payload.account_no) {
+      sheep.$helper.toast('请完善您的账户信息');
+      return;
+    }
+
+    if (sheep.$platform.name === 'H5' && payload.type === 'wechat') {
+      sheep.$helper.toast('请使用微信浏览器操作');
+      return;
+    }
+
+    let { error, msg, data } = await sheep.$api.pay.withdraw.apply(payload);
+    if (error === -1) {
+      sheep.$platform.useProvider('wechat').bind();
+    }
+    if (error === 0) {
+      userStore.getInfo();
+      uni.showModal({
+        title: '操作成功',
+        content: '您的提现申请已成功提交',
+        cancelText: '继续提现',
+        confirmText: '查看记录',
+        success: function (res) {
+          res.confirm && sheep.$router.go('/pages/pay/withdraw-log');
+        },
+      });
+    }
+  };
+
+  async function getWithdrawRules() {
+    let { error, data } = await sheep.$api.pay.withdraw.rules();
+    if (error === 0) {
+      state.rules = data;
+      state.rulesList = filterRules(state.rules);
+    }
+  }
+
+  onBeforeMount(() => {
+    getWithdrawRules();
+  });
+</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>

+ 57 - 0
pages/public/error.vue

@@ -0,0 +1,57 @@
+<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>
+    <s-empty
+      v-else-if="errCode === 'TemplateError'"
+      icon="/static/internet-empty.png"
+      text="未找到模板"
+      showAction
+      actionText="重新加载"
+      @clickAction="onReconnect"
+      buttonColor="#ff3000"
+    ></s-empty>
+    <s-empty
+      v-else-if="errCode !== ''"
+      icon="/static/internet-empty.png"
+      :text="errMsg"
+      showAction
+      actionText="重新加载"
+      @clickAction="onReconnect"
+      buttonColor="#ff3000"
+    ></s-empty>
+  </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',
+    });
+    ShoproInit();
+  }
+</script>
+
+<style lang="scss" scoped>
+  .error-page {
+    width: 100%;
+  }
+</style>

+ 110 - 0
pages/public/faq.vue

@@ -0,0 +1,110 @@
+<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(() => {
+    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>

+ 226 - 0
pages/public/feedback.vue

@@ -0,0 +1,226 @@
+<template>
+  <s-layout class="set-wrap" title="问题反馈">
+    <uni-forms ref="form" :modelValue="state.formData" border>
+      <view class="bg-white type-box ss-p-x-20 ss-p-y-30">
+        <view class="title ss-m-b-44">请选择类型</view>
+        <view class="ss-m-l-12">
+          <radio-group @change="radioChange">
+            <label
+              class="ss-flex ss-col-center ss-m-b-40"
+              v-for="item in state.radioList"
+              :key="item.type"
+            >
+              <radio :value="item.type" color="var(--ui-BG-Main)" style="transform: scale(0.8)" />
+              <view class="radio-subtitle">{{ item.type }}</view>
+            </label>
+          </radio-group>
+        </view>
+      </view>
+      <view class="bg-white ss-p-x-20 ss-p-y-30 ss-m-t-20">
+        <view class="title ss-m-b-30"> 相关描述 </view>
+        <view class="textarea">
+          <uni-easyinput
+            :inputBorder="false"
+            type="textarea"
+            v-model="state.formData.content"
+            placeholderStyle="color:#BBBBBB;font-size:30rpx;font-weight:400;line-height:normal"
+            placeholder="客官~请描述您遇到的问题,建议上传照片"
+            clearable
+          ></uni-easyinput>
+          <s-uploader
+            v-model:url="state.formData.images"
+            fileMediatype="image"
+            limit="9"
+            mode="grid"
+            :imageStyles="{ width: '168rpx', height: '168rpx' }"
+          ></s-uploader>
+        </view>
+      </view>
+      <view class="bg-white ss-p-x-20 ss-p-y-30 ss-m-t-20">
+        <view class="title ss-m-b-30"> 联系方式 </view>
+        <view class="mobile-box">
+          <uni-easyinput
+            :inputBorder="false"
+            type="number"
+            v-model="state.formData.phone"
+            paddingLeft="10"
+            placeholder="请输入您的联系电话"
+          />
+        </view>
+      </view>
+    </uni-forms>
+    <su-fixed bottom placeholder>
+      <view class="ss-flex ss-row-between ss-p-x-30 ss-p-y-10">
+        <button class="kefu-btn ss-reset-button" @tap="sheep.$router.go('/pages/chat/index')">
+          联系客服
+        </button>
+        <button class="submit-btn ss-reset-button ui-BG-Main ui-Shadow-Main" @tap="onSubmit">
+          提交
+        </button>
+      </view>
+    </su-fixed>
+  </s-layout>
+</template>
+
+<script setup>
+  import { onLoad } from '@dcloudio/uni-app';
+  import { computed, reactive, ref, unref } from 'vue';
+  import sheep from '@/sheep';
+
+  const filesRef = ref(null);
+  const state = reactive({
+    radioList: [
+      {
+        type: '产品功能问题反馈',
+      },
+      {
+        type: '建议及意见反馈',
+      },
+      {
+        type: '投诉客服其他问题',
+      },
+    ],
+    formData: {
+      content: '',
+      phone: '',
+      images: [],
+      type: '',
+    },
+    imageFiles: [],
+    current: 0,
+  });
+
+  async function onSubmit() {
+    if (!state.formData.type) {
+      sheep.$helper.toast('请选择类型');
+      return;
+    }
+    if (!state.formData.content) {
+      sheep.$helper.toast('请描述您遇到的问题');
+      return;
+    }
+    if (!state.formData.phone) {
+      sheep.$helper.toast('请输入您的联系方式');
+      return;
+    }
+
+    const { error } = await sheep.$api.app.feedback(state.formData);
+    if (error === 0) {
+      sheep.$router.back();
+    }
+  }
+
+  function radioChange(e) {
+    state.formData.type = e.detail.value;
+  }
+</script>
+
+<style lang="scss" scoped>
+  .type-box {
+    border-top: 2rpx solid #f9fafb;
+  }
+
+  .uni-forms {
+    width: 100%;
+  }
+
+  .title {
+    font-size: 30rpx;
+    font-weight: bold;
+    color: #333333;
+    line-height: normal;
+  }
+
+  :deep() {
+    .uni-easyinput__placeholder-class {
+      color: #bbbbbb !important;
+      font-size: 28rpx !important;
+      font-weight: 400 !important;
+      line-height: normal !important;
+    }
+
+    .uni-forms-item__label .label-text {
+      font-size: 28rpx !important;
+      color: #333333 !important;
+      line-height: normal !important;
+    }
+
+    .uni-list-item__content-title {
+      font-size: 28rpx !important;
+      color: #333333 !important;
+      line-height: normal !important;
+    }
+
+    .uni-easyinput__content-textarea {
+      font-size: 28rpx !important;
+      color: #333333 !important;
+      line-height: normal !important;
+      margin-top: 4rpx !important;
+      padding-left: 20rpx !important;
+    }
+
+    .uni-icons {
+      font-size: 40rpx !important;
+    }
+
+    .icon-del-box {
+      width: 32rpx;
+      height: 32rpx;
+      top: 0;
+      right: 0;
+
+      .icon-del {
+        width: 24rpx;
+      }
+    }
+  }
+
+  .radio-subtitle {
+    font-size: 28rpx;
+    font-weight: 500;
+    color: #333333;
+    line-height: 42rpx;
+  }
+
+  .textarea {
+    min-height: 322rpx;
+    background: #f9fafb;
+    border-radius: 20rpx;
+    padding: 20rpx;
+    margin: 30rpx 20rpx 46rpx 0;
+
+    .area {
+      height: 238rpx;
+      font-size: 26rpx;
+      font-weight: 500;
+      color: #333;
+      line-height: 50rpx;
+      width: 100%;
+    }
+
+    .pl-style {
+      font-size: 24rpx;
+      color: #b1b3c7;
+      font-weight: 500;
+    }
+  }
+
+  .mobile-box {
+    background: #f9fafb;
+    border-radius: 20rpx;
+  }
+
+  .submit-btn {
+    width: 334rpx;
+    height: 74rpx;
+    border-radius: 37rpx;
+  }
+
+  .kefu-btn {
+    width: 334rpx;
+    height: 74rpx;
+    border-radius: 37rpx;
+    background: #eeeeee;
+    color: #333333;
+  }
+</style>

+ 47 - 0
pages/public/richtext.vue

@@ -0,0 +1,47 @@
+<template>
+  <s-layout class="set-wrap" :title="state.title" :bgStyle="{ color: '#FFF' }">
+    <view class="ss-p-30"><mp-html class="richtext" :content="state.content"></mp-html></view>
+  </s-layout>
+</template>
+
+<script setup>
+  import { onLoad } from '@dcloudio/uni-app';
+  import { reactive } from 'vue';
+  import sheep from '@/sheep';
+
+  const state = reactive({
+    title: '',
+    content: '',
+  });
+
+  async function getRichTextContent(id) {
+    const { error, data } = await sheep.$api.data.richtext(id);
+    if (error === 0) {
+      state.content = data.content;
+      if (state.title === '') {
+        state.title = data.title;
+        uni.setNavigationBarTitle({
+          title: state.title,
+        });
+      }
+    }
+  }
+  onLoad((options) => {
+    if (options.title) {
+      state.title = options.title;
+      uni.setNavigationBarTitle({
+        title: state.title,
+      });
+    }
+    getRichTextContent(options.id);
+  });
+</script>
+
+<style lang="scss" scoped>
+  .set-title {
+    margin: 0 30rpx;
+  }
+
+  .richtext {
+  }
+</style>

+ 239 - 0
pages/public/setting.vue

@@ -0,0 +1,239 @@
+<template>
+  <s-layout class="set-wrap" title="系统设置" :bgStyle="{ color: '#fff' }">
+    <view class="header-box ss-flex-col ss-row-center ss-col-center">
+      <image
+        class="logo-img ss-m-b-46"
+        :src="sheep.$url.cdn(appInfo.logo)"
+        mode="aspectFit"
+      ></image>
+      <view class="name ss-m-b-24">{{ appInfo.name }}</view>
+    </view>
+
+    <view class="container-list">
+      <uni-list :border="false">
+        <uni-list-item
+          title="当前版本"
+          :rightText="appInfo.version"
+          showArrow
+          clickable
+          :border="false"
+          class="list-border"
+          @tap="onCheckUpdate"
+        ></uni-list-item>
+        <uni-list-item
+          title="本地缓存"
+          :rightText="storageSize"
+          showArrow
+          :border="false"
+          class="list-border"
+        ></uni-list-item>
+        <uni-list-item
+          title="意见反馈"
+          showArrow
+          clickable
+          :border="false"
+          class="list-border"
+          @tap="sheep.$router.go('/pages/public/feedback')"
+        ></uni-list-item>
+        <uni-list-item
+          title="关于我们"
+          showArrow
+          clickable
+          :border="false"
+          class="list-border"
+          @tap="
+            sheep.$router.go('/pages/public/richtext', {
+              id: appInfo.about_us.id,
+              title: appInfo.about_us.title,
+            })
+          "
+        ></uni-list-item>
+        <!-- 为了过审 只有iOS-App有注销账号功能 -->
+        <uni-list-item
+          v-if="isLogin && sheep.$platform.os === 'ios' && sheep.$platform.name === 'App'"
+          title="注销账号"
+          rightText=""
+          showArrow
+          clickable
+          :border="false"
+          class="list-border"
+          @click="onLogoff"
+        ></uni-list-item>
+      </uni-list>
+    </view>
+
+    <view class="set-footer ss-flex-col ss-row-center ss-col-center">
+      <view class="agreement-box ss-flex ss-col-center ss-m-b-40">
+        <view class="ss-flex ss-col-center ss-m-b-10">
+          <view
+            class="tcp-text"
+            @tap="
+              sheep.$router.go('/pages/public/richtext', {
+                id: appInfo.user_protocol.id,
+                title: appInfo.user_protocol.title,
+              })
+            "
+          >
+            《{{ appInfo.user_protocol.title }}》
+          </view>
+          <view class="agreement-text">与</view>
+          <view
+            class="tcp-text"
+            @tap="
+              sheep.$router.go('/pages/public/richtext', {
+                id: appInfo.privacy_protocol.id,
+                title: appInfo.privacy_protocol.title,
+              })
+            "
+          >
+            《{{ appInfo.privacy_protocol.title }}》
+          </view>
+        </view>
+      </view>
+      <view class="copyright-text ss-m-b-10">{{ appInfo.copyright }}</view>
+      <view class="copyright-text">{{ appInfo.copytime }}</view>
+    </view>
+    <su-fixed bottom placeholder>
+      <view class="ss-p-x-20 ss-p-b-40">
+        <button
+          class="loginout-btn ss-reset-button ui-BG-Main ui-Shadow-Main"
+          @tap="onLogout"
+          v-if="isLogin"
+        >
+          退出登录
+        </button>
+      </view>
+    </su-fixed>
+  </s-layout>
+</template>
+
+<script setup>
+  import sheep from '@/sheep';
+  import { computed, reactive } from 'vue';
+
+  const appInfo = computed(() => sheep.$store('app').info);
+  const isLogin = computed(() => sheep.$store('user').isLogin);
+  const storageSize = uni.getStorageInfoSync().currentSize + 'Kb';
+  const state = reactive({
+    showModal: false,
+  });
+
+  function onCheckUpdate() {
+    sheep.$platform.checkUpdate();
+    // 小程序初始化时已检查更新
+    // H5实时更新无需检查
+    // App 1.跳转应用市场更新 2.手动热更新 3.整包更新
+  }
+  function onLogoff() {
+    uni.showModal({
+      title: '提示',
+      content: '确认注销账号?',
+      success: async function (res) {
+        if (res.confirm) {
+          const { error } = await sheep.$api.user.logoff();
+          if (error === 0) {
+            sheep.$store('user').logout();
+            sheep.$router.go('/pages/index/user');
+          }
+        }
+      },
+    });
+  }
+
+  function onLogout() {
+    uni.showModal({
+      title: '提示',
+      content: '确认退出账号?',
+      success: async function (res) {
+        if (res.confirm) {
+          const result = await sheep.$store('user').logout();
+          if (result) {
+            sheep.$router.go('/pages/index/user');
+          }
+        }
+      },
+    });
+  }
+</script>
+
+<style lang="scss" scoped>
+  .container-list {
+    width: 100%;
+  }
+
+  .set-title {
+    margin: 0 30rpx;
+  }
+
+  .header-box {
+    padding: 100rpx 0;
+
+    .logo-img {
+      width: 160rpx;
+      height: 160rpx;
+      border-radius: 50%;
+    }
+
+    .name {
+      font-size: 42rpx;
+      font-weight: 400;
+      color: $dark-3;
+    }
+
+    .version {
+      font-size: 32rpx;
+      font-weight: 500;
+      line-height: 32rpx;
+      color: $gray-b;
+    }
+  }
+
+  .set-footer {
+    margin: 100rpx 0 0 0;
+
+    .copyright-text {
+      font-size: 22rpx;
+      font-weight: 500;
+      color: $gray-c;
+      line-height: 30rpx;
+    }
+
+    .agreement-box {
+      font-size: 26rpx;
+      font-weight: 500;
+
+      .tcp-text {
+        color: var(--ui-BG-Main);
+      }
+
+      .agreement-text {
+        color: $dark-9;
+      }
+    }
+  }
+
+  .loginout-btn {
+    width: 100%;
+    height: 80rpx;
+    border-radius: 40rpx;
+    font-size: 30rpx;
+  }
+
+  .list-border {
+    font-size: 28rpx;
+    font-weight: 400;
+    color: #333333;
+    border-bottom: 2rpx solid #eeeeee;
+  }
+
+  :deep(.uni-list-item__content-title) {
+    font-size: 28rpx;
+    font-weight: 500;
+    color: #333;
+  }
+
+  :deep(.uni-list-item__extra-text) {
+    color: #bbbbbb;
+    font-size: 28rpx;
+  }
+</style>

+ 15 - 0
pages/public/webview.vue

@@ -0,0 +1,15 @@
+<template>
+  <view><web-view :src="url"></web-view></view>
+</template>
+
+<script setup>
+  import { onLoad } from '@dcloudio/uni-app';
+  import { ref } from 'vue';
+
+  const url = ref('');
+  onLoad((options) => {
+    url.value = decodeURIComponent(options.url);
+  });
+</script>
+
+<style lang="scss" scoped></style>

+ 261 - 0
pages/user/address/edit.vue

@@ -0,0 +1,261 @@
+<template>
+	<s-layout :title="state.model.id ? '编辑地址' : '新增地址'">
+		<uni-forms ref="addressFormRef" v-model="state.model" :rules="state.rules" validateTrigger="bind"
+			labelWidth="160" labelAlign="left" border :labelStyle="{ fontWeight: 'bold' }">
+			<view class="bg-white form-box ss-p-x-30">
+				<uni-forms-item name="consignee" label="收货人" class="form-item">
+					<uni-easyinput v-model="state.model.consignee" placeholder="请填写收货人姓名" :inputBorder="false"
+						placeholderStyle="color:#BBBBBB;font-size:30rpx;font-weight:400;line-height:normal" />
+				</uni-forms-item>
+
+				<uni-forms-item name="mobile" label="手机号" class="form-item">
+					<uni-easyinput v-model="state.model.mobile" type="number" placeholder="请输入手机号" :inputBorder="false"
+						placeholderStyle="color:#BBBBBB;font-size:30rpx;font-weight:400;line-height:normal">
+					</uni-easyinput>
+				</uni-forms-item>
+				<uni-forms-item name="region" label="省市区" @tap="state.showRegion = true" class="form-item">
+					<uni-easyinput v-model="state.model.region" disabled :inputBorder="false"
+						:styles="{ disableColor: '#fff', color: '#333' }"
+						placeholderStyle="color:#BBBBBB;font-size:30rpx;font-weight:400;line-height:normal"
+						placeholder="请选择省市区">
+						<template v-slot:right>
+							<uni-icons type="right"></uni-icons>
+						</template>
+					</uni-easyinput>
+				</uni-forms-item>
+				<uni-forms-item name="address" label="详细地址" :formItemStyle="{ alignItems: 'flex-start' }"
+					:labelStyle="{ lineHeight: '5em' }" class="textarea-item">
+					<uni-easyinput :inputBorder="false" type="textarea" v-model="state.model.address"
+						placeholderStyle="color:#BBBBBB;font-size:30rpx;font-weight:400;line-height:normal"
+						placeholder="请输入详细地址" clearable></uni-easyinput>
+				</uni-forms-item>
+			</view>
+
+			<view class="ss-m-y-20 bg-white ss-p-x-30 ss-flex ss-row-between ss-col-center default-box">
+				<view class="default-box-title"> 设为默认地址 </view>
+				<su-switch style="transform: scale(0.8)" v-model="state.model.is_default"></su-switch>
+			</view>
+		</uni-forms>
+		<su-fixed bottom :opacity="false" bg="" placeholder :noFixed="false" :index="10">
+			<view class="footer-box ss-flex-col ss-row-between ss-p-20">
+				<view class="ss-m-b-20"><button class="ss-reset-button save-btn ui-Shadow-Main"
+						@tap="onSave">保存</button></view>
+				<button v-if="state.model.id" class="ss-reset-button cancel-btn" @tap="onDelete">
+					删除
+				</button>
+			</view>
+		</su-fixed>
+		<!-- 省市区弹窗 -->
+		<su-region-picker :show="state.showRegion" @cancel="state.showRegion = false" @confirm="onRegionConfirm">
+		</su-region-picker>
+	</s-layout>
+</template>
+
+<script setup>
+	import {
+		computed,
+		watch,
+		ref,
+		reactive,
+		unref
+	} from 'vue';
+	import sheep from '@/sheep';
+	import {
+		onLoad,
+		onPageScroll
+	} from '@dcloudio/uni-app';
+	import _ from 'lodash';
+	import {
+		consignee,
+		mobile,
+		address,
+		region
+	} from '@/sheep/validate/form';
+
+	const addressFormRef = ref(null);
+	const state = reactive({
+		showRegion: false,
+		model: {
+			consignee: '',
+			mobile: '',
+			address: '',
+			is_default: false,
+			region: '',
+		},
+		rules: {
+			consignee,
+			mobile,
+			address,
+			region,
+		},
+	});
+	watch(
+		() => state.model.province_name,
+		(newValue) => {
+			if (newValue) {
+				state.model.region =
+					`${state.model.province_name}-${state.model.city_name}-${state.model.district_name}`;
+			}
+		}, {
+			deep: true,
+		},
+	);
+	const onRegionConfirm = (e) => {
+		state.model = {
+			...state.model,
+			...e,
+		};
+		state.showRegion = false;
+	};
+	const getAreaData = () => {
+		if (_.isEmpty(uni.getStorageSync('areaData'))) {
+			sheep.$api.data.area().then((res) => {
+				if (res.code === 0) {
+					uni.setStorageSync('areaData', res.data);
+				}
+			});
+		}
+	};
+	const onSave = async () => {
+		const validate = await unref(addressFormRef)
+			.validate()
+			.catch((error) => {
+				console.log('error: ', error);
+			});
+		if (!validate) return;
+
+		let res = null;
+		if (state.model.id) {
+			res = await sheep.$api.user.address.update({
+				id: state.model.id,
+				areaId: state.model.district_id,
+				defaultStatus: state.model.is_default,
+				detailAddress: state.model.address,
+				mobile: state.model.mobile,
+				name: state.model.consignee
+			});
+		} else {
+			res = await sheep.$api.user.address.create({
+				areaId: state.model.district_id,
+				defaultStatus: state.model.is_default,
+				detailAddress: state.model.address,
+				mobile: state.model.mobile,
+				name: state.model.consignee
+			});
+		}
+		if (res.code === 0) {
+			sheep.$router.back();
+		}
+	};
+
+	const onDelete = () => {
+		uni.showModal({
+			title: '提示',
+			content: '确认删除此收货地址吗?',
+			success: async function(res) {
+				if (res.confirm) {
+					const {
+						code
+					} = await sheep.$api.user.address.delete(state.model.id);
+					if (code === 0) {
+						sheep.$router.back();
+					}
+				}
+			},
+		});
+	};
+	onLoad(async (options) => {
+		getAreaData();
+		if (options.id) {
+			let res = await sheep.$api.user.address.detail(options.id);
+			if (res.code === 0) {
+				state.model = {
+					...state.model,
+					district_id: res.data.areaId,
+					is_default: res.data.defaultStatus,
+					address: res.data.detailAddress,
+					mobile: res.data.mobile,
+					consignee: res.data.name,
+					id: res.data.id,
+					province_name: res.data.areaName.split(' ')[0],
+					city_name: res.data.areaName.split(' ')[1],
+					district_name: res.data.areaName.split(' ')[2]
+				};
+			}
+		}
+
+		if (options.data) {
+			let data = JSON.parse(options.data);
+			console.log(data)
+			state.model = {
+				...state.model,
+				...data,
+			};
+		}
+	});
+</script>
+
+<style lang="scss" scoped>
+	:deep() {
+		.uni-forms-item__label .label-text {
+			font-size: 28rpx !important;
+			color: #333333 !important;
+			line-height: normal !important;
+		}
+
+		.uni-easyinput__content-input {
+			font-size: 28rpx !important;
+			color: #333333 !important;
+			line-height: normal !important;
+			padding-left: 0 !important;
+		}
+
+		.uni-easyinput__content-textarea {
+			font-size: 28rpx !important;
+			color: #333333 !important;
+			line-height: normal !important;
+			margin-top: 8rpx !important;
+		}
+
+		.uni-icons {
+			font-size: 40rpx !important;
+		}
+
+		.is-textarea-icon {
+			margin-top: 22rpx;
+		}
+
+		.is-disabled {
+			color: #333333;
+		}
+	}
+
+	.default-box {
+		width: 100%;
+		box-sizing: border-box;
+		height: 100rpx;
+
+		.default-box-title {
+			font-size: 28rpx;
+			color: #333333;
+			line-height: normal;
+		}
+	}
+
+	.footer-box {
+		.save-btn {
+			width: 710rpx;
+			height: 80rpx;
+			border-radius: 40rpx;
+			background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
+			color: $white;
+		}
+
+		.cancel-btn {
+			width: 710rpx;
+			height: 80rpx;
+			border-radius: 40rpx;
+			background: var(--ui-BG);
+		}
+	}
+</style>

+ 147 - 0
pages/user/address/list.vue

@@ -0,0 +1,147 @@
+<template>
+	<s-layout title="收货地址" :bgStyle="{ color: '#FFF' }">
+		<view v-if="state.list.length">
+			<s-address-item hasBorderBottom v-for="item in state.list" :key="item.id" :item="item"
+				@tap="onSelect(item)">
+			</s-address-item>
+		</view>
+
+		<su-fixed bottom placeholder>
+			<view class="footer-box ss-flex ss-row-between ss-p-20">
+				<!-- 微信小程序和微信H5 -->
+				<button v-if="['WechatMiniProgram', 'WechatOfficialAccount'].includes(sheep.$platform.name)"
+					@tap="importWechatAddress"
+					class="border ss-reset-button sync-wxaddress ss-m-20 ss-flex ss-row-center ss-col-center">
+					<text class="cicon-weixin ss-p-r-10" style="color: #09bb07; font-size: 40rpx"></text>
+					导入微信地址
+				</button>
+				<button class="add-btn ss-reset-button ui-Shadow-Main"
+					@tap="sheep.$router.go('/pages/user/address/edit')">
+					新增收货地址
+				</button>
+			</view>
+		</su-fixed>
+		<s-empty v-if="state.list.length === 0 && !state.loading" text="暂无收货地址" icon="/static/data-empty.png" />
+	</s-layout>
+</template>
+
+<script setup>
+	import {
+		reactive,
+		onBeforeMount
+	} from 'vue';
+	import {
+		onShow
+	} from '@dcloudio/uni-app';
+	import sheep from '@/sheep';
+	import {
+		isEmpty
+	} from 'lodash';
+
+	const state = reactive({
+		list: [],
+		loading: true,
+	});
+
+	// 选择收货地址
+	const onSelect = (addressInfo) => {
+		uni.$emit('SELECT_ADDRESS', {
+			addressInfo,
+		});
+		sheep.$router.back();
+	};
+
+	// 导入微信地址
+	function importWechatAddress() {
+		let wechatAddress = {};
+		// #ifdef MP
+		uni.chooseAddress({
+			success: (res) => {
+				wechatAddress = {
+					consignee: res.userName,
+					mobile: res.telNumber,
+					province_name: res.provinceName,
+					city_name: res.cityName,
+					district_name: res.countyName,
+					address: res.detailInfo,
+					region: '',
+					is_default: false,
+				};
+				if (!isEmpty(wechatAddress)) {
+					sheep.$router.go('/pages/user/address/edit', {
+						data: JSON.stringify(wechatAddress),
+					});
+				}
+			},
+			fail: (err) => {
+				console.log('%cuni.chooseAddress,调用失败', 'color:green;background:yellow');
+			},
+		});
+		// #endif
+		// #ifdef H5
+		sheep.$platform.useProvider('wechat').jssdk.openAddress({
+			success: (res) => {
+				wechatAddress = {
+					consignee: res.userName,
+					mobile: res.telNumber,
+					province_name: res.provinceName,
+					city_name: res.cityName,
+					district_name: res.countryName,
+					address: res.detailInfo,
+					region: '',
+					is_default: false,
+				};
+				if (!isEmpty(wechatAddress)) {
+					sheep.$router.go('/pages/user/address/edit', {
+						data: JSON.stringify(wechatAddress),
+					});
+				}
+			},
+		});
+		// #endif
+	}
+
+	onShow(async () => {
+		state.list = (await sheep.$api.user.address.list()).data;
+		state.loading = false;
+	});
+
+	onBeforeMount(() => {
+		if (!!uni.getStorageSync('areaData')) {
+			return;
+		}
+		// 提前加载省市区数据
+		sheep.$api.data.area().then((res) => {
+			if (res.error === 0) {
+				uni.setStorageSync('areaData', res.data);
+			}
+		});
+	});
+</script>
+
+<style lang="scss" scoped>
+	.footer-box {
+		.add-btn {
+			flex: 1;
+			background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
+			border-radius: 80rpx;
+			font-size: 30rpx;
+			font-weight: 500;
+			line-height: 80rpx;
+			color: $white;
+			position: relative;
+			z-index: 1;
+		}
+
+		.sync-wxaddress {
+			flex: 1;
+			line-height: 80rpx;
+			background: $white;
+			border-radius: 80rpx;
+			font-size: 30rpx;
+			font-weight: 500;
+			color: $dark-6;
+			margin-right: 18rpx;
+		}
+	}
+</style>

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