first commit

This commit is contained in:
慕下 2024-08-07 10:31:42 +08:00
commit f1c5ac864a
605 changed files with 89082 additions and 0 deletions

24
.env Normal file
View File

@ -0,0 +1,24 @@
# 版本号
SHOPRO_VERSION = v1.8.3
# 后端接口 - 正式环境(通过 process.env.NODE_ENV 非 development
SHOPRO_BASE_URL = https://zysc.fjptzykj.com
# 后端接口 - 测试环境(通过 process.env.NODE_ENV = development
SHOPRO_DEV_BASE_URL = https://zysc.fjptzykj.com
### SHOPRO_DEV_BASE_URL = http://yunai.natapp1.cc
# 后端接口前缀(一般不建议调整)
SHOPRO_API_PATH = /app-api
# 后端 websocket 接口前缀
SHOPRO_WEBSOCKET_PATH = /infra/ws
# 开发环境运行端口
SHOPRO_DEV_PORT = 3000
# 客户端静态资源地址 空=默认使用服务端指定的CDN资源地址前缀 | local=本地 | http(s)://xxx.xxx=自定义静态资源地址前缀
SHOPRO_STATIC_URL = https://zysc.fjptzykj.com
# 是否开启直播 1 开启直播 | 0 关闭直播 (小程序官方后台未审核开通直播权限时请勿开启)
SHOPRO_MPLIVE_ON = 0

11
.gitignore vendored Normal file
View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

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

39
App.vue Normal file
View File

@ -0,0 +1,39 @@
<script setup>
import { onLaunch, onShow, onError } from '@dcloudio/uni-app';
import { ShoproInit } from './sheep';
onLaunch(() => {
// 使
uni.hideTabBar();
// Shopro
ShoproInit();
});
onError((err) => {
console.log('AppOnError:', err);
});
onShow((options) => {
// #ifdef APP-PLUS
// urlSchemes
const args = plus.runtime.arguments;
if (args) {
}
//
uni.getClipboardData({
success: (res) => { },
});
// #endif
// #ifdef MP-WEIXIN
//
console.log(options,'options');
// #endif
});
</script>
<style lang="scss">
@import '@/sheep/scss/index.scss';
</style>

21
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.

56
README.md Normal file
View File

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

3
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,
};
}

239
manifest.json Normal file
View File

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

102
package.json Normal file
View File

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

655
pages.json Normal file
View File

