项目初始化

This commit is contained in:
jerry
2025-01-21 01:46:34 +08:00
parent 364021b042
commit 48153e7761
962 changed files with 172070 additions and 0 deletions

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 @芋艿签到这里改成已累计签到改版接入 sheepjs -->
<view class="bg-white ss-m-t-16 ss-p-t-30 ss-p-b-60 ss-p-x-40">
<view class="activity-title ss-m-b-30">签到说明</view>
<view class="activity-des">1已累计签到{{state.signInfo.totalDay}}</view>
<view class="activity-des">
2据说连续签到第 {{ state.maxDay }} 天可获得超额积分一定要坚持签到哦~~~
</view>
</view>
</view>
<!-- 签到结果弹窗 -->
<su-popup :show="state.showModel" type="center" round="10" :isMaskClick="false">
<view class="model-box ss-flex-col">
<view class="ss-m-t-56 ss-flex-col ss-col-center">
<text class="cicon-check-round"></text>
<view class="score-title">
<text v-if="state.signResult.point">{{ state.signResult.point }} 积分 </text>
<text v-if="state.signResult.experience"> {{ state.signResult.experience }} 经验</text>
</view>
<view class="model-title ss-flex ss-col-center ss-m-t-22 ss-m-b-30">
已连续打卡 {{ state.signResult.day }}
</view>
</view>
<view class="model-bg ss-flex-col ss-col-center ss-row-right">
<view class="title ss-m-b-64">签到成功</view>
<view class="ss-m-b-40">
<button class="ss-reset-button confirm-btn" @tap="onConfirm">确认</button>
</view>
</view>
</view>
</su-popup>
</s-layout>
</template>
<script setup>
import sheep from '@/sheep';
import { onReady } from '@dcloudio/uni-app';
import { reactive } from 'vue';
import SignInApi from '@/sheep/api/member/signin';
const headerBg = sheep.$url.css('/static/img/shop/app/sign.png');
const state = reactive({
loading: true,
signInfo: {}, // 签到信息
signConfigList: [], // 签到配置列表
maxDay: 0, // 最大的签到天数
showModel: false, // 签到弹框
signResult: {}, // 签到结果
});
// 发起签到
async function onSign() {
const { code, data } = await SignInApi.createSignInRecord();
if (code !== 0) {
return;
}
state.showModel = true;
state.signResult = data;
// 重新获得签到信息
await getSignInfo();
}
// 签到确认刷新页面
function onConfirm() {
state.showModel = false;
}
// 获得个人签到统计
async function getSignInfo() {
const { code, data } = await SignInApi.getSignInRecordSummary();
if (code !== 0) {
return;
}
state.signInfo = data;
state.loading = false;
}
// 获取签到配置
async function getSignConfigList() {
const { code, data } = await SignInApi.getSignInConfigList();
if (code !== 0) {
return;
}
state.signConfigList = data;
if (data.length > 0) {
state.maxDay = data[data.length - 1].day;
}
}
onReady(() => {
getSignInfo();
getSignConfigList();
});
// TODO 芋艿1css 需要优化例如说引入的图片2删除多余的样式
</script>
<style lang="scss" scoped>
.header-box {
border-top: 2rpx solid rgba(#dfdfdf, 0.5);
}
// 日历
.calendar {
background: #fff;
.sign-everyday {
height: 100rpx;
background: rgba(255, 255, 255, 1);
border: 2rpx solid rgba(223, 223, 223, 0.4);
.sign-everyday-title {
font-size: 32rpx;
color: rgba(51, 51, 51, 1);
font-weight: 500;
}
.sign-num-box {
font-size: 26rpx;
font-weight: 500;
color: rgba(153, 153, 153, 1);
.sign-num {
font-size: 30rpx;
font-weight: 600;
color: #ff6000;
padding: 0 10rpx;
font-family: OPPOSANS;
}
}
}
// 年月日
.bar {
height: 100rpx;
.date {
font-size: 30rpx;
font-family: OPPOSANS;
font-weight: 500;
color: #333333;
line-height: normal;
}
}
.cicon-back {
margin-top: 6rpx;
font-size: 30rpx;
color: #c4c4c4;
line-height: normal;
}
.cicon-forward {
margin-top: 6rpx;
font-size: 30rpx;
color: #c4c4c4;
line-height: normal;
}
// 星期
.week {
.week-item {
font-size: 24rpx;
font-weight: 500;
color: rgba(153, 153, 153, 1);
flex: 1;
}
}
// 日历表
.myDateTable {
display: flex;
flex-wrap: wrap;
.dateCell {
width: calc(750rpx / 7);
height: 80rpx;
font-size: 26rpx;
font-weight: 400;
color: rgba(51, 51, 51, 1);
}
}
}
.is-sign {
width: 48rpx;
height: 48rpx;
position: relative;
.is-sign-num {
font-size: 24rpx;
font-family: OPPOSANS;
font-weight: 500;
line-height: normal;
}
.is-sign-image {
position: absolute;
left: 0;
top: 0;
width: 48rpx;
height: 48rpx;
}
}
.cell-num {
font-size: 24rpx;
font-family: OPPOSANS;
font-weight: 500;
color: #333333;
line-height: normal;
}
.cicon-title {
position: absolute;
right: -10rpx;
top: -6rpx;
font-size: 20rpx;
color: red;
}
// 签到按钮
.sign-box {
height: 140rpx;
width: 100%;
.sign-btn {
width: 710rpx;
height: 80rpx;
border-radius: 35rpx;
font-size: 30rpx;
font-weight: 500;
box-shadow: 0 0.2em 0.5em rgba(#ff6000, 0.4);
background: linear-gradient(90deg, #ff6000, #fe832a);
color: #fff;
}
.already-btn {
width: 710rpx;
height: 80rpx;
border-radius: 35rpx;
font-size: 30rpx;
font-weight: 500;
}
}
.model-box {
width: 520rpx;
// height: 590rpx;
background: linear-gradient(177deg, #ff6000 0%, #fe832a 100%);
// background: linear-gradient(177deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
border-radius: 10rpx;
.cicon-check-round {
font-size: 70rpx;
color: #fff;
}
.score-title {
font-size: 34rpx;
font-family: OPPOSANS;
font-weight: 500;
color: #fcff00;
}
.model-title {
font-size: 28rpx;
font-weight: 500;
color: #ffffff;
}
.model-bg {
width: 520rpx;
height: 344rpx;
background-size: 100% 100%;
background-image: v-bind(headerBg);
background-repeat: no-repeat;
border-radius: 0 0 10rpx 10rpx;
.title {
font-size: 34rpx;
font-weight: bold;
// color: var(--ui-BG-Main);
color: #ff6000;
}
.subtitle {
font-size: 26rpx;
font-weight: 500;
color: #999999;
}
.cancel-btn {
width: 220rpx;
height: 70rpx;
border: 2rpx solid #ff6000;
border-radius: 35rpx;
font-size: 28rpx;
font-weight: 500;
color: #ff6000;
line-height: normal;
margin-right: 10rpx;
}
.confirm-btn {
width: 220rpx;
height: 70rpx;
background: linear-gradient(90deg, #ff6000, #fe832a);
box-shadow: 0 0.2em 0.5em rgba(#ff6000, 0.4);
border-radius: 35rpx;
font-size: 28rpx;
font-weight: 500;
color: #ffffff;
line-height: normal;
}
}
}
//签到说明
.activity-title {
font-size: 32rpx;
font-weight: 500;
color: #333333;
line-height: normal;
}
.activity-des {
font-size: 26rpx;
font-weight: 500;
color: #666666;
line-height: 40rpx;
}
.reward {
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEsAAAA4CAYAAAC1+AWFAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA3ZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTQyIDc5LjE2MDkyNCwgMjAxNy8wNy8xMy0wMTowNjozOSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDowYWFmYjU3Mi03MGJhLTRiNDctOTI2Yi0zOThlZDkzZDkxMDkiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6RUEyNzZEMjZEMDFCMTFFOEIzQzhEMjMxNjI1NENDQjciIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6RUEyNzZEMjVEMDFCMTFFOEIzQzhEMjMxNjI1NENDQjciIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTggKFdpbmRvd3MpIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6ZmRjNTM0MmUtNmFkOC1iMDRhLThjZTEtMjk2YWYzM2FkMmUxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjBhYWZiNTcyLTcwYmEtNGI0Ny05MjZiLTM5OGVkOTNkOTEwOSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PuGQ9m8AAAf3SURBVHja7Jxbb9xEFMe9Xu81u6VJL0kqSCToZ0CCNyp4CQ0FqVAkQAgeEE98EPgEwBOCSoCEQPTCG30BVSpfAaQ2KQ1tc4Fm77v2cv7jmc3UGdvjsTdrJEY68m527Zn5zZlzszeF8XhspWl37/xhZdwK/Di2jqA9ufqM9ndtKz+tTvIpSYdkl7+u52h8lpMjUFdIzvH3VZIPSM6SrHOAM292DkHJ7Rz/rP4/rGhQuQNm5xxUroAVNm//PhUnQ7JEco9LGlBy+znGhsX1O1vNajYbjwm1F0l+JfmN5BeSlwKfx4KybTtWw6TriXao39xpFga8v98q08tPSN7Dn0yvVSqVrEqlbBUKBQvxX78/sIbDoenlxhzs6ySDPNmsj0k+SguqWq0wUGwV6Yj35XIpTXD7Csk3eTPw76c5GUAARtUqlVTA0C5wOzd7WLQF19JoFEAASFRLCQwa9hXJ2rQj+GdJ3sCcQj4vkrw7TVAyMLTBwMiGHSP5luQLEjfkO0O+ZW+ZGPjnuIE8YQoDEyyVnIkdmmaDQxgOR+QU+mku85DkZe5RE23D19KCguYcBSjhEHxNLStBarZTJBdNtuGcqlNVx6q4CBo1iwavipDjsUk6Rdq+I0u1brZdkKpCrDVMYLlBUMVikQ0mGM4EBydWeiYpiaLfWq1GwEbKUAwQPc+Vz3NTlWgEqHq9xo7BpoKVpwbNDwtNbHto9XpdAjaOXWBtWNhWAOV53lQnBiPdarWs+fnjmRr/MLtVLjvUp2O57tCK2wwJ4qxCyECyhXX//n3r3tZWxp4yel4+pPiJRMHydAbR7XYzm5Trutbu3p41pFjq0aNHmV0XY0ywqJ4JrIoOKEwwq7a9s2M5RYe5/+3tnUwXIQGwSlJYyKfeMgVlkprAFu7t/c3OLZfLLLhst9vGmUEKYG/x+WvBQgT7ZTDfE8ZPB5RuCiO33d095o0cx2HhCeThw+1UAbEusIAXbPL5r8XBWuf5UVP2JNgWGLzrelMBhT72yFaVCFSRBK6+SNux1+sxmSYwaDQ0GfOTPGaTc1gPyw2R3nwdTJoRMiCow4U6HXNQKOJ1uz0KAvssGR4MBuw4Gg2ZHyqX/O0HzRKTwHcQw/mhCwLisj8x9EVH9FetViOhYTurkm+EQZgXlAqahf4wvtFoJGsaAshLJD/IcdYaJ1mSV9uvM/mD6fX6oaAANE6jcO6dOxtsUEVKP+yCzVINaJJDAwckTEAu/mG1cRyNXBZlQ8vanY7lkYa77shaWVmJhYVxiSQ7OB6ARLCKz6HNtVqVaR364+Mocy6vkvwEzUKR/6blF/snoDB4ROyixLu/3wodUKMxp5XeQLs2NzdZtGwXAazABolzZZHH4SGgJMCAjF0CaJClpSWr2Wxqb/NWS+0s5Dq+r2Eu30GePJZNkuehWSsyKFmrsPKYWFwqoZsHQlNWV1etra2/2ORlUGF5XhGLxaFiXK5XsM6cXmZbKEm+iPOD2UewXz+tI013SqRdfTmifwqMYOA3SO7GlTUOJ9AHBjLJwyXYamfOLDO7k+Q88c3lpcVEoCYaqkjThH3USI3AZ8PmLz7klcIJcRhFWRWx91VJtDCiSRPbpcVFZqy9iLxNngBGsUigTLytanyYS/Bavn0cMacjadWQ87knQodr3OoPxEm+i+2xFfG3il/qUAEzqVDimsePP0HG2o2EJbRibq7OPKYJqKBxlz2hvIAHc55UIIQ3vBaMs77nVcK2TBkns5WNAQZNTAoM13Y1qhjCrZuACoYNKlAHc+1OlINzuMi5KIPSK5xkS1wEHgzeQUfDkgLr9roJwHanBgoaFfCALc7hSly6A5V7h2RfqCc6hSofxEDZAOt1+4e2GzTN46/F9hQrr5u0q0AdhEKHzQGCXym22ufzv6abSCNivRzMC4O5ol+udZTA9Ap9AxZrjflWw7VOnzrJPF61WmNwvIkJsLW1K9g/dod4LCDchk5eXhYRe5JKaax6IA7DILDqJvZEBJ0O+br5hXlmxEU7eXKBJt2gnPEf2iIdNhlkEY1Gw6D6Opgshka40jcpK8dWUYXhN2mwEdCWEwsL1rFjTeV1EFosLp5ixv3BgwcMmsndueXlZV9L9UridqoafFwIYNKQ0509+3TU40WThpxtdXWFwzKrwWdxtykBrPFkb2dRdweApK1erxvDCltoP2IPv8eQGJbwiOK+4YxuCabS/jDNQgkINldDwSNhFVWBIaDJ/YatWlaqn4UWAYbqoTh8FZ8Fxlo0gdUOrg6LgxSxjsruIC5L+VyVUVNBYZmCMkYrMI0KLGrLxOMhzN9RlTqCEhUYjsfjI9MoPyAeKNMl1bgVzzmg6P+diWbd5BXUS3IFVbFV8XzWXBiwqGg+ac0+rESs6bGxU1I9nxVn4G9FnSylR1e13ElIpK0DzBQUb0hh3iS5nkZ7s3i0+zqHZdR0csmUoJBevJ0WVCaweLuYZjBRwFKC8jioH7OYZCawSLsGJLg5i/tswWwX79fp8wKE27cbKmDI/YRDwBHvFaBw7px0vUJEvxesHD7aLaBd5QPc4H+6jff87+I7yFnOq4DB7eMuTLvdYbfuFWEAzjnPryE3Zb9pzIPSUUzxtzvLJH9a4b+hqfPJvKB5zRsccidlv+awZvyzX11gOqDMVvU/9LPf0C15FKBmarOmACw3oPICSwb2OQlu4+Cxv8/yBCoPNitsAcdWDv9Vwb8CDACdCFE+P8dk8gAAAABJRU5ErkJggg==');
width: 75rpx;
height: 56rpx;
}
.rewardTxt {
width: 74rpx;
height: 32rpx;
background-color: #f4b409;
border-radius: 16rpx;
font-size: 20rpx;
color: #a57d3f;
line-height: 32rpx;
}
.venusSelect {
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADgAAAA4CAYAAACohjseAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA3ZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTQyIDc5LjE2MDkyNCwgMjAxNy8wNy8xMy0wMTowNjozOSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDowYWFmYjU3Mi03MGJhLTRiNDctOTI2Yi0zOThlZDkzZDkxMDkiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6QzkwRkI4NEFEMDFCMTFFODhDNDdBMDVGOTBBN0U2NTQiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6QzkwRkI4NDlEMDFCMTFFODhDNDdBMDVGOTBBN0U2NTQiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTggKFdpbmRvd3MpIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6ZmRjNTM0MmUtNmFkOC1iMDRhLThjZTEtMjk2YWYzM2FkMmUxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjBhYWZiNTcyLTcwYmEtNGI0Ny05MjZiLTM5OGVkOTNkOTEwOSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PkX00M0AAAfVSURBVHjaxFpdcBNVFD67STbppn9poU0LtSBYykBlgKrjqKOtihbq8ICKILyJwxM+6fjCk2/6pC+O4hs+6fgAbWFw1OID+ICMAwwKlRmKLW3Tpvlrskk3m13Pvdm0NNn//HA73+ymd3Pv+e4595xzT5bJ3DgJNWyMelUqNaD36a8N+9kaEeMR3yAERES952sxsbtya2lIbgQxqH72IT5EbEW8pZKuHkGlugyLyT3aBtW+qpJkKb/qgEeMIAYNnhlUn+Edz2NuokqtNVdTTbqVyhO0Q67qJMt2MnXbTq/cp+9+ZkyOYYFxN4EiLaF5SbokccyKkWRS1z4we4ZDfIE4hmhxvJLNu8HTtg85ekGRRcjOXwIp9pfT4aKIs4iP+f4zYrl78HPEqbLMJNAPXHBI/SQjSTd+PoD3LpCi15wMGVBlSiM+NfSihJ8Jjlt4Rheu5n7wtL+B93IJPO37aH8Z45+ohBdtKUtz7a+jJLK+/dN+BTX5p5MpWh6HF6XNE9iLwr9mSG6VZP65bPR6NVI1BwQZF3BtA+Bq2oG3Pt3HFCVnfUHaX6XQHCeXgVz8Nojz4+SDzTgo2yfIBV9G89utzi5DtRvDcnQ+JSciyd+rH+hdjb22tFOp5mreCUrocg0CPet5LATJvHbldWSiGllIzZpdeR2ZqFPtZTNJSIQeQGv3DucEbcrLmkRSXvP/cs4RZm6Ow/T1McffpyiSJTZ+jDfOZFDlOuARo5p9aKJ2IYkpCN+9AmLsIcSm/3Y0BkWpPCPRX9/nDVI1BTTAI0YRA9r99gWbv3MF90MGvG4WQrfHnRMslWcQMRL55SivbaJk064FjxhFDGj0gad5u22zkrMZiExcBQ7JcW4GlhfuQ3L+gSMTzc9fItcgYiTy8xHeTIOGmnMHtoEn2G971cP3MAUTk+B2MeBBkh4kGbp92ZEGyfxEDj1NLl56j9fbg4U9N6C179zNPZhK7cV7yRZkSYTI3atIjgUXywCLINf03L8gRGZtj0dA5CDyaMg5SPZk+OLhFZLMwtg7hTLDKGJAM09s6QGuY6+upxLTKcgkFmE5FUdFIYQEiHgv4VURl4BjgZon0SA9EeKKi1kZliU8Nnn84OabwIPw+huB8zdR+OqbwdcYMAwB4ux1yEYmtLp+I5WBdft/EApx8Cs9cu6mblyxXXTl9FpOFGDyjzGQMwlwIQlUEDAMagqvhBTVHl4ZplDezpsq+UdOTkMuIcByfBYElCWHmsgpLHQ/NwTe+gaTBH0XWkgGpPgDrfLHl4gTTOj8IVLdTqkF2aIslwF+2zCeGDjzIJ5OwtSVc6Cko9QMCUmWyZMiZJn8cI8E7Lwm11wJOTypBPuHoCG4yVrgz2VBmFBj69pGTvv1hVSN0XSxHE8LRUbaW9G01wdPvHgQQtcugJwMU5LFpIrWDjXMUGIsMNRR5DwcBPcMQV1L0NKchYHIkU2WkiVrTvgT6XEjyN/TY08R5KyAaWc6n3tagMvjhuCzQ+jlOmylVPRZjw/an9kPdYF1lucjIPLJmERoyP9j+8GflIIXPYW4XOKVJAmWZ27Y8nAMaq5t9yB4WjeumJ4hOaIErx/W978JXH2TbY9K5ZNKMhzC5dSjyTapQ5IyFxozvLJGz9Hp/Am+Y7utH8kan+yDhYVp8lVgdDWX33f+ji3gruNpnLPTxNl/8vKtbeTAeKDj0DmhuGShT3Jxkp4guGCv5cnT0QX0hgq4dOnBSshIxxehUbZJbu4OSJEpbXJvnxf0zoP6JMP/keVGTfZYEiCDQlttmXjE1hlTnJ3A+Ketuc53RwSz86AuSXFxitYzueBWUyGWE9E1pqiozoRhXdQJMLAaEyUMMdJyGp2Ux4Lm7iG5h5rkNhweFUpLFtonZANNTmOA3WzmFiGLGY3XlSel0DpOCzRs6gWXj4fkgwnIhKZIcKDhgsTLTCICfMs683gb1tbchiNjgt0TfYFkym7JQkyl8nkoixkMesiGzT1Q19q20t+0dTvwnU/A0iQSDYeoNinBQLOTE/2BjUcvCE5rMoJ2XcSYYBr3FOPmoHHLU+APdtJMpvg7bp8XAr19mKJ1weK9u5CJxUDpMt+HxfJ2HbsoVLzwa1aT4fw8dD3/Au43lv7YYjQF19gAHXv6UYMJa7UepQZFJzMT9fp9lJidorCvgbfkSRXbBB2UDRX5MdREnZYNwZECRcxQXLUnl8s5KPw6MNFsdBFzzdaaE8xGwrUx0eXZORrw3c1NNdEk0ZwUi2OQn7fvZBz9fEZKDjNzFLqn7dYApnVtNtKvecx5oxVfHCsmSt4ts/0rrxiO5NO6jvUWyC0guZgT+SPllu4Jzjr9AT0bjqKWQ1qH0RWQfvKcwzm+s6BB01X6RC1pHIf82w02NRmnsng7WjX28iJqLu5Ec4XXSE5XYg+S91A+UlHS/H2riXfq1n3N8mM2HKOOgutsodkNqZKIMxGQokvFw40jhnFMoZZ70CzyrpLd2S0kb00Oa5KMJDC8LAHL4QEmK4HGKYaSq+/bJFTyZ/GyX+VK3pzUStA1SRJrkTNZrWHG1e8IGuMZt5eqrUH9U8iwUbVci1w1BKnW65RWSVaVnBomAKoI3E9IQEEipX3jap9Q1hxmBElBocp/AmIYcQaRQSQQ36r/E8odvepOxoa5khfRT1pf+8q0/wUYAFU/P0XyeZQPAAAAAElFTkSuQmCC');
}
.venus {
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADgAAAA4CAYAAACohjseAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA3ZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTQyIDc5LjE2MDkyNCwgMjAxNy8wNy8xMy0wMTowNjozOSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDowYWFmYjU3Mi03MGJhLTRiNDctOTI2Yi0zOThlZDkzZDkxMDkiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6RDQ3N0E3RkJEMDFCMTFFODhGODVBOTc1NEM5Nzg2NzkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6RDQ3N0E3RkFEMDFCMTFFODhGODVBOTc1NEM5Nzg2NzkiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTggKFdpbmRvd3MpIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6ZmRjNTM0MmUtNmFkOC1iMDRhLThjZTEtMjk2YWYzM2FkMmUxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjBhYWZiNTcyLTcwYmEtNGI0Ny05MjZiLTM5OGVkOTNkOTEwOSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PnZClGIAAAT7SURBVHja7JtLcxtFEMdnn1rJUhIrTiS7KLsK/EXwNck3gCMcuUFx4QvACW48buHGMZxJPkiowpjC2LElcKzXSqul/6OdoOB9zezsCCimqmvXtdqd+U33dPf0rq2Tn54zg81KjrGuB75x8FbuddsQWIvkS5IxySA5b5no2DUE94TkKPk7IHmf5JDkUQJdW7MNw623o+Ra698KmAdnDNLeIJwRSHvDcLVDVnYynU771fnLl9eFcLZts+VymQf5iJ6pzfFYJeKgT/IZyTskXdWOPM9jjYbPLMticRyz2Sxk8/lc9XFDksckH1IcDKtq8FOSD6rMIuCCoPHXrBIk/qYDC0MlyO1kTBOSj6uuwXerwPn+63DrrdFo8OsV2ns6nEy3Chwg8lpFyK4OwNrgNGpSvxfFoDzP5etJR8PzsiYETmk+X5BjmpkBrHPGU109TeKqv5X3rT3QQ3ObaPDGRjIZXWZpol+b/cebUUA4iuHw938UoNbk9+zsjP16eqoV4JfjH1uqgLjxe10DiaKIDYZDNqfU7OrqSifjkzxIuwDubV2juLi8ZK7j8oT74uJSJ+BRHqQtC6cS/7A9wtrDvb7v84A9Go2UMyQZSFsWrmz6td4GgyF38a7r8lgGefHiQjnjkYG064ZDmjWktecRnEOCDa9DpjqdTrnUDSk2vJXgsHGdTKa0t5vx/V0Yhvy4WMx5hdf3VqYJDQqTxW+QdmECPM8h8flvPPRFR/QXBEEuKEw9Yz/5AyoDtBkei5zriyw4pGVFmoOHPD7+mQ/ccR1mWzZpyuIacx2Hgzl0FJkIjjBTHBeLiO6LuDZH4zFbRkt63oLt7+8XAmJcIhFP0eTn2C9CgzBTrPjUp7XbW6VSJGjx5OSEBhsz2wGkxc0R967LuukuSWKaFEwMnXJQSL/fZ51Op/QSuL5OdVjY7beFBq2sAlHZ/A8aOTg4YKenv/EBr8Nl5ZUO6jPJRGCg0dJie/d3WbPZlMpPMwpZsN0Y2sOVb7PcOzou22CGe3u7fB3J3Cd+udvvScG9soT0Kt13tAZj4UVRwHmWtZClcj+azX6vxx0GN8ECUFyHjnsEp+KlM8b3TBTKBCDqkA/SIFV20jCbO3duk8OIcgHF7G9ttbinVYFLcTBgeAAP+vc4mAkJVywLibARpZvOjWWA36rApYSI1+DSMhltkJPpRGIyJrXAZeWiWiCnk9kNU4RGl8m5MN1VLFzwWKobLm83kQtZbnMb8lgYJ2aIYH//3g73lEHQ5ECAXYUSu7QWZeCKqmoCcqSyPkQgd8lHbne3uSMRbWenSwNtU476BxtT9oJQOZ3OKKloq6SmmXBlyoZKb3nG4wnXyt1ul9261UkN9ggjvd497mDOz885KGN3pfvKg1OuixY15JCHh2/ymFjUms2AMqD9BFB/qwUQg5ZtrVY9b7H/LxtmZSCbaCr9KgGmpEeG6qpzM2tQBHudb5eKNLfKiUMzgAIyL6uRreHklB9qX4MDlQfLpHUV4AY6AB+rzl4ZyIqa+0aHiX6UlDTwMcK2CqQoEGmEE5+RfFK4N636vWjKh0Cp5ceS38k8JXko8yHQ7W7PXKBPBvYwGegNF4/q12g05mV7HXClqgs1ffEr+/LmaTIx0nCb+uI3U5M64Tadi5aBrBXORLKdB1k7nKndhID8GqUaErze/coEXJ1OJm9CY2bw3wr+FGAAoa6PIUihovYAAAAASUVORK5CYII=');
background-repeat: no-repeat;
background-size: 100% 100%;
width: 56rpx;
height: 56rpx;
margin: 10rpx 0;
}
.num {
font-size: 24rpx;
font-family: 'Guildford Pro';
}
.item {
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #eee;
height: 130rpx;
}
.reward {
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEsAAAA4CAYAAAC1+AWFAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA3ZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTQyIDc5LjE2MDkyNCwgMjAxNy8wNy8xMy0wMTowNjozOSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDowYWFmYjU3Mi03MGJhLTRiNDctOTI2Yi0zOThlZDkzZDkxMDkiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6RUEyNzZEMjZEMDFCMTFFOEIzQzhEMjMxNjI1NENDQjciIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6RUEyNzZEMjVEMDFCMTFFOEIzQzhEMjMxNjI1NENDQjciIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTggKFdpbmRvd3MpIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6ZmRjNTM0MmUtNmFkOC1iMDRhLThjZTEtMjk2YWYzM2FkMmUxIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjBhYWZiNTcyLTcwYmEtNGI0Ny05MjZiLTM5OGVkOTNkOTEwOSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PuGQ9m8AAAf3SURBVHja7Jxbb9xEFMe9Xu81u6VJL0kqSCToZ0CCNyp4CQ0FqVAkQAgeEE98EPgEwBOCSoCEQPTCG30BVSpfAaQ2KQ1tc4Fm77v2cv7jmc3UGdvjsTdrJEY68m527Zn5zZlzszeF8XhspWl37/xhZdwK/Di2jqA9ufqM9ndtKz+tTvIpSYdkl7+u52h8lpMjUFdIzvH3VZIPSM6SrHOAM292DkHJ7Rz/rP4/rGhQuQNm5xxUroAVNm//PhUnQ7JEco9LGlBy+znGhsX1O1vNajYbjwm1F0l+JfmN5BeSlwKfx4KybTtWw6TriXao39xpFga8v98q08tPSN7Dn0yvVSqVrEqlbBUKBQvxX78/sIbDoenlxhzs6ySDPNmsj0k+SguqWq0wUGwV6Yj35XIpTXD7Csk3eTPw76c5GUAARtUqlVTA0C5wOzd7WLQF19JoFEAASFRLCQwa9hXJ2rQj+GdJ3sCcQj4vkrw7TVAyMLTBwMiGHSP5luQLEjfkO0O+ZW+ZGPjnuIE8YQoDEyyVnIkdmmaDQxgOR+QU+mku85DkZe5RE23D19KCguYcBSjhEHxNLStBarZTJBdNtuGcqlNVx6q4CBo1iwavipDjsUk6Rdq+I0u1brZdkKpCrDVMYLlBUMVikQ0mGM4EBydWeiYpiaLfWq1GwEbKUAwQPc+Vz3NTlWgEqHq9xo7BpoKVpwbNDwtNbHto9XpdAjaOXWBtWNhWAOV53lQnBiPdarWs+fnjmRr/MLtVLjvUp2O57tCK2wwJ4qxCyECyhXX//n3r3tZWxp4yel4+pPiJRMHydAbR7XYzm5Trutbu3p41pFjq0aNHmV0XY0ywqJ4JrIoOKEwwq7a9s2M5RYe5/+3tnUwXIQGwSlJYyKfeMgVlkprAFu7t/c3OLZfLLLhst9vGmUEKYG/x+WvBQgT7ZTDfE8ZPB5RuCiO33d095o0cx2HhCeThw+1UAbEusIAXbPL5r8XBWuf5UVP2JNgWGLzrelMBhT72yFaVCFSRBK6+SNux1+sxmSYwaDQ0GfOTPGaTc1gPyw2R3nwdTJoRMiCow4U6HXNQKOJ1uz0KAvssGR4MBuw4Gg2ZHyqX/O0HzRKTwHcQw/mhCwLisj8x9EVH9FetViOhYTurkm+EQZgXlAqahf4wvtFoJGsaAshLJD/IcdYaJ1mSV9uvM/mD6fX6oaAANE6jcO6dOxtsUEVKP+yCzVINaJJDAwckTEAu/mG1cRyNXBZlQ8vanY7lkYa77shaWVmJhYVxiSQ7OB6ARLCKz6HNtVqVaR364+Mocy6vkvwEzUKR/6blF/snoDB4ROyixLu/3wodUKMxp5XeQLs2NzdZtGwXAazABolzZZHH4SGgJMCAjF0CaJClpSWr2Wxqb/NWS+0s5Dq+r2Eu30GePJZNkuehWSsyKFmrsPKYWFwqoZsHQlNWV1etra2/2ORlUGF5XhGLxaFiXK5XsM6cXmZbKEm+iPOD2UewXz+tI013SqRdfTmifwqMYOA3SO7GlTUOJ9AHBjLJwyXYamfOLDO7k+Q88c3lpcVEoCYaqkjThH3USI3AZ8PmLz7klcIJcRhFWRWx91VJtDCiSRPbpcVFZqy9iLxNngBGsUigTLytanyYS/Bavn0cMacjadWQ87knQodr3OoPxEm+i+2xFfG3il/qUAEzqVDimsePP0HG2o2EJbRibq7OPKYJqKBxlz2hvIAHc55UIIQ3vBaMs77nVcK2TBkns5WNAQZNTAoM13Y1qhjCrZuACoYNKlAHc+1OlINzuMi5KIPSK5xkS1wEHgzeQUfDkgLr9roJwHanBgoaFfCALc7hSly6A5V7h2RfqCc6hSofxEDZAOt1+4e2GzTN46/F9hQrr5u0q0AdhEKHzQGCXym22ufzv6abSCNivRzMC4O5ol+udZTA9Ap9AxZrjflWw7VOnzrJPF61WmNwvIkJsLW1K9g/dod4LCDchk5eXhYRe5JKaax6IA7DILDqJvZEBJ0O+br5hXlmxEU7eXKBJt2gnPEf2iIdNhlkEY1Gw6D6Opgshka40jcpK8dWUYXhN2mwEdCWEwsL1rFjTeV1EFosLp5ixv3BgwcMmsndueXlZV9L9UridqoafFwIYNKQ0509+3TU40WThpxtdXWFwzKrwWdxtykBrPFkb2dRdweApK1erxvDCltoP2IPv8eQGJbwiOK+4YxuCabS/jDNQgkINldDwSNhFVWBIaDJ/YatWlaqn4UWAYbqoTh8FZ8Fxlo0gdUOrg6LgxSxjsruIC5L+VyVUVNBYZmCMkYrMI0KLGrLxOMhzN9RlTqCEhUYjsfjI9MoPyAeKNMl1bgVzzmg6P+diWbd5BXUS3IFVbFV8XzWXBiwqGg+ac0+rESs6bGxU1I9nxVn4G9FnSylR1e13ElIpK0DzBQUb0hh3iS5nkZ7s3i0+zqHZdR0csmUoJBevJ0WVCaweLuYZjBRwFKC8jioH7OYZCawSLsGJLg5i/tswWwX79fp8wKE27cbKmDI/YRDwBHvFaBw7px0vUJEvxesHD7aLaBd5QPc4H+6jff87+I7yFnOq4DB7eMuTLvdYbfuFWEAzjnPryE3Zb9pzIPSUUzxtzvLJH9a4b+hqfPJvKB5zRsccidlv+awZvyzX11gOqDMVvU/9LPf0C15FKBmarOmACw3oPICSwb2OQlu4+Cxv8/yBCoPNitsAcdWDv9Vwb8CDACdCFE+P8dk8gAAAABJRU5ErkJggg==');
width: 75rpx;
height: 56rpx;
}
.on {
background-color: #999 !important;
}
</style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

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

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

View File

@@ -0,0 +1,183 @@
<template>
<view>
<view class="form-item">
<view class="label">上传头像</view>
</view>
<view class="upload-box">
<!-- <button v-if="mp_is_new" class="avatar-box" open-type="chooseAvatar" @chooseavatar="onChooseAvatar">
<u-avatar size="180" :src="avatarUrl"></u-avatar>
<view class="icon">
<u-icon name="camera" color="#fff" size="30"></u-icon>
</view>
</button> -->
<view class="avatar-box" @click="chooseImage">
<u-avatar size="180" :src="avatarUrl"></u-avatar>
<view class="icon">
<u-icon name="camera" color="#fff" size="30"></u-icon>
</view>
</view>
</view>
</view>
</template>
<script>
import FileApi from '@/sheep/api/infra/file';
export default {
components: {
},
props: {
modelValue: {
type: String,
default: ''
},
},
data() {
return {
mp_is_new: false,
}
},
created() {
// #ifdef MP-WEIXIN
const version = uni.getSystemInfoSync().SDKVersion;
if(this.compareVersion(version, '2.21.2') >= 0){
this.mp_is_new = true;
}
// #endif
},
computed: {
avatarUrl() {
return this.modelValue;
},
},
watch: {
},
methods: {
/**
* 小程序比较版本信息
* @param v1 当前版本
* @param v2 进行比较的版本
* @return boolen
*
*/
compareVersion(v1, v2) {
v1 = v1.split('.')
v2 = v2.split('.')
const len = Math.max(v1.length, v2.length)
while (v1.length < len) {
v1.push('0')
}
while (v2.length < len) {
v2.push('0')
}
for (let i = 0; i < len; i++) {
const num1 = parseInt(v1[i])
const num2 = parseInt(v2[i])
if (num1 > num2) {
return 1
} else if (num1 < num2) {
return -1
}
}
return 0
},
//选照片 or 拍照
chooseImage() {
uni.chooseImage({
count: 1, //默认9
sourceType: ['album', 'camera'],
sizeType: ['original', 'compressed'], //可以指定是原图还是压缩图,默认二者都有
success: (res) => {
for (let i = 0; i < res.tempFilePaths.length; i++) {
uni.getImageInfo({
src: res.tempFilePaths[i],
success: (image) => {
this.uploadImage(image.path);
}
});
}
}
});
},
uploadImage(path) {
FileApi.uploadFile(path).then((res) => {
this.$emit('update:modelValue', res.data);
});
},
// 微信头像获取
onChooseAvatar(e) {
const {
avatarUrl
} = e.detail
this.uploadImage(avatarUrl);
},
}
}
</script>
<style lang="scss" scoped>
.form-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 15px;
.label {
font-size: 30rpx;
min-width: 200rpx;
}
}
.upload-box {
padding: 15px;
display: flex;
justify-content: center;
align-items: center;
padding-top: 0;
.avatar-box {
display: flex;
justify-content: center;
align-items: center;
position: relative;
.icon {
position: absolute;
right: 0;
bottom: 5px;
background-color: var(--ui-BG-Main);
width: 50rpx;
height: 50rpx;
display: flex;
justify-content: center;
align-items: center;
border-radius: 100%;
}
}
button{
padding: unset;
margin: unset;
border: unset;
position: relative;
line-height: unset;
background-color: unset;
font-size: unset;
color: unset;
border-radius: unset;
text-align: unset;
text-decoration: unset;
display: unset;
overflow: unset;
}
button::after{
border: none;
}
}
</style>

View File

@@ -0,0 +1,69 @@
<template>
<view>
<view class="form-item">
<view class="label">所在城市</view>
<u-input @click="citySelect" input-align="right" type="select" placeholder="请选择所在的城市" v-model="city" />
</view>
<!-- 省市区弹窗 -->
<su-region-picker :show="show" @cancel="show = false" @confirm="cityOk" />
</view>
</template>
<script>
export default {
components: {
},
props: {
modelValue: {
type: String,
default: ''
},
},
data() {
return {
show: false,
params: {
province: true,
city: true,
area: false
},
}
},
created() {
},
computed: {
city() {
return this.modelValue;
},
},
watch: {
},
methods: {
citySelect() {
this.show = true;
},
cityOk(e) {
this.$emit('update:modelValue', e.city_name);
this.show = false;
},
}
}
</script>
<style lang="scss" scoped>
.form-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 15px;
.label {
font-size: 30rpx;
min-width: 200rpx;
}
}
</style>

View File

@@ -0,0 +1,73 @@
<template>
<view>
<view class="form-item">
<view class="label">上传图片</view>
<view>{{imgList.length}}/{{number}}</view>
</view>
<view class="upload-box">
<shmily-drag-image :number="number" v-model="imgList"></shmily-drag-image>
</view>
</view>
</template>
<script>
export default {
components: {
},
props: {
number: {
type: Number,
default: 6
},
modelValue: {
type: Array,
default: []
},
},
data() {
return {
imgList: [],
}
},
created() {
this.imgList = this.modelValue;
},
computed: {
},
watch: {
imgList: {
handler: function(newVal, oldVal) {
this.$emit('update:modelValue', newVal);
}
},
modelValue: {
handler: function(newVal, oldVal) {
this.imgList = newVal;
}
},
},
methods: {
}
}
</script>
<style lang="scss" scoped>
.form-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 15px;
.label {
font-size: 30rpx;
min-width: 200rpx;
}
}
.upload-box {
padding: 15px;
}
</style>

View File

@@ -0,0 +1,90 @@
<template>
<view class="form-item">
<view class="label">性别</view>
<view class="radio-box">
<view @click="change(item)" class="text" :class="item.value == valueDom ? 'active' : ''" v-for="(item,index) in list">{{item.name}}</view>
</view>
</view>
</template>
<script>
export default {
components: {
},
props: {
modelValue: {
type: String,
default: ''
},
},
data() {
return {
list: [
{
name: '男',
value: '0',
},
{
name: '女',
value: '1',
},
],
}
},
created() {
},
computed: {
valueDom() {
return this.modelValue;
},
},
watch: {
},
methods: {
change(e) {
this.$emit('update:modelValue', e.value);
},
}
}
</script>
<style lang="scss" scoped>
.form-item {
background-color: #fff;
display: flex;
align-items: center;
justify-content: space-between;
padding: 15px;
.label {
font-size: 30rpx;
min-width: 200rpx;
}
}
.radio-box {
display: flex;
justify-content: space-between;
align-items: center;
.text {
width: 70rpx;
height: 70rpx;
display: flex;
align-items: center;
justify-content: center;
background-color: #ececec;
color: #949494;
border-radius: 100%;
margin-left: 10px;
}
.active {
color: #fff;
background-color: #949494;
}
}
</style>

View File

@@ -0,0 +1,255 @@
<template>
<view class="form-bg">
<view class="form-item">
<view class="label">录音</view>
<view class="bubble-box">
<u-input @click="topBubble" input-align="right" type="select" :placeholder="voice.name" />
<tui-bubble-popup :show="show" :mask="false" position="absolute" direction="right" triangleRight="-22rpx" triangleTop="30rpx" @close="topBubble" :flexEnd="false">
<view @click="change(item)" class="tui-menu-item" v-for="(item,index) in list">{{item.name}}</view>
</tui-bubble-popup>
</view>
</view>
<view>
<view class="voice-box" v-if="voice.type == 'voice'">
<view v-if="voiceUrl" @click="playAudio" class="upload-btn-box">
<view class="icon">
<u-icon v-if="play" name="pause" color="#fff" size="70"></u-icon>
<u-icon v-else name="play-right-fill" color="#fff" size="70"></u-icon>
</view>
<view class="upload-btn" v-if="play">停止播放</view>
<view class="upload-btn" v-else>播放录音</view>
</view>
<view v-else>
<all-speech ref="speech" @okClick="voiceOk"></all-speech>
</view>
<view v-if="voiceUrl" @click="reloadBtn" class="reload-btn">
<u-icon name="reload" size="30"></u-icon>
<text class="text">重录</text>
</view>
</view>
<view class="voice-box" v-if="voice.type == 'upload'">
<view v-if="voiceUrl" @click="playAudio" class="upload-btn-box">
<view class="icon">
<u-icon v-if="play" name="pause" color="#fff" size="70"></u-icon>
<u-icon v-else name="play-right-fill" color="#fff" size="70"></u-icon>
</view>
<view class="upload-btn" v-if="play">停止播放</view>
<view class="upload-btn" v-else>播放录音</view>
</view>
<view v-else @click="chooseVoice" class="upload-btn-box">
<view class="icon">
<u-icon name="plus" color="#fff" size="70"></u-icon>
</view>
<view class="upload-btn">上传录音文件</view>
</view>
<view v-if="voiceUrl" @click="reloadBtn" class="reload-btn">
<u-icon name="reload" size="30"></u-icon>
<text class="text">重录</text>
</view>
</view>
</view>
</view>
</template>
<script>
import FileApi from '@/sheep/api/infra/file';
import tuiBubblePopup from "@/components/thorui/tui-bubble-popup/tui-bubble-popup.vue"
const audio = uni.createInnerAudioContext();
export default {
components: {
tuiBubblePopup,
},
props: {
modelValue: {
type: String,
default: ''
},
},
data() {
return {
voice: {
},
list: [
{
name: '直接录音',
type: 'voice',
},
],
current: 0,
voicePath: '',
show: false,
play: false,
}
},
created() {
this.voice = this.list[this.current];
// #ifndef MP-WEIXIN
var voiceType = {
name: '上传手机音频',
type: 'upload',
};
this.list.push(voiceType);
// #endif
},
computed: {
voiceUrl(){
return this.modelValue;
}
},
methods: {
topBubble() {
this.show = !this.show;
},
change(e) {
this.reloadBtn();
this.topBubble();
this.voice = e;
},
reloadBtn() {
this.play = false;
this.$emit('update:modelValue', "");
this.$emit('sec', "");
},
playAudio() {
if(this.play){
this.play = false;
audio.stop();
}else{
this.play = true;
//语音自然播放结束
audio.onEnded((res) => {
this.play = false;
});
audio.src = this.modelValue;
audio.play();
}
},
chooseVoice() {
var that = this;
uni.chooseFile({
count: 1, //默认100
extension:['.mp3','.mp4','.m4a'],
success: function (res) {
var fileSize = res.tempFiles[0].size;
if (fileSize > 1024 * 1024 * 10) { // 假设设置的文件大小限制为5MB
uni.showToast({
title: '文件大小限制为10MB',
icon: 'none'
});
return;
}
that.voicePath = res.tempFilePaths[0];
console.log(JSON.stringify(res.tempFilePaths));
that.uploadVoice(that.voicePath);
}
});
},
uploadVoice(path) {
FileApi.uploadFile(path).then((res) => {
this.$emit('update:modelValue', res.data);
});
},
voiceOk(e) {
this.voicePath = e.path;
this.uploadVoice(this.voicePath);
this.$emit('sec', e.sec);
},
}
}
</script>
<style lang="scss" scoped>
.form-bg {
padding: 0 15px;
}
.form-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 0;
.label {
font-size: 30rpx;
min-width: 200rpx;
}
}
.bubble-box {
position: relative;
.tui-menu-item {
width: 100%;
padding: 30rpx 20rpx;
text-align: center;
position: relative;
}
.tui-menu-item:after {
position: absolute;
box-sizing: border-box;
content: " ";
pointer-events: none;
top: 0%;
right: 10%;
bottom: 0%;
left: 10%;
border: 0 solid #ebedf0;
border-color: #646566;
border-bottom-width: 1px;
}
.tui-menu-item:last-child:after {
border-bottom-width: 0;
}
}
.voice-box {
height: 400rpx;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
.icon {
display: flex;
background-color: #3cc9a4;
border-radius: 100%;
width: 170rpx;
height: 170rpx;
justify-content: center;
align-items: center;
}
.upload-btn-box {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.upload-btn {
font-size: 34rpx;
color: #aaa;
margin-top: 10px;
}
.reload-btn {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
right: 0px;
font-size: 30rpx;
color: #3cc9a4;
.text {
margin-left: 4px;
}
}
}
</style>

213
pages/clerk/apply/edit.vue Normal file
View File

@@ -0,0 +1,213 @@
<template>
<view class="page-app theme-light main-green font-1">
<su-navbar title="编辑资料" statusBar></su-navbar>
<view class="form-box">
<form-avatar v-model="form.avatar"></form-avatar>
</view>
<view class="form-box">
<view class="form-item">
<view class="label">昵称</view>
<u-input input-align="right" placeholder="请输入昵称" v-model="form.nickname" />
</view>
<form-sex v-model="form.sex"></form-sex>
<view class="form-item">
<view class="label">年龄</view>
<u-input input-align="right" placeholder="请输入年龄" type="number" v-model="form.age" />
</view>
<view class="form-item" v-if="isPass">
<view class="label">微信</view>
<u-input input-align="right" placeholder="请输入您的微信" v-model="form.weixin" />
</view>
<view class="form-item">
<view class="label">手机号</view>
<u-input input-align="right" placeholder="请输入手机号" type="number" v-model="form.mobile" />
</view>
<view class="form-item">
<view class="label">相关经验</view>
<u-input input-align="right" placeholder="是否有其它店铺的经验" v-model="form.experience" />
</view>
<view class="form-item">
<view class="label">自我介绍</view>
<u-input input-align="right" placeholder="请输入自我介绍" v-model="form.intro" />
</view>
<view class="form-item">
<view class="label">所在城市</view>
<u-input input-align="right" placeholder="请输入所在城市" v-model="form.city" />
</view>
</view>
<view class="form-box">
<form-image :number="6" v-model="imgList"></form-image>
</view>
<view class="form-box">
<form-voice @sec="toSec" v-model="form.sound"></form-voice>
</view>
<view class="submit-box">
<view class="sub-btn" @click="saveApply">提交申请</view>
</view>
<s-menu-tools />
<s-auth-modal />
</view>
</template>
<script>
import FormAvatar from '@/pages/clerk/apply/components/formAvatar.vue';
import FormSex from '@/pages/clerk/apply/components/formSex.vue';
import FormVoice from '@/pages/clerk/apply/components/formVoice.vue';
import FormImage from '@/pages/clerk/apply/components/formImage.vue';
import ClerkApi from '@/sheep/api/worker/clerk';
import sheep from '@/sheep';
export default {
components: {
FormAvatar,
FormSex,
FormVoice,
FormImage,
},
props: {
},
data() {
return {
form: {
id: 0,
avatar: '',
nickname: '',
sex: '',
age: '',
weixin: '',
mobile: '',
experience: '',
intro: '',
city: '',
albums: '',
sound: '',
soundTime: '',
},
imgList: [],
}
},
onLoad(options) {
this.form.id = options.id;
this.init();
},
computed: {
isPass() {
return sheep.$store('user').tradeConfig.weixinEnabled;
},
},
methods: {
init() {
ClerkApi.getClerkApply(this.form.id).then((res) => {
this.form = res.data;
if(this.form.albums){
this.imgList = this.form.albums.split(',');
}
});
},
saveApply() {
if(!this.form.avatar){
sheep.$helper.toast('请上传头像');
return;
}
if(!this.form.nickname){
sheep.$helper.toast('请输入昵称');
return;
}
if(!this.form.sex){
sheep.$helper.toast('请选择性别');
return;
}
if(!this.form.age){
sheep.$helper.toast('请输入年龄');
return;
}
if(this.isPass && !this.form.weixin){
sheep.$helper.toast('请输入正确的微信号');
return;
}
if(!this.form.mobile){
sheep.$helper.toast('请输入正确的手机号');
return;
}
if(!this.form.experience){
sheep.$helper.toast('请输入相关经验');
return;
}
if(!this.form.intro){
sheep.$helper.toast('请输入自我介绍');
return;
}
if(!this.form.city){
sheep.$helper.toast('请输入所在城市');
return;
}
if(this.imgList.length < 1){
sheep.$helper.toast('请上传图片');
return;
}
this.form.albums = this.imgList.join(',');
ClerkApi.updateClerkApply(this.form).then((res) => {
});
},
toSec(e) {
this.form.soundTime = e;
},
}
}
</script>
<style lang="scss" scoped>
.page-app {
background-color: #fafafa;
padding-bottom: 140rpx;
}
.form-box {
background-color: #fff;
margin: 15px;
border-radius: 10px;
}
.form-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 15px;
.label {
font-size: 30rpx;
min-width: 200rpx;
}
}
.submit-box {
display: flex;
align-items: center;
position: fixed;
left: 0;
right: 0;
bottom: 0;
height: 140rpx;
padding: 0 15px;
z-index: 99;
.sub-btn {
background-color: var(--ui-BG-Main);
display: flex;
flex: 1;
justify-content: center;
align-items: center;
padding: 10px;
color: #fff;
border-radius: 40px;
font-size: 30rpx;
}
}
</style>

307
pages/clerk/apply/index.vue Normal file
View File

@@ -0,0 +1,307 @@
<template>
<view class="page-app theme-light main-green font-1">
<su-navbar title="达人申请" statusBar></su-navbar>
<!-- #ifdef MP -->
<view v-if="showSubscribeBtn" class="subscribe-box">
<u-icon name="bell-fill" color="var(--ui-BG-Main)" size="44"></u-icon>
<view class="info">获取实时审核结果</view>
<view class="sub-btn" @tap="subscribeMessage">立即订阅</view>
</view>
<!-- #endif -->
<view class="form-box">
<form-avatar v-model="form.avatar"></form-avatar>
</view>
<view class="form-box">
<view class="form-item">
<view class="label">昵称</view>
<u-input input-align="right" placeholder="请输入昵称" v-model="form.nickname" />
</view>
<form-sex v-model="form.sex"></form-sex>
<view class="form-item">
<view class="label">年龄</view>
<u-input input-align="right" placeholder="请输入年龄" type="number" v-model="form.age" />
</view>
<view class="form-item" v-if="isPass">
<view class="label">微信</view>
<u-input input-align="right" placeholder="请输入您的微信" v-model="form.weixin" />
</view>
<view class="form-item">
<view class="label">手机号</view>
<u-input input-align="right" placeholder="请输入手机号" type="number" v-model="form.mobile" />
</view>
<view class="form-item">
<view class="label">自我介绍</view>
<u-input input-align="right" placeholder="请输入自我介绍" v-model="form.intro" />
</view>
<view class="form-item">
<view class="label">所在城市</view>
<u-input input-align="right" placeholder="请输入所在城市" v-model="form.city" />
</view>
</view>
<view class="form-box">
<form-image :number="6" v-model="imgList"></form-image>
</view>
<view class="form-box">
<form-voice @sec="toSec" v-model="form.sound"></form-voice>
</view>
<view class="check-box" @click="changeCheck">
<u-icon size="44" v-if="check" name="checkmark-circle-fill" color="var(--ui-BG-Main)"></u-icon>
<u-icon size="44" v-else name="checkmark-circle" color="var(--ui-BG-Main)"></u-icon>
<text class="info">我已阅读并接受</text>
<text @tap.stop="toAggre()" class="sub-btn">达人申请协议</text>
</view>
<view class="submit-box">
<view class="sub-btn" @click="saveApply">提交申请</view>
</view>
<s-menu-tools />
<s-auth-modal />
<qrcode-modal />
</view>
</template>
<script>
import FormAvatar from '@/pages/clerk/apply/components/formAvatar.vue';
import FormSex from '@/pages/clerk/apply/components/formSex.vue';
import FormVoice from '@/pages/clerk/apply/components/formVoice.vue';
import FormImage from '@/pages/clerk/apply/components/formImage.vue';
import qrcodeModal from '@/components/qrcode-modal/qrcode-modal.vue';
import ClerkApi from '@/sheep/api/worker/clerk';
import test from '@/sheep/helper/test.js';
import { WxaSubscribeTemplate } from '@/sheep/util/const';
import sheep from '@/sheep';
export default {
components: {
FormAvatar,
FormSex,
FormVoice,
FormImage,
qrcodeModal,
},
props: {
},
data() {
return {
form: {
avatar: '',
nickname: '',
sex: '',
age: '',
weixin: '',
mobile: '',
experience: '',
intro: '',
city: '',
albums: '',
sound: '',
soundTime: '',
},
imgList: [],
showSubscribeBtn: false,
check: true,
}
},
onLoad() {
// #ifdef MP
// 订阅只能由用户主动触发,只能包一层 showModal 诱导用户点击
this.autoSubscribeMessage();
// #endif
},
computed: {
isPass() {
return sheep.$store('user').tradeConfig.weixinEnabled;
},
},
methods: {
saveApply() {
// #ifdef MP
// 订阅只能由用户主动触发,只能包一层 showModal 诱导用户点击
this.autoSubscribeMessage();
// #endif
if(!this.form.avatar){
sheep.$helper.toast('请上传头像');
return;
}
if(!this.form.nickname){
sheep.$helper.toast('请输入昵称');
return;
}
if(!this.form.sex){
sheep.$helper.toast('请选择性别');
return;
}
if(!this.form.age){
sheep.$helper.toast('请输入年龄');
return;
}
if(this.form.age < 18){
sheep.$helper.toast('未成年禁止申请');
return;
}
if(this.isPass && !this.form.weixin){
sheep.$helper.toast('请输入正确的微信号');
return;
}
if(!this.form.mobile || !test.mobile(this.form.mobile)){
sheep.$helper.toast('请输入正确的手机号');
return;
}
if(!this.form.intro){
sheep.$helper.toast('请输入自我介绍');
return;
}
if(!this.form.city){
sheep.$helper.toast('请输入所在城市');
return;
}
if(this.imgList.length < 1){
sheep.$helper.toast('请上传图片');
return;
}
if(this.imgList.length < 1){
sheep.$helper.toast('请上传图片');
return;
}
if(!this.check){
sheep.$helper.toast('未同意协议');
return;
}
this.form.albums = this.imgList.join(',');
ClerkApi.createClerkApply(this.form).then((res) => {
if(res.data){
sheep.$router.go('/pages/worker/levelList/index', {id: res.data});
}
});
},
toSec(e) {
this.form.soundTime = e;
},
subscribeMessage() {
const event = [WxaSubscribeTemplate.CLERK_APPLY_SUCCESS];
event.push(WxaSubscribeTemplate.CLERK_BLIND);
event.push(WxaSubscribeTemplate.CLERK_ORDER);
sheep.$platform.useProvider('wechat').subscribeMessage(event, () => {
// 订阅后记录一下订阅状态
uni.removeStorageSync(WxaSubscribeTemplate.CLERK_APPLY_SUCCESS);
uni.setStorageSync(WxaSubscribeTemplate.CLERK_APPLY_SUCCESS, '已订阅');
// 隐藏订阅按钮
this.showSubscribeBtn = false;
});
},
async autoSubscribeMessage() {
// 1. 校验是否手动订阅过
const subscribeBtnStatus = uni.getStorageSync(WxaSubscribeTemplate.CLERK_APPLY_SUCCESS);
if (!subscribeBtnStatus) {
this.showSubscribeBtn = true;
}
// 2. 订阅消息
this.subscribeMessage();
},
changeCheck() {
if(this.check){
this.check = false;
}else{
this.check = true;
}
},
toAggre() {
sheep.$router.go('/pages/public/richtext', {title: '店员申请协议'})
},
}
}
</script>
<style lang="scss" scoped>
.page-app {
background-color: #fafafa;
padding-bottom: 140rpx;
}
.form-box {
background-color: #fff;
margin: 15px;
border-radius: 10px;
}
.form-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 15px;
.label {
font-size: 30rpx;
min-width: 200rpx;
}
}
.submit-box {
display: flex;
align-items: center;
position: fixed;
left: 0;
right: 0;
bottom: 0;
height: 140rpx;
padding: 0 15px;
z-index: 99;
.sub-btn {
background-color: var(--ui-BG-Main);
display: flex;
flex: 1;
justify-content: center;
align-items: center;
padding: 10px;
color: #fff;
border-radius: 40px;
font-size: 30rpx;
}
}
.subscribe-box {
display: flex;
align-items: center;
padding: 10px;
padding-bottom: 0;
justify-content: center;
font-size: 28rpx;
.info {
margin: 0 10rpx;
}
.sub-btn {
color: var(--ui-BG-Main);
}
}
.check-box {
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
.info{
margin: 0 10rpx;
}
.sub-btn {
color: var(--ui-BG-Main);
}
}
</style>

View File

@@ -0,0 +1,225 @@
<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="tabOpacityVal > 0.4 ? 'black' : ''" class="sicon-back" v-if="hasHistory" />
<text :class="tabOpacityVal > 0.4 ? 'black' : ''" 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="tabOpacityVal > 0.4 ? 'black' : ''" class="sicon-more" />
</view>
</view>
<!-- -->
<view class="detail-tab-card ss-flex-1" :style="[{ opacity: 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 tabList"
:key="item.value"
@tap="onTab(item)"
>
<view class="tab-title" :class="currentTab === item.value ? 'cur-tab-title' : ''">
{{ item.label }}
</view>
<view v-show="currentTab === item.value" class="tab-line"></view>
</view>
</view>
</view>
<!-- #ifdef MP -->
<view :style="[capsuleStyle]"></view>
<!-- #endif -->
</view>
</su-fixed>
</template>
<script>
import sheep from '@/sheep';
import { showMenuTools, closeMenuTools } from '@/sheep/hooks/useModal';
export default {
components: {
},
props: {
title: {
type: String,
default: '',
},
//滚动条滚动距离
scrollTop: {
type: [Number, String],
default: 0
},
},
data() {
return {
sys_navBar: sheep.$platform.navbar,
sys_statusBar: sheep.$platform.device.statusBarHeight,
hasHistory: sheep.$router.hasHistory(),
tabList: [
{
label: '价格',
value: 1,
},
{
label: '动态',
value: 2,
},
{
label: '评价',
value: 3,
},
],
capsuleStyle: {
width: sheep.$platform.capsule.width + 'px',
height: sheep.$platform.capsule.height + 'px',
},
}
},
computed: {
tabOpacityVal() {
return this.scrollTop > sheep.$platform.navbar ? 1 : this.scrollTop * 0.01;;
},
currentTab() {
return sheep.$store('sys').clerkTabIndex;
},
},
methods: {
onClickLeft() {
if (this.hasHistory) {
sheep.$router.back();
} else {
sheep.$router.go('/pages/tabbar/index');
}
},
onClickRight() {
showMenuTools();
},
onTab(e) {
this.$emit('onTab', e.value);
},
}
}
</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: #fff;
}
.sicon-home {
font-size: 32rpx;
color: #fff;
}
.sicon-more {
font-size: 32rpx;
color: #fff;
}
.black {
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,150 @@
<template>
<view>
<view class="option-box">
<view class="tag-box">
<u-icon name="tags" color="#f6f6f6" size="34"></u-icon>
<view class="name">服务类型</view>
</view>
<view class="span-box">
<view @click="changeGame(option)" class="span" :class="catId == option.id ? 'active': '' " v-for="(option,t) in optionList">{{option.name}}</view>
</view>
</view>
<view class="option-box" v-if="categoryList.length > 0">
<view class="tag-box">
<u-icon name="tags" color="#f6f6f6" size="34"></u-icon>
<view class="name">选项</view>
</view>
<view class="span-box">
<view @click="changeCategory(option)" class="span" :class="gameId == option.id ? 'active': '' " v-for="(option,t) in categoryList">{{option.name}}</view>
</view>
</view>
<view class="option-box" v-if="goodsList.length > 0">
<view class="tag-box">
<u-icon name="tags" color="#f6f6f6" size="34"></u-icon>
<view class="name">选项</view>
</view>
<view class="span-box">
<view @click="changeGoods(option)" class="span" :class="goodsId == option.id ? 'active': '' " v-for="(option,t) in goodsList">{{option.name}}</view>
</view>
</view>
</view>
</template>
<script>
export default {
components: {
},
props: {
optionList: {
type: Array,
default: [],
},
modelValue: {
type: Object,
default: {}
},
},
data() {
return {
catId: -1,
gameId: -1,
goodsId: -1,
categoryList: [],
goodsList: [],
goodsList: [],
}
},
methods: {
changeGame(e) {
this.gameId = -1;
this.goodsId = -1;
this.catId = e.id;
if(e.categoryList){
this.goodsList = [];
this.categoryList = e.categoryList;
}else{
this.categoryList = [];
this.goodsList = e.goodsList;
}
var node = {
catId: this.catId,
gameId: this.gameId,
goodsId: this.goodsId,
price: 0,
}
this.$emit('update:modelValue', node);
},
changeCategory(e) {
this.goodsId = -1;
this.gameId = e.id;
this.goodsList = e.goodsList;
var node = {
catId: this.catId,
gameId: this.gameId,
goodsId: this.goodsId,
price: 0,
}
this.$emit('update:modelValue', node);
},
changeGoods(e) {
this.goodsId = e.id;
var node = {
catId: this.catId,
gameId: this.gameId,
goodsId: this.goodsId,
price: e.price,
}
this.$emit('update:modelValue', node);
},
}
}
</script>
<style lang="scss" scoped>
.option-box {
margin-bottom: 10px;
.tag-box {
display: flex;
align-items: center;
margin-bottom: 12px;
.name {
font-size: 28rpx;
color: #333;
margin-left: 5px;
}
}
.span-box {
display: flex;
flex-wrap: wrap;
justify-content: space-around;
.span {
background-color: #f6f6f6;
padding: 14rpx 20rpx;
border-radius: 40px;
margin-right: 12px;
margin-bottom: 12px;
min-width: 140rpx;
display: flex;
justify-content: center;
align-items: center;
font-size: 24rpx;
color: #949494;
}
.active {
background-color: var(--ui-BG-Main);
color: #fff;
}
}
}
</style>

View File

@@ -0,0 +1,316 @@
<template>
<view v-show="showPop" class="gift-box">
<scroll-view style="height: 100%;" scroll-y>
<view class="top-box">
<view class="title">选择礼物</view>
<view class="btn-box" @click="close">
<u-icon name="close"></u-icon>
<view class="text">取消</view>
</view>
</view>
<view class="list-box">
<view v-for="(item,index) in dataList" class="gift" @click="sendGift(item)">
<view class="img-box">
<img class="img" :src="item.img"></img>
<view class="tag" v-if="item.giftType == 1">
<text>特效</text>
<text v-if="item.tag">·{{item.tag}}</text>
</view>
<view class="tag" v-if="item.giftType == 0 && item.tag">
<text>{{item.tag}}</text>
</view>
</view>
<view class="name">{{item.name}}</view>
<view class="price">{{ fen2yuan(item.money) }} 钻石</view>
</view>
</view>
</scroll-view>
</view>
<view class="svga-box" :class="giftFlag ? 'svga-show': 'svga-hide'">
<c-svga ref="cSvgaRef" :canvasId='canvasId' :src="src" :loops='0' :auto-play="false" @frame='onFrame' @finished='onFinished' @percentage='onPercentage' @loaded='onLoaded'></c-svga>
<view class="close-btn">
<view class="bottom-box">
<view class="title">{{gift.name}}</view>
<view class="price">{{ fen2yuan(gift.money) }} 钻石</view>
<view class="btn-box">
<view class="btn" @click="cannel">取消</view>
<view class="btn active" @click="ok">确定</view>
</view>
</view>
</view>
</view>
</template>
<script>
import sheep from '@/sheep';
export default {
components: {
},
props: {
modelValue: {
type: Object,
default: {}
},
showPop: {
type: Boolean,
default: false
},
dataList: {
type: Array,
default: []
},
},
emits: ["close", "update:modelValue"],
data() {
return {
giftFlag: false,
src: '',
canvasId: 'myCanvas2',
gift: {},
}
},
methods: {
close() {
this.$emit('close');
},
sendGift(e) {
this.gift = e;
if(e.giftType == 0){
// 普通礼物不播放
this.close();
this.$emit('update:modelValue', this.gift);
return;
}
this.src = e.pic;
this.giftFlag = true;
},
onFinished() {
this.giftFlag = false;
console.log('动画停止播放时回调');
},
onFrame(frame) {//动画播放至某帧后回调
// console.log(frame);
},
onPercentage(percentage) { //动画播放至某进度后回调
// console.log(percentage);
},
onLoaded() {
this.$refs.cSvgaRef.call('setContentMode', 'AspectFill');
console.log('加载完成');
this.$refs.cSvgaRef.call('startAnimation');
},
closeSvga() {
this.src = "";
this.$refs.cSvgaRef.call('stopAnimation');
this.giftFlag = false;
},
fen2yuan(price) {
var f = 0;
var p = (price / 100.0).toFixed(0);
var p1 = (price / 100.0).toFixed(1);
var p2 = (price / 100.0).toFixed(2);
if(p*100 == price){
f = 0;
}else if(p1*100 == price){
f = 1;
}else if(p2*100 == price){
f = 2;
}
return (price / 100.0).toFixed(f)
},
cannel() {
this.closeSvga();
},
ok() {
this.closeSvga();
this.close();
this.$emit('update:modelValue', this.gift);
},
}
}
</script>
<style lang="scss" scoped>
.gift-box {
background-color: #fff;
position: absolute;
top: 200rpx;
bottom: 0;
left: 0;
right: 0;
z-index: 9999;
padding: 15px;
border-top-right-radius: 20rpx;
border-top-left-radius: 20rpx;
padding-bottom: 0;
.top-box {
display: flex;
justify-content: space-between;
align-items: center;
position: fixed;
left: 0;
right: 0;
background-color: #fff;
top: 200rpx;
padding: 0 15px;
border-top-right-radius: 20rpx;
border-top-left-radius: 20rpx;
height: 110rpx;
z-index: 1;
.title {
font-size: 28rpx;
}
.btn-box {
background-color: var(--ui-BG-Main);
padding: 7px 15px;
font-size: 24rpx;
border-radius: 40px;
display: flex;
justify-content: center;
align-items: center;
color: #fff;
.text {
margin-left: 5px;
}
}
}
.list-box {
display: flex;
flex-wrap: wrap;
padding-top: 60rpx;
.gift {
width: 25%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 20rpx 0px;
.img-box {
position: relative;
.img{
max-width: 160rpx;
height: 160rpx;
}
.tag {
position: absolute;
top: 0;
right: 0;
font-size: 16rpx;
color: #fff;
background-color: var(--ui-BG-Main);
border-radius: 40px;
padding: 2rpx 8rpx;
}
}
.name {
font-size: 24rpx;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
max-width: 160rpx;
margin-top: 2px;
margin-bottom: 7px;
}
.price {
font-size: 20rpx;
color: var(--ui-BG-Main);
}
}
}
}
.svga-box {
position: fixed;
top: 0;
left: 0;
z-index: 999999999;
width: 100%;
height: 100%;
background-color: black;
.close-btn {
color: #fff;
position: absolute;
z-index: 999999999;
padding: 5px 10px;
left: 0;
right: 0;
bottom: 100rpx;
.bottom-box {
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
.title {
font-size: 28rpx;
margin-bottom: 5px;
}
.price {
font-size: 24rpx;
}
.btn-box {
display: flex;
align-items: center;
margin-top: 10px;
.btn {
border: 1px solid #fff;
font-size: 28rpx;
padding: 20rpx 110rpx;
border-radius: 40px;
margin: 15px;
color: #fff;
}
.active {
border: 1px solid var(--ui-BG-Main);
background-color: var(--ui-BG-Main);
}
}
}
}
}
.svga-hide {
/* #ifdef MP */
transform: translate(-100%, 0);
/* #endif */
/* #ifndef MP */
display: none;
/* #endif */
}
.svga-show {
/* #ifdef MP */
transform: translate(0, 0);
/* #endif */
/* #ifndef MP */
display: block;
/* #endif */
}
</style>

View File

@@ -0,0 +1,444 @@
<template>
<tui-bottom-popup :zIndex="1002" :maskZIndex="1001" :show="popupShow" @close="hiddenPopup">
<view class="order-box">
<view class="avatar-box">
<u-image width="140" height="140" border-radius="20" :src="clerk.avatar"></u-image>
<view class="close-span" @click="hiddenPopup">
<u-icon name="close-circle" color="#98a2a1" size="50"></u-icon>
</view>
</view>
<scroll-view style="height: 700rpx;" scroll-y>
<view class="page-box">
<view class="form-box">
<view class="input-box">
<view class="tag-box">
<view class="name">服务类型</view>
</view>
<view class="tab-span">
<!-- <view @click="changeTab(0)" class="btn" :class="form.rewardType == 0 ? 'active' : ''">赠送</view> -->
<view @click="changeTab(1)" class="btn" :class="form.rewardType == 1 ? 'active' : ''">赠礼</view>
</view>
</view>
<view class="input-box" v-if="form.rewardType == 0">
<view class="tag-box">
<view class="name">打赏金额</view>
</view>
<view class="input-span">
<u-input v-model="payMoney" type="number" :placeholder-style="`fontSize: 24rpx; color: #cbced5;`" placeholder="请输入赠送金额"></u-input>
</view>
</view>
<view class="input-box" v-if="form.rewardType == 1">
<view class="tag-box">
<view class="name">赠送礼物</view>
</view>
<view class="select-span" @click="openGift">
<view v-if="gift.id > 0">已选{{gift.name}}</view>
<view v-else>点击选择礼物</view>
<u-icon name="arrow-right"></u-icon>
</view>
</view>
<view class="input-box" v-if="gift.id > 0">
<view class="tag-box">
<view class="name">礼物数量</view>
</view>
<view class="step-span">
<img class="img" :src="gift.img"></img>
<tui-numberbox iconBgColor="var(--ui-BG-Main)" iconColor="#fff" backgroundColor="#fff" :min="1" :value="form.count" @change="change"></tui-numberbox>
</view>
</view>
<view class="input-box">
<view class="textarea-box">
<view class="name">心动留言</view>
</view>
<view class="textarea-span">
<u-input v-model="form.msg" type="textarea" :placeholder-style="`fontSize: 24rpx; color: #cbced5;`" placeholder="请输入心动留言"></u-input>
</view>
</view>
<view class="input-box" v-if="isPass">
<view class="tag-box">
<view class="name">您的微信号</view>
</view>
<view class="input-span">
<u-input v-model="form.weixin" :placeholder-style="`fontSize: 24rpx; color: #cbced5;`" placeholder="请输入正确的微信号"></u-input>
</view>
</view>
</view>
</view>
</scroll-view>
<view class="bottom-box2" v-if="form.rewardType == 0">
<view class="btn-box">
<view class="pay-btn" @click="confirm">立即赠送</view>
</view>
</view>
<view class="bottom-box" v-if="form.rewardType == 1">
<view class="price-box">
<text>总价</text>
<text class="price">{{ fen2yuan(gift.money*form.count) }}</text>
<text>钻石</text>
</view>
<view class="btn-box">
<view class="pay-btn" @click="confirm">立即赠送</view>
</view>
</view>
</view>
</tui-bottom-popup>
<gift-list :showPop="giftFlag" :dataList="giftList" v-model="gift" @close="closeGift"></gift-list>
</template>
<script>
import tuiBottomPopup from "@/components/thorui/tui-bottom-popup/tui-bottom-popup.vue"
import tuiNumberbox from "@/components/thorui/tui-numberbox/tui-numberbox.vue"
import GiftList from '@/pages/clerk/detail/components/giftList.vue';
import RewardApi from '@/sheep/api/worker/reward';
import sheep from '@/sheep';
export default {
components: {
tuiBottomPopup,
tuiNumberbox,
GiftList,
},
props: {
},
data() {
return {
popupShow: false,
gift: {
id: -1,
money: 0,
},
giftFlag: false,
giftList: [],
payMoney: '',
form: {
rewardType: 1,
count: 1,
giftId: -1,
money: '',
weixin: '',
msg: '',
payMoney: '',
},
}
},
computed: {
clerk() {
return sheep.$store('sys').clerk;
},
userInfo() {
return sheep.$store('user').userInfo;
},
isPass() {
return sheep.$store('user').tradeConfig.weixinEnabled;
},
},
methods: {
//调用此方法显示弹层
showPopup() {
this.form.weixin = this.userInfo.weixin;
RewardApi.getGiftList().then(res => {
this.giftList = res.data;
this.popupShow = true
});
},
showGiftPopup(e) {
this.gift = e;
this.form.rewardType = 1;
this.form.weixin = this.userInfo.weixin;
RewardApi.getGiftList().then(res => {
this.giftList = res.data;
this.popupShow = true
});
},
hiddenPopup() {
this.popupShow = false
},
change(e) {
this.form.count = e.value
},
changeTab(e) {
this.form.rewardType = e;
this.gift = {
id: -1,
money: 0,
};
},
fen2yuan(price) {
var f = 0;
var p = (price / 100.0).toFixed(0);
var p1 = (price / 100.0).toFixed(1);
var p2 = (price / 100.0).toFixed(2);
if(p*100 == price){
f = 0;
}else if(p1*100 == price){
f = 1;
}else if(p2*100 == price){
f = 2;
}
return (price / 100.0).toFixed(f)
},
yuan2fen(price) {
return (price * 100.0).toFixed(0)
},
openGift() {
this.giftFlag = true;
},
closeGift() {
this.form.count = 1;
this.giftFlag = false;
},
confirm() {
if(this.isPass && !this.form.weixin){
sheep.$helper.toast('请输入正确的微信');
return;
}
if(this.form.rewardType == 0){
this.form.payMoney = this.yuan2fen(this.payMoney);
if(this.form.payMoney < 1){
sheep.$helper.toast('请输入打赏金额');
return;
}
}else{
this.form.giftId = this.gift.id;
if(this.form.giftId < 1){
sheep.$helper.toast('请选择打赏礼物');
return;
}
}
this.form.workerClerkId = this.clerk.id;
RewardApi.createRewardOrder(this.form).then(res => {
// 跳转到支付页面
this.$u.route({
url: 'pages/pay/reward/index',
params: {
id: res.data.payOrderId,
}
});
});
},
}
}
</script>
<style lang="scss" scoped>
.avatar-box {
padding: 10px 0;
display: flex;
justify-content: center;
align-items: center;
.close-span {
position: absolute;
right: 5px;
top: 5px;
width: 70rpx;
height: 70rpx;
display: flex;
justify-content: center;
align-items: center;
}
}
.page-box {
padding: 15px;
padding-bottom: 10px;
.form-box {
margin-bottom: 10px;
.tag-box {
display: flex;
align-items: center;
width: 180rpx;
.name {
font-size: 24rpx;
color: #333;
margin-left: 5px;
}
}
.textarea-box {
height: 20px;
display: flex;
width: 180rpx;
.name {
font-size: 24rpx;
color: #333;
margin-left: 5px;
}
}
.input-box {
display: flex;
margin-bottom: 20px;
align-items: center;
.input-span {
background-color: #f6f6f6;
margin-left: 15px;
display: flex;
flex: 1;
padding: 0 12px;
border-radius: 10rpx;
}
.select-span {
font-size: 24rpx;
background-color: #f6f6f6;
margin-left: 15px;
display: flex;
flex: 1;
padding: 12px 12px;
border-radius: 10rpx;
color: #aaaaaa;
justify-content: space-between;
}
.tab-span {
display: flex;
flex: 1;
padding: 0 12px;
align-items: center;
.btn {
background-color: #f6f6f6;
font-size: 24rpx;
padding: 7px 0;
border-radius: 40px;
min-width: 120rpx;
display: flex;
justify-content: center;
align-items: center;
margin-right: 15px;
}
.active {
color: #fff;
background-color: var(--ui-BG-Main);
}
}
.textarea-span {
background-color: #f6f6f6;
margin-left: 15px;
flex: 1;
padding: 2px 12px;
border-radius: 10px;
}
.step-span {
display: flex;
justify-content: space-between;
flex: 1;
padding-left: 12px;
align-items: center;
.img {
width: 80rpx;
height: 80rpx;
border-radius: 100%;
box-shadow: 0 0 8px rgba(0, 0, 0, 0.5);
}
}
}
}
}
.bottom-box {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
box-shadow: 0 0 6px 0 #ccc;
.price-box {
color: #fb932c;
font-size: 28rpx;
width: 50%;
display: flex;
justify-content: center;
align-items: center;
.price {
margin-right: 5px;
}
}
.btn-box {
width: 50%;
padding-left: 15px;
.pay-btn {
background-color: var(--ui-BG-Main);
color: #fff;
display: flex;
justify-content: center;
align-items: center;
border-radius: 40px;
font-size: 28rpx;
height: 70rpx;
}
}
}
.bottom-box2 {
display: flex;
justify-content: center;
align-items: center;
padding: 15px;
box-shadow: 0 0 6px 0 #ccc;
.price-box {
color: #fb932c;
font-size: 28rpx;
width: 50%;
display: flex;
justify-content: center;
align-items: center;
.price {
margin-right: 5px;
}
}
.btn-box {
width: 50%;
.pay-btn {
background-color: var(--ui-BG-Main);
color: #fff;
display: flex;
justify-content: center;
align-items: center;
border-radius: 40px;
font-size: 28rpx;
height: 70rpx;
}
}
}
</style>

View File

@@ -0,0 +1,263 @@
<!-- 虚拟列表演示(不使用内置列表)(vue) -->
<!-- 写法较简单在页面中对当前需要渲染的虚拟列表数据进行for循环在vue3中兼容性良好 -->
<!-- 在各平台兼容性请查阅https://z-paging.zxlee.cn/module/virtual-list.html -->
<template>
<view class="content">
<!-- 如果页面中的cell高度是固定不变的则不需要设置cell-height-mode如果页面中高度是动态改变的则设置cell-height-mode="dynamic" -->
<!-- 原先的v-model修改为@virtualListChange="virtualListChange"并赋值处理后的虚拟列表 -->
<z-paging ref="paging" :show-loading-more-no-more-view="showLoad" :show-default-loading-more-text="showLoad" use-virtual-list :force-close-inner-list="true" :paging-style="{ paddingTop: 0 + 'px', paddingBottom: paddingBottom + 'rpx' }" cell-height-mode="dynamic" @scroll="scroll" @virtualListChange="virtualListChange" @query="queryList">
<!-- 需要固定在顶部不滚动的view放在slot="top"的view中如果需要跟着滚动则不要设置slot="top" -->
<template #top>
<nav-bar :title="title" :scrollTop="scrollTop" @onTab="change" @initNav="initNav"></nav-bar>
</template>
<view>
<user-box :clerk="clerk" :clerkLevel="clerk.clerkLevel" :paddingTop="paddingTop"></user-box>
<sticky-box :clerk="clerk" @change="change"></sticky-box>
</view>
<!-- :id="`zp-id-${item.zp_index}`":key="item.zp_index" 必须写必须写 -->
<!-- 这里for循环的index不是数组中真实的index了请使用item.zp_index获取真实的index -->
<view class="data-box">
<star-list v-if="currentTab == 0" @openGift="openGift" :clerk="clerk" :virtualList="giftList"></star-list>
<price-list v-if="currentTab == 1" :clerk="clerk"></price-list>
<post-list v-if="currentTab == 2" :virtualList="virtualList"></post-list>
<rate-list v-if="currentTab == 3" :clerk="clerk" :virtualList="virtualList"></rate-list>
</view>
<template #bottom>
<view v-if="isGift" class="bottom-box">
<view class="order" @click="doOrder">立即下单</view>
<view class="gift" @click="doGift">赠礼</view>
</view>
<view v-else class="bottom-box">
<view class="order-btn" @click="doOrder">立即下单</view>
</view>
</template>
</z-paging>
<order-popup ref="orderPopup"></order-popup>
<gift-popup ref="giftPopup"></gift-popup>
</view>
</template>
<script>
import UserBox from '@/pages/clerk/detail/components/userBox.vue';
import StickyBox from '@/pages/clerk/detail/components/stickyBox.vue';
import StarList from '@/pages/clerk/detail/components/starList.vue';
import PriceList from '@/pages/clerk/detail/components/priceList.vue';
import PostList from '@/pages/clerk/detail/components/postList.vue';
import RateList from '@/pages/clerk/detail/components/rateList.vue';
import NavBar from '@/pages/clerk/detail/components/navBar.vue';
import OrderPopup from '@/pages/clerk/detail/components/orderPopup.vue';
import GiftPopup from '@/pages/clerk/detail/components/giftPopup.vue';
import UserApi from '@/sheep/api/member/user';
import ClerkApi from '@/sheep/api/worker/clerk';
import TrendApi from '@/sheep/api/worker/trend';
import RewardApi from '@/sheep/api/worker/reward';
import CommentApi from '@/sheep/api/product/comment';
import { showAuthModal } from '@/sheep/hooks/useModal';
import sheep from '@/sheep';
export default {
components: {
UserBox,
StickyBox,
StarList,
PriceList,
PostList,
RateList,
NavBar,
OrderPopup,
GiftPopup,
},
props: {
title: {
type: String,
default: '',
}
},
data() {
return {
// 虚拟列表数组,通过@virtualListChange监听获得最新数组
virtualList: [],
scrollTop: 0,
paddingTop: 0,
paddingBottom: 0,
height: 0,
clerk: {},
workerClerkId: 0,
giftList: [],
}
},
computed: {
currentTab() {
return sheep.$store('sys').clerkTabIndex;
},
showLoad() {
return sheep.$store('sys').clerkTabIndex > 1;
},
isLogin: {
get() {
return sheep.$store('user').isLogin;
},
},
userInfo: {
get() {
return sheep.$store('user').userInfo;
},
},
isGift() {
return sheep.$store('user').tradeConfig.giftEnabled;
},
},
methods: {
initNav(e) {
this.height = e.height;
this.paddingTop = this.height;
},
initData(options) {
this.workerClerkId = options.id;
ClerkApi.getClerk({
id: options.id,
userId: this.userInfo.id,
}).then(res => {
this.clerk = res.data;
sheep.$store('sys').setClerk(res.data);
});
RewardApi.getRewardGiftList({
workerClerkId: options.id,
}).then(res => {
this.giftList = res.data;
});
},
scroll(e) {
this.scrollTop = e.detail.scrollTop;
},
// 监听虚拟列表数组改变并赋值给virtualList进行重新渲染
virtualListChange(vList) {
this.virtualList = vList;
},
queryList(pageNo, pageSize) {
// 组件加载时会自动触发此方法,因此默认页面加载时会自动触发,无需手动调用
// 这里的pageNo和pageSize会自动计算好直接传给服务器即可
// 模拟请求服务器获取分页数据,请替换成自己的网络请求
const params = {
pageNo: pageNo,
pageSize: pageSize,
workerClerkId: this.workerClerkId,
}
if(this.currentTab == 0){
}else if(this.currentTab == 1){
}else if(this.currentTab == 2){
TrendApi.getTrendPage(params).then(res => {
// 将请求的结果数组传递给z-paging
this.$refs.paging.complete(res.data.list);
}).catch(res => {
// 如果请求失败写this.$refs.paging.complete(false);
// 注意每次都需要在catch中写这句话很麻烦z-paging提供了方案可以全局统一处理
// 在底层的网络请求抛出异常时写uni.$emit('z-paging-error-emit');即可
this.$refs.paging.complete(false);
})
}else if(this.currentTab == 3){
CommentApi.getCommentPage(this.workerClerkId,pageNo,pageSize,0).then(res => {
// 将请求的结果数组传递给z-paging
this.$refs.paging.complete(res.data.list);
}).catch(res => {
// 如果请求失败写this.$refs.paging.complete(false);
// 注意每次都需要在catch中写这句话很麻烦z-paging提供了方案可以全局统一处理
// 在底层的网络请求抛出异常时写uni.$emit('z-paging-error-emit');即可
this.$refs.paging.complete(false);
})
}
},
change(e) {
sheep.$store('sys').setClerkTabIndex(e);
this.scrollTop = 0;
this.$refs.paging.reload();
},
doOrder() {
if(this.isLogin){
this.$refs.orderPopup.showPopup();
}else{
showAuthModal();
}
},
doGift() {
if(this.isLogin){
this.$refs.giftPopup.showPopup();
}else{
showAuthModal();
}
},
openGift(e) {
if(this.isLogin){
this.$refs.giftPopup.showGiftPopup(e);
}else{
showAuthModal();
}
},
}
}
</script>
<style lang="scss" scoped>
.data-box {
padding: 0 15px;
}
.bottom-box {
display: flex;
justify-content: center;
align-items: center;
padding: 10px 30px;
box-shadow: 0 0 6px 0 #ccc;
.order {
background-color: var(--ui-BG-Main);
padding: 10px;
border-top-left-radius: 40px;
border-bottom-left-radius: 40px;
color: #fff;
width: 50%;
display: flex;
justify-content: center;
align-items: center;
height: 88rpx;
font-size: 28rpx;
}
.order-btn {
background-color: var(--ui-BG-Main);
padding: 10px;
border-radius: 40px;
color: #fff;
flex: 1;
display: flex;
justify-content: center;
align-items: center;
height: 88rpx;
font-size: 28rpx;
}
.gift {
background-color: #eef3f2;
padding: 10px;
border-top-right-radius: 40px;
border-bottom-right-radius: 40px;
width: 50%;
display: flex;
justify-content: center;
align-items: center;
height: 88rpx;
color: #aaa;
font-size: 28rpx;
}
}
</style>

View File

@@ -0,0 +1,89 @@
<template>
<view>
<tui-navigation-bar @init="initNavigation" @change="opacityChange" :scrollTop="scrollTop" backgroundColor="#fff" color="#333">
<detailNavbar @onTab="onTab" :scrollTop="scrollTop" />
</tui-navigation-bar>
</view>
</template>
<script>
import tuiNavigationBar from "@/components/thorui/tui-navigation-bar/tui-navigation-bar.vue";
import detailNavbar from '@/pages/clerk/detail/components/detail-navbar.vue';
export default {
components: {
tuiNavigationBar,
detailNavbar,
},
props: {
title: {
type: String,
default: '',
},
//滚动条滚动距离
scrollTop: {
type: [Number, String],
default: 0
},
},
data() {
return {
top: 0, //标题图标距离顶部距离
opacity: 0,
height: 0,
}
},
methods: {
initNavigation(e) {
this.height = e.height;
this.opacity = e.opacity;
this.top = e.top;
this.$emit('initNav', e);
},
opacityChange(e) {
this.opacity = e.opacity;
},
onTab(e) {
this.$emit('onTab', e);
},
}
}
</script>
<style lang="scss" scoped>
.content-bpx {
color: #fff;
display: flex;
align-items: center;
height: 44px;
justify-content: center;
.left-box {
display: flex;
position: absolute;
left: 0;
}
.nickname {
position: absolute;
width: 100px;
display: flex;
justify-content: center;
align-items: center;
height: 44px;
font-size: 15px;
font-weight: bolder;
color: #1f2122;
}
.set-btn {
padding: 0 15px;
height: 44px;
display: flex;
align-items: center;
}
}
</style>

View File

@@ -0,0 +1,282 @@
<template>
<tui-bottom-popup :zIndex="1002" :maskZIndex="1001" :show="popupShow" @close="hiddenPopup">
<view class="order-box">
<view class="avatar-box">
<u-image width="140" height="140" border-radius="20" :src="clerk.avatar"></u-image>
<view class="close-span" @click="hiddenPopup">
<u-icon name="close-circle" color="#98a2a1" size="50"></u-icon>
</view>
</view>
<scroll-view style="height: 700rpx;" scroll-y>
<view class="page-box">
<game-box v-model="game" :optionList="clerk.goodsList"></game-box>
<view class="form-box">
<!-- <view class="input-box">
<view class="tag-box">
<u-icon name="tags" color="#f6f6f6" size="34"></u-icon>
<view class="name">你的微信号</view>
</view>
<view class="input-span">
<u-input placeholder="请输入正确的微信号~"></u-input>
</view>
</view>
<view class="input-box">
<view class="textarea-box">
<u-icon name="tags" color="#f6f6f6" size="34"></u-icon>
<view class="name">其它备注</view>
</view>
<view class="textarea-span">
<u-input type="textarea" placeholder="请输入备注内容"></u-input>
</view>
</view> -->
<view class="input-box">
<view class="tag-box">
<u-icon name="tags" color="#f6f6f6" size="34"></u-icon>
<view class="name">购买数量</view>
</view>
<view class="step-span">
<tui-numberbox iconBgColor="var(--ui-BG-Main)" iconColor="#fff" backgroundColor="#fff" :min="1" :value="order.num" @change="change"></tui-numberbox>
</view>
</view>
<!-- <view class="input-box">
<radio-box style="width: 100%;" v-model="payMethod" :optionList="optionList"></radio-box>
</view> -->
</view>
</view>
</scroll-view>
<view class="bottom-box">
<view class="price-box">
<text>总价</text>
<text class="price">{{ fen2yuan(game.price*order.num) }}</text>
<text>钻石</text>
</view>
<view class="btn-box">
<view class="pay-btn" @click="confirm">立即下单</view>
</view>
</view>
</view>
</tui-bottom-popup>
</template>
<script>
import tuiBottomPopup from "@/components/thorui/tui-bottom-popup/tui-bottom-popup.vue"
import tuiNumberbox from "@/components/thorui/tui-numberbox/tui-numberbox.vue"
import RadioBox from '@/pages/clerk/detail/components/radioBox.vue';
import GameBox from '@/pages/clerk/detail/components/gameBox.vue';
import sheep from '@/sheep';
export default {
components: {
tuiBottomPopup,
tuiNumberbox,
RadioBox,
GameBox,
},
props: {
},
data() {
return {
popupShow: false,
optionList: [
{
name: '余额支付',
value: '0',
type: 'number',
number: '1000',
},
{
name: '微信支付',
value: '1',
type: 'icon',
},
],
payMethod: '0',
game: {
goodsId: -1,
price: 0,
},
order: {
num: 1,
goodsId: -1,
price: 0,
},
}
},
computed: {
clerk() {
return sheep.$store('sys').clerk;
},
},
methods: {
//调用此方法显示弹层
showPopup() {
this.popupShow = true
},
hiddenPopup() {
this.popupShow = false
},
change(e) {
this.order.num = e.value
},
// 选中某个单选框时由radio时触发
radioChange(e) {
// console.log(e);
},
// 选中任一radio时由radio-group触发
radioGroupChange(e) {
// console.log(e);
},
fen2yuan(price) {
return (price / 100.0).toFixed(0)
},
confirm() {
this.order.goodsId = this.game.goodsId,
this.order.price = this.game.price*this.order.num
if(this.order.goodsId < 0) {
sheep.$helper.toast('请选择商品');
return;
}
var data = {
"clerkId": this.clerk.id,
'items' : [{"skuId": this.order.goodsId, "count": this.order.num}]
}
this.$u.route({
url: 'pages/order/worker/confirm',
params: {
data: JSON.stringify(data),
}
});
},
}
}
</script>
<style lang="scss" scoped>
.avatar-box {
padding: 10px 0;
display: flex;
justify-content: center;
align-items: center;
.close-span {
position: absolute;
right: 5px;
top: 5px;
width: 70rpx;
height: 70rpx;
display: flex;
justify-content: center;
align-items: center;
}
}
.page-box {
padding: 15px;
padding-bottom: 10px;
.form-box {
margin-bottom: 10px;
.tag-box {
display: flex;
align-items: center;
width: 200rpx;
.name {
font-size: 28rpx;
color: #333;
margin-left: 5px;
}
}
.textarea-box {
height: 20px;
display: flex;
width: 200rpx;
.name {
font-size: 28rpx;
color: #333;
margin-left: 5px;
}
}
.input-box {
display: flex;
margin-bottom: 20px;
.input-span {
background-color: #f6f6f6;
margin-left: 15px;
display: flex;
flex: 1;
padding: 0 12px;
border-radius: 40px;
}
.textarea-span {
background-color: #f6f6f6;
margin-left: 15px;
flex: 1;
padding: 2px 12px;
border-radius: 10px;
}
.step-span {
display: flex;
justify-content: flex-end;
flex: 1;
align-items: center;
}
}
}
}
.bottom-box {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
box-shadow: 0 0 6px 0 #ccc;
.price-box {
color: #fb932c;
font-size: 28rpx;
width: 50%;
display: flex;
justify-content: center;
align-items: center;
.price {
margin-right: 5px;
}
}
.btn-box {
width: 50%;
padding-left: 15px;
.pay-btn {
background-color: var(--ui-BG-Main);
color: #fff;
display: flex;
justify-content: center;
align-items: center;
border-radius: 40px;
font-size: 28rpx;
height: 70rpx;
}
}
}
</style>

View File

@@ -0,0 +1,50 @@
<template>
<view class="photo-box">
<scroll-view scroll-x>
<view class="scroll-box">
<view class="img-box" @click="showImage(item)" v-for="(item,index) in dataList">
<u-image border-radius="20" width="170rpx" height="170rpx" :src="item"></u-image>
</view>
</view>
</scroll-view>
</view>
</template>
<script>
export default {
components: {
},
props: {
dataList: {
type: Array,
default: [],
},
},
data() {
return {
}
},
methods: {
showImage(src) {
uni.previewImage({
indicator: "none",
current: src,
urls: this.dataList,
});
},
}
}
</script>
<style lang="scss" scoped>
.photo-box {
.scroll-box {
display: flex;
}
.img-box {
margin-right: 15px;
}
}
</style>

View File

@@ -0,0 +1,342 @@
<template>
<view>
<view class="item" :id="`zp-id-${item.zp_index}`" :key="item.zp_index" v-for="(item,index) in orderList">
<view class="card">
<view class="right">
<view class="card-header">
<view class="box3">
<view class="box1">
<view class="tag-list">
<!-- <view class="tag">Ta爱玩王者荣耀</view> -->
<view class="tag">发布于 {{item.createTimeStr}}</view>
</view>
</view>
</view>
</view>
<view class="card-content">
<view class="text-box">
<rich-text :nodes="item.content"></rich-text>
</view>
</view>
<view class="image-box" v-if="item.fileType == 0">
<img-box :file="item.file"></img-box>
</view>
<view class="voice-box" v-if="item.fileType == 1">
<voice-play :sec="item.seconds" @tap.stop="playAudio(item)" :isPlay="item.id == playId"></voice-play>
</view>
<view class="video-box" v-if="item.fileType == 2">
<video-box :file="item.file"></video-box>
</view>
<view class="topic-list">
<view class="topic">
<u-icon name="map-fill" size="40" color="#3cc9a4"></u-icon>
<view class="tag-text">{{item.city}}</view>
<u-icon name="arrow-right" size="24" color="#3cc9a4"></u-icon>
</view>
</view>
<view class="card-footer">
<!-- <view class="toolbar">
<u-icon color="#f880ab" name="presentfill" size="38" custom-prefix="iconfont"></u-icon>
<view class="toolbar-text">3</view>
</view> -->
<!-- <view class="toolbar">
<u-icon name="pinglun" size="38" custom-prefix="iconfont"></u-icon>
<view class="toolbar-text">22</view>
</view> -->
<view class="toolbar" @click="thumb(item)">
<u-icon v-if="item.like" name="thumb-up-fill" color="var(--ui-BG-Main)" size="40"></u-icon>
<u-icon v-else name="thumb-up" size="40"></u-icon>
<view class="toolbar-text" v-if="item.likeNum > 0">{{item.likeNum}}</view>
<view class="text" v-else>点赞</view>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import ImgBox from '@/pages/tabbar/components/trend/imgBox.vue';
import VideoBox from '@/pages/tabbar/components/trend/videoBox.vue';
import VoicePlay from '@/pages/tabbar/components/trend/voicePlay.vue';
import sheep from '@/sheep';
import TrendApi from '@/sheep/api/worker/trend';
const audio = uni.createInnerAudioContext();
export default {
components: {
ImgBox,
VideoBox,
VoicePlay,
},
props: {
virtualList: {
type: Array,
default: [],
},
},
data() {
return {
playId: null,
}
},
computed: {
orderList() {
this.virtualList.forEach((order) => order.createTimeStr = sheep.$helper.timeFormat(order.createTime, 'yyyy-mm-dd hh:MM'));
return this.virtualList;
},
},
methods: {
playAudio(e) {
if(this.playId == e.id){
this.playId = null;
audio.stop();
return;
}
this.playId = e.id;
//语音自然播放结束
audio.onEnded((res) => {
this.playId = null;
});
audio.src = e.file;
audio.play();
},
thumb(e) {
TrendApi.createTrendLike({
trendId: e.id,
}).then((res) => {
if(res) {
if(e.like){
e.like = false;
e.likeNum = e.likeNum-1;
sheep.$helper.toast('取消点赞');
}else{
e.like = true;
e.likeNum = e.likeNum+1;
sheep.$helper.toast('点赞成功');
}
}
});
},
fans(e) {
TrendApi.createClerkFans({
workerClerkId: e.workerClerkId,
}).then((res) => {
if(res){
if(e.fans){
sheep.$helper.toast('取消收藏');
e.fans = false;
}else{
sheep.$helper.toast('收藏成功');
e.fans = true;
}
}
});
},
detail(e) {
this.$u.route({
url: 'pages/clerk/detail/index',
params: {
id: e.workerClerkId,
}
});
},
}
}
</script>
<style lang="scss" scoped>
.item {
display: flex;
flex-direction: column;
}
.card {
padding: 30rpx 20rpx;
border-bottom: 1rpx solid #f5f5f5;
display: flex;
.right {
display: flex;
flex-direction: column;
flex: 1;
}
}
.avatar-img{
margin-right: 5px;
width: 110rpx;
height: 110rpx;
position: relative;
display: flex;
justify-content: center;
align-items: center;
padding: 8rpx;
.img {
width: 100%;
height: 100%;
border-radius: 100%;
}
}
.avatar{
margin-right: 5px;
width: 110rpx;
height: 110rpx;
position: relative;
display: flex;
justify-content: center;
align-items: center;
.img {
width: 75%;
height: 75%;
border-radius: 100%;
}
.gif {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
width: 100%;
height: 100%;
}
}
.card-header {
display: flex;
align-items: center;
.box1 {
.tag-list {
display: flex;
align-items: center;
font-size: 22rpx;
margin-top: 4rpx;
.tag::after {
content: '·';
padding: 0 10rpx; /* 添加一些间隔 */
}
}
.tag-list .tag:last-child::after {
content: unset;
}
}
.box2 {
display: flex;
align-items: center;
line-height: 40rpx;
.nickname {
font-size: 28rpx;
color: #333333;
}
.sex-box {
display: flex;
align-items: center;
justify-content: center;
background-color: #d06b8d1f;
border-radius: 20px;
font-size: 20rpx;
padding: 4rpx 10rpx;
color: #d06b8d;
line-height: 24rpx;
margin: 0 5px;
.age {
margin-left: 2px;
}
}
.nan {
background-color: #007aff1a;
color: #007aff;
}
}
.box3 {
display: flex;
justify-content: space-between;
flex: 1;
align-items: center;
}
.fans {
width: 60rpx;
display: flex;
justify-content: flex-end;
height: 60rpx;
align-items: center;
color: var(--ui-BG-Main);
font-size: 24rpx;
padding-right: 3px;
}
}
.card-content {
margin-top: 20rpx;
margin-bottom: 10rpx;
.text-box {
font-size: 26rpx;
line-height: 180%;
white-space: pre-wrap;
letter-spacing: 2rpx;
}
}
.topic-list {
display: flex;
align-items: center;
margin-top: 10px;
.topic {
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
margin-right: 20rpx;
color: var(--ui-BG-Main);
font-weight: bold;
.tag-text {
margin-left: 2px;
}
}
}
.card-footer {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 30rpx;
.toolbar {
display: flex;
align-items: center;
justify-content: flex-end;
color: #666666;
width: 80rpx;
height: 40rpx;
flex: 1;
}
.toolbar-text {
font-size: 26rpx;
margin-left: 5px;
}
.text {
font-size: 24rpx;
margin-left: 5px;
}
}
</style>

View File

@@ -0,0 +1,50 @@
<template>
<view class="price-box">
<table-list :tableList="clerk.goodsList"></table-list>
<view class="price-remark">
<u-parse :html="content"></u-parse>
</view>
</view>
</template>
<script>
import TableList from '@/pages/clerk/detail/components/tableList.vue';
export default {
components: {
TableList,
},
props: {
clerk: {
type: Object,
default: {},
},
},
data() {
return {
content: `
<p>露从今夜白,月是故乡明</p>
`
}
},
methods: {
}
}
</script>
<style lang="scss" scoped>
.price-box {
padding: 30rpx 20rpx;
padding-top: 0px;
.price-remark {
padding: 5px 15px;
}
}
</style>

View File

@@ -0,0 +1,112 @@
<template>
<view class="check-box">
<view class="check-option" @click="change(option)" v-for="(option,index) in optionList">
<view class="icon-box">
<view class="check-icon-span" v-if="option.value == valueDom">
<u-icon name="checkmark-circle-fill" color="var(--ui-BG-Main)" size="50"></u-icon>
</view>
<text class="check-icon" v-else></text>
<text class="check-label">{{option.name}}</text>
</view>
<view class="price-box" v-if="option.type == 'number'">
<text class="num">{{option.number}}</text>
<text>钻石</text>
</view>
<view v-if="option.type == 'icon'">
<u-icon name="weixin-fill" color="#39b54a" size="50"></u-icon>
</view>
</view>
</view>
</template>
<script>
export default {
components: {
},
props: {
optionList: {
type: Array,
default: [],
},
modelValue: {
type: String,
default: ''
},
},
data() {
return {
}
},
computed: {
valueDom() {
return this.modelValue;
},
},
methods: {
change(e) {
this.$emit('update:modelValue', e.value);
},
}
}
</script>
<style lang="scss" scoped>
.check-box {
display: flex;
flex-direction: column;
flex: 1;
.check-option {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
.icon-box {
display: flex;
align-items: center;
.check-icon {
width: 40rpx;
height: 40rpx;
border-radius: 100%;
border: 1px solid var(--ui-BG-Main);
display: flex;
justify-content: center;
align-items: center;
}
.check-icon-span {
width: 40rpx;
height: 44rpx;
display: flex;
justify-content: center;
align-items: center;
}
.check-label {
font-size: 28rpx;
color: var(--ui-BG-Main);
margin-left: 5px;
}
}
.price-box {
font-size: 26rpx;
color: var(--ui-BG-Main);
.num {
font-size: 40rpx;
margin-right: 2px;
}
}
}
.check-option:last-child {
margin-bottom: 0;
}
}
</style>

View File

@@ -0,0 +1,125 @@
<template>
<view :id="`zp-id-${item.zp_index}`" :key="item.zp_index" v-for="(item,index) in orderList" class="card-box">
<view class="avatar-box">
<u-avatar v-if="item.anonymous" size="80"></u-avatar>
<u-avatar v-else size="80" :src="item.userAvatar"></u-avatar>
<view class="nickname-box">
<view class="nickname">
<text class="text">{{item.userNickname}}</text>
<uni-rate :readonly="true" v-model="item.scores" size="14" />
</view>
<view class="time">{{item.createTimeStr}}</view>
</view>
</view>
<view class="spu-box">
<text class="tag">{{clerk.nickname}}</text>
<text class="tag">服务类型{{item.skuName}}</text>
</view>
<view class="content">
<rich-text :nodes="item.content"></rich-text>
</view>
<view class="img-box" v-if="item.picUrls && item.picUrls.length > 0">
<img-box :file="item.picUrls.join(',')"></img-box>
</view>
</view>
</template>
<script>
import ImgBox from '@/pages/tabbar/components/trend/imgBox.vue';
import sheep from '@/sheep';
export default {
components: {
ImgBox
},
props: {
clerk: {
type: Object,
default: {},
},
virtualList: {
type: Array,
default: [],
},
},
data() {
return {
}
},
computed: {
orderList() {
this.virtualList.forEach((order) => order.createTimeStr = sheep.$helper.timeFormat(order.createTime, 'yyyy-mm-dd'));
this.virtualList.forEach(function(e){
if(e.skuProperties){
var skuNameList = [];
e.skuProperties.forEach(function(sku){
skuNameList.push(sku.valueName);
});
e.skuName = skuNameList.join('-');
}
});
return this.virtualList;
},
},
methods: {
}
}
</script>
<style lang="scss" scoped>
.card-box {
padding: 30rpx 20rpx;
border-bottom: 1rpx solid #eeeeee;
}
.avatar-box {
display: flex;
align-items: center;
.nickname-box {
font-size: 24rpx;
margin-left: 10px;
.nickname {
letter-spacing: 0.0625rem;
display: flex;
align-items: center;
.text {
letter-spacing: 2rpx;
margin-right: 10px;
}
}
.time {
color: rgb(164, 164, 164);
font-size: 22rpx;
margin-top: 3px;
}
}
}
.spu-box {
font-size: 24rpx;
color: rgb(164, 164, 164);
margin: 10px 0;
letter-spacing: 2rpx;
.tag {
margin-right: 20rpx;
}
}
.content {
font-size: 26rpx;
line-height: 180%;
white-space: pre-wrap;
letter-spacing: 2rpx;
}
</style>

View File

@@ -0,0 +1,310 @@
<template>
<view>
<view v-if="isGift" class="star-box">
<view class="title-box">
<view class="title">礼物墙</view>
<view class="num">(已点亮{{starNum}}/{{total}})</view>
</view>
<view class="list-box">
<view @click="sendGift(item)" v-for="(item,index) in virtualList" :key="item.id" class="gift">
<view class="img-box">
<img class="img" :class="item.playStatus == 0 ? 'hui' : ''" :src="item.img"></img>
<view class="tag" v-if="item.giftType == 1">
<text>特效</text>
<text v-if="item.tag">·{{item.tag}}</text>
</view>
<view class="tag" v-if="item.giftType == 0 && item.tag">
<text>{{item.tag}}</text>
</view>
</view>
<view class="name">{{item.name}}</view>
<view class="price">x {{item.playNum}}</view>
<view class="btn" @tap.stop="openGift(item)" v-if="item.playStatus == 0">点亮</view>
<view class="btn" @tap.stop="openGift(item)" v-if="item.playStatus == 1">赠送</view>
<view class="btn" v-if="item.playStatus == 2">已下架</view>
</view>
</view>
</view>
<view class="svga-box" :class="giftFlag ? 'svga-show': 'svga-hide'">
<c-svga ref="cSvgaRef" :canvasId='canvasId' :src="src" :loops='0' :auto-play="false" @frame='onFrame' @finished='onFinished' @percentage='onPercentage' @loaded='onLoaded'></c-svga>
<view class="close-btn" @click="closeSvga">
<view class="bottom-box">
<view class="title">{{gift.name}}</view>
<view class="price">{{ fen2yuan(gift.money) }} 钻石</view>
<view class="btn-box">
<view class="btn" @click="cannel">取消</view>
<view v-if="gift.playStatus == 0" class="btn active" @click="ok">点亮</view>
<view v-if="gift.playStatus == 1" class="btn active" @click="ok">赠送</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import sheep from '@/sheep';
export default {
components: {
},
props: {
virtualList: {
type: Array,
default: [],
},
},
data() {
return {
giftFlag: false,
src: '',
canvasId: 'myCanvas',
gift: {},
}
},
computed: {
starNum() {
var count = 0;
this.virtualList.forEach(e => {
if(e.playStatus > 0){
count ++;
}
});
return count;
},
total() {
return this.virtualList.length;
},
isGift() {
return sheep.$store('user').tradeConfig.giftEnabled;
},
},
methods: {
sendGift(e) {
this.gift = e;
if(e.giftType == 0){
// 普通礼物不播放
this.$emit('openGift', this.gift);
return;
}
this.src = e.pic;
this.giftFlag = true;
},
onFinished() {
this.giftFlag = false;
console.log('动画停止播放时回调');
},
onFrame(frame) {//动画播放至某帧后回调
// console.log(frame);
},
onPercentage(percentage) { //动画播放至某进度后回调
// console.log(percentage);
},
onLoaded() {
this.$refs.cSvgaRef.call('setContentMode', 'AspectFill');
console.log('加载完成');
this.$refs.cSvgaRef.call('startAnimation');
},
closeSvga() {
this.src = "";
this.$refs.cSvgaRef.call('stopAnimation');
this.giftFlag = false;
},
fen2yuan(price) {
var f = 0;
var p = (price / 100.0).toFixed(0);
var p1 = (price / 100.0).toFixed(1);
var p2 = (price / 100.0).toFixed(2);
if(p*100 == price){
f = 0;
}else if(p1*100 == price){
f = 1;
}else if(p2*100 == price){
f = 2;
}
return (price / 100.0).toFixed(f)
},
cannel() {
this.closeSvga();
},
ok() {
this.closeSvga();
this.$emit('openGift', this.gift);
},
openGift(e) {
this.$emit('openGift', e);
},
}
}
</script>
<style lang="scss" scoped>
.star-box {
padding: 30rpx 20rpx;
.title-box {
display: flex;
align-items: center;
.title {
font-size: 28rpx;
margin-right: 10px;
}
.num {
font-size: 24rpx;
color: var(--ui-BG-Main);
}
}
.list-box {
display: flex;
flex-wrap: wrap;
padding-top: 30rpx;
.gift {
width: 25%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 20rpx 0px;
.img-box {
position: relative;
.img{
max-width: 150rpx;
height: 150rpx;
}
.hui {
filter: grayscale(100%);
}
.tag {
position: absolute;
top: 0;
right: 0;
font-size: 16rpx;
color: #fff;
background-color: var(--ui-BG-Main);
border-radius: 40px;
padding: 2rpx 8rpx;
}
}
.name {
font-size: 24rpx;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
max-width: 150rpx;
margin-top: 2px;
margin-bottom: 7px;
}
.price {
font-size: 20rpx;
color: var(--ui-BG-Main);
margin-bottom: 7px;
}
.btn {
border: 1px solid var(--ui-BG-Main);
border-radius: 40px;
color: var(--ui-BG-Main);
padding: 8rpx 20rpx;
font-size: 24rpx;
}
}
}
}
.svga-box {
position: fixed;
top: 0;
left: 0;
z-index: 999999999;
width: 100%;
height: 100%;
background-color: black;
.close-btn {
color: #fff;
position: absolute;
z-index: 999999999;
padding: 5px 10px;
left: 0;
right: 0;
bottom: 100rpx;
.bottom-box {
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
.title {
font-size: 28rpx;
margin-bottom: 5px;
}
.price {
font-size: 24rpx;
}
.btn-box {
display: flex;
align-items: center;
margin-top: 10px;
.btn {
border: 1px solid #fff;
font-size: 28rpx;
padding: 20rpx 110rpx;
border-radius: 40px;
margin: 15px;
color: #fff;
}
.active {
border: 1px solid var(--ui-BG-Main);
background-color: var(--ui-BG-Main);
}
}
}
}
}
.svga-hide {
/* #ifdef MP */
transform: translate(-100%, 0);
/* #endif */
/* #ifndef MP */
display: none;
/* #endif */
}
.svga-show {
/* #ifdef MP */
transform: translate(0, 0);
/* #endif */
/* #ifndef MP */
display: block;
/* #endif */
}
</style>

View File

@@ -0,0 +1,64 @@
<template>
<view>
<view class="sticky-box">
<view>
<tui-tabs :tabs="tabList" badgeBgColor="var(--ui-BG-Main)" selectedColor="var(--ui-BG-Main)" sliderBgColor="var(--ui-BG-Main)" :currentTab="currentTab" @change="change"></tui-tabs>
</view>
</view>
</view>
</template>
<script>
import tuiTabs from "@/components/thorui/tui-tabs/tui-tabs.vue"
import sheep from '@/sheep';
export default {
components: {
tuiTabs,
},
props: {
clerk: {
type: Object,
default: {},
},
},
data() {
return {
}
},
computed: {
tabList() {
var tabs = [{
name: '礼物墙'
},{
name: '价格'
}, {
name: '动态',
num: this.clerk.trendNum,
}, {
name: '评价',
num: this.clerk.commentNum,
}];
return tabs;
},
currentTab() {
return sheep.$store('sys').clerkTabIndex;
},
},
created() {
},
methods: {
change(e) {
this.$emit('change', e.index);
},
}
}
</script>
<style lang="scss" scoped>
.sticky-box {
background-color: #fff;
padding: 5px 0;
}
</style>

View File

@@ -0,0 +1,100 @@
<template>
<view class="tn-table-class tn-table" :class="[tableClass]" :style="[tableStyle]">
<slot></slot>
</view>
</template>
<script>
export default {
name: 'table-bd',
props: {
// 边框宽度
borderWidth: {
type: [String, Number],
default: ''
},
// 边框颜色
borderColor: {
type: String,
default: ''
},
// 显示上边框
borderTop: {
type: Boolean,
default: true
},
// 显示右边框
borderRight: {
type: Boolean,
default: false
},
// 显示下边框
borderBottom: {
type: Boolean,
default: false
},
// 显示左边框
borderLeft: {
type: Boolean,
default: true
}
},
computed: {
parentData() {
return [this.borderWidth, this.borderColor]
},
tableClass() {
let clazz = ''
return clazz
},
tableStyle() {
let style = {}
if (this.borderWidth !== '') {
style.borderWidth = this.borderWidth + 'rpx'
}
if (this.borderColor) {
style.borderColor = this.borderColor
}
if (this.borderLeft) {
style.borderLeftStyle = 'solid'
}
if (this.borderRight) {
style.borderRightStyle = 'solid'
}
if (this.borderTop) {
style.borderTopStyle = 'solid'
}
if (this.borderBottom) {
style.borderBottomStyle = 'solid'
}
return style
}
},
data() {
return {}
},
created() {
this.children = []
},
watch: {
parentData() {
// 更新子组件的数据
if (this.children.length) {
this.children.map((child) => {
typeof(child.updateParentData) === 'function' && child.updateParentData()
})
}
}
}
}
</script>
<style lang="scss" scoped>
.tn-table {
box-sizing: border-box;
width: 100%;
border-width: 1rpx;
border-style: none;
border-color: var(--ui-BG-Main);;
}
</style>

View File

@@ -0,0 +1,305 @@
<template>
<view class="tn-td-class tn-td" :class="[tdClass]" :style="[tdStyle]" @tap.stop="handleClick">
<slot></slot>
</view>
</template>
<script>
export default {
name: 'table-td',
options: {
// 在微信小程序中将组件节点渲染为虚拟节点更加接近Vue组件的表现(不会出现shadow节点下再去创建元素)
virtualHost: true
},
props: {
// 占整个表格的宽度跨度
// [1-24]
span: {
type: Number,
default: 24
},
// 宽度
// 优先级比span高
width: {
type: [String, Number],
default: ''
},
// 高度
height: {
type: [String, Number],
default: ''
},
// 字体加粗
bold: {
type: Boolean,
default: false
},
// 格内边距
padding: {
type: String,
default: ''
},
// 边框颜色
borderColor: {
type: String,
default: ''
},
// 边框宽度
borderWidth: {
type: [String, Number],
default: ''
},
// 左边框
borderLeft: {
type: Boolean,
default: false
},
// 下边框
borderBottom: {
type: Boolean,
default: false
},
// 右边框
borderRight: {
type: Boolean,
default: true
},
// 文字超出隐藏
ellipsis: {
type: Boolean,
default: false
},
// 文本对齐方式
// left center right
textAlign: {
type: String,
default: 'left'
},
// 排列方式
// left center right
alignItems: {
type: String,
default: 'left'
},
// 收缩表格
shrink: {
type: Boolean,
default: true
},
// 铺满剩余空间
grow: {
type: Boolean,
default: false
},
// 隐藏
hidden: {
type: Boolean,
default: false
},
// 固定列数据
fixed: {
type: Boolean,
default: false
},
// zIndex
zIndex: {
type: Number,
default: 0
},
// 列数
index: {
type: [String, Number],
default: 0
},
// keys
keys: {
type: [String, Number],
default: ''
}
},
computed: {
tdClass() {
let clazz = ''
clazz += `${this.ellipsis ? 'tn-td--ellipsis' : 'tn-td--normal'}`
/* if (this.backgroundColorClass) {
clazz += ` ${this.backgroundColorClass}`
}
if (this.fontColorClass) {
clazz += ` ${this.fontColorClass}`
} */
if (this.alignItems) {
clazz += ` tn-td--${this.alignItems}`
}
if (this.textAlign) {
clazz += ` tn-td__text--${this.textAlign}`
}
if (!this.shrink) {
clazz += ' tn-td--shrink'
}
if (this.grow) {
clazz += ' tn-td--grow'
}
if (this.hidden) {
clazz += ' tn-td--hidden'
}
return clazz
},
tdStyle() {
let style = {}
/* if (this.backgroundColorStyle) {
style.backgroundColor = this.backgroundColorStyle
} */
/* if (this.fontColorStyle) {
style.color = this.fontColorStyle
} */
/* if (this.fontSizeStyle) {
style.fontSize = this.fontSizeStyle
} */
style.width = this.getWidth()
if (this.height) {
style.height = this.height + 'rpx';
}
style.fontWeight = this.bold ? 'bold' : 'normal'
if (this.padding) {
style.padding = this.padding
}
if (this.borderWidth !== '' || this.parentData.borderWidthValue !== '') {
style.borderWidth = this.borderWidth !== '' ? this.borderWidth+'rpx' : this.parentData.borderWidthValue + 'rpx'
}
if (this.borderColor || this.parentData.borderColorValue) {
style.borderColor = this.borderColor || this.parentData.borderColorValue
}
if (this.borderLeft) {
style.borderLeftStyle = 'solid'
}
if (this.borderRight) {
style.borderRightStyle = 'solid'
}
if (this.borderBottom) {
style.borderBottomStyle = 'solid'
}
if (this.fixed) {
style.zIndex = this.zIndex ? this.zIndex : 1000
}
return style
}
},
data() {
return {
parentData: {
borderColorValue: null,
borderWidthValue: null
}
}
},
created() {
this.parent = false
this.updateParentData()
this.parent && this.parent.children.push(this)
},
methods: {
// 获取表格宽度
getWidth() {
if (this.width) {
return this.width + 'rpx'
}
return [
'4.16666667%',
'8.33333333%',
'12.5%',
'16.66666667%',
'20.83333333%',
'25%',
'29.16666667%',
'33.33333333%',
'37.5%',
'41.66666667%',
'45.83333333%',
'50%',
'54.16666667%',
'58.33333333%',
'62.5%',
'66.66666667%',
'70.83333333%',
'75%',
'79.16666667%',
'83.33333333%',
'87.5%',
'91.66666667%',
'95.83333333%',
'100%'
][this.span - 1]
},
// 点击事件
handleClick() {
this.$emit('click', {
index: this.index,
key: this.keys
})
},
// 更新父组件信息
updateParentData() {
this.getParentData('tn-tr')
}
}
}
</script>
<style lang="scss" scoped>
.tn-td {
box-sizing: border-box;
position: relative;
word-break: break-all;
background-color: transparent;
height: auto;
padding: 12rpx;
border-width: 1rpx;
border-style: none;
border-color: var(--ui-BG-Main);
&--normal {
display: inline-flex;
align-items: center;
}
&--ellipsis {
display: inline-block;
overflow: hidden;
white-space: nowrap !important;
text-overflow: ellipsis;
}
&--shrink {
flex-shrink: 0;
}
&--grow {
flex-grow: 1;
}
&--left {
justify-content: flex-start;
}
&--center {
justify-content: center;
}
&--right {
justify-content: flex-end;
}
&__text {
&--left {
text-align: left;
}
&--center {
text-align: center;
}
&--right {
text-align: right;
}
}
&--hidden {
visibility: hidden;
}
}
</style>

View File

@@ -0,0 +1,208 @@
<template>
<view class="tn-tr-class tn-tr" :class="[trClass]" :style="[trStyle]">
<slot></slot>
</view>
</template>
<script>
export default {
name: 'table-tr',
options: {
// 在微信小程序中将组件节点渲染为虚拟节点更加接近Vue组件的表现(不会出现shadow节点下再去创建元素)
virtualHost: true
},
props: {
// 宽度
width: {
type: [String, Number],
default: ''
},
// 边框颜色
borderColor: {
type: String,
default: ''
},
// 边框宽度
borderWidth: {
type: [String, Number],
default: ''
},
// 左边框
borderLeft: {
type: Boolean,
default: false
},
// 上边框
borderTop: {
type: Boolean,
default: false
},
// 换行显示
wrap: {
type: Boolean,
default: false
},
// 固定表格
fixed: {
type: Boolean,
default: false
},
// left偏移值
left: {
type: [String, Number],
default: 0
},
// right偏移值
right: {
type: [String, Number],
default: 0
},
// top偏移值(自定义顶部导航栏时用到)
top: {
type: [String, Number],
default: 0
},
// 外边距
margin: {
type: String,
default: ''
},
// zIndex
zIndex: {
type: Number,
default: 0
},
// 行数索引
index: {
type: [String, Number],
default: 0
},
// 参数
params: {
type: String,
default: ''
}
},
computed: {
borderWidthValue() {
return this.borderWidth || this.parentData.borderWidth || ''
},
borderColorValue() {
return this.borderColor || this.parentData.borderColor || ''
},
trClass() {
let clazz = ''
/* if (this.backgroundColorClass) {
clazz += ` ${this.backgroundColorClass}`
}
if (this.fontColorClass) {
clazz += ` ${this.fontColorClass}`
} */
if (this.wrap) {
clazz += ' tn-tr--wrap'
}
if (this.fixed) {
clazz += ' tn-tr--fixed'
}
return clazz
},
trStyle() {
let style = {}
if (this.width) {
style.width = this.width + 'rpx'
}
/* if (this.backgroundColorStyle) {
style.backgroundColor = this.backgroundColorStyle
}
if (this.fontColorStyle) {
style.color = this.fontColorStyle
}
if (this.fontSizeStyle) {
style.fontSize = this.fontSizeStyle
} */
if (this.borderWidth !== '' || this.parentData.borderWidth !== '') {
style.borderWidth = this.borderWidth !== '' ? this.borderWidth + 'rpx' : this.parentData.borderWidth + 'rpx'
}
if (this.borderColor || this.parentData.borderColor) {
style.borderColor = this.borderColor || this.parentData.borderColor
}
if (this.borderLeft) {
style.borderLeftStyle = 'solid'
}
if (this.borderTop) {
style.borderTopStyle = 'solid'
}
if (this.fixed) {
style.left = this.left ? this.left + 'rpx' : 'auto'
style.right = this.right ? this.right + 'rpx' : 'auto'
style.top = this.top ? this.top + 'rpx' : 'auto'
}
if (this.margin) {
style.margin = this.margin
}
style.zIndex = this.zIndex ? this.zIndex : 1000
return style
}
},
data() {
return {
parentData: {
borderColor: null,
borderWidth: null
}
}
},
watch: {
parentData: {
handler() {
// 更新子组件的数据
if (this.children.length) {
this.children.map((child) => {
typeof(child.updateParentData) === 'function' && child.updateParentData()
})
}
},
deep: true
}
},
created() {
this.children = []
this.parent = false
this.updateParentData()
this.parent && this.parent.children.push(this)
},
methods: {
handleClick() {
this.$emit('click', {
index: this.index,
params: this.params
})
},
// 更新父组件信息
updateParentData() {
this.getParentData('tn-table')
}
}
}
</script>
<style lang="scss" scoped>
.tn-tr {
width: 100%;
display: flex;
box-sizing: border-box;
background-color: #FFFFFF;
border-width: 1rpx;
border-style: none none solid none;
border-color: var(--ui-BG-Main);;
&--wrap {
flex-wrap: wrap;
}
&--fixed {
position: fixed;
}
}
</style>

View File

@@ -0,0 +1,98 @@
<template>
<view>
<table-bd style="width: 100%;">
<table-tr>
<table-td :span="18" height="80rpx" :fontSize="30" :bold="true" fontColor="#01BEFF" alignItems="center">服务项目</table-td>
<table-td :span="6" height="80rpx" :fontSize="30" :bold="true" fontColor="#01BEFF" alignItems="center">价格</table-td>
</table-tr>
<block v-for="(tb,t) in tableList">
<table-tr>
<table-td :span="6" alignItems="center">{{tb.name}}</table-td>
<block v-if="tb.goodsList">
<table-td :span="12" :ellipsis="true" padding="0">
<block v-for="(tb2,t2) in tb.goodsList">
<table-tr :borderWidth="t2 == tb.goodsList.length-1 ? 0 : 1">
<table-td :span="24" :borderRight="false" alignItems="center">{{tb2.name}}</table-td>
</table-tr>
</block>
</table-td>
<table-td :span="6" :ellipsis="true" padding="0">
<block v-for="(tb2,t2) in tb.goodsList">
<table-tr :borderWidth="t2 == tb.goodsList.length-1 ? 0 : 1">
<table-td :span="24" borderWidth="1rpx" :borderRight="false" alignItems="center">{{ fen2yuan(tb2.price) }}</table-td>
</table-tr>
</block>
</table-td>
</block>
<block v-if="tb.categoryList">
<table-td :span="12" :ellipsis="true" padding="0">
<block v-for="(tb2,t2) in tb.categoryList">
<table-tr :borderWidth="t2 == tb.categoryList.length-1 ? 0 : 1">
<table-td :span="12" borderWidth="1rpx" alignItems="center">{{tb2.name}}</table-td>
<table-td :span="12" :ellipsis="true" :borderRight="false" padding="0">
<block v-for="(tb3,t3) in tb2.goodsList">
<table-tr :borderWidth="t3 == tb2.goodsList.length-1 ? 0 : 1">
<table-td :span="24" :borderRight="false" alignItems="center">{{tb3.name}}</table-td>
</table-tr>
</block>
</table-td>
</table-tr>
</block>
</table-td>
<table-td :span="6" :ellipsis="true" padding="0">
<block v-for="(tb2,t2) in tb.categoryList">
<block v-for="(tb3,t3) in tb2.goodsList">
<table-tr :borderWidth="t2 == tb.categoryList.length-1 && t3 == tb2.goodsList.length-1 ? 0 : 1">
<table-td :span="24" borderWidth="1rpx" :borderRight="false" alignItems="center">{{ fen2yuan(tb3.price) }}</table-td>
</table-tr>
</block>
</block>
</table-td>
</block>
</table-tr>
</block>
</table-bd>
</view>
</template>
<script>
import TableBd from '@/pages/clerk/detail/components/table-bd.vue';
import TableTd from '@/pages/clerk/detail/components/table-td.vue';
import TableTr from '@/pages/clerk/detail/components/table-tr.vue';
export default {
components: {
TableBd,
TableTd,
TableTr,
},
props: {
tableList: {
type: Array,
default: [],
},
},
data() {
return {
}
},
methods: {
fen2yuan(price) {
return (price / 100.0).toFixed(0)
},
}
}
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,270 @@
<template>
<view class="user-box" :style="`background-size: contain;background-image: url(${clerk.avatar});`">
<view class="info-box" :style="{ paddingTop: paddingTop + 20 + 'px'}">
<view class="avatar-box">
<view class="avatar-span">
<u-image width="100%" height="100%" shape="circle" :src="clerk.avatar"></u-image>
<view class="fans-btn" @click="fans(clerk)">
<u-icon v-if="clerk.isFans" color="var(--ui-BG-Main)" name="checkmark-circle-fill" size="46"></u-icon>
<u-icon v-else color="var(--ui-BG-Main)" name="plus-circle-fill" size="46"></u-icon>
</view>
</view>
<view class="right-box">
<view class="nickname-box">
<view class="nickname">{{clerk.nickname}}</view>
<view class="icon">
<u-icon name="wode_duanwei" size="36" custom-prefix="iconfont"></u-icon>
<text>{{clerkLevel.name}}</text>
</view>
</view>
<view class="online" v-if="clerk.onlineStatus">可接单</view>
<view class="online" v-else>休息中</view>
</view>
</view>
<view class="sex-box">
<view class="span" v-if="clerk.sex == 1">
<u-icon name="ziyuan2" color="#f898c5" size="34" custom-prefix="iconfont"></u-icon>
</view>
<view class="span" v-if="clerk.sex == 0">
<u-icon name="ziyuan3" color="#0081ff" size="34" custom-prefix="iconfont"></u-icon>
</view>
<view class="span">{{clerk.age}}</view>
<!-- <view class="span">{{clerk.city}}</view> -->
</view>
<view class="sign-box">{{clerk.intro}}</view>
<view class="voice-box" v-if="clerk.sound">
<view class="voice-span">
<voice-play :sec="clerk.soundTime" :src="clerk.sound"></voice-play>
</view>
<!-- <view class="span">全声线</view>
<view class="span">御姐音</view>
<view class="span">夹子音</view> -->
</view>
<view class="tag-list">
<view class="tag-box">
<span class="tag" v-for="(categoryName,index) in clerk.categoryNameList">{{categoryName}}</span>
</view>
</view>
<!-- <view class="tags-box">
<view class="span">搞笑</view>
<view class="span">风骚走位</view>
<view class="span">乖巧懂事🍑 极致体验</view>
</view> -->
<photo-box v-if="clerk.albums" :dataList="clerk.albums.split(',')"></photo-box>
</view>
</view>
</template>
<script>
import VoicePlay from '@/pages/clerk/detail/components/voicePlay.vue';
import PhotoBox from '@/pages/clerk/detail/components/photoBox.vue';
import sheep from '@/sheep';
import TrendApi from '@/sheep/api/worker/trend';
export default {
components: {
VoicePlay,
PhotoBox,
},
props: {
paddingTop: {
type: Number,
default: 0,
},
clerk: {
type: Object,
default: {},
},
clerkLevel: {
type: Object,
default: {},
},
},
data() {
return {
photo: 'http://cos.duopei.feiniaowangluo.com/34/clerk/1707236152609pJKyvtedS9.jpg,http://cos.duopei.feiniaowangluo.com/34/clerk/1720278125689P34fP9MU1c.jpg?imageView2/1/w/200/h/200',
}
},
methods: {
fans(e) {
TrendApi.createClerkFans({
workerClerkId: e.id,
}).then((res) => {
if(res){
if(e.isFans){
sheep.$helper.toast('取消关注');
e.isFans = false;
}else{
sheep.$helper.toast('关注成功');
e.isFans = true;
}
}
});
},
}
}
</script>
<style lang="scss" scoped>
.info-box {
backdrop-filter: blur(10px);
background-color: rgba(0, 0, 0, 0.5);
padding: 15px;
.avatar-box {
display: flex;
align-items: center;
.avatar-span {
border-radius: 100%;
width: 140rpx;
height: 140rpx;
border: 2px solid #fff;
position: relative;
.fans-btn {
position: absolute;
right: 0;
bottom: 0;
}
}
.right-box {
color: #fff;
display: flex;
justify-content: space-between;
flex: 1;
align-items: center;
margin-left: 10px;
.nickname {
font-size: 34rpx;
font-weight: bold;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
max-width: 320rpx;
}
.icon {
background-color: #ff5ebd;
display: flex;
justify-content: center;
align-items: center;
border-radius: 40px;
font-weight: bold;
color: #fff;
font-size: 22rpx;
line-height: 22rpx;
padding: 5px 5px;
margin-top: 10px;
}
.online {
border-bottom: 1px solid #fff;
font-size: 28rpx;
}
}
}
.sex-box {
color: #fff;
display: flex;
align-items: center;
line-height: 30px;
.span {
margin-right: 5px;
font-size: 26rpx;
}
}
.sign-box {
color: #cfcfcf;
font-size: 28rpx;
margin-top: 10px;
margin-bottom: 10px;
}
.voice-box {
display: flex;
flex-wrap: wrap;
align-items: center;
.voice-span {
margin-bottom: 10px;
margin-right: 10px;
}
.span {
padding: 10rpx 20rpx;
background-color: rgba(132, 167, 164, .81);
border-radius: 20px;
font-size: 24rpx;
margin-bottom: 10px;
margin-right: 10px;
color: #fff;
}
}
.tags-box {
display: flex;
flex-wrap: wrap;
align-items: center;
margin: 5px 0;
.span {
font-size: 20rpx;
color: #fff;
background-color: #000000;
border-radius: 999px;
padding: 3px 10px;
margin: 0 10px 10px 0;
}
}
.tag-list {
display: flex;
margin: 5px 0;
}
.tag-box {
display: inline-block;
align-items: center;
margin-bottom: 5px;
background-color: #eef2f26e;
border-radius: 3px;
color: #fff;
font-size: 12px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
max-width: 100%;
padding: 5px 0;
.tag {
white-space: nowrap;
padding: 5px 5px;
position: relative;
}
.tag:after {
content: ' ';
border-left: 1px solid #fff;
display: inline-block;
height: 10px;
position: absolute;
top: 50%;
transform: translateY(-50%);
right: 0;
}
.tag:last-child:after{
display: none;
}
}
}
</style>

View File

@@ -0,0 +1,183 @@
<template>
<view class="voice-btn" @click="playBtn" :class="play?'audioplaying':''">
<img class="play-btn" src="@/static/images/audio.png"></img>
<view class="audio-box">
<view class="audio1 wave1"></view>
<view class="audio2 wave2"></view>
<view class="audio3 wave3"></view>
<view class="audio2 wave2"></view>
<view class="audio1 wave1"></view>
<view class="audio2 wave2"></view>
<view class="audio3 wave3"></view>
<view class="audio2 wave2"></view>
<view class="audio1 wave1"></view>
<view class="audio2 wave2"></view>
<view class="audio3 wave3"></view>
<view class="audio2 wave2"></view>
</view>
<view>{{sec}}"</view>
</view>
</template>
<script>
const audio = uni.createInnerAudioContext();
export default {
emits: ['playAudio'],
props: {
src: {
type: String,
default: ''
},
sec: {
type: Number,
default: 0
},
},
data() {
return {
play: false,
};
},
methods: {
playBtn() {
if(this.play){
this.play = false;
audio.stop();
return;
}
this.play = true;
//语音自然播放结束
audio.onEnded((res) => {
this.play = false;
});
audio.src = this.src;
audio.play();
},
}
};
</script>
<style lang="scss" scoped>
view{
box-sizing: border-box;
}
.voice-btn {
background-color: #3cc9a4;
width: 140rpx;
border-radius: 20px;
padding: 10rpx 10rpx;
display: flex;
align-items: center;
justify-content: space-between;
color: #fff;
font-size: 20rpx;
}
.play-btn {
width: 24rpx;
height: 24rpx;
}
.audio-box {
height: 20rpx;
display: flex;
align-items: center;
justify-content: center;
}
.audio1{
margin-right: 2rpx;
width: 1px;
height: 33.3%;
border-radius: 50px;
background-color: #fff;
}
.audio2{
margin-right: 2rpx;
width: 1px;
height: 66.6%;
border-radius: 50px;
background-color: #fff;
}
.audio3{
margin-right: 2rpx;
width: 1px;
height: 100%;
border-radius: 50px;
background-color: #fff;
}
.audioplaying .wave1 {
animation: wave1 1s linear 0s infinite;
}
.audioplaying .wave2 {
animation: wave2 1s linear 0s infinite;
}
.audioplaying .wave3 {
animation: wave3 1s linear 0s infinite;
}
@keyframes wave1{
0% {
width: 1px;
height: 33%;
}
25% {
width: 1px;
height: 66%;
}
50% {
width: 1px;
height: 100%;
}
75% {
width: 1px;
height: 66%;
}
100% {
width: 1px;
height: 33%;
}
}
@keyframes wave2{
0% {
width: 1px;
height: 66%;
}
25% {
width: 1px;
height: 33%;
}
50% {
width: 1px;
height: 66%;
}
75% {
width: 1px;
height: 100%;
}
100% {
width: 1px;
height: 66%;
}
}
@keyframes wave3{
0% {
width: 1px;
height: 100%;
}
50% {
width: 1px;
height: 33%;
}
100% {
width: 1px;
height: 100%;
}
}
</style>

View File

@@ -0,0 +1,61 @@
<template>
<view class="container page-app theme-light main-green font-1">
<layout ref="skill" title="选人下单">
</layout>
<s-auth-modal />
<s-menu-tools />
</view>
</template>
<script>
import layout from '@/pages/clerk/detail/components/layout.vue';
import sheep from '@/sheep';
export default {
components: {
layout,
},
data() {
return {
}
},
// 分享小程序
onShareAppMessage(res) {
return {
title: this.shareInfo.title,
imageUrl: this.shareInfo.image,
};
},
onShareTimeline() {
return {
title: this.shareInfo.title,
imageUrl: this.shareInfo.image,
}
},
onLoad(options) {
this.$nextTick(() => {
this.$refs.skill.initData(options);
});
},
computed: {
shareInfo() {
return sheep.$platform.share.getShareInfo();
},
},
methods: {
}
}
</script>
<style lang="scss" scoped>
.container {
background-color: #fff;
height: calc(100vh);
padding-bottom: env(safe-area-inset-bottom);
}
</style>

View File

@@ -0,0 +1,82 @@
<!-- 虚拟列表演示(不使用内置列表)(vue) -->
<!-- 写法较简单在页面中对当前需要渲染的虚拟列表数据进行for循环在vue3中兼容性良好 -->
<!-- 在各平台兼容性请查阅https://z-paging.zxlee.cn/module/virtual-list.html -->
<template>
<view class="content">
<!-- 如果页面中的cell高度是固定不变的则不需要设置cell-height-mode如果页面中高度是动态改变的则设置cell-height-mode="dynamic" -->
<!-- 原先的v-model修改为@virtualListChange="virtualListChange"并赋值处理后的虚拟列表 -->
<z-paging ref="paging" :auto="true" use-virtual-list :force-close-inner-list="true" :paging-style="{ paddingTop: 0 + 'px', paddingBottom: paddingBottom + 'rpx' }" cell-height-mode="dynamic" @scroll="scroll" @virtualListChange="virtualListChange" @query="queryList">
<!-- 需要固定在顶部不滚动的view放在slot="top"的view中如果需要跟着滚动则不要设置slot="top" -->
<template #top>
<su-navbar :title="title" statusBar></su-navbar>
</template>
<!-- :id="`zp-id-${item.zp_index}`":key="item.zp_index" 必须写必须写 -->
<!-- 这里for循环的index不是数组中真实的index了请使用item.zp_index获取真实的index -->
<order-list :virtualList="virtualList"></order-list>
</z-paging>
</view>
</template>
<script>
import OrderList from '@/pages/clerk/fans/components/orderList.vue';
import ClerkApi from '@/sheep/api/worker/clerk';
import {
fen2yuan,
} from '@/sheep/hooks/useGoods';
export default {
components: {
OrderList,
},
props: {
title: {
type: String,
default: '我收藏的店员',
},
},
data() {
return {
// 虚拟列表数组,通过@virtualListChange监听获得最新数组
virtualList: [],
scrollTop: 0,
paddingTop: 0,
paddingBottom: 0,
height: 0,
}
},
methods: {
scroll(e) {
this.scrollTop = e.detail.scrollTop;
},
// 监听虚拟列表数组改变并赋值给virtualList进行重新渲染
virtualListChange(vList) {
this.virtualList = vList;
},
queryList(pageNo, pageSize) {
// 组件加载时会自动触发此方法,因此默认页面加载时会自动触发,无需手动调用
// 这里的pageNo和pageSize会自动计算好直接传给服务器即可
// 模拟请求服务器获取分页数据,请替换成自己的网络请求
const params = {
pageNo: pageNo,
pageSize: pageSize,
}
ClerkApi.getClerkFansPage(params).then(res => {
// 将请求的结果数组传递给z-paging
this.$refs.paging.complete(res.data.list);
}).catch(res => {
// 如果请求失败写this.$refs.paging.complete(false);
// 注意每次都需要在catch中写这句话很麻烦z-paging提供了方案可以全局统一处理
// 在底层的网络请求抛出异常时写uni.$emit('z-paging-error-emit');即可
this.$refs.paging.complete(false);
})
},
}
}
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,212 @@
<template>
<view :id="`zp-id-${item.zp_index}`" :key="item.zp_index" v-for="(item,index) in orderList" class="order-card">
<view class="main-box" :class="index == orderList.length-1 ? '':'bt'">
<view @click="detail(item)">
<u-avatar mode="square" size="170" :src="item.avatar"></u-avatar>
</view>
<view class="right-box">
<view class="nickname-box">
<view class="nickname">{{item.nickname}}</view>
<view class="oline" v-if="item.onlineStatus">
<tui-badge :scaleRatio="0.8" type="green" dot></tui-badge>
<text class="text">在线</text>
</view>
<view class="oline un" v-else>
<tui-badge :scaleRatio="0.8" type="gray" dot></tui-badge>
<text class="text">休息中</text>
</view>
</view>
<view class="city">{{item.city}}</view>
<view class="sex-box">
<view class="sex-tag">
<text class="tag" v-if="item.sex == 1"></text>
<text class="tag" v-if="item.sex == 0"></text>
<text class="tag">{{item.age}}</text>
</view>
<view class="fans-box" @click="fans(item)">
<view class="fans">{{item.fansNum}}人收藏</view>
<u-icon v-if="item.fans" name="star-fill" size="36" color="var(--ui-BG-Main)"></u-icon>
<u-icon v-else name="star" size="36" color="#ddd"></u-icon>
</view>
</view>
<view class="info">{{item.intro}}</view>
</view>
</view>
</view>
</template>
<script>
import TuiBadge from "@/components/thorui/tui-badge/tui-badge.vue";
import TrendApi from '@/sheep/api/worker/trend';
import sheep from '@/sheep';
export default {
components: {
TuiBadge,
},
props: {
virtualList: {
type: Array,
default: [],
},
},
data() {
return {
}
},
computed: {
orderList() {
return this.virtualList;
},
},
methods: {
fans(e) {
TrendApi.createClerkFans({
workerClerkId: e.workerClerkId,
}).then((res) => {
if(res){
if(e.fans){
sheep.$helper.toast('取消收藏');
e.fans = false;
}else{
sheep.$helper.toast('收藏成功');
e.fans = true;
}
}
});
},
detail(e) {
this.$u.route({
url: 'pages/clerk/detail/index',
params: {
id: e.workerClerkId,
}
});
},
}
}
</script>
<style lang="scss" scoped>
.order-card {
padding: 0 10px;
background-color: #fff;
display: flex;
flex-direction: column;
flex: 1;
.main-box {
display: flex;
padding: 10px 0;
.right-box {
display: flex;
justify-content: center;
flex: 1;
margin-left: 10px;
flex-direction: column;
.nickname-box {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
.nickname {
font-size: 28rpx;
}
.oline {
display: flex;
align-items: center;
.text {
font-size: 24rpx;
margin-left: 10rpx;
}
}
.un {
color: rgb(100, 101, 102);
}
}
.city {
font-size: 24rpx;
margin-bottom: 10rpx;
}
.sex-box {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 18rpx;
.sex-tag {
font-size: 24rpx;
.tag {
margin-right: 20rpx;
}
}
.fans-box {
display: flex;
align-items: center;
.fans {
font-size: 22rpx;
color: rgb(100, 101, 102);
margin-right: 10rpx;
}
}
}
.info {
font-size: 24rpx;
color: rgb(100, 101, 102);
line-height: 22rpx;
margin-bottom: 10rpx;
display: flex;
align-items: center;
}
}
}
.bt {
border-bottom: 1px solid #fbf0f0;
}
.bottom-box {
display: flex;
justify-content: flex-end;
.btn-box {
display: flex;
align-items: center;
}
.btn {
background-color: #ddd;
color: rgb(100, 101, 102);
font-size: 22rpx;
border-radius: 40px;
padding: 5px 10px;
margin-left: 10px;
min-width: 140rpx;
display: flex;
justify-content: center;
}
.active {
background-color: var(--ui-BG-Main);
color: #fff;
}
}
}
</style>

40
pages/clerk/fans/list.vue Normal file
View File

@@ -0,0 +1,40 @@
<template>
<view class="page-app theme-light main-green font-1">
<layout ref="order"></layout>
<s-menu-tools />
<s-auth-modal />
</view>
</template>
<script>
import layout from '@/pages/clerk/fans/components/layout.vue';
export default {
components: {
layout,
},
props: {
},
data() {
return {
}
},
onLoad(options) {
},
methods: {
}
}
</script>
<style lang="scss" scoped>
.page-app {
background-color: #fafafa;
padding-bottom: 140rpx;
height: calc(100vh);
padding-bottom: env(safe-area-inset-bottom);
}
</style>

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,112 @@
<!-- 分销商信息 -->
<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;
margin: -88rpx 20rpx 0 20rpx;
padding-top: 138rpx;
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,181 @@
<!-- 分销首页明细列表 -->
<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-es';
import dayjs from 'dayjs';
import BrokerageApi from '@/sheep/api/trade/brokerage';
import { fen2yuan } from '../../../sheep/hooks/useGoods';
const state = reactive({
loadStatus: '',
pagination: {
list: [],
total: 0,
pageNo: 1,
pageSize: 10,
},
});
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,137 @@
<!-- 分销商菜单栏 -->
<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',
},
/* {
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>

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

@@ -0,0 +1,156 @@
<!-- 分销商品列表 -->
<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-es';
import { showShareModal } from '@/sheep/hooks/useModal';
import SpuApi from '@/sheep/api/product/spu';
import BrokerageApi from '@/sheep/api/trade/brokerage';
import { fen2yuan } from '../../sheep/hooks/useGoods';
const state = reactive({
pagination: {
list: [],
total: 0,
pageNo: 1,
pageSize: 10,
},
loadStatus: '',
shareInfo: {},
});
// TODO @puhui999【分享】接入
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,51 @@
<!-- 分销中心 -->
<template>
<s-layout navbar="inner" class="index-wrap" title="分销中心" :bgStyle="bgStyle" :onShareAppMessage="shareInfo">
<!-- 分销商信息 -->
<commission-info />
<!-- 账户信息 -->
<account-info />
<!-- 菜单栏 -->
<commission-menu />
<!-- 分销记录 -->
<commission-log />
<!-- 权限弹窗 -->
<commission-auth />
</s-layout>
</template>
<script setup>
import { computed } from 'vue';
import { onPageScroll } from '@dcloudio/uni-app';
import commissionInfo from './components/commission-info.vue';
import accountInfo from './components/account-info.vue';
import commissionLog from './components/commission-log.vue';
import commissionMenu from './components/commission-menu.vue';
import commissionAuth from './components/commission-auth.vue';
import sheep from '@/sheep';
const shareInfo = computed(() => {
return sheep.$platform.share.getShareInfo({
params: {
page: '6',
},
}, {
type: 'user',
});
});
const bgStyle = {
backgroundColor: '#F7D598',
};
onPageScroll((e) => {
});
</script>
<style lang="scss" scoped>
:deep(.page-main) {
background-size: 100% 100% !important;
}
</style>

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

@@ -0,0 +1,328 @@
<!-- 分销 - 订单明细 -->
<template>
<s-layout title="分销订单" :class="state.scrollTop ? 'order-warp' : ''" navbar="inner">
<view
class="header-box"
:style="[
{
marginTop: '-' + Number(statusBarHeight + 88 + 22) + 'rpx',
paddingTop: Number(statusBarHeight + 108 + 22) + '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-es';
import { onPageScroll } from '@dcloudio/uni-app';
import { resetPagination } from '@/sheep/util';
import BrokerageApi from '@/sheep/api/trade/brokerage';
import { fen2yuan } from '../../sheep/hooks/useGoods';
const statusBarHeight = sheep.$platform.device.statusBarHeight * 2;
const headerBg = sheep.$url.css('/static/img/shop/user/withdraw_bg.png');
onPageScroll((e) => {
state.scrollTop = e.scrollTop <= 100;
});
const state = reactive({
totals: 0, // 累计推广订单(单)
scrollTop: false,
currentTab: 0,
loadStatus: '',
pagination: {
list: [],
total: 0,
pageNo: 1,
pageSize: 10,
},
});
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.pageNo,
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

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

@@ -0,0 +1,585 @@
<!-- 页面 TODO 芋艿该页面的实现代码需要优化包括 js css以及相关的样式设计 -->
<template>
<s-layout title="我的团队" :class="state.scrollTop ? 'team-wrap' : ''" :bgStyle="bgStyle" 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="https://rbtnet.oss-cn-hangzhou.aliyuncs.com/3de207ef9b45973cea9e050ae17a4e411769bb57cd7e4050381678c23776488b.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="https://rbtnet.oss-cn-hangzhou.aliyuncs.com/a6525d55086903956fab235ba4ff92a9d8edd7097489059354667f6e17313c55.png" />
</view>
<view
class="sortItem"
@click="setSort('userCount', 'desc')"
v-else-if="sort === 'userCountASC'"
>
团队排序
<image src="https://rbtnet.oss-cn-hangzhou.aliyuncs.com/a6525d55086903956fab235ba4ff92a9d8edd7097489059354667f6e17313c55.png" />
</view>
<view class="sortItem" @click="setSort('userCount', 'desc')" v-else>
团队排序
<image src="https://rbtnet.oss-cn-hangzhou.aliyuncs.com/a6525d55086903956fab235ba4ff92a9d8edd7097489059354667f6e17313c55.png" />
</view>
<view class="sortItem" @click="setSort('price', 'asc')" v-if="sort === 'priceDESC'">
金额排序
<image src="https://rbtnet.oss-cn-hangzhou.aliyuncs.com/a6525d55086903956fab235ba4ff92a9d8edd7097489059354667f6e17313c55.png" />
</view>
<view
class="sortItem"
@click="setSort('price', 'desc')"
v-else-if="sort === 'priceASC'"
>
金额排序
<image src="https://rbtnet.oss-cn-hangzhou.aliyuncs.com/a6525d55086903956fab235ba4ff92a9d8edd7097489059354667f6e17313c55.png" />
</view>
<view class="sortItem" @click="setSort('price', 'desc')" v-else>
金额排序
<image src="https://rbtnet.oss-cn-hangzhou.aliyuncs.com/a6525d55086903956fab235ba4ff92a9d8edd7097489059354667f6e17313c55.png" />
</view>
<view
class="sortItem"
@click="setSort('orderCount', 'asc')"
v-if="sort === 'orderCountDESC'"
>
订单排序
<image src="https://rbtnet.oss-cn-hangzhou.aliyuncs.com/a6525d55086903956fab235ba4ff92a9d8edd7097489059354667f6e17313c55.png" />
</view>
<view
class="sortItem"
@click="setSort('orderCount', 'desc')"
v-else-if="sort === 'orderCountASC'"
>
订单排序
<image src="https://rbtnet.oss-cn-hangzhou.aliyuncs.com/a6525d55086903956fab235ba4ff92a9d8edd7097489059354667f6e17313c55.png" />
</view>
<view class="sortItem" @click="setSort('orderCount', 'desc')" v-else>
订单排序
<image src="https://rbtnet.oss-cn-hangzhou.aliyuncs.com/a6525d55086903956fab235ba4ff92a9d8edd7097489059354667f6e17313c55.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">
<s-empty icon="/static/data-empty.png" text="暂无推广人数"></s-empty>
</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-es';
import { onPageScroll } from '@dcloudio/uni-app';
import BrokerageApi from '@/sheep/api/trade/brokerage';
const statusBarHeight = sheep.$platform.device.statusBarHeight * 2;
// const agentInfo = computed(() => sheep.$store('user').agentInfo);
const userInfo = computed(() => sheep.$store('user').userInfo);
const headerBg = sheep.$url.css('/static/img/shop/user/withdraw_bg.png');
onPageScroll((e) => {
state.scrollTop = e.scrollTop <= 100;
});
let sort = ref();
const state = reactive({
summary: {},
pagination: {
pageNo: 1,
pageSize: 8,
list: [],
total: 0,
},
loadStatus: '',
// ↓ 新 ui 逻辑
level: 1,
nickname: ref(''),
sortKey: '',
isAsc: '',
});
function filterUserNum(num) {
if (_.isNil(num)) {
return '';
}
return `下级团队${num}`;
}
function submitForm() {
state.pagination.list = [];
getTeamList();
}
async function getTeamList() {
state.loadStatus = 'loading';
let { code, data } = await BrokerageApi.getBrokerageUserChildSummaryPage({
pageNo: state.pagination.pageNo,
pageSize: state.pagination.pageSize,
level: state.level,
'sortingField.order': state.isAsc,
'sortingField.field': state.sortKey,
nickname: state.nickname,
});
if (code !== 0) {
return;
}
state.pagination.list = _.concat(state.pagination.list, data.list);
state.pagination.total = data.total;
state.loadStatus = state.pagination.list.length < state.pagination.total ? 'more' : 'noMore';
}
function setType(e) {
state.pagination.list = [];
state.level = e + '';
getTeamList();
}
function setSort(sortKey, isAsc) {
state.pagination.list = [];
sort = sortKey + isAsc.toUpperCase();
state.isAsc = isAsc;
state.sortKey = sortKey;
getTeamList();
}
onLoad(async () => {
await getTeamList();
// 概要数据
let { data } = await BrokerageApi.getBrokerageUserSummary();
state.summary = data;
});
// 加载更多
function loadMore() {
if (state.loadStatus === 'noMore') {
return;
}
state.pagination.pageNo++;
getTeamList();
}
const bgStyle = {
backgroundColor: 'var(--ui-BG-Main)',
};
// 上拉加载更多
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: var(--ui-BG-Main);
// $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>

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

@@ -0,0 +1,533 @@
<!-- 分销 - 佣金明细 -->
<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.description }}</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>
<view class="status-box">
<text class="time">{{
sheep.$helper.timeFormat(item.createTime, 'yyyy-mm-dd hh:MM:ss')
}}</text>
<text class="status" v-if="item.price >= 0 && item.status == 0">待结算</text>
<text class="status" v-if="item.price >= 0 && item.status == 1">已结算</text>
</view>
</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-es';
import BrokerageApi from '@/sheep/api/trade/brokerage';
import { fen2yuan } from '@/sheep/hooks/useGoods';
import { resetPagination } from '@/sheep/util';
const headerBg = sheep.$url.css('/static/img/shop/user/wallet_card_bg.png');
const state = reactive({
showMoney: false,
summary: {}, // 分销信息
today: '',
date: [],
currentTab: 0,
pagination: {
list: [],
total: 0,
pageNo: 1,
pageSize: 10,
},
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>
.status-box {
display: flex;
justify-content: space-between;
align-items: center;
.status {
font-size: 22rpx;
}
}
// 钱包
.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,467 @@
<!-- 分佣提现 -->
<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 + 22) + 'rpx',
paddingTop: Number(statusBarHeight + 88 + 22) + '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" v-if="isPass">
<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="['3', '4'].includes(state.accountInfo.type)">收款码</view>
<view
class="input-box ss-flex ss-col-center"
v-show="['3', '4'].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 === '2'">持卡人</view>
<view
class="input-box ss-flex ss-col-center border-bottom"
v-show="state.accountInfo.type === '2'"
>
<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 === '2'">提现银行</view>
<view
class="input-box ss-flex ss-col-center border-bottom"
v-show="state.accountInfo.type === '2'"
>
<view class="unit" />
<!--银行改为下拉选择-->
<picker
@change="bankChange"
:value="state.bankListSelectedIndex"
:range="state.bankList"
range-key="label"
style="width: 100%"
>
<uni-easyinput
:inputBorder="false"
:value="state.accountInfo.bankName"
placeholder="请选择银行"
suffixIcon="right"
disabled
:styles="{ disableColor: '#fff', borderColor: '#fff', color: '#333!important' }"
/>
</picker>
</view>
<!-- 开户地址 -->
<view class="card-title" v-show="state.accountInfo.type === '2'">开户地址</view>
<view
class="input-box ss-flex ss-col-center border-bottom"
v-show="state.accountInfo.type === '2'"
>
<view class="unit" />
<uni-easyinput
:inputBorder="false"
class="ss-flex-1 ss-p-l-10"
v-model="state.accountInfo.bankAddress"
placeholder="请输入开户地址"
/>
</view>
<button class="ss-reset-button save-btn ui-BG-Main-Gradient ui-Shadow-Main" @tap="onConfirm">
确认提现
</button>
</view>
<!-- 提现说明 -->
<view class="draw-notice">
<view class="title ss-m-b-30">提现说明</view>
<view class="draw-list"> 最低提现金额 {{ fen2yuan(state.minPrice) }} </view>
<view class="draw-list">
冻结佣金<text>{{ fen2yuan(state.brokerageInfo.frozenPrice) }}</text>
每笔佣金的冻结期为 {{ state.frozenDays }} 到期后可提现
</view>
</view>
<!-- 选择提现账户 -->
<account-type-select
:show="state.accountSelect"
@close="onAccountSelect(false)"
round="10"
v-model="state.accountInfo"
:methods="state.withdrawTypes"
/>
</s-layout>
</template>
<script setup>
import { computed, reactive, onBeforeMount } from 'vue';
import sheep from '@/sheep';
import accountTypeSelect from './components/account-type-select.vue';
import { fen2yuan } from '@/sheep/hooks/useGoods';
import TradeConfigApi from '@/sheep/api/trade/config';
import BrokerageApi from '@/sheep/api/trade/brokerage';
import DictApi from '@/sheep/api/system/dict';
const isPass = computed(() => {
return sheep.$store('user').tradeConfig.weixinEnabled;
});
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: [], // 提现方式
bankList: [], // 银行字典数据
bankListSelectedIndex: '', // 选中银行 bankList 的 index
});
// 打开提现方式的弹窗
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;
}
}
// 获取提现银行配置字典
async function getDictDataListByType() {
let { code, data } = await DictApi.getDictDataListByType('brokerage_bank_name');
if (code !== 0) {
return;
}
if (data && data.length > 0) {
state.bankList = data;
}
}
// 银行选择
function bankChange(e) {
const value = e.detail.value;
state.bankListSelectedIndex = value;
state.accountInfo.bankName = state.bankList[value].label;
}
onBeforeMount(() => {
getWithdrawRules();
getBrokerageUser();
getDictDataListByType(); //获取银行字典数据
});
</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>

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

@@ -0,0 +1,390 @@
<!-- 优惠券详情 -->
<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>
<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-es';
import CouponApi from '@/sheep/api/promotion/coupon';
import { fen2yuan } from '@/sheep/hooks/useGoods';
import SpuApi from '@/sheep/api/product/spu';
import CategoryApi from '@/sheep/api/product/category';
import { resetPagination } from '@/sheep/util';
const state = reactive({
id: 0, // 优惠劵模版编号 templateId
couponId: 0, // 用户优惠劵编号 couponId
coupon: {}, // 优惠劵信息
pagination: {
list: [],
total: 0,
pageNo: 1,
pageSize: 1,
},
categoryId: 0, // 选中的商品分类编号
tabMaps: [], // 指定分类时,每个分类构成一个 tab
currentTab: 0, // 选中的 tabMaps 下标
loadStatus: '',
});
function onTabsChange(e) {
resetPagination(state.pagination);
state.currentTab = e.index;
state.categoryId = e.value;
getGoodsListByCategory();
}
async function getGoodsListByCategory() {
state.loadStatus = 'loading';
const { code, data } = await SpuApi.getSpuPage({
categoryId: state.categoryId,
pageNo: state.pagination.pageNo,
pageSize: state.pagination.pageSize,
});
if (code !== 0) {
return;
}
state.pagination.list = _.concat(state.pagination.list, data.list);
state.pagination.total = data.total;
state.loadStatus = state.pagination.list.length < state.pagination.total ? 'more' : 'noMore';
}
// 获得商品列表,指定商品范围
async function getGoodsListById() {
const { data, code } = await SpuApi.getSpuListByIds(state.coupon.productScopeValues.join(','));
if (code !== 0) {
return;
}
state.pagination.list = data;
}
// 获得分类列表
async function getCategoryList() {
const { data, code } = await CategoryApi.getCategoryListByIds(
state.coupon.productScopeValues.join(','),
);
if (code !== 0) {
return;
}
state.tabMaps = data.map((category) => ({ name: category.name, value: category.id }));
// 加载第一个分类的商品列表
if (state.tabMaps.length > 0) {
state.categoryId = state.tabMaps[0].value;
await getGoodsListByCategory();
}
}
// 领取优惠劵
async function getCoupon() {
const { code } = await CouponApi.takeCoupon(state.id);
if (code !== 0) {
return;
}
uni.showToast({
title: '领取成功',
});
setTimeout(() => {
getCouponContent();
}, 1000);
}
// 加载优惠劵信息
async function getCouponContent() {
const { code, data } =
state.id > 0
? await CouponApi.getCouponTemplate(state.id)
: await CouponApi.getCoupon(state.couponId);
if (code !== 0) {
return;
}
state.coupon = data;
// 不同指定范围,加载不同数据
if (state.coupon.productScope === 2) {
await getGoodsListById();
} else if (state.coupon.productScope === 3) {
await getCategoryList();
}
}
// 加载更多
function loadMore() {
if (state.loadStatus === 'noMore') {
return;
}
state.pagination.pageNo++;
getGoodsListByCategory();
}
onLoad((options) => {
state.id = options.id;
state.couponId = options.couponId;
getCouponContent(state.id, state.couponId);
});
// 上拉加载更多
onReachBottom(() => {
loadMore();
});
</script>
<style lang="scss" scoped>
.goods-title {
font-size: 34rpx;
font-weight: bold;
color: #333333;
}
.detail-wrap {
background: linear-gradient(
180deg,
var(--ui-BG-Main),
var(--ui-BG-Main-gradient),
var(--ui-BG-Main),
#fff
);
}
.detail-box {
// background-color: var(--ui-BG);
border-radius: 6rpx;
position: relative;
margin-top: 100rpx;
.tag-box {
width: 140rpx;
height: 140rpx;
background: var(--ui-BG);
border-radius: 50%;
position: absolute;
top: -70rpx;
left: 50%;
z-index: 6;
transform: translateX(-50%);
.tag-image {
width: 104rpx;
height: 104rpx;
border-radius: 50%;
}
}
.top {
background-color: #fff;
border-radius: 20rpx 20rpx 0 0;
-webkit-mask: radial-gradient(circle at 16rpx 100%, #0000 16rpx, red 0) -16rpx;
padding: 110rpx 0 0 0;
position: relative;
z-index: 5;
.title {
font-size: 40rpx;
color: #333;
font-weight: bold;
}
.subtitle {
font-size: 28rpx;
color: #333333;
}
.use-btn {
width: 386rpx;
height: 80rpx;
line-height: 80rpx;
background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
border-radius: 40rpx;
color: $white;
}
.disable-btn {
width: 386rpx;
height: 80rpx;
line-height: 80rpx;
background: #e5e5e5;
border-radius: 40rpx;
color: $white;
}
.time {
font-size: 26rpx;
font-weight: 400;
color: #999999;
}
.coupon-line {
width: 95%;
border-bottom: 2rpx solid #eeeeee;
}
}
.bottom {
background-color: #fff;
border-radius: 0 0 20rpx 20rpx;
-webkit-mask: radial-gradient(circle at 16rpx 0%, #0000 16rpx, red 0) -16rpx;
padding: 40rpx 30rpx;
.type {
height: 96rpx;
border-bottom: 2rpx solid #eeeeee;
}
}
.des {
font-size: 24rpx;
font-weight: 400;
color: #666666;
}
}
.all-user {
width: 100%;
height: 300rpx;
font-size: 34rpx;
font-weight: bold;
color: #333333;
}
</style>

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

@@ -0,0 +1,222 @@
<!-- 优惠券中心 -->
<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-es';
import { resetPagination } from '@/sheep/util';
import CouponApi from '@/sheep/api/promotion/coupon';
// 数据
const state = reactive({
currentTab: 0, // 当前 tab
type: '1',
pagination: {
list: [],
total: 0,
pageNo: 1,
pageSize: 5,
},
loadStatus: '',
});
const tabMaps = [
{
name: '领券中心',
value: 'all',
},
{
name: '已领取',
value: '1',
},
{
name: '已使用',
value: '2',
},
{
name: '已失效',
value: '3',
},
];
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>

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

@@ -0,0 +1,190 @@
<!-- 评价 -->
<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="宝贝满足你的期待吗?说说你的使用心得,分享给想买的他们吧~"
/>
<view class="img-box">
<s-uploader
v-model:url="state.commentList[index].images"
fileMediatype="image"
limit="9"
mode="grid"
:imageStyles="{ width: '168rpx', height: '168rpx' }"
@success="(payload) => uploadSuccess(payload, index)"
/>
</view>
</view>
<view class="checkbox-container">
<checkbox-group @change="(event) => toggleAnonymous(index, event)">
<label>
<checkbox value="anonymousChecked" />
匿名评论
</label>
</checkbox-group>
</view>
</view>
</view>
</view>
</view>
<su-fixed bottom placeholder>
<view class="foot_box ss-flex ss-row-center ss-col-center">
<button class="ss-reset-button post-btn ui-BG-Main-Gradient ui-Shadow-Main" @tap="onSubmit">
发布
</button>
</view>
</su-fixed>
</s-layout>
</template>
<script setup>
import sheep from '@/sheep';
import { onLoad } from '@dcloudio/uni-app';
import { reactive, ref } from 'vue';
import OrderApi from '@/sheep/api/trade/order';
const state = reactive({
orderInfo: {},
commentList: [],
id: null,
});
/**
* 切换是否匿名
*
* @param commentIndex 当前评论下标
* @param event 复选框事件
*/
function toggleAnonymous(commentIndex, event) {
state.commentList[commentIndex].anonymous = event.detail.value[0] === 'anonymousChecked';
}
/**
* 发布评论
*
* @returns {Promise<void>}
*/
async function onSubmit() {
// 顺序提交评论
for (const comment of state.commentList) {
await OrderApi.createOrderItemComment(comment);
}
// 都评论好,返回
sheep.$router.back();
}
/**
* 图片添加到表单
*
* @param payload 上传成功后的回调数据
* @param commentIndex 当前评论的下标
*/
function uploadSuccess(payload, commentIndex) {
state.commentList[commentIndex].picUrls = state.commentList[commentIndex].images;
}
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;
}
}
.checkbox-container {
padding: 10rpx;
}
.post-btn {
width: 690rpx;
line-height: 80rpx;
border-radius: 40rpx;
color: rgba(#fff, 0.9);
margin-bottom: 20rpx;
}
</style>

View File

@@ -0,0 +1,180 @@
<!-- 评价 -->
<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"
@success="(payload) => uploadSuccess(payload, index)"
limit="9" mode="grid" :imageStyles="{ width: '168rpx', height: '168rpx' }" />
</view>
</view>
<view class="checkbox-container">
<checkbox-group @change="(event) => toggleAnonymous(index, event)">
<label>
<checkbox value="anonymousChecked" />
匿名评论
</label>
</checkbox-group>
</view>
</view>
</view>
</view>
</view>
<!-- TODO 卢越评论是否匿名 -->
<su-fixed bottom placeholder>
<view class="foot_box ss-flex ss-row-center ss-col-center">
<button class="ss-reset-button post-btn ui-BG-Main-Gradient ui-Shadow-Main" @tap="onSubmit">
发布
</button>
</view>
</su-fixed>
</s-layout>
</template>
<script setup>
import sheep from '@/sheep';
import { onLoad } from '@dcloudio/uni-app';
import { reactive } from 'vue';
import OrderApi from '@/sheep/api/trade/order';
const state = reactive({
orderInfo: {},
commentList: [],
id: null
});
/**
* 切换是否匿名
*
* @param commentIndex 当前评论下标
* @param event 复选框事件
*/
function toggleAnonymous(commentIndex, event) {
state.commentList[commentIndex].anonymous = event.detail.value[0] === 'anonymousChecked';
}
async function onSubmit() {
// 顺序提交评论
for (const comment of state.commentList) {
await OrderApi.createOrderItemComment(comment);
}
// 都评论好,返回
sheep.$router.go('/pages/order/my/list');
//sheep.$router.back();
}
/**
* 图片添加到表单
*
* @param payload 上传成功后的回调数据
* @param commentIndex 当前评论的下标
*/
function uploadSuccess(payload, commentIndex) {
state.commentList[commentIndex].picUrls = state.commentList[commentIndex].images;
}
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;
padding-bottom: 30rpx;
.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;
}
}
.checkbox-container {
padding: 20rpx 30rpx;
}
.post-btn {
width: 690rpx;
line-height: 80rpx;
border-radius: 40rpx;
color: rgba(#fff, 0.9);
margin-bottom: 20rpx;
}
</style>

View File

@@ -0,0 +1,41 @@
<template>
<view>
<view class="card-notice">
<u-notice-bar mode="horizontal" :list="list"></u-notice-bar>
<view class="btn">投诉举报</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
list: [
'平台绝对不会收取实名认证等任何费用。让你加微信支付或者点链接、扫二维码的都是骗子!平台绝对不会收取实名认证等任何费用。让你加微信支付或者点链接、扫二维码的都是骗子!平台绝对不会收取实名认证等任何费用。让你加微信支付或者点链接、扫二维码的都是骗子!平台绝对不会收取实名认证等任何费用。让你加微信支付或者点链接、扫二维码的都是骗子!平台绝对不会收取实名认证等任何费用。让你加微信支付或者点链接、扫二维码的都是骗子!平台绝对不会收取实名认证等任何费用。让你加微信支付或者点链接、扫二维码的都是骗子!平台绝对不会收取实名认证等任何费用。让你加微信支付或者点链接、扫二维码的都是骗子!',
],
}
},
methods: {
onBanner(index) {
console.log(22222, index);
},
}
}
</script>
<style lang="scss" scoped>
.card-notice {
background-color: #fff;
.btn {
display: flex;
justify-content: center;
background-color: #fff;
height: 60px;
align-items: center;
color: #ffa31a;
font-size: 12px;
}
}
</style>

