feat:初始化项目

This commit is contained in:
sundongyu 2024-05-24 09:16:17 +08:00
commit 959b01b9fb
621 changed files with 92876 additions and 0 deletions

21
.env Normal file
View File

@ -0,0 +1,21 @@
# 版本号
SHOPRO_VERSION = v1.8.3
# 后端接口 - 正式环境(通过 process.env.NODE_ENV 非 development
SHOPRO_BASE_URL = http://api-dashboard.yudao.iocoder.cn
# 后端接口 - 测试环境(通过 process.env.NODE_ENV = development
SHOPRO_DEV_BASE_URL = http://192.168.1.14:48080
### SHOPRO_DEV_BASE_URL = http://192.168.1.14:48080
# 后端接口前缀(一般不建议调整)
SHOPRO_API_PATH = /app-api
# 开发环境运行端口
SHOPRO_DEV_PORT = 3000
# 客户端静态资源地址 空=默认使用服务端指定的CDN资源地址前缀 | local=本地 | http(s)://xxx.xxx=自定义静态资源地址前缀
SHOPRO_STATIC_URL = https://file.sheepjs.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

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>

3
androidPrivacy.json Normal file
View File

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

45
api/infra/file.js Normal file
View File

@ -0,0 +1,45 @@
import { baseUrl, apiPath } from '@/sheep/config';
const FileApi = {
// 上传文件
uploadFile: (file) => {
// TODO 芋艿:访问令牌的接入;
const token = uni.getStorageSync('token');
uni.showLoading({
title: '上传中',
});
return new Promise((resolve, reject) => {
uni.uploadFile({
url: baseUrl + apiPath + '/infra/file/upload',
filePath: file,
name: 'file',
header: {
// Accept: 'text/json',
Accept : '*/*',
'tenant-id' :'1',
// Authorization: 'Bearer test247',
},
success: (uploadFileRes) => {
let result = JSON.parse(uploadFileRes.data);
if (result.error === 1) {
uni.showToast({
icon: 'none',
title: result.msg,
});
} else {
return resolve(result);
}
},
fail: (error) => {
console.log('上传失败:', error);
return resolve(false);
},
complete: () => {
uni.hideLoading();
},
});
});
},
};
export default FileApi;

53
api/member/address.js Normal file
View File

@ -0,0 +1,53 @@
import request from '@/sheep/request';
const AddressApi = {
// 获得用户收件地址列表
getAddressList: () => {
return request({
url: '/member/address/list',
method: 'GET'
});
},
// 创建用户收件地址
createAddress: (data) => {
return request({
url: '/member/address/create',
method: 'POST',
data,
custom: {
showSuccess: true,
successMsg: '保存成功'
},
});
},
// 更新用户收件地址
updateAddress: (data) => {
return request({
url: '/member/address/update',
method: 'PUT',
data,
custom: {
showSuccess: true,
successMsg: '更新成功'
},
});
},
// 获得用户收件地址
getAddress: (id) => {
return request({
url: '/member/address/get',
method: 'GET',
params: { id }
});
},
// 删除用户收件地址
deleteAddress: (id) => {
return request({
url: '/member/address/delete',
method: 'DELETE',
params: { id }
});
},
};
export default AddressApi;

132
api/member/auth.js Normal file
View File

@ -0,0 +1,132 @@
import request from '@/sheep/request';
const AuthUtil = {
// 使用手机 + 密码登录
login: (data) => {
return request({
url: '/member/auth/login',
method: 'POST',
data,
custom: {
showSuccess: true,
loadingMsg: '登录中',
successMsg: '登录成功',
},
});
},
// 使用手机 + 验证码登录
smsLogin: (data) => {
return request({
url: '/member/auth/sms-login',
method: 'POST',
data,
custom: {
showSuccess: true,
loadingMsg: '登录中',
successMsg: '登录成功',
},
});
},
// 发送手机验证码
sendSmsCode: (mobile, scene) => {
return request({
url: '/member/auth/send-sms-code',
method: 'POST',
data: {
mobile,
scene,
},
custom: {
loadingMsg: '发送中',
showSuccess: true,
successMsg: '发送成功',
},
});
},
// 登出系统
logout: () => {
return request({
url: '/member/auth/logout',
method: 'POST',
});
},
// 刷新令牌
refreshToken: (refreshToken) => {
return request({
url: '/member/auth/refresh-token',
method: 'POST',
params: {
refreshToken
},
custom: {
loading: false, // 不用加载中
showError: false, // 不展示错误提示
},
});
},
// 社交授权的跳转
socialAuthRedirect: (type, redirectUri) => {
return request({
url: '/member/auth/social-auth-redirect',
method: 'GET',
params: {
type,
redirectUri,
},
custom: {
showSuccess: true,
loadingMsg: '登陆中',
},
});
},
// 社交快捷登录
socialLogin: (type, code, state) => {
return request({
url: '/member/auth/social-login',
method: 'POST',
data: {
type,
code,
state,
},
custom: {
showSuccess: true,
loadingMsg: '登陆中',
},
});
},
// 微信小程序的一键登录
weixinMiniAppLogin: (phoneCode, loginCode, state) => {
return request({
url: '/member/auth/weixin-mini-app-login',
method: 'POST',
data: {
phoneCode,
loginCode,
state
},
custom: {
showSuccess: true,
loadingMsg: '登陆中',
successMsg: '登录成功',
},
});
},
// 创建微信 JS SDK 初始化所需的签名
createWeixinMpJsapiSignature: (url) => {
return request({
url: '/member/auth/create-weixin-jsapi-signature',
method: 'POST',
params: {
url
},
custom: {
showError: false,
showLoading: false,
},
})
},
//
};
export default AuthUtil;

19
api/member/point.js Normal file
View File

@ -0,0 +1,19 @@
import request from '@/sheep/request';
const PointApi = {
// 获得用户积分记录分页
getPointRecordPage: (params) => {
if (params.addStatus === undefined) {
delete params.addStatus
}
const queryString = Object.keys(params)
.map((key) => encodeURIComponent(key) + '=' + params[key])
.join('&');
return request({
url: `/member/point/record/page?${queryString}`,
method: 'GET',
});
}
};
export default PointApi;

37
api/member/signin.js Normal file
View File

@ -0,0 +1,37 @@
import request from '@/sheep/request';
const SignInApi = {
// 获得签到规则列表
getSignInConfigList: () => {
return request({
url: '/member/sign-in/config/list',
method: 'GET',
});
},
// 获得个人签到统计
getSignInRecordSummary: () => {
return request({
url: '/member/sign-in/record/get-summary',
method: 'GET',
});
},
// 签到
createSignInRecord: () => {
return request({
url: '/member/sign-in/record/create',
method: 'POST',
});
},
// 获得签到记录分页
getSignRecordPage: (params) => {
const queryString = Object.keys(params)
.map((key) => encodeURIComponent(key) + '=' + params[key])
.join('&');
return request({
url: `/member/sign-in/record/page?${queryString}`,
method: 'GET',
});
},
};
export default SignInApi;