@ -0,0 +1,655 @@
{
"easycom": {
"autoscan": true,
"custom": {
"^s-(.*)": "@/sheep/components/s-$1/s-$1.vue",
"^su-(.*)": "@/sheep/ui/su-$1/su-$1.vue"
}
},
"pages": [{
"path": "pages/index/index",
"aliasPath": "/",
"style": {
"navigationBarTitleText": "首页",
"enablePullDownRefresh": true
},
"meta": {
"auth": false,
"sync": true,
"title": "首页",
"group": "商城"
}
},
{
"path": "pages/index/user",
"style": {
"navigationBarTitleText": "个人中心",
"enablePullDownRefresh": true
},
"meta": {
"sync": true,
"title": "个人中心",
"group": "商城"
}
},
{
"path": "pages/index/category",
"style": {
"navigationBarTitleText": "商品分类"
},
"meta": {
"sync": true,
"title": "商品分类",
"group": "商城"
}
},
{
"path": "pages/index/cart",
"style": {
"navigationBarTitleText": "购物车"
},
"meta": {
"sync": true,
"title": "购物车",
"group": "商城"
}
},
{
"path": "pages/index/login",
"style": {
"navigationBarTitleText": "登录"
}
},
{
"path": "pages/index/search",
"style": {
"navigationBarTitleText": "搜索"
},
"meta": {
"sync": true,
"title": "搜索",
"group": "商城"
}
},
{
"path": "pages/index/page",
"style": {
"navigationBarTitleText": ""
},
"meta": {
"auth": false,
"sync": true,
"title": "自定义页面",
"group": "商城"
}
}
],
"subPackages": [{
"root": "pages/goods",
"pages": [{
"path": "index",
"style": {
"navigationBarTitleText": "商品详情"
},
"meta": {
"sync": true,
"title": "普通商品",
"group": "商品"
}
},
{
"path": "groupon",
"style": {
"navigationBarTitleText": "拼团商品"
},
"meta": {
"sync": true,
"title": "拼团商品",
"group": "商品"
}
},
{
"path": "seckill",
"style": {
"navigationBarTitleText": "秒杀商品"
},
"meta": {
"sync": true,
"title": "秒杀商品",
"group": "商品"
}
},
{
"path": "list",
"style": {
"navigationBarTitleText": "商品列表"
},
"meta": {
"sync": true,
"title": "商品列表",
"group": "商品"
}
},
{
"path": "comment/add",
"style": {
"navigationBarTitleText": "评价商品"
},
"meta": {
"auth": true
}
},
{
"path": "comment/list",
"style": {
"navigationBarTitleText": "商品评价"
}
}
]
},
{
"root": "pages/order",
"pages": [{
"path": "detail",
"style": {
"navigationBarTitleText": "订单详情"
},
"meta": {
"auth": true,
"title": "订单详情"
}
},
{
"path": "confirm",
"style": {
"navigationBarTitleText": "确认订单"
},
"meta": {
"auth": true,
"title": "确认订单"
}
},
{
"path": "list",
"style": {
"navigationBarTitleText": "我的订单",
"enablePullDownRefresh": true
},
"meta": {
"auth": true,
"sync": true,
"title": "用户订单",
"group": "订单中心"
}
},
{
"path": "aftersale/apply",
"style": {
"navigationBarTitleText": "申请售后"
},
"meta": {
"auth": true,
"title": "申请售后"
}
},
{
"path": "aftersale/return-delivery",
"style": {
"navigationBarTitleText": "退货物流"
},
"meta": {
"auth": true,
"title": "退货物流"
}
},
{
"path": "aftersale/list",
"style": {
"navigationBarTitleText": "售后列表"
},
"meta": {
"auth": true,
"sync": true,
"title": "售后订单",
"group": "订单中心"
}
},
{
"path": "aftersale/detail",
"style": {
"navigationBarTitleText": "售后详情"
},
"meta": {
"auth": true,
"title": "售后详情"
}
},
{
"path": "aftersale/log",
"style": {
"navigationBarTitleText": "售后进度"
},
"meta": {
"auth": true,
"title": "售后进度"
}
},
{
"path": "express/log",
"style": {
"navigationBarTitleText": "物流轨迹"
},
"meta": {
"auth": true,
"title": "物流轨迹"
}
}
]
},
{
"root": "pages/user",
"pages": [{
"path": "info",
"style": {
"navigationBarTitleText": "我的信息"
},
"meta": {
"auth": true,
"sync": true,
"title": "用户信息",
"group": "用户中心"
}
},
{
"path": "goods-collect",
"style": {
"navigationBarTitleText": "我的收藏"
},
"meta": {
"auth": true,
"sync": true,
"title": "商品收藏",
"group": "用户中心"
}
},
{
"path": "goods-log",
"style": {
"navigationBarTitleText": "我的足迹"
},
"meta": {
"auth": true,
"sync": true,
"title": "浏览记录",
"group": "用户中心"
}
},
{
"path": "address/list",
"style": {
"navigationBarTitleText": "收货地址"
},
"meta": {
"auth": true,
"sync": true,
"title": "地址管理",
"group": "用户中心"
}
},
{
"path": "address/edit",
"style": {
"navigationBarTitleText": "编辑地址"
},
"meta": {
"auth": true,
"title": "编辑地址"
}
},
{
"path": "wallet/money",
"style": {
"navigationBarTitleText": "我的余额"
},
"meta": {
"auth": true,
"sync": true,
"title": "用户余额",
"group": "用户中心"
}
},
{
"path": "wallet/score",
"style": {
"navigationBarTitleText": "我的积分"
},
"meta": {
"auth": true,
"sync": true,
"title": "用户积分",
"group": "用户中心"
}
}
]
},
{
"root": "pages/commission",
"pages": [{
"path": "index",
"style": {
"navigationBarTitleText": "分销"
},
"meta": {
"auth": true,
"sync": true,
"title": "分销中心",
"group": "分销商城"
}
},
{
"path": "wallet",
"style": {
"navigationBarTitleText": "我的佣金"
},
"meta": {
"auth": true,
"sync": true,
"title": "用户佣金",
"group": "分销中心"
}
},
{
"path": "goods",
"style": {
"navigationBarTitleText": "推广商品"
},
"meta": {
"auth": true,
"sync": true,
"title": "推广商品",
"group": "分销商城"
}
},
{
"path": "order",
"style": {
"navigationBarTitleText": "分销订单"
},
"meta": {
"auth": true,
"sync": true,
"title": "分销订单",
"group": "分销商城"
}
},
{
"path": "team",
"style": {
"navigationBarTitleText": "我的团队"
},
"meta": {
"auth": true,
"sync": true,
"title": "我的团队",
"group": "分销商城"
}
}, {
"path": "promoter",
"style": {
"navigationBarTitleText": "推广人排行榜"
},
"meta": {
"auth": true,
"sync": true,
"title": "推广人排行榜",
"group": "分销商城"
}
}, {
"path": "commission-ranking",
"style": {
"navigationBarTitleText": "佣金排行榜"
},
"meta": {
"auth": true,
"sync": true,
"title": "佣金排行榜",
"group": "分销商城"
}
}, {
"path": "withdraw",
"style": {
"navigationBarTitleText": "申请提现"
},
"meta": {
"auth": true,
"sync": true,
"title": "申请提现",
"group": "分销商城"
}
}
]
},
{
"root": "pages/app",
"pages": [{
"path": "sign",
"style": {
"navigationBarTitleText": "签到中心"
},
"meta": {
"auth": true,
"sync": true,
"title": "签到中心",
"group": "应用"
}
}]
},
{
"root": "pages/public",
"pages": [{
"path": "setting",
"style": {
"navigationBarTitleText": "系统设置"
},
"meta": {
"sync": true,
"title": "系统设置",
"group": "通用"
}
},
{
"path": "richtext",
"style": {
"navigationBarTitleText": "富文本"
},
"meta": {
"sync": true,
"title": "富文本",
"group": "通用"
}
},
{
"path": "faq",
"style": {
"navigationBarTitleText": "常见问题"
},
"meta": {
"sync": true,
"title": "常见问题",
"group": "通用"
}
},
{
"path": "error",
"style": {
"navigationBarTitleText": "错误页面"
}
},
{
"path": "webview",
"style": {
"navigationBarTitleText": ""
}
}
]
},
{
"root": "pages/coupon",
"pages": [{
"path": "list",
"style": {
"navigationBarTitleText": "领券中心"
},
"meta": {
"sync": true,
"title": "领券中心",
"group": "优惠券"
}
},
{
"path": "detail",
"style": {
"navigationBarTitleText": "优惠券"
},
"meta": {
"auth": false,
"sync": true,
"title": "优惠券详情",
"group": "优惠券"
}
}
]
},
{
"root": "pages/chat",
"pages": [{
"path": "index",
"style": {
"navigationBarTitleText": "客服"
},
"meta": {
"auth": true,
"sync": true,
"title": "客服",
"group": "客服"
}
}]
},
{
"root": "pages/pay",
"pages": [{
"path": "index",
"style": {
"navigationBarTitleText": "收银台"
}
},
{
"path": "result",
"style": {
"navigationBarTitleText": "支付结果"
}
},
{
"path": "recharge",
"style": {
"navigationBarTitleText": "充值余额"
},
"meta": {
"auth": true,
"sync": true,
"title": "充值余额",
"group": "支付"
}
},
{
"path": "recharge-log",
"style": {
"navigationBarTitleText": "充值记录"
},
"meta": {
"auth": true,
"sync": true,
"title": "充值记录",
"group": "支付"
}
}
]
},
{
"root": "pages/activity",
"pages": [{
"path": "groupon/detail",
"style": {
"navigationBarTitleText": "拼团详情"
}
},
{
"path": "groupon/order",
"style": {
"navigationBarTitleText": "我的拼团",
"enablePullDownRefresh": true
},
"meta": {
"auth": true,
"sync": true,
"title": "拼团订单",
"group": "营销活动"
}
},
{
"path": "index",
"style": {
"navigationBarTitleText": "营销商品"
},
"meta": {
"sync": true,
"title": "营销商品",
"group": "营销活动"
}
},
{
"path": "groupon/list",
"style": {
"navigationBarTitleText": "拼团活动"
},
"meta": {
"sync": true,
"title": "拼团活动",
"group": "营销活动"
}
},
{
"path": "seckill/list",
"style": {
"navigationBarTitleText": "秒杀活动"
},
"meta": {
"sync": true,
"title": "秒杀活动",
"group": "营销活动"
}
}
]
}
],
"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,508 @@
<!-- 拼团订单的详情 -->
<template>
<s-layout title="拼团详情" class="detail-wrap" :navbar="state.data && !state.loading ? 'inner': 'normal'" :onShareAppMessage="shareInfo">
<view v-if="state.loading"></view>
<view v-if="state.data && !state.loading">
<!-- 团长信息 + 活动信息 -->
<view
class="recharge-box"
v-if="state.data.headRecord"
:style="[
{
marginTop: '-' + Number(statusBarHeight + 88) + 'rpx',
paddingTop: Number(statusBarHeight + 108) + 'rpx',
},
]"
>
<s-goods-item
class="goods-box"
:img="state.data.headRecord.picUrl"
:title="state.data.headRecord.spuName"
:price="state.data.headRecord.combinationPrice"
priceColor="#E1212B"
@tap="
sheep.$router.go('/pages/goods/groupon', {
id: state.data.headRecord.activityId
})
"
:style="[{ top: Number(statusBarHeight + 108) + 'rpx' }]"
>
<template #groupon>
<view class="ss-flex">
<view class="sales-title">{{ state.data.headRecord.userSize }}人团</view>
<view class="num-title ss-m-l-20">已拼{{ state.data.headRecord.userCount }}</view>
</view>
</template>
</s-goods-item>
</view>
<view class="countdown-box detail-card ss-p-t-44 ss-flex-col ss-col-center">
<!-- 情况一拼团成功 -->
<view v-if="state.data.headRecord.status === 1">
<view v-if="state.data.orderId">
<view class="countdown-title ss-flex">
<text class="cicon-check-round" />
恭喜您~拼团成功
</view>
</view>
<view v-else>
<view class="countdown-title ss-flex">
<text class="cicon-info" />
抱歉~该团已满员
</view>
</view>
</view>
<!-- 情况二拼团失败 -->
<view v-if="state.data.headRecord.status === 2">
<view class="countdown-title ss-flex">
<text class="cicon-info"></text>
{{ state.data.orderId ? '拼团超时,已自动退款' : '该团已解散' }}
</view>
</view>
<!-- 情况三拼团进行中 -->
<view v-if="state.data.headRecord.status === 0">
<view v-if="state.data.headRecord.expireTime <= new Date().getTime()">
<view class="countdown-title ss-flex">
<text class="cicon-info"></text>
拼团已结束,请关注下次活动
</view>
</view>
<view class="countdown-title ss-flex" v-else>
还差
<view class="num">{{ state.data.headRecord.userSize - state.data.headRecord.userCount }}</view>
拼团成功
<view class="ss-flex countdown-time">
<view class="countdown-h ss-flex ss-row-center">{{ endTime.h }}</view>
<view class="ss-m-x-4">:</view>
<view class="countdown-num ss-flex ss-row-center">
{{ endTime.m }}
</view>
<view class="ss-m-x-4">:</view>
<view class="countdown-num ss-flex ss-row-center">
{{ endTime.s }}
</view>
</view>
</view>
</view>
<!-- 拼团的记录列表展示每个参团人 -->
<view class="ss-m-t-60 ss-flex ss-flex-wrap ss-row-center">
<!-- 团长 -->
<view class="header-avatar ss-m-r-24 ss-m-b-20">
<image :src="sheep.$url.cdn(state.data.headRecord.avatar)" class="avatar-img"></image>
<view class="header-tag ss-flex ss-col-center ss-row-center">团长</view>
</view>
<!-- 团员 -->
<view
class="header-avatar ss-m-r-24 ss-m-b-20"
v-for="item in state.data.memberRecords"
:key="item.id"
>
<image :src="sheep.$url.cdn(item.avatar)" class="avatar-img"></image>
<view
class="header-tag ss-flex ss-col-center ss-row-center"
v-if="item.is_leader == '1'"
>
团长
</view>
</view>
<!-- 还有几个坑位 -->
<view class="default-avatar ss-m-r-24 ss-m-b-20" v-for="item in state.remainNumber" :key="item">
<image
:src="sheep.$url.static('/static/img/shop/avatar/unknown.png')"
class="avatar-img"
></image>
</view>
</view>
</view>
<!-- 情况一拼团成功情况二拼团失败 -->
<view
v-if="state.data.headRecord.status === 1 || state.data.headRecord.status === 2"
class="ss-m-t-40 ss-flex ss-row-center"
>
<button
class="ss-reset-button order-btn"
v-if="state.data.orderId"
@tap="onDetail(state.data.orderId)"
>
查看订单
</button>
<button class="ss-reset-button join-btn" v-else @tap="onCreateGroupon"> 我要开团 </button>
</view>
<!-- 情况三拼团进行中查看订单或参加或邀请好友或参加 -->
<view v-if="state.data.headRecord.status === 0" class="ss-m-t-40 ss-flex ss-row-center">
<view v-if="state.data.headRecord.expireTime <= new Date().getTime()">
<button
class="ss-reset-button join-btn"
v-if="state.data.orderId"
@tap="onDetail(state.data.orderId)"
>
查看订单
</button>
<button
class="ss-reset-button disabled-btn"
v-else
disabled
@tap="onDetail(state.data.orderId)"
>
去参团
</button>
</view>
<view v-else class="ss-flex ss-row-center">
<view v-if="state.data.orderId">
<button
class="ss-reset-button join-btn"
:disabled="endTime.ms <= 0"
@tap="onShare"
>
邀请好友来拼团
</button>
</view>
<view v-else>
<button
class="ss-reset-button join-btn"
:disabled="endTime.ms <= 0"
@tap="onJoinGroupon()"
>
立即参团
</button>
</view>
</view>
</view>
<!-- TODO 芋艿这里暂时没接入 -->
<view v-if="state.data.goods">
<s-select-groupon-sku
:show="state.showSelectSku"
:goodsInfo="state.data.goods"
:grouponAction="state.grouponAction"
:grouponNum="state.grouponNum"
@buy="onBuy"
@change="onSkuChange"
@close="state.showSelectSku = false"
/>
</view>
</view>
<s-empty v-if="!state.data && !state.loading" icon="/static/goods-empty.png" />
</s-layout>
</template>
<script setup>
import { computed, reactive } from 'vue';
import sheep from '@/sheep';
import { onLoad } from '@dcloudio/uni-app';
import { useDurationTime } from '@/sheep/hooks/useGoods';
import { showShareModal } from '@/sheep/hooks/useModal';
import { isEmpty } from 'lodash';
import CombinationApi from "@/sheep/api/promotion/combination";
const headerBg = sheep.$url.css('/static/img/shop/user/withdraw_bg.png');
const statusBarHeight = sheep.$platform.device.statusBarHeight * 2;
const state = reactive({
data: {}, //
loading: true,
grouponAction: 'create',
showSelectSku: false,
grouponNum: 0,
number: 0,
activity: {},
combinationHeadId: null, //
});
// todo
const shareInfo = computed(() => {
if (isEmpty(state.data)) return {};
return sheep.$platform.share.getShareInfo(
{
title: state.data.headRecord.spuName,
image: sheep.$url.cdn(state.data.headRecord.picUrl),
desc: state.data.goods?.subtitle,
params: {
page: '5',
query: state.data.id,
},
},
{
type: 'groupon', //
title: state.data.headRecord.spuName, //
image: sheep.$url.cdn(state.data.headRecord.picUrl), //
price: state.data.goods?.price, //
original_price: state.data.goods?.original_price, //
},
);
});
//
function onDetail(orderId) {
sheep.$router.go('/pages/order/detail', {
id: orderId,
});
}
// TODO
function onCreateGroupon() {
state.grouponAction = 'create';
state.grouponId = 0;
state.showSelectSku = true;
}
// TODO
function onSkuChange(e) {
state.selectedSkuPrice = e;
}
// TODO
function onJoinGroupon() {
state.grouponAction = 'join';
state.grouponId = state.data.activityId;
state.combinationHeadId = state.data.id;
state.grouponNum = state.data.num;
state.showSelectSku = true;
}
// TODO
function onBuy(sku) {
sheep.$router.go('/pages/order/confirm', {
data: JSON.stringify({
order_type: 'goods',
combinationActivityId: state.data.activity.id,
combinationHeadId: state.combinationHeadId,
items: [
{
skuId: sku.id,
count: sku.count,
},
],
}),
});
}
const endTime = computed(() => {
return useDurationTime(state.data.headRecord.expireTime);
});
//
async function getGrouponDetail(id) {
const { code, data } = await CombinationApi.getCombinationRecordDetail(id);
if (code === 0) {
state.data = data;
const remainNumber = Number(state.data.headRecord.userSize - state.data.headRecord.userCount);
state.remainNumber = remainNumber > 0 ? remainNumber : 0;
//
const { data: activity } = await CombinationApi.getCombinationActivity(data.headRecord.activityId);
state.activity = activity;
} else {
state.data = null;
}
state.loading = false;
}
function onShare() {
showShareModal();
}
onLoad((options) => {
getGrouponDetail(options.id);
});
</script>
<style lang="scss" scoped>
.recharge-box {
position: relative;
margin-bottom: 120rpx;
background: v-bind(headerBg) center/750rpx 100%
no-repeat,
linear-gradient(115deg, #f44739 0%, #ff6600 100%);
border-radius: 0 0 5% 5%;
height: 100rpx;
.goods-box {
width: 710rpx;
border-radius: 20rpx;
position: absolute;
left: 20rpx;
box-sizing: border-box;
}
.sales-title {
height: 32rpx;
background: rgba(#ffe0e2, 0.29);
border-radius: 16rpx;
font-size: 24rpx;
font-weight: 400;
padding: 6rpx 20rpx;
color: #f7979c;
}
.num-title {
font-size: 24rpx;
font-weight: 400;
color: #999999;
}
}
.countdown-time {
font-size: 26rpx;
font-weight: 500;
color: #383a46;
.countdown-h {
font-size: 24rpx;
font-family: OPPOSANS;
font-weight: 500;
color: #ffffff;
padding: 0 4rpx;
margin-left: 16rpx;
height: 40rpx;
background: linear-gradient(90deg, #ff6000 0%, #fe832a 100%);
border-radius: 6rpx;
}
.countdown-num {
font-size: 24rpx;
font-family: OPPOSANS;
font-weight: 500;
color: #ffffff;
width: 40rpx;
height: 40rpx;
background: linear-gradient(90deg, #ff6000 0%, #fe832a 100%);
border-radius: 6rpx;
}
}
.countdown-box {
// height: 364rpx;
background: #ffffff;
border-radius: 10rpx;
box-sizing: border-box;
.countdown-title {
font-size: 28rpx;
font-weight: 500;
color: #333333;
.cicon-check-round {
color: #42b111;
margin-right: 24rpx;
}
.cicon-info {
color: #d71e08;
margin-right: 24rpx;
}
.num {
color: #ff6000;
}
}
.header-avatar {
width: 86rpx;
height: 86rpx;
background: #ececec;
border-radius: 50%;
border: 4rpx solid #edc36c;
position: relative;
box-sizing: border-box;
.avatar-img {
width: 100%;
height: 100%;
border-radius: 50%;
}
.header-tag {
width: 72rpx;
height: 36rpx;
font-size: 24rpx;
line-height: nor;
background: linear-gradient(132deg, #f3dfb1, #f3dfb1, #ecbe60);
border-radius: 16rpx;
position: absolute;
left: 4rpx;
top: -36rpx;
}
}
.default-avatar {
width: 86rpx;
height: 86rpx;
background: #ececec;
border-radius: 50%;
.avatar-img {
width: 100%;
height: 100%;
border-radius: 50%;
}
}
.user-avatar {
width: 86rpx;
height: 86rpx;
background: #ececec;
border-radius: 50%;
}
}
.order-btn {
width: 668rpx;
height: 70rpx;
border: 2rpx solid #dfdfdf;
border-radius: 35rpx;
color: #999999;
font-weight: 500;
font-size: 26rpx;
line-height: normal;
}
.disabled-btn {
width: 668rpx;
height: 70rpx;
background: #dddddd;
border-radius: 35rpx;
color: #999999;
font-weight: 500;
font-size: 28rpx;
line-height: normal;
}
.join-btn {
width: 668rpx;
height: 70rpx;
background: linear-gradient(90deg, #ff6000 0%, #fe832a 100%);
box-shadow: 0px 8rpx 6rpx 0px rgba(255, 104, 4, 0.22);
border-radius: 35rpx;
color: #fff;
font-weight: 500;
font-size: 28rpx;
line-height: normal;
}
.detail-cell-wrap {
width: 100%;
padding: 10rpx 20rpx;
box-sizing: border-box;
border-top: 2rpx solid #dfdfdf;
background-color: #fff;
// min-height: 60rpx;
.label-text {
font-size: 28rpx;
font-weight: 400;
}
.cell-content {
font-size: 28rpx;
font-weight: 500;
color: $dark-6;
}
.right-forwrad-icon {
font-size: 28rpx;
font-weight: 500;
color: $dark-9;
}
}
</style>

View File

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

View File

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

206
pages/activity/index.vue Normal file
View File

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

View File

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

451
pages/app/sign.vue Normal file
View File

@ -0,0 +1,451 @@
<!-- 签到界面 -->
<template>
<s-layout title="签到有礼">
<s-empty v-if="state.loading" icon="/static/data-empty.png" text="签到活动还未开始" />
<view v-if="state.loading" />
<view class="sign-wrap" v-else-if="!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.signInfo.continuousDay }}</text>
</view>
</view>
<view
class="list acea-row row-between-wrapper"
style="
padding: 0 30rpx;
height: 240rpx;
display: flex;
justify-content: space-between;
align-items: center;
"
>
<view class="item" v-for="(item, index) in state.signConfigList" :key="index">
<view
:class="
(index === state.signConfigList.length ? 'reward' : '') +
' ' +
(state.signInfo.continuousDay >= item.day ? 'rewardTxt' : '')
"
>
{{ item.day }}
</view>
<view
class="venus"
:class="
(index + 1 === state.signConfigList.length ? 'reward' : '') +
' ' +
(state.signInfo.continuousDay >= item.day ? 'venusSelect' : '')
"
>
</view>
<view class="num" :class="state.signInfo.continuousDay >= item.day ? 'on' : ''">
+ {{ item.point }}
</view>
</view>
</view>
<!-- 签到按钮 -->
<view class="myDateTable">
<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.signInfo.todaySignIn"
@tap="onSign"
>
签到
</button>
<button class="ss-reset-button already-btn" v-else disabled> 已签到 </button>
</view>
</view>
</view>
<!-- 签到说明 TODO @科举这里改成已累计签到 -->
<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.signInfo.totalDay}}</view>
<view class="activity-des">
2据说连续签到第 {{ state.maxDay }} 天可获得超额积分一定要坚持签到哦~~~
</view>
</view>
</view>
<!-- 签到结果弹窗 -->
<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">
<text v-if="state.signResult.point">{{ state.signResult.point }} 积分 </text>
<text v-if="state.signResult.experience"> {{ state.signResult.experience }} 经验</text>
</view>
<view class="model-title ss-flex ss-col-center ss-m-t-22 ss-m-b-30">
已连续打卡 {{ state.signResult.day }}
</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>
</s-layout>
</template>
<script setup>
import sheep from '@/sheep';
import { onReady } from '@dcloudio/uni-app';
import { reactive } from 'vue';
import SignInApi from '@/sheep/api/member/signin';
const headerBg = sheep.$url.css('/static/img/shop/app/sign.png');
const state = reactive({
loading: true,
signInfo: {}, //
signConfigList: [], //
maxDay: 0, //
showModel: false, //
signResult: {}, //
});
//
async function onSign() {
const { code, data } = await SignInApi.createSignInRecord();
if (code !== 0) {
return;
}
state.showModel = true;
state.signResult = data;
//
await getSignInfo();
}
//
function onConfirm() {
state.showModel = false;
}
//
async function getSignInfo() {
const { code, data } = await SignInApi.getSignInRecordSummary();
if (code !== 0) {
return;
}
state.signInfo = data;
state.loading = false;
}
//
async function getSignConfigList() {
const { code, data } = await SignInApi.getSignInConfigList();
if (code !== 0) {
return;
}
state.signConfigList = data;
if (data.length > 0) {
state.maxDay = data[data.length - 1].day;
}
}
onReady(() => {
getSignInfo();
getSignConfigList();
});
// TODO 1css 2
</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;
}
.reward {
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEsAAAA4CAYAAAC1+AWFAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA3ZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTQyIDc5LjE2MDkyNCwgMjAxNy8wNy8xMy0wMTowNjozOSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDowYWFmYjU3Mi03MGJhLTRiNDctOTI2Yi0zOThlZDkzZDkxMDkiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6RUEyNzZEMjZEMDFCMTFFOEIzQzhEMjMxNjI1NENDQjciIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6RUEyNzZEMjVEMDFCMTFFOEIzQzhEMjMxNjI1NENDQjciIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTggKFdpbmRvd3MpIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6ZmRjNTM0MmUtNmFkOC1iMDRhLThjZTEtMjk2YWYzM2FkMmUxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjBhYWZiNTcyLTcwYmEtNGI0Ny05MjZiLTM5OGVkOTNkOTEwOSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PuGQ9m8AAAf3SURBVHja7Jxbb9xEFMe9Xu81u6VJL0kqSCToZ0CCNyp4CQ0FqVAkQAgeEE98EPgEwBOCSoCEQPTCG30BVSpfAaQ2KQ1tc4Fm77v2cv7jmc3UGdvjsTdrJEY68m527Zn5zZlzszeF8XhspWl37/xhZdwK/Di2jqA9ufqM9ndtKz+tTvIpSYdkl7+u52h8lpMjUFdIzvH3VZIPSM6SrHOAM292DkHJ7Rz/rP4/rGhQuQNm5xxUroAVNm//PhUnQ7JEco9LGlBy+znGhsX1O1vNajYbjwm1F0l+JfmN5BeSlwKfx4KybTtWw6TriXao39xpFga8v98q08tPSN7Dn0yvVSqVrEqlbBUKBQvxX78/sIbDoenlxhzs6ySDPNmsj0k+SguqWq0wUGwV6Yj35XIpTXD7Csk3eTPw76c5GUAARtUqlVTA0C5wOzd7WLQF19JoFEAASFRLCQwa9hXJ2rQj+GdJ3sCcQj4vkrw7TVAyMLTBwMiGHSP5luQLEjfkO0O+ZW+ZGPjnuIE8YQoDEyyVnIkdmmaDQxgOR+QU+mku85DkZe5RE23D19KCguYcBSjhEHxNLStBarZTJBdNtuGcqlNVx6q4CBo1iwavipDjsUk6Rdq+I0u1brZdkKpCrDVMYLlBUMVikQ0mGM4EBydWeiYpiaLfWq1GwEbKUAwQPc+Vz3NTlWgEqHq9xo7BpoKVpwbNDwtNbHto9XpdAjaOXWBtWNhWAOV53lQnBiPdarWs+fnjmRr/MLtVLjvUp2O57tCK2wwJ4qxCyECyhXX//n3r3tZWxp4yel4+pPiJRMHydAbR7XYzm5Trutbu3p41pFjq0aNHmV0XY0ywqJ4JrIoOKEwwq7a9s2M5RYe5/+3tnUwXIQGwSlJYyKfeMgVlkprAFu7t/c3OLZfLLLhst9vGmUEKYG/x+WvBQgT7ZTDfE8ZPB5RuCiO33d095o0cx2HhCeThw+1UAbEusIAXbPL5r8XBWuf5UVP2JNgWGLzrelMBhT72yFaVCFSRBK6+SNux1+sxmSYwaDQ0GfOTPGaTc1gPyw2R3nwdTJoRMiCow4U6HXNQKOJ1uz0KAvssGR4MBuw4Gg2ZHyqX/O0HzRKTwHcQw/mhCwLisj8x9EVH9FetViOhYTurkm+EQZgXlAqahf4wvtFoJGsaAshLJD/IcdYaJ1mSV9uvM/mD6fX6oaAANE6jcO6dOxtsUEVKP+yCzVINaJJDAwckTEAu/mG1cRyNXBZlQ8vanY7lkYa77shaWVmJhYVxiSQ7OB6ARLCKz6HNtVqVaR364+Mocy6vkvwEzUKR/6blF/snoDB4ROyixLu/3wodUKMxp5XeQLs2NzdZtGwXAazABolzZZHH4SGgJMCAjF0CaJClpSWr2Wxqb/NWS+0s5Dq+r2Eu30GePJZNkuehWSsyKFmrsPKYWFwqoZsHQlNWV1etra2/2ORlUGF5XhGLxaFiXK5XsM6cXmZbKEm+iPOD2UewXz+tI013SqRdfTmifwqMYOA3SO7GlTUOJ9AHBjLJwyXYamfOLDO7k+Q88c3lpcVEoCYaqkjThH3USI3AZ8PmLz7klcIJcRhFWRWx91VJtDCiSRPbpcVFZqy9iLxNngBGsUigTLytanyYS/Bavn0cMacjadWQ87knQodr3OoPxEm+i+2xFfG3il/qUAEzqVDimsePP0HG2o2EJbRibq7OPKYJqKBxlz2hvIAHc55UIIQ3vBaMs77nVcK2TBkns5WNAQZNTAoM13Y1qhjCrZuACoYNKlAHc+1OlINzuMi5KIPSK5xkS1wEHgzeQUfDkgLr9roJwHanBgoaFfCALc7hSly6A5V7h2RfqCc6hSofxEDZAOt1+4e2GzTN46/F9hQrr5u0q0AdhEKHzQGCXym22ufzv6abSCNivRzMC4O5ol+udZTA9Ap9AxZrjflWw7VOnzrJPF61WmNwvIkJsLW1K9g/dod4LCDchk5eXhYRe5JKaax6IA7DILDqJvZEBJ0O+br5hXlmxEU7eXKBJt2gnPEf2iIdNhlkEY1Gw6D6Opgshka40jcpK8dWUYXhN2mwEdCWEwsL1rFjTeV1EFosLp5ixv3BgwcMmsndueXlZV9L9UridqoafFwIYNKQ0509+3TU40WThpxtdXWFwzKrwWdxtykBrPFkb2dRdweApK1erxvDCltoP2IPv8eQGJbwiOK+4YxuCabS/jDNQgkINldDwSNhFVWBIaDJ/YatWlaqn4UWAYbqoTh8FZ8Fxlo0gdUOrg6LgxSxjsruIC5L+VyVUVNBYZmCMkYrMI0KLGrLxOMhzN9RlTqCEhUYjsfjI9MoPyAeKNMl1bgVzzmg6P+diWbd5BXUS3IFVbFV8XzWXBiwqGg+ac0+rESs6bGxU1I9nxVn4G9FnSylR1e13ElIpK0DzBQUb0hh3iS5nkZ7s3i0+zqHZdR0csmUoJBevJ0WVCaweLuYZjBRwFKC8jioH7OYZCawSLsGJLg5i/tswWwX79fp8wKE27cbKmDI/YRDwBHvFaBw7px0vUJEvxesHD7aLaBd5QPc4H+6jff87+I7yFnOq4DB7eMuTLvdYbfuFWEAzjnPryE3Zb9pzIPSUUzxtzvLJH9a4b+hqfPJvKB5zRsccidlv+awZvyzX11gOqDMVvU/9LPf0C15FKBmarOmACw3oPICSwb2OQlu4+Cxv8/yBCoPNitsAcdWDv9Vwb8CDACdCFE+P8dk8gAAAABJRU5ErkJggg==');
width: 75rpx;
height: 56rpx;
}
.rewardTxt {
width: 74rpx;
height: 32rpx;
background-color: #f4b409;
border-radius: 16rpx;
font-size: 20rpx;
color: #a57d3f;
line-height: 32rpx;
}
.venusSelect {
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADgAAAA4CAYAAACohjseAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA3ZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTQyIDc5LjE2MDkyNCwgMjAxNy8wNy8xMy0wMTowNjozOSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDowYWFmYjU3Mi03MGJhLTRiNDctOTI2Yi0zOThlZDkzZDkxMDkiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6QzkwRkI4NEFEMDFCMTFFODhDNDdBMDVGOTBBN0U2NTQiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6QzkwRkI4NDlEMDFCMTFFODhDNDdBMDVGOTBBN0U2NTQiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTggKFdpbmRvd3MpIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6ZmRjNTM0MmUtNmFkOC1iMDRhLThjZTEtMjk2YWYzM2FkMmUxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjBhYWZiNTcyLTcwYmEtNGI0Ny05MjZiLTM5OGVkOTNkOTEwOSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PkX00M0AAAfVSURBVHjaxFpdcBNVFD67STbppn9poU0LtSBYykBlgKrjqKOtihbq8ICKILyJwxM+6fjCk2/6pC+O4hs+6fgAbWFw1OID+ICMAwwKlRmKLW3Tpvlrskk3m13Pvdm0NNn//HA73+ymd3Pv+e4595xzT5bJ3DgJNWyMelUqNaD36a8N+9kaEeMR3yAERES952sxsbtya2lIbgQxqH72IT5EbEW8pZKuHkGlugyLyT3aBtW+qpJkKb/qgEeMIAYNnhlUn+Edz2NuokqtNVdTTbqVyhO0Q67qJMt2MnXbTq/cp+9+ZkyOYYFxN4EiLaF5SbokccyKkWRS1z4we4ZDfIE4hmhxvJLNu8HTtg85ekGRRcjOXwIp9pfT4aKIs4iP+f4zYrl78HPEqbLMJNAPXHBI/SQjSTd+PoD3LpCi15wMGVBlSiM+NfSihJ8Jjlt4Rheu5n7wtL+B93IJPO37aH8Z45+ohBdtKUtz7a+jJLK+/dN+BTX5p5MpWh6HF6XNE9iLwr9mSG6VZP65bPR6NVI1BwQZF3BtA+Bq2oG3Pt3HFCVnfUHaX6XQHCeXgVz8Nojz4+SDzTgo2yfIBV9G89utzi5DtRvDcnQ+JSciyd+rH+hdjb22tFOp5mreCUrocg0CPet5LATJvHbldWSiGllIzZpdeR2ZqFPtZTNJSIQeQGv3DucEbcrLmkRSXvP/cs4RZm6Ow/T1McffpyiSJTZ+jDfOZFDlOuARo5p9aKJ2IYkpCN+9AmLsIcSm/3Y0BkWpPCPRX9/nDVI1BTTAI0YRA9r99gWbv3MF90MGvG4WQrfHnRMslWcQMRL55SivbaJk064FjxhFDGj0gad5u22zkrMZiExcBQ7JcW4GlhfuQ3L+gSMTzc9fItcgYiTy8xHeTIOGmnMHtoEn2G971cP3MAUTk+B2MeBBkh4kGbp92ZEGyfxEDj1NLl56j9fbg4U9N6C179zNPZhK7cV7yRZkSYTI3atIjgUXywCLINf03L8gRGZtj0dA5CDyaMg5SPZk+OLhFZLMwtg7hTLDKGJAM09s6QGuY6+upxLTKcgkFmE5FUdFIYQEiHgv4VURl4BjgZon0SA9EeKKi1kZliU8Nnn84OabwIPw+huB8zdR+OqbwdcYMAwB4ux1yEYmtLp+I5WBdft/EApx8Cs9cu6mblyxXXTl9FpOFGDyjzGQMwlwIQlUEDAMagqvhBTVHl4ZplDezpsq+UdOTkMuIcByfBYElCWHmsgpLHQ/NwTe+gaTBH0XWkgGpPgDrfLHl4gTTOj8IVLdTqkF2aIslwF+2zCeGDjzIJ5OwtSVc6Cko9QMCUmWyZMiZJn8cI8E7Lwm11wJOTypBPuHoCG4yVrgz2VBmFBj69pGTvv1hVSN0XSxHE8LRUbaW9G01wdPvHgQQtcugJwMU5LFpIrWDjXMUGIsMNRR5DwcBPcMQV1L0NKchYHIkU2WkiVrTvgT6XEjyN/TY08R5KyAaWc6n3tagMvjhuCzQ+jlOmylVPRZjw/an9kPdYF1lucjIPLJmERoyP9j+8GflIIXPYW4XOKVJAmWZ27Y8nAMaq5t9yB4WjeumJ4hOaIErx/W978JXH2TbY9K5ZNKMhzC5dSjyTapQ5IyFxozvLJGz9Hp/Am+Y7utH8kan+yDhYVp8lVgdDWX33f+ji3gruNpnLPTxNl/8vKtbeTAeKDj0DmhuGShT3Jxkp4guGCv5cnT0QX0hgq4dOnBSshIxxehUbZJbu4OSJEpbXJvnxf0zoP6JMP/keVGTfZYEiCDQlttmXjE1hlTnJ3A+Ketuc53RwSz86AuSXFxitYzueBWUyGWE9E1pqiozoRhXdQJMLAaEyUMMdJyGp2Ux4Lm7iG5h5rkNhweFUpLFtonZANNTmOA3WzmFiGLGY3XlSel0DpOCzRs6gWXj4fkgwnIhKZIcKDhgsTLTCICfMs683gb1tbchiNjgt0TfYFkym7JQkyl8nkoixkMesiGzT1Q19q20t+0dTvwnU/A0iQSDYeoNinBQLOTE/2BjUcvCE5rMoJ2XcSYYBr3FOPmoHHLU+APdtJMpvg7bp8XAr19mKJ1weK9u5CJxUDpMt+HxfJ2HbsoVLzwa1aT4fw8dD3/Au43lv7YYjQF19gAHXv6UYMJa7UepQZFJzMT9fp9lJidorCvgbfkSRXbBB2UDRX5MdREnZYNwZECRcxQXLUnl8s5KPw6MNFsdBFzzdaaE8xGwrUx0eXZORrw3c1NNdEk0ZwUi2OQn7fvZBz9fEZKDjNzFLqn7dYApnVtNtKvecx5oxVfHCsmSt4ts/0rrxiO5NO6jvUWyC0guZgT+SPllu4Jzjr9AT0bjqKWQ1qH0RWQfvKcwzm+s6BB01X6RC1pHIf82w02NRmnsng7WjX28iJqLu5Ec4XXSE5XYg+S91A+UlHS/H2riXfq1n3N8mM2HKOOgutsodkNqZKIMxGQokvFw40jhnFMoZZ70CzyrpLd2S0kb00Oa5KMJDC8LAHL4QEmK4HGKYaSq+/bJFTyZ/GyX+VK3pzUStA1SRJrkTNZrWHG1e8IGuMZt5eqrUH9U8iwUbVci1w1BKnW65RWSVaVnBomAKoI3E9IQEEipX3jap9Q1hxmBElBocp/AmIYcQaRQSQQ36r/E8odvepOxoa5khfRT1pf+8q0/wUYAFU/P0XyeZQPAAAAAElFTkSuQmCC');
}
.venus {
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADgAAAA4CAYAAACohjseAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA3ZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTQyIDc5LjE2MDkyNCwgMjAxNy8wNy8xMy0wMTowNjozOSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDowYWFmYjU3Mi03MGJhLTRiNDctOTI2Yi0zOThlZDkzZDkxMDkiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6RDQ3N0E3RkJEMDFCMTFFODhGODVBOTc1NEM5Nzg2NzkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6RDQ3N0E3RkFEMDFCMTFFODhGODVBOTc1NEM5Nzg2NzkiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTggKFdpbmRvd3MpIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6ZmRjNTM0MmUtNmFkOC1iMDRhLThjZTEtMjk2YWYzM2FkMmUxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjBhYWZiNTcyLTcwYmEtNGI0Ny05MjZiLTM5OGVkOTNkOTEwOSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PnZClGIAAAT7SURBVHja7JtLcxtFEMdnn1rJUhIrTiS7KLsK/EXwNck3gCMcuUFx4QvACW48buHGMZxJPkiowpjC2LElcKzXSqul/6OdoOB9zezsCCimqmvXtdqd+U33dPf0rq2Tn54zg81KjrGuB75x8FbuddsQWIvkS5IxySA5b5no2DUE94TkKPk7IHmf5JDkUQJdW7MNw623o+Ra698KmAdnDNLeIJwRSHvDcLVDVnYynU771fnLl9eFcLZts+VymQf5iJ6pzfFYJeKgT/IZyTskXdWOPM9jjYbPLMticRyz2Sxk8/lc9XFDksckH1IcDKtq8FOSD6rMIuCCoPHXrBIk/qYDC0MlyO1kTBOSj6uuwXerwPn+63DrrdFo8OsV2ns6nEy3Chwg8lpFyK4OwNrgNGpSvxfFoDzP5etJR8PzsiYETmk+X5BjmpkBrHPGU109TeKqv5X3rT3QQ3ObaPDGRjIZXWZpol+b/cebUUA4iuHw938UoNbk9+zsjP16eqoV4JfjH1uqgLjxe10DiaKIDYZDNqfU7OrqSifjkzxIuwDubV2juLi8ZK7j8oT74uJSJ+BRHqQtC6cS/7A9wtrDvb7v84A9Go2UMyQZSFsWrmz6td4GgyF38a7r8lgGefHiQjnjkYG064ZDmjWktecRnEOCDa9DpjqdTrnUDSk2vJXgsHGdTKa0t5vx/V0Yhvy4WMx5hdf3VqYJDQqTxW+QdmECPM8h8flvPPRFR/QXBEEuKEw9Yz/5AyoDtBkei5zriyw4pGVFmoOHPD7+mQ/ccR1mWzZpyuIacx2Hgzl0FJkIjjBTHBeLiO6LuDZH4zFbRkt63oLt7+8XAmJcIhFP0eTn2C9CgzBTrPjUp7XbW6VSJGjx5OSEBhsz2wGkxc0R967LuukuSWKaFEwMnXJQSL/fZ51Op/QSuL5OdVjY7beFBq2sAlHZ/A8aOTg4YKenv/EBr8Nl5ZUO6jPJRGCg0dJie/d3WbPZlMpPMwpZsN0Y2sOVb7PcOzou22CGe3u7fB3J3Cd+udvvScG9soT0Kt13tAZj4UVRwHmWtZClcj+azX6vxx0GN8ECUFyHjnsEp+KlM8b3TBTKBCDqkA/SIFV20jCbO3duk8OIcgHF7G9ttbinVYFLcTBgeAAP+vc4mAkJVywLibARpZvOjWWA36rApYSI1+DSMhltkJPpRGIyJrXAZeWiWiCnk9kNU4RGl8m5MN1VLFzwWKobLm83kQtZbnMb8lgYJ2aIYH//3g73lEHQ5ECAXYUSu7QWZeCKqmoCcqSyPkQgd8lHbne3uSMRbWenSwNtU476BxtT9oJQOZ3OKKloq6SmmXBlyoZKb3nG4wnXyt1ul9261UkN9ggjvd497mDOz885KGN3pfvKg1OuixY15JCHh2/ymFjUms2AMqD9BFB/qwUQg5ZtrVY9b7H/LxtmZSCbaCr9KgGmpEeG6qpzM2tQBHudb5eKNLfKiUMzgAIyL6uRreHklB9qX4MDlQfLpHUV4AY6AB+rzl4ZyIqa+0aHiX6UlDTwMcK2CqQoEGmEE5+RfFK4N636vWjKh0Cp5ceS38k8JXko8yHQ7W7PXKBPBvYwGegNF4/q12g05mV7HXClqgs1ffEr+/LmaTIx0nCb+uI3U5M64Tadi5aBrBXORLKdB1k7nKndhID8GqUaErze/coEXJ1OJm9CY2bw3wr+FGAAoa6PIUihovYAAAAASUVORK5CYII=');
background-repeat: no-repeat;
background-size: 100% 100%;
width: 56rpx;
height: 56rpx;
margin: 10rpx 0;
}
.num {
font-size: 36rpx;
font-family: 'Guildford Pro';
}
.item {
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #eee;
height: 130rpx;
}
.reward {
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEsAAAA4CAYAAAC1+AWFAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA3ZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTQyIDc5LjE2MDkyNCwgMjAxNy8wNy8xMy0wMTowNjozOSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDowYWFmYjU3Mi03MGJhLTRiNDctOTI2Yi0zOThlZDkzZDkxMDkiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6RUEyNzZEMjZEMDFCMTFFOEIzQzhEMjMxNjI1NENDQjciIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6RUEyNzZEMjVEMDFCMTFFOEIzQzhEMjMxNjI1NENDQjciIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTggKFdpbmRvd3MpIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6ZmRjNTM0MmUtNmFkOC1iMDRhLThjZTEtMjk2YWYzM2FkMmUxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjBhYWZiNTcyLTcwYmEtNGI0Ny05MjZiLTM5OGVkOTNkOTEwOSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PuGQ9m8AAAf3SURBVHja7Jxbb9xEFMe9Xu81u6VJL0kqSCToZ0CCNyp4CQ0FqVAkQAgeEE98EPgEwBOCSoCEQPTCG30BVSpfAaQ2KQ1tc4Fm77v2cv7jmc3UGdvjsTdrJEY68m527Zn5zZlzszeF8XhspWl37/xhZdwK/Di2jqA9ufqM9ndtKz+tTvIpSYdkl7+u52h8lpMjUFdIzvH3VZIPSM6SrHOAM292DkHJ7Rz/rP4/rGhQuQNm5xxUroAVNm//PhUnQ7JEco9LGlBy+znGhsX1O1vNajYbjwm1F0l+JfmN5BeSlwKfx4KybTtWw6TriXao39xpFga8v98q08tPSN7Dn0yvVSqVrEqlbBUKBQvxX78/sIbDoenlxhzs6ySDPNmsj0k+SguqWq0wUGwV6Yj35XIpTXD7Csk3eTPw76c5GUAARtUqlVTA0C5wOzd7WLQF19JoFEAASFRLCQwa9hXJ2rQj+GdJ3sCcQj4vkrw7TVAyMLTBwMiGHSP5luQLEjfkO0O+ZW+ZGPjnuIE8YQoDEyyVnIkdmmaDQxgOR+QU+mku85DkZe5RE23D19KCguYcBSjhEHxNLStBarZTJBdNtuGcqlNVx6q4CBo1iwavipDjsUk6Rdq+I0u1brZdkKpCrDVMYLlBUMVikQ0mGM4EBydWeiYpiaLfWq1GwEbKUAwQPc+Vz3NTlWgEqHq9xo7BpoKVpwbNDwtNbHto9XpdAjaOXWBtWNhWAOV53lQnBiPdarWs+fnjmRr/MLtVLjvUp2O57tCK2wwJ4qxCyECyhXX//n3r3tZWxp4yel4+pPiJRMHydAbR7XYzm5Trutbu3p41pFjq0aNHmV0XY0ywqJ4JrIoOKEwwq7a9s2M5RYe5/+3tnUwXIQGwSlJYyKfeMgVlkprAFu7t/c3OLZfLLLhst9vGmUEKYG/x+WvBQgT7ZTDfE8ZPB5RuCiO33d095o0cx2HhCeThw+1UAbEusIAXbPL5r8XBWuf5UVP2JNgWGLzrelMBhT72yFaVCFSRBK6+SNux1+sxmSYwaDQ0GfOTPGaTc1gPyw2R3nwdTJoRMiCow4U6HXNQKOJ1uz0KAvssGR4MBuw4Gg2ZHyqX/O0HzRKTwHcQw/mhCwLisj8x9EVH9FetViOhYTurkm+EQZgXlAqahf4wvtFoJGsaAshLJD/IcdYaJ1mSV9uvM/mD6fX6oaAANE6jcO6dOxtsUEVKP+yCzVINaJJDAwckTEAu/mG1cRyNXBZlQ8vanY7lkYa77shaWVmJhYVxiSQ7OB6ARLCKz6HNtVqVaR364+Mocy6vkvwEzUKR/6blF/snoDB4ROyixLu/3wodUKMxp5XeQLs2NzdZtGwXAazABolzZZHH4SGgJMCAjF0CaJClpSWr2Wxqb/NWS+0s5Dq+r2Eu30GePJZNkuehWSsyKFmrsPKYWFwqoZsHQlNWV1etra2/2ORlUGF5XhGLxaFiXK5XsM6cXmZbKEm+iPOD2UewXz+tI013SqRdfTmifwqMYOA3SO7GlTUOJ9AHBjLJwyXYamfOLDO7k+Q88c3lpcVEoCYaqkjThH3USI3AZ8PmLz7klcIJcRhFWRWx91VJtDCiSRPbpcVFZqy9iLxNngBGsUigTLytanyYS/Bavn0cMacjadWQ87knQodr3OoPxEm+i+2xFfG3il/qUAEzqVDimsePP0HG2o2EJbRibq7OPKYJqKBxlz2hvIAHc55UIIQ3vBaMs77nVcK2TBkns5WNAQZNTAoM13Y1qhjCrZuACoYNKlAHc+1OlINzuMi5KIPSK5xkS1wEHgzeQUfDkgLr9roJwHanBgoaFfCALc7hSly6A5V7h2RfqCc6hSofxEDZAOt1+4e2GzTN46/F9hQrr5u0q0AdhEKHzQGCXym22ufzv6abSCNivRzMC4O5ol+udZTA9Ap9AxZrjflWw7VOnzrJPF61WmNwvIkJsLW1K9g/dod4LCDchk5eXhYRe5JKaax6IA7DILDqJvZEBJ0O+br5hXlmxEU7eXKBJt2gnPEf2iIdNhlkEY1Gw6D6Opgshka40jcpK8dWUYXhN2mwEdCWEwsL1rFjTeV1EFosLp5ixv3BgwcMmsndueXlZV9L9UridqoafFwIYNKQ0509+3TU40WThpxtdXWFwzKrwWdxtykBrPFkb2dRdweApK1erxvDCltoP2IPv8eQGJbwiOK+4YxuCabS/jDNQgkINldDwSNhFVWBIaDJ/YatWlaqn4UWAYbqoTh8FZ8Fxlo0gdUOrg6LgxSxjsruIC5L+VyVUVNBYZmCMkYrMI0KLGrLxOMhzN9RlTqCEhUYjsfjI9MoPyAeKNMl1bgVzzmg6P+diWbd5BXUS3IFVbFV8XzWXBiwqGg+ac0+rESs6bGxU1I9nxVn4G9FnSylR1e13ElIpK0DzBQUb0hh3iS5nkZ7s3i0+zqHZdR0csmUoJBevJ0WVCaweLuYZjBRwFKC8jioH7OYZCawSLsGJLg5i/tswWwX79fp8wKE27cbKmDI/YRDwBHvFaBw7px0vUJEvxesHD7aLaBd5QPc4H+6jff87+I7yFnOq4DB7eMuTLvdYbfuFWEAzjnPryE3Zb9pzIPSUUzxtzvLJH9a4b+hqfPJvKB5zRsccidlv+awZvyzX11gOqDMVvU/9LPf0C15FKBmarOmACw3oPICSwb2OQlu4+Cxv8/yBCoPNitsAcdWDv9Vwb8CDACdCFE+P8dk8gAAAABJRU5ErkJggg==');
width: 75rpx;
height: 56rpx;
}
.on {
background-color: #999 !important;
}
</style>

View File

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

View File

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

View File

@ -0,0 +1,94 @@
<template>
<!-- 聊天虚拟列表 -->
<z-paging ref="pagingRef" v-model="messageList" use-chat-record-mode use-virtual-list
cell-height-mode="dynamic" default-page-size="20" :auto-clean-list-when-reload="false"
safe-area-inset-bottom bottom-bg-color="#f8f8f8" :back-to-top-style="backToTopStyle"
:auto-show-back-to-top="showNewMessageTip" @backToTopClick="onBackToTopClick"
@scrolltoupper="onScrollToUpper" @query="queryList">
<template #top>
<!-- 撑一下顶部导航 -->
<view style="height: 45px"></view>
</template>
<!-- style="transform: scaleY(-1)"必须写否则会导致列表倒置 -->
<!-- 注意不要直接在chat-item组件标签上设置style因为在微信小程序中是无效的请包一层view -->
<template #cell="{item,index}">
<view style="transform: scaleY(-1)">
<!-- 消息渲染 -->
<MessageListItem :message="item" :message-index="index" :message-list="messageList"></MessageListItem>
</view>
</template>
<!-- 底部聊天输入框 -->
<template #bottom>
<slot name="bottom"></slot>
</template>
<!-- 查看最新消息 -->
<template #backToTop>
<text>有新消息</text>
</template>
</z-paging>
</template>
<script setup>
import MessageListItem from '@/pages/chat/components/messageListItem.vue';
import { reactive, ref } from 'vue';
import KeFuApi from '@/sheep/api/promotion/kefu';
import { isEmpty } from '@/sheep/helper/utils';
const messageList = ref([]); //
const showNewMessageTip = ref(false); //
const backToTopStyle = reactive({
'width': '100px',
'background-color': '#fff',
'border-radius': '30px',
'box-shadow': '0 2px 4px rgba(0, 0, 0, 0.1)',
'display': 'flex',
'justifyContent': 'center',
'alignItems': 'center',
}); //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
});
const pagingRef = ref(null); //
const queryList = async (pageNo, pageSize) => {
//
// pageNopageSize
queryParams.pageNo = pageNo;
queryParams.pageSize = pageSize;
await getMessageList();
};
//
const getMessageList = async () => {
const { data } = await KeFuApi.getKefuMessagePage(queryParams);
if (isEmpty(data.list)) {
return;
}
pagingRef.value.completeByTotal(data.list, data.total);
};
/** 刷新消息列表 */
const refreshMessageList = (message = undefined) => {
if (queryParams.pageNo != 1 && message !== undefined) {
showNewMessageTip.value = true;
//
pagingRef.value.addChatRecordData([message], false);
return;
}
pagingRef.value.reload();
};
/** 滚动到最新消息 */
const onBackToTopClick = (event) => {
event(false); //
pagingRef.value.scrollToBottom();
};
/** 监听滚动到底部事件(因为 scroll 翻转了顶就是底) */
const onScrollToUpper = () => {
//
if (queryParams.pageNo === 1) {
return;
}
showNewMessageTip.value = false;
//
refreshMessageList();
};
defineExpose({ getMessageList, refreshMessageList });
</script>

View File

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

View File

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

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

View File

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

187
pages/chat/index.vue Normal file
View File

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

View File

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

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

150
pages/commission/goods.vue Normal file
View File

@ -0,0 +1,150 @@
<!-- 分销商品列表 -->
<template>
<s-layout title="推广商品" :onShareAppMessage="state.shareInfo">
<view class="goods-item ss-m-20" v-for="item in state.pagination.list" :key="item.id">
<s-goods-item
size="lg"
:img="item.picUrl"
:title="item.name"
:subTitle="item.introduction"
:price="item.price"
:originPrice="item.marketPrice"
priceColor="#333"
@tap="sheep.$router.go('/pages/goods/index', { id: item.id })"
>
<template #rightBottom>
<view class="ss-flex ss-row-between">
<view class="commission-num" v-if="item.brokerageMinPrice === undefined">预计佣金计算中</view>
<view class="commission-num" v-else-if="item.brokerageMinPrice === item.brokerageMaxPrice">
预计佣金{{ fen2yuan(item.brokerageMinPrice) }}
</view>
<view class="commission-num" v-else>
预计佣金{{ fen2yuan(item.brokerageMinPrice) }} ~ {{ fen2yuan(item.brokerageMaxPrice) }}
</view>
<button
class="ss-reset-button share-btn ui-BG-Main-Gradient"
@tap.stop="onShareGoods(item)"
>
分享赚
</button>
</view>
</template>
</s-goods-item>
</view>
<s-empty
v-if="state.pagination.total === 0"
icon="/static/goods-empty.png"
text="暂无推广商品"
/>
<!-- 加载更多 -->
<uni-load-more
v-if="state.pagination.total > 0"
:status="state.loadStatus"
:content-text="{
contentdown: '上拉加载更多',
}"
@tap="loadMore"
/>
</s-layout>
</template>
<script setup>
import sheep from '@/sheep';
import $share from '@/sheep/platform/share';
import { onLoad, onReachBottom } from '@dcloudio/uni-app';
import { reactive } from 'vue';
import _ from 'lodash';
import { showShareModal } from '@/sheep/hooks/useModal';
import SpuApi from '@/sheep/api/product/spu';
import BrokerageApi from '@/sheep/api/trade/brokerage';
import { fen2yuan } from '../../sheep/hooks/useGoods';
const state = reactive({
pagination: {
list: [],
total: 0,
pageNo: 1,
pageSize: 1,
},
loadStatus: '',
shareInfo: {},
});
// TODO
function onShareGoods(goodsInfo) {
state.shareInfo = $share.getShareInfo(
{
title: goodsInfo.title,
image: sheep.$url.cdn(goodsInfo.image),
desc: goodsInfo.subtitle,
params: {
page: '2',
query: goodsInfo.id,
},
},
{
type: 'goods', //
title: goodsInfo.title, //
image: sheep.$url.cdn(goodsInfo.image), //
price: goodsInfo.price[0], //
original_price: goodsInfo.original_price, //
},
);
showShareModal();
}
async function getGoodsList() {
state.loadStatus = 'loading';
let { code, data } = await SpuApi.getSpuPage({
pageSize: state.pagination.pageSize,
pageNo: state.pagination.pageNo,
});
if (code !== 0) {
return;
}
state.pagination.list = _.concat(state.pagination.list, data.list);
state.pagination.total = data.total;
state.loadStatus = state.pagination.list.length < state.pagination.total ? 'more' : 'noMore';
//
data.list.forEach((item) => {
BrokerageApi.getProductBrokeragePrice(item.id).then((res) => {
item.brokerageMinPrice = res.data.brokerageMinPrice;
item.brokerageMaxPrice = res.data.brokerageMaxPrice;
});
});
}
onLoad(() => {
getGoodsList();
});
//
function loadMore() {
if (state.loadStatus === 'noMore') {
return;
}
state.pagination.pageNo++;
getGoodsList();
}
//
onReachBottom(() => {
loadMore();
});
</script>
<style lang="scss" scoped>
.goods-item {
.commission-num {
font-size: 24rpx;
font-weight: 500;
color: $red;
}
.share-btn {
width: 120rpx;
height: 50rpx;
border-radius: 25rpx;
}
}
</style>