View File

@@ -0,0 +1,225 @@
<template>
<view>
<uni-swiper-dot class="uni-swiper-dot-box" @clickItem=clickItem :info="info" :current="current" :mode="mode" :dots-styles="dotsStyles" field="content">
<swiper class="swiper-box" @change="change" :current="swiperDotIndex">
<swiper-item v-for="(item, index) in 3" :key="index">
<view class="swiper-item">
<view class="card">
<image class="logo" src="http://rbtnet.oss-cn-hangzhou.aliyuncs.com/test/20240726/98a24075202b429d9c9722df541621c6.jpg"></image>
<view class="content-box">
<view class="left-box">
<view class="title">15开黑卡/</view>
<view class="text-gray">LOL手游</view>
<view class="text-gray">下单前聊一聊</view>
</view>
<view class="btn">找TA开黑</view>
</view>
</view>
</view>
</swiper-item>
</swiper>
</uni-swiper-dot>
</view>
</template>
<script>
export default {
data() {
return {
info: [{
colorClass: 'uni-bg-red',
url: 'https://qiniu-web-assets.dcloud.net.cn/unidoc/zh/shuijiao.jpg',
content: '内容 A'
},
{
colorClass: 'uni-bg-green',
url: 'https://qiniu-web-assets.dcloud.net.cn/unidoc/zh/shuijiao.jpg',
content: '内容 B'
},
{
colorClass: 'uni-bg-blue',
url: 'https://qiniu-web-assets.dcloud.net.cn/unidoc/zh/shuijiao.jpg',
content: '内容 C'
}
],
dotStyle: [{
backgroundColor: '#00c8b0',
border: '1px rgba(0, 0, 0, .3) solid',
color: '#fff',
selectedBackgroundColor: 'rgba(0, 0, 0, .9)',
selectedBorder: '1px rgba(0, 0, 0, .9) solid'
},
{
backgroundColor: 'rgba(255, 90, 95,0.3)',
border: '1px rgba(255, 90, 95,0.3) solid',
color: '#fff',
selectedBackgroundColor: 'rgba(255, 90, 95,0.9)',
selectedBorder: '1px rgba(255, 90, 95,0.9) solid'
},
{
backgroundColor: 'rgba(83, 200, 249,0.3)',
border: '1px rgba(83, 200, 249,0.3) solid',
color: '#fff',
selectedBackgroundColor: 'rgba(83, 200, 249,0.9)',
selectedBorder: '1px rgba(83, 200, 249,0.9) solid'
}
],
modeIndex: -1,
styleIndex: -1,
current: 0,
mode: 'default',
dotsStyles: {
selectedBackgroundColor: '#00c8b0',
},
swiperDotIndex: 0,
current: 0,
}
},
methods: {
change(e) {
this.current = e.detail.current
},
selectStyle(index) {
this.dotsStyles = this.dotStyle[index]
this.styleIndex = index
},
selectMode(mode, index) {
this.mode = mode
this.modeIndex = index
this.styleIndex = -1
this.dotsStyles = this.dotStyle[0]
},
clickItem(e) {
this.swiperDotIndex = e
},
onBanner(index) {
console.log(22222, index);
},
}
}
</script>
<style lang="scss" scoped>
.swiper-box {
height: 80px;
}
.swiper-item {
height: 80px;
}
.image {
width: 750rpx;
}
/* #ifndef APP-NVUE */
::v-deep .image img {
-webkit-user-drag: none;
-khtml-user-drag: none;
-moz-user-drag: none;
-o-user-drag: none;
user-drag: none;
}
/* #endif */
.uni-bg-red {
background-color: #ff5a5f;
}
.uni-bg-green {
background-color: #09bb07;
}
.uni-bg-blue {
background-color: #007aff;
}
.example-body {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
padding: 20rpx;
}
.example-body-item {
flex-direction: row;
justify-content: center;
align-items: center;
margin: 15rpx;
padding: 15rpx;
height: 60rpx;
/* #ifndef APP-NVUE */
display: flex;
padding: 0 15rpx;
/* #endif */
flex: 1;
border-color: #e5e5e5;
border-style: solid;
border-width: 1px;
border-radius: 5px;
}
.example-body-item-text {
font-size: 28rpx;
color: #333;
}
.example-body-dots {
width: 16rpx;
height: 16rpx;
border-radius: 50px;
background-color: #333333;
margin-left: 10rpx;
}
.active {
border-style: solid;
border-color: #007aff;
border-width: 1px;
}
.card {
background-color: #fff;
display: flex;
align-items: center;
height: 80px;
padding: 10px;
.logo {
width: 40px;
height: 40px;
border-radius: 100%;;
}
.content-box {
display: flex;
flex: 1;
justify-content: space-between;
align-items: center;
margin-left: 10px;
.title {
font-size: 28rpx;
color: #333333;
}
.text-gray {
font-size: 24rpx;
color: #aaaaaa;
margin: 3px 0;
}
.btn {
background-color: #00c8b0;
color: #fff;
font-size: 24rpx;
border-radius: 45px;
padding: 5px 10px;
}
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,336 @@
<!-- 聊天记录模式+虚拟列表演示(vue)加载更多聊天记录无闪动&支持渲染大量数据 -->
<!-- 注意虚拟列表缓存高度默认仅在cell初始化时获取一次若您的聊天列表中带有图片强烈建议给图片一个固定的高度如果高度根据内容动态撑高可能导致虚拟列表抖动 -->
<!-- 如果必须实现高度根据内容动态撑高+虚拟列表功能可监听图片加载完毕事件并在其中调用z-paging的didUpdateVirtualListCell刷新缓存高度 -->
<template>
<view class="content">
<!-- use-chat-record-mode开启聊天记录模式 -->
<!-- use-virtual-list开启虚拟列表模式 -->
<!-- cell-height-mode设置虚拟列表模式高度不固定 -->
<!-- safe-area-inset-bottom开启底部安全区域适配 -->
<!-- bottom-bg-color设置slot="bottom"容器的背景色这里设置为和chat-input-bar的背景色一致 -->
<z-paging ref="paging" v-model="dataList" :paging-style="{ paddingTop: paddingTop + 'px' }" default-page-size="20" :innerListStyle="innerListStyle" use-chat-record-mode use-virtual-list cell-height-mode="dynamic" :auto-clean-list-when-reload="false" safe-area-inset-bottom bottom-bg-color="#fafafa"
@query="queryList" :back-to-top-style="backToTopStyle" :auto-show-back-to-top="showNewMessageTip" @scrolltoupper="onScrollToUpper" @backToTopClick="onBackToTopClick" @keyboardHeightChange="keyboardHeightChange" @hidedKeyboard="hidedKeyboard">
<!-- 顶部提示文字 -->
<template #top>
<navigation-Bar :isReconnecting="isReconnecting" :user="user" @initNav="initNav" @more="navMore">
<template #card-swiper>
<card-swiper v-if="navSwiper"></card-swiper>
</template>
<template #card-notice>
<card-notice v-if="navNotice"></card-notice>
</template>
</navigation-Bar>
</template>
<!-- style="transform: scaleY(-1)"必须写否则会导致列表倒置 -->
<!-- 注意不要直接在chat-item组件标签上设置style因为在微信小程序中是无效的请包一层view -->
<template #cell="{item,index}">
<view style="transform: scaleY(-1)">
<view-item @playAudio="playAudio" :playId="playId" :messageList="dataList" :messageIndex="index" :item="item"></view-item>
</view>
</template>
<!-- 底部聊天输入框 -->
<template #bottom>
<slot name="chat-bar" :user="user" :keyboardHeight="keyboardHeight"></slot>
</template>
<!-- 查看最新消息 -->
<template #backToTop>
<text>有新消息</text>
</template>
</z-paging>
</view>
</template>
<script>
import dayjs from 'dayjs';
import relativeTime from "dayjs/plugin/relativeTime";
dayjs.extend(relativeTime);
import NavigationBar from '@/pages/im/components/navigationBar.vue';
import cardSwiper from '@/pages/im/components/cardSwiper.vue';
import cardNotice from '@/pages/im/components/cardNotice.vue';
import ViewItem from '@/pages/im/components/viewItem.vue';
import ImMessageApi from '@/sheep/api/im/memberMessage';
import UserApi from '@/sheep/api/member/user';
export default {
components: {
NavigationBar,
cardSwiper,
cardNotice,
ViewItem,
},
props: {
groupId: 0,
userId: 0,
isReconnecting: false,
},
data() {
return {
dataList: [],
keyboardHeight: -1,
paddingTop: 0,
navSwiper: false,
navSwiperH: 80,
navNotice: false,
navNoticeH: 90,
height: 0,
queryParams: {
pageNo: 1,
pageSize: 10,
groupId: 0,
},
showNewMessageTip: false,
backToTopStyle: {
'width': '100px',
'background-color': '#fff',
'border-radius': '30px',
'box-shadow': '0 2px 4px rgba(0, 0, 0, 0.1)',
'display': 'flex',
'justifyContent': 'center',
'alignItems': 'center',
},
innerListStyle: {
borderBottom: '20px solid #fafafa',
},
audioList: [],
voiceIndex: -1,
playId: null,
user: {},
}
},
created() {
this.getUserInfo();
},
methods: {
getUserInfo() {
UserApi.getUserInfoById(this.userId).then(res => {
this.user = res.data;
this.user.time = this.showDayTime(this.user.updateTime);
});
},
showDayTime(datetime) {
if (!datetime) return "";
return dayjs(datetime).fromNow();
},
initNav(e) {
this.height = e.height;
this.paddingTop = this.height;
if(this.navSwiper){
this.paddingTop = this.paddingTop + this.navSwiperH;
}
if(this.navNotice){
this.paddingTop = this.paddingTop + this.navNoticeH;
}
},
navMore() {
this.navNotice = !this.navNotice;
if(this.navNotice){
this.paddingTop = this.paddingTop + this.navNoticeH;
}else{
this.paddingTop = this.paddingTop - this.navNoticeH;
}
},
queryList(pageNo, pageSize) {
// 组件加载时会自动触发此方法,因此默认页面加载时会自动触发,无需手动调用
// 这里的pageNo和pageSize会自动计算好直接传给服务器即可
// 模拟请求服务器获取分页数据,请替换成自己的网络请求
this.queryParams.pageNo = pageNo;
this.queryParams.pageSize = pageSize;
this.queryParams.groupId = this.groupId;
/* this.$refs.paging.complete([
{
dd:'2',
msgType: 'message',
message: {
time: false,
src: 'https://rbtnet.oss-cn-hangzhou.aliyuncs.com/laohu.png',
position: 'left',
isRead: true,
src: '',
sec: '30',
msgType: 'voice',
},
},
{
dd:'2',
msgType: 'info',
message: {
title: '温馨提示',
content: '请礼貌用语友好沟通,如遇骚扰等不文明行为,可以将对方屏蔽并投诉。',
msgType: 'card',
},
},
{
dd:'2',
msgType: 'message',
message: {
time: true,
timeDesc: '14:52',
src: 'https://rbtnet.oss-cn-hangzhou.aliyuncs.com/laohu.png',
position: 'right',
isRead: true,
msgType: 'emoji',
},
},
{
dd:'2',
msgType: 'message',
message: {
time: true,
timeDesc: '14:52',
src: 'http://rbtnet.oss-cn-hangzhou.aliyuncs.com/test/20240726/98a24075202b429d9c9722df541621c6.jpg',
position: 'right',
isRead: true,
msgType: 'img',
},
},
{
dd:'2',
msgType: 'message',
message: {
time: false,
src: 'https://rbtnet.oss-cn-hangzhou.aliyuncs.com/laohu.png',
position: 'right',
isRead: true,
src: '',
sec: '130',
msgType: 'voice',
},
},
{
dd:'2',
msgType: 'message',
message: {
time: false,
position: 'right',
content: '地址广东省广州市番禺区祈福新村129号',
msgType: 'map',
},
},
{
dd:'2',
msgType: 'message',
message: {
time: false,
position: 'right',
content: '吃饭了吗',
msgType: 'text',
},
},
{
dd:'2',
msgType: 'info',
message: {
content: '长按图片、文字或视频,支持撤回消息哦~',
msgType: 'text',
},
},
{
dd:'2',
msgType: 'message',
message: {
time: true,
timeDesc: '14:52',
src: 'https://rbtnet.oss-cn-hangzhou.aliyuncs.com/laohu.png',
position: 'left',
isRead: true,
msgType: 'emoji',
},
},
]);
*/
ImMessageApi.getMemberMessagePage(this.queryParams).then(res => {
// 将请求的结果数组传递给z-paging
this.$refs.paging.complete(res.data.list);
}).catch(res => {
// 如果请求失败写this.$refs.paging.complete(false);
// 注意每次都需要在catch中写这句话很麻烦z-paging提供了方案可以全局统一处理
// 在底层的网络请求抛出异常时写uni.$emit('z-paging-error-emit');即可
this.$refs.paging.complete(false);
})
},
/** 监听滚动到底部事件(因为 scroll 翻转了顶就是底) */
onScrollToUpper() {
// 若已是第一页则不做处理
if (this.queryParams.pageNo === 1) {
return;
}
this.showNewMessageTip = false;
// 到底重置消息列表
this.reload();
},
// 监听键盘高度改变请不要直接通过uni.onKeyboardHeightChange监听否则可能导致z-paging内置的键盘高度改变监听失效如果不需要切换表情面板则不用写
keyboardHeightChange(res) {
this.keyboardHeight = res.height;
},
// 用户尝试隐藏键盘此时如果表情面板在展示中应当通知chatInputBar隐藏表情面板如果不需要切换表情面板则不用写
hidedKeyboard() {
this.$emit('hidedKeyboard');
},
reload(message) {
if(message){
if(this.queryParams.pageNo > 1){
this.showNewMessageTip = true;
}
if(message.readStatus){
this.updateReadStatus(message);
}else{
this.$refs.paging.addChatRecordData([message]);
}
}else{
this.$refs.paging.reload();
}
},
updateReadStatus(message) {
this.dataList.forEach(item => {
if (item.id == message.id) {
item.readStatus = true;
}
});
},
/** 滚动到最新消息 */
onBackToTopClick(event) {
event(false); // 禁用默认操作
this.$refs.paging.scrollToBottom();
},
playAudio(e) {
this.playId = e.id;
this.clearAllAudio();
var audio = uni.createInnerAudioContext();
this.audioList.push(audio);
//语音自然播放结束
audio.onEnded((res) => {
this.playId = null;
});
audio.src = e.content;
audio.play();
},
clearAllAudio() {
this.audioList.forEach(function(e){
e.destroy();
})
},
}
}
</script>
<style scoped>
.content {
background-color: #fafafa;
height: calc(100vh);
}
</style>

View File

@@ -0,0 +1,194 @@
<template>
<view>
<tui-navigation-bar :isOpacity="false" @init="initNavigation" @change="opacityChange" :scrollTop="scrollTop" backgroundColor="#fff" color="#333">
<view>
<view class="nav-box">
<view class="title">
<text class="nickname">{{user.nickname}}</text>
<view class="sex-badge" v-if="user.sex == 2">
<u-icon name="ziyuan2" custom-prefix="iconfont"></u-icon>
</view>
<view class="sex-man" v-if="user.sex == 1">
<u-icon name="ziyuan3" custom-prefix="iconfont"></u-icon>
</view>
</view>
<view class="online" v-if="user.online">
<view v-if="isReconnecting">会话重连中</view>
<view class="text" v-else>
<tui-badge :scaleRatio="0.8" type="green" dot></tui-badge>
<text class="time">在线</text>
<!-- <u-icon name="arrow-right" size="16"></u-icon> -->
</view>
</view>
<!-- <view class="online" v-else>
<view v-if="isReconnecting">会话重连中</view>
<view v-else>
<text class="time">{{user.time}}</text>
</view>
</view> -->
<view class="action" @click="goBack">
<u-icon name="nav-back" size="44"></u-icon>
</view>
</view>
<view>
<slot name="card-swiper"></slot>
</view>
<view>
<slot name="card-notice"></slot>
</view>
<!-- <view class="more">
<view class="more-btn" @click="more">
<view class="line"></view>
<view class="line"></view>
</view>
</view> -->
</view>
</tui-navigation-bar>
</view>
</template>
<script>
import tuiBadge from "@/components/thorui/tui-badge/tui-badge.vue";
import tuiNavigationBar from "@/components/thorui/tui-navigation-bar/tui-navigation-bar.vue";
export default {
components: {
tuiBadge,
tuiNavigationBar,
},
props: {
user: {},
isReconnecting: false,
},
data() {
return {
top: 0, //标题图标距离顶部距离
opacity: 1,
height: 0,
scrollTop: 0.5,
}
},
methods: {
initNavigation(e) {
this.height = e.height;
this.opacity = e.opacity;
this.top = e.top;
this.$emit('initNav', e);
},
opacityChange(e) {
this.opacity = e.opacity;
},
goBack() {
uni.navigateBack();
},
more() {
this.$emit('more');
},
}
}
</script>
<style lang="scss" scoped>
.nav-box {
height: 44px;
display: flex;
align-items: center;
justify-content: center;
background-color: #fff;
flex-direction: column;
font-size: 22rpx;
position: relative;
.title {
justify-content: center;
align-items: center;
text-align: center;
margin-bottom: 3px;
display: flex;
width: 100%;
.nickname {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
max-width: calc(100% - 440rpx);
font-size: 12px;
}
}
.online {
font-size: 10px;
line-height: 10px;
color: #303133;
display: flex;
align-items: center;
.time {
margin: 0 3px;
}
.text {
display: flex;
align-items: center;
}
}
.action {
position: absolute;
left: 0;
top: 0;
bottom: 0;
display: flex;
padding: 0 10px;
align-items: center;
width: 50px;
}
}
.sex-badge {
display: flex;
align-items: center;
justify-content: center;
background-color: #e03997;
color: #ffffff;
border-radius: 3px;
font-size: 8px;
height: 30rpx;
width: 30rpx;
margin: 0 10rpx;
}
.sex-man {
display: flex;
align-items: center;
justify-content: center;
background-color: #0081ff;
color: #ffffff;
border-radius: 3px;
font-size: 8px;
height: 30rpx;
width: 30rpx;
margin: 0 10rpx;
}
.more {
display: flex;
justify-content: center;
align-items: center;
.more-btn {
background-color: #fff;
padding: 0 7px;
width: 31px;
height: 15px;
border-radius: 0 0 3px 3px;
.line {
background-color: rgba(0, 0, 0, 0.1);
margin: 3px 0;
height: 1px;
width: 100%;
}
}
}
</style>

View File

@@ -0,0 +1,385 @@
<template>
<view v-if="userId > 0">
<view v-if="item.sendType == 1">
<view class="tip-box" v-if="item.contentType == 1">
<text class="time">{{item.content}}</text>
</view>
<view class="tip-card" v-if="item.contentType == 2">
<!-- <image class="icon" src="@/static/images/help.png"></image> -->
<view class="content-box">
<view class="title">温馨提示</view>
<view class="content">{{item.content}}</view>
</view>
</view>
</view>
<view v-if="item.sendType == 2">
<view class="tip-box" v-if="showTime(item, messageIndex)">
<text class="time">{{showDayTime(item.createTime)}}</text>
</view>
<view class="chat-box left" v-if="item.sendUserId != userId">
<view class="avatar" @click="detail(item)">
<u-avatar width="80rpx" height="80rpx" :src="item.sendAvatar"></u-avatar>
</view>
<view class="content-box" v-if="item.contentType == 1">
<view class="content" >{{item.content}}</view>
</view>
<view class="content-box" v-if="item.contentType == 2">
<view class="content" >{{item.content}}</view>
<u-icon name="map-fill" color="#3D7EFF" size="40"></u-icon>
</view>
<view class="content voice" :class="item.id == playId ?'play':''" @click="playAudio(item)" v-if="item.contentType == 3">
<view class="icon">
<u-icon name="voice1" custom-prefix="iconfont" color="#080808" size="40"></u-icon>
</view>
<text>{{item.voiceSec}}</text>
</view>
<view v-if="item.contentType == 4" @click="showPic(item)" class="msg-img">
<u-image width="420rpx" border-radius="5px" height="220rpx" :fade="false" :src="item.content"></u-image>
</view>
<view v-if="item.contentType == 5" class="msg-img">
<u-image width="200rpx" height="200rpx" :fade="false" :src="item.content"></u-image>
</view>
</view>
<view class="chat-box right" v-if="item.sendUserId == userId">
<view class="content-box" v-if="item.contentType == 1">
<u-icon v-if="item.readStatus" name="checkmark-circle-fill" color="#00c8b0" size="40"></u-icon>
<u-icon v-else name="checkmark-circle" color="#00c8b0" size="40"></u-icon>
<view class="content" >{{item.content}}</view>
</view>
<view class="content-box" v-if="item.contentType == 2">
<u-icon name="map-fill" color="#3D7EFF" size="40"></u-icon>
<view class="content" >{{item.content}}</view>
</view>
<view class="content-box" v-if="item.contentType == 3">
<u-icon v-if="item.readStatus" name="checkmark-circle-fill" color="#00c8b0" size="40"></u-icon>
<u-icon v-else name="checkmark-circle" color="#00c8b0" size="40"></u-icon>
<view class="content voice" :class="item.id == playId ?'play':''" @click="playAudio(item)">
<text>{{item.voiceSec}}</text>
<view class="icon">
<u-icon name="chat_voice" custom-prefix="iconfont" color="#fff" size="40"></u-icon>
</view>
</view>
</view>
<view class="content-box" v-if="item.contentType == 4">
<u-icon v-if="item.readStatus" name="checkmark-circle-fill" color="#00c8b0" size="40"></u-icon>
<u-icon v-else name="checkmark-circle" color="#00c8b0" size="40"></u-icon>
<view class="msg-img" @click="showPic(item)">
<u-image width="420rpx" border-radius="5px" height="220rpx" :fade="false" :src="item.content"></u-image>
</view>
</view>
<view class="content-box" v-if="item.contentType == 5">
<u-icon v-if="item.readStatus" name="checkmark-circle-fill" color="#00c8b0" size="40"></u-icon>
<u-icon v-else name="checkmark-circle" color="#00c8b0" size="40"></u-icon>
<view class="msg-img">
<u-image width="200rpx" height="200rpx" :fade="false" :src="item.content"></u-image>
</view>
</view>
<view class="avatar" @click="detail(item)">
<u-avatar width="80rpx" height="80rpx" :src="item.sendAvatar"></u-avatar>
</view>
</view>
</view>
</view>
</template>
<script>
import dayjs from 'dayjs';
import isToday from "dayjs/plugin/isToday";
import isYesterday from "dayjs/plugin/isYesterday";
dayjs.extend(isToday);
dayjs.extend(isYesterday);
import sheep from '@/sheep';
export default {
props: {
item: {
type: Object,
default: {
createTime: 0,
},
},
// 消息索引
messageIndex: {
type: Number,
default: 0,
},
// 消息列表
messageList:{
type: Array,
default: () => [],
},
playId: {
type: Number,
default: 0,
},
},
data() {
return {
userId: 0,
}
},
created() {
var user = sheep.$store('user').userInfo;
this.userId = user.id;
},
methods: {
// 预览图片
showPic(e) {
uni.previewImage({
indicator: "none",
current: e.content,
urls: [e.content],
});
},
detail(e) {
sheep.$router.go('/pages/user/detail/index',{id: e.sendUserId});
},
playAudio(e) {
this.$emit('playAudio', e);
},
formatDate(date, format= 'YYYY-MM-DD HH:mm:ss') {
// 日期不存在,则返回空
if (!date) {
return ''
}
// 日期存在,则进行格式化
if (format === undefined) {
format = 'YYYY-MM-DD HH:mm:ss'
}
return dayjs(date).format(format);
},
showDayTime(datetime) {
if (!datetime) return "";
const weeks = [
"星期日",
"星期一",
"星期二",
"星期三",
"星期四",
"星期五",
"星期六",
];
// 当天 显示时分
if (dayjs(datetime).isToday()) {
return dayjs(datetime).format("HH:mm");
}
// 昨天 昨天+时分
if (dayjs(datetime).isYesterday()) {
return `昨天 ${dayjs(datetime).format("HH:mm")}`;
}
// 本周 显示周几+时分
if (dayjs().isSame(datetime, "week")) {
const weekIndex = dayjs().day();
return `${weeks[weekIndex]} ${dayjs(datetime).format("HH:mm")}`;
}
// 本周之前 显示年月日
if (dayjs(datetime).isBefore(new Date(), "week")) {
return dayjs(datetime).format("YYYY-MM-DD");
}
},
showTime(item, index) {
// 最后一个
var createTime = new Date().getTime();
if(index > 0){
var message = this.messageList[index + 1];
if(message){
createTime = message.createTime;
}
}
let dateString = dayjs(createTime).fromNow();
let nowDateString = dayjs(item.createTime).fromNow();
return dateString !== nowDateString;
},
}
}
</script>
<style lang="scss" scoped>
.chat-box {
padding: 6px 12px;
display: flex;
.content-box {
display: flex;
align-items: center;
}
}
.left {
.content {
border-radius: 0px 8px 8px;
width: fit-content;
background-color: #fff;
color: #080808;
padding: 8px;
margin: 8px;
text-align: justify;
max-width: 470rpx;
font-size: 11px;
font-family: Helvetica Neue, Helvetica, sans-serif;
align-items: center;
display: flex;
}
.msg-img {
margin: 8px;
}
.icon {
margin-right: 5px;
}
.play{
.icon {
color: #fff;
}
.icon:after
{
border-right: solid 3px #fff;
animation: other-play 1s linear infinite;
z-index: 9;
}
}
}
.right {
display: flex;
justify-content: flex-end;
.content {
border-radius: 8px 0px 8px 8px;
width: fit-content;
background-color: #00c8b0;
color: #080808;
padding: 8px;
margin: 8px;
color: #fff;
text-align: justify;
max-width: 470rpx;
font-size: 11px;
font-family: Helvetica Neue, Helvetica, sans-serif;
align-items: center;
display: flex;
}
.msg-img {
margin: 8px;
}
.icon {
margin-left: 5px;
}
.play{
.icon {
color: #fff;
}
.icon:after
{
border-left: solid 3px #00c8b0;
animation: my-play 1s linear infinite;
z-index: 9;
}
}
}
@keyframes my-play {
0% {
transform: translateX(80%);
}
100% {
transform: translateX(0%);
}
}
@keyframes other-play {
0% {
transform: translateX(-80%);
}
100% {
transform: translateX(0%);
}
}
.voice{
.icon{
font-size: 40upx;
display: flex;
align-items: center;
}
.icon:after
{
content:" ";
width: 20px;
height: 20px;
border-radius: 100%;
position: absolute;
box-sizing: border-box;
}
}
.avatar {
margin-top: 8px;
}
.tip-box {
display: flex;
justify-content: center;
padding: 7px 0;
padding: 7px 20px;
text-align: center;
.time {
color: #aaaaaa;
font-size: 10px;
}
}
.tip-card {
display: flex;
justify-content: center;
background-color: #fff;
border-radius: 3px;
padding: 7px;
margin: 12px;
text-align: justify;
.icon {
width: 60rpx;
height: 60rpx;
}
.content-box {
display: flex;
flex-direction: column;
flex: 1;
margin-left: 7px;
.title {
font-size: 11px;
color: #333333;
}
.content {
margin-top: 3px;
color: #aaaaaa;
font-size: 10px;
}
}
}
</style>

204
pages/im/index - 副本.vue Normal file
View File

@@ -0,0 +1,204 @@
<template>
<view>
<message-list @hidedKeyboard="hidedKeyboard">
<template #chat-bar="{keyboardHeight}">
<chat-bar ref="chatBar" :keyboardHeight="keyboardHeight" @openGift="openGift"></chat-bar>
</template>
</message-list>
<gift-bar ref="gift" @sendGift="sendGift"></gift-bar>
<view class="svga-box" :class="giftFlag ? 'svga-show': 'svga-hide'">
<c-svga ref="cSvgaRef" :canvasId='canvasId' :src="src" :loops='1' :auto-play="true" @frame='onFrame' @finished='onFinished' @percentage='onPercentage' @loaded='onLoaded'></c-svga>
<view class="close-btn" @click="closeSvga"></view>
</view>
</view>
</template>
<script>
import MessageList from '@/pages/im/components/messageList.vue';
import ChatBar from "@/components/chat-bar/chat-bar.vue"
import GiftBar from "@/components/gift-bar/gift-bar.vue"
export default {
components: {
MessageList,
ChatBar,
GiftBar,
},
data() {
return {
giftFlag: false,
src: 'https://cos.duopei.feiniaowangluo.com/1866/reward/gift/171241866882267KgIoSuLt.svga',
canvasId: 'myCanvas',
modal: true,
current: 0, // tabs组件的current值表示当前活动的tab选项
swiperCurrent: 0,
typeidx: 0,
currentTab: 1,
showMoreSheet: false,
modalButton: [{
text: "置顶该聊天",
type: "green",
plain: false
},
{
text: "删除该聊天",
type: "green",
plain: false
},
{
text: "设置免打扰",
type: "green",
plain: false
}],
itemList: [{
text: "删除",
color: "#2B2B2B"
}, {
text: "举报",
color: "#2B2B2B"
}, {
text: "分享",
color: "#2B2B2B"
}],
}
},
created() {
},
computed: {
swiperHeight: {
get() {
let that = this;
return ' calc( 100vh - 100rpx )';
},
set(v) {}
},
},
methods: {
hidedKeyboard() {
this.$refs.chatBar.hideDrawer();
},
toChat() {
this.$u.route({
url: 'pages/chat/index',
});
},
//调用此方法显示组件
showModal() {
this.modal = true;
},
//隐藏组件
hideModal() {
this.modal = false;
},
handleClick(e){
let index = e.index;
if (index === 0) {
} else {
}
this.hideModal();
},
openGift() {
this.$refs.gift.open();
},
sendGift(e) {
this.src = e.src;
this.$refs.cSvgaRef.call('startAnimation');
this.giftFlag = true;
},
onFinished() {
this.giftFlag = false;
console.log('动画停止播放时回调');
},
onFrame(frame) {//动画播放至某帧后回调
// console.log(frame);
},
onPercentage(percentage) { //动画播放至某进度后回调
// console.log(percentage);
},
onLoaded() {
this.$refs.cSvgaRef.call('setContentMode', 'AspectFill');
console.log('加载完成');
},
closeSvga() {
this.$refs.cSvgaRef.call('stopAnimation');
this.giftFlag = false;
}
}
}
</script>
<style lang="scss" scoped>
.container {
background-color: #fafafa;
height: calc(100vh);
padding-bottom: env(safe-area-inset-bottom);
}
.nav-box {
display: flex;
justify-content: center;
align-items: center;
}
.tab-box {
display: flex;
justify-content: center;
align-items: center;
}
.tab-box .title-label {
height: 44px;
padding: 0 20rpx;
display: flex;
align-items: center;
justify-content: center;
position: relative;
color: #333333;
font-size: 28rpx;
}
.tab-box .title-label text {
font-weight: bold;
transition: all 0.3s;
}
.svga-box {
position: fixed;
top: 0;
left: 0;
z-index: 999999999;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, .7);
.close-btn {
color: #fff;
font-size: 14px;
position: absolute;
z-index: 999999999;
//background: rgba(0, 0, 0, .5);
padding: 5px 10px;
left: 0;
right: 0;
top: 0;
bottom: 0;
}
}
.svga-hide {
transform: translate(-100%, 0);
}
.svga-show {
transform: translate(0, 0);
}
</style>

194
pages/im/index.vue Normal file
View File

@@ -0,0 +1,194 @@
<template>
<view class="container page-app theme-light main-green font-1">
<layout v-if="groupId > 0 && receiveUserId > 0" @hidedKeyboard="hidedKeyboard" :isReconnecting="isReconnecting" :groupId="groupId" :userId="receiveUserId" ref="im">
<template #chat-bar="{keyboardHeight, user}">
<chat-bar ref="chatBar" :user="user" :keyboardHeight="keyboardHeight" @send="sendMessage" @openGift="openGift" @voiceOk="voiceOk" @getWeixin="getWeixin" @imageOk="imageOk"></chat-bar>
</template>
</layout>
<s-auth-modal />
<qrcode-modal />
</view>
</template>
<script>
import Layout from '@/pages/im/components/layout.vue';
import ChatBar from "@/pages/im/components/chatBar.vue";
import ImMessageApi from '@/sheep/api/im/memberMessage';
import qrcodeModal from '@/components/qrcode-modal/qrcode-modal.vue';
import FileApi from '@/sheep/api/infra/file';
import UserApi from '@/sheep/api/member/user';
import sheep from '@/sheep';
import { WxaSubscribeTemplate, WebSocketMessageTypeConstants } from '@/sheep/util/const';
import { useWebSocket } from '@/sheep/hooks/useWebSocket';
export default {
components: {
Layout,
ChatBar,
qrcodeModal,
},
data() {
return {
groupId: 0,
receiveUserId: 0,
Reconnect: {},
}
},
onLoad(option) {
this.groupId = option.groupId;
this.receiveUserId = option.receiveUserId;
},
onShow() {
//======================= 聊天工具相关 end =======================
const { options } = useWebSocket({
// 连接成功
onConnected: async () => {
console.log('连接成功');
},
// 收到消息
onMessage: async (data) => {
console.log('接收消息');
const type = data.type;
if (!type) {
console.error('未知的消息类型:' + data.value);
return;
}
if (type == WebSocketMessageTypeConstants.IM_MESSAGE_READ) {
console.log('刷新消息');
var that = this;
setTimeout(function(){
var message = JSON.parse(data.content);
if(message.groupId != that.groupId){
return;
}
that.$nextTick(function() {
that.$refs.im.reload(message);
});
},1000);
return;
}
if (type == WebSocketMessageTypeConstants.IM_MESSAGE_NEWS) {
var message = JSON.parse(data.content);
if(message.groupId != this.groupId){
return;
}
ImMessageApi.updateReadStatus(message.id);
this.$nextTick(function() {
this.$refs.im.reload(message);
});
return;
}
// 2.2 消息类型KEFU_MESSAGE_TYPE
if (type == WebSocketMessageTypeConstants.KEFU_MESSAGE_TYPE) {
console.log('刷新消息');
// 刷新消息列表
//await messageListRef.value.refreshMessageList(JSON.parse(data.content));
return;
}
// 2.3 消息类型KEFU_MESSAGE_ADMIN_READ
if (type == WebSocketMessageTypeConstants.KEFU_MESSAGE_ADMIN_READ) {
console.log('管理员已读消息');
}
},
});
this.Reconnect = options; // 重连状态
},
computed: {
isReconnecting() {
return this.Reconnect.isReconnecting;
},
},
onHide() {
},
methods: {
hidedKeyboard() {
this.$refs.chatBar.hideDrawer();
},
sendMessage(e) {
// #ifdef MP
// 订阅只能由用户主动触发,只能包一层 showModal 诱导用户点击
this.autoSubscribeMessage();
// #endif
ImMessageApi.sendImMessage({
groupId: this.groupId,
receiveUserId: this.receiveUserId,
contentType: e.contentType,
content: e.content,
voiceSec: e.voiceSec,
imgWidth: e.imgWidth,
imgHeight: e.imgHeight,
}).then(res => {
this.$refs.im.reload();
});
},
async imageOk(e) {
const res = await FileApi.uploadFile(e.path);
var data = {
contentType: 4,
content: res.data,
imgWidth: e.imgWidth,
imgHeight: e.imgHeight,
}
this.sendMessage(data);
},
openGift() {
sheep.$router.go('/pages/user/detail/index',{id: this.receiveUserId});
},
getWeixin() {
UserApi.getQrcodeByUserId(this.receiveUserId).then(res => {
if(res.data){
uni.previewImage({
current: 0, //预览图片的下标
urls: [res.data], //预览图片的地址,必须要数组形式,如果不是数组形式就转换成数组形式就可以
indicator: 'number',
loop: true
});
}
});
},
showDayTime(datetime) {
if (!datetime) return "";
return dayjs(datetime).fromNow();
},
async voiceOk(e) {
const res = await FileApi.uploadFile(e.path);
var data = {
contentType: 3,
content: res.data,
voiceSec: e.sec,
}
this.sendMessage(data);
},
subscribeMessage() {
const event = [WxaSubscribeTemplate.UNREAD_MESSAGE];
event.push(WxaSubscribeTemplate.CLERK_BLIND);
event.push(WxaSubscribeTemplate.CLERK_ORDER);
sheep.$platform.useProvider('wechat').subscribeMessage(event, () => {
// 订阅后记录一下订阅状态
uni.removeStorageSync(WxaSubscribeTemplate.UNREAD_MESSAGE);
uni.setStorageSync(WxaSubscribeTemplate.UNREAD_MESSAGE, '已订阅');
});
},
async autoSubscribeMessage() {
// 2. 订阅消息
this.subscribeMessage();
},
}
}
</script>
<style lang="scss" scoped>
.container {
background-color: #fafafa;
height: calc(100vh);
padding-bottom: env(safe-area-inset-bottom);
}
</style>

33
pages/im/util/emoji.js Normal file
View File

@@ -0,0 +1,33 @@
export default [
"😀", "😁", "😃", "😄", "😅", "😆", "😉", "😊", "😋", "😎", "😍",
"😘", "😗", "😙", "😚", "😇", "😐", "😑", "😶", "😏", "😣", "😥", "😮", "😯", "😪",
"😫", "😴", "😌", "😛", "😜", "😝", "😒", "😓", "😔", "😕", "😲", "😷", "😖", "😞", "😟",
"😤", "😢", "😭", "😦", "😧", "😨", "😬", "😰", "😱", "😳", "😵", "😡", "😠",
"👦", "👧", "👨", "👩", "👴", "👵", "👶", "👱", "👮", "👲", "👳", "👷", "👸", "💂", "🎅", "👰", "👼",
"💆", "💇", "🙍", "🙎", "🙅", "🙆", "💁", "🙋", "🙇", "🙌", "🙏", "👤", "👥", "🚶", "🏃", "👯",
"💃", "👫", "👬", "👭", "💏", "💑", "👪", "💪", "👈", "👉", "☝", "👆", "👇", "✌", "✋", "👌",
"👍", "👎", "✊", "👊", "👋", "👏", "👐", "✍", "👣", "👀", "👂", "👃", "👅", "👄", "💋", "👓",
"👔", "👙", "👛", "👜", "👝", "🎒", "💼", "👞", "👟", "👠", "👡", "👢", "👑",
"👒", "🎩", "🎓", "💄", "💅", "💍", "🌂", "📶", "📳", "📴", "♻", "🏧","🚮", "🚰", "♿", "🚹", "🚺",
"🚻", "🚼", "🚾", "⚠", "🚸", "⛔", "🚫", "🚳", "🚭", "🚯", "🚱", "🚷", "🔞", "💈",
"🙈", "🐒", "🐶", "🐕", "🐩", "🐺", "🐱","🐈", "🐯", "🐅", "🐆", "🐴", "🐎", "🐮", "🐂",
"🐃","🐄","🐷","🐖","🐗","🐽","🐏","🐑","🐐","🐪","🐫","🐘","🐭",
"🐁","🐀","🐹","🐰","🐇","🐻","🐨","🐼","🐾","🐔","🐓","🐣","🐤","🐥",
"🐦", "🐧", "🐸", "🐊","🐢", "🐍", "🐲", "🐉", "🐳", "🐋", "🐬", "🐟", "🐠", "🐡",
"🐙", "🐚", "🐌", "🐛", "🐜", "🐝", "🐞", "🦋", "💐", "🌸", "💮", "🌹", "🌺",
"🌻", "🌼", "🌷", "🌱", "🌲", "🌳", "🌴", "🌵", "🌾", "🌿", "🍀", "🍁", "🍂", "🍃",
"🌍","🌎","🌏","🌐","🌑","🌒","🌓","🌔","🌕","🌖","🌗","🌘","🌙","🌚",
"🌛","🌜","☀","🌝","🌞","⭐","🌟","🌠","☁","⛅","☔","⚡","❄","🔥","💧","🌊",
"🏀", "🏈", "🏉", "🎾", "🎱", "🎳", "⛳", "🎣", "🎽", "🎿",
"😈", "👿", "👹", "👺", "💀", "☠", "👻", "👽", "👾", "💣",
"🌋", "🗻", "🏠", "🏡", "🏢", "🏣", "🏤", "🏥", "🏦", "🏨",
"⛲", "🌁", "🌃", "🌆", "🌇", "🎠", "🎡", "🎢", "🚂",
"🚌", "🚍", "🚎", "🚏", "🚐", "🚑", "🚒", "🚓", "🚔", "🚕", "🚖", "🚗", "🚘",
"💌", "💎", "🔪", "💈", "🚪", "🚽", "🚿", "🛁", "⌛", "⏳", "⌚", "⏰", "🎈", "🎉",
"💤", "💢", "💬", "💭", "♨", "🌀", "🔔", "🔕", "✡", "✝", "🔯", "📛", "🔰", "🔱", "⭕", "✅",
"☑", "✔", "✖", "❌", "❎", "", "", "➗", "➰", "➿", "〽", "✳", "✴", "❇", "‼", "⁉", "❓", "❔", "❕", "❗",
"🕛", "🕧", "🕐", "🕜", "🕑", "🕝", "🕒", "🕞", "🕓", "🕟", "🕔", "🕠", "🕕", "🕡",
"🕖", "🕢", "🕗", "🕣", "🕘", "🕤", "🕙", "🕥", "🕚", "🕦", "⏱", "⏲", "🕰",
"💘", "❤", "💓", "💔", "💕", "💖", "💗", "💙", "💚", "💛", "💜", "💝", "💞", "💟❣",
"🍇", "🍈", "🍉", "🍊", "🍋", "🍌", "🍍", "🍎", "🍏", "🍐", "🍑", "🍒", "🍓",
]

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

@@ -0,0 +1,192 @@
<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,
categoryId: item.spu.categoryId
})
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({
items
}),
});
}
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>