54
api/member/social.js Normal file
View File

@ -0,0 +1,54 @@
import request from '@/sheep/request';
const SocialApi = {
// 获得社交用户
getSocialUser: (type) => {
return request({
url: '/member/social-user/get',
method: 'GET',
params: {
type
},
custom: {
showLoading: false,
},
});
},
// 社交绑定
socialBind: (type, code, state) => {
return request({
url: '/member/social-user/bind',
method: 'POST',
data: {
type,
code,
state
},
custom: {
custom: {
showSuccess: true,
loadingMsg: '绑定中',
successMsg: '绑定成功',
},
},
});
},
// 社交绑定
socialUnbind: (type, openid) => {
return request({
url: '/member/social-user/unbind',
method: 'DELETE',
data: {
type,
openid
},
custom: {
showLoading: false,
loadingMsg: '解除绑定',
successMsg: '解绑成功',
},
});
},
};
export default SocialApi;

85
api/member/user.js Normal file
View File

@ -0,0 +1,85 @@
import request from '@/sheep/request';
const UserApi = {
// 获得基本信息
getUserInfo: () => {
return request({
url: '/member/user/get',
method: 'GET',
custom: {
showLoading: false,
auth: true,
},
});
},
// 修改基本信息
updateUser: (data) => {
return request({
url: '/member/user/update',
method: 'PUT',
data,
custom: {
auth: true,
showSuccess: true,
successMsg: '更新成功'
},
});
},
// 修改用户手机
updateUserMobile: (data) => {
return request({
url: '/member/user/update-mobile',
method: 'PUT',
data,
custom: {
loadingMsg: '验证中',
showSuccess: true,
successMsg: '修改成功'
},
});
},
// 基于微信小程序的授权码,修改用户手机
updateUserMobileByWeixin: (code) => {
return request({
url: '/member/user/update-mobile-by-weixin',
method: 'PUT',
data: {
code
},
custom: {
showSuccess: true,
loadingMsg: '获取中',
successMsg: '修改成功'
},
});
},
// 修改密码
updateUserPassword: (data) => {
return request({
url: '/member/user/update-password',
method: 'PUT',
data,
custom: {
loadingMsg: '验证中',
showSuccess: true,
successMsg: '修改成功'
},
});
},
// 重置密码
resetUserPassword: (data) => {
return request({
url: '/member/user/reset-password',
method: 'PUT',
data,
custom: {
loadingMsg: '验证中',
showSuccess: true,
successMsg: '修改成功'
}
});
},
};
export default UserApi;

21
api/migration/app.js Normal file
View File

@ -0,0 +1,21 @@
import request from '@/sheep/request';
// TODO 芋艿:小程序直播还不支持
export default {
//小程序直播
mplive: {
getRoomList: (ids) =>
request({
url: 'app/mplive/getRoomList',
method: 'GET',
params: {
ids: ids.join(','),
}
}),
getMpLink: () =>
request({
url: 'app/mplive/getMpLink',
method: 'GET'
}),
},
};

14
api/migration/chat.js Normal file
View File

@ -0,0 +1,14 @@
import request from '@/sheep/request';
// TODO 芋艿:暂不支持 socket 聊天
export default {
// 获取聊天token
unifiedToken: () =>
request({
url: 'unifiedToken',
custom: {
showError: false,
showLoading: false,
},
}),
};

10
api/migration/index.js Normal file
View File

@ -0,0 +1,10 @@
const files = import.meta.glob('./*.js', { eager: true });
let api = {};
Object.keys(files).forEach((key) => {
api = {
...api,
[key.replace(/(.*\/)*([^.]+).*/gi, '$2')]: files[key].default,
};
});
export default api;

48
api/migration/third.js Normal file
View File

@ -0,0 +1,48 @@
import request from '@/sheep/request';
export default {
// 微信相关
wechat: {
// 小程序订阅消息
subscribeTemplate: (params) =>
request({
url: 'third/wechat/subscribeTemplate',
method: 'GET',
params: {
platform: 'miniProgram',
},
custom: {
showError: false,
showLoading: false,
},
}),
// 获取微信小程序码
getWxacode: async (path, query) => {
return await request({
url: '/member/social-user/wxa-qrcode',
method: 'POST',
data: {
scene: query,
path,
checkPath: false, // TODO 开发环境暂不检查 path 是否存在
},
});
},
},
// 苹果相关
apple: {
// 第三方登录
login: (data) =>
request({
url: 'third/apple/login',
method: 'POST',
data,
custom: {
showSuccess: true,
loadingMsg: '登陆中',
},
}),
},
};

14
api/pay/channel.js Normal file
View File

@ -0,0 +1,14 @@
import request from '@/sheep/request';
const PayChannelApi = {
// 获得指定应用的开启的支付渠道编码列表
getEnableChannelCodeList: (appId) => {
return request({
url: '/pay/channel/get-enable-code-list',
method: 'GET',
params: { appId }
});
},
};
export default PayChannelApi;

22
api/pay/order.js Normal file
View File

@ -0,0 +1,22 @@
import request from '@/sheep/request';
const PayOrderApi = {
// 获得支付订单
getOrder: (id) => {
return request({
url: '/pay/order/get',
method: 'GET',
params: { id }
});
},
// 提交支付订单
submitOrder: (data) => {
return request({
url: '/pay/order/submit',
method: 'POST',
data
});
}
};
export default PayOrderApi;

68
api/pay/wallet.js Normal file
View File

@ -0,0 +1,68 @@
import request from '@/sheep/request';
const PayWalletApi = {
// 获取钱包
getPayWallet() {
return request({
url: '/pay/wallet/get',
method: 'GET',
custom: {
showLoading: false,
auth: true,
},
});
},
// 获得钱包流水分页
getWalletTransactionPage: (params) => {
const queryString = Object.keys(params)
.map((key) => encodeURIComponent(key) + '=' + params[key])
.join('&');
return request({
url: `/pay/wallet-transaction/page?${queryString}`,
method: 'GET',
});
},
// 获得钱包流水统计
getWalletTransactionSummary: (params) => {
const queryString = `createTime=${params.createTime[0]}&createTime=${params.createTime[1]}`;
return request({
url: `/pay/wallet-transaction/get-summary?${queryString}`,
// url: `/pay/wallet-transaction/get-summary`,
method: 'GET',
// params: params
});
},
// 获得钱包充值套餐列表
getWalletRechargePackageList: () => {
return request({
url: '/pay/wallet-recharge-package/list',
method: 'GET',
custom: {
showError: false,
showLoading: false,
},
});
},
// 创建钱包充值记录(发起充值)
createWalletRecharge: (data) => {
return request({
url: '/pay/wallet-recharge/create',
method: 'POST',
data,
});
},
// 获得钱包充值记录分页
getWalletRechargePage: (params) => {
return request({
url: '/pay/wallet-recharge/page',
method: 'GET',
params,
custom: {
showError: false,
showLoading: false,
},
});
},
};
export default PayWalletApi;