View File

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

331
pages/commission/order.vue Normal file
View File

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

File diff suppressed because one or more lines are too long

581
pages/commission/team.vue Normal file
View File

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

470
pages/commission/wallet.vue Normal file
View File

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

View File

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

378
pages/coupon/detail.vue Normal file
View File

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

218
pages/coupon/list.vue Normal file
View File

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

145
pages/goods/comment/add.vue Normal file
View File

@ -0,0 +1,145 @@
<!-- 评价 -->
<template>
<s-layout title="评价">
<view>
<view v-for="(item, index) in state.orderInfo.items" :key="item.id">
<view>
<view class="commont-from-wrap">
<!-- 评价商品 -->
<s-goods-item
:img="item.picUrl"
:title="item.spuName"
:skuText="item.properties.map((property) => property.valueName).join(' ')"
:price="item.payPrice"
:num="item.count"
/>
</view>
<view class="form-item">
<!-- 评分 -->
<view class="star-box ss-flex ss-col-center">
<view class="star-title ss-m-r-40">商品质量</view>
<uni-rate v-model="state.commentList[index].descriptionScores" />
</view>
<view class="star-box ss-flex ss-col-center">
<view class="star-title ss-m-r-40">服务态度</view>
<uni-rate v-model="state.commentList[index].benefitScores" />
</view>
<!-- 评价 -->
<view class="area-box">
<uni-easyinput :inputBorder="false" type="textarea" maxlength="120" autoHeight
v-model="state.commentList[index].content"
placeholder="宝贝满足你的期待吗?说说你的使用心得,分享给想买的他们吧~" />
<!-- TODO 芋艿文件上传 -->
<view class="img-box">
<s-uploader v-model:url="state.commentList[index].images" fileMediatype="image"
limit="9" mode="grid" :imageStyles="{ width: '168rpx', height: '168rpx' }" />
</view>
</view>
</view>
</view>
</view>
</view>
<!-- TODO 芋艿是否匿名 -->
<su-fixed bottom placeholder>
<view class="foot_box ss-flex ss-row-center ss-col-center">
<button class="ss-reset-button post-btn ui-BG-Main-Gradient ui-Shadow-Main" @tap="onSubmit">
发布
</button>
</view>
</su-fixed>
</s-layout>
</template>
<script setup>
import sheep from '@/sheep';
import { onLoad } from '@dcloudio/uni-app';
import { reactive } from 'vue';
import OrderApi from '@/sheep/api/trade/order';
const state = reactive({
orderInfo: {},
commentList: [],
id: null
});
async function onSubmit() {
//
for (const comment of state.commentList) {
await OrderApi.createOrderItemComment(comment);
}
//
sheep.$router.back();
}
onLoad(async (options) => {
if (!options.id) {
sheep.$helper.toast(`缺少订单信息,请检查`);
return
}
state.id = options.id;
const { code, data } = await OrderApi.getOrder(state.id);
if (code !== 0) {
sheep.$helper.toast('无待评价订单');
return
}
//
data.items.forEach((item) => {
state.commentList.push({
anonymous: false,
orderItemId: item.id,
descriptionScores: 5,
benefitScores: 5,
content: '',
picUrls: []
});
});
state.orderInfo = data;
});
</script>
<style lang="scss" scoped>
//
.goods-card {
margin: 10rpx 0;
padding: 20rpx;
background: #fff;
}
//
.form-item {
background: #fff;
.star-box {
height: 100rpx;
padding: 0 25rpx;
}
.star-title {
font-weight: 600;
}
}
.area-box {
width: 690rpx;
min-height: 306rpx;
background: rgba(249, 250, 251, 1);
border-radius: 20rpx;
padding: 28rpx;
margin: auto;
.img-box {
margin-top: 20rpx;
}
}
.post-btn {
width: 690rpx;
line-height: 80rpx;
border-radius: 40rpx;
color: rgba(#fff, 0.9);
margin-bottom: 20rpx;
}
</style>

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

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

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

View File

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

View File

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

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

View File

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

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

532
pages/goods/groupon.vue Normal file
View File

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

409
pages/goods/index.vue Normal file
View File

@ -0,0 +1,409 @@
<template>
<view>
<s-layout :onShareAppMessage="shareInfo" navbar="goods">
<!-- 标题栏 -->
<detailNavbar />
<!-- 骨架屏 -->
<detailSkeleton v-if="state.skeletonLoading" />
<!-- 下架/售罄提醒 -->
<s-empty v-else-if="state.goodsInfo === null" text="商品不存在或已下架" icon="/static/soldout-empty.png" showAction
actionText="再逛逛" actionUrl="/pages/goods/list" />
<block v-else>
<view class="detail-swiper-selector">
<!-- 商品轮播图 -->
<su-swiper class="ss-m-b-14" isPreview :list="formatGoodsSwiper(state.goodsInfo.sliderPicUrls)"
otStyle="tag" imageMode="widthFix" dotCur="bg-mask-40" :seizeHeight="750" />
<!-- 价格+标题 -->
<view class="title-card detail-card ss-p-y-40 ss-p-x-20">
<view class="ss-flex ss-row-between ss-col-center ss-m-b-26">
<view class="price-box ss-flex ss-col-bottom">
<view class="price-text ss-m-r-16">
{{ fen2yuan(state.selectedSku.price || state.goodsInfo.price) }}
</view>
<view class="origin-price-text" v-if="state.goodsInfo.marketPrice > 0">
{{ fen2yuan(state.selectedSku.marketPrice || state.goodsInfo.marketPrice) }}
</view>
</view>
<view class="sales-text">
{{ formatSales('exact', state.goodsInfo.salesCount) }}
</view>
</view>
<view class="discounts-box ss-flex ss-row-between ss-m-b-28">
<!-- 满减送/限时折扣活动的提示 -->
<div class="tag-content">
<view class="tag-box ss-flex">
<view class="tag ss-m-r-10" v-for="promos in state.activityInfo"
:key="promos.id" @tap="onActivity">
{{ promos.name }}
</view>
</view>
</div>
<!-- 优惠劵 -->
<view class="get-coupon-box ss-flex ss-col-center ss-m-l-20" @tap="state.showModel = true"
v-if="state.couponInfo.length">
<view class="discounts-title ss-m-r-8">领券</view>
<text class="cicon-forward"></text>
</view>
</view>
<view class="title-text ss-line-2 ss-m-b-6">{{ state.goodsInfo.name }}</view>
<view class="subtitle-text ss-line-1">{{ state.goodsInfo.introduction }}</view>
</view>
<!-- 功能卡片 -->
<view class="detail-cell-card detail-card ss-flex-col">
<detail-cell-sku v-model="state.selectedSku.goods_sku_text" :sku="state.selectedSku"
@tap="state.showSelectSku = true" />
</view>
<!-- 规格与数量弹框 -->
<s-select-sku :goodsInfo="state.goodsInfo" :show="state.showSelectSku" @addCart="onAddCart"
@buy="onBuy" @change="onSkuChange" @close="state.showSelectSku = false" />
</view>
<!-- 评价 -->
<detail-comment-card class="detail-comment-selector" :goodsId="state.goodsId" />
<!-- 详情 -->
<detail-content-card class="detail-content-selector" :content="state.goodsInfo.description" />
<!-- 活动跳转拼团/秒杀/砍价活动 -->
<detail-activity-tip v-if="state.activityList.length > 0" :activity-list="state.activityList" />
<!-- 详情 tabbar -->
<detail-tabbar v-model="state.goodsInfo">
<view class="buy-box ss-flex ss-col-center ss-p-r-20" v-if="state.goodsInfo.stock > 0">
<button class="ss-reset-button add-btn ui-Shadow-Main" @tap="state.showSelectSku = true">
加入购物车
</button>
<button class="ss-reset-button buy-btn ui-Shadow-Main" @tap="state.showSelectSku = true">
立即购买
</button>
</view>
<view class="buy-box ss-flex ss-col-center ss-p-r-20" v-else>
<button class="ss-reset-button disabled-btn" disabled> 已售罄 </button>
</view>
</detail-tabbar>
<!-- 优惠劵弹窗 -->
<s-coupon-get v-model="state.couponInfo" :show="state.showModel" @close="state.showModel = false"
@get="onGet" />
<!-- 满减送/限时折扣活动弹窗 -->
<s-activity-pop v-model="state.activityInfo" :show="state.showActivityModel"
@close="state.showActivityModel = false" />
</block>
</s-layout>
</view>
</template>
<script setup>
import { reactive, computed } from 'vue';
import { onLoad, onPageScroll } from '@dcloudio/uni-app';
import sheep from '@/sheep';
import CouponApi from '@/sheep/api/promotion/coupon';
import ActivityApi from '@/sheep/api/promotion/activity';
import FavoriteApi from '@/sheep/api/product/favorite';
import { formatSales, formatGoodsSwiper, fen2yuan } from '@/sheep/hooks/useGoods';
import detailNavbar from './components/detail/detail-navbar.vue';
import detailCellSku from './components/detail/detail-cell-sku.vue';
import detailTabbar from './components/detail/detail-tabbar.vue';
import detailSkeleton from './components/detail/detail-skeleton.vue';
import detailCommentCard from './components/detail/detail-comment-card.vue';
import detailContentCard from './components/detail/detail-content-card.vue';
import detailActivityTip from './components/detail/detail-activity-tip.vue';
import { isEmpty } from 'lodash';
import SpuApi from '@/sheep/api/product/spu';
onPageScroll(() => {});
const isLogin = computed(() => sheep.$store('user').isLogin);
const state = reactive({
goodsId: 0,
skeletonLoading: true, // SPU
goodsInfo: {}, // SPU
showSelectSku: false, // SKU
selectedSku: {}, // SKU
showModel: false, // Coupon
couponInfo: [], // Coupon
showActivityModel: false, // / Activity
activityInfo: [], // / Activity
activityList: [], // // Activity
});
//
function onSkuChange(e) {
state.selectedSku = e;
}
//
function onAddCart(e) {
if (!e.id) {
sheep.$helper.toast('请选择商品规格');
return;
}
sheep.$store('cart').add(e);
}
//
function onBuy(e) {
if (!state.selectedSku.id) {
sheep.$helper.toast('请选择商品规格');
return;
}
sheep.$router.go('/pages/order/confirm', {
data: JSON.stringify({
items: [{
skuId: e.id,
count: e.goods_num
}],
// TODO 2
deliveryType: 1,
pointStatus: false,
}),
});
}
//
function onActivity() {
state.showActivityModel = true;
}
//
async function onGet(id) {
const { code } = await CouponApi.takeCoupon(id);
if (code !== 0) {
return;
}
uni.showToast({
title: '领取成功',
});
setTimeout(() => {
getCoupon();
}, 1000);
}
// TODO
const shareInfo = computed(() => {
if (isEmpty(state.goodsInfo)) return {};
return sheep.$platform.share.getShareInfo({
title: state.goodsInfo.name,
image: sheep.$url.cdn(state.goodsInfo.picUrl),
desc: state.goodsInfo.introduction,
params: {
page: '2',
query: state.goodsInfo.id,
},
}, {
type: 'goods', //
title: state.goodsInfo.name, //
image: sheep.$url.cdn(state.goodsInfo.picUrl), //
price: fen2yuan(state.goodsInfo.price), //
original_price: fen2yuan(state.goodsInfo.marketPrice), //
});
});
async function getCoupon() {
const { code, data } = await CouponApi.getCouponTemplateList(state.goodsId, 2, 10);
if (code === 0) {
state.couponInfo = data;
}
}
onLoad((options) => {
//
if (!options.id) {
state.goodsInfo = null;
return;
}
state.goodsId = options.id;
// 1.
SpuApi.getSpuDetail(state.goodsId).then((res) => {
//
if (res.code !== 0 || !res.data) {
state.goodsInfo = null;
return;
}
//
state.skeletonLoading = false;
state.goodsInfo = res.data;
//
if (isLogin.value) {
FavoriteApi.isFavoriteExists(state.goodsId, 'goods').then((res) => {
if (res.code !== 0) {
return;
}
state.goodsInfo.favorite = res.data;
});
}
});
// 2.
getCoupon();
// 3.
ActivityApi.getActivityListBySpuId(state.goodsId).then((res) => {
if (res.code !== 0) {
return;
}
res.data.forEach(activity => {
if ([1, 2, 3].includes(activity.type)) { // //
state.activityList.push(activity);
} else if (activity.type === 5) { //
state.activityInfo.push(activity);
} else { // TODO
console.log('待实现!优先级不高');
}
})
});
});
</script>
<style lang="scss" scoped>
.detail-card {
background-color: #ffff;
margin: 14rpx 20rpx;
border-radius: 10rpx;
overflow: hidden;
}
//
.title-card {
.price-box {
.price-text {
font-size: 42rpx;
font-weight: 500;
color: #ff3000;
line-height: 30rpx;
font-family: OPPOSANS;
&::before {
content: '¥';
font-size: 30rpx;
}
}
.origin-price-text {
font-size: 26rpx;
font-weight: 400;
text-decoration: line-through;
color: $gray-c;
font-family: OPPOSANS;
&::before {
content: '¥';
}
}
}
.sales-text {
font-size: 26rpx;
font-weight: 500;
color: $gray-c;
}
.discounts-box {
.tag-content {
flex: 1;
min-width: 0;
white-space: nowrap;
}
.tag-box {
overflow: hidden;
text-overflow: ellipsis;
}
.tag {
flex-shrink: 0;
padding: 4rpx 10rpx;
font-size: 24rpx;
font-weight: 500;
border-radius: 4rpx;
color: var(--ui-BG-Main);
background: var(--ui-BG-Main-tag);
}
.discounts-title {
font-size: 24rpx;
font-weight: 500;
color: var(--ui-BG-Main);
line-height: normal;
}
.cicon-forward {
color: var(--ui-BG-Main);
font-size: 24rpx;
line-height: normal;
margin-top: 4rpx;
}
}
.title-text {
font-size: 30rpx;
font-weight: bold;
line-height: 42rpx;
}
.subtitle-text {
font-size: 26rpx;
font-weight: 400;
color: $dark-9;
line-height: 42rpx;
}
}
//
.buy-box {
.add-btn {
width: 214rpx;
height: 72rpx;
font-weight: 500;
font-size: 28rpx;
border-radius: 40rpx 0 0 40rpx;
background-color: var(--ui-BG-Main-light);
color: var(--ui-BG-Main);
}
.buy-btn {
width: 214rpx;
height: 72rpx;
font-weight: 500;
font-size: 28rpx;
border-radius: 0 40rpx 40rpx 0;
background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
color: $white;
}
.disabled-btn {
width: 428rpx;
height: 72rpx;
border-radius: 40rpx;
background: #999999;
color: $white;
}
}
.model-box {
height: 60vh;
.model-content {
height: 56vh;
}
.title {
font-size: 36rpx;
font-weight: bold;
color: #333333;
}
.subtitle {
font-size: 26rpx;
font-weight: 500;
color: #333333;
}
}
</style>

362
pages/goods/list.vue Normal file
View File

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

555
pages/goods/seckill.vue Normal file
View File

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

196
pages/index/cart.vue Normal file
View File

@ -0,0 +1,196 @@
<template>
<s-layout title="购物车" tabbar="/pages/index/cart" :bgStyle="{ color: '#fff' }">
<s-empty v-if="state.list.length === 0" text="购物车空空如也,快去逛逛吧~" icon="/static/cart-empty.png" />
<!-- 头部 -->
<view class="cart-box ss-flex ss-flex-col ss-row-between" v-if="state.list.length">
<view class="cart-header ss-flex ss-col-center ss-row-between ss-p-x-30">
<view class="header-left ss-flex ss-col-center ss-font-26">
<text class="goods-number ui-TC-Main ss-flex">{{ state.list.length }}</text>
件商品
</view>
<view class="header-right">
<button v-if="state.editMode" class="ss-reset-button" @tap="state.editMode = false">
取消
</button>
<button v-else class="ss-reset-button ui-TC-Main" @tap="state.editMode = true">
编辑
</button>
</view>
</view>
<!-- 内容 -->
<view class="cart-content ss-flex-1 ss-p-x-30 ss-m-b-40">
<view class="goods-box ss-r-10 ss-m-b-14" v-for="item in state.list" :key="item.id">
<view class="ss-flex ss-col-center">
<label class="check-box ss-flex ss-col-center ss-p-l-10" @tap="onSelectSingle(item.id)">
<radio :checked="state.selectedIds.includes(item.id)" color="var(--ui-BG-Main)"
style="transform: scale(0.8)" @tap.stop="onSelectSingle(item.id)" />
</label>
<s-goods-item :title="item.spu.name" :img="item.spu.picUrl || item.goods.image"
:price="item.sku.price"
:skuText="item.sku.properties.length>1? item.sku.properties.reduce((items2,items)=>items2.valueName+' '+items.valueName):item.sku.properties[0].valueName"
priceColor="#FF3000" :titleWidth="400">
<template v-if="!state.editMode" v-slot:tool>
<su-number-box :min="0" :max="item.sku.stock" :step="1" v-model="item.count" @change="onNumberChange($event, item)" />
</template>
</s-goods-item>
</view>
</view>
</view>
<!-- 底部 -->
<su-fixed bottom :val="48" placeholder v-if="state.list.length > 0" :isInset="false">
<view class="cart-footer ss-flex ss-col-center ss-row-between ss-p-x-30 border-bottom">
<view class="footer-left ss-flex ss-col-center">
<label class="check-box ss-flex ss-col-center ss-p-r-30" @tap="onSelectAll">
<radio :checked="state.isAllSelected" color="var(--ui-BG-Main)"
style="transform: scale(0.8)" @tap.stop="onSelectAll" />
<view class="ss-m-l-8"> 全选 </view>
</label>
<text>合计</text>
<view class="text-price price-text">
{{ fen2yuan(state.totalPriceSelected) }}
</view>
</view>
<view class="footer-right">
<button v-if="state.editMode" class="ss-reset-button ui-BG-Main-Gradient pay-btn ui-Shadow-Main"
@tap="onDelete">
删除
</button>
<button v-else class="ss-reset-button ui-BG-Main-Gradient pay-btn ui-Shadow-Main"
@tap="onConfirm">
去结算
{{ state.selectedIds?.length ? `(${state.selectedIds.length})` : '' }}
</button>
</view>
</view>
</su-fixed>
</view>
</s-layout>
</template>
<script setup>
import sheep from '@/sheep';
import { computed, reactive } from 'vue';
import { fen2yuan } from '../../sheep/hooks/useGoods';
const sys_navBar = sheep.$platform.navbar;
const cart = sheep.$store('cart');
const state = reactive({
editMode: false,
list: computed(() => cart.list),
selectedList: [],
selectedIds: computed(() => cart.selectedIds),
isAllSelected: computed(() => cart.isAllSelected),
totalPriceSelected: computed(() => cart.totalPriceSelected),
});
//
function onSelectSingle(id) {
cart.selectSingle(id);
}
//
function onSelectAll() {
cart.selectAll(!state.isAllSelected);
}
//
function onConfirm() {
let items = []
let goods_list = [];
state.selectedList = state.list.filter((item) => state.selectedIds.includes(item.id));
state.selectedList.map((item) => {
//
items.push({
skuId: item.sku.id,
count: item.count,
cartId: item.id,
})
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>

88
pages/index/index.vue Normal file
View File

@ -0,0 +1,88 @@
<!-- 首页支持店铺装修 -->
<template>
<view v-if="template">
<s-layout title="首页" navbar="custom" tabbar="/pages/index/index" :bgStyle="template.page"
:navbarStyle="template.navigationBar" onShareAppMessage>
<s-block v-for="(item, index) in template.components" :key="index" :styles="item.property.style">
<s-block-item :type="item.id" :data="item.property" :styles="item.property.style" />
</s-block>
</s-layout>
</view>
</template>
<script setup>
import {
computed
} from 'vue';
import {
onLoad,
onPageScroll,
onPullDownRefresh
} from '@dcloudio/uni-app';
import sheep from '@/sheep';
import $share from '@/sheep/platform/share';
// tabBar
uni.hideTabBar();
const template = computed(() => sheep.$store('app').template?.home);
//
// (async function() {
// console.log('',template)
// let {
// data
// } = await index2Api.decorate();
// console.log('',JSON.parse(data[1].value))
// id
// let {
// data: datas
// } = await index2Api.spids();
// template.value.data[9].data.goodsIds = datas.list.map(item => item.id);
// template.value.data[0].data.list = JSON.parse(data[0].value).map(item => {
// return {
// src: item.picUrl,
// url: item.url,
// title: item.name,
// type: "image"
// }
// })
// }())
onLoad((options) => {
// #ifdef MP
//
if (options.scene) {
const sceneParams = decodeURIComponent(options.scene).split('=');
console.log("sceneParams=>",sceneParams);
options[sceneParams[0]] = sceneParams[1];
}
// #endif
//
if (options.templateId) {
sheep.$store('app').init(options.templateId);
}
//
if (options.spm) {
$share.decryptSpm(options.spm);
}
// ()
if (options.page) {
sheep.$router.go(decodeURIComponent(options.page));
}
});
//
onPullDownRefresh(() => {
sheep.$store('app').init();
setTimeout(function() {
uni.stopPullDownRefresh();
}, 800);
});
onPageScroll(() => {});
</script>
<style></style>

38
pages/index/login.vue Normal file
View File

@ -0,0 +1,38 @@
<!-- 微信公众号的登录回调页 -->
<template>
<!-- 空登陆页 -->
<view />
</template>
<script setup>
import sheep from '@/sheep';
import { onLoad } from '@dcloudio/uni-app';
onLoad(async (options) => {
// #ifdef H5
// search options 便
new URLSearchParams(location.search).forEach((value, key) => {
options[key] = value;
});
const event = options.event;
const code = options.code;
const state = options.state;
if (event === 'login') { //
const res = await sheep.$platform.useProvider().login(code, state);
} else if (event === 'bind') { //
sheep.$platform.useProvider().bind(code, state);
}
// H5
let returnUrl = uni.getStorageSync('returnUrl');
if (returnUrl) {
uni.removeStorage({key:'returnUrl'});
location.replace(returnUrl);
} else {
uni.switchTab({
url: '/',
});
}
// #endif
});
</script>

51
pages/index/page.vue Normal file
View File

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

119
pages/index/search.vue Normal file
View File

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

42
pages/index/user.vue Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

405
pages/order/confirm.vue Normal file
View File

@ -0,0 +1,405 @@
<template>
<s-layout title="确认订单">
<!-- TODO这个判断先删除 v-if="state.orderInfo.need_address === 1" -->
<view class="bg-white address-box ss-m-b-14 ss-r-b-10" @tap="onSelectAddress">
<s-address-item :item="state.addressInfo" :hasBorderBottom="false">
<view class="ss-rest-button">
<text class="_icon-forward" />
</view>
</s-address-item>
</view>
<!-- 商品信息 -->
<view class="order-card-box ss-m-b-14">
<s-goods-item
v-for="item in state.orderInfo.items"
:key="item.skuId"
:img="item.picUrl"
:title="item.spuName"
:skuText="item.properties.map((property) => property.valueName).join(' ')"
:price="item.price"
:num="item.count"
marginBottom="10"
/>
<view class="order-item ss-flex ss-col-center ss-row-between ss-p-x-20 bg-white ss-r-10">
<view class="item-title">订单备注</view>
<view class="ss-flex ss-col-center">
<uni-easyinput
maxlength="20"
placeholder="建议留言前先与商家沟通"
v-model="state.orderPayload.remark"
:inputBorder="false"
:clearable="false"
/>
</view>
</view>
</view>
<!-- 价格信息 -->
<view class="bg-white total-card-box ss-p-20 ss-m-b-14 ss-r-10">
<view class="total-box-content border-bottom">
<view class="order-item ss-flex ss-col-center ss-row-between">
<view class="item-title">商品金额</view>
<view class="ss-flex ss-col-center">
<text class="item-value ss-m-r-24">
{{ fen2yuan(state.orderInfo.price.totalPrice) }}
</text>
</view>
</view>
<!-- TODO 芋艿接入积分 -->
<view
class="order-item ss-flex ss-col-center ss-row-between"
v-if="state.orderPayload.order_type === 'score'"
>
<view class="item-title">扣除积分</view>
<view class="ss-flex ss-col-center">
<image
:src="sheep.$url.static('/static/img/shop/goods/score1.svg')"
class="score-img"
/>
<text class="item-value ss-m-r-24">{{ state.orderInfo.score_amount }}</text>
</view>
</view>
<view class="order-item ss-flex ss-col-center ss-row-between">
<view class="item-title">运费</view>
<view class="ss-flex ss-col-center">
<text class="item-value ss-m-r-24">
+{{ fen2yuan(state.orderInfo.price.deliveryPrice) }}
</text>
</view>
</view>
<!-- 优惠劵只有 type = 0 普通订单非拼团秒杀砍价才可以使用优惠劵 -->
<view
class="order-item ss-flex ss-col-center ss-row-between"
v-if="state.orderInfo.type === 0"
>
<view class="item-title">优惠券</view>
<view class="ss-flex ss-col-center" @tap="state.showCoupon = true">
<text class="item-value text-red" v-if="state.orderPayload.couponId > 0">
-{{ fen2yuan(state.orderInfo.price.couponPrice) }}
</text>
<text
class="item-value"
:class="state.couponInfo.length > 0 ? 'text-red' : 'text-disabled'"
v-else
>
{{
state.couponInfo.length > 0 ? state.couponInfo.length + ' 张可用' : '暂无可用优惠券'
}}
</text>
<text class="_icon-forward item-icon" />
</view>
</view>
<view
class="order-item ss-flex ss-col-center ss-row-between"
v-if="state.orderInfo.price.discountPrice > 0"
>
<view class="item-title">活动优惠</view>
<view class="ss-flex ss-col-center">
<!-- @tap="state.showDiscount = true" TODO 芋艿后续要把优惠信息打进去 -->
<text class="item-value text-red">
-{{ fen2yuan(state.orderInfo.price.discountPrice) }}
</text>
<text class="_icon-forward item-icon" />
</view>
</view>
<view
class="order-item ss-flex ss-col-center ss-row-between"
v-if="state.orderInfo.price.vipPrice > 0"
>
<view class="item-title">会员优惠</view>
<view class="ss-flex ss-col-center">
<text class="item-value text-red">
-{{ fen2yuan(state.orderInfo.price.vipPrice) }}
</text>
</view>
</view>
</view>
<view class="total-box-footer ss-font-28 ss-flex ss-row-right ss-col-center ss-m-r-28">
<view class="total-num ss-m-r-20">
{{ state.orderInfo.items.reduce((acc, item) => acc + item.count, 0) }}
</view>
<view>合计</view>
<view class="total-num text-red"> {{ fen2yuan(state.orderInfo.price.payPrice) }} </view>
</view>
</view>
<!-- 选择优惠券弹框 -->
<s-coupon-select
v-model="state.couponInfo"
:show="state.showCoupon"
@confirm="onSelectCoupon"
@close="state.showCoupon = false"
/>
<!-- 满额折扣弹框 TODO 芋艿后续要把优惠信息打进去 -->
<s-discount-list
v-model="state.orderInfo"
:show="state.showDiscount"
@close="state.showDiscount = false"
/>
<!-- 底部 -->
<su-fixed bottom :opacity="false" bg="bg-white" placeholder :noFixed="false" :index="200">
<view class="footer-box border-top ss-flex ss-row-between ss-p-x-20 ss-col-center">
<view class="total-box-footer ss-flex ss-col-center">
<view class="total-num ss-font-30 text-red">
{{ fen2yuan(state.orderInfo.price.payPrice) }}
</view>
</view>
<button
class="ss-reset-button ui-BG-Main-Gradient ss-r-40 submit-btn ui-Shadow-Main"
@tap="onConfirm"
>
提交订单
</button>
</view>
</su-fixed>
</s-layout>
</template>
<script setup>
import { reactive } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import sheep from '@/sheep';
import { isEmpty } from 'lodash';
import OrderApi from '@/sheep/api/trade/order';
import CouponApi from '@/sheep/api/promotion/coupon';
import { fen2yuan } from '@/sheep/hooks/useGoods';
const state = reactive({
orderPayload: {},
orderInfo: {
items: [], //
price: {}, //
},
addressInfo: {}, //
showCoupon: false, //
couponInfo: [], //
showDiscount: false, //
});
//
function onSelectAddress() {
uni.$once('SELECT_ADDRESS', (e) => {
changeConsignee(e.addressInfo);
});
sheep.$router.go('/pages/user/address/list');
}
// &
async function changeConsignee(addressInfo = {}) {
if (!isEmpty(addressInfo)) {
state.addressInfo = addressInfo;
}
await getOrderInfo();
}
//
async function onSelectCoupon(couponId) {
state.orderPayload.couponId = couponId || 0;
await getOrderInfo();
state.showCoupon = false;
}
//
function onConfirm() {
if (!state.addressInfo.id) {
sheep.$helper.toast('请选择收货地址');
return;
}
submitOrder();
}
// &
async function submitOrder() {
const { code, data } = await OrderApi.createOrder({
items: state.orderPayload.items,
couponId: state.orderPayload.couponId,
addressId: state.addressInfo.id,
deliveryType: 1, // TODO
pointStatus: false, // TODO
combinationActivityId: state.orderPayload.combinationActivityId,
combinationHeadId: state.orderPayload.combinationHeadId,
seckillActivityId: state.orderPayload.seckillActivityId
});
if (code !== 0) {
return;
}
//
if (state.orderPayload.items[0].cartId > 0) {
sheep.$store('cart').getList();
}
//
sheep.$router.redirect('/pages/pay/index', {
id: data.payOrderId,
});
}
// &
async function getOrderInfo() {
//
const { data, code } = await OrderApi.settlementOrder({
items: state.orderPayload.items,
couponId: state.orderPayload.couponId,
addressId: state.addressInfo.id,
deliveryType: 1, // TODO
pointStatus: false, // TODO
combinationActivityId: state.orderPayload.combinationActivityId,
combinationHeadId: state.orderPayload.combinationHeadId,
seckillActivityId: state.orderPayload.seckillActivityId
});
if (code !== 0) {
return;
}
state.orderInfo = data;
//
if (state.orderInfo.address) {
state.addressInfo = state.orderInfo.address;
}
}
//
async function getCoupons() {
const { code, data } = await CouponApi.getMatchCouponList(
state.orderInfo.price.payPrice,
state.orderInfo.items.map((item) => item.spuId),
state.orderPayload.items.map((item) => item.skuId),
state.orderPayload.items.map((item) => item.categoryId),
);
if (code === 0) {
state.couponInfo = data;
}
}
onLoad(async (options) => {
if (!options.data) {
sheep.$helper.toast('参数不正确,请检查!');
return;
}
state.orderPayload = JSON.parse(options.data);
await getOrderInfo();
await getCoupons();
});
</script>
<style lang="scss" scoped>
:deep() {
.uni-input-wrapper {
width: 320rpx;
}
.uni-easyinput__content-input {
font-size: 28rpx;
height: 72rpx;
text-align: right !important;
padding-right: 0 !important;
.uni-input-input {
font-weight: 500;
color: #333333;
font-size: 26rpx;
height: 32rpx;
margin-top: 4rpx;
}
}
.uni-easyinput__content {
display: flex !important;
align-items: center !important;
justify-content: right !important;
}
}
.score-img {
width: 36rpx;
height: 36rpx;
margin: 0 4rpx;
}
.order-item {
height: 80rpx;
.item-title {
font-size: 28rpx;
font-weight: 400;
}
.item-value {
font-size: 28rpx;
font-weight: 500;
font-family: OPPOSANS;
}
.text-disabled {
color: #bbbbbb;
}
.item-icon {
color: $dark-9;
}
.remark-input {
text-align: right;
}
.item-placeholder {
color: $dark-9;
font-size: 26rpx;
text-align: right;
}
}
.total-box-footer {
height: 90rpx;
.total-num {
color: #333333;
font-family: OPPOSANS;
}
}
.footer-box {
height: 100rpx;
.submit-btn {
width: 240rpx;
height: 70rpx;
font-size: 28rpx;
font-weight: 500;
.goto-pay-text {
line-height: 28rpx;
}
}
.cancel-btn {
width: 240rpx;
height: 80rpx;
font-size: 26rpx;
background-color: #e5e5e5;
color: $dark-9;
}
}
.title {
font-size: 36rpx;
font-weight: bold;
color: #333333;
}
.subtitle {
font-size: 28rpx;
color: #999999;
}
.cicon-checkbox {
font-size: 36rpx;
color: var(--ui-BG-Main);
}
.cicon-box {
font-size: 36rpx;
color: #999999;
}
</style>

633
pages/order/detail.vue Normal file
View File

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

162
pages/order/express/log.vue Normal file
View File

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

453
pages/order/list.vue Normal file
View File

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

288
pages/pay/index.vue Normal file
View File

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

165
pages/pay/recharge-log.vue Normal file
View File

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

259
pages/pay/recharge.vue Normal file
View File

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

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