237
pages/index/category.vue Normal file
View File

@@ -0,0 +1,237 @@
<!-- 商品分类列表 -->
<template>
<s-layout title="分类" tabbar="/pages/index/category" :bgStyle="{ color: '#fff' }">
<view class="s-category">
<view class="three-level-wrap ss-flex ss-col-top" :style="[{ height: pageHeight + 'px' }]">
<!-- 商品分类 -->
<scroll-view class="side-menu-wrap" scroll-y :style="[{ height: pageHeight + 'px' }]">
<view
class="menu-item ss-flex"
v-for="(item, index) in state.categoryList"
:key="item.id"
:class="[{ 'menu-item-active': index === state.activeMenu }]"
@tap="onMenu(index)"
>
<view class="menu-title ss-line-1">
{{ item.name }}
</view>
</view>
</scroll-view>
<!-- 商品分类 -->
<scroll-view
class="goods-list-box"
scroll-y
:style="[{ height: pageHeight + 'px' }]"
v-if="state.categoryList?.length"
>
<image
v-if="state.categoryList[state.activeMenu].picUrl"
class="banner-img"
:src="sheep.$url.cdn(state.categoryList[state.activeMenu].picUrl)"
mode="widthFix"
/>
<first-one v-if="state.style === 'first_one'" :pagination="state.pagination" />
<first-two v-if="state.style === 'first_two'" :pagination="state.pagination" />
<second-one
v-if="state.style === 'second_one'"
:data="state.categoryList"
:activeMenu="state.activeMenu"
/>
<uni-load-more
v-if="
(state.style === 'first_one' || state.style === 'first_two') &&
state.pagination.total > 0
"
:status="state.loadStatus"
:content-text="{
contentdown: '点击查看更多',
}"
@tap="loadMore"
/>
</scroll-view>
</view>
</view>
</s-layout>
</template>
<script setup>
import secondOne from './components/second-one.vue';
import firstOne from './components/first-one.vue';
import firstTwo from './components/first-two.vue';
import sheep from '@/sheep';
import CategoryApi from '@/sheep/api/product/category';
import SpuApi from '@/sheep/api/product/spu';
import { onLoad, onReachBottom } from '@dcloudio/uni-app';
import { computed, reactive } from 'vue';
import _ from 'lodash-es';
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 (params) => {
await getList();
// 首页点击分类的处理:查找满足条件的分类
const foundCategory = state.categoryList.find(category => category.id === params.id);
// 如果找到则调用 onMenu 自动勾选相应分类,否则调用 onMenu(0) 勾选第一个分类
onMenu(foundCategory ? state.categoryList.indexOf(foundCategory) : 0);
});
onReachBottom(() => {
loadMore();
});
</script>
<style lang="scss" scoped>
.s-category {
:deep() {
.side-menu-wrap {
width: 200rpx;
height: 100%;
padding-left: 12rpx;
background-color: #f6f6f6;
.menu-item {
width: 100%;
height: 88rpx;
position: relative;
transition: all linear 0.2s;
.menu-title {
line-height: 32rpx;
font-size: 30rpx;
font-weight: 400;
color: #333;
margin-left: 28rpx;
position: relative;
z-index: 0;
&::before {
content: '';
width: 64rpx;
height: 12rpx;
background: linear-gradient(
90deg,
var(--ui-BG-Main-gradient),
var(--ui-BG-Main-light)
) !important;
position: absolute;
left: -64rpx;
bottom: 0;
z-index: -1;
transition: all linear 0.2s;
}
}
&.menu-item-active {
background-color: #fff;
border-radius: 20rpx 0 0 20rpx;
&::before {
content: '';
position: absolute;
right: 0;
bottom: -20rpx;
width: 20rpx;
height: 20rpx;
background: radial-gradient(circle at 0 100%, transparent 20rpx, #fff 0);
}
&::after {
content: '';
position: absolute;
top: -20rpx;
right: 0;
width: 20rpx;
height: 20rpx;
background: radial-gradient(circle at 0% 0%, transparent 20rpx, #fff 0);
}
.menu-title {
font-weight: 600;
&::before {
left: 0;
}
}
}
}
}
.goods-list-box {
background-color: #fff;
width: calc(100vw - 100px);
padding: 10px;
}
.banner-img {
width: calc(100vw - 130px);
border-radius: 5px;
margin-bottom: 20rpx;
}
}
}
</style>

View File

@@ -0,0 +1,26 @@
<!-- 分类展示first-one 风格 -->
<template>
<view class="ss-flex-col">
<view class="goods-box" v-for="item in pagination.list" :key="item.id">
<s-goods-column
size="sl"
:data="item"
@click="sheep.$router.go('/pages/goods/index', { id: item.id })"
/>
</view>
</view>
</template>
<script setup>
import sheep from '@/sheep';
const props = defineProps({
pagination: Object,
});
</script>
<style lang="scss" scoped>
.goods-box {
width: 100%;
}
</style>

View File

@@ -0,0 +1,66 @@
<!-- 分类展示first-two 风格 -->
<template>
<view>
<view class="ss-flex flex-wrap">
<view class="goods-box" v-for="item in pagination?.list" :key="item.id">
<view @click="sheep.$router.go('/pages/goods/index', { id: item.id })">
<view class="goods-img">
<image class="goods-img" :src="item.picUrl" mode="aspectFit" />
</view>
<view class="goods-content">
<view class="goods-title ss-line-1 ss-m-b-28">{{ item.title }}</view>
<view class="goods-price">{{ fen2yuan(item.price) }}</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import sheep from '@/sheep';
import { fen2yuan } from '@/sheep/hooks/useGoods';
const props = defineProps({
pagination: Object,
});
</script>
<style lang="scss" scoped>
.goods-box {
width: calc((100% - 20rpx) / 2);
margin-bottom: 20rpx;
.goods-img {
width: 100%;
height: 246rpx;
border-radius: 10rpx 10rpx 0px 0px;
}
.goods-content {
width: 100%;
background: #ffffff;
box-shadow: 0px 0px 20rpx 4rpx rgba(199, 199, 199, 0.22);
padding: 20rpx 0 32rpx 16rpx;
box-sizing: border-box;
border-radius: 0 0 10rpx 10rpx;
.goods-title {
font-size: 26rpx;
font-weight: bold;
color: #333333;
}
.goods-price {
font-size: 24rpx;
font-family: OPPOSANS;
font-weight: 500;
color: #e1212b;
}
}
&:nth-child(2n + 1) {
margin-right: 20rpx;
}
}
</style>

View File

@@ -0,0 +1,80 @@
<!-- 分类展示second-one 风格 -->
<template>
<view>
<!-- 一级分类的名字 -->
<view class="title-box ss-flex ss-col-center ss-row-center ss-p-b-30">
<view class="title-line-left" />
<view class="title-text ss-p-x-20">{{ props.data[activeMenu].name }}</view>
<view class="title-line-right" />
</view>
<!-- 二级分类的名字 -->
<view class="goods-item-box ss-flex ss-flex-wrap ss-p-b-20">
<view
class="goods-item"
v-for="item in props.data[activeMenu].children"
:key="item.id"
@tap="
sheep.$router.go('/pages/goods/list', {
categoryId: item.id,
})
"
>
<image class="goods-img" :src="item.picUrl" mode="aspectFill" />
<view class="ss-p-10">
<view class="goods-title ss-line-1">{{ item.name }}</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import sheep from '@/sheep';
const props = defineProps({
data: {
type: Object,
default: () => ({}),
},
activeMenu: [Number, String],
});
</script>
<style lang="scss" scoped>
.title-box {
.title-line-left,
.title-line-right {
width: 15px;
height: 1px;
background: #d2d2d2;
}
}
.goods-item {
width: calc((100% - 20px) / 3);
margin-right: 10px;
margin-bottom: 10px;
&:nth-of-type(3n) {
margin-right: 0;
}
.goods-img {
width: calc((100vw - 140px) / 3);
height: calc((100vw - 140px) / 3);
}
.goods-title {
font-size: 26rpx;
font-weight: bold;
color: #333333;
line-height: 40rpx;
text-align: center;
}
.goods-price {
color: $red;
line-height: 40rpx;
}
}
</style>

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

@@ -0,0 +1,88 @@
<!-- 首页支持店铺装修 -->
<template>
<view v-if="template">
<s-layout title="首页" navbar="custom" tabbar="/pages/tabbar/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>

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

@@ -0,0 +1,39 @@
<!-- 微信公众号的登录回调页 -->
<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;
});
// 执行登录 or 绑定,注意需要 await 绑定
const event = options.event;
const code = options.code;
const state = options.state;
if (event === 'login') { // 场景一:登录
await sheep.$platform.useProvider().login(code, state);
} else if (event === 'bind') { // 场景二:绑定
await sheep.$platform.useProvider().bind(code, state);
}
// 检测 H5 登录回调
let returnUrl = uni.getStorageSync('returnUrl');
if (returnUrl) {
uni.removeStorage({key:'returnUrl'});
location.replace(returnUrl);
} else {
uni.switchTab({
url: '/',
});
}
// #endif
});
</script>

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

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

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

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

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

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