21
api/product/category.js Normal file
View File

@ -0,0 +1,21 @@
import request from '@/sheep/request';
const CategoryApi = {
// 查询分类列表
getCategoryList: () => {
return request({
url: '/product/category/list',
method: 'GET',
});
},
// 查询分类列表,指定编号
getCategoryListByIds: (ids) => {
return request({
url: '/product/category/list-by-ids',
method: 'GET',
params: { ids },
});
},
};
export default CategoryApi;

22
api/product/comment.js Normal file
View File

@ -0,0 +1,22 @@
import request from '@/sheep/request';
const CommentApi = {
// 获得商品评价分页
getCommentPage: (spuId, pageNo, pageSize, type) => {
return request({
url: '/product/comment/page',
method: 'GET',
params: {
spuId,
pageNo,
pageSize,
type,
},
custom: {
showLoading: false,
showError: false,
},
});
},
};
export default CommentApi;

54
api/product/favorite.js Normal file
View File

@ -0,0 +1,54 @@
import request from '@/sheep/request';
const FavoriteApi = {
// 获得商品收藏分页
getFavoritePage: (data) => {
return request({
url: '/product/favorite/page',
method: 'GET',
params: data,
});
},
// 检查是否收藏过商品
isFavoriteExists: (spuId) => {
return request({
url: '/product/favorite/exits',
method: 'GET',
params: {
spuId,
},
});
},
// 添加商品收藏
createFavorite: (spuId) => {
return request({
url: '/product/favorite/create',
method: 'POST',
data: {
spuId,
},
custom: {
auth: true,
showSuccess: true,
successMsg: '收藏成功',
},
});
},
// 取消商品收藏
deleteFavorite: (spuId) => {
return request({
url: '/product/favorite/delete',
method: 'DELETE',
data: {
spuId,
},
custom: {
auth: true,
showSuccess: true,
successMsg: '取消成功',
},
});
},
};
export default FavoriteApi;

39
api/product/history.js Normal file
View File

@ -0,0 +1,39 @@
import request from '@/sheep/request';
const SpuHistoryApi = {
// 删除商品浏览记录
deleteBrowseHistory: (spuIds) => {
return request({
url: '/product/browse-history/delete',
method: 'DELETE',
data: { spuIds },
custom: {
showSuccess: true,
successMsg: '删除成功',
},
});
},
// 清空商品浏览记录
cleanBrowseHistory: () => {
return request({
url: '/product/browse-history/clean',
method: 'DELETE',
custom: {
showSuccess: true,
successMsg: '清空成功',
},
});
},
// 获得商品浏览记录分页
getBrowseHistoryPage: (data) => {
return request({
url: '/product/browse-history/page',
method: 'GET',
data,
custom: {
showLoading: false
},
});
},
};
export default SpuHistoryApi;

41
api/product/spu.js Normal file
View File

@ -0,0 +1,41 @@
import request from '@/sheep/request';
const SpuApi = {
// 获得商品 SPU 列表
getSpuListByIds: (ids) => {
return request({
url: '/product/spu/list-by-ids',
method: 'GET',
params: { ids },
custom: {
showLoading: false,
showError: false,
},
});
},
// 获得商品 SPU 分页
getSpuPage: (params) => {
return request({
url: '/product/spu/page',
method: 'GET',
params,
custom: {
showLoading: false,
showError: false,
},
});
},
// 查询商品
getSpuDetail: (id) => {
return request({
url: '/product/spu/get-detail',
method: 'GET',
params: { id },
custom: {
showLoading: false,
showError: false,
},
});
},
};
export default SpuApi;

16
api/promotion/activity.js Normal file
View File

@ -0,0 +1,16 @@
import request from '@/sheep/request';
const ActivityApi = {
// 获得单个商品,近期参与的每个活动
getActivityListBySpuId: (spuId) => {
return request({
url: '/promotion/activity/list-by-spu-id',
method: 'GET',
params: {
spuId,
},
});
},
};
export default ActivityApi;

12
api/promotion/article.js Normal file
View File

@ -0,0 +1,12 @@
import request from '@/sheep/request';
export default {
// 获得文章详情
getArticle: (id, title) => {
return request({
url: '/promotion/article/get',
method: 'GET',
params: { id, title }
});
}
}

View File

@ -0,0 +1,76 @@
import request from '@/sheep/request';
// 拼团 API
const CombinationApi = {
// 获得拼团活动列表
getCombinationActivityList: (count) => {
return request({
url: '/promotion/combination-activity/list',
method: 'GET',
params: { count },
});
},
// 获得拼团活动分页
getCombinationActivityPage: (params) => {
return request({
url: '/promotion/combination-activity/page',
method: 'GET',
params,
});
},
// 获得拼团活动明细
getCombinationActivity: (id) => {
return request({
url: '/promotion/combination-activity/get-detail',
method: 'GET',
params: {
id,
},
});
},
// 获得最近 n 条拼团记录(团长发起的)
getHeadCombinationRecordList: (activityId, status, count) => {
return request({
url: '/promotion/combination-record/get-head-list',
method: 'GET',
params: {
activityId,
status,
count,
},
});
},
// 获得我的拼团记录分页
getCombinationRecordPage: (params) => {
return request({
url: "/promotion/combination-record/page",
method: 'GET',
params
});
},
// 获得拼团记录明细
getCombinationRecordDetail: (id) => {
return request({
url: '/promotion/combination-record/get-detail',
method: 'GET',
params: {
id,
},
});
},
// 获得拼团记录的概要信息
getCombinationRecordSummary: () => {
return request({
url: '/promotion/combination-record/get-summary',
method: 'GET',
});
},
};
export default CombinationApi;

101
api/promotion/coupon.js Normal file
View File

@ -0,0 +1,101 @@
import request from '@/sheep/request';
const CouponApi = {
// 获得优惠劵模板列表
getCouponTemplateListByIds: (ids) => {
return request({
url: '/promotion/coupon-template/list-by-ids',
method: 'GET',
params: { ids },
custom: {
showLoading: false, // 不展示 Loading避免领取优惠劵时不成功提示
showError: false,
},
});
},
// 获得优惠劵模版列表
getCouponTemplateList: (spuId, productScope, count) => {
return request({
url: '/promotion/coupon-template/list',
method: 'GET',
params: { spuId, productScope, count },
});
},
// 获得优惠劵模版分页
getCouponTemplatePage: (params) => {
return request({
url: '/promotion/coupon-template/page',
method: 'GET',
params,
});
},
// 获得优惠劵模版
getCouponTemplate: (id) => {
return request({
url: '/promotion/coupon-template/get',
method: 'GET',
params: { id },
});
},
// 我的优惠劵列表
getCouponPage: (params) => {
return request({
url: '/promotion/coupon/page',
method: 'GET',
params,
});
},
// 领取优惠券
takeCoupon: (templateId) => {
return request({
url: '/promotion/coupon/take',
method: 'POST',
data: { templateId },
custom: {
auth: true,
showLoading: true,
loadingMsg: '领取中',
showSuccess: true,
successMsg: '领取成功',
},
});
},
// 获得优惠劵
getCoupon: (id) => {
return request({
url: '/promotion/coupon/get',
method: 'GET',
params: { id },
});
},
// 获得未使用的优惠劵数量
getUnusedCouponCount: () => {
return request({
url: '/promotion/coupon/get-unused-count',
method: 'GET',
custom: {
showLoading: false,
auth: true,
},
});
},
// 获得匹配指定商品的优惠劵列表
getMatchCouponList: (price, spuIds, skuIds, categoryIds) => {
return request({
url: '/promotion/coupon/match-list',
method: 'GET',
params: {
price,
spuIds: spuIds.join(','),
skuIds: skuIds.join(','),
categoryIds: categoryIds.join(','),
},
custom: {
showError: false,
showLoading: false, // 避免影响 settlementOrder 结算的结果
},
});
}
};
export default CouponApi;

