合并初始化

This commit is contained in:
落日晚风
2023-12-11 09:24:16 +08:00
parent 376376b875
commit c5b1f3ac93
533 changed files with 82087 additions and 0 deletions

12
.gitignore vendored Normal file
View File

@@ -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
.prettierignore Normal file
View File

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

10
.prettierrc Normal file
View File

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

46
App.vue Normal file
View File

@@ -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
LICENSE Normal file
View File

@@ -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
README.md Normal file
View File

@@ -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
androidPrivacy.json Normal file
View File

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

17
index.html Normal file
View File

@@ -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
jsconfig.json Normal file
View File

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

15
main.js Normal file
View File

@@ -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
manifest.json Normal file
View File

@@ -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
package.json Normal file
View File

@@ -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
pages.json Normal file
View File

@@ -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"
}
]
}
}

View File

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

View File

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

View File

@@ -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
pages/activity/index.vue Normal file
View File

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

View File

@@ -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
pages/app/score-shop.vue Normal file
View File

@@ -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
pages/app/sign.vue Normal file
View File

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

View File

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

View File

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

View File

@@ -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
pages/chat/emoji.js Normal file
View File

@@ -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
pages/chat/index.vue Normal file
View File

@@ -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
pages/chat/socket.js Normal file
View File

@@ -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
pages/commission/apply.vue Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
pages/commission/goods.vue Normal file
View File

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

View File

@@ -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
pages/commission/order.vue Normal file
View File

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

View File

@@ -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
pages/commission/team.vue Normal file
View File

@@ -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
pages/coupon/detail.vue Normal file
View File

@@ -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
pages/coupon/list.vue Normal file
View File

@@ -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
pages/goods/comment/add.vue Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
pages/goods/groupon.vue Normal file
View File

@@ -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
pages/goods/index.vue Normal file
View File

@@ -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
pages/goods/list.vue Normal file
View File

@@ -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
pages/goods/score.vue Normal file
View File

@@ -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
pages/goods/seckill.vue Normal file
View File

@@ -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
pages/index/cart.vue Normal file
View File

@@ -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
pages/index/category.vue Normal file
View File

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

View File

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

View File

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

View File

@@ -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
pages/index/index.vue Normal file
View File

@@ -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
pages/index/login.vue Normal file
View File

@@ -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
pages/index/page.vue Normal file
View File

@@ -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
pages/index/search.vue Normal file
View File

@@ -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
pages/index/user.vue Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
pages/order/confirm.vue Normal file
View File

@@ -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
pages/order/detail.vue Normal file
View File

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

View File

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

View File

@@ -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
pages/order/express/log.vue Normal file
View File

@@ -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
pages/order/invoice.vue Normal file
View File

@@ -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
pages/order/list.vue Normal file
View File

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

View File

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

View File

@@ -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
pages/pay/index.vue Normal file
View File

@@ -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
pages/pay/recharge-log.vue Normal file
View File

@@ -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
pages/pay/recharge.vue Normal file
View File

@@ -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
pages/pay/result.vue Normal file
View File

@@ -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
pages/pay/withdraw-log.vue Normal file
View File

@@ -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
pages/pay/withdraw.vue Normal file
View File

@@ -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
pages/public/error.vue Normal file
View File

@@ -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
pages/public/faq.vue Normal file
View File

@@ -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
pages/public/feedback.vue Normal file
View File

@@ -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
pages/public/richtext.vue Normal file
View File

@@ -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
pages/public/setting.vue Normal file
View File

@@ -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
pages/public/webview.vue Normal file
View File

@@ -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
pages/user/address/edit.vue Normal file
View File

@@ -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
pages/user/address/list.vue Normal file
View File

@@ -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 have changed in this diff Show More