View File

@@ -0,0 +1,261 @@
<!-- 下单界面收货地址 or 自提门店的选择组件 -->
<template>
<view class="allAddress" :style="state.isPickUp ? '':'padding-top:10rpx;'">
<view class="nav flex flex-wrap">
<view class="item font-color" :class="state.deliveryType === 1 ? 'on' : 'on2'"
@tap="switchDeliveryType(1)" v-if='state.isPickUp' />
<view class="item font-color" :class="state.deliveryType === 2 ? 'on' : 'on2'"
@tap="switchDeliveryType(2)" v-if='state.isPickUp' />
</view>
<!-- 情况一收货地址的选择 -->
<view class='address flex flex-wrap flex-center ss-row-between' @tap='onSelectAddress' v-if='state.deliveryType === 1'
:style="state.isPickUp ? '':'border-top-left-radius: 14rpx;border-top-right-radius: 14rpx;'">
<view class='addressCon' v-if="state.addressInfo.name">
<view class='name'>{{ state.addressInfo.name }}
<text class='phone'>{{ state.addressInfo.mobile }}</text>
</view>
<view class="flex flex-wrap">
<text class='default font-color' v-if="state.addressInfo.defaultStatus">[默认]</text>
<text class="line2">{{ state.addressInfo.areaName }} {{ state.addressInfo.detailAddress }}</text>
</view>
</view>
<view class='addressCon' v-else>
<view class='setaddress'>设置收货地址</view>
</view>
<view class='iconfont'>
<view class="ss-rest-button">
<text class="_icon-forward" />
</view>
</view>
</view>
<!-- 情况二门店的选择 -->
<view class='address flex flex-wrap flex-center ss-row-between' v-else @tap="onSelectAddress">
<view class='addressCon' v-if="state.pickUpInfo.name">
<view class='name'>{{ state.pickUpInfo.name }}
<text class='phone'>{{ state.pickUpInfo.phone }}</text>
</view>
<view class="line1"> {{ state.pickUpInfo.areaName }}{{ ', ' + state.pickUpInfo.detailAddress }}
</view>
</view>
<view class='addressCon' v-else>
<view class='setaddress'>选择自提门店</view>
</view>
<view class='iconfont'>
<view class="ss-rest-button">
<text class="_icon-forward" />
</view>
</view>
</view>
<view class='line'>
<image :src="sheep.$url.static('/static/images/line.png', 'local')" />
</view>
</view>
</template>
<script setup>
import { computed } from 'vue';
import sheep from '@/sheep';
import { isEmpty } from 'lodash-es';
const props = defineProps({
modelValue: {
type: Object,
default() {},
}
});
const emits = defineEmits(['update:modelValue']);
// computed 解决父子组件双向数据同步
const state = computed({
get(){
return new Proxy(props.modelValue, {
set(obj, name, val) {
emits('update:modelValue', {
...obj,
[name]: val,
});
return true;
}
})
},
set(val){
emits('update:modelValue', val);
}
})
// 选择地址
function onSelectAddress() {
let emitName = 'SELECT_ADDRESS'
let addressPage = '/pages/user/address/list?type=select';
if (state.value.deliveryType === 2){
emitName = 'SELECT_PICK_UP_INFO'
addressPage = '/pages/user/goods_details_store/index'
}
uni.$once(emitName, (e) => {
changeConsignee(e.addressInfo);
});
sheep.$router.go(addressPage);
}
// 更改收货人地址&计算订单信息
async function changeConsignee(addressInfo = {}) {
if (!isEmpty(addressInfo)) {
if (state.value.deliveryType === 1){
state.value.addressInfo = addressInfo;
}
if (state.value.deliveryType === 2){
state.value.pickUpInfo = addressInfo;
}
}
}
// 收货方式切换
const switchDeliveryType = (type) =>{
state.value.deliveryType = type;
}
</script>
<style scoped lang="scss">
.allAddress .font-color{
color: #E93323!important
}
.line2{
width: 504rpx;
}
.textR {
text-align: right;
}
.line {
width: 100%;
height: 3rpx;
}
.line image {
width: 100%;
height: 100%;
display: block;
}
.address {
padding: 28rpx;
background-color: #fff;
box-sizing: border-box;
}
.address .addressCon {
width: 596rpx;
font-size: 26rpx;
color: #666;
}
.address .addressCon .name {
font-size: 30rpx;
color: #282828;
font-weight: bold;
margin-bottom: 10rpx;
}
.address .addressCon .name .phone {
margin-left: 50rpx;
}
.address .addressCon .default {
margin-right: 12rpx;
}
.address .addressCon .setaddress {
color: #333;
font-size: 28rpx;
}
.address .iconfont {
font-size: 35rpx;
color: #707070;
}
.allAddress {
width: 100%;
background: linear-gradient(to bottom, #e93323 0%, #f5f5f5 100%);
// background-image: linear-gradient(to bottom, #e93323 0%, #f5f5f5 100%);
// background-image: -webkit-linear-gradient(to bottom, #e93323 0%, #f5f5f5 100%);
// background-image: -moz-linear-gradient(to bottom, #e93323 0%, #f5f5f5 100%);
//padding: 100rpx 30rpx 0 30rpx;
padding-top: 100rpx;
padding-bottom: 10rpx;
}
.allAddress .nav {
width: 690rpx;
margin: 0 auto;
}
.allAddress .nav .item {
width: 334rpx;
}
.allAddress .nav .item.on {
position: relative;
width: 230rpx;
}
.allAddress .nav .item.on::before {
position: absolute;
bottom: 0;
content: "快递配送";
font-size: 28rpx;
display: block;
height: 0;
width: 336rpx;
border-width: 0 20rpx 80rpx 0;
border-style: none solid solid;
border-color: transparent transparent #fff;
z-index: 2;
border-radius: 14rpx 36rpx 0 0;
text-align: center;
line-height: 80rpx;
}
.allAddress .nav .item:nth-of-type(2).on::before {
content: "到店自提";
border-width: 0 0 80rpx 20rpx;
border-radius: 36rpx 14rpx 0 0;
}
.allAddress .nav .item.on2 {
position: relative;
}
.allAddress .nav .item.on2::before {
position: absolute;
bottom: 0;
content: "到店自提";
font-size: 28rpx;
display: block;
height: 0;
width: 401rpx;
border-width: 0 0 60rpx 60rpx;
border-style: none solid solid;
border-color: transparent transparent #f7c1bd;
border-radius: 36rpx 14rpx 0 0;
text-align: center;
line-height: 60rpx;
}
.allAddress .nav .item:nth-of-type(1).on2::before {
content: "快递配送";
border-width: 0 60rpx 60rpx 0;
border-radius: 14rpx 36rpx 0 0;
}
.allAddress .address {
width: 690rpx;
max-height: 180rpx;
margin: 0 auto;
}
.allAddress .line {
width: 100%;
margin: 0 auto;
}
</style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,182 @@
<!-- 虚拟列表演示(不使用内置列表)(vue) -->
<!-- 写法较简单在页面中对当前需要渲染的虚拟列表数据进行for循环在vue3中兼容性良好 -->
<!-- 在各平台兼容性请查阅https://z-paging.zxlee.cn/module/virtual-list.html -->
<template>
<view class="content">
<!-- 如果页面中的cell高度是固定不变的则不需要设置cell-height-mode如果页面中高度是动态改变的则设置cell-height-mode="dynamic" -->
<!-- 原先的v-model修改为@virtualListChange="virtualListChange"并赋值处理后的虚拟列表 -->
<z-paging ref="paging" :auto="false" use-virtual-list :force-close-inner-list="true" :paging-style="{ paddingTop: 0 + 'px', paddingBottom: paddingBottom + 'rpx' }" cell-height-mode="dynamic" @scroll="scroll" @virtualListChange="virtualListChange" @query="queryList">
<!-- 需要固定在顶部不滚动的view放在slot="top"的view中如果需要跟着滚动则不要设置slot="top" -->
<template #top>
<su-navbar :title="title" statusBar></su-navbar>
<tab-box :tabList="tabList" :currentValue="currentIndex" @change="tabChange"></tab-box>
</template>
<!-- :id="`zp-id-${item.zp_index}`":key="item.zp_index" 必须写必须写 -->
<!-- 这里for循环的index不是数组中真实的index了请使用item.zp_index获取真实的index -->
<order-list @onCancel="onCancel" @onConfirm="onConfirm" @onOrderConfirm="onOrderConfirm" :virtualList="virtualList"></order-list>
</z-paging>
</view>
</template>
<script>
import TabBox from '@/pages/order/blind/components/tabBox.vue';
import OrderList from '@/pages/order/blind/components/orderList.vue';
import OrderApi from '@/sheep/api/trade/order';
import { WxaSubscribeTemplate } from '@/sheep/util/const';
import sheep from '@/sheep';
import {
formatBlindOrderStatus,
handleBlindOrderButtons,
fen2yuan,
} from '@/sheep/hooks/useGoods';
export default {
components: {
TabBox,
OrderList,
},
props: {
title: {
type: String,
default: '抢单中心',
},
},
data() {
return {
// 虚拟列表数组,通过@virtualListChange监听获得最新数组
virtualList: [],
scrollTop: 0,
paddingTop: 0,
paddingBottom: 0,
height: 0,
tabList: [{
name: '全部',
}, {
name: '待抢单',
value: 10,
}, {
name: '服务中',
value: 20,
}, {
name: '已完成',
value: 30,
}],
currentIndex: 0,
}
},
methods: {
scroll(e) {
this.scrollTop = e.detail.scrollTop;
},
// 监听虚拟列表数组改变并赋值给virtualList进行重新渲染
virtualListChange(vList) {
this.virtualList = vList;
},
queryList(pageNo, pageSize) {
// 组件加载时会自动触发此方法,因此默认页面加载时会自动触发,无需手动调用
// 这里的pageNo和pageSize会自动计算好直接传给服务器即可
// 模拟请求服务器获取分页数据,请替换成自己的网络请求
const params = {
pageNo: pageNo,
pageSize: pageSize,
status: this.tabList[this.currentIndex].value,
commentStatus: this.tabList[this.currentIndex].value === 30 ? false : null,
}
OrderApi.getBlindOrderPage(params).then(res => {
// 将请求的结果数组传递给z-paging
res.data.list.forEach((order) => handleBlindOrderButtons(order, sheep.$store('user').userInfo.id));
res.data.list.forEach((order) => formatBlindOrderStatus(order));
res.data.list.forEach((order) => order.brokeragePrice = fen2yuan(order.brokeragePrice));
this.$refs.paging.complete(res.data.list);
}).catch(res => {
// 如果请求失败写this.$refs.paging.complete(false);
// 注意每次都需要在catch中写这句话很麻烦z-paging提供了方案可以全局统一处理
// 在底层的网络请求抛出异常时写uni.$emit('z-paging-error-emit');即可
this.$refs.paging.complete(false);
})
},
tabChange(e) {
this.currentIndex = e;
this.$refs.paging.reload();
},
// 取消订单
async onCancel(orderId) {
var that = this;
uni.showModal({
title: '提示',
content: '确定要取消订单吗?',
success: async function (res) {
if (!res.confirm) {
return;
}
const { code } = await OrderApi.cancelOrder(orderId);
if (code === 0) {
that.$refs.paging.reload();
}
},
});
},
// 开始接单
async onOrderConfirm(order) {
var that = this;
uni.showModal({
title: '是否确认抢单',
content: '请确认符合老板要求',
success: async function (res) {
if (!res.confirm) {
return;
}
// 正常的确认收货流程
const { code } = await OrderApi.deliveryOrder(order.id);
if (code === 0) {
that.tabChange(2);
}
},
});
},
// 确认收货
async onConfirm(order) {
// #ifdef MP
// 订阅只能由用户主动触发,只能包一层 showModal 诱导用户点击
this.subscribeMessage();
// #endif
var that = this;
uni.showModal({
title: '确定要结束服务么?',
content: '请确认经过老板同意',
success: async function (res) {
if (!res.confirm) {
return;
}
// 正常的确认收货流程
const { code } = await OrderApi.receiveOrder(order.id);
if (code === 0) {
that.$refs.paging.reload();
}
},
});
},
subscribeMessage() {
const event = [WxaSubscribeTemplate.CLERK_ORDER];
event.push(WxaSubscribeTemplate.REWARD_SUCCESS);
sheep.$platform.useProvider('wechat').subscribeMessage(event, () => {
// 订阅后记录一下订阅状态
uni.removeStorageSync(WxaSubscribeTemplate.CLERK_ORDER);
uni.setStorageSync(WxaSubscribeTemplate.CLERK_ORDER, '已订阅');
});
},
}
}
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,248 @@
<template>
<view @click="onOrderDetail(order.id)" :id="`zp-id-${order.zp_index}`" :key="order.zp_index" v-for="(order,index) in orderList" class="order-card">
<view class="no-box">
<view class="order-no">
<u-icon name="order"></u-icon>
<text class="number">{{ order.no }}</text>
</view>
<view class="status">{{order.statusStr}}</view>
</view>
<view class="main-box" v-for="item in order.items" :key="item.id">
<view>
<u-avatar size="100" :src="order.avatar"></u-avatar>
</view>
<view class="right-box">
<view class="nickname-box">
<view class="nickname">{{order.nickname}}</view>
<view class="info">订单时间{{order.createTimeStr}}</view>
<view class="info">服务内容{{item.spuName}} {{item.properties.map((property) => property.valueName).join(' ')}}×{{item.count}}</view>
<view class="info" v-if="order.sex == '1'">性别要求女生</view>
<view class="info" v-if="order.sex == '0'">性别要求男生</view>
<view class="info" v-if="userInfo.id == order.workerUserId">
<text class="weixin">微信{{order.weixin}}</text>
<view @tap.stop="onCopy(order.weixin)">
<u-icon name="outline-copy" custom-prefix="iconfont"></u-icon>
</view>
</view>
<view class="info">
<text>剩余时间</text>
<view v-if="order.receiveTime">
<text v-if="order.end">已结束</text>
<view v-else>
<u-count-down :timestamp="order.timestamp" format="DD天HH时mm分ss秒" @end="order.end = true"></u-count-down>
</view>
</view>
<text v-else>暂未接单</text>
</view>
<view class="info" v-if="order.userRemark">备注{{order.userRemark}}</view>
<view class="info" v-if="order.cancelReason">取消原因{{order.cancelReason}}</view>
</view>
<view class="price-box">
<text class="price">{{order.brokeragePrice}}</text>
<text>/</text>
</view>
</view>
</view>
<view class="bottom-box">
<view class="btn-box">
<view v-if="order.buttons.includes('other')" class="btn">也被其它店员抢单</view>
<view v-if="order.buttons.includes('detail')" @tap.stop="onOrderDetail(order.id)" class="btn">查看详情</view>
<view v-if="order.buttons.includes('unpay')" @tap.stop="onOrderCancel(order.id, order.items[0].id)" class="btn">取消接单</view>
<view v-if="order.buttons.includes('cancel')" @tap.stop="onCancel(order.id)" class="btn">取消订单</view>
<view v-if="order.buttons.includes('invite')" @tap.stop="onBuy(order.items[0].spuId)" class="btn active">邀请好友抢单</view>
<view v-if="order.buttons.includes('invite2')" @tap.stop="onBuy(order.items[0].spuId)" class="btn active">邀请评价</view>
<view v-if="order.buttons.includes('agree')" @tap.stop="onOrderConfirm(order)" class="btn active">立即抢单</view>
<view v-if="order.buttons.includes('confirm')" @tap.stop="onConfirm(order)" class="btn active">结束服务</view>
</view>
</view>
</view>
</template>
<script>
import sheep from '@/sheep';
import dayjs from 'dayjs';
export default {
components: {
},
props: {
virtualList: {
type: Array,
default: [],
},
},
emits: ["onCancel", "onConfirm", "onOrderConfirm"],
data() {
return {
}
},
computed: {
userInfo: {
get() {
return sheep.$store('user').userInfo;
},
},
orderList() {
this.virtualList.forEach((order) => order.createTimeStr = sheep.$helper.timeFormat(order.createTime, 'yyyy-mm-dd hh:MM:ss'));
this.virtualList.forEach((order) => order.timestamp = order.receiveTime ? sheep.$helper.parseTimeData(order.receiveTime) : 0);
return this.virtualList;
},
},
methods: {
// 订单详情
onOrderDetail(id) {
sheep.$router.go('/pages/order/blind/detail', {
id,
});
},
onCancel(orderId) {
this.$emit('onCancel', orderId);
},
onBuy(id) {
sheep.$router.go('/pages/clerk/detail/index', {
id,
});
},
// 评价
onComment(id) {
sheep.$router.go('/pages/goods/comment/worker/add', {
id,
});
},
// 继续支付
onPay(payOrderId) {
sheep.$router.go('/pages/pay/worker/index', {
id: payOrderId,
});
},
onOrderCancel(orderId, itemId) {
sheep.$router.go('/pages/order/worker/aftersale/apply', {
orderId: orderId,
itemId: itemId
});
},
onOrderConfirm(order) {
this.$emit('onOrderConfirm', order);
},
// 复制
onCopy(text) {
sheep.$helper.copyText(text);
},
// 确认收货
onConfirm(order) {
this.$emit('onConfirm', order);
},
}
}
</script>
<style lang="scss" scoped>
.order-card {
padding: 10px;
margin-top: 10px;
background-color: #fff;
display: flex;
flex-direction: column;
flex: 1;
.no-box {
display: flex;
align-items: center;
justify-content: space-between;
flex: 1;
padding-bottom: 10px;
color: rgb(100, 101, 102);
.order-no {
.number {
margin-left: 5px;
font-size: 22rpx;
}
}
.status {
font-size: 22rpx;
}
}
.main-box {
display: flex;
padding: 10px 0;
border-top: 1px solid #fbf0f0;
border-bottom: 1px solid #fbf0f0;
margin-bottom: 10px;
.right-box {
display: flex;
justify-content: space-between;
flex: 1;
align-items: center;
margin-left: 10px;
.nickname {
font-size: 12px;
font-weight: bold;
margin-bottom: 10px;
}
.info {
font-size: 22rpx;
color: rgb(100, 101, 102);
line-height: 22rpx;
margin-bottom: 5px;
display: flex;
align-items: center;
.weixin {
margin-right: 5px;
}
}
.price-box {
font-size: 22rpx;
color: rgb(100, 101, 102);
.price {
font-size: 34rpx;
font-weight: bold;
color: var(--ui-BG-Main);
}
}
}
}
.bottom-box {
display: flex;
justify-content: flex-end;
.btn-box {
display: flex;
align-items: center;
}
.btn {
background-color: #ddd;
color: rgb(100, 101, 102);
font-size: 22rpx;
border-radius: 40px;
padding: 5px 10px;
margin-left: 10px;
min-width: 140rpx;
display: flex;
justify-content: center;
}
.active {
background-color: var(--ui-BG-Main);
color: #fff;
}
}
}
</style>

View File

@@ -0,0 +1,47 @@
<template>
<view>
<u-tabs :list="tabList" bg-color="#fff" font-size="28" active-color="var(--ui-BG-Main)" :is-scroll="false" v-model="current" @change="change"></u-tabs>
</view>
</template>
<script>
export default {
components: {
},
props: {
currentValue: 0,
tabList: {
type: Array,
default: [],
},
},
data() {
return {
}
},
computed: {
current: {
get() {
return this.currentValue;
},
set(newValue) {
}
},
},
methods: {
change(index) {
this.$emit('change', index);
},
}
}
</script>
<style lang="scss" scoped>
.tab-box {
background-color: #fff;
padding: 5px 0;
}
</style>

View File

@@ -0,0 +1,435 @@
<template>
<s-layout title="确认订单">
<!-- 商品信息 -->
<view class="order-card-box ss-m-b-14">
<s-goods-item
v-for="item in state.orderInfo.items"
:key="item.skuId"
:img="item.picUrl"
:title="item.spuName"
:skuText="item.properties.map((property) => property.valueName).join(' ')"
:price="item.price"
:num="item.count"
marginBottom="10"
/>
<view class="order-item ss-flex ss-col-center ss-row-between ss-p-x-20 bg-white ss-r-10">
<view class="item-title">订单备注</view>
<view class="ss-flex ss-col-center">
<uni-easyinput
maxlength="20"
placeholder="请输入备注内容"
v-model="state.orderPayload.remark"
:inputBorder="false"
:clearable="false"
/>
</view>
</view>
</view>
<!-- 价格信息 -->
<view class="bg-white total-card-box ss-p-20 ss-m-b-14 ss-r-10">
<view class="total-box-content border-bottom">
<view class="order-item ss-flex ss-col-center ss-row-between">
<view class="item-title">技能金额</view>
<view class="ss-flex ss-col-center">
<text class="item-value ss-m-r-24">
{{ fen2yuan(state.orderInfo.price.totalPrice) }}
</text>
</view>
</view>
<view
class="order-item ss-flex ss-col-center ss-row-between"
v-if="state.orderInfo.type === 6"
>
<view class="item-title">积分抵扣</view>
<view class="ss-flex ss-col-center">
{{ state.pointStatus ? '剩余积分' : '当前积分' }}
<image
:src="sheep.$url.static('/static/img/shop/goods/score1.svg')"
class="score-img"
/>
<text class="item-value ss-m-r-24">
{{ state.pointStatus ? state.orderInfo.totalPoint - state.orderInfo.usePoint : (state.orderInfo.totalPoint || 0) }}
</text>
<checkbox-group @change="changeIntegral">
<checkbox :checked='state.pointStatus' :disabled="!state.orderInfo.totalPoint || state.orderInfo.totalPoint <= 0" />
</checkbox-group>
</view>
</view>
<view v-if="isPass" class="order-item ss-flex ss-col-center ss-row-between">
<view class="item-title">TA添加你的微信号</view>
<view class="ss-flex ss-col-center">
<uni-easyinput
maxlength="20"
placeholder="输入正确微信号"
v-model="state.orderInfo.weixin"
:inputBorder="false"
:clearable="false"
/>
</view>
</view>
<!-- 优惠劵只有 type = 0 普通订单非拼团秒杀砍价才可以使用优惠劵 -->
<view
class="order-item ss-flex ss-col-center ss-row-between"
v-if="state.orderInfo.type === 6"
>
<view class="item-title">优惠券</view>
<view class="ss-flex ss-col-center" @tap="state.showCoupon = true">
<text class="item-value text-red" v-if="state.orderPayload.couponId > 0">
-{{ fen2yuan(state.orderInfo.price.couponPrice) }}
</text>
<text
class="item-value"
:class="state.couponInfo.length > 0 ? 'text-red' : 'text-disabled'"
v-else
>
{{
state.couponInfo.length > 0 ? state.couponInfo.length + ' 张可用' : '暂无可用优惠券'
}}
</text>
<text class="_icon-forward item-icon" />
</view>
</view>
<view
class="order-item ss-flex ss-col-center ss-row-between"
v-if="state.orderInfo.price.discountPrice > 0"
>
<view class="item-title">活动优惠</view>
<view class="ss-flex ss-col-center">
<!-- @tap="state.showDiscount = true" TODO puhui999折扣后续要把优惠信息打进去 -->
<text class="item-value text-red">
-{{ fen2yuan(state.orderInfo.price.discountPrice) }}
</text>
<text class="_icon-forward item-icon" />
</view>
</view>
<view
class="order-item ss-flex ss-col-center ss-row-between"
v-if="state.orderInfo.price.vipPrice > 0"
>
<view class="item-title">会员优惠</view>
<view class="ss-flex ss-col-center">
<text class="item-value text-red">
-{{ fen2yuan(state.orderInfo.price.vipPrice) }}
</text>
</view>
</view>
</view>
<view class="total-box-footer ss-font-28 ss-flex ss-row-right ss-col-center ss-m-r-28">
<view class="total-num ss-m-r-20">
{{ state.orderInfo.items.reduce((acc, item) => acc + item.count, 0) }}
</view>
<view>合计</view>
<view class="total-num text-red"> {{ fen2yuan(state.orderInfo.price.payPrice) }}</view>
</view>
</view>
<!-- 选择优惠券弹框 -->
<s-coupon-select
v-model="state.couponInfo"
:show="state.showCoupon"
@confirm="onSelectCoupon"
@close="state.showCoupon = false"
/>
<!-- 满额折扣弹框 TODO @puhui999折扣后续要把优惠信息打进去 -->
<s-discount-list
v-model="state.orderInfo"
:show="state.showDiscount"
@close="state.showDiscount = false"
/>
<!-- 底部 -->
<su-fixed bottom :opacity="false" bg="bg-white" placeholder :noFixed="false" :index="200">
<view class="footer-box border-top ss-flex ss-row-between ss-p-x-20 ss-col-center">
<view class="total-box-footer ss-flex ss-col-center">
<view class="total-num ss-font-30 text-red">
{{ fen2yuan(state.orderInfo.price.payPrice) }}
</view>
</view>
<button
class="ss-reset-button ui-BG-Main-Gradient ss-r-40 submit-btn ui-Shadow-Main"
@tap="onConfirm"
>
提交订单
</button>
</view>
</su-fixed>
<qrcode-modal />
</s-layout>
</template>
<script setup>
import { reactive, ref, watch, computed } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import AddressSelection from '@/pages/order/addressSelection.vue';
import qrcodeModal from '@/components/qrcode-modal/qrcode-modal.vue';
import sheep from '@/sheep';
import OrderApi from '@/sheep/api/trade/order';
import CouponApi from '@/sheep/api/promotion/coupon';
import { fen2yuan } from '@/sheep/hooks/useGoods';
const isPass = computed(() => {
return sheep.$store('user').tradeConfig.weixinEnabled;
});
const state = reactive({
orderPayload: {},
orderInfo: {
items: [], // 商品项列表
price: {}, // 价格信息
},
showCoupon: false, // 是否展示优惠劵
couponInfo: [], // 优惠劵列表
showDiscount: false, // 是否展示营销活动
// ========== 积分 ==========
pointStatus: false, //是否使用积分
});
const addressState = ref({
addressInfo: {}, // 选择的收货地址
deliveryType: 1, // 收货方式1-快递配送2-门店自提
isPickUp: true, // 门店自提是否开启 TODO puhui999: 默认开启,看看后端有开关的话接入
pickUpInfo: {}, // 选择的自提门店信息
weixin: '', // 收件人名称
receiverMobile: '', // 收件人手机
});
// ========== 积分 ==========
/**
* 使用积分抵扣
*/
const changeIntegral = async () => {
state.pointStatus = !state.pointStatus;
await getOrderInfo();
};
// 选择优惠券
async function onSelectCoupon(couponId) {
state.orderPayload.couponId = couponId;
await getOrderInfo();
state.showCoupon = false;
}
// 提交订单
function onConfirm() {
if (isPass && !state.orderInfo.weixin) {
sheep.$helper.toast('请填写正确的微信号');
return;
}
submitOrder();
}
// 创建订单&跳转
async function submitOrder() {
const { code, data } = await OrderApi.createWorkerOrder({
items: state.orderPayload.items,
couponId: state.orderPayload.couponId,
remark: state.orderPayload.remark,
deliveryType: addressState.value.deliveryType,
addressId: addressState.value.addressInfo.id, // 收件地址编号
pickUpStoreId: addressState.value.pickUpInfo.id,//自提门店编号
weixin: state.orderInfo.weixin,// 微信名称
receiverMobile: addressState.value.receiverMobile,// 选择门店自提时,该字段为联系人手机
pointStatus: state.pointStatus,
combinationActivityId: state.orderPayload.combinationActivityId,
combinationHeadId: state.orderPayload.combinationHeadId,
seckillActivityId: state.orderPayload.seckillActivityId,
workerClerkLevelId: state.orderPayload.workerClerkLevelId,
sex: state.orderPayload.sex,
blind: state.orderPayload.blind,
});
if (code !== 0) {
return;
}
// 更新购物车列表,如果来自购物车
if (state.orderPayload.items[0].cartId > 0) {
sheep.$store('cart').getList();
}
// 跳转到支付页面
sheep.$router.redirect('/pages/pay/worker/index', {
id: data.payOrderId,
});
}
// 检查库存 & 计算订单价格
async function getOrderInfo() {
// 计算价格
const { data, code } = await OrderApi.clerkOrder({
items: state.orderPayload.items,
couponId: state.orderPayload.couponId,
deliveryType: addressState.value.deliveryType,
addressId: addressState.value.addressInfo.id, // 收件地址编号
pickUpStoreId: addressState.value.pickUpInfo.id,//自提门店编号
weixin: state.orderInfo.weixin,// 微信名称
receiverMobile: addressState.value.receiverMobile,// 选择门店自提时,该字段为联系人手机
pointStatus: state.pointStatus,
combinationActivityId: state.orderPayload.combinationActivityId,
combinationHeadId: state.orderPayload.combinationHeadId,
seckillActivityId: state.orderPayload.seckillActivityId,
workerClerkLevelId: state.orderPayload.workerClerkLevelId,
});
if (code !== 0) {
return;
}
state.orderInfo = data;
// 设置收货地址
if (state.orderInfo.address) {
addressState.value.addressInfo = state.orderInfo.address;
}
}
// 获取可用优惠券
async function getCoupons() {
const { code, data } = await CouponApi.getMatchCouponList(
state.orderInfo.price.payPrice,
state.orderInfo.items.map((item) => item.spuId),
state.orderPayload.items.map((item) => item.skuId),
state.orderPayload.items.map((item) => item.categoryId),
);
if (code === 0) {
state.couponInfo = data;
}
}
onLoad(async (options) => {
if (!options.data) {
sheep.$helper.toast('参数不正确,请检查!');
return;
}
state.orderPayload = JSON.parse(options.data);
await getOrderInfo();
await getCoupons();
});
// 使用 watch 监听地址和配送方式的变化
watch(addressState, async (newAddress, oldAddress) => {
// 如果收货地址或配送方式有变化,则重新计算价格
if (newAddress.addressInfo.id !== oldAddress.addressInfo.id || newAddress.deliveryType !== oldAddress.deliveryType) {
await getOrderInfo();
}
});
</script>
<style lang="scss" scoped>
:deep() {
.uni-input-wrapper {
width: 320rpx;
}
.uni-easyinput__content-input {
font-size: 28rpx;
height: 72rpx;
text-align: right !important;
padding-right: 0 !important;
.uni-input-input {
font-weight: 500;
color: #333333;
font-size: 26rpx;
height: 32rpx;
margin-top: 4rpx;
}
}
.uni-easyinput__content {
display: flex !important;
align-items: center !important;
justify-content: right !important;
}
}
.score-img {
width: 36rpx;
height: 36rpx;
margin: 0 4rpx;
}
.order-item {
height: 80rpx;
.item-title {
font-size: 28rpx;
font-weight: 400;
}
.item-value {
font-size: 28rpx;
font-weight: 500;
font-family: OPPOSANS;
}
.text-disabled {
color: #bbbbbb;
}
.item-icon {
color: $dark-9;
}
.remark-input {
text-align: right;
}
.item-placeholder {
color: $dark-9;
font-size: 26rpx;
text-align: right;
}
}
.total-box-footer {
height: 90rpx;
.total-num {
color: #333333;
font-family: OPPOSANS;
}
}
.footer-box {
height: 100rpx;
.submit-btn {
width: 240rpx;
height: 70rpx;
font-size: 28rpx;
font-weight: 500;
.goto-pay-text {
line-height: 28rpx;
}
}
.cancel-btn {
width: 240rpx;
height: 80rpx;
font-size: 26rpx;
background-color: #e5e5e5;
color: $dark-9;
}
}
.title {
font-size: 36rpx;
font-weight: bold;
color: #333333;
}
.subtitle {
font-size: 28rpx;
color: #999999;
}
.cicon-checkbox {
font-size: 36rpx;
color: var(--ui-BG-Main);
}
.cicon-box {
font-size: 36rpx;
color: #999999;
}
</style>

View File

@@ -0,0 +1,599 @@
<!-- 订单详情 -->
<template>
<s-layout title="订单详情" class="index-wrap" navbar="inner">
<!-- 订单状态 TODO -->
<view
class="state-box ss-flex-col ss-col-center ss-row-right"
:style="[
{
marginTop: '-' + Number(statusBarHeight + 88 + 22) + 'rpx',
paddingTop: Number(statusBarHeight + 88 + 22) + 'rpx',
},
]"
>
<view class="ss-flex ss-m-t-32 ss-m-b-20">
<image
v-if="
state.orderInfo.status_code == 'unpaid' ||
state.orderInfo.status === 10 || // 待发货
state.orderInfo.status_code == 'nocomment'
"
class="state-img"
:src="sheep.$url.static('/static/img/shop/order/order_loading.png')"
>
</image>
<image
v-if="
state.orderInfo.status_code == 'completed' ||
state.orderInfo.status_code == 'refund_agree'
"
class="state-img"
:src="sheep.$url.static('/static/img/shop/order/order_success.png')"
>
</image>
<image
v-if="state.orderInfo.status_code == 'cancel' || state.orderInfo.status_code == 'closed'"
class="state-img"
:src="sheep.$url.static('/static/img/shop/order/order_close.png')"
>
</image>
<image
v-if="state.orderInfo.status_code == 'noget'"
class="state-img"
:src="sheep.$url.static('/static/img/shop/order/order_express.png')"
>
</image>
<view class="ss-font-30">{{ formatBlindOrderStatus(state.orderInfo) }}</view>
</view>
<view class="ss-font-26 ss-m-x-20 ss-m-b-70">
{{ formatBlindOrderStatusDescription(state.orderInfo) }}
</view>
</view>
<!-- 收货地址 -->
<view class="order-address-box" v-if="state.orderInfo.receiverAreaId > 0">
<view class="ss-flex ss-col-center">
<text class="address-username">
{{ state.orderInfo.receiverName }}
</text>
<text class="address-phone">{{ state.orderInfo.receiverMobile }}</text>
</view>
<view class="address-detail">
{{ state.orderInfo.receiverAreaName }} {{ state.orderInfo.receiverDetailAddress }}
</view>
</view>
<view
class="detail-goods"
:style="[{ marginTop: state.orderInfo.receiverAreaId > 0 ? '0' : '-40rpx' }]"
>
<!-- 订单信 -->
<view class="order-list" v-for="item in state.orderInfo.items" :key="item.goods_id">
<view class="order-card">
<s-goods-item
@tap="onGoodsDetail(item.spuId)"
:img="item.picUrl"
:title="item.spuName"
:skuText="item.properties.map((property) => property.valueName).join(' ')"
:price="item.price"
:num="item.count"
>
<template #tool>
<view class="ss-flex">
<button
class="ss-reset-button apply-btn"
v-if="[10].includes(state.orderInfo.status) && item.afterSaleStatus === 0"
@tap.stop="
sheep.$router.go('/pages/order/blind/list', {
type: 1,
})
"
>
立即抢单
</button>
<button
class="ss-reset-button apply-btn"
v-if="[20].includes(state.orderInfo.status) && item.afterSaleStatus === 0 && state.orderInfo.workerUserId == sheep.$store('user').userInfo.id"
@tap.stop="
sheep.$router.go('/pages/order/worker/aftersale/apply', {
orderId: state.orderInfo.id,
itemId: item.id,
})
"
>
取消接单
</button>
</view>
</template>
<template #priceSuffix>
<button class="ss-reset-button tag-btn" v-if="item.status_text">
{{ item.status_text }}
</button>
</template>
</s-goods-item>
</view>
</view>
</view>
<!-- 自提核销 -->
<PickUpVerify :order-info="state.orderInfo" :systemStore="systemStore" ref="pickUpVerifyRef"></PickUpVerify>
<!-- 订单信息 -->
<view class="notice-box">
<view class="notice-box__content">
<view class="notice-item--center">
<view class="ss-flex ss-flex-1">
<text class="title">订单编号:</text>
<text class="detail">{{ state.orderInfo.no }}</text>
</view>
<button class="ss-reset-button copy-btn" @tap="onCopy">复制</button>
</view>
<view class="notice-item">
<text class="title">下单时间:</text>
<text class="detail">
{{ sheep.$helper.timeFormat(state.orderInfo.createTime, 'yyyy-mm-dd hh:MM:ss') }}
</text>
</view>
<view class="notice-item" v-if="state.orderInfo.payTime">
<text class="title">支付时间:</text>
<text class="detail">
{{ sheep.$helper.timeFormat(state.orderInfo.payTime, 'yyyy-mm-dd hh:MM:ss') }}
</text>
</view>
<view class="notice-item">
<text class="title">支付方式:</text>
<text class="detail">{{ state.orderInfo.payChannelName || '-' }}</text>
</view>
</view>
</view>
<!-- 价格信息 -->
<view class="order-price-box">
<view class="notice-item ss-flex ss-row-between">
<text class="title">订单总额</text>
<view class="ss-flex">
<text class="detail">¥{{ fen2yuan(state.orderInfo.totalPrice) }}</text>
</view>
</view>
<view class="notice-item ss-flex ss-row-between" v-if="state.orderInfo.couponPrice > 0">
<text class="title">优惠劵金额</text>
<text class="detail">-¥{{ fen2yuan(state.orderInfo.couponPrice) }}</text>
</view>
<view class="notice-item ss-flex ss-row-between" v-if="state.orderInfo.pointPrice > 0">
<text class="title">积分抵扣</text>
<text class="detail">-¥{{ fen2yuan(state.orderInfo.pointPrice) }}</text>
</view>
<view class="notice-item ss-flex ss-row-between" v-if="state.orderInfo.discountPrice > 0">
<text class="title">活动优惠</text>
<text class="detail">¥{{ fen2yuan(state.orderInfo.discountPrice) }}</text>
</view>
<view class="notice-item ss-flex ss-row-between" v-if="state.orderInfo.vipPrice > 0">
<text class="title">会员优惠</text>
<text class="detail">-¥{{ fen2yuan(state.orderInfo.vipPrice) }}</text>
</view>
<view class="notice-item ss-flex ss-row-between">
<text class="title">用户付款</text>
<text class="detail">¥{{ fen2yuan(state.orderInfo.payPrice) }}</text>
</view>
<view class="notice-item ss-flex ss-row-between">
<text class="title">佣金比例</text>
<text class="detail">{{state.orderInfo.brokeragePercent}}%</text>
</view>
<view class="notice-item all-rpice-item ss-flex ss-m-t-20">
<text class="title">{{ state.orderInfo.status == '30' ? '结算佣金' : '可得佣金' }}</text>
<text class="detail all-price">¥{{ fen2yuan(state.orderInfo.brokeragePrice) }}</text>
</view>
<view
class="notice-item all-rpice-item ss-flex ss-m-t-20"
v-if="state.orderInfo.refundPrice > 0"
>
<text class="title">已退款</text>
<text class="detail all-price">¥{{ fen2yuan(state.orderInfo.refundPrice) }}</text>
</view>
</view>
</s-layout>
</template>
<script setup>
import sheep from '@/sheep';
import { onLoad } from '@dcloudio/uni-app';
import { reactive, ref } from 'vue';
import { isEmpty } from 'lodash-es';
import {
fen2yuan,
formatBlindOrderStatus,
formatBlindOrderStatusDescription,
handleOrderButtons,
} from '@/sheep/hooks/useGoods';
import OrderApi from '@/sheep/api/trade/order';
import DeliveryApi from '@/sheep/api/trade/delivery';
import PickUpVerify from '@/pages/order/pickUpVerify.vue';
const statusBarHeight = sheep.$platform.device.statusBarHeight * 2;
const headerBg = sheep.$url.css('/static/img/shop/order/order_bg.png');
const state = reactive({
orderInfo: {},
merchantTradeNo: '', // 商户订单号
comeinType: '', // 进入订单详情的来源类型
});
// ========== 门店自提(核销) ==========
const systemStore = ref({}); // 门店信息
// 复制
const onCopy = () => {
sheep.$helper.copyText(state.orderInfo.no);
};
// 去支付
function onPay(payOrderId) {
sheep.$router.go('/pages/pay/index', {
id: payOrderId,
});
}
// 查看商品
function onGoodsDetail(id) {
if(id) {
sheep.$router.go('/pages/clerk/detail/index', {
id,
});
}
}
// 取消订单
async function onCancel(orderId) {
uni.showModal({
title: '提示',
content: '确定要取消订单吗?',
success: async function(res) {
if (!res.confirm) {
return;
}
const { code } = await OrderApi.cancelOrder(orderId);
if (code === 0) {
await getOrderDetail(orderId);
}
},
});
}
// 查看物流
async function onExpress(id) {
sheep.$router.go('/pages/order/express/log', {
id,
});
}
// 确认收货
async function onConfirm(orderId, ignore = false) {
// 需开启确认收货组件
// todo: 芋艿:待接入微信
// 1.怎么检测是否开启了发货组件功能如果没有开启的话就不能在这里return出去
// 2.如果开启了走mpConfirm方法,需要在App.vue的show方法中拿到确认收货结果
let isOpenBusinessView = true;
if (
sheep.$platform.name === 'WechatMiniProgram' &&
!isEmpty(state.orderInfo.wechat_extra_data) &&
isOpenBusinessView &&
!ignore
) {
mpConfirm(orderId);
return;
}
// 正常的确认收货流程
const { code } = await OrderApi.receiveOrder(orderId);
if (code === 0) {
await getOrderDetail(orderId);
}
}
// #ifdef MP-WEIXIN
// 小程序确认收货组件
function mpConfirm(orderId) {
if (!wx.openBusinessView) {
sheep.$helper.toast(`请升级微信版本`);
return;
}
wx.openBusinessView({
businessType: 'weappOrderConfirm',
extraData: {
merchant_trade_no: state.orderInfo.wechat_extra_data.merchant_trade_no,
transaction_id: state.orderInfo.wechat_extra_data.transaction_id,
},
success(response) {
console.log('success:', response);
if (response.errMsg === 'openBusinessView:ok') {
if (response.extraData.status === 'success') {
onConfirm(orderId, true);
}
}
},
fail(error) {
console.log('error:', error);
},
complete(result) {
console.log('result:', result);
},
});
}
// #endif
// 评价
function onComment(id) {
sheep.$router.go('/pages/goods/comment/add', {
id,
});
}
const pickUpVerifyRef = ref();
async function getOrderDetail(id) {
// 对详情数据进行适配
let res;
if (state.comeinType === 'wechat') {
// TODO 芋艿:微信场景下
res = await OrderApi.getOrder(id, {
merchant_trade_no: state.merchantTradeNo,
});
} else {
res = await OrderApi.getOrder(id);
}
if (res.code === 0) {
state.orderInfo = res.data;
handleOrderButtons(state.orderInfo);
// 配送方式:门店自提
if (res.data.pickUpStoreId) {
const { data } = await DeliveryApi.getDeliveryPickUpStore(res.data.pickUpStoreId);
systemStore.value = data || {};
}
if (state.orderInfo.deliveryType === 2 && state.orderInfo.payStatus) {
pickUpVerifyRef.value && pickUpVerifyRef.value.markCode(res.data.pickUpVerifyCode);
}
} else {
sheep.$router.back();
}
}
onLoad(async (options) => {
let id = 0;
if (options.id) {
id = options.id;
}
// TODO 芋艿:下面两个变量,后续接入
state.comeinType = options.comein_type;
if (state.comeinType === 'wechat') {
state.merchantTradeNo = options.merchant_trade_no;
}
await getOrderDetail(id);
});
</script>
<style lang="scss" scoped>
.score-img {
width: 36rpx;
height: 36rpx;
margin: 0 4rpx;
}
.apply-btn {
width: 140rpx;
height: 50rpx;
border-radius: 25rpx;
font-size: 24rpx;
border: 2rpx solid #dcdcdc;
line-height: normal;
margin-left: 16rpx;
}
.state-box {
color: rgba(#fff, 0.9);
width: 100%;
background: v-bind(headerBg) no-repeat,
linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
background-size: 750rpx 100%;
box-sizing: border-box;
.state-img {
width: 60rpx;
height: 60rpx;
margin-right: 20rpx;
}
}
.order-address-box {
background-color: #fff;
border-radius: 10rpx;
margin: -50rpx 20rpx 16rpx 20rpx;
padding: 44rpx 34rpx 42rpx 20rpx;
font-size: 30rpx;
box-sizing: border-box;
font-weight: 500;
color: rgba(51, 51, 51, 1);
.address-username {
margin-right: 20rpx;
}
.address-detail {
font-size: 26rpx;
font-weight: 500;
color: rgba(153, 153, 153, 1);
margin-top: 20rpx;
}
}
.detail-goods {
border-radius: 10rpx;
margin: 0 20rpx 20rpx 20rpx;
.order-list {
margin-bottom: 20rpx;
background-color: #fff;
.order-card {
padding: 20rpx 0;
.order-sku {
font-size: 24rpx;
font-weight: 400;
color: rgba(153, 153, 153, 1);
width: 450rpx;
margin-bottom: 20rpx;
.order-num {
margin-right: 10rpx;
}
}
.tag-btn {
margin-left: 16rpx;
font-size: 24rpx;
height: 36rpx;
color: var(--ui-BG-Main);
border: 2rpx solid var(--ui-BG-Main);
border-radius: 14rpx;
padding: 0 4rpx;
}
}
}
}
// 订单信息。
.notice-box {
background: #fff;
border-radius: 10rpx;
margin: 0 20rpx 20rpx 20rpx;
.notice-box__head {
font-size: 30rpx;
font-weight: 500;
color: rgba(51, 51, 51, 1);
line-height: 80rpx;
border-bottom: 1rpx solid #dfdfdf;
padding: 0 25rpx;
}
.notice-box__content {
padding: 20rpx;
.self-pickup-box {
width: 100%;
.self-pickup--img {
width: 200rpx;
height: 200rpx;
margin: 40rpx 0;
}
}
}
.notice-item,
.notice-item--center {
display: flex;
align-items: center;
line-height: normal;
margin-bottom: 24rpx;
.title {
font-size: 28rpx;
color: #999;
}
.detail {
font-size: 28rpx;
color: #333;
flex: 1;
}
}
}
.copy-btn {
width: 100rpx;
line-height: 50rpx;
border-radius: 25rpx;
padding: 0;
background: rgba(238, 238, 238, 1);
font-size: 22rpx;
font-weight: 400;
color: rgba(51, 51, 51, 1);
}
// 订单价格信息
.order-price-box {
background-color: #fff;
border-radius: 10rpx;
padding: 20rpx;
margin: 0 20rpx 20rpx 20rpx;
.notice-item {
line-height: 70rpx;
.title {
font-size: 28rpx;
color: #999;
}
.detail {
font-size: 28rpx;
color: #333;
font-family: OPPOSANS;
}
}
.all-rpice-item {
justify-content: flex-end;
align-items: center;
.title {
font-size: 26rpx;
font-weight: 500;
color: #333333;
line-height: normal;
}
.all-price {
font-size: 26rpx;
font-family: OPPOSANS;
line-height: normal;
color: $red;
}
}
}
// 底部
.footer-box {
height: 100rpx;
width: 100%;
box-sizing: border-box;
border-radius: 10rpx;
padding-right: 20rpx;
.cancel-btn {
width: 160rpx;
height: 60rpx;
background: #eeeeee;
border-radius: 30rpx;
margin-right: 20rpx;
font-size: 26rpx;
font-weight: 400;
color: #333333;
}
.pay-btn {
width: 160rpx;
height: 60rpx;
font-size: 26rpx;
border-radius: 30rpx;
font-weight: 500;
color: #fff;
}
}
</style>

View File

@@ -0,0 +1,45 @@
<template>
<view class="page-app theme-light main-green font-1">
<layout ref="order"></layout>
<s-menu-tools />
<s-auth-modal />
</view>
</template>
<script>
import layout from '@/pages/order/blind/components/layout.vue';
export default {
components: {
layout,
},
props: {
},
data() {
return {
currentIndex: 0,
}
},
onLoad(options) {
if (options.type) {
this.currentIndex = options.type;
}
this.$nextTick(() => {
this.$refs.order.tabChange(this.currentIndex);
});
},
methods: {
}
}
</script>
<style lang="scss" scoped>
.page-app {
background-color: #fafafa;
padding-bottom: 140rpx;
height: calc(100vh);
padding-bottom: env(safe-area-inset-bottom);
}
</style>

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

@@ -0,0 +1,468 @@
<template>
<s-layout title="确认订单">
<!-- 头部地址选择配送地址自提地址 -->
<AddressSelection v-model="addressState" />
<!-- 商品信息 -->
<view class="order-card-box ss-m-b-14">
<s-goods-item
v-for="item in state.orderInfo.items"
:key="item.skuId"
:img="item.picUrl"
:title="item.spuName"
:skuText="item.properties.map((property) => property.valueName).join(' ')"
:price="item.price"
:num="item.count"
marginBottom="10"
/>
<view class="order-item ss-flex ss-col-center ss-row-between ss-p-x-20 bg-white ss-r-10">
<view class="item-title">订单备注</view>
<view class="ss-flex ss-col-center">
<uni-easyinput
maxlength="20"
placeholder="建议留言前先与商家沟通"
v-model="state.orderPayload.remark"
:inputBorder="false"
:clearable="false"
/>
</view>
</view>
</view>
<!-- 价格信息 -->
<view class="bg-white total-card-box ss-p-20 ss-m-b-14 ss-r-10">
<view class="total-box-content border-bottom">
<view class="order-item ss-flex ss-col-center ss-row-between">
<view class="item-title">商品金额</view>
<view class="ss-flex ss-col-center">
<text class="item-value ss-m-r-24">
{{ fen2yuan(state.orderInfo.price.totalPrice) }}
</text>
</view>
</view>
<view
class="order-item ss-flex ss-col-center ss-row-between"
v-if="state.orderInfo.type === 0"
>
<view class="item-title">积分抵扣</view>
<view class="ss-flex ss-col-center">
{{ state.pointStatus ? '剩余积分' : '当前积分' }}
<image
:src="sheep.$url.static('/static/img/shop/goods/score1.svg')"
class="score-img"
/>
<text class="item-value ss-m-r-24">
{{ state.pointStatus ? state.orderInfo.totalPoint - state.orderInfo.usePoint : (state.orderInfo.totalPoint || 0) }}
</text>
<checkbox-group @change="changeIntegral">
<checkbox :checked='state.pointStatus' :disabled="!state.orderInfo.totalPoint || state.orderInfo.totalPoint <= 0" />
</checkbox-group>
</view>
</view>
<!-- 快递配置时信息的展示 -->
<view class="order-item ss-flex ss-col-center ss-row-between" v-if='addressState.deliveryType === 1'>
<view class="item-title">运费</view>
<view class="ss-flex ss-col-center">
<text class="item-value ss-m-r-24" v-if="state.orderInfo.price.deliveryPrice > 0">
+{{ fen2yuan(state.orderInfo.price.deliveryPrice) }}
</text>
<view class='item-value ss-m-r-24' v-else>免运费</view>
</view>
</view>
<!-- 门店自提时需要填写姓名和手机号 -->
<view class="order-item ss-flex ss-col-center ss-row-between" v-if='addressState.deliveryType === 2'>
<view class="item-title">联系人</view>
<view class="ss-flex ss-col-center">
<uni-easyinput
maxlength="20"
placeholder="请填写您的联系姓名"
v-model="addressState.receiverName"
:inputBorder="false"
:clearable="false"
/>
</view>
</view>
<view class="order-item ss-flex ss-col-center ss-row-between" v-if='addressState.deliveryType === 2'>
<view class="item-title">联系电话</view>
<view class="ss-flex ss-col-center">
<uni-easyinput
maxlength="20"
placeholder="请填写您的联系电话"
v-model="addressState.receiverMobile"
:inputBorder="false"
:clearable="false"
/>
</view>
</view>
<!-- 优惠劵只有 type = 0 普通订单非拼团秒杀砍价才可以使用优惠劵 -->
<view
class="order-item ss-flex ss-col-center ss-row-between"
v-if="state.orderInfo.type === 0"
>
<view class="item-title">优惠券</view>
<view class="ss-flex ss-col-center" @tap="state.showCoupon = true">
<text class="item-value text-red" v-if="state.orderPayload.couponId > 0">
-{{ fen2yuan(state.orderInfo.price.couponPrice) }}
</text>
<text
class="item-value"
:class="state.couponInfo.length > 0 ? 'text-red' : 'text-disabled'"
v-else
>
{{
state.couponInfo.length > 0 ? state.couponInfo.length + ' 张可用' : '暂无可用优惠券'
}}
</text>
<text class="_icon-forward item-icon" />
</view>
</view>
<view
class="order-item ss-flex ss-col-center ss-row-between"
v-if="state.orderInfo.price.discountPrice > 0"
>
<view class="item-title">活动优惠</view>
<view class="ss-flex ss-col-center">
<!-- @tap="state.showDiscount = true" TODO puhui999折扣后续要把优惠信息打进去 -->
<text class="item-value text-red">
-{{ fen2yuan(state.orderInfo.price.discountPrice) }}
</text>
<text class="_icon-forward item-icon" />
</view>
</view>
<view
class="order-item ss-flex ss-col-center ss-row-between"
v-if="state.orderInfo.price.vipPrice > 0"
>
<view class="item-title">会员优惠</view>
<view class="ss-flex ss-col-center">
<text class="item-value text-red">
-{{ fen2yuan(state.orderInfo.price.vipPrice) }}
</text>
</view>
</view>
</view>
<view class="total-box-footer ss-font-28 ss-flex ss-row-right ss-col-center ss-m-r-28">
<view class="total-num ss-m-r-20">
{{ state.orderInfo.items.reduce((acc, item) => acc + item.count, 0) }}
</view>
<view>合计</view>
<view class="total-num text-red"> {{ fen2yuan(state.orderInfo.price.payPrice) }}</view>
</view>
</view>
<!-- 选择优惠券弹框 -->
<s-coupon-select
v-model="state.couponInfo"
:show="state.showCoupon"
@confirm="onSelectCoupon"
@close="state.showCoupon = false"
/>
<!-- 满额折扣弹框 TODO @puhui999折扣后续要把优惠信息打进去 -->
<s-discount-list
v-model="state.orderInfo"
:show="state.showDiscount"
@close="state.showDiscount = false"
/>
<!-- 底部 -->
<su-fixed bottom :opacity="false" bg="bg-white" placeholder :noFixed="false" :index="200">
<view class="footer-box border-top ss-flex ss-row-between ss-p-x-20 ss-col-center">
<view class="total-box-footer ss-flex ss-col-center">
<view class="total-num ss-font-30 text-red">
{{ fen2yuan(state.orderInfo.price.payPrice) }}
</view>
</view>
<button
class="ss-reset-button ui-BG-Main-Gradient ss-r-40 submit-btn ui-Shadow-Main"
@tap="onConfirm"
>
提交订单
</button>
</view>
</su-fixed>
</s-layout>
</template>
<script setup>
import { reactive, ref, watch } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import AddressSelection from '@/pages/order/addressSelection.vue';
import sheep from '@/sheep';
import OrderApi from '@/sheep/api/trade/order';
import CouponApi from '@/sheep/api/promotion/coupon';
import { fen2yuan } from '@/sheep/hooks/useGoods';
const state = reactive({
orderPayload: {},
orderInfo: {
items: [], // 商品项列表
price: {}, // 价格信息
},
showCoupon: false, // 是否展示优惠劵
couponInfo: [], // 优惠劵列表
showDiscount: false, // 是否展示营销活动
// ========== 积分 ==========
pointStatus: false, //是否使用积分
});
const addressState = ref({
addressInfo: {}, // 选择的收货地址
deliveryType: 1, // 收货方式1-快递配送2-门店自提
isPickUp: true, // 门店自提是否开启 TODO puhui999: 默认开启,看看后端有开关的话接入
pickUpInfo: {}, // 选择的自提门店信息
receiverName: '', // 收件人名称
receiverMobile: '', // 收件人手机
});
// ========== 积分 ==========
/**
* 使用积分抵扣
*/
const changeIntegral = async () => {
state.pointStatus = !state.pointStatus;
await getOrderInfo();
};
// 选择优惠券
async function onSelectCoupon(couponId) {
state.orderPayload.couponId = couponId;
await getOrderInfo();
state.showCoupon = false;
}
// 提交订单
function onConfirm() {
if (addressState.value.deliveryType === 1 && !addressState.value.addressInfo.id) {
sheep.$helper.toast('请选择收货地址');
return;
}
if (addressState.value.deliveryType === 2) {
if (!addressState.value.pickUpInfo.id) {
sheep.$helper.toast('请选择自提门店地址');
return;
}
if (addressState.value.receiverName === '' || addressState.value.receiverMobile === '') {
sheep.$helper.toast('请填写联系人或联系人电话');
return;
}
if (!/^[\u4e00-\u9fa5\w]{2,16}$/.test(addressState.value.receiverName)) {
sheep.$helper.toast('请填写您的真实姓名');
return;
}
if (!/^1(3|4|5|7|8|9|6)\d{9}$/.test(addressState.value.receiverMobile)) {
sheep.$helper.toast('请填写正确的手机号');
return;
}
}
submitOrder();
}
// 创建订单&跳转
async function submitOrder() {
const { code, data } = await OrderApi.createOrder({
items: state.orderPayload.items,
couponId: state.orderPayload.couponId,
remark: state.orderPayload.remark,
deliveryType: addressState.value.deliveryType,
addressId: addressState.value.addressInfo.id, // 收件地址编号
pickUpStoreId: addressState.value.pickUpInfo.id,//自提门店编号
receiverName: addressState.value.receiverName,// 选择门店自提时,该字段为联系人名
receiverMobile: addressState.value.receiverMobile,// 选择门店自提时,该字段为联系人手机
pointStatus: state.pointStatus,
combinationActivityId: state.orderPayload.combinationActivityId,
combinationHeadId: state.orderPayload.combinationHeadId,
seckillActivityId: state.orderPayload.seckillActivityId,
});
if (code !== 0) {
return;
}
// 更新购物车列表,如果来自购物车
if (state.orderPayload.items[0].cartId > 0) {
sheep.$store('cart').getList();
}
// 跳转到支付页面
sheep.$router.redirect('/pages/pay/index', {
id: data.payOrderId,
});
}
// 检查库存 & 计算订单价格
async function getOrderInfo() {
// 计算价格
const { data, code } = await OrderApi.settlementOrder({
items: state.orderPayload.items,
couponId: state.orderPayload.couponId,
deliveryType: addressState.value.deliveryType,
addressId: addressState.value.addressInfo.id, // 收件地址编号
pickUpStoreId: addressState.value.pickUpInfo.id,//自提门店编号
receiverName: addressState.value.receiverName,// 选择门店自提时,该字段为联系人名
receiverMobile: addressState.value.receiverMobile,// 选择门店自提时,该字段为联系人手机
pointStatus: state.pointStatus,
combinationActivityId: state.orderPayload.combinationActivityId,
combinationHeadId: state.orderPayload.combinationHeadId,
seckillActivityId: state.orderPayload.seckillActivityId,
});
if (code !== 0) {
return;
}
state.orderInfo = data;
// 设置收货地址
if (state.orderInfo.address) {
addressState.value.addressInfo = state.orderInfo.address;
}
}
// 获取可用优惠券
async function getCoupons() {
const { code, data } = await CouponApi.getMatchCouponList(
state.orderInfo.price.payPrice,
state.orderInfo.items.map((item) => item.spuId),
state.orderPayload.items.map((item) => item.skuId),
state.orderPayload.items.map((item) => item.categoryId),
);
if (code === 0) {
state.couponInfo = data;
}
}
onLoad(async (options) => {
if (!options.data) {
sheep.$helper.toast('参数不正确,请检查!');
return;
}
state.orderPayload = JSON.parse(options.data);
await getOrderInfo();
await getCoupons();
});
// 使用 watch 监听地址和配送方式的变化
watch(addressState, async (newAddress, oldAddress) => {
// 如果收货地址或配送方式有变化,则重新计算价格
if (newAddress.addressInfo.id !== oldAddress.addressInfo.id || newAddress.deliveryType !== oldAddress.deliveryType) {
await getOrderInfo();
}
});
</script>
<style lang="scss" scoped>
:deep() {
.uni-input-wrapper {
width: 320rpx;
}
.uni-easyinput__content-input {
font-size: 28rpx;
height: 72rpx;
text-align: right !important;
padding-right: 0 !important;
.uni-input-input {
font-weight: 500;
color: #333333;
font-size: 26rpx;
height: 32rpx;
margin-top: 4rpx;
}
}
.uni-easyinput__content {
display: flex !important;
align-items: center !important;
justify-content: right !important;
}
}
.score-img {
width: 36rpx;
height: 36rpx;
margin: 0 4rpx;
}
.order-item {
height: 80rpx;
.item-title {
font-size: 28rpx;
font-weight: 400;
}
.item-value {
font-size: 28rpx;
font-weight: 500;
font-family: OPPOSANS;
}
.text-disabled {
color: #bbbbbb;
}
.item-icon {
color: $dark-9;
}
.remark-input {
text-align: right;
}
.item-placeholder {
color: $dark-9;
font-size: 26rpx;
text-align: right;
}
}
.total-box-footer {
height: 90rpx;
.total-num {
color: #333333;
font-family: OPPOSANS;
}
}
.footer-box {
height: 100rpx;
.submit-btn {
width: 240rpx;
height: 70rpx;
font-size: 28rpx;
font-weight: 500;
.goto-pay-text {
line-height: 28rpx;
}
}
.cancel-btn {
width: 240rpx;
height: 80rpx;
font-size: 26rpx;
background-color: #e5e5e5;
color: $dark-9;
}
}
.title {
font-size: 36rpx;
font-weight: bold;
color: #333333;
}
.subtitle {
font-size: 28rpx;
color: #999999;
}
.cicon-checkbox {
font-size: 36rpx;
color: var(--ui-BG-Main);
}
.cicon-box {
font-size: 36rpx;
color: #999999;
}
</style>

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

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

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

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

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

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

View File

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

View File

@@ -0,0 +1,184 @@
<!-- 售后列表 -->
<template>
<s-layout title="退款列表">
<s-empty v-if="state.pagination.total === 0" icon="/static/data-empty.png" text="暂无数据" />
<!-- 列表 -->
<view v-if="state.pagination.total > 0">
<view
class="list-box ss-m-y-20"
v-for="order in state.pagination.list"
:key="order.id"
@tap="sheep.$router.go('/pages/clerk/detail/index', { id: order.spuId })"
>
<view class="order-head ss-flex ss-col-center ss-row-between">
<text class="no">服务单号{{ order.no }}</text>
<text class="state">{{ formatAfterSaleStatus(order) }}</text>
</view>
<s-goods-item
:img="order.picUrl"
:title="order.spuName"
:skuText="order.properties.map((property) => property.valueName).join(' ')"
:price="order.refundPrice"
/>
</view>
</view>
<uni-load-more
v-if="state.pagination.total > 0"
:status="state.loadStatus"
:content-text="{
contentdown: '上拉加载更多',
}"
@tap="loadMore"
/>
</s-layout>
</template>
<script setup>
import sheep from '@/sheep';
import { onLoad, onReachBottom } from '@dcloudio/uni-app';
import { reactive } from 'vue';
import _ from 'lodash-es';
import {
formatAfterSaleStatus,
formatAfterSaleStatusDescription,
handleAfterSaleButtons,
} from '@/sheep/hooks/useGoods';
import AfterSaleApi from '@/sheep/api/trade/afterSale';
import { resetPagination } from '@/sheep/util';
const state = reactive({
currentTab: 0,
showApply: false,
pagination: {
list: [],
total: 0,
pageNo: 1,
pageSize: 10,
},
loadStatus: '',
});
// TODO 芋艿:优化点,增加筛选
const tabMaps = [
{
name: '全部',
value: 'all',
},
// {
// name: '申请中',
// value: 'nooper',
// },
// {
// name: '处理中',
// value: 'ing',
// },
// {
// name: '已完成',
// value: 'completed',
// },
// {
// name: '已拒绝',
// value: 'refuse',
// },
];
// 切换选项卡
function onTabsChange(e) {
resetPagination(state.pagination);
state.currentTab = e.index;
getOrderList();
}
// 获取售后列表
async function getOrderList() {
state.loadStatus = 'loading';
let { data, code } = await AfterSaleApi.getAfterSalePage({
// type: tabMaps[state.currentTab].value,
pageNo: state.pagination.pageNo,
pageSize: state.pagination.pageSize,
});
if (code !== 0) {
return;
}
data.list.forEach((order) => handleAfterSaleButtons(order));
state.pagination.list = _.concat(state.pagination.list, data.list);
state.pagination.total = data.total;
state.loadStatus = state.pagination.list.length < state.pagination.total ? 'more' : 'noMore';
}
function onApply(orderId) {
uni.showModal({
title: '提示',
content: '确定要取消此申请吗?',
success: async function (res) {
if (!res.confirm) {
return;
}
const { code } = await AfterSaleApi.cancelAfterSale(orderId);
if (code === 0) {
resetPagination(state.pagination);
await getOrderList();
}
},
});
}
onLoad(async (options) => {
if (options.type) {
state.currentTab = options.type;
}
await getOrderList();
});
// 加载更多
function loadMore() {
if (state.loadStatus === 'noMore') {
return;
}
state.pagination.pageNo++;
getOrderList();
}
// 上拉加载更多
onReachBottom(() => {
loadMore();
});
</script>
<style lang="scss" scoped>
.list-box {
background-color: #fff;
.order-head {
padding: 0 25rpx;
height: 77rpx;
}
.apply-box {
height: 82rpx;
.title {
font-size: 24rpx;
}
.value {
font-size: 22rpx;
color: $dark-6;
}
}
.tool-btn-box {
height: 100rpx;
.tool-btn {
width: 160rpx;
height: 60rpx;
background: #f6f6f6;
border-radius: 30rpx;
font-size: 26rpx;
font-weight: 400;
}
}
}
</style>