38
api/promotion/diy.js Normal file
View File

@ -0,0 +1,38 @@
import request from '@/sheep/request';
const DiyApi = {
getUsedDiyTemplate: () => {
return request({
url: '/promotion/diy-template/used',
method: 'GET',
custom: {
showError: false,
showLoading: false,
},
});
},
getDiyTemplate: (id) => {
return request({
url: '/promotion/diy-template/get',
method: 'GET',
params: {
id,
},
custom: {
showError: false,
showLoading: false,
},
});
},
getDiyPage: (id) => {
return request({
url: '/promotion/diy-page/get',
method: 'GET',
params: {
id,
},
});
},
};
export default DiyApi;

View File

@ -0,0 +1,14 @@
import request from '@/sheep/request';
const RewardActivityApi = {
// 获得满减送活动
getRewardActivity: (id) => {
return request({
url: '/promotion/reward-activity/get',
method: 'GET',
params: { id },
});
}
};
export default RewardActivityApi;

33
api/promotion/seckill.js Normal file
View File

@ -0,0 +1,33 @@
import request from "@/sheep/request";
const SeckillApi = {
// 获得秒杀时间段列表
getSeckillConfigList: () => {
return request({ url: 'promotion/seckill-config/list', method: 'GET' });
},
// 获得当前秒杀活动
getNowSeckillActivity: () => {
return request({ url: 'promotion/seckill-activity/get-now', method: 'GET' });
},
// 获得秒杀活动分页
getSeckillActivityPage: (params) => {
return request({ url: 'promotion/seckill-activity/page', method: 'GET', params });
},
/**
* 获得秒杀活动明细
* @param {number} id 秒杀活动编号
* @return {*}
*/
getSeckillActivity: (id) => {
return request({
url: 'promotion/seckill-activity/get-detail',
method: 'GET',
params: { id }
});
}
}
export default SeckillApi;

13
api/system/area.js Normal file
View File

@ -0,0 +1,13 @@
import request from '@/sheep/request';
const AreaApi = {
// 获得地区树
getAreaTree: () => {
return request({
url: '/system/area/tree',
method: 'GET'
});
},
};
export default AreaApi;

63
api/trade/afterSale.js Normal file
View File

@ -0,0 +1,63 @@
import request from '@/sheep/request';
const AfterSaleApi = {
// 获得售后分页
getAfterSalePage: (params) => {
return request({
url: `/trade/after-sale/page`,
method: 'GET',
params,
custom: {
showLoading: false,
},
});
},
// 创建售后
createAfterSale: (data) => {
return request({
url: `/trade/after-sale/create`,
method: 'POST',
data,
});
},
// 获得售后
getAfterSale: (id) => {
return request({
url: `/trade/after-sale/get`,
method: 'GET',
params: {
id,
},
});
},
// 取消售后
cancelAfterSale: (id) => {
return request({
url: `/trade/after-sale/cancel`,
method: 'DELETE',
params: {
id,
},
});
},
// 获得售后日志列表
getAfterSaleLogList: (afterSaleId) => {
return request({
url: `/trade/after-sale-log/list`,
method: 'GET',
params: {
afterSaleId,
},
});
},
// 退回货物
deliveryAfterSale: (data) => {
return request({
url: `/trade/after-sale/delivery`,
method: 'PUT',
data,
});
}
};
export default AfterSaleApi;

85
api/trade/brokerage.js Normal file
View File

@ -0,0 +1,85 @@
import request from '@/sheep/request';
const BrokerageApi = {
// 获得个人分销信息
getBrokerageUser: () => {
return request({
url: '/trade/brokerage-user/get',
method: 'GET'
});
},
// 获得个人分销统计
getBrokerageUserSummary: () => {
return request({
url: '/trade/brokerage-user/get-summary',
method: 'GET',
});
},
// 获得分销记录分页
getBrokerageRecordPage: params => {
if (params.status === undefined) {
delete params.status
}
const queryString = Object.keys(params)
.map(key => encodeURIComponent(key) + '=' + params[key])
.join('&');
return request({
url: `/trade/brokerage-record/page?${queryString}`,
method: 'GET',
});
},
// 创建分销提现
createBrokerageWithdraw: data => {
return request({
url: '/trade/brokerage-withdraw/create',
method: 'POST',
data,
});
},
// 获得商品的分销金额
getProductBrokeragePrice: spuId => {
return request({
url: '/trade/brokerage-record/get-product-brokerage-price',
method: 'GET',
params: { spuId }
});
},
// 获得分销用户排行(基于佣金)
getRankByPrice: params => {
const queryString = `times=${params.times[0]}&times=${params.times[1]}`;
return request({
url: `/trade/brokerage-user/get-rank-by-price?${queryString}`,
method: 'GET',
});
},
// 获得分销用户排行分页(基于佣金)
getBrokerageUserChildSummaryPageByPrice: params => {
const queryString = Object.keys(params)
.map(key => encodeURIComponent(key) + '=' + params[key])
.join('&');
return request({
url: `/trade/brokerage-user/rank-page-by-price?${queryString}`,
method: 'GET',
});
},
// 获得分销用户排行分页(基于用户量)
getBrokerageUserRankPageByUserCount: params => {
const queryString = Object.keys(params)
.map(key => encodeURIComponent(key) + '=' + params[key])
.join('&');
return request({
url: `/trade/brokerage-user/rank-page-by-user-count?${queryString}`,
method: 'GET',
});
},
// 获得下级分销统计分页
getBrokerageUserChildSummaryPage: params => {
return request({
url: '/trade/brokerage-user/child-summary-page',
method: 'GET',
params,
})
}
}
export default BrokerageApi

50
api/trade/cart.js Normal file
View File

@ -0,0 +1,50 @@
import request from '@/sheep/request';
const CartApi = {
addCart: (data) => {
return request({
url: '/trade/cart/add',
method: 'POST',
data: data,
custom: {
showSuccess: true,
successMsg: '已添加到购物车~',
}
});
},
updateCartCount: (data) => {
return request({
url: '/trade/cart/update-count',
method: 'PUT',
data: data
});
},
updateCartSelected: (data) => {
return request({
url: '/trade/cart/update-selected',
method: 'PUT',
data: data
});
},
deleteCart: (ids) => {
return request({
url: '/trade/cart/delete',
method: 'DELETE',
params: {
ids
}
});
},
getCartList: () => {
return request({
url: '/trade/cart/list',
method: 'GET',
custom: {
showLoading: false,
auth: true,
},
});
},
};
export default CartApi;

13
api/trade/config.js Normal file
View File

@ -0,0 +1,13 @@
import request from '@/sheep/request';
const TradeConfigApi = {
// 获得交易配置
getTradeConfig: () => {
return request({
url: `/trade/config/get`,
method: 'GET',
});
},
};
export default TradeConfigApi;

13
api/trade/delivery.js Normal file
View File

@ -0,0 +1,13 @@
import request from '@/sheep/request';
const DeliveryApi = {
// 获得快递公司列表
getDeliveryExpressList: () => {
return request({
url: `/trade/delivery/express/list`,
method: 'get',
});
}
};
export default DeliveryApi;

139
api/trade/order.js Normal file
View File

@ -0,0 +1,139 @@
import request from '@/sheep/request';
const OrderApi = {
// 计算订单信息
settlementOrder: (data) => {
const data2 = {
...data,
};
// 移除多余字段
if (!(data.couponId > 0)) {
delete data2.couponId;
}
if (!(data.addressId > 0)) {
delete data2.addressId;
}
if (!(data.combinationActivityId > 0)) {
delete data2.combinationActivityId;
}
if (!(data.combinationHeadId > 0)) {
delete data2.combinationHeadId;
}
if (!(data.seckillActivityId > 0)) {
delete data2.seckillActivityId;
}
// 解决 SpringMVC 接受 List<Item> 参数的问题
delete data2.items;
for (let i = 0; i < data.items.length; i++) {
data2[encodeURIComponent('items[' + i + '' + '].skuId')] = data.items[i].skuId + '';
data2[encodeURIComponent('items[' + i + '' + '].count')] = data.items[i].count + '';
if (data.items[i].cartId) {
data2[encodeURIComponent('items[' + i + '' + '].cartId')] = data.items[i].cartId + '';
}
}
const queryString = Object.keys(data2)
.map((key) => key + '=' + data2[key])
.join('&');
return request({
url: `/trade/order/settlement?${queryString}`,
method: 'GET',
custom: {
showError: true,
showLoading: true,
},
});
},
// 创建订单
createOrder: (data) => {
return request({
url: `/trade/order/create`,
method: 'POST',
data,
});
},
// 获得订单
getOrder: (id) => {
return request({
url: `/trade/order/get-detail`,
method: 'GET',
params: {
id,
},
custom: {
showLoading: false,
},
});
},
// 订单列表
getOrderPage: (params) => {
return request({
url: '/trade/order/page',
method: 'GET',
params,
custom: {
showLoading: false,
},
});
},
// 确认收货
receiveOrder: (id) => {
return request({
url: `/trade/order/receive`,
method: 'PUT',
params: {
id,
},
});
},
// 取消订单
cancelOrder: (id) => {
return request({
url: `/trade/order/cancel`,
method: 'DELETE',
params: {
id,
},
});
},
// 删除订单
deleteOrder: (id) => {
return request({
url: `/trade/order/delete`,
method: 'DELETE',
params: {
id,
},
});
},
// 获得交易订单的物流轨迹
getOrderExpressTrackList: (id) => {
return request({
url: `/trade/order/get-express-track-list`,
method: 'GET',
params: {
id,
},
});
},
// 获得交易订单数量
getOrderCount: () => {
return request({
url: '/trade/order/get-count',
method: 'GET',
custom: {
showLoading: false,
auth: true,
},
});
},
// 创建单个评论
createOrderItemComment: (data) => {
return request({
url: `/trade/order/item/create-comment`,
method: 'POST',
data,
});
},
};
export default OrderApi;

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": "wx98df718e528399d2",
"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"
}

103
package.json Normal file
View File

@ -0,0 +1,103 @@
{
"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": {
"@hyoga/uni-socket.io": "^3.0.4",
"dayjs": "^1.11.11",
"lodash": "^4.17.21",
"luch-request": "^3.1.1",
"pinia": "^2.1.7",
"pinia-plugin-persist-uni": "^1.3.1",
"weixin-js-sdk": "^1.6.5"
},
"devDependencies": {
"prettier": "^3.2.5",
"vconsole": "^3.15.1"
}
}

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

View File

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

View File

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

58
pages/chat/emoji.js Normal file
View File

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

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