View File

@@ -0,0 +1,130 @@
<!-- 虚拟列表演示(不使用内置列表)(vue) -->
<!-- 写法较简单在页面中对当前需要渲染的虚拟列表数据进行for循环在vue3中兼容性良好 -->
<!-- 在各平台兼容性请查阅https://z-paging.zxlee.cn/module/virtual-list.html -->
<template>
<view class="content">
<!-- 如果页面中的cell高度是固定不变的则不需要设置cell-height-mode如果页面中高度是动态改变的则设置cell-height-mode="dynamic" -->
<!-- 原先的v-model修改为@virtualListChange="virtualListChange"并赋值处理后的虚拟列表 -->
<z-paging ref="paging" :auto="false" use-virtual-list :force-close-inner-list="true" :paging-style="{ paddingTop: 0 + 'px', paddingBottom: paddingBottom + 'rpx' }" cell-height-mode="dynamic" @scroll="scroll" @virtualListChange="virtualListChange" @query="queryList">
<!-- 需要固定在顶部不滚动的view放在slot="top"的view中如果需要跟着滚动则不要设置slot="top" -->
<template #top>
<su-navbar :title="title" statusBar></su-navbar>
<tab-box :tabList="tabList" :currentValue="currentIndex" @change="tabChange"></tab-box>
</template>
<!-- :id="`zp-id-${item.zp_index}`":key="item.zp_index" 必须写必须写 -->
<!-- 这里for循环的index不是数组中真实的index了请使用item.zp_index获取真实的index -->
<order-list @onCancel="onCancel" :virtualList="virtualList"></order-list>
</z-paging>
</view>
</template>
<script>
import TabBox from '@/pages/order/my/components/tabBox.vue';
import OrderList from '@/pages/order/my/components/orderList.vue';
import OrderApi from '@/sheep/api/trade/order';
import {
formatMyOrderStatus,
handleMyOrderButtons,
fen2yuan,
} from '@/sheep/hooks/useGoods';
export default {
components: {
TabBox,
OrderList,
},
props: {
title: {
type: String,
default: '我的订单',
},
},
data() {
return {
// 虚拟列表数组,通过@virtualListChange监听获得最新数组
virtualList: [],
scrollTop: 0,
paddingTop: 0,
paddingBottom: 0,
height: 0,
tabList: [{
name: '全部',
}, {
name: '待付款',
value: 0,
}, {
name: '待服务',
value: 10,
}, {
name: '服务中',
value: 20,
}, {
name: '待评价',
value: 30,
}],
currentIndex: 0,
}
},
methods: {
scroll(e) {
this.scrollTop = e.detail.scrollTop;
},
// 监听虚拟列表数组改变并赋值给virtualList进行重新渲染
virtualListChange(vList) {
this.virtualList = vList;
},
queryList(pageNo, pageSize) {
// 组件加载时会自动触发此方法,因此默认页面加载时会自动触发,无需手动调用
// 这里的pageNo和pageSize会自动计算好直接传给服务器即可
// 模拟请求服务器获取分页数据,请替换成自己的网络请求
const params = {
pageNo: pageNo,
pageSize: pageSize,
status: this.tabList[this.currentIndex].value,
commentStatus: this.tabList[this.currentIndex].value === 30 ? false : null,
}
OrderApi.getOrderPage(params).then(res => {
// 将请求的结果数组传递给z-paging
res.data.list.forEach((order) => handleMyOrderButtons(order));
res.data.list.forEach((order) => formatMyOrderStatus(order));
res.data.list.forEach((order) => order.payPrice = fen2yuan(order.payPrice));
this.$refs.paging.complete(res.data.list);
}).catch(res => {
// 如果请求失败写this.$refs.paging.complete(false);
// 注意每次都需要在catch中写这句话很麻烦z-paging提供了方案可以全局统一处理
// 在底层的网络请求抛出异常时写uni.$emit('z-paging-error-emit');即可
this.$refs.paging.complete(false);
})
},
tabChange(e) {
this.currentIndex = e;
this.$refs.paging.reload();
},
// 取消订单
async onCancel(orderId) {
var that = this;
uni.showModal({
title: '提示',
content: '确定要取消订单吗?',
success: async function (res) {
if (!res.confirm) {
return;
}
const { code } = await OrderApi.cancelOrder(orderId);
if (code === 0) {
that.$refs.paging.reload();
}
},
});
}
}
}
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,219 @@
<template>
<view @click="onOrderDetail(order.id)" :id="`zp-id-${order.zp_index}`" :key="order.zp_index" v-for="(order,index) in orderList" class="order-card">
<view class="no-box">
<view class="order-no">
<u-icon name="order"></u-icon>
<text class="number">{{ order.no }}</text>
</view>
<view class="status">{{order.statusStr}}</view>
</view>
<view class="main-box" v-for="item in order.items" :key="item.id">
<view @tap.stop="onBuy(item.spuId)">
<u-avatar size="100" :src="item.avatar"></u-avatar>
</view>
<view class="right-box">
<view class="nickname-box">
<view class="nickname" v-if="item.nickname">{{item.nickname}}</view>
<view class="nickname" v-else>待接单..</view>
<view class="info">订单时间{{order.createTimeStr}}</view>
<view class="info">服务内容{{item.spuName}} {{item.properties.map((property) => property.valueName).join(' ')}}×{{item.count}}</view>
<view class="info">开始服务时间{{order.deliveryTimeStr}}</view>
<view class="info">
<text>剩余时间</text>
<view v-if="order.receiveTime">
<text v-if="order.end">已结束</text>
<view v-else>
<u-count-down :timestamp="order.timestamp" format="DD天HH时mm分ss秒" @end="order.end = true"></u-count-down>
</view>
</view>
<text v-else>暂未接单</text>
</view>
<view class="info" v-if="order.status == 40">取消原因{{order.cancelReason}}</view>
</view>
<view class="price-box">
<text class="price">{{order.payPrice}}</text>
<text>/</text>
</view>
</view>
</view>
<view class="bottom-box">
<view class="btn-box">
<view v-if="order.buttons.includes('detail')" @tap.stop="onOrderDetail(order.id)" class="btn">查看详情</view>
<view v-if="order.buttons.includes('unpay')" @tap.stop="onOrderDetail(order.id)" class="btn">取消订单</view>
<view v-if="order.buttons.includes('cancel')" @tap.stop="onCancel(order.id)" class="btn">取消订单</view>
<view v-if="order.buttons.includes('buy') && order.items[0].spuId" @tap.stop="onBuy(order.items[0].spuId)" class="btn active">再来一单</view>
<view v-if="order.buttons.includes('comment')" @tap.stop="onComment(order.id)" class="btn active">评价</view>
<view v-if="order.buttons.includes('pay')" @tap.stop="onPay(order.payOrderId)" class="btn active">继续支付</view>
</view>
</view>
</view>
</template>
<script>
import sheep from '@/sheep';
import dayjs from 'dayjs';
export default {
components: {
},
props: {
virtualList: {
type: Array,
default: [],
},
},
emits: ["onCancel"],
data() {
return {
}
},
computed: {
orderList() {
this.virtualList.forEach((order) => order.createTimeStr = sheep.$helper.timeFormat(order.createTime, 'yyyy-mm-dd hh:MM:ss'));
this.virtualList.forEach((order) => order.deliveryTimeStr = order.deliveryTime ? sheep.$helper.timeFormat(order.deliveryTime, 'yyyy-mm-dd hh:MM:ss') : '');
this.virtualList.forEach((order) => order.timestamp = order.receiveTime ? sheep.$helper.parseTimeData(order.receiveTime) : 0);
return this.virtualList;
},
},
methods: {
// 订单详情
onOrderDetail(id) {
sheep.$router.go('/pages/order/my/detail', {
id,
});
},
onCancel(orderId) {
this.$emit('onCancel', orderId);
},
onBuy(id) {
if(id){
sheep.$router.go('/pages/clerk/detail/index', {
id,
});
}
},
// 评价
onComment(id) {
sheep.$router.go('/pages/goods/comment/worker/add', {
id,
});
},
// 继续支付
onPay(payOrderId) {
sheep.$router.go('/pages/pay/worker/index', {
id: payOrderId,
});
}
}
}
</script>
<style lang="scss" scoped>
.order-card {
padding: 10px;
margin-top: 10px;
background-color: #fff;
display: flex;
flex-direction: column;
flex: 1;
.no-box {
display: flex;
align-items: center;
justify-content: space-between;
flex: 1;
padding-bottom: 10px;
color: rgb(100, 101, 102);
.order-no {
.number {
margin-left: 5px;
font-size: 22rpx;
}
}
.status {
font-size: 22rpx;
}
}
.main-box {
display: flex;
padding: 10px 0;
border-top: 1px solid #fbf0f0;
border-bottom: 1px solid #fbf0f0;
margin-bottom: 10px;
.right-box {
display: flex;
justify-content: space-between;
flex: 1;
align-items: center;
margin-left: 10px;
.nickname {
font-size: 12px;
font-weight: bold;
margin-bottom: 10px;
}
.info {
font-size: 22rpx;
color: rgb(100, 101, 102);
line-height: 22rpx;
margin-bottom: 5px;
display: flex;
align-items: center;
.weixin {
margin-right: 5px;
}
}
.price-box {
font-size: 22rpx;
color: rgb(100, 101, 102);
.price {
font-size: 34rpx;
font-weight: bold;
color: var(--ui-BG-Main);
}
}
}
}
.bottom-box {
display: flex;
justify-content: flex-end;
.btn-box {
display: flex;
align-items: center;
}
.btn {
background-color: #ddd;
color: rgb(100, 101, 102);
font-size: 22rpx;
border-radius: 40px;
padding: 5px 10px;
margin-left: 10px;
min-width: 140rpx;
display: flex;
justify-content: center;
}
.active {
background-color: var(--ui-BG-Main);
color: #fff;
}
}
}
</style>

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