@ -0,0 +1,870 @@
<template>
<s-layout class="chat-wrap" title="客服" navbar="inner">
<div class="status">
{{ socketState.isConnect ? customerServiceInfo.title : '网络已断开,请检查网络后刷新重试' }}
</div>
<div class="page-bg" :style="{ height: sys_navBar + 'px' }"></div>
<view class="chat-box" :style="{ height: pageHeight + 'px' }">
<scroll-view
:style="{ height: pageHeight + 'px' }"
scroll-y="true"
:scroll-with-animation="false"
:enable-back-to-top="true"
:scroll-into-view="chat.scrollInto"
>
<button
class="loadmore-btn ss-reset-button"
v-if="
chatList.length &&
chatHistoryPagination.lastPage > 1 &&
loadingMap[chatHistoryPagination.loadStatus].title
"
@click="onLoadMore"
>
{{ loadingMap[chatHistoryPagination.loadStatus].title }}
<i
class="loadmore-icon sa-m-l-6"
:class="loadingMap[chatHistoryPagination.loadStatus].icon"
></i>
</button>
<view class="message-item ss-flex-col" v-for="(item, index) in chatList" :key="index">
<view class="ss-flex ss-row-center ss-col-center">
<!-- 日期 -->
<view v-if="item.from !== 'system' && showTime(item, index)" class="date-message">
{{ formatTime(item.date) }}
</view>
<!-- 系统消息 -->
<view v-if="item.from === 'system'" class="system-message">
{{ item.content.text }}
</view>
</view>
<!-- 常见问题 -->
<view v-if="item.mode === 'template' && item.content.list.length" class="template-wrap">
<view class="title">猜你想问</view>
<view
class="item"
v-for="(item, index) in item.content.list"
:key="index"
@click="onTemplateList(item)"
>
* {{ item.title }}
</view>
</view>
<view
v-if="
(item.from === 'customer_service' && item.mode !== 'template') ||
item.from === 'customer'
"
class="ss-flex ss-col-top"
:class="[
item.from === 'customer_service'
? `ss-row-left`
: item.from === 'customer'
? `ss-row-right`
: '',
]"
>
<!-- 客服头像 -->
<image
v-show="item.from === 'customer_service'"
class="chat-avatar ss-m-r-24"
:src="
sheep.$url.cdn(item?.sender?.avatar) ||
sheep.$url.static('/static/img/shop/chat/default.png')
"
mode="aspectFill"
></image>
<!-- 发送状态 -->
<span
v-if="
item.from === 'customer' &&
index == chatData.chatList.length - 1 &&
chatData.isSendSucces !== 0
"
class="send-status"
>
<image
v-if="chatData.isSendSucces == -1"
class="loading"
:src="sheep.$url.static('/static/img/shop/chat/loading.png')"
mode="aspectFill"
></image>
<!-- <image
v-if="chatData.isSendSucces == 1"
class="warning"
:src="sheep.$url.static('/static/img/shop/chat/warning.png')"
mode="aspectFill"
@click="onAgainSendMessage(item)"
></image> -->
</span>
<!-- 内容 -->
<template v-if="item.mode === 'text'">
<view class="message-box" :class="[item.from]">
<div
class="message-text ss-flex ss-flex-wrap"
@click="onRichtext"
v-html="replaceEmoji(item.content.text)"
></div>
</view>
</template>
<template v-if="item.mode === 'image'">
<view class="message-box" :class="[item.from]" :style="{ width: '200rpx' }">
<su-image
class="message-img"
isPreview
:previewList="[sheep.$url.cdn(item.content.url)]"
:current="0"
:src="sheep.$url.cdn(item.content.url)"
:height="200"
:width="200"
mode="aspectFill"
></su-image>
</view>
</template>
<template v-if="item.mode === 'goods'">
<GoodsItem
:goodsData="item.content.item"
@tap="
sheep.$router.go('/pages/goods/index', {
id: item.content.item.id,
})
"
/>
</template>
<template v-if="item.mode === 'order'">
<OrderItem
from="msg"
:orderData="item.content.item"
@tap="
sheep.$router.go('/pages/order/detail', {
id: item.content.item.id,
})
"
/>
</template>
<!-- user头像 -->
<image
v-show="item.from === 'customer'"
class="chat-avatar ss-m-l-24"
:src="sheep.$url.cdn(customerUserInfo.avatar)"
mode="aspectFill"
>
</image>
</view>
</view>
<view id="scrollBottom"></view>
</scroll-view>
</view>
<su-fixed bottom>
<view class="send-wrap ss-flex">
<view class="left ss-flex ss-flex-1">
<uni-easyinput
class="ss-flex-1 ss-p-l-22"
:inputBorder="false"
:clearable="false"
v-model="chat.msg"
placeholder="请输入你要咨询的问题"
></uni-easyinput>
</view>
<text class="sicon-basic bq" @tap.stop="onTools('emoji')"></text>
<text
v-if="!chat.msg"
class="sicon-edit"
:class="{ 'is-active': chat.toolsMode == 'tools' }"
@tap.stop="onTools('tools')"
></text>
<button v-if="chat.msg" class="ss-reset-button send-btn" @tap="onSendMessage">
发送
</button>
</view>
</su-fixed>
<su-popup
:show="chat.showTools"
@close="
chat.showTools = false;
chat.toolsMode = '';
"
>
<view class="ss-modal-box ss-flex-col">
<view class="send-wrap ss-flex">
<view class="left ss-flex ss-flex-1">
<uni-easyinput
class="ss-flex-1 ss-p-l-22"
:inputBorder="false"
:clearable="false"
v-model="chat.msg"
placeholder="请输入你要咨询的问题"
></uni-easyinput>
</view>
<text class="sicon-basic bq" @tap.stop="onTools('emoji')"></text>
<text></text>
<text
v-if="!chat.msg"
class="sicon-edit"
:class="{ 'is-active': chat.toolsMode == 'tools' }"
@tap.stop="onTools('tools')"
></text>
<button v-if="chat.msg" class="ss-reset-button send-btn" @tap="onSendMessage">
发送
</button>
</view>
<view class="content ss-flex ss-flex-1">
<template v-if="chat.toolsMode == 'emoji'">
<swiper
class="emoji-swiper"
:indicator-dots="true"
circular
indicator-active-color="#7063D2"
indicator-color="rgba(235, 231, 255, 1)"
:autoplay="false"
:interval="3000"
:duration="1000"
>
<swiper-item v-for="emoji in emojiPage" :key="emoji">
<view class="ss-flex ss-flex-wrap">
<template v-for="item in emoji" :key="item">
<image
class="emoji-img"
:src="sheep.$url.cdn(`/static/img/chat/emoji/${item.file}`)"
@tap="onEmoji(item)"
>
</image>
</template>
</view>
</swiper-item>
</swiper>
</template>
<template v-else>
<view class="image">
<s-uploader
file-mediatype="image"
:imageStyles="{ width: 50, height: 50, border: false }"
@select="onSelect({ type: 'image', data: $event })"
>
<image
class="icon"
:src="sheep.$url.static('/static/img/shop/chat/image.png')"
mode="aspectFill"
></image>
</s-uploader>
<view>图片</view>
</view>
<view class="goods" @tap="onShowSelect('goods')">
<image
class="icon"
:src="sheep.$url.static('/static/img/shop/chat/goods.png')"
mode="aspectFill"
></image>
<view>商品</view>
</view>
<view class="order" @tap="onShowSelect('order')">
<image
class="icon"
:src="sheep.$url.static('/static/img/shop/chat/order.png')"
mode="aspectFill"
></image>
<view>订单</view>
</view>
</template>
</view>
</view>
</su-popup>
<SelectPopup
:mode="chat.selectMode"
:show="chat.showSelect"
@select="onSelect"
@close="chat.showSelect = false"
/>
</s-layout>
</template>
<script setup>
import sheep from '@/sheep';
import { computed, reactive, toRefs } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import { emojiList, emojiPage } from './emoji.js';
import SelectPopup from './components/select-popup.vue';
import GoodsItem from './components/goods.vue';
import OrderItem from './components/order.vue';
import { useChatWebSocket } from './socket';
const {
socketInit,
state: chatData,
socketSendMsg,
formatChatInput,
socketHistoryList,
onDrop,
onPaste,
getFocus,
// upload,
getUserToken,
// socketTest,
showTime,
formatTime,
} = useChatWebSocket();
const chatList = toRefs(chatData).chatList;
const customerServiceInfo = toRefs(chatData).customerServerInfo;
const chatHistoryPagination = toRefs(chatData).chatHistoryPagination;
const customerUserInfo = toRefs(chatData).customerUserInfo;
const socketState = toRefs(chatData).socketState;
const sys_navBar = sheep.$platform.navbar;
const chatConfig = computed(() => sheep.$store('app').chat);
const { screenHeight, safeAreaInsets, safeArea, screenWidth } = sheep.$platform.device;
const pageHeight = safeArea.height - 44 - 35 - 50;
const chatStatus = {
online: {
text: '在线',
colorVariate: '#46c55f',
},
offline: {
text: '离线',
colorVariate: '#b5b5b5',
},
busy: {
text: '忙碌',
colorVariate: '#ff0e1b',
},
};
//
const loadingMap = {
loadmore: {
title: '查看更多',
icon: 'el-icon-d-arrow-left',
},
nomore: {
title: '没有更多了',
icon: '',
},
loading: {
title: '加载中... ',
icon: 'el-icon-loading',
},
};
const onLoadMore = () => {
chatHistoryPagination.value.page < chatHistoryPagination.value.lastPage && socketHistoryList();
};
const chat = reactive({
msg: '',
scrollInto: '',
showTools: false,
toolsMode: '',
showSelect: false,
selectMode: '',
chatStyle: {
mode: 'inner',
color: '#F8270F',
type: 'color',
alwaysShow: 1,
src: '',
list: {},
},
});
//
function onTools(mode) {
if (!socketState.value.isConnect) {
sheep.$helper.toast(socketState.value.tip || '您已掉线!请返回重试');
return;
}
if (!chat.toolsMode || chat.toolsMode === mode) {
chat.showTools = !chat.showTools;
}
chat.toolsMode = mode;
if (!chat.showTools) {
chat.toolsMode = '';
}
}
function onShowSelect(mode) {
chat.showTools = false;
chat.showSelect = true;
chat.selectMode = mode;
}
async function onSelect({ type, data }) {
let msg = '';
switch (type) {
case 'image':
const { path, fullurl } = await sheep.$api.app.upload(data.tempFiles[0].path, 'default');
msg = {
from: 'customer',
mode: 'image',
date: new Date().getTime(),
content: {
url: fullurl,
path: path,
},
};
break;
case 'goods':
msg = {
from: 'customer',
mode: 'goods',
date: new Date().getTime(),
content: {
item: {
id: data.goods.id,
title: data.goods.title,
image: data.goods.image,
price: data.goods.price,
stock: data.goods.stock,
},
},
};
break;
case 'order':
msg = {
from: 'customer',
mode: 'order',
date: new Date().getTime(),
content: {
item: {
id: data.id,
order_sn: data.order_sn,
create_time: data.create_time,
pay_fee: data.pay_fee,
items: data.items.filter((item) => ({
goods_id: item.goods_id,
goods_title: item.goods_title,
goods_image: item.goods_image,
goods_price: item.goods_price,
})),
status_text: data.status_text,
},
},
};
break;
}
if (msg) {
socketSendMsg(msg, () => {
scrollBottom();
});
// scrollBottom();
chat.showTools = false;
chat.showSelect = false;
chat.selectMode = '';
}
}
function onAgainSendMessage(item) {
if (!socketState.value.isConnect) {
sheep.$helper.toast(socketState.value.tip || '您已掉线!请返回重试');
return;
}
if (!item) return;
const data = {
from: 'customer',
mode: 'text',
date: new Date().getTime(),
content: item.content,
};
socketSendMsg(data, () => {
scrollBottom();
});
}
function onSendMessage() {
if (!socketState.value.isConnect) {
sheep.$helper.toast(socketState.value.tip || '您已掉线!请返回重试');
return;
}
if (!chat.msg) return;
const data = {
from: 'customer',
mode: 'text',
date: new Date().getTime(),
content: {
text: chat.msg,
},
};
socketSendMsg(data, () => {
scrollBottom();
});
chat.showTools = false;
// scrollBottom();
setTimeout(() => {
chat.msg = '';
}, 100);
}
//
function onTemplateList(e) {
if (!socketState.value.isConnect) {
sheep.$helper.toast(socketState.value.tip || '您已掉线!请返回重试');
return;
}
const data = {
from: 'customer',
mode: 'text',
date: new Date().getTime(),
content: {
text: e.title,
},
customData: {
question_id: e.id,
},
};
socketSendMsg(data, () => {
scrollBottom();
});
// scrollBottom();
}
function onEmoji(item) {
chat.msg += item.name;
}
function selEmojiFile(name) {
for (let index in emojiList) {
if (emojiList[index].name === name) {
return emojiList[index].file;
}
}
return false;
}
function replaceEmoji(data) {
let newData = data;
if (typeof newData !== 'object') {
let reg = /\[(.+?)\]/g; // []
let zhEmojiName = newData.match(reg);
if (zhEmojiName) {
zhEmojiName.forEach((item) => {
let emojiFile = selEmojiFile(item);
newData = newData.replace(
item,
`<img class="chat-img" style="width: 24px;height: 24px;margin: 0 3px;" src="${sheep.$url.cdn(
'/static/img/chat/emoji/' + emojiFile,
)}"/>`,
);
});
}
}
return newData;
}
function scrollBottom() {
let timeout = null;
chat.scrollInto = '';
clearTimeout(timeout);
timeout = setTimeout(() => {
chat.scrollInto = 'scrollBottom';
}, 100);
}
onLoad(async () => {
const { error } = await getUserToken();
if (error === 0) {
socketInit(chatConfig.value, () => {
scrollBottom();
});
} else {
socketState.value.isConnect = false;
}
});
</script>
<style lang="scss" scoped>
.page-bg {
width: 100%;
position: absolute;
top: 0;
left: 0;
background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
background-size: 750rpx 100%;
z-index: 1;
}
.chat-wrap {
// :deep() {
// .ui-navbar-box {
// background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
// }
// }
.status {
position: relative;
box-sizing: border-box;
z-index: 3;
height: 70rpx;
padding: 0 30rpx;
background: var(--ui-BG-Main-opacity-1);
display: flex;
align-items: center;
font-size: 30rpx;
font-weight: 400;
color: var(--ui-BG-Main);
}
.chat-box {
padding: 0 20rpx 0;
.loadmore-btn {
width: 98%;
height: 40px;
font-size: 12px;
color: #8c8c8c;
.loadmore-icon {
transform: rotate(90deg);
}
}
.message-item {
margin-bottom: 33rpx;
}
.date-message,
.system-message {
width: fit-content;
border-radius: 12rpx;
padding: 8rpx 16rpx;
margin-bottom: 16rpx;
background-color: var(--ui-BG-3);
color: #999;
font-size: 24rpx;
}
.chat-avatar {
width: 70rpx;
height: 70rpx;
border-radius: 50%;
}
.send-status {
color: #333;
height: 80rpx;
margin-right: 8rpx;
display: flex;
align-items: center;
.loading {
width: 32rpx;
height: 32rpx;
-webkit-animation: rotating 2s linear infinite;
animation: rotating 2s linear infinite;
@-webkit-keyframes rotating {
0% {
transform: rotateZ(0);
}
100% {
transform: rotateZ(360deg);
}
}
@keyframes rotating {
0% {
transform: rotateZ(0);
}
100% {
transform: rotateZ(360deg);
}
}
}
.warning {
width: 32rpx;
height: 32rpx;
color: #ff3000;
}
}
.message-box {
max-width: 50%;
font-size: 16px;
line-height: 20px;
// max-width: 500rpx;
white-space: normal;
word-break: break-all;
word-wrap: break-word;
padding: 20rpx;
border-radius: 10rpx;
color: #fff;
background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
&.customer_service {
background: #fff;
color: #333;
}
:deep() {
.imgred {
width: 100%;
}
.imgred,
img {
width: 100%;
}
}
}
:deep() {
.goods,
.order {
max-width: 500rpx;
}
}
.message-img {
width: 100px;
height: 100px;
border-radius: 6rpx;
}
.template-wrap {
// width: 100%;
padding: 20rpx 24rpx;
background: #fff;
border-radius: 10rpx;
.title {
font-size: 26rpx;
font-weight: 500;
color: #333;
margin-bottom: 29rpx;
}
.item {
font-size: 24rpx;
color: var(--ui-BG-Main);
margin-bottom: 16rpx;
&:last-of-type {
margin-bottom: 0;
}
}
}
.error-img {
width: 400rpx;
height: 400rpx;
}
#scrollBottom {
height: 120rpx;
}
}
.send-wrap {
padding: 18rpx 20rpx;
background: #fff;
.left {
height: 64rpx;
border-radius: 32rpx;
background: var(--ui-BG-1);
}
.bq {
font-size: 50rpx;
margin-left: 10rpx;
}
.sicon-edit {
font-size: 50rpx;
margin-left: 10rpx;
transform: rotate(0deg);
transition: all linear 0.2s;
&.is-active {
transform: rotate(45deg);
}
}
.send-btn {
width: 100rpx;
height: 60rpx;
line-height: 60rpx;
border-radius: 30rpx;
background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
font-size: 26rpx;
color: #fff;
margin-left: 11rpx;
}
}
}
.content {
width: 100%;
align-content: space-around;
border-top: 1px solid #dfdfdf;
padding: 20rpx 0 0;
.emoji-swiper {
width: 100%;
height: 280rpx;
padding: 0 20rpx;
.emoji-img {
width: 50rpx;
height: 50rpx;
display: inline-block;
margin: 10rpx;
}
}
.image,
.goods,
.order {
width: 33.3%;
height: 280rpx;
text-align: center;
font-size: 24rpx;
color: #333;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.icon {
width: 50rpx;
height: 50rpx;
margin-bottom: 21rpx;
}
}
:deep() {
.uni-file-picker__container {
justify-content: center;
}
.file-picker__box {
display: none;
&:last-of-type {
display: flex;
}
}
}
}
</style>
<style>
.chat-img {
width: 24px;
height: 24px;
margin: 0 3px;
}
.full-img {
object-fit: cover;
width: 100px;
height: 100px;
border-radius: 6px;
}
</style>

821
pages/chat/socket.js Normal file
View File

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

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/user/wallet/commission')">
<view class="header-title ss-m-r-4">查看明细</view>
<text class="cicon-play-arrow" />
</view>
</view>
<!-- 收益 -->
<view class="card-content ss-flex">
<view class="ss-flex-1 ss-flex-col ss-col-center">
<view class="item-title">当前佣金()</view>
<view class="item-detail">
{{ state.showMoney ? fen2yuan(state.summary.brokeragePrice || 0) : '***' }}
</view>
</view>
<view class="ss-flex-1 ss-flex-col ss-col-center">
<view class="item-title">昨天的佣金()</view>
<view class="item-detail">
{{ state.showMoney ? fen2yuan(state.summary.yesterdayPrice || 0) : '***' }}
</view>
</view>
<view class="ss-flex-1 ss-flex-col ss-col-center">
<view class="item-title">累计已提()</view>
<view class="item-detail">
{{ state.showMoney ? fen2yuan(state.summary.withdrawPrice || 0) : '***' }}
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import sheep from '@/sheep';
import { computed, reactive, onMounted } from 'vue';
import BrokerageApi from '@/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 '@/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 '@/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 '@/api/product/spu';
import BrokerageApi from '@/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,37 @@
<!-- 分销中心 -->
<template>
<s-layout navbar="inner" class="index-wrap" title="分销中心" :bgStyle="bgStyle" onShareAppMessage>
<!-- 分销商信息 -->
<commission-info />
<!-- 账户信息 -->
<account-info />
<!-- 菜单栏 -->
<commission-menu />
<!-- 分销记录 -->
<commission-log />
<!-- 权限弹窗 -->
<commission-auth />
</s-layout>
</template>
<script setup>
import { reactive } from 'vue';
import commissionInfo from './components/commission-info.vue';
import accountInfo from './components/account-info.vue';
import commissionLog from './components/commission-log.vue';
import commissionMenu from './components/commission-menu.vue';
import commissionAuth from './components/commission-auth.vue';
const state = reactive({});
const bgStyle = {
color: '#F7D598',
};
</script>
<style lang="scss" scoped>
:deep(.page-main) {
background-size: 100% 100% !important;
}
</style>

331
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 '@/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 '@/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 '@/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 '@/api/trade/config';
import BrokerageApi from '@/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 '@/api/promotion/coupon';
import { fen2yuan } from '@/sheep/hooks/useGoods';
import SpuApi from '@/api/product/spu';
import CategoryApi from '@/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 '@/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 '@/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 '@/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 '@/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 '@/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 "@/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 "@/api/promotion/combination";
import SpuApi from "@/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>

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

@ -0,0 +1,412 @@
<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 '@/api/promotion/coupon';
import ActivityApi from '@/api/promotion/activity';
import FavoriteApi from '@/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 '@/api/product/spu';
onPageScroll(() => {});
const state = reactive({
goodsId: 0,
skeletonLoading: true, // SPU
goodsInfo: {}, // SPU
showSelectSku: false, // SKU
selectedSku: {}, // SKU
showModel: false, // Coupon
couponInfo: [], // Coupon
showActivityModel: false, // / Activity
activityInfo: [], // / Activity
activityList: [], // // Activity
});
//
function onSkuChange(e) {
state.selectedSku = e;
}
//
function onAddCart(e) {
if (!e.id) {
sheep.$helper.toast('请选择商品规格');
return;
}
sheep.$store('cart').add(e);
}
//
function onBuy(e) {
if (!state.selectedSku.id) {
sheep.$helper.toast('请选择商品规格');
return;
}
sheep.$router.go('/pages/order/confirm', {
data: JSON.stringify({
items: [{
skuId: e.id,
count: e.goods_num
}],
// TODO 2
deliveryType: 1,
pointStatus: false,
}),
});
}
//
function onActivity() {
state.showActivityModel = true;
}
//
async function onGet(id) {
const { code } = await CouponApi.takeCoupon(id);
if (code !== 0) {
return;
}
uni.showToast({
title: '领取成功',
});
setTimeout(() => {
getCoupon();
}, 1000);
}
// TODO
const shareInfo = computed(() => {
if (isEmpty(state.goodsInfo)) return {};
return sheep.$platform.share.getShareInfo({
title: state.goodsInfo.name,
image: sheep.$url.cdn(state.goodsInfo.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;
//
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 '@/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 "@/api/promotion/seckill";
import SpuApi from "@/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 '@/api/product/category';
import SpuApi from '@/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>

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

@ -0,0 +1,91 @@
<!-- 首页支持店铺装修 -->
<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 '@/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>

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