feat: 初始化项目配置和样式- 添加项目环境变量配置文件 (.env)

- 创建代码忽略文件列表 (.gitignore)
- 设置代码格式化规则 (.prettierrc)- 添加各种样式文件,包括背景、边框、按钮、卡片等组件样式
- 创建暗黑主题样式文件
- 添加表单和代码样式文件
This commit is contained in:
liguigong
2025-09-12 09:53:45 +08:00
commit b91b273686
582 changed files with 90552 additions and 0 deletions
+539
View File
@@ -0,0 +1,539 @@
<!-- 拼团订单的详情 -->
<template>
<s-layout
title="拼团详情"
class="detail-wrap"
:navbar="state.data && !state.loading ? 'inner' : 'normal'"
:onShareAppMessage="shareInfo"
>
<view v-if="state.loading"></view>
<view v-if="state.data && !state.loading">
<!-- 团长信息 + 活动信息 -->
<view
class="recharge-box"
v-if="state.data.headRecord"
:style="[
{
marginTop: '-' + Number(statusBarHeight + 88) + 'rpx',
paddingTop: Number(statusBarHeight + 108) + 'rpx',
},
]"
>
<s-goods-item
class="goods-box"
:img="state.data.headRecord.picUrl"
:title="state.data.headRecord.spuName"
:price="state.data.headRecord.combinationPrice"
priceColor="#E1212B"
@tap="
sheep.$router.go('/pages/goods/groupon', {
id: state.data.headRecord.activityId,
})
"
:style="[{ top: Number(statusBarHeight + 108) + 'rpx' }]"
>
<template #groupon>
<view class="ss-flex">
<view class="sales-title">{{ state.data.headRecord.userSize }}人团</view>
<view class="num-title ss-m-l-20">已拼{{ state.data.headRecord.userCount }}</view>
</view>
</template>
</s-goods-item>
</view>
<view class="countdown-box detail-card ss-p-t-44 ss-flex-col ss-col-center">
<!-- 情况一拼团成功 -->
<view v-if="state.data.headRecord.status === 1">
<view v-if="state.data.orderId">
<view class="countdown-title ss-flex">
<text class="cicon-check-round" />
恭喜您~拼团成功
</view>
</view>
<view v-else>
<view class="countdown-title ss-flex">
<text class="cicon-info" />
抱歉~该团已满员
</view>
</view>
</view>
<!-- 情况二拼团失败 -->
<view v-if="state.data.headRecord.status === 2">
<view class="countdown-title ss-flex">
<text class="cicon-info"></text>
{{ state.data.orderId ? '拼团超时,已自动退款' : '该团已解散' }}
</view>
</view>
<!-- 情况三拼团进行中 -->
<view v-if="state.data.headRecord.status === 0">
<view v-if="state.data.headRecord.expireTime <= new Date().getTime()">
<view class="countdown-title ss-flex">
<text class="cicon-info"></text>
拼团已结束,请关注下次活动
</view>
</view>
<view class="countdown-title ss-flex" v-else>
还差
<view class="num"
>{{ state.data.headRecord.userSize - state.data.headRecord.userCount }}</view
>
拼团成功
<view class="ss-flex countdown-time">
<view class="countdown-h ss-flex ss-row-center">{{ endTime.h }}</view>
<view class="ss-m-x-4">:</view>
<view class="countdown-num ss-flex ss-row-center">
{{ endTime.m }}
</view>
<view class="ss-m-x-4">:</view>
<view class="countdown-num ss-flex ss-row-center">
{{ endTime.s }}
</view>
</view>
</view>
</view>
<!-- 拼团的记录列表展示每个参团人 -->
<view class="ss-m-t-60 ss-flex ss-flex-wrap ss-row-center">
<!-- 团长 -->
<view class="header-avatar ss-m-r-24 ss-m-b-20">
<image :src="sheep.$url.cdn(state.data.headRecord.avatar) || sheep.$url.static('/static/img/shop/default_avatar.png')" class="avatar-img"></image>
<view class="header-tag ss-flex ss-col-center ss-row-center">团长</view>
</view>
<!-- 团员 -->
<view
class="header-avatar ss-m-r-24 ss-m-b-20"
v-for="item in state.data.memberRecords"
:key="item.id"
>
<image :src="sheep.$url.cdn(item.avatar) || sheep.$url.static('/static/img/shop/default_avatar.png')" class="avatar-img"></image>
<view
class="header-tag ss-flex ss-col-center ss-row-center"
v-if="item.is_leader == '1'"
>
团长
</view>
</view>
<!-- 还有几个坑位 -->
<view
class="default-avatar ss-m-r-24 ss-m-b-20"
v-for="item in state.remainNumber"
:key="item"
>
<image
:src="sheep.$url.static('/static/img/shop/avatar/unknown.png')"
class="avatar-img"
></image>
</view>
</view>
</view>
<!-- 情况一拼团成功情况二拼团失败 -->
<view
v-if="state.data.headRecord.status === 1 || state.data.headRecord.status === 2"
class="ss-m-t-40 ss-flex ss-row-center"
>
<button
class="ss-reset-button order-btn"
v-if="state.data.orderId"
@tap="onDetail(state.data.orderId)"
>
查看订单
</button>
<button class="ss-reset-button join-btn" v-else @tap="onCreateGroupon"> 我要开团 </button>
</view>
<!-- 情况三拼团进行中查看订单或参加或邀请好友或参加 -->
<view v-if="state.data.headRecord.status === 0" class="ss-m-t-40 ss-flex ss-row-center">
<view v-if="state.data.headRecord.expireTime <= new Date().getTime()">
<button
class="ss-reset-button join-btn"
v-if="state.data.orderId"
@tap="onDetail(state.data.orderId)"
>
查看订单
</button>
<button
class="ss-reset-button disabled-btn"
v-else
disabled
@tap="onDetail(state.data.orderId)"
>
去参团
</button>
</view>
<view v-else class="ss-flex ss-row-center">
<view v-if="state.data.orderId">
<button class="ss-reset-button join-btn" :disabled="endTime.ms <= 0" @tap="onShare">
邀请好友来拼团
</button>
</view>
<view v-else>
<button
class="ss-reset-button join-btn"
:disabled="endTime.ms <= 0"
@tap="onJoinGroupon()"
>
立即参团
</button>
</view>
</view>
</view>
<view v-if="!isEmpty(state.goodsInfo)">
<!-- 规格与数量弹框 -->
<s-select-groupon-sku
:show="state.showSelectSku"
:goodsInfo="state.goodsInfo"
:grouponAction="state.grouponAction"
:grouponNum="state.grouponNum"
@buy="onBuy"
@change="onSkuChange"
@close="state.showSelectSku = false"
/>
</view>
</view>
<s-empty v-if="!state.data && !state.loading" icon="/static/goods-empty.png" />
</s-layout>
</template>
<script setup>
import { computed, reactive } from 'vue';
import sheep from '@/sheep';
import { onLoad } from '@dcloudio/uni-app';
import { fen2yuan, useDurationTime } from '@/sheep/hooks/useGoods';
import { showShareModal } from '@/sheep/hooks/useModal';
import { isEmpty } from 'lodash-es';
import CombinationApi from '@/sheep/api/promotion/combination';
import SpuApi from '@/sheep/api/product/spu';
import { SharePageEnum } from '@/sheep/helper/const';
const headerBg = sheep.$url.css('/static/img/shop/user/withdraw_bg.png');
const statusBarHeight = sheep.$platform.device.statusBarHeight * 2;
const state = reactive({
data: {}, // 拼团详情
goodsId: 0, // 商品ID
goodsInfo: {}, // 商品信息
showSelectSku: false, // 显示规格弹框
selectedSkuPrice: {}, // 选中的规格价格
activity: {}, // 团购活动
grouponId: 0, // 团购ID
grouponNum: 0, // 团购人数
grouponAction: 'create', // 团购操作
combinationHeadId: null, // 拼团团长编号
loading: true,
});
const shareInfo = computed(() => {
if (isEmpty(state.data)) return {};
return sheep.$platform.share.getShareInfo(
{
title: state.data.headRecord.spuName,
image: sheep.$url.cdn(state.data.headRecord.picUrl),
desc: state.data.goods?.subtitle,
params: {
page: SharePageEnum.GROUPON_DETAIL.value,
query: state.data.headRecord.id,
},
},
{
type: 'groupon', // 邀请拼团海报
title: state.data.headRecord.spuName, // 商品标题
image: sheep.$url.cdn(state.data.headRecord.picUrl), // 商品主图
price: fen2yuan(state.data.headRecord.combinationPrice), // 商品价格
grouponNum: state.data.headRecord.userSize, // 拼团人数
},
);
});
// 订单详情
function onDetail(orderId) {
sheep.$router.go('/pages/order/detail', {
id: orderId,
});
}
// 去开团
function onCreateGroupon() {
state.grouponAction = 'create';
state.grouponId = 0;
state.showSelectSku = true;
}
// 规格变更
function onSkuChange(e) {
state.selectedSkuPrice = e;
}
// 立即参团
function onJoinGroupon() {
state.grouponAction = 'join';
state.grouponId = state.data.headRecord.activityId;
state.combinationHeadId = state.data.headRecord.id;
state.grouponNum = state.data.headRecord.userSize;
state.showSelectSku = true;
}
// 立即购买
function onBuy(sku) {
sheep.$router.go('/pages/order/confirm', {
data: JSON.stringify({
order_type: 'goods',
combinationActivityId: state.activity.id,
combinationHeadId: state.combinationHeadId,
items: [
{
skuId: sku.id,
count: sku.count,
},
],
}),
});
}
const endTime = computed(() => {
return useDurationTime(state.data.headRecord.expireTime);
});
// 获取拼团团队详情
async function getGrouponDetail(id) {
const { code, data } = await CombinationApi.getCombinationRecordDetail(id);
if (code === 0) {
state.data = data;
const remainNumber = Number(state.data.headRecord.userSize - state.data.headRecord.userCount);
state.remainNumber = remainNumber > 0 ? remainNumber : 0;
// 获取活动信息
const { data: activity } = await CombinationApi.getCombinationActivity(
data.headRecord.activityId,
);
state.activity = activity;
state.grouponNum = activity.userSize;
// 加载商品信息
const { data: spu } = await SpuApi.getSpuDetail(activity.spuId);
state.goodsId = spu.id;
// 默认显示最低价
activity.products.forEach((product) => {
spu.price = Math.min(spu.price, product.combinationPrice); // 设置 SPU 的最低价格
});
state.goodsInfo = spu;
// 价格、库存使用活动的
spu.skus.forEach((sku) => {
const product = activity.products.find((product) => product.skuId === sku.id);
if (product) {
sku.price = product.combinationPrice;
} else {
// 找不到可能是没配置,则不能发起秒杀
sku.stock = 0;
}
});
} else {
state.data = null;
}
state.loading = false;
}
function onShare() {
showShareModal();
}
onLoad((options) => {
getGrouponDetail(options.id);
});
</script>
<style lang="scss" scoped>
.recharge-box {
position: relative;
margin-bottom: 120rpx;
background: v-bind(headerBg) center/750rpx 100% no-repeat,
linear-gradient(115deg, #f44739 0%, #ff6600 100%);
border-radius: 0 0 5% 5%;
height: 100rpx;
.goods-box {
width: 710rpx;
border-radius: 20rpx;
position: absolute;
left: 20rpx;
box-sizing: border-box;
}
.sales-title {
height: 32rpx;
background: rgba(#ffe0e2, 0.29);
border-radius: 16rpx;
font-size: 24rpx;
font-weight: 400;
padding: 6rpx 20rpx;
color: #f7979c;
}
.num-title {
font-size: 24rpx;
font-weight: 400;
color: #999999;
}
}
.countdown-time {
font-size: 26rpx;
font-weight: 500;
color: #383a46;
.countdown-h {
font-size: 24rpx;
font-family: OPPOSANS;
font-weight: 500;
color: #ffffff;
padding: 0 4rpx;
margin-left: 16rpx;
height: 40rpx;
background: linear-gradient(90deg, #ff6000 0%, #fe832a 100%);
border-radius: 6rpx;
}
.countdown-num {
font-size: 24rpx;
font-family: OPPOSANS;
font-weight: 500;
color: #ffffff;
width: 40rpx;
height: 40rpx;
background: linear-gradient(90deg, #ff6000 0%, #fe832a 100%);
border-radius: 6rpx;
}
}
.countdown-box {
// height: 364rpx;
background: #ffffff;
border-radius: 10rpx;
box-sizing: border-box;
.countdown-title {
font-size: 28rpx;
font-weight: 500;
color: #333333;
.cicon-check-round {
color: #42b111;
margin-right: 24rpx;
}
.cicon-info {
color: #d71e08;
margin-right: 24rpx;
}
.num {
color: #ff6000;
}
}
.header-avatar {
width: 86rpx;
height: 86rpx;
background: #ececec;
border-radius: 50%;
border: 4rpx solid #edc36c;
position: relative;
box-sizing: border-box;
.avatar-img {
width: 100%;
height: 100%;
border-radius: 50%;
}
.header-tag {
width: 72rpx;
height: 36rpx;
font-size: 24rpx;
line-height: nor;
background: linear-gradient(132deg, #f3dfb1, #f3dfb1, #ecbe60);
border-radius: 16rpx;
position: absolute;
left: 4rpx;
top: -36rpx;
}
}
.default-avatar {
width: 86rpx;
height: 86rpx;
background: #ececec;
border-radius: 50%;
.avatar-img {
width: 100%;
height: 100%;
border-radius: 50%;
}
}
.user-avatar {
width: 86rpx;
height: 86rpx;
background: #ececec;
border-radius: 50%;
}
}
.order-btn {
width: 668rpx;
height: 70rpx;
border: 2rpx solid #dfdfdf;
border-radius: 35rpx;
color: #999999;
font-weight: 500;
font-size: 26rpx;
line-height: normal;
}
.disabled-btn {
width: 668rpx;
height: 70rpx;
background: #dddddd;
border-radius: 35rpx;
color: #999999;
font-weight: 500;
font-size: 28rpx;
line-height: normal;
}
.join-btn {
width: 668rpx;
height: 70rpx;
background: linear-gradient(90deg, #ff6000 0%, #fe832a 100%);
box-shadow: 0px 8rpx 6rpx 0px rgba(255, 104, 4, 0.22);
border-radius: 35rpx;
color: #fff;
font-weight: 500;
font-size: 28rpx;
line-height: normal;
}
.detail-cell-wrap {
width: 100%;
padding: 10rpx 20rpx;
box-sizing: border-box;
border-top: 2rpx solid #dfdfdf;
background-color: #fff;
// min-height: 60rpx;
.label-text {
font-size: 28rpx;
font-weight: 400;
}
.cell-content {
font-size: 28rpx;
font-weight: 500;
color: $dark-6;
}
.right-forwrad-icon {
font-size: 28rpx;
font-weight: 500;
color: $dark-9;
}
}
</style>
+225
View File
@@ -0,0 +1,225 @@
<!-- 拼团活动列表 -->
<template>
<s-layout :bgStyle="{ color: '#FE832A' }" navbar="inner">
<view class="page-bg" :style="[{ marginTop: '-' + Number(statusBarHeight + 88) + 'rpx' }]" />
<view class="list-content">
<!-- 参团会员统计 -->
<view class="content-header ss-flex-col ss-col-center ss-row-center">
<view class="content-header-title ss-flex ss-row-center">
<view
v-for="(item, index) in state.summaryData.avatars"
:key="index"
class="picture"
:style="index === 6 ? 'position: relative' : 'position: static'"
>
<span class="avatar" :style="`background-image: url(${item})`" />
<span v-if="index === 6 && state.summaryData.avatars.length > 3" class="mengceng">
<i>···</i>
</span>
</view>
<text class="pic_count">{{ state.summaryData.userCount || 0 }}人参与</text>
</view>
</view>
<scroll-view
class="scroll-box"
:style="{ height: pageHeight + 'rpx' }"
scroll-y="true"
:scroll-with-animation="false"
:enable-back-to-top="true"
>
<view class="goods-box ss-m-b-20" v-for="item in state.pagination.list" :key="item.id">
<s-goods-column
class=""
size="lg"
:data="item"
:grouponTag="true"
@click="sheep.$router.go('/pages/goods/groupon', { id: item.id })"
>
<template v-slot:cart>
<button class="ss-reset-button cart-btn">去拼团</button>
</template>
</s-goods-column>
</view>
<uni-load-more
v-if="state.pagination.total > 0"
:status="state.loadStatus"
:content-text="{
contentdown: '上拉加载更多',
}"
@tap="loadMore"
/>
</scroll-view>
</view>
</s-layout>
</template>
<script setup>
import { reactive } from 'vue';
import { onLoad, onReachBottom } from '@dcloudio/uni-app';
import sheep from '@/sheep';
import CombinationApi from '@/sheep/api/promotion/combination';
const { safeAreaInsets, safeArea } = sheep.$platform.device;
const sysNavBar = sheep.$platform.navbar;
const statusBarHeight = sheep.$platform.device.statusBarHeight * 2;
const pageHeight =
(safeArea.height + safeAreaInsets.bottom) * 2 + statusBarHeight - sysNavBar - 350;
const headerBg = sheep.$url.css('/static/img/shop/goods/groupon-header.png');
const state = reactive({
pagination: {
list: [],
total: 0,
pageNo: 1,
pageSize: 10,
},
loadStatus: '',
summaryData: {},
});
// 加载统计数据
const getSummary = async () => {
const { data } = await CombinationApi.getCombinationRecordSummary();
state.summaryData = data;
};
// 加载活动列表
async function getList() {
state.loadStatus = 'loading';
const { data } = await CombinationApi.getCombinationActivityPage({
pageNo: state.pagination.pageNo,
pageSize: state.pagination.pageSize,
});
data.list.forEach((activity) => {
state.pagination.list.push({ ...activity, price: activity.combinationPrice });
});
state.pagination.total = data.total;
state.loadStatus = state.pagination.list.length < state.pagination.total ? 'more' : 'noMore';
}
// 加载更多
function loadMore() {
if (state.loadStatus === 'noMore') {
return;
}
state.pagination.pageNo++;
getList();
}
// 上拉加载更多
onReachBottom(() => loadMore());
// 页面初始化
onLoad(() => {
getSummary();
getList();
});
</script>
<style lang="scss" scoped>
.page-bg {
width: 100%;
height: 458rpx;
margin-top: -88rpx;
background: v-bind(headerBg) no-repeat;
background-size: 100% 100%;
}
.list-content {
position: relative;
z-index: 3;
margin: -190rpx 20rpx 0 20rpx;
background: #fff;
border-radius: 20rpx 20rpx 0 0;
.content-header {
width: 100%;
border-radius: 20rpx 20rpx 0 0;
height: 100rpx;
background: linear-gradient(180deg, #fff4f7, #ffe4d1);
.content-header-title {
width: 100%;
font-size: 30rpx;
font-weight: 500;
color: #ff2923;
line-height: 30rpx;
position: relative;
.more {
position: absolute;
right: 30rpx;
top: 0;
font-size: 24rpx;
font-weight: 400;
color: #999999;
line-height: 30rpx;
}
.picture {
display: inline-table;
}
.avatar {
width: 38rpx;
height: 38rpx;
display: inline-table;
vertical-align: middle;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
border-radius: 50%;
background-repeat: no-repeat;
background-size: cover;
background-position: 0 0;
margin-right: -10rpx;
box-shadow: 0 0 0 1px #fe832a;
}
.pic_count {
margin-left: 30rpx;
font-size: 22rpx;
font-weight: 500;
width: auto;
height: auto;
background: linear-gradient(90deg, #ff6600 0%, #fe832a 100%);
color: #ffffff;
border-radius: 19rpx;
padding: 4rpx 14rpx;
}
.mengceng {
width: 40rpx;
height: 40rpx;
line-height: 36rpx;
background: rgba(51, 51, 51, 0.6);
text-align: center;
border-radius: 50%;
opacity: 1;
position: absolute;
left: -2rpx;
color: #fff;
top: 2rpx;
i {
font-style: normal;
font-size: 20rpx;
}
}
}
}
.scroll-box {
height: 900rpx;
.goods-box {
position: relative;
.cart-btn {
position: absolute;
bottom: 10rpx;
right: 20rpx;
z-index: 11;
height: 50rpx;
line-height: 50rpx;
padding: 0 20rpx;
border-radius: 25rpx;
font-size: 24rpx;
color: #fff;
background: linear-gradient(90deg, #ff6600 0%, #fe832a 100%);
}
}
}
}
</style>
+240
View File
@@ -0,0 +1,240 @@
<!-- 我的拼团订单列表 -->
<template>
<s-layout title="我的拼团">
<su-sticky bgColor="#fff">
<su-tabs
:list="tabMaps"
:scrollable="false"
@change="onTabsChange"
:current="state.currentTab"
></su-tabs>
</su-sticky>
<s-empty v-if="state.pagination.total === 0" icon="/static/goods-empty.png" />
<view v-if="state.pagination.total > 0">
<view
class="order-list-card-box bg-white ss-r-10 ss-m-t-14 ss-m-20"
v-for="record in state.pagination.list"
:key="record.id"
>
<view class="order-card-header ss-flex ss-col-center ss-row-between ss-p-x-20">
<view class="order-no">拼团编号{{ record.id }}</view>
<view class="ss-font-26" :class="formatOrderColor(record)">
{{ tabMaps.find((item) => item.value === record.status).name }}
</view>
</view>
<view class="border-bottom">
<s-goods-item
:img="record.picUrl"
:title="record.spuName"
:price="record.combinationPrice"
>
<template #groupon>
<view class="ss-flex">
<view class="sales-title"> {{ record.userSize }} 人团 </view>
</view>
</template>
</s-goods-item>
</view>
<view class="order-card-footer ss-flex ss-row-right ss-p-x-20">
<button
class="detail-btn ss-reset-button"
@tap="sheep.$router.go('/pages/order/detail', { id: record.orderId })"
>
订单详情
</button>
<button
class="tool-btn ss-reset-button"
:class="{ 'ui-BG-Main-Gradient': record.status === 0 }"
@tap="sheep.$router.go('/pages/activity/groupon/detail', { id: record.id })"
>
{{ record.status === 0 ? '邀请拼团' : '拼团详情' }}
</button>
</view>
</view>
</view>
<uni-load-more
v-if="state.pagination.total > 0"
:status="state.loadStatus"
:content-text="{
contentdown: '上拉加载更多',
}"
@tap="loadMore"
/>
</s-layout>
</template>
<script setup>
import { reactive } from 'vue';
import { onLoad, onReachBottom, onPullDownRefresh } from '@dcloudio/uni-app';
import sheep from '@/sheep';
import _ from 'lodash-es';
import { formatOrderColor } from '@/sheep/hooks/useGoods';
import { resetPagination } from '@/sheep/helper/utils';
import CombinationApi from '@/sheep/api/promotion/combination';
// 数据
const state = reactive({
currentTab: 0,
pagination: {
list: [],
total: 0,
pageNo: 1,
pageSize: 5,
},
loadStatus: '',
deleteOrderId: 0,
});
const tabMaps = [
{
name: '全部',
},
{
name: '进行中',
value: 0,
},
{
name: '拼团成功',
value: 1,
},
{
name: '拼团失败',
value: 2,
},
];
// 切换选项卡
function onTabsChange(e) {
resetPagination(state.pagination);
state.currentTab = e.index;
getGrouponList();
}
// 获取订单列表
async function getGrouponList() {
state.loadStatus = 'loading';
const { code, data } = await CombinationApi.getCombinationRecordPage({
pageNo: state.pagination.pageNo,
pageSize: state.pagination.pageSize,
status: tabMaps[state.currentTab].value,
});
if (code !== 0) {
return;
}
state.pagination.list = _.concat(state.pagination.list, data.list);
state.pagination.total = data.total;
state.loadStatus = state.pagination.list.length < state.pagination.total ? 'more' : 'noMore';
}
onLoad((options) => {
if (options.type) {
state.currentTab = options.type;
}
getGrouponList();
});
// 加载更多
function loadMore() {
if (state.loadStatus === 'noMore') {
return;
}
state.pagination.pageNo++;
getGrouponList();
}
// 上拉加载更多
onReachBottom(() => {
loadMore();
});
//下拉刷新
onPullDownRefresh(() => {
resetPagination(state.pagination);
getGrouponList();
setTimeout(function () {
uni.stopPullDownRefresh();
}, 800);
});
</script>
<style lang="scss" scoped>
.swiper-box {
flex: 1;
.swiper-item {
height: 100%;
width: 100%;
}
}
.order-list-card-box {
.order-card-header {
height: 80rpx;
.order-no {
font-size: 26rpx;
font-weight: 500;
}
}
.order-card-footer {
height: 100rpx;
.detail-btn {
width: 210rpx;
height: 66rpx;
border: 2rpx solid #dfdfdf;
border-radius: 33rpx;
font-size: 26rpx;
font-weight: 400;
color: #999999;
margin-right: 20rpx;
}
.tool-btn {
width: 210rpx;
height: 66rpx;
border-radius: 33rpx;
font-size: 26rpx;
font-weight: 400;
margin-right: 20rpx;
background: #f6f6f6;
}
.invite-btn {
width: 210rpx;
height: 66rpx;
background: linear-gradient(90deg, #fe832a, #ff6600);
box-shadow: 0px 8rpx 6rpx 0px rgba(255, 104, 4, 0.22);
border-radius: 33rpx;
color: #fff;
font-size: 26rpx;
font-weight: 500;
}
}
}
.sales-title {
height: 32rpx;
background: rgba(#ffe0e2, 0.29);
border-radius: 16rpx;
font-size: 24rpx;
font-weight: 400;
padding: 6rpx 20rpx;
color: #f7979c;
}
.num-title {
font-size: 24rpx;
font-weight: 400;
color: #999999;
}
.warning-color {
color: #faad14;
}
.danger-color {
color: #ff3000;
}
.success-color {
color: #52c41a;
}
</style>
+214
View File
@@ -0,0 +1,214 @@
<!-- 指定满减送的活动列表 -->
<template>
<s-layout class="activity-wrap" :title="state.activityInfo.title">
<!-- 活动信息 -->
<su-sticky bgColor="#fff">
<view class="ss-flex ss-col-top tip-box">
<view class="type-text ss-flex ss-row-center">满减</view>
<view class="ss-flex-1">
<view class="tip-content" v-for="item in state.activityInfo.rules" :key="item">
{{ item.description }}
</view>
</view>
<image class="activity-left-image" src="/static/activity-left.png" />
<image class="activity-right-image" src="/static/activity-right.png" />
</view>
</su-sticky>
<!-- 商品信息 -->
<view class="ss-flex ss-flex-wrap ss-p-x-20 ss-m-t-20 ss-col-top">
<view class="goods-list-box">
<view class="left-list" v-for="item in state.leftGoodsList" :key="item.id">
<s-goods-column
class="goods-md-box"
size="md"
:data="item"
@click="sheep.$router.go('/pages/goods/index', { id: item.id })"
@getHeight="mountMasonry($event, 'left')"
>
<template v-slot:cart>
<button class="ss-reset-button cart-btn"> </button>
</template>
</s-goods-column>
</view>
</view>
<view class="goods-list-box">
<view class="right-list" v-for="item in state.rightGoodsList" :key="item.id">
<s-goods-column
class="goods-md-box"
size="md"
:data="item"
@click="sheep.$router.go('/pages/goods/index', { id: item.id })"
@getHeight="mountMasonry($event, 'right')"
>
<template v-slot:cart>
<button class="ss-reset-button cart-btn" />
</template>
</s-goods-column>
</view>
</view>
</view>
<uni-load-more
v-if="state.pagination.total > 0"
:status="state.loadStatus"
:content-text="{
contentdown: '上拉加载更多',
}"
@tap="loadMore"
/>
</s-layout>
</template>
<script setup>
import { reactive } from 'vue';
import { onLoad, onReachBottom } from '@dcloudio/uni-app';
import sheep from '@/sheep';
import _ from 'lodash-es';
import RewardActivityApi from '@/sheep/api/promotion/rewardActivity';
import SpuApi from '@/sheep/api/product/spu';
import { appendSettlementProduct } from '@/sheep/hooks/useGoods';
import OrderApi from '@/sheep/api/trade/order';
const state = reactive({
activityId: 0, // 获得编号
activityInfo: {}, // 获得信息
pagination: {
list: [],
total: 1,
pageNo: 1,
pageSize: 8,
},
loadStatus: '',
leftGoodsList: [],
rightGoodsList: [],
});
// 加载瀑布流
let count = 0;
let leftHeight = 0;
let rightHeight = 0;
function mountMasonry(height = 0, where = 'left') {
if (!state.pagination.list[count]) return;
if (where === 'left') {
leftHeight += height;
} else {
rightHeight += height;
}
if (leftHeight <= rightHeight) {
state.leftGoodsList.push(state.pagination.list[count]);
} else {
state.rightGoodsList.push(state.pagination.list[count]);
}
count++;
}
// 加载商品信息
async function getList() {
// 处理拓展参数
const params = {};
if (state.activityInfo.productScope === 2) {
params.ids = state.activityInfo.productSpuIds.join(',');
} else if (state.activityInfo.productScope === 3) {
params.categoryIds = state.activityInfo.productSpuIds.join(',');
}
// 请求数据
state.loadStatus = 'loading';
const { code, data } = await SpuApi.getSpuPage({
pageNo: state.pagination.pageNo,
pageSize: state.pagination.pageSize,
...params,
});
if (code !== 0) {
return;
}
// 拼接结算信息(营销)
await OrderApi.getSettlementProduct(data.list.map((item) => item.id).join(',')).then((res) => {
if (res.code !== 0) {
return;
}
appendSettlementProduct(data.list, res.data);
});
state.pagination.list = _.concat(state.pagination.list, data.list);
state.pagination.total = data.total;
state.loadStatus = state.pagination.list.length < state.pagination.total ? 'more' : 'noMore';
mountMasonry();
}
// 加载活动信息
async function getActivity(id) {
const { code, data } = await RewardActivityApi.getRewardActivity(id);
if (code === 0) {
state.activityInfo = data;
}
}
// 加载更多
function loadMore() {
if (state.loadStatus === 'noMore') {
return;
}
state.pagination.pageNo++;
getList();
}
// 上拉加载更多
onReachBottom(() => {
loadMore();
});
onLoad(async (options) => {
state.activityId = options.activityId;
await getActivity(state.activityId);
await getList(state.activityId);
});
</script>
<style lang="scss" scoped>
.goods-list-box {
width: 50%;
box-sizing: border-box;
.left-list {
margin-right: 10rpx;
margin-bottom: 20rpx;
}
.right-list {
margin-left: 10rpx;
margin-bottom: 20rpx;
}
}
.tip-box {
background: #fff0e7;
padding: 20rpx;
width: 100%;
position: relative;
box-sizing: border-box;
.activity-left-image {
position: absolute;
bottom: 0;
left: 0;
width: 58rpx;
height: 36rpx;
}
.activity-right-image {
position: absolute;
top: 0;
right: 0;
width: 72rpx;
height: 50rpx;
}
.type-text {
font-size: 26rpx;
font-weight: 500;
color: #ff6000;
line-height: 42rpx;
}
.tip-content {
font-size: 26rpx;
font-weight: 500;
color: #ff6000;
line-height: 42rpx;
}
}
</style>
+76
View File
@@ -0,0 +1,76 @@
<!-- 积分商城商品列表 -->
<template>
<s-layout title="积分商城" navbar="normal" :leftWidth="0" :rightWidth="0">
<scroll-view
class="scroll-box"
:style="{ height: pageHeight + 'rpx' }"
scroll-y="true"
:scroll-with-animation="false"
:enable-back-to-top="true"
>
<s-point-card ref="sPointCardRef" class="ss-p-x-20 ss-m-t-20" />
<s-empty
v-if="activityTotal === 0"
icon="/static/goods-empty.png"
text="暂无积分商品"
></s-empty>
<uni-load-more
v-if="activityTotal > 0"
:status="loadStatus"
:content-text="{
contentdown: '上拉加载更多',
}"
@tap="loadMore"
/>
</scroll-view>
</s-layout>
</template>
<script setup>
import sheep from '@/sheep';
import { onLoad, onReachBottom } from '@dcloudio/uni-app';
import { reactive, ref } from 'vue';
import PointApi from '@/sheep/api/promotion/point';
import SLayout from '@/sheep/components/s-layout/s-layout.vue';
// 计算页面高度
const { safeAreaInsets, safeArea } = sheep.$platform.device;
const statusBarHeight = sheep.$platform.device.statusBarHeight * 2;
const pageHeight =
(safeArea.height + safeAreaInsets.bottom) * 2 + statusBarHeight - sheep.$platform.navbar - 350;
const sPointCardRef = ref();
// 查询活动列表
const activityPageParams = reactive({
pageNo: 1, // 页码
pageSize: 5, // 每页数量
});
const activityTotal = ref(0); // 活动总数
const activityCount = ref(0); // 已加载活动数量
const loadStatus = ref(''); // 页面加载状态
async function getActivityList() {
loadStatus.value = 'loading';
const { data } = await PointApi.getPointActivityPage(activityPageParams);
await sPointCardRef.value.concatActivity(data.list);
activityCount.value = sPointCardRef.value.getActivityCount();
activityTotal.value = data.total;
loadStatus.value = activityCount.value < activityTotal.value ? 'more' : 'noMore';
}
// 加载更多
function loadMore() {
if (loadStatus.value !== 'noMore') {
activityPageParams.pageNo += 1;
getActivityList();
}
}
// 上拉加载更多
onReachBottom(() => {
loadMore();
});
onLoad(() => {
getActivityList();
});
</script>
+460
View File
@@ -0,0 +1,460 @@
<!-- 秒杀活动列表 -->
<template>
<s-layout :bgStyle="{ color: 'rgb(245,28,19)' }" navbar="inner">
<!--顶部背景图-->
<view
class="page-bg"
:style="[{ marginTop: '-' + Number(statusBarHeight + 88) + 'rpx' }]"
></view>
<!-- 时间段轮播图 -->
<view class="header" v-if="activeTimeConfig?.sliderPicUrls?.length > 0">
<swiper
indicator-dots="true"
autoplay="true"
:circular="true"
interval="3000"
duration="1500"
indicator-color="rgba(255,255,255,0.6)"
indicator-active-color="#fff"
>
<block v-for="(picUrl, index) in activeTimeConfig.sliderPicUrls" :key="index">
<swiper-item class="borRadius14">
<image :src="picUrl" class="slide-image borRadius14" lazy-load />
</swiper-item>
</block>
</swiper>
</view>
<!-- 时间段列表 -->
<view class="flex align-center justify-between ss-p-25">
<!-- 左侧图标 -->
<view class="time-icon">
<image
class="ss-w-100 ss-h-100"
:src="sheep.$url.static('/static/img/shop/priceTag.png')"
/>
</view>
<scroll-view
class="time-list"
:scroll-into-view="activeTimeElId"
scroll-x
scroll-with-animation
>
<view
v-for="(config, index) in timeConfigList"
:key="index"
:class="['item', { active: activeTimeIndex === index }]"
:id="`timeItem${index}`"
@tap="handleChangeTimeConfig(index, config.id)"
>
<!-- 活动起始时间 -->
<view class="time">{{ config.startTime }}</view>
<!-- 活动状态 -->
<view class="status">{{ config?.status }}</view>
</view>
</scroll-view>
</view>
<!-- 内容区 -->
<view class="list-content">
<!-- 活动倒计时 -->
<view class="content-header ss-flex-col ss-col-center ss-row-center">
<view class="content-header-box ss-flex ss-row-center">
<view
class="countdown-box ss-flex"
v-if="activeTimeConfig?.status === TimeStatusEnum.STARTED"
>
<view class="countdown-title ss-m-r-12">距结束</view>
<view class="ss-flex countdown-time">
<view class="ss-flex countdown-h">{{ countDown.h }}</view>
<view class="ss-m-x-4">:</view>
<view class="countdown-num ss-flex ss-row-center">{{ countDown.m }}</view>
<view class="ss-m-x-4">:</view>
<view class="countdown-num ss-flex ss-row-center">{{ countDown.s }}</view>
</view>
</view>
<view v-else> {{ activeTimeConfig?.status }} </view>
</view>
</view>
<!-- 活动列表 -->
<scroll-view
class="scroll-box"
:style="{ height: pageHeight + 'rpx' }"
scroll-y="true"
:scroll-with-animation="false"
:enable-back-to-top="true"
>
<view class="goods-box ss-m-b-20" v-for="activity in activityList" :key="activity.id">
<s-goods-column
size="lg"
:data="{ ...activity, price: activity.seckillPrice }"
:goodsFields="goodsFields"
:seckillTag="true"
>
<!-- 抢购进度 -->
<template #activity>
<view class="limit">
限量
<text class="ss-m-l-5">{{ activity.stock }} {{ activity.unitName }}</text>
</view>
<su-progress :percentage="activity.percent" strokeWidth="10" textInside isAnimate />
</template>
<!-- 抢购按钮 -->
<template #cart>
<button
:class="[
'ss-reset-button cart-btn',
{ disabled: activeTimeConfig?.status === TimeStatusEnum.END },
]"
v-if="activeTimeConfig?.status === TimeStatusEnum.WAIT_START"
>
<span>未开始</span>
</button>
<button
:class="[
'ss-reset-button cart-btn',
{ disabled: activeTimeConfig?.status === TimeStatusEnum.END },
]"
@click="sheep.$router.go('/pages/goods/seckill', { id: activity.id })"
v-else-if="activeTimeConfig?.status === TimeStatusEnum.STARTED"
>
<span>马上抢</span>
</button>
<button
:class="[
'ss-reset-button cart-btn',
{ disabled: activeTimeConfig?.status === TimeStatusEnum.END },
]"
v-else
>
<span>已结束</span>
</button>
</template>
</s-goods-column>
</view>
<uni-load-more
v-if="activityTotal > 0"
:status="loadStatus"
:content-text="{
contentdown: '上拉加载更多',
}"
@tap="loadMore"
/>
</scroll-view>
</view>
</s-layout>
</template>
<script setup>
import { computed, nextTick, reactive, ref } from 'vue';
import { onLoad, onReachBottom } from '@dcloudio/uni-app';
import sheep from '@/sheep';
import { useDurationTime } from '@/sheep/hooks/useGoods';
import SeckillApi from '@/sheep/api/promotion/seckill';
import dayjs from 'dayjs';
import { TimeStatusEnum } from '@/sheep/helper/const';
// 计算页面高度
const { safeAreaInsets, safeArea } = sheep.$platform.device;
const statusBarHeight = sheep.$platform.device.statusBarHeight * 2;
const pageHeight =
(safeArea.height + safeAreaInsets.bottom) * 2 + statusBarHeight - sheep.$platform.navbar - 350;
const headerBg = sheep.$url.css('/static/img/shop/goods/seckill-header.png');
// 商品控件显示的字段(不显示库存、销量。改为显示自定义的进度条)
const goodsFields = {
name: {
show: true,
},
introduction: {
show: true,
},
price: {
show: true,
},
marketPrice: {
show: true,
},
};
//#region 时间段
// 时间段列表
const timeConfigList = ref([]);
// 查询时间段
const getSeckillConfigList = async () => {
const { data } = await SeckillApi.getSeckillConfigList();
const now = dayjs();
const today = now.format('YYYY-MM-DD');
const select = ref([]);
// 判断时间段的状态
data.forEach((config, index) => {
const startTime = dayjs(`${today} ${config.startTime}`);
const endTime = dayjs(`${today} ${config.endTime}`);
select.value[index] = config.id;
if (now.isBefore(startTime)) {
config.status = TimeStatusEnum.WAIT_START;
} else if (now.isAfter(endTime)) {
config.status = TimeStatusEnum.END;
} else {
config.status = TimeStatusEnum.STARTED;
activeTimeIndex.value = index;
}
});
timeConfigList.value = data;
// 默认选中进行中的活动
handleChangeTimeConfig(activeTimeIndex.value, select.value[activeTimeIndex.value]);
// 滚动到进行中的时间段
scrollToTimeConfig(activeTimeIndex.value);
};
// 滚动到指定时间段
const activeTimeElId = ref(''); // 当前选中的时间段的元素ID
const scrollToTimeConfig = (index) => {
nextTick(() => (activeTimeElId.value = `timeItem${index}`));
};
// 切换时间段
const activeTimeIndex = ref(0); // 当前选中的时间段的索引
const activeTimeConfig = computed(() => timeConfigList.value[activeTimeIndex.value]); // 当前选中的时间段
const handleChangeTimeConfig = (index, id) => {
activeTimeIndex.value = index;
// 查询活动列表
activityPageParams.pageNo = 1;
activityPageParams.configId = id;
activityList.value = [];
getActivityList();
};
// 倒计时
const countDown = computed(() => {
const endTime = activeTimeConfig.value?.endTime;
if (endTime) {
return useDurationTime(`${dayjs().format('YYYY-MM-DD')} ${endTime}`);
}
});
//#endregion
//#region 分页查询活动列表
// 查询活动列表
const activityPageParams = reactive({
configId: 0, // 时间段 ID
pageNo: 1, // 页码
pageSize: 5, // 每页数量
});
const activityTotal = ref(0); // 活动总数
const activityList = ref([]); // 活动列表
const loadStatus = ref(''); // 页面加载状态
async function getActivityList() {
loadStatus.value = 'loading';
const { data } = await SeckillApi.getSeckillActivityPage(activityPageParams);
data.list.forEach((activity) => {
// 计算抢购进度
activity.percent = parseInt(
(100 * (activity.totalStock - activity.stock)) / activity.totalStock,
);
});
activityList.value = activityList.value.concat(...data.list);
activityTotal.value = data.total;
loadStatus.value = activityList.value.length < activityTotal.value ? 'more' : 'noMore';
}
// 加载更多
function loadMore() {
if (loadStatus.value !== 'noMore') {
activityPageParams.pageNo += 1;
getActivityList();
}
}
// 上拉加载更多
onReachBottom(() => loadMore());
//#endregion
// 页面初始化
onLoad(async () => {
await getSeckillConfigList();
});
</script>
<style lang="scss" scoped>
// 顶部背景图
.page-bg {
width: 100%;
height: 458rpx;
background: v-bind(headerBg) no-repeat;
background-size: 100% 100%;
}
// 时间段轮播图
.header {
width: 710rpx;
height: 330rpx;
margin: -276rpx auto 0 auto;
border-radius: 14rpx;
overflow: hidden;
swiper {
height: 330rpx !important;
border-radius: 14rpx;
overflow: hidden;
}
image {
width: 100%;
height: 100%;
border-radius: 14rpx;
overflow: hidden;
img {
border-radius: 14rpx;
}
}
}
// 时间段列表:左侧图标
.time-icon {
width: 75rpx;
height: 70rpx;
}
// 时间段列表
.time-list {
width: 596rpx;
white-space: nowrap;
// 时间段
.item {
display: inline-block;
font-size: 20rpx;
color: #666;
text-align: center;
box-sizing: border-box;
margin-right: 30rpx;
width: 130rpx;
// 开始时间
.time {
font-size: 36rpx;
font-weight: 600;
color: #333;
}
// 选中的时间段
&.active {
.time {
color: var(--ui-BG-Main);
}
// 状态
.status {
height: 30rpx;
line-height: 30rpx;
border-radius: 15rpx;
width: 128rpx;
background: linear-gradient(90deg, var(--ui-BG-Main) 0%, var(--ui-BG-Main-gradient) 100%);
color: #fff;
}
}
}
}
// 内容区
.list-content {
position: relative;
z-index: 3;
margin: 0 20rpx 0 20rpx;
background: #fff;
border-radius: 20rpx 20rpx 0 0;
.content-header {
width: 100%;
border-radius: 20rpx 20rpx 0 0;
height: 150rpx;
background: linear-gradient(180deg, #fff4f7, #ffe6ec);
.content-header-box {
width: 678rpx;
height: 64rpx;
background: rgba($color: #fff, $alpha: 0.66);
border-radius: 32px;
// 场次倒计时内容
.countdown-title {
font-size: 28rpx;
font-weight: 500;
color: #333333;
line-height: 28rpx;
}
// 场次倒计时
.countdown-time {
font-size: 28rpx;
color: rgba(#ed3c30, 0.23);
// 场次倒计时:小时部分
.countdown-h {
font-size: 24rpx;
font-family: OPPOSANS;
font-weight: 500;
color: #ffffff;
padding: 0 4rpx;
height: 40rpx;
background: rgba(#ed3c30, 0.23);
border-radius: 6rpx;
}
// 场次倒计时:分钟、秒
.countdown-num {
font-size: 24rpx;
font-family: OPPOSANS;
font-weight: 500;
color: #ffffff;
width: 40rpx;
height: 40rpx;
background: rgba(#ed3c30, 0.23);
border-radius: 6rpx;
}
}
}
}
// 活动列表
.scroll-box {
height: 900rpx;
// 活动
.goods-box {
position: relative;
// 抢购按钮
.cart-btn {
position: absolute;
bottom: 10rpx;
right: 20rpx;
z-index: 11;
height: 44rpx;
line-height: 50rpx;
padding: 0 20rpx;
border-radius: 25rpx;
font-size: 24rpx;
color: #fff;
background: linear-gradient(90deg, #ff6600 0%, #fe832a 100%);
&.disabled {
background: $gray-b;
color: #fff;
}
}
// 秒杀限量商品数
.limit {
font-size: 22rpx;
color: $dark-9;
margin-bottom: 5rpx;
}
}
}
}
</style>
+452
View File
@@ -0,0 +1,452 @@
<!-- 签到界面 -->
<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>
<!-- 签到说明 -->
<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 class="activity-des"> 3.积分可以在购物时抵现金结算的哦 ~~</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();
});
</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-TC);
}
.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: var(--ui-BG-Main-TC);
line-height: 32rpx;
text-align: center;
padding: 2rpx;
}
.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 auto;
}
.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');
}
.num {
font-size: 36rpx;
font-family: 'Guildford Pro';
}
.item {
align-items: center;
justify-content: space-between;
flex-direction: column;
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 {
color: #f4b409;
}
</style>
+21
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>
+110
View File
@@ -0,0 +1,110 @@
<template>
<view class="send-wrap ss-flex">
<view class="left ss-flex ss-flex-1">
<optimize-input
class="ss-flex-1 ss-p-l-22"
:inputBorder="false"
:clearable="false"
v-model="message"
placeholder="请输入你要咨询的问题"
></optimize-input>
</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')"
/>
<button
v-if="message"
class="ss-reset-button send-btn"
@tap="sendMessage"
:disabled="isDisabled || sending"
:class="{ disabled: isDisabled || sending }"
>
<text v-if="sending">发送中</text>
<text v-else>发送</text>
</button>
</view>
</template>
<script setup>
import { computed, ref, onUnmounted } from 'vue';
import OptimizeInput from '@/pages/chat/components/optimize-input.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>
+293
View File
@@ -0,0 +1,293 @@
<template>
<!-- 聊天列表使用scroll-view原生组件整体倒置 -->
<scroll-view
:scroll-top="scroll.top"
class="chat-scroll-view"
scroll-y
:refresher-enabled="false"
@scroll="onScroll"
@scrolltolower="loadMoreHistory"
style="transform: scaleY(-1)"
>
<!-- 消息列表容器 -->
<view class="message-container">
<!-- 加载更多提示 -->
<view v-if="isLoading" class="loading-more" style="transform: scaleY(-1)">
<text>加载中...</text>
</view>
<!-- 消息列表 -->
<view class="message-list">
<view
v-for="(item, index) in messageList"
:key="item.id"
class="message-item"
style="transform: scaleY(-1)"
>
<!-- 消息渲染 -->
<MessageListItem
:message="item"
:message-index="index"
:message-list="messageList"
></MessageListItem>
</view>
</view>
</view>
</scroll-view>
<!-- 底部聊天输入框 -->
<su-fixed bottom>
<view v-if="showTip" class="back-top ss-flex ss-row-center ss-m-b-10" @tap="scrollToTop">
<text class="back-top-item ss-flex ss-row-center">
{{ showNewMessageTip ? '有新消息' : '回到底部' }}
</text>
</view>
<slot name="bottom"></slot>
</su-fixed>
</template>
<script setup>
import MessageListItem from '@/pages/chat/components/messageListItem.vue';
import { onMounted, reactive, ref, computed } from 'vue';
import KeFuApi from '@/sheep/api/promotion/kefu';
import { isEmpty } from '@/sheep/helper/utils';
import { formatDate } from '@/sheep/helper/utils';
import sheep from '@/sheep';
const { safeAreaInsets } = sheep.$platform.device;
const safeAreaInsetsBottom = safeAreaInsets.bottom + 'px'; // 底部安全区域
const messageList = ref([]); // 消息列表
const showTip = ref(false); // 显示提示
const showNewMessageTip = ref(false); // 显示有新消息提示
const refreshMessage = ref(false); // 更新消息列表
const isLoading = ref(false); // 是否正在加载更多
const hasMore = ref(true); // 是否还有更多数据
const keyboardHeight = ref(0); // 键盘高度
const scroll = ref({
top: 0,
oldTop: 0,
}); // 滚动位置记录
const queryParams = reactive({
no: 1,
limit: 20,
createTime: undefined,
}); // 查询参数
// 计算聊天窗口高度
const chatScrollHeight = computed(() => {
const baseHeight = 'calc(100vh - 150px - ' + safeAreaInsetsBottom + ')';
if (keyboardHeight.value > 0) {
// 键盘弹起状态,减去键盘高度
return `calc(${baseHeight} - ${keyboardHeight.value}px)`;
}
return baseHeight;
});
// 获得消息分页列表
const getMessageList = async () => {
isLoading.value = true;
try {
const { data } = await KeFuApi.getKefuMessageList(queryParams);
if (isEmpty(data)) {
hasMore.value = false;
return;
}
if (queryParams.no > 1 && refreshMessage.value) {
const newMessageList = [];
for (const message of data) {
if (messageList.value.some((val) => val.id === message.id)) {
continue;
}
newMessageList.push(message);
}
// 新消息追加到开头
messageList.value = [...newMessageList, ...messageList.value];
refreshMessage.value = false; // 更新好后重置状态
return;
}
if (queryParams.no > 1) {
// 加载更多历史消息,追加到现有列表末尾(因为是倒置的,所以旧消息在底部/列表末尾)
if (data.length < queryParams.limit) {
hasMore.value = false; // 如果返回的数据少于请求的数量,说明没有更多数据了
}
// 过滤掉已存在的消息
const historyMessages = data.filter(
(msg) => !messageList.value.some((existing) => existing.id === msg.id),
);
if (historyMessages.length > 0) {
messageList.value = [...messageList.value, ...historyMessages];
}
} else {
// 首次加载
messageList.value = data;
if (data.length < queryParams.limit) {
hasMore.value = false;
}
}
if (data.slice(-1).length > 0) {
// 设置最后一次历史查询的最后一条消息的 createTime
queryParams.createTime = formatDate(data.slice(-1)[0].createTime);
}
} finally {
isLoading.value = false;
}
};
/** 加载更多历史数据 */
const loadMoreHistory = async () => {
if (isLoading.value || !hasMore.value) return;
// 增加页码
queryParams.no += 1;
await getMessageList();
};
/** 刷新消息列表 */
const refreshMessageList = async (message = undefined) => {
if (typeof message !== 'undefined') {
// 追加数据到列表开头(因为是倒置的,所以新消息在顶部/列表开头)
messageList.value.unshift(message);
showNewMessageTip.value = true;
} else {
queryParams.createTime = undefined;
refreshMessage.value = true;
await getMessageList();
}
// 若已是第一页则不做处理
if (queryParams.no > 1) {
showTip.value = true;
} else {
scrollToTop();
}
};
/** 滚动到顶部(倒置后相当于滚动到最新消息) */
const scrollToTop = () => {
scroll.value.top = scroll.value.oldTop;
setTimeout(() => {
scroll.value.top = 0;
}, 200); // 等待 view 层同步
showTip.value = false;
};
/** 设置键盘高度 */
const setKeyboardHeight = (height) => {
keyboardHeight.value = height;
// 键盘弹起时,滚动到最新消息
if (height > 0) {
scrollToTop();
}
};
defineExpose({ getMessageList, refreshMessageList });
/** 监听消息列表滚动 */
const onScroll = (e) => {
const { scrollTop } = e.detail;
scroll.value.oldTop = scrollTop;
// 当滚动位置超过一定值时,显示"新消息"提示
if (scrollTop > 100) {
showTip.value = true;
} else {
showTip.value = false;
}
};
// 监听键盘弹起和收起事件
const setupKeyboardListeners = () => {
// #ifdef H5
// H5环境
window.addEventListener('resize', () => {
// 窗口大小变化可能是由键盘引起的
if (
document.activeElement &&
(document.activeElement.tagName === 'INPUT' ||
document.activeElement.tagName === 'TEXTAREA')
) {
// 估算键盘高度,实际上是窗口高度变化
const currentHeight = window.innerHeight;
const viewportHeight = window.visualViewport
? window.visualViewport.height
: window.innerHeight;
const keyboardHeight = currentHeight - viewportHeight;
setKeyboardHeight(keyboardHeight > 0 ? keyboardHeight : 0);
} else {
setKeyboardHeight(0);
}
});
// #endif
// #ifdef MP-WEIXIN
// TODO puhui999: 小程序键盘弹起还有点问题,看看怎么适配
// 微信小程序环境
uni.onKeyboardHeightChange((res) => {
setKeyboardHeight(res.height);
});
// #endif
};
onMounted(() => {
queryParams.no = 1; // 确保首次加载是第一页
scroll.value = {
top: 0,
oldTop: 0,
};
getMessageList();
setupKeyboardListeners();
});
</script>
<style lang="scss" scoped>
.chat-scroll-view {
height: v-bind(chatScrollHeight);
width: 100%;
position: relative;
background-color: #f8f8f8;
z-index: 1;
}
.message-container {
width: 100%;
/* 确保容器至少有一屏高度 */
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: flex-end;
}
.message-list {
width: 100%;
display: flex;
flex-direction: column;
padding-bottom: 20px;
}
.message-item {
margin-bottom: 10px;
}
.loading-more {
width: 100%;
height: 40px;
display: flex;
justify-content: center;
align-items: center;
color: #999;
font-size: 14px;
}
.back-top {
.back-top-item {
height: 30px;
width: 100px;
background-color: #fff;
border-radius: 30px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
}
</style>
+297
View File
@@ -0,0 +1,297 @@
<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"
class="system-message"
>
{{ message.content }}
</view>
<!-- 日期 - 移到消息内容上方显示 -->
<view
v-if="
message.contentType !== KeFuMessageContentTypeEnum.SYSTEM &&
showTime(message, messageIndex)
"
class="date-message"
>
{{ formatDate(message.createTime) }}
</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('')"
mode="aspectFill"
lazy-load
/>
<!-- 内容 -->
<template v-if="message.contentType === KeFuMessageContentTypeEnum.TEXT">
<view class="message-box" :class="{ admin: message.senderType === UserTypeEnum.ADMIN }">
<mp-html :content="processedContent" :domain="sheep.$url.cdn('')" lazy-load />
</view>
</template>
<template v-if="message.contentType === KeFuMessageContentTypeEnum.IMAGE">
<view
class="message-box"
:class="{ admin: message.senderType === UserTypeEnum.ADMIN }"
:style="{ width: '200rpx' }"
>
<su-image
class="message-img"
isPreview
:previewList="[sheep.$url.cdn(getMessageContent(message).picUrl || message.content)]"
:current="0"
:src="sheep.$url.cdn(getMessageContent(message).picUrl || message.content)"
:height="200"
:width="200"
mode="aspectFill"
/>
</view>
</template>
<template v-if="message.contentType === KeFuMessageContentTypeEnum.PRODUCT">
<div class="ss-m-b-10">
<GoodsItem
:goodsData="getMessageContent(message)"
@tap="
sheep.$router.go('/pages/goods/index', { id: getMessageContent(message).spuId })
"
/>
</div>
</template>
<template v-if="message.contentType === KeFuMessageContentTypeEnum.ORDER">
<OrderItem
:orderData="getMessageContent(message)"
@tap="sheep.$router.go('/pages/order/detail', { id: getMessageContent(message).id })"
/>
</template>
<!-- user头像 -->
<image
v-if="message.senderType === UserTypeEnum.MEMBER"
class="chat-avatar ss-m-l-24"
:src="
sheep.$url.cdn(userInfo.avatar) ||
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, jsonParse } from '@/sheep/helper/utils';
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) => jsonParse(item.content)); // 解析消息内容
const userInfo = computed(() => sheep.$store('user').userInfo);
//======================= 工具 =======================
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;
});
// 缓存表情映射
const emojiMap = computed(() => {
const map = new Map();
emojiList.forEach((emoji) => {
map.set(emoji.name, emoji.file);
});
return map;
});
// 处理表情 - 进行缓存优化
function replaceEmoji(data) {
let newData = data;
if (typeof newData !== 'object') {
let reg = /\[(.+?)]/g; // [] 中括号
let zhEmojiName = newData.match(reg);
if (zhEmojiName) {
zhEmojiName.forEach((item) => {
const emojiFile = emojiMap.value.get(item) || '';
if (emojiFile) {
newData = newData.replace(
item,
`<img class="chat-img" style="width: 24px;height: 24px;margin: 0 3px;vertical-align: middle;" src="${sheep.$url.cdn(
'/static/img/chat/emoji/' + emojiFile,
)}"/>`,
);
}
});
}
}
return newData;
}
// 预处理内容,避免重复计算
const processedContent = computed(() => {
if (props.message.contentType === KeFuMessageContentTypeEnum.TEXT) {
return replaceEmoji(getMessageContent.value(props.message).text || props.message.content);
}
return props.message.content;
});
</script>
<style scoped lang="scss">
.message-item {
margin-bottom: 10rpx;
}
.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;
white-space: normal;
word-break: break-all;
word-wrap: break-word;
padding: 20rpx;
color: #fff;
background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
margin-top: 3px;
margin-bottom: 9px;
border-top-left-radius: 10px;
border-bottom-right-radius: 10px;
border-bottom-left-radius: 10px;
&.admin {
background: #fff;
color: #333;
margin-top: 3px;
margin-bottom: 9px;
border-radius: 0 10px 10px 10px;
}
:deep() {
.imgred {
width: 100%;
}
.imgred,
img {
width: 100%;
}
}
}
:deep() {
.goods,
.order {
max-width: 500rpx;
}
}
.message-img {
width: 100px;
height: 100px;
border-radius: 6rpx;
}
.error-img {
width: 400rpx;
height: 400rpx;
}
</style>
+540
View File
@@ -0,0 +1,540 @@
<template>
<view
class="uni-easyinput"
:class="{ 'uni-easyinput-error': msg }"
:style="{ color: inputBorder && msg ? '#e43d33' : styles.color }"
>
<view
class="uni-easyinput__content"
:class="{
'is-input-border': inputBorder,
'is-input-error-border': inputBorder && msg,
'is-textarea': type === 'textarea',
'is-disabled': disabled
}"
:style="{
'border-color': inputBorder && msg ? '#dd524d' : styles.borderColor,
'background-color': disabled ? styles.disableColor : ''
}"
>
<uni-icons
v-if="prefixIcon"
class="content-clear-icon"
:type="prefixIcon"
color="#c0c4cc"
@click="onClickIcon('prefix')"
></uni-icons>
<textarea
v-if="type === 'textarea'"
class="uni-easyinput__content-textarea"
:class="{ 'input-padding': inputBorder }"
:name="name"
:value="val"
:placeholder="placeholder"
:placeholderStyle="placeholderStyle"
:disabled="disabled"
placeholder-class="uni-easyinput__placeholder-class"
:maxlength="inputMaxlength"
:focus="focused"
:autoHeight="autoHeight"
:adjust-position="false"
@input="onInput"
@blur="onBlur"
@focus="onFocus"
@confirm="onConfirm"
></textarea>
<input
v-else
:type="type === 'password' ? 'text' : type"
class="uni-easyinput__content-input"
:style="{
'padding-right': type === 'password' || clearable || prefixIcon ? '' : '10px',
'padding-left': paddingLeft + 'px'
}"
:name="name"
:value="val"
:password="!showPassword && type === 'password'"
:placeholder="placeholder"
:placeholderStyle="placeholderStyle"
placeholder-class="uni-easyinput__placeholder-class"
:disabled="disabled"
:maxlength="inputMaxlength"
:focus="focused"
:confirmType="confirmType"
:adjust-position="false"
@focus="onFocus"
@blur="onBlur"
@input="onInput"
@change="onInput"
@confirm="onConfirm"
:cursor-spacing="30"
always-embed
/>
<template v-if="type === 'password' && passwordIcon">
<uni-icons
v-if="val"
class="content-clear-icon"
:class="{ 'is-textarea-icon': type === 'textarea' }"
:type="showPassword ? 'eye-slash-filled' : 'eye-filled'"
:size="18"
color="#c0c4cc"
@click="onEyes"
></uni-icons>
</template>
<template v-else-if="suffixIcon">
<uni-icons
v-if="suffixIcon"
class="content-clear-icon"
:type="suffixIcon"
color="#c0c4cc"
@click="onClickIcon('suffix')"
></uni-icons>
</template>
<template v-else>
<uni-icons
class="content-clear-icon"
:class="{ 'is-textarea-icon': type === 'textarea' }"
type="clear"
:size="clearSize"
v-if="clearable && val && !disabled"
color="#c0c4cc"
@click="onClear"
></uni-icons>
</template>
<slot name="right"></slot>
</view>
</view>
</template>
<script>
// import {
// debounce,
// throttle
// } from './common.js'
/**
* Easyinput 输入框
* @description 此组件可以实现表单的输入与校验,包括 "text" 和 "textarea" 类型。
* @tutorial https://ext.dcloud.net.cn/plugin?id=3455
* @property {String} value 输入内容
* @property {String } type 输入框的类型(默认text password/text/textarea/..
* @value text 文本输入键盘
* @value textarea 多行文本输入键盘
* @value password 密码输入键盘
* @value number 数字输入键盘,注意iOS上app-vue弹出的数字键盘并非9宫格方式
* @value idcard 身份证输入键盘,信、支付宝、百度、QQ小程序
* @value digit 带小数点的数字键盘 ,App的nvue页面、微信、支付宝、百度、头条、QQ小程序支持
* @property {Boolean} clearable 是否显示右侧清空内容的图标控件,点击可清空输入框内容(默认true)
* @property {Boolean} autoHeight 是否自动增高输入区域,type为textarea时有效(默认false
* @property {String } placeholder 输入框的提示文字
* @property {String } placeholderStyle placeholder的样式(内联样式,字符串),如"color: #ddd"
* @property {Boolean} focus 是否自动获得焦点(默认false)
* @property {Boolean} disabled 是否禁用(默认false
* @property {Number } maxlength 最大输入长度,设置为 -1 的时候不限制最大长度(默认140)
* @property {String } confirmType 设置键盘右下角按钮的文字,仅在type="text"时生效(默认done
* @property {Number } clearSize 清除图标的大小,单位px(默认15)
* @property {String} prefixIcon 输入框头部图标
* @property {String} suffixIcon 输入框尾部图标
* @property {Boolean} trim 是否自动去除两端的空格
* @value both 去除两端空格
* @value left 去除左侧空格
* @value right 去除右侧空格
* @value start 去除左侧空格
* @value end 去除右侧空格
* @value all 去除全部空格
* @value none 不去除空格
* @property {Boolean} inputBorder 是否显示input输入框的边框(默认true)
* @property {Boolean} passwordIcon type=password时是否显示小眼睛图标
* @property {Object} styles 自定义颜色
* @event {Function} input 输入框内容发生变化时触发
* @event {Function} focus 输入框获得焦点时触发
* @event {Function} blur 输入框失去焦点时触发
* @event {Function} confirm 点击完成按钮时触发
* @event {Function} iconClick 点击图标时触发
* @example <uni-easyinput v-model="mobile"></uni-easyinput>
*/
export default {
name: 'optimize-input',
emits: ['click', 'iconClick', 'update:modelValue', 'input', 'focus', 'blur', 'confirm'],
model: {
prop: 'modelValue',
event: 'update:modelValue'
},
props: {
name: String,
value: [Number, String],
modelValue: [Number, String],
type: {
type: String,
default: 'text'
},
clearable: {
type: Boolean,
default: true
},
autoHeight: {
type: Boolean,
default: false
},
placeholder: String,
placeholderStyle: String,
focus: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
},
maxlength: {
type: [Number, String],
default: 140
},
confirmType: {
type: String,
default: 'done'
},
clearSize: {
type: [Number, String],
default: 15
},
inputBorder: {
type: Boolean,
default: true
},
prefixIcon: {
type: String,
default: ''
},
suffixIcon: {
type: String,
default: ''
},
trim: {
type: [Boolean, String],
default: true
},
passwordIcon: {
type: Boolean,
default: true
},
styles: {
type: Object,
default() {
return {
color: '#333',
disableColor: '#F7F6F6',
borderColor: '#e5e5e5'
};
}
},
errorMessage: {
type: [String, Boolean],
default: ''
},
paddingLeft:{
type: [Number, String],
default: 0
}
},
data() {
return {
focused: false,
errMsg: '',
val: '',
showMsg: '',
border: false,
isFirstBorder: false,
showClearIcon: false,
showPassword: false
};
},
computed: {
msg() {
return this.errorMessage || this.errMsg;
},
// 因为uniapp的input组件的maxlength组件必须要数值,这里转为数值,用户可以传入字符串数值
inputMaxlength() {
return Number(this.maxlength);
}
},
watch: {
value(newVal) {
if (this.errMsg) this.errMsg = '';
this.val = newVal;
// fix by mehaotian is_reset 在 uni-forms 中定义
if (this.form && this.formItem && !this.is_reset) {
this.is_reset = false;
this.formItem.setValue(newVal);
}
},
modelValue(newVal) {
if (this.errMsg) this.errMsg = '';
this.val = newVal;
if (this.form && this.formItem && !this.is_reset) {
this.is_reset = false;
this.formItem.setValue(newVal);
}
},
focus(newVal) {
this.$nextTick(() => {
this.focused = this.focus;
});
}
},
created() {
if (!this.value && this.value !== 0) {
this.val = this.modelValue;
}
if (!this.modelValue && this.modelValue !== 0) {
this.val = this.value;
}
this.form = this.getForm('uniForms');
this.formItem = this.getForm('uniFormsItem');
if (this.form && this.formItem) {
if (this.formItem.name) {
if (!this.is_reset) {
this.is_reset = false;
this.formItem.setValue(this.val);
}
this.rename = this.formItem.name;
this.form.inputChildrens.push(this);
}
}
},
mounted() {
this.$nextTick(() => {
this.focused = this.focus;
});
},
methods: {
/**
* 初始化变量值
*/
init() {},
onClickIcon(type) {
this.$emit('iconClick', type);
},
/**
* 获取父元素实例
*/
getForm(name = 'uniForms') {
let parent = this.$parent;
let parentName = parent.$options.name;
while (parentName !== name) {
parent = parent.$parent;
if (!parent) return false;
parentName = parent.$options.name;
}
return parent;
},
onEyes() {
this.showPassword = !this.showPassword;
},
onInput(event) {
let value = event.detail.value;
// 判断是否去除空格
if (this.trim) {
if (typeof this.trim === 'boolean' && this.trim) {
value = this.trimStr(value);
}
if (typeof this.trim === 'string') {
value = this.trimStr(value, this.trim);
}
}
if (this.errMsg) this.errMsg = '';
this.val = value;
// TODO 兼容 vue2
this.$emit('input', value);
// TODO 兼容 vue3
this.$emit('update:modelValue', value);
},
onFocus(event) {
this.$emit('focus', event);
},
onBlur(event) {
let value = event.detail.value;
this.$emit('blur', event);
},
onConfirm(e) {
this.$emit('confirm', e.detail.value);
},
onClear(event) {
this.val = '';
// TODO 兼容 vue2
this.$emit('input', '');
// TODO 兼容 vue2
// TODO 兼容 vue3
this.$emit('update:modelValue', '');
},
fieldClick() {
this.$emit('click');
},
trimStr(str, pos = 'both') {
if (pos === 'both') {
return str.trim();
} else if (pos === 'left') {
return str.trimLeft();
} else if (pos === 'right') {
return str.trimRight();
} else if (pos === 'start') {
return str.trimStart();
} else if (pos === 'end') {
return str.trimEnd();
} else if (pos === 'all') {
return str.replace(/\s+/g, '');
} else if (pos === 'none') {
return str;
}
return str;
}
}
};
</script>
<style lang="scss">
$uni-error: #e43d33;
$uni-border-1: #dcdfe6 !default;
.uni-easyinput {
/* #ifndef APP-NVUE */
width: 100%;
/* #endif */
flex: 1;
position: relative;
text-align: left;
color: #333;
font-size: 14px;
}
.uni-easyinput__content {
flex: 1;
/* #ifndef APP-NVUE */
width: 100%;
display: flex;
box-sizing: border-box;
min-height: 72rpx;
/* #endif */
flex-direction: row;
align-items: center;
}
.uni-easyinput__content-input {
/* #ifndef APP-NVUE */
width: auto;
/* #endif */
position: relative;
overflow: hidden;
flex: 1;
line-height: 56rpx;
font-size: 28rpx;
height: 56rpx;
}
.uni-easyinput__placeholder-class {
color: #bbbbbb;
font-size: 28rpx;
font-weight: 400;
line-height: normal;
}
.is-textarea {
align-items: flex-start;
}
.is-textarea-icon {
margin-top: 5px;
}
.uni-easyinput__content-textarea {
position: relative;
overflow: hidden;
flex: 1;
line-height: 1.5;
font-size: 14px;
padding-top: 6px;
padding-bottom: 10px;
height: 80px;
/* #ifndef APP-NVUE */
min-height: 80px;
width: auto;
/* #endif */
}
.input-padding {
padding-left: 10px;
}
.content-clear-icon {
padding: 0 5px;
}
.label-icon {
margin-right: 5px;
margin-top: -1px;
}
// 显示边框
.is-input-border {
/* #ifndef APP-NVUE */
display: flex;
box-sizing: border-box;
/* #endif */
flex-direction: row;
align-items: center;
border: 1px solid $uni-border-1;
border-radius: 4px;
}
.uni-error-message {
position: absolute;
bottom: -17px;
left: 0;
line-height: 12px;
color: $uni-error;
font-size: 12px;
text-align: left;
}
.uni-error-msg--boeder {
position: relative;
bottom: 0;
line-height: 22px;
}
.is-input-error-border {
border-color: $uni-error;
.uni-easyinput__placeholder-class {
// color: mix(#fff, $uni-error, 50%);
}
}
.uni-easyinput--border {
margin-bottom: 0;
padding: 10px 15px;
// padding-bottom: 0;
border-top: 1px #eee solid;
}
.uni-easyinput-error {
padding-bottom: 0;
}
.is-first-border {
/* #ifndef APP-NVUE */
border: none;
/* #endif */
/* #ifdef APP-NVUE */
border-width: 0;
/* #endif */
}
.is-disabled {
border-color: red;
background-color: #f7f6f6;
color: #d5d5d5;
.uni-easyinput__placeholder-class {
color: #d5d5d5;
font-size: 12px;
}
}
</style>
+114
View File
@@ -0,0 +1,114 @@
<template>
<view class="bg-white order-list-card-box ss-r-10 ss-m-t-14 ss-m-20"
:key="orderData.id">
<view class="order-card-header ss-flex ss-col-center ss-row-between ss-p-x-20">
<view class="order-no">订单号{{ orderData.no }}</view>
<view class="order-state ss-font-26" :class="formatOrderColor(orderData)">
{{ formatOrderStatus(orderData) }}
</view>
</view>
<view class="border-bottom" v-for="item in orderData.items" :key="item.id">
<s-goods-item
:img="item.picUrl"
:title="item.spuName"
:skuText="item.properties.map((property) => property.valueName).join(' ')"
:price="item.price"
:num="item.count"
/>
</view>
<view class="pay-box ss-m-t-30 ss-flex ss-row-right ss-p-r-20">
<view class="ss-flex ss-col-center">
<view class="discounts-title pay-color"> {{ orderData.productCount }} 件商品,总金额:</view>
<view class="discounts-money pay-color">
{{ fen2yuan(orderData.payPrice) }}
</view>
</view>
</view>
</view>
</template>
<script setup>
import { fen2yuan, formatOrderColor, formatOrderStatus } from '@/sheep/hooks/useGoods';
const props = defineProps({
orderData: {
type: Object,
default: {},
},
});
</script>
<style lang="scss" scoped>
.order-list-card-box {
.order-card-header {
height: 80rpx;
.order-no {
font-size: 26rpx;
font-weight: 500;
}
.order-state {}
}
.pay-box {
.discounts-title {
font-size: 24rpx;
line-height: normal;
color: #999999;
}
.discounts-money {
font-size: 24rpx;
line-height: normal;
color: #999;
font-family: OPPOSANS;
}
.pay-color {
color: #333;
}
}
.order-card-footer {
height: 100rpx;
.more-item-box {
padding: 20rpx;
.more-item {
height: 60rpx;
.title {
font-size: 26rpx;
}
}
}
.more-btn {
color: $dark-9;
font-size: 24rpx;
}
.content {
width: 154rpx;
color: #333333;
font-size: 26rpx;
font-weight: 500;
}
}
}
.warning-color {
color: #faad14;
}
.danger-color {
color: #ff3000;
}
.success-color {
color: #52c41a;
}
.info-color {
color: #999999;
}
</style>
+151
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>
+166
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>
+222
View File
@@ -0,0 +1,222 @@
<template>
<s-layout
class="chat-wrap"
:title="!isReconnecting ? '连接客服成功' : '会话重连中'"
navbar="inner"
>
<!-- 覆盖头部导航栏背景颜色 -->
<view class="page-bg" :style="{ height: sys_navBar + 'px' }"></view>
<!-- 聊天区域 -->
<MessageList ref="messageListRef">
<template #bottom>
<message-input
v-model="chat.msg"
@on-tools="onTools"
@send-message="onSendMessage"
:auto-focus="false"
:show-char-count="true"
:max-length="500"
></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"
:auto-focus="false"
:show-char-count="true"
:max-length="500"
></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 './util/useWebSocket';
import { jsonParse } from '@/sheep/helper/utils';
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: JSON.stringify({ text: chat.msg }),
};
await KeFuApi.sendKefuMessage(data);
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.showTools && chat.toolsMode === mode) {
handleToolsClose();
return;
}
// 切换工具栏
if (chat.showTools && chat.toolsMode !== mode) {
chat.showTools = false;
chat.toolsMode = '';
}
// 延迟打开等一下过度效果
setTimeout(() => {
chat.toolsMode = mode;
chat.showTools = true;
}, 200);
}
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: JSON.stringify({ picUrl: 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);
return;
}
// 2.2 消息类型:KEFU_MESSAGE_TYPE
if (type === WebSocketMessageTypeConstants.KEFU_MESSAGE_TYPE) {
// 刷新消息列表
await messageListRef.value.refreshMessageList(jsonParse(data.content));
return;
}
// 2.3 消息类型:KEFU_MESSAGE_ADMIN_READ
if (type === WebSocketMessageTypeConstants.KEFU_MESSAGE_ADMIN_READ) {
console.log('管理员已读消息');
// 更新消息已读状态
sheep.$helper.toast('客服已读您的消息');
}
},
});
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>
+19
View File
@@ -0,0 +1,19 @@
export const KeFuMessageContentTypeEnum = {
TEXT: 1, // 文本消息
IMAGE: 2, // 图片消息
VOICE: 3, // 语音消息
VIDEO: 4, // 视频消息
SYSTEM: 5, // 系统消息
// ========== 商城特殊消息 ==========
PRODUCT: 10,// 商品消息
ORDER: 11,// 订单消息"
};
export const UserTypeEnum = {
MEMBER: 1, // 会员 面向 c 端,普通用户
ADMIN: 2, // 管理员 面向 b 端,管理后台
};
// Promotion 的 WebSocket 消息类型枚举类
export const WebSocketMessageTypeConstants = {
KEFU_MESSAGE_TYPE: 'kefu_message_type', // 客服消息类型
KEFU_MESSAGE_ADMIN_READ: 'kefu_message_read_status_change' // 客服消息管理员已读
}
+58
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);
});
+150
View File
@@ -0,0 +1,150 @@
import { onBeforeUnmount, reactive, ref } from 'vue';
import { baseUrl, websocketPath } from '@/sheep/config';
import { copyValueToTarget } from '@/sheep/helper/utils';
import { getRefreshToken } from '@/sheep/request';
/**
* WebSocket 创建 hook
* @param opt 连接配置
* @return {{options: *}}
*/
export function useWebSocket(opt) {
const options = reactive({
url: (baseUrl + websocketPath).replace('http', 'ws') + '?token=' + getRefreshToken(), // ws 地址
isReconnecting: false, // 正在重新连接
reconnectInterval: 3000, // 重连间隔,单位毫秒
heartBeatInterval: 5000, // 心跳间隔,单位毫秒
pingTimeoutDuration: 1000, // 超过这个时间,后端没有返回pong,则判定后端断线了。
heartBeatTimer: null, // 心跳计时器
destroy: false, // 是否销毁
pingTimeout: null, // 心跳检测定时器
reconnectTimeout: null, // 重连定时器ID的属性
onConnected: () => {}, // 连接成功时触发
onClosed: () => {}, // 连接关闭时触发
onMessage: (data) => {}, // 收到消息
});
const SocketTask = ref(null); // SocketTask 由 uni.connectSocket() 接口创建
const initEventListeners = () => {
// 监听 WebSocket 连接打开事件
SocketTask.value.onOpen(() => {
console.log('WebSocket 连接成功');
// 连接成功时触发
options.onConnected();
// 开启心跳检查
startHeartBeat();
});
// 监听 WebSocket 接受到服务器的消息事件
SocketTask.value.onMessage((res) => {
try {
if (res.data === 'pong') {
// 收到心跳重置心跳超时检查
resetPingTimeout();
} else {
options.onMessage(JSON.parse(res.data));
}
} catch (error) {
console.error(error);
}
});
// 监听 WebSocket 连接关闭事件
SocketTask.value.onClose((event) => {
// 情况一:实例销毁
if (options.destroy) {
options.onClosed();
} else {
// 情况二:连接失败重连
// 停止心跳检查
stopHeartBeat();
// 重连
reconnect();
}
});
};
// 发送消息
const sendMessage = (message) => {
if (SocketTask.value && !options.destroy) {
SocketTask.value.send({ data: message });
}
};
// 开始心跳检查
const startHeartBeat = () => {
options.heartBeatTimer = setInterval(() => {
sendMessage('ping');
options.pingTimeout = setTimeout(() => {
// 如果在超时时间内没有收到 pong,则认为连接断开
reconnect();
}, options.pingTimeoutDuration);
}, options.heartBeatInterval);
};
// 停止心跳检查
const stopHeartBeat = () => {
clearInterval(options.heartBeatTimer);
resetPingTimeout();
};
// WebSocket 重连
const reconnect = () => {
if (options.destroy || !SocketTask.value) {
// 如果WebSocket已被销毁或尚未完全关闭,不进行重连
return;
}
// 重连中
options.isReconnecting = true;
// 清除现有的重连标志,以避免多次重连
if (options.reconnectTimeout) {
clearTimeout(options.reconnectTimeout);
}
// 设置重连延迟
options.reconnectTimeout = setTimeout(() => {
// 检查组件是否仍在运行和WebSocket是否关闭
if (!options.destroy) {
// 重置重连标志
options.isReconnecting = false;
// 初始化新的WebSocket连接
initSocket();
}
}, options.reconnectInterval);
};
const resetPingTimeout = () => {
if (options.pingTimeout) {
clearTimeout(options.pingTimeout);
options.pingTimeout = null; // 清除超时ID
}
};
const close = () => {
options.destroy = true;
stopHeartBeat();
if (options.reconnectTimeout) {
clearTimeout(options.reconnectTimeout);
}
if (SocketTask.value) {
SocketTask.value.close();
SocketTask.value = null;
}
};
const initSocket = () => {
options.destroy = false;
copyValueToTarget(options, opt);
SocketTask.value = uni.connectSocket({
url: options.url,
complete: () => {},
success: () => {},
});
initEventListeners();
};
initSocket();
onBeforeUnmount(() => {
close();
});
return { options };
}
File diff suppressed because one or more lines are too long
@@ -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>
@@ -0,0 +1,171 @@
<!-- 提现方式的 select 组件 -->
<template>
<su-popup :show="show" class="ss-checkout-counter-wrap" @close="hideModal">
<view class="ss-modal-box bg-white ss-flex-col">
<view class="modal-header ss-flex-col ss-col-left">
<text class="modal-title ss-m-b-20">选择提现方式</text>
</view>
<view class="modal-content ss-flex-1 ss-p-b-100">
<radio-group @change="onChange">
<label
class="container-list ss-p-l-34 ss-p-r-24 ss-flex ss-col-center ss-row-center"
v-for="(item, index) in typeList"
:key="index"
>
<view class="container-icon ss-flex ss-m-r-20">
<image :src="sheep.$url.static(item.icon)" />
</view>
<view class="ss-flex-1">{{ item.title }}</view>
<radio
:value="item.value"
color="var(--ui-BG-Main)"
:checked="item.value === state.currentValue"
:disabled="!methods.includes(parseInt(item.value))"
/>
</label>
</radio-group>
</view>
<view class="modal-footer ss-flex ss-row-center ss-col-center">
<button class="ss-reset-button save-btn" @tap="onConfirm">确定</button>
</view>
</view>
</su-popup>
</template>
<script setup>
import { reactive } from 'vue';
import sheep from '@/sheep';
const props = defineProps({
modelValue: {
type: Object,
default() {},
},
show: {
type: Boolean,
default: false,
},
methods: {
// 开启的提现方式
type: Array,
default: [],
},
});
const emits = defineEmits(['update:modelValue', 'change', 'close']);
const state = reactive({
currentValue: '',
});
const typeList = [
{
icon: '/static/img/shop/pay/wallet.png',
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',
},
{
icon: '/static/img/shop/pay/wechat_api.png',
title: '微信零钱', // 微信 API 转账
value: '5',
},
{
icon: '/static/img/shop/pay/alipay_api.png',
title: '支付宝余额', // 支付宝 API 转账
value: '6',
},
];
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>
@@ -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>
@@ -0,0 +1,114 @@
<!-- 分销商信息 -->
<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: 700rpx;
height: 192rpx;
margin: 0 auto;
margin-top: -88rpx;
padding-top: 88rpx;
background: v-bind(headerBg) no-repeat;
background-size: 100% 100%;
.head-img-box {
margin-right: 20rpx;
width: 100rpx;
height: 100rpx;
border-radius: 50%;
position: relative;
background: #fce0ad;
.head-img {
width: 92rpx;
height: 92rpx;
border-radius: 50%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
.card-top {
box-sizing: border-box;
padding-bottom: 34rpx;
.user-name {
font-size: 32rpx;
font-weight: bold;
color: #692e04;
line-height: 30rpx;
margin-bottom: 20rpx;
}
.log-btn {
width: 84rpx;
height: 42rpx;
border: 2rpx solid rgba(#ffffff, 0.33);
border-radius: 21rpx;
font-size: 22rpx;
font-weight: 400;
color: #ffffff;
margin-bottom: 20rpx;
}
.look-btn {
color: #fff;
width: 40rpx;
height: 40rpx;
}
}
.user-info-box {
.tag-box {
background: #ff6000;
border-radius: 18rpx;
line-height: 36rpx;
.tag-img {
width: 36rpx;
height: 36rpx;
border-radius: 50%;
margin-left: -2rpx;
}
.tag-title {
font-size: 24rpx;
padding: 0 10rpx;
font-weight: 500;
line-height: 36rpx;
color: #fff;
}
}
}
}
</style>
@@ -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: 8,
},
});
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>
@@ -0,0 +1,145 @@
<!-- 分销商菜单栏 -->
<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,
// },
{
img: '/static/img/shop/commission/commission_icon7.png',
title: '邀请海报',
path: 'action:showShareModal',
},
{
img: '/static/img/shop/commission/commission_icon8.png',
title: '推广排行',
path: '/pages/commission/promoter',
},
{
img: '/static/img/shop/commission/commission_icon9.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>
+165
View File
@@ -0,0 +1,165 @@
<!-- 分销商品列表 -->
<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: 8,
},
loadStatus: '',
shareInfo: {},
});
function onShareGoods(goodsInfo) {
state.shareInfo = $share.getShareInfo(
{
title: goodsInfo.name,
image: sheep.$url.cdn(goodsInfo.picUrl),
desc: goodsInfo.introduction,
params: {
page: '2',
query: goodsInfo.id,
},
},
{
type: 'goods', // 商品海报
title: goodsInfo.name, // 商品名称
image: sheep.$url.cdn(goodsInfo.picUrl), // 商品主图
price: fen2yuan(goodsInfo.price), // 商品价格
original_price: fen2yuan(goodsInfo.marketPrice), // 商品原价
},
);
showShareModal();
}
async function getGoodsList() {
state.loadStatus = 'loading';
let { code, data } = await SpuApi.getSpuPage({
pageSize: state.pagination.pageSize,
pageNo: state.pagination.pageNo,
});
if (code !== 0) {
state.loadStatus = 'error'; // 处理错误状态
return;
}
// 使用 Promise.all 来等待所有佣金请求完成
await Promise.all(
data.list.map(async (item) => {
try {
const res = await BrokerageApi.getProductBrokeragePrice(item.id);
item.brokerageMinPrice = res.data.brokerageMinPrice;
item.brokerageMaxPrice = res.data.brokerageMaxPrice;
} catch (error) {
console.error(`获取商品【${item.name}】的佣金时出错:`, error);
}
}),
);
// 在所有请求完成后合并列表和更新状态
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(() => {
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>
+57
View File
@@ -0,0 +1,57 @@
<!-- 分销中心 -->
<template>
<s-layout
navbar="inner"
class="index-wrap"
title="分销中心"
:bgStyle="bgStyle"
:onShareAppMessage="shareInfo"
>
<!-- 分销商信息 -->
<commission-info />
<!-- 账户信息 -->
<account-info />
<!-- 菜单栏 -->
<commission-menu />
<!-- 分销记录 -->
<commission-log />
<!-- 权限弹窗 -->
<commission-auth />
</s-layout>
</template>
<script setup>
import { computed } from 'vue';
import commissionInfo from './components/commission-info.vue';
import accountInfo from './components/account-info.vue';
import commissionLog from './components/commission-log.vue';
import commissionMenu from './components/commission-menu.vue';
import commissionAuth from './components/commission-auth.vue';
import sheep from '@/sheep';
import { SharePageEnum } from '@/sheep/helper/const';
/** 分销邀请 */
const shareInfo = computed(() => {
return sheep.$platform.share.getShareInfo(
{
params: {
page: SharePageEnum.HOME.value, // 用户通邀请进入到首页
},
},
{
type: 'user',
},
);
});
const bgStyle = {
color: '#F7D598',
};
</script>
<style lang="scss" scoped>
:deep(.page-main) {
background-size: 100% 100% !important;
}
</style>
+332
View File
@@ -0,0 +1,332 @@
<!-- 分销 - 订单明细 -->
<template>
<s-layout title="分销订单" :class="state.scrollTop ? 'order-warp' : ''" navbar="inner">
<view
class="header-box"
:style="[
{
marginTop: '-' + Number(statusBarHeight + 88) + 'rpx',
paddingTop: Number(statusBarHeight + 108) + 'rpx',
},
]"
>
<!-- 团队数据总览 -->
<view class="team-data-box ss-flex ss-col-center ss-row-between" style="width: 100%">
<view class="data-card" style="width: 100%">
<view class="total-item" style="width: 100%">
<view class="item-title" style="text-align: center">累计推广订单</view>
<view class="total-num" style="text-align: center">
{{ state.totals }}
</view>
</view>
</view>
</view>
</view>
<!-- tab -->
<su-sticky bgColor="#fff">
<su-tabs
:list="tabMaps"
:scrollable="false"
:current="state.currentTab"
@change="onTabsChange"
>
</su-tabs>
</su-sticky>
<!-- 订单 -->
<view class="order-box">
<view class="order-item" v-for="item in state.pagination.list" :key="item">
<view class="order-header">
<view class="no-box ss-flex ss-col-center ss-row-between">
<text class="order-code">订单编号{{ item.bizId }}</text>
<text class="order-state">
{{ item.status === 0 ? '待结算' : item.status === 1 ? '已结算' : '已取消' }}
( 佣金 {{ fen2yuan(item.price) }} )
</text>
</view>
<view class="order-from ss-flex ss-col-center ss-row-between">
<view class="from-user ss-flex ss-col-center">
<text>{{ item.title }}</text>
</view>
<view class="order-time">
{{ sheep.$helper.timeFormat(item.createTime, 'yyyy-mm-dd hh:MM:ss') }}
</view>
</view>
</view>
</view>
<!-- 数据为空 -->
<s-empty v-if="state.pagination.total === 0" icon="/static/order-empty.png" text="暂无订单" />
<!-- 加载更多 -->
<uni-load-more
v-if="state.pagination.total > 0"
:status="state.loadStatus"
:content-text="{
contentdown: '上拉加载更多',
}"
@tap="loadMore"
/>
</view>
</s-layout>
</template>
<script setup>
import sheep from '@/sheep';
import { onLoad, onPageScroll, onReachBottom } from '@dcloudio/uni-app';
import { reactive } from 'vue';
import _ from 'lodash-es';
import { resetPagination } from '@/sheep/helper/utils';
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: 8,
},
});
const tabMaps = [
{
name: '全部',
value: -1,
},
{
name: '待结算',
value: 0, // 待结算
},
{
name: '已结算',
value: 1, // 已结算
},
];
// 切换选项卡
function onTabsChange(e) {
resetPagination(state.pagination);
state.currentTab = e.index;
getOrderList();
}
// 获取订单列表
async function getOrderList() {
state.loadStatus = 'loading';
const tab = tabMaps[state.currentTab];
const queryParams = {
pageSize: state.pagination.pageSize,
pageNo: state.pagination.pageNo,
bizType: 1, // 获得推广佣金
status: tab.value,
};
if (tab.value < 0) {
delete queryParams.status;
}
const { code, data } = await BrokerageApi.getBrokerageRecordPage(queryParams);
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
+604
View File
@@ -0,0 +1,604 @@
<!-- 页面 -->
<template>
<s-layout title="我的团队" :class="state.scrollTop ? 'team-wrap' : ''" navbar="inner">
<view
class="header-box"
:style="[
{
marginTop: '-' + Number(statusBarHeight + 88) + 'rpx',
paddingTop: Number(statusBarHeight + 108) + 'rpx',
},
]"
>
<!-- 推广数据总览 -->
<view 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.summary.firstBrokerageUserCount + state.summary.secondBrokerageUserCount || 0
}}
</view>
</view>
</view>
</view>
</view>
<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 20rpx">
<view class="nav acea-row row-around l1" style="margin-top: 20rpx">
<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="sheep.$url.static('/static/img/shop/search.png')"
mode=""
style="width: 60rpx; height: 64rpx"
@click="submitForm"
/>
</view>
<view class="list">
<view class="sortNav acea-row row-middle" style="display: flex; align-items: center">
<view
class="sortItem"
@click="setSort('userCount', 'asc')"
v-if="sort === 'userCountDESC'"
>
团队排序
<image :src="sheep.$url.static('/static/img/shop/sort1.png')" />
</view>
<view
class="sortItem"
@click="setSort('userCount', 'desc')"
v-else-if="sort === 'userCountASC'"
>
团队排序
<image :src="sheep.$url.static('/static/img/shop/sort3.png')" />
</view>
<view class="sortItem" @click="setSort('userCount', 'desc')" v-else>
团队排序
<image :src="sheep.$url.static('/static/img/shop/sort2.png')" />
</view>
<view class="sortItem" @click="setSort('price', 'asc')" v-if="sort === 'priceDESC'">
金额排序
<image :src="sheep.$url.static('/static/img/shop/sort1.png')" />
</view>
<view
class="sortItem"
@click="setSort('price', 'desc')"
v-else-if="sort === 'priceASC'"
>
金额排序
<image :src="sheep.$url.static('/static/img/shop/sort3.png')" />
</view>
<view class="sortItem" @click="setSort('price', 'desc')" v-else>
金额排序
<image :src="sheep.$url.static('/static/img/shop/sort2.png')" />
</view>
<view
class="sortItem"
@click="setSort('orderCount', 'asc')"
v-if="sort === 'orderCountDESC'"
>
订单排序
<image :src="sheep.$url.static('/static/img/shop/sort1.png')" />
</view>
<view
class="sortItem"
@click="setSort('orderCount', 'desc')"
v-else-if="sort === 'orderCountASC'"
>
订单排序
<image :src="sheep.$url.static('/static/img/shop/sort3.png')" />
</view>
<view class="sortItem" @click="setSort('orderCount', 'desc')" v-else>
订单排序
<image :src="sheep.$url.static('/static/img/shop/sort2.png')" />
</view>
</view>
<block v-for="(item, index) in state.pagination.list" :key="index">
<view class="item acea-row row-between-wrapper" style="display: flex">
<view
class="picTxt acea-row row-between-wrapper"
style="display: flex; align-items: center"
>
<view class="pictrue">
<image :src="item.avatar" />
</view>
<view class="text">
<view class="name line1">{{ item.nickname }}</view>
<view>
加入时间:
{{ sheep.$helper.timeFormat(item.brokerageTime, 'yyyy-mm-dd hh:MM:ss') }}
</view>
</view>
</view>
<view
class="right"
style="
justify-content: center;
flex-direction: column;
display: flex;
margin-left: auto;
"
>
<view>
<text class="num font-color">{{ item.brokerageUserCount || 0 }} </text>
</view>
<view>
<text class="num">{{ item.orderCount || 0 }}</text
></view
>
<view>
<text class="num">{{ fen2yuan(item.brokeragePrice) || 0 }}</text
>
</view>
</view>
</view>
</block>
<block v-if="state.pagination.list.length === 0">
<view style="text-align: center; margin-top: 30rpx">暂无推广人数</view>
</block>
</view>
</view>
</view>
<!-- <home></home> -->
<!-- <view class="header-box" :style="[
{
marginTop: '-' + Number(statusBarHeight + 88) + 'rpx',
paddingTop: Number(statusBarHeight + 108) + 'rpx',
},
]">
<view v-if="userInfo.parent_user" class="referrer-box ss-flex ss-col-center">
推荐人
<image class="referrer-avatar ss-m-r-10" :src="sheep.$url.cdn(userInfo.parent_user.avatar)"
mode="aspectFill">
</image>
{{ userInfo.parent_user.nickname }}
</view>
<view class="team-data-box ss-flex ss-col-center ss-row-between">
<view class="data-card">
<view class="total-item">
<view class="item-title">团队总人数</view>
<view class="total-num">
{{ (state.summary.firstBrokerageUserCount+ state.summary.secondBrokerageUserCount)|| 0 }}
</view>
</view>
<view class="category-item ss-flex">
<view class="ss-flex-1">
<view class="item-title">一级成员</view>
<view class="category-num">{{ state.summary.firstBrokerageUserCount || 0 }}</view>
</view>
<view class="ss-flex-1">
<view class="item-title">二级成员</view>
<view class="category-num">{{ state.summary.secondBrokerageUserCount || 0 }}</view>
</view>
</view>
</view>
<view class="data-card">
<view class="total-item">
<view class="item-title">团队分销商人数</view>
<view class="total-num">{{ agentInfo?.child_agent_count_all || 0 }}</view>
</view>
<view class="category-item ss-flex">
<view class="ss-flex-1">
<view class="item-title">一级分销商</view>
<view class="category-num">{{ agentInfo?.child_agent_count_1 || 0 }}</view>
</view>
<view class="ss-flex-1">
<view class="item-title">二级分销商</view>
<view class="category-num">{{ agentInfo?.child_agent_count_2 || 0 }}</view>
</view>
</view>
</view>
</view>
</view>
<view class="list-box">
<uni-list :border="false">
<uni-list-chat v-for="item in state.pagination.data" :key="item.id" :avatar-circle="true"
:title="item.nickname" :avatar="sheep.$url.cdn(item.avatar)"
:note="filterUserNum(item.agent?.child_user_count_1)">
<view class="chat-custom-right">
<view v-if="item.avatar" class="tag-box ss-flex ss-col-center">
<image class="tag-img" :src="sheep.$url.cdn(item.avatar)" mode="aspectFill">
</image>
<text class="tag-title">{{ item.nickname }}</text>
</view>
<text
class="time-text">{{ sheep.$helper.timeFormat(item.brokerageTime, 'yyyy-mm-dd hh:MM:ss') }}</text>
</view>
</uni-list-chat>
</uni-list>
</view>
<s-empty v-if="state.pagination.total === 0" icon="/static/data-empty.png" text="暂无团队信息">
</s-empty> -->
</s-layout>
</template>
<script setup>
import sheep from '@/sheep';
import { onLoad, onReachBottom } from '@dcloudio/uni-app';
import { computed, reactive, ref } from 'vue';
import _ from 'lodash-es';
import { onPageScroll } from '@dcloudio/uni-app';
import BrokerageApi from '@/sheep/api/trade/brokerage';
import { fen2yuan } from '../../sheep/hooks/useGoods';
const statusBarHeight = sheep.$platform.device.statusBarHeight * 2;
// const agentInfo = computed(() => sheep.$store('user').agentInfo);
const userInfo = computed(() => sheep.$store('user').userInfo);
const headerBg = sheep.$url.css('/static/img/shop/user/withdraw_bg.png');
onPageScroll((e) => {
state.scrollTop = e.scrollTop <= 100;
});
let sort = ref();
const state = reactive({
summary: {},
pagination: {
pageNo: 1,
pageSize: 8,
list: [],
total: 0,
},
loadStatus: '',
// ↓ 新 ui 逻辑
level: 1,
nickname: ref(''),
sortKey: '',
isAsc: '',
});
function filterUserNum(num) {
if (_.isNil(num)) {
return '';
}
return `下级团队${num}`;
}
function submitForm() {
state.pagination.list = [];
getTeamList();
}
async function getTeamList() {
state.loadStatus = 'loading';
let { code, data } = await BrokerageApi.getBrokerageUserChildSummaryPage({
pageNo: state.pagination.pageNo,
pageSize: state.pagination.pageSize,
level: state.level,
'sortingField.order': state.isAsc,
'sortingField.field': state.sortKey,
nickname: state.nickname,
});
if (code !== 0) {
return;
}
state.pagination.list = _.concat(state.pagination.list, data.list);
state.pagination.total = data.total;
state.loadStatus = state.pagination.list.length < state.pagination.total ? 'more' : 'noMore';
}
function setType(e) {
state.pagination.list = [];
state.level = e + '';
getTeamList();
}
function setSort(sortKey, isAsc) {
state.pagination.list = [];
sort = sortKey + isAsc.toUpperCase();
state.isAsc = isAsc;
state.sortKey = sortKey;
getTeamList();
}
onLoad(async () => {
await getTeamList();
// 概要数据
let { data } = await BrokerageApi.getBrokerageUserSummary();
state.summary = data;
});
// 加载更多
function loadMore() {
if (state.loadStatus === 'noMore') {
return;
}
state.pagination.pageNo++;
getTeamList();
}
// 上拉加载更多
onReachBottom(() => {
loadMore();
});
</script>
<style lang="scss" scoped>
.l1 {
background-color: #fff;
height: 86rpx;
line-height: 86rpx;
font-size: 28rpx;
color: #282828;
border-bottom: 1rpx solid #eee;
border-top-left-radius: 14rpx;
border-top-right-radius: 14rpx;
display: flex;
justify-content: space-around;
}
.header-box {
box-sizing: border-box;
padding: 0 20rpx 20rpx 20rpx;
width: 750rpx;
z-index: 3;
position: relative;
background: v-bind(headerBg) no-repeat,
linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
background-size: 750rpx 100%;
// 团队信息总览
.team-data-box {
.data-card {
width: 305rpx;
background: #ffffff;
border-radius: 20rpx;
padding: 20rpx;
.item-title {
font-size: 22rpx;
font-weight: 500;
color: #999999;
line-height: 30rpx;
margin-bottom: 10rpx;
}
.total-item {
margin-bottom: 30rpx;
}
.total-num {
font-size: 38rpx;
font-weight: 500;
color: #333333;
font-family: OPPOSANS;
}
.category-num {
font-size: 26rpx;
font-weight: 500;
color: #333333;
font-family: OPPOSANS;
}
}
}
}
.list-box {
z-index: 3;
position: relative;
}
.chat-custom-right {
.time-text {
font-size: 22rpx;
font-weight: 400;
color: #999999;
}
.tag-box {
background: rgba(0, 0, 0, 0.2);
border-radius: 21rpx;
line-height: 30rpx;
padding: 5rpx 10rpx;
width: 140rpx;
.tag-img {
width: 34rpx;
height: 34rpx;
margin-right: 6rpx;
border-radius: 50%;
}
.tag-title {
font-size: 18rpx;
font-weight: 500;
color: rgba(255, 255, 255, 1);
line-height: 20rpx;
}
}
}
// 推荐人
.referrer-box {
font-size: 28rpx;
font-weight: 500;
color: #ffffff;
padding: 20rpx;
.referrer-avatar {
width: 34rpx;
height: 34rpx;
border-radius: 50%;
}
}
.promoter-list .nav {
background-color: #fff;
height: 86rpx;
line-height: 86rpx;
font-size: 28rpx;
color: #282828;
border-bottom: 1rpx solid #eee;
border-top-left-radius: 14rpx;
border-top-right-radius: 14rpx;
margin-top: -30rpx;
}
.promoter-list .nav .item.on {
border-bottom: 5rpx solid;
// $theme-color
color: 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>
+643
View File
@@ -0,0 +1,643 @@
<!-- 分销 - 佣金明细 -->
<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 v-if="state.currentTab === 0">
<view
class="wallet-list ss-flex border-bottom"
v-for="item in state.pagination.list"
:key="item.id"
>
<view class="list-content">
<view class="title-box ss-flex ss-row-between ss-m-b-20">
<text class="title ss-line-1">{{ item.title }}</text>
<view class="money">
<text v-if="item.price >= 0" class="add">+{{ fen2yuan(item.price) }}</text>
<text v-else class="minus">{{ fen2yuan(item.price) }}</text>
</view>
</view>
<view class="ss-flex ss-row-between ss-col-center">
<text class="time">
{{ sheep.$helper.timeFormat(item.createTime, 'yyyy-mm-dd hh:MM:ss') }}
</text>
<view class="ss-flex ss-col-center">
<text class="status" :class="'status-' + item.status">{{ item.statusName }}</text>
</view>
</view>
</view>
</view>
</view>
<!-- 提现列表 -->
<view v-else>
<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.typeName }}</text>
<view class="money">
<text class="minus">{{ fen2yuan(item.price) }}</text>
</view>
</view>
<view class="ss-flex ss-row-between ss-col-center">
<text class="time">
{{ sheep.$helper.timeFormat(item.createTime, 'yyyy-mm-dd hh:MM:ss') }}
</text>
<button
v-if="item.status === 10 && item.type === 5 && item.payTransferId > 0"
class="ss-reset-button confirm-btn ss-m-l-20"
@tap="onRequestMerchantTransfer(item)"
>
确认收款
</button>
<text v-else class="status" :class="'status-' + item.status">{{
item.statusName
}}</text>
</view>
</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/helper/utils';
import PayTransferApi from '@/sheep/api/pay/transfer';
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: 8,
},
loadStatus: '',
price: undefined,
showModal: false,
});
const tabMaps = [
{
name: '分佣',
value: '1',
},
{
name: '提现',
value: '2',
},
];
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 (state.currentTab === 0
? BrokerageApi.getBrokerageRecordPage({
pageSize: state.pagination.pageSize,
pageNo: state.pagination.pageNo,
'createTime[0]': state.date[0] + ' 00:00:00',
'createTime[1]': state.date[1] + ' 23:59:59',
})
: BrokerageApi.getBrokerageWithdrawPage({
pageSize: state.pagination.pageSize,
pageNo: state.pagination.pageNo,
'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;
}
// 微信场景下:用户确认收款
// 可见 https://pay.weixin.qq.com/doc/v3/merchant/4012716430 文档
async function onRequestMerchantTransfer(item) {
const requestMerchantTransfer = sheep.$platform.useProvider()
? sheep.$platform.useProvider().requestMerchantTransfer
: undefined;
if (!requestMerchantTransfer) {
sheep.$helper.toast('仅微信平台支持该功能');
return;
}
// 获取提现详情
const { code, data } = await BrokerageApi.getBrokerageWithdraw(item.id);
if (code !== 0) {
return;
}
if (data.status === 11) {
sheep.$helper.toast('该提现单已确认收款');
item.status = 11;
return;
}
if (!data.transferChannelMchId || !data.transferChannelPackageInfo) {
sheep.$helper.toast('提现信息异常,请稍后再试');
return;
}
// 调用微信确认收款
const payTransferId = data.payTransferId;
await requestMerchantTransfer(
data.transferChannelMchId,
data.transferChannelPackageInfo,
async (res) => {
if (res.result !== 'success') {
sheep.$helper.toast(res.errMsg);
return;
}
// 同步转账单状态
try {
const syncTransferResult = await PayTransferApi.syncTransfer(payTransferId);
console.log('syncTransferResult 结果', syncTransferResult);
} catch (e) {
console.error('syncTransferResult 异常', e);
}
// 查询提现单最新状态
const { data } = await BrokerageApi.getBrokerageWithdraw(item.id);
if (data && data.status !== 11) {
sheep.$helper.toast('确认收款成功,但数据存在延迟,请以实际【微信支付】到账为准');
return;
}
sheep.$helper.toast('确认收款成功');
// 更新到列表中
item.status = 11;
},
);
}
onLoad(async (options) => {
state.today = dayjs().format('YYYY-MM-DD');
state.date = [state.today, state.today];
if (options.type === '2') {
// 切换到"提现" tab 下
state.currentTab = 1;
}
getLogList();
getAgentInfo();
});
onReachBottom(() => {
if (state.loadStatus === 'noMore') {
return;
}
state.pagination.pageNo++;
getLogList();
});
</script>
<style lang="scss" scoped>
// 钱包
.header-box {
background-color: $white;
padding: 30rpx;
.card-box {
width: 100%;
min-height: 300rpx;
padding: 40rpx;
background-size: 100% 100%;
border-radius: 30rpx;
overflow: hidden;
position: relative;
z-index: 1;
box-sizing: border-box;
&::after {
content: '';
display: block;
width: 100%;
height: 100%;
z-index: 2;
position: absolute;
top: 0;
left: 0;
background: v-bind(headerBg) no-repeat;
pointer-events: none;
}
.card-head {
color: $white;
font-size: 24rpx;
}
.ss-eye-icon {
font-size: 40rpx;
color: $white;
}
.money-num {
font-size: 40rpx;
line-height: normal;
font-weight: 500;
color: $white;
font-family: OPPOSANS;
}
.reduce-num {
font-size: 26rpx;
font-weight: 400;
color: $white;
}
.withdraw-btn {
width: 120rpx;
height: 60rpx;
line-height: 60rpx;
border-radius: 30px;
font-size: 24rpx;
font-weight: 500;
background-color: $white;
color: var(--ui-BG-Main);
}
.balance-btn {
width: 120rpx;
height: 60rpx;
line-height: 60rpx;
border-radius: 30px;
font-size: 24rpx;
font-weight: 500;
color: $white;
border: 1px solid $white;
}
}
}
.loading-money {
margin-top: 56rpx;
.loading-money-title {
font-size: 24rpx;
font-weight: 400;
color: #ffffff;
line-height: normal;
margin-bottom: 30rpx;
}
.loading-money-num {
font-size: 30rpx;
font-family: OPPOSANS;
font-weight: 500;
color: #fefefe;
}
}
// 筛选
.filter-box {
height: 120rpx;
padding: 0 30rpx;
background-color: $bg-page;
.total-box {
font-size: 24rpx;
font-weight: 500;
color: $dark-9;
}
.date-btn {
background-color: $white;
line-height: 54rpx;
border-radius: 27rpx;
padding: 0 20rpx;
font-size: 24rpx;
font-weight: 500;
color: $dark-6;
.ss-seldate-icon {
font-size: 50rpx;
color: $dark-9;
}
}
}
// tab
.wallet-tab-card {
.tab-item {
height: 80rpx;
position: relative;
.tab-title {
font-size: 30rpx;
}
.cur-tab-title {
font-weight: $font-weight-bold;
}
.tab-line {
width: 60rpx;
height: 6rpx;
border-radius: 6rpx;
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: 2rpx;
background-color: var(--ui-BG-Main);
}
}
}
// 钱包记录
.wallet-list {
padding: 30rpx;
background-color: #ffff;
.head-img {
width: 70rpx;
height: 70rpx;
border-radius: 50%;
background: $gray-c;
}
.list-content {
justify-content: space-between;
align-items: flex-start;
flex: 1;
.title {
font-size: 28rpx;
color: $dark-3;
width: 400rpx;
}
.time {
color: $gray-c;
font-size: 22rpx;
}
}
.money {
font-size: 28rpx;
font-weight: bold;
font-family: OPPOSANS;
.add {
color: var(--ui-BG-Main);
}
.minus {
color: $dark-3;
}
}
.confirm-btn {
font-size: 22rpx;
color: var(--ui-BG-Main);
background: rgba(var(--ui-BG-Main-rgb), 0.1);
padding: 4rpx 16rpx;
margin: 0;
line-height: 1.4;
border-radius: 20rpx;
border: 1px solid var(--ui-BG-Main);
}
}
.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;
}
}
.status {
font-size: 22rpx;
&.status-0 {
color: #ff9900;
}
&.status-1 {
color: #19be6b;
}
&.status-2 {
color: #fa3534;
}
}
</style>
+486
View File
@@ -0,0 +1,486 @@
<!-- 分佣提现 -->
<template>
<s-layout title="申请提现" class="withdraw-wrap" navbar="inner">
<view class="page-bg"></view>
<view
class="wallet-num-box ss-flex ss-col-center ss-row-between"
:style="[
{
marginTop: '-' + Number(statusBarHeight + 88) + 'rpx',
paddingTop: Number(statusBarHeight + 108) + 'rpx',
},
]"
>
<view class="">
<view class="num-title">可提现金额</view>
<view class="wallet-num">{{ fen2yuan(state.brokerageInfo.brokeragePrice) }}</view>
</view>
<button
class="ss-reset-button log-btn"
@tap="sheep.$router.go('/pages/commission/wallet', { type: 2 })"
>
提现记录
</button>
</view>
<!-- 提现输入卡片-->
<view class="draw-card">
<view class="bank-box ss-flex ss-col-center ss-row-between ss-m-b-30">
<view class="name">提现至</view>
<view class="bank-list ss-flex ss-col-center" @tap="onAccountSelect(true)">
<view v-if="!state.accountInfo.type" class="empty-text">请选择提现方式</view>
<view v-if="state.accountInfo.type === '1'" class="empty-text">钱包余额</view>
<view v-if="state.accountInfo.type === '2'" class="empty-text">银行卡转账</view>
<view v-if="state.accountInfo.type === '3'" class="empty-text">微信账户</view>
<view v-if="state.accountInfo.type === '4'" class="empty-text">支付宝账户</view>
<view v-if="state.accountInfo.type === '5'" 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', '6'].includes(state.accountInfo.type)">
提现账号
</view>
<view
class="input-box ss-flex ss-col-center border-bottom"
v-show="['2', '6'].includes(state.accountInfo.type)"
>
<view class="unit" />
<uni-easyinput
:inputBorder="false"
class="ss-flex-1 ss-p-l-10"
v-model="state.accountInfo.userAccount"
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.qrCodeUrl"
fileMediatype="image"
limit="1"
mode="grid"
:imageStyles="{ width: '168rpx', height: '168rpx' }"
@success="(payload) => (state.accountInfo.qrCodeUrl = payload.tempFilePaths[0])"
/>
</view>
</view>
<!-- 持卡人姓名 -->
<view class="card-title" v-show="['2', '5', '6'].includes(state.accountInfo.type)">
收款真名
</view>
<view
class="input-box ss-flex ss-col-center border-bottom"
v-show="['2', '5', '6'].includes(state.accountInfo.type)"
>
<view class="unit" />
<uni-easyinput
:inputBorder="false"
class="ss-flex-1 ss-p-l-10"
v-model="state.accountInfo.userName"
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 { onBeforeMount, reactive } 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';
import SLayout from '@/sheep/components/s-layout/s-layout.vue';
import { getWeixinPayChannelCode, goBindWeixin } from '@/sheep/platform/pay';
const headerBg = sheep.$url.css('/static/img/shop/user/withdraw_bg.png');
const statusBarHeight = sheep.$platform.device.statusBarHeight * 2;
const state = reactive({
accountInfo: {
// 提现表单
type: undefined,
userAccount: undefined,
userName: undefined,
qrCodeUrl: 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 () => {
// 参数校验
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 openid;
if (state.accountInfo.type === '5') {
openid = await sheep.$platform.useProvider('wechat').getOpenid();
// 如果获取不到 openid,微信无法发起支付,此时需要引导
if (!openid) {
goBindWeixin();
return;
}
}
// 提交请求
const data = {
...state.accountInfo,
price: state.accountInfo.price * 100,
};
if (state.accountInfo.type === '5') {
data.userAccount = openid;
data.transferChannelCode = getWeixinPayChannelCode();
} else if (state.accountInfo.type === '6' || state.accountInfo.type === '2') {
delete data.transferChannelCode;
} else {
delete data.userAccount;
delete data.transferChannelCode;
}
let { code } = await BrokerageApi.createBrokerageWithdraw(data);
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>
+81
View File
@@ -0,0 +1,81 @@
<template>
<view class="content-container">
<span v-for="(part, index) in formattedContent" :key="index"
@click="handleClick(part)"
:class="{'highlight-number': part.isNumber, 'phone-number': part.isPhone}">
{{ part.text }}
</span>
</view>
</template>
<script>
export default {
name: 'HighlightNumber',
props: {
content: {
type: String,
required: true
}
},
computed: {
formattedContent() {
const phoneRegex = /(1[3-9]\d{9})/g;
const numberRegex = /(\d+)/g;
let text = this.content;
let result = [];
let match;
// Step 1: 提取手机号
while ((match = phoneRegex.exec(text)) !== null) {
if (match.index > 0) {
const before = text.slice(0, match.index);
result.push(...this.splitAndPush(before, false, false));
}
result.push({ text: match[0], isNumber: true, isPhone: true });
text = text.slice(match.index + match[0].length);
}
// Step 2: 提取普通数字
while ((match = numberRegex.exec(text)) !== null) {
if (match.index > 0) {
const before = text.slice(0, match.index);
result.push(...this.splitAndPush(before, false, false));
}
result.push({ text: match[0], isNumber: true });
text = text.slice(match.index + match[0].length);
}
// Step 3: 添加剩余文本
if (text.length > 0) {
result.push(...this.splitAndPush(text, false, false));
}
return result;
}
},
methods: {
splitAndPush(str, isNumber = false, isPhone = false) {
return str.split('').map(char => ({ text: char, isNumber, isPhone }));
},
handleClick(part) {
if (part.isPhone) {
this.$emit('phone-click', { phoneNumber: part.text });
} else if (part.isNumber) {
this.$emit('number-click', { number: part.text });
}
}
}
};
</script>
<style scoped>
.highlight-number {
color: #ff5722;
font-weight: bold;
}
.phone-number {
color: #007AFF;
text-decoration: underline;
}
</style>
+390
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/helper/utils';
const state = reactive({
id: 0, // 优惠劵模版编号 templateId
couponId: 0, // 用户优惠劵编号 couponId
coupon: {}, // 优惠劵信息
pagination: {
list: [],
total: 0,
pageNo: 1,
pageSize: 8,
},
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
View File
@@ -0,0 +1,222 @@
<!-- 优惠券中心 -->
<template>
<s-layout :bgStyle="{ color: '#f2f2f2' }" 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/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/helper/utils';
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
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 } 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 = payload.tempFilePaths;
}
onLoad(async (options) => {
if (!options.id) {
sheep.$helper.toast(`缺少订单信息,请检查`);
return;
}
state.id = options.id;
const { code, data } = await OrderApi.getOrderDetail(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>
+168
View File
@@ -0,0 +1,168 @@
<!-- 商品评论的分页 -->
<template>
<s-layout title="全部评论">
<su-tabs
:list="state.type"
:scrollable="false"
@change="onTabsChange"
:current="state.currentTab"
/>
<!-- 评论列表 -->
<view class="ss-m-t-20">
<view class="list-item" v-for="item in state.pagination.list" :key="item">
<comment-item :item="item" />
</view>
</view>
<s-empty v-if="state.pagination.total === 0" text="暂无数据" icon="/static/data-empty.png" />
<!-- 下拉 -->
<uni-load-more
icon-type="auto"
v-if="state.pagination.total > 0"
:status="state.loadStatus"
:content-text="{
contentdown: '上拉加载更多',
}"
@tap="loadMore"
/>
</s-layout>
</template>
<script setup>
import CommentApi from '@/sheep/api/product/comment';
import { onLoad, onReachBottom } from '@dcloudio/uni-app';
import { reactive } from 'vue';
import _ from 'lodash-es';
import commentItem from '../components/detail/comment-item.vue';
const state = reactive({
id: 0, // 商品 SPU 编号
type: [
{ type: 0, name: '全部' },
{ type: 1, name: '好评' },
{ type: 2, name: '中评' },
{ type: 3, name: '差评' },
],
currentTab: 0, // 选中的 TAB
loadStatus: '',
pagination: {
list: [],
total: 0,
pageNo: 1,
pageSize: 8,
},
});
// 切换选项卡
function onTabsChange(e) {
state.currentTab = e.index;
// 加载列表
state.pagination.pageNo = 1;
state.pagination.list = [];
state.pagination.total = 0;
getList();
}
async function getList() {
// 加载列表
state.loadStatus = 'loading';
let res = await CommentApi.getCommentPage(
state.id,
state.pagination.pageNo,
state.pagination.pageSize,
state.type[state.currentTab].type,
);
if (res.code !== 0) {
return;
}
// 合并列表
state.pagination.list = _.concat(state.pagination.list, res.data.list);
state.pagination.total = res.data.total;
state.loadStatus = state.pagination.list.length < state.pagination.total ? 'more' : 'noMore';
}
// 加载更多
function loadMore() {
if (state.loadStatus === 'noMore') {
return;
}
state.pagination.pageNo++;
getList();
}
onLoad((options) => {
state.id = options.id;
getList();
});
// 上拉加载更多
onReachBottom(() => {
loadMore();
});
</script>
<style lang="scss" scoped>
.list-item {
padding: 32rpx 30rpx 20rpx 20rpx;
background: #fff;
.avatar {
width: 52rpx;
height: 52rpx;
border-radius: 50%;
}
.nickname {
font-size: 26rpx;
font-weight: 500;
color: #999999;
}
.create-time {
font-size: 24rpx;
font-weight: 500;
color: #c4c4c4;
}
.content-title {
font-size: 26rpx;
font-weight: 400;
color: #666666;
line-height: 42rpx;
}
.content-img {
width: 174rpx;
height: 174rpx;
}
.cicon-info-o {
font-size: 26rpx;
color: #c4c4c4;
}
.foot-title {
font-size: 24rpx;
font-weight: 500;
color: #999999;
}
}
.btn-box {
width: 100%;
height: 120rpx;
background: #fff;
border-top: 2rpx solid #eee;
}
.tab-btn {
width: 130rpx;
height: 62rpx;
background: #eeeeee;
border-radius: 31rpx;
font-size: 28rpx;
font-weight: 400;
color: #999999;
border: 1px solid #e5e5e5;
margin-right: 10rpx;
}
</style>
@@ -0,0 +1,94 @@
<!-- 商品评论项 -->
<template>
<view>
<!-- 用户评论 -->
<view class="user ss-flex ss-m-b-14">
<view class="ss-m-r-20 ss-flex">
<image class="avatar" :src="item.userAvatar"></image>
</view>
<view class="nickname ss-m-r-20">{{ item.userNickname }}</view>
<view class="">
<uni-rate :readonly="true" v-model="item.scores" size="18" />
</view>
</view>
<view class="content"> {{ item.content }} </view>
<view class="ss-m-t-24" v-if="item.picUrls?.length">
<scroll-view class="scroll-box" scroll-x scroll-anchoring>
<view class="ss-flex">
<view v-for="(picUrl, index) in item.picUrls" :key="picUrl" class="ss-m-r-10">
<su-image
class="content-img"
isPreview
:previewList="item.picUrls"
:current="index"
:src="picUrl"
:height="120"
:width="120"
mode="aspectFill"
/>
</view>
</view>
</scroll-view>
</view>
<!-- 商家回复 -->
<view class="ss-m-t-20 reply-box" v-if="item.replyTime">
<view class="reply-title">商家回复</view>
<view class="reply-content">{{ item.replyContent }}</view>
</view>
</view>
</template>
<script setup>
const props = defineProps({
item: {
type: Object,
default() {},
},
});
</script>
<style lang="scss" scoped>
.avatar {
width: 52rpx;
height: 52rpx;
border-radius: 50%;
}
.nickname {
font-size: 26rpx;
font-weight: 500;
color: #999999;
}
.content {
width: 636rpx;
font-size: 26rpx;
font-weight: 400;
color: #333333;
}
.reply-box {
position: relative;
background: #f8f8f8;
border-radius: 8rpx;
padding: 16rpx;
}
.reply-title {
position: absolute;
left: 16rpx;
top: 16rpx;
font-weight: 400;
font-size: 26rpx;
line-height: 40rpx;
color: #333333;
}
.reply-content {
text-indent: 128rpx;
font-weight: 400;
font-size: 26rpx;
line-height: 40rpx;
color: #333333;
}
</style>
@@ -0,0 +1,99 @@
<template>
<su-fixed bottom placeholder :val="44">
<view>
<view v-for="activity in props.activityList" :key="activity.id">
<view
class="activity-box ss-p-x-38 ss-flex ss-row-between ss-col-center"
:class="activity.type === 1 ? 'seckill-box' : 'groupon-box'"
>
<view class="activity-title ss-flex">
<view class="ss-m-r-16">
<image
v-if="activity.type === 1"
:src="sheep.$url.static('/static/img/shop/goods/seckill-icon.png')"
class="activity-icon"
/>
<image
v-else-if="activity.type === 3"
:src="sheep.$url.static('/static/img/shop/goods/groupon-icon.png')"
class="activity-icon"
/>
</view>
<view>该商品正在参与{{ activity.name }}活动</view>
</view>
<button class="ss-reset-button activity-go" @tap="onActivity(activity)"> GO </button>
</view>
</view>
</view>
</su-fixed>
</template>
<script setup>
import sheep from '@/sheep';
const seckillBg = sheep.$url.css('/static/img/shop/goods/seckill-tip-bg.png');
const grouponBg = sheep.$url.css('/static/img/shop/goods/groupon-tip-bg.png');
const props = defineProps({
activityList: {
type: Array,
default() {
return [];
},
},
});
function onActivity(activity) {
const type = activity.type;
const typePath = type === 1 ? 'seckill' : type === 3 ? 'groupon' : undefined;
if (!typePath) {
return;
}
sheep.$router.go(`/pages/goods/${typePath}`, {
id: activity.id,
});
}
</script>
<style lang="scss" scoped>
.activity-box {
width: 100%;
height: 80rpx;
box-sizing: border-box;
margin-bottom: 10rpx;
.activity-title {
font-size: 26rpx;
font-weight: 500;
color: #ffffff;
line-height: 42rpx;
.activity-icon {
width: 38rpx;
height: 38rpx;
}
}
.activity-go {
width: 70rpx;
height: 32rpx;
background: #ffffff;
border-radius: 16rpx;
font-weight: 500;
color: #ff6000;
font-size: 24rpx;
line-height: normal;
}
}
//秒杀卡片
.seckill-box {
background: v-bind(seckillBg) no-repeat;
background-size: 100% 100%;
}
.groupon-box {
background: v-bind(grouponBg) no-repeat;
background-size: 100% 100%;
}
</style>
@@ -0,0 +1,31 @@
<template>
<!-- SKU 选择的提示框 -->
<detail-cell label="选择" :value="value" />
</template>
<script setup>
import { computed } from 'vue';
import detailCell from './detail-cell.vue';
const props = defineProps({
modelValue: {
type: Array,
default() {
return [];
},
},
sku: {
type: Object
}
});
const value = computed(() => {
if (!props.sku?.id) {
return '请选择商品规格';
}
let str = '';
props.sku.properties.forEach(property => {
str += property.propertyName + ':' + property.valueName + ' ';
});
return str;
});
</script>
@@ -0,0 +1,60 @@
<!-- 商品详情cell 组件 -->
<template>
<view class="detail-cell-wrap ss-flex ss-col-center ss-row-between" @tap="onClick">
<view class="label-text">{{ label }}</view>
<view class="cell-content ss-line-1 ss-flex-1">{{ value }}</view>
<button class="ss-reset-button">
<text class="_icon-forward right-forwrad-icon"></text>
</button>
</view>
</template>
<script setup>
/**
* 详情 cell
*
*/
const props = defineProps({
label: {
type: String,
default: '',
},
value: {
type: String,
default: '',
},
});
const emits = defineEmits(['click']);
// 点击
const onClick = () => {
emits('click');
};
</script>
<style lang="scss" scoped>
.detail-cell-wrap {
padding: 10rpx 20rpx;
// min-height: 60rpx;
.label-text {
font-size: 28rpx;
font-weight: 500;
color: $dark-9;
margin-right: 38rpx;
}
.cell-content {
font-size: 28rpx;
font-weight: 500;
color: $dark-6;
}
.right-forwrad-icon {
font-size: 28rpx;
font-weight: 500;
color: $dark-9;
}
}
</style>
@@ -0,0 +1,106 @@
<!-- 商品评论的卡片 -->
<template>
<view class="detail-comment-card bg-white">
<view class="card-header ss-flex ss-col-center ss-row-between ss-p-b-30">
<view class="ss-flex ss-col-center">
<view class="line"></view>
<view class="title ss-m-l-20 ss-m-r-10">评价</view>
<view class="des">({{ state.total }})</view>
</view>
<view
class="ss-flex ss-col-center"
@tap="sheep.$router.go('/pages/goods/comment/list', { id: goodsId })"
v-if="state.commentList.length > 0"
>
<button class="ss-reset-button more-btn">查看全部</button>
<text class="cicon-forward" />
</view>
</view>
<!-- 评论列表 -->
<view class="card-content">
<view class="comment-box ss-p-y-30" v-for="item in state.commentList" :key="item.id">
<comment-item :item="item" />
</view>
<s-empty
v-if="state.commentList.length === 0"
paddingTop="0"
icon="/static/comment-empty.png"
text="期待您的第一个评价"
/>
</view>
</view>
</template>
<script setup>
import { reactive, onBeforeMount } from 'vue';
import sheep from '@/sheep';
import CommentApi from '@/sheep/api/product/comment';
import commentItem from './comment-item.vue';
const props = defineProps({
goodsId: {
type: [Number, String],
default: 0,
},
});
const state = reactive({
commentList: [], // 评论列表,只展示最近的 3 条
total: 0, // 总评论数
});
async function getComment(id) {
const { data } = await CommentApi.getCommentPage(id, 1, 3, 0);
state.commentList = data.list;
state.total = data.total;
}
onBeforeMount(() => {
getComment(props.goodsId);
});
</script>
<style lang="scss" scoped>
.detail-comment-card {
margin: 0 20rpx 20rpx 20rpx;
padding: 20rpx 20rpx 0 20rpx;
.card-header {
.line {
width: 6rpx;
height: 30rpx;
background: linear-gradient(180deg, var(--ui-BG-Main) 0%, var(--ui-BG-Main-gradient) 100%);
border-radius: 3rpx;
}
.title {
font-size: 30rpx;
font-weight: bold;
line-height: normal;
}
.des {
font-size: 24rpx;
color: $dark-9;
}
.more-btn {
font-size: 24rpx;
color: var(--ui-BG-Main);
line-height: normal;
}
.cicon-forward {
font-size: 24rpx;
line-height: normal;
color: var(--ui-BG-Main);
margin-top: 4rpx;
}
}
}
.comment-box {
border-bottom: 2rpx solid #eeeeee;
&:last-child {
border: none;
}
}
</style>
@@ -0,0 +1,54 @@
<template>
<view class="detail-content-card bg-white ss-m-x-20 ss-p-t-20">
<view class="card-header ss-flex ss-col-center ss-m-b-30 ss-m-l-20">
<view class="line"></view>
<view class="title ss-m-l-20 ss-m-r-20">详情</view>
</view>
<view class="card-content">
<mp-html :content="content"></mp-html>
</view>
</view>
</template>
<script setup>
const props = defineProps({
content: {
type: String,
default: '',
},
});
</script>
<style lang="scss" scoped>
.detail-content-card {
.card-header {
.line {
width: 6rpx;
height: 30rpx;
background: linear-gradient(180deg, var(--ui-BG-Main) 0%, var(--ui-BG-Main-gradient) 100%);
border-radius: 3rpx;
}
.title {
font-size: 30rpx;
font-weight: bold;
}
.des {
font-size: 24rpx;
color: $dark-9;
}
.more-btn {
font-size: 24rpx;
color: var(--ui-BG-Main);
}
}
}
:deep() {
image {
display: block;
}
}
</style>
@@ -0,0 +1,257 @@
<!-- 商品详情商品/评价/详情的 nav -->
<template>
<su-fixed alway :bgStyles="{ background: '#fff' }" :val="0" noNav opacity :placeholder="false">
<su-status-bar />
<view
class="ui-bar ss-flex ss-col-center ss-row-between ss-p-x-20"
:style="[{ height: sys_navBar - sys_statusBar + 'px' }]"
>
<!-- -->
<view class="icon-box ss-flex">
<view class="icon-button icon-button-left ss-flex ss-row-center" @tap="onClickLeft">
<text class="sicon-back" v-if="hasHistory" />
<text class="sicon-home" v-else />
</view>
<view class="line"></view>
<view class="icon-button icon-button-right ss-flex ss-row-center" @tap="onClickRight">
<text class="sicon-more" />
</view>
</view>
<!-- -->
<view class="detail-tab-card ss-flex-1" :style="[{ opacity: state.tabOpacityVal }]">
<view class="tab-box ss-flex ss-col-center ss-row-around">
<view
class="tab-item ss-flex-1 ss-flex ss-row-center ss-col-center"
v-for="item in state.tabList"
:key="item.value"
@tap="onTab(item)"
>
<view class="tab-title" :class="state.curTab === item.value ? 'cur-tab-title' : ''">
{{ item.label }}
</view>
<view v-show="state.curTab === item.value" class="tab-line"></view>
</view>
</view>
</view>
<!-- #ifdef MP -->
<view :style="[capsuleStyle]"></view>
<!-- #endif -->
</view>
</su-fixed>
</template>
<script setup>
import { reactive } from 'vue';
import { onPageScroll } from '@dcloudio/uni-app';
import sheep from '@/sheep';
import throttle from '@/sheep/helper/throttle.js';
import { showMenuTools } from '@/sheep/hooks/useModal';
const sys_statusBar = sheep.$platform.device.statusBarHeight;
const sys_navBar = sheep.$platform.navbar;
const capsuleStyle = {
width: sheep.$platform.capsule.width + 'px',
height: sheep.$platform.capsule.height + 'px',
};
const state = reactive({
tabOpacityVal: 0,
curTab: 'goods',
tabList: [
{
label: '商品',
value: 'goods',
to: 'detail-swiper-selector',
},
{
label: '评价',
value: 'comment',
to: 'detail-comment-selector',
},
{
label: '详情',
value: 'detail',
to: 'detail-content-selector',
},
],
});
const emits = defineEmits(['clickLeft']);
const hasHistory = sheep.$router.hasHistory();
function onClickLeft() {
if (hasHistory) {
sheep.$router.back();
} else {
sheep.$router.go('/pages/index/index');
}
emits('clickLeft');
}
function onClickRight() {
showMenuTools();
}
let commentCard = {
top: 0,
bottom: 0,
};
function getCommentCardNode() {
return new Promise((res, rej) => {
uni
.createSelectorQuery()
.select('.detail-comment-selector')
.boundingClientRect((data) => {
if (data) {
commentCard.top = data.top;
commentCard.bottom = data.top + data.height;
res(data);
} else {
res(null);
}
})
.exec();
});
}
function onTab(tab) {
let scrollTop = 0;
if (tab.value === 'comment') {
scrollTop = commentCard.top - sys_navBar + 1;
} else if (tab.value === 'detail') {
scrollTop = commentCard.bottom - sys_navBar + 1;
}
uni.pageScrollTo({
scrollTop,
duration: 200,
});
}
onPageScroll((e) => {
state.tabOpacityVal = e.scrollTop > sheep.$platform.navbar ? 1 : e.scrollTop * 0.01;
if (commentCard.top === 0) {
throttle(() => {
getCommentCardNode();
}, 50);
}
if (e.scrollTop < commentCard.top - sys_navBar) {
state.curTab = 'goods';
} else if (
e.scrollTop >= commentCard.top - sys_navBar &&
e.scrollTop <= commentCard.bottom - sys_navBar
) {
state.curTab = 'comment';
} else {
state.curTab = 'detail';
}
});
</script>
<style lang="scss" scoped>
.icon-box {
box-shadow: 0px 0px 4rpx rgba(51, 51, 51, 0.08), 0px 4rpx 6rpx 2rpx rgba(102, 102, 102, 0.12);
border-radius: 30rpx;
width: 134rpx;
height: 56rpx;
margin-left: 8rpx;
border: 1px solid rgba(#fff, 0.4);
.line {
width: 2rpx;
height: 24rpx;
background: #e5e5e7;
}
.sicon-back {
font-size: 32rpx;
color: #000;
}
.sicon-home {
font-size: 32rpx;
color: #000;
}
.sicon-more {
font-size: 32rpx;
color: #000;
}
.icon-button {
width: 67rpx;
height: 56rpx;
&-left:hover {
background: rgba(0, 0, 0, 0.16);
border-radius: 30rpx 0px 0px 30rpx;
}
&-right:hover {
background: rgba(0, 0, 0, 0.16);
border-radius: 0px 30rpx 30rpx 0px;
}
}
}
.left-box {
position: relative;
width: 60rpx;
height: 60rpx;
display: flex;
justify-content: center;
align-items: center;
.circle {
position: absolute;
left: 0;
top: 0;
width: 60rpx;
height: 60rpx;
background: rgba(#fff, 0.6);
border: 1rpx solid #ebebeb;
border-radius: 50%;
box-sizing: border-box;
z-index: -1;
}
}
.right {
position: relative;
width: 60rpx;
height: 60rpx;
display: flex;
justify-content: center;
align-items: center;
.circle {
position: absolute;
left: 0;
top: 0;
width: 60rpx;
height: 60rpx;
background: rgba(#ffffff, 0.6);
border: 1rpx solid #ebebeb;
box-sizing: border-box;
border-radius: 50%;
z-index: -1;
}
}
.detail-tab-card {
width: 50%;
.tab-item {
height: 80rpx;
position: relative;
z-index: 11;
.tab-title {
font-size: 30rpx;
}
.cur-tab-title {
font-weight: $font-weight-bold;
}
.tab-line {
width: 60rpx;
height: 6rpx;
border-radius: 6rpx;
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: 10rpx;
background-color: var(--ui-BG-Main);
z-index: 12;
}
}
}
</style>
@@ -0,0 +1,40 @@
<!-- 秒杀商品抢购进度 -->
<template>
<view class="ss-flex ss-col-center">
<view class="progress-title ss-m-r-10"> 已抢{{ percent }}% </view>
<view class="progress-box ss-flex ss-col-center">
<view class="progerss-active" :style="{ width: percent < 10 ? '10%' : percent + '%' }">
</view>
</view>
</view>
</template>
<script setup>
const props = defineProps({
percent: {
type: Number,
default: 0,
},
});
</script>
<style lang="scss" scoped>
.progress-title {
font-size: 20rpx;
font-weight: 500;
color: #ffffff;
}
.progress-box {
width: 168rpx;
height: 18rpx;
background: #f6f6f6;
border-radius: 9rpx;
}
.progerss-active {
height: 24rpx;
background: linear-gradient(86deg, #f60600, #d00500);
border-radius: 12rpx;
}
</style>
@@ -0,0 +1,177 @@
<template>
<view
class="skeleton-wrap"
:class="['theme-' + sys.mode, 'main-' + sys.theme, 'font-' + sys.fontSize]"
>
<view class="skeleton-banner"></view>
<view class="container-box">
<view class="container-box-strip title ss-m-b-58"></view>
<view class="container-box-strip ss-m-b-20"></view>
<view class="container-box-strip ss-m-b-20"></view>
<view class="container-box-strip w-364"></view>
</view>
<view class="container-box">
<view class="ss-flex ss-row-between ss-m-b-34">
<view class="container-box-strip w-380"></view>
<view class="circle"></view>
</view>
<view class="ss-flex ss-row-between ss-m-b-34">
<view class="container-box-strip w-556"></view>
<view class="circle"></view>
</view>
<view class="ss-flex ss-row-between">
<view class="container-box-strip w-556"></view>
<view class="circle"></view>
</view>
</view>
<view class="container-box">
<view class="container-box-strip w-198 ss-m-b-42"></view>
<view class="ss-flex">
<view class="circle ss-m-r-12"></view>
<view class="container-box-strip w-252"></view>
</view>
</view>
<su-fixed bottom placeholder bg="bg-white">
<view class="ui-tabbar-box">
<view class="foot ss-flex ss-col-center">
<view class="ss-m-r-54 ss-m-l-32">
<view class="rec ss-m-b-8"></view>
<view class="oval"></view>
</view>
<view class="ss-m-r-54">
<view class="rec ss-m-b-8"></view>
<view class="oval"></view>
</view>
<view class="ss-m-r-50">
<view class="rec ss-m-b-8"></view>
<view class="oval"></view>
</view>
<button class="ss-reset-button add-btn ui-Shadow-Main"></button>
<button class="ss-reset-button buy-btn ui-Shadow-Main"></button>
</view>
</view>
</su-fixed>
</view>
</template>
<script setup>
import { computed } from 'vue';
import sheep from '@/sheep';
const sys = computed(() => sheep.$store('sys'));
</script>
<style lang="scss" scoped>
@keyframes loading {
0% {
opacity: 0.5;
}
50% {
opacity: 1;
}
100% {
opacity: 0.5;
}
}
.skeleton-wrap {
width: 100%;
height: 100vh;
position: relative;
.skeleton-banner {
width: 100%;
height: calc(100vh - 882rpx);
background: #f4f4f4;
}
.container-box {
padding: 24rpx 18rpx;
margin: 14rpx 20rpx;
background: var(--ui-BG);
animation: loading 1.4s ease infinite;
.container-box-strip {
height: 40rpx;
background: #f3f3f1;
border-radius: 20rpx;
}
.title {
width: 470rpx;
height: 50rpx;
border-radius: 25rpx;
}
.w-364 {
width: 364rpx;
}
.w-380 {
width: 380rpx;
}
.w-556 {
width: 556rpx;
}
.w-198 {
width: 198rpx;
}
.w-252 {
width: 252rpx;
}
.circle {
width: 40rpx;
height: 40rpx;
background: #f3f3f1;
border-radius: 50%;
}
}
.ui-tabbar-box {
box-shadow: 0px -6px 10px 0px rgba(51, 51, 51, 0.2);
}
.foot {
height: 100rpx;
background: var(--ui-BG);
.rec {
width: 38rpx;
height: 38rpx;
background: #f3f3f1;
border-radius: 8rpx;
}
.oval {
width: 38rpx;
height: 22rpx;
background: #f3f3f1;
border-radius: 11rpx;
}
.add-btn {
width: 214rpx;
height: 72rpx;
font-weight: 500;
font-size: 28rpx;
border-radius: 40rpx 0 0 40rpx;
background-color: var(--ui-BG-Main-light);
color: var(--ui-BG-Main);
}
.buy-btn {
width: 214rpx;
height: 72rpx;
font-weight: 500;
font-size: 28rpx;
border-radius: 0 40rpx 40rpx 0;
background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
color: $white;
}
}
}
</style>
@@ -0,0 +1,169 @@
<!-- 商品详情的底部导航 -->
<template>
<su-fixed bottom placeholder bg="bg-white">
<view class="ui-tabbar-box">
<view class="ui-tabbar ss-flex ss-col-center ss-row-between">
<view
v-if="collectIcon"
class="detail-tabbar-item ss-flex ss-flex-col ss-row-center ss-col-center"
@tap="onFavorite"
>
<block v-if="modelValue.favorite">
<image
class="item-icon"
:src="sheep.$url.static('/static/img/shop/goods/collect_1.gif')"
mode="aspectFit"
/>
<view class="item-title">已收藏</view>
</block>
<block v-else>
<image
class="item-icon"
:src="sheep.$url.static('/static/img/shop/goods/collect_0.png')"
mode="aspectFit"
/>
<view class="item-title">收藏</view>
</block>
</view>
<view
v-if="serviceIcon"
class="detail-tabbar-item ss-flex ss-flex-col ss-row-center ss-col-center"
@tap="onChat"
>
<image
class="item-icon"
:src="sheep.$url.static('/static/img/shop/goods/message.png')"
mode="aspectFit"
/>
<view class="item-title">客服</view>
</view>
<view
v-if="shareIcon"
class="detail-tabbar-item ss-flex ss-flex-col ss-row-center ss-col-center"
@tap="showShareModal"
>
<image
class="item-icon"
:src="sheep.$url.static('/static/img/shop/goods/share.png')"
mode="aspectFit"
/>
<view class="item-title">分享</view>
</view>
<slot></slot>
</view>
</view>
</su-fixed>
</template>
<script setup>
/**
*
* 底部导航
*
* @property {String} bg - 背景颜色Class
* @property {String} ui - 自定义样式Class
* @property {Boolean} noFixed - 是否定位
* @property {Boolean} topRadius - 上圆角
*/
import { reactive } from 'vue';
import sheep from '@/sheep';
import { showShareModal } from '@/sheep/hooks/useModal';
import FavoriteApi from '@/sheep/api/product/favorite';
// 数据
const state = reactive({});
// 接收参数
const props = defineProps({
modelValue: {
type: Object,
default() {},
},
bg: {
type: String,
default: 'bg-white',
},
bgStyles: {
type: Object,
default() {},
},
ui: {
type: String,
default: '',
},
noFixed: {
type: Boolean,
default: false,
},
topRadius: {
type: Number,
default: 0,
},
collectIcon: {
type: Boolean,
default: true,
},
serviceIcon: {
type: Boolean,
default: true,
},
shareIcon: {
type: Boolean,
default: true,
},
});
async function onFavorite() {
// 情况一:取消收藏
if (props.modelValue.favorite) {
const { code } = await FavoriteApi.deleteFavorite(props.modelValue.id);
if (code !== 0) {
return
}
sheep.$helper.toast('取消收藏');
props.modelValue.favorite = false;
// 情况二:添加收藏
} else {
const { code } = await FavoriteApi.createFavorite(props.modelValue.id);
if (code !== 0) {
return
}
sheep.$helper.toast('收藏成功');
props.modelValue.favorite = true;
}
}
const onChat = () => {
sheep.$router.go('/pages/chat/index', {
id: props.modelValue.id,
});
};
</script>
<style lang="scss" scoped>
.ui-tabbar-box {
box-shadow: 0px -6px 10px 0px rgba(51, 51, 51, 0.2);
}
.ui-tabbar {
display: flex;
height: 50px;
background: #fff;
.detail-tabbar-item {
width: 100rpx;
.item-icon {
width: 40rpx;
height: 40rpx;
}
.item-title {
font-size: 20rpx;
font-weight: 500;
line-height: 20rpx;
margin-top: 12rpx;
}
}
}
</style>
@@ -0,0 +1,140 @@
<!-- 拼团活动参团记录卡片 -->
<template>
<view v-if="state.list.length > 0" class="groupon-list detail-card ss-p-x-20">
<view class="join-activity ss-flex ss-row-between ss-m-t-30">
<view class="">已有{{ state.list.length }}人参与活动</view>
<text class="cicon-forward"></text>
</view>
<view
v-for="(record, index) in state.list"
@tap="sheep.$router.go('/pages/activity/groupon/detail', { id: record.id })"
:key="index"
class="ss-m-t-40 ss-flex ss-row-between border-bottom ss-p-b-30"
>
<view class="ss-flex ss-col-center">
<image :src="sheep.$url.cdn(record.avatar)" class="user-avatar"></image>
<view class="user-nickname ss-m-l-20 ss-line-1">{{ record.nickname }}</view>
</view>
<view class="ss-flex ss-col-center">
<view class="ss-flex-col ss-col-bottom ss-m-r-20">
<view class="title ss-flex ss-m-b-14">
还差
<view class="num">{{ record.userSize - record.userCount }}</view>
成团
</view>
<view class="end-time">{{ endTime(record.expireTime) }}</view>
</view>
<view class="">
<button class="ss-reset-button go-btn" @tap.stop="onJoinGroupon(record)"> 去参团 </button>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { onMounted, reactive } from 'vue';
import sheep from '@/sheep';
import { useDurationTime } from '@/sheep/hooks/useGoods';
import CombinationApi from '@/sheep/api/promotion/combination';
const props = defineProps({
modelValue: {
type: Object,
default() {},
},
});
const state = reactive({
list: [],
});
// 去参团
const emits = defineEmits(['join']);
function onJoinGroupon(record) {
emits('join', record);
}
// 结束时间或状态
function endTime(time) {
const durationTime = useDurationTime(time);
if (durationTime.ms <= 0) {
return '该团已解散';
}
let timeText = '剩余 ';
timeText += `${durationTime.h}`;
timeText += `${durationTime.m}`;
timeText += `${durationTime.s}`;
return timeText;
}
// 初始化
onMounted(async () => {
// 查询参团记录
// status = 0 表示未成团
const { data } = await CombinationApi.getHeadCombinationRecordList(props.modelValue.id, 0, 10);
state.list = data;
});
</script>
<style lang="scss" scoped>
.detail-card {
background-color: $white;
margin: 14rpx 20rpx;
border-radius: 10rpx;
overflow: hidden;
}
.groupon-list {
.join-activity {
font-size: 28rpx;
font-weight: 500;
color: #999999;
.cicon-forward {
font-weight: 400;
}
}
.user-avatar {
width: 60rpx;
height: 60rpx;
background: #ececec;
border-radius: 60rpx;
}
.user-nickname {
font-size: 28rpx;
font-weight: 500;
color: #333333;
width: 160rpx;
}
.title {
font-size: 24rpx;
font-weight: 500;
color: #666666;
.num {
color: #ff6000;
}
}
.end-time {
font-size: 24rpx;
font-weight: 500;
color: #999999;
}
.go-btn {
width: 140rpx;
height: 60rpx;
background: linear-gradient(90deg, #ff6000 0%, #fe832a 100%);
border-radius: 30rpx;
color: #fff;
font-weight: 500;
font-size: 26rpx;
line-height: normal;
}
}
</style>
@@ -0,0 +1,103 @@
<!-- 页面暂时没用到 -->
<template>
<view class="list-goods-card ss-flex-col" @tap="onClick">
<view class="md-img-box">
<image class="goods-img md-img-box" :src="sheep.$url.cdn(img)" mode="aspectFill"></image>
</view>
<view class="md-goods-content ss-flex-col ss-row-around">
<view class="md-goods-title ss-line-2 ss-m-x-20 ss-m-t-6 ss-m-b-16">{{ title }}</view>
<view class="md-goods-subtitle ss-line-1 ss-p-y-10 ss-p-20">{{ subTitle }}</view>
<view class="ss-flex ss-col-center ss-row-between ss-m-b-16 ss-m-x-20">
<view class="md-goods-price text-price">{{ price }}</view>
<view class="goods-origin-price text-price">{{ originPrice }}</view>
<view class="sales-text">已售{{ sales }}</view>
</view>
</view>
</view>
</template>
<script setup>
import sheep from '@/sheep';
import { onLoad } from '@dcloudio/uni-app';
import { computed, reactive } from 'vue';
const props = defineProps({
img: {
type: String,
default: '',
},
subTitle: {
type: String,
default: '',
},
title: {
type: String,
default: '',
},
price: {
type: [String, Number],
default: '',
},
originPrice: {
type: [String, Number],
default: '',
},
sales: {
type: [String, Number],
default: '',
},
});
const emits = defineEmits(['click']);
const onClick = () => {
emits('click');
};
</script>
<style lang="scss" scoped>
.goods-img {
width: 100%;
height: 100%;
background-color: #f5f5f5;
}
.sales-text {
font-size: 20rpx;
color: #c4c4c4;
}
.goods-origin-price {
font-size: 20rpx;
color: #c4c4c4;
text-decoration: line-through;
}
.list-goods-card {
overflow: hidden;
width: 344rpx;
position: relative;
z-index: 1;
background-color: $white;
box-shadow: 0 0 20rpx 4rpx rgba(199, 199, 199, 0.22);
border-radius: 20rpx;
.md-img-box {
width: 344rpx;
height: 344rpx;
}
.md-goods-title {
font-size: 26rpx;
color: #333;
}
.md-goods-subtitle {
background-color: var(--ui-BG-Main-tag);
color: var(--ui-BG-Main);
font-size: 20rpx;
}
.md-goods-price {
font-size: 30rpx;
color: $red;
}
}
</style>
@@ -0,0 +1,93 @@
<!-- 页面暂时没用到 -->
<template>
<su-fixed
alway
:bgStyles="{ background: '#fff' }"
:val="0"
noNav
:opacity="false"
placeholder
index="10090"
>
<su-status-bar />
<view
class="ui-bar ss-flex ss-col-center ss-row-between ss-p-x-20"
:style="[{ height: sys_navBar - sys_statusBar + 'px' }]"
>
<!-- -->
<view class="left-box">
<text
class="_icon-back back-icon"
@tap="toBack"
:style="[{ color: state.iconColor }]"
></text>
</view>
<!-- -->
<uni-search-bar
class="ss-flex-1"
radius="33"
:placeholder="placeholder"
cancelButton="none"
:focus="true"
v-model="state.searchVal"
@confirm="onSearch"
/>
<!-- -->
<view class="right">
<text class="sicon-more" :style="[{ color: state.iconColor }]" @tap="showMenuTools" />
</view>
<!-- #ifdef MP -->
<view :style="[capsuleStyle]"></view>
<!-- #endif -->
</view>
</su-fixed>
</template>
<script setup>
import { reactive } from 'vue';
import sheep from '@/sheep';
import { showMenuTools } from '@/sheep/hooks/useModal';
const sys_statusBar = sheep.$platform.device.statusBarHeight;
const sys_navBar = sheep.$platform.navbar;
const capsuleStyle = {
width: sheep.$platform.capsule.width + 'px',
height: sheep.$platform.capsule.height + 'px',
margin: '0 ' + (sheep.$platform.device.windowWidth - sheep.$platform.capsule.right) + 'px',
};
const state = reactive({
iconColor: '#000',
searchVal: '',
});
const props = defineProps({
placeholder: {
type: String,
default: '搜索内容',
},
});
const emits = defineEmits(['searchConfirm']);
// 返回
const toBack = () => {
sheep.$router.back();
};
// 搜索
const onSearch = () => {
emits('searchConfirm', state.searchVal);
};
const onTab = (item) => {};
</script>
<style lang="scss" scoped>
.back-icon {
font-size: 40rpx;
}
.sicon-more {
font-size: 48rpx;
}
</style>
+562
View File
@@ -0,0 +1,562 @@
<!-- 拼团商品详情 -->
<template>
<s-layout :onShareAppMessage="shareInfo" navbar="goods">
<!-- 标题栏 -->
<detailNavbar />
<!-- 骨架屏 -->
<detailSkeleton v-if="state.skeletonLoading" />
<!-- 下架/售罄提醒 -->
<s-empty
v-else-if="
state.goodsInfo === null ||
state.activity.status !== 0 ||
state.activity.endTime < new Date().getTime()
"
text="活动不存在或已结束"
icon="/static/soldout-empty.png"
showAction
actionText="返回上一页"
@clickAction="sheep.$router.back()"
/>
<block v-else>
<view class="detail-swiper-selector">
<!-- 商品图轮播 -->
<su-swiper
class="ss-m-b-14"
isPreview
:list="state.goodsSwiper"
dotStyle="tag"
imageMode="widthFix"
dotCur="bg-mask-40"
:seizeHeight="750"
/>
<!-- 价格+标题 -->
<view class="title-card detail-card ss-m-y-14 ss-m-x-20 ss-p-x-20 ss-p-y-34">
<view class="ss-flex ss-row-between ss-m-b-60">
<view>
<view class="price-box ss-flex ss-col-bottom ss-m-b-18">
<view class="price-text ss-m-r-16">
{{ fen2yuan(state.activity.price || state.goodsInfo.price) }}
</view>
<view class="tig ss-flex ss-col-center">
<view class="tig-icon ss-flex ss-col-center ss-row-center">
<view class="groupon-tag">
<image
:src="sheep.$url.static('/static/img/shop/goods/groupon-tag.png')"
></image>
</view>
</view>
<view class="tig-title">拼团价</view>
</view>
</view>
<view class="ss-flex ss-row-between">
<view class="origin-price ss-flex ss-col-center" v-if="state.goodsInfo.price">
单买价:
<view class="origin-price-text">
{{ fen2yuan(state.goodsInfo.marketPrice) }}
</view>
</view>
</view>
</view>
<view class="countdown-box" v-if="endTime.ms > 0">
<view class="countdown-title ss-m-b-20">距结束仅剩</view>
<view class="ss-flex countdown-time">
<view class="ss-flex countdown-h">{{ endTime.h }}</view>
<view class="ss-m-x-4">:</view>
<view class="countdown-num ss-flex ss-row-center">{{ endTime.m }}</view>
<view class="ss-m-x-4">:</view>
<view class="countdown-num ss-flex ss-row-center">{{ endTime.s }}</view>
</view>
</view>
<view class="countdown-title" v-else> 活动已结束 </view>
</view>
<view class="title-text ss-line-2 ss-m-b-6">{{ state.goodsInfo.name }}</view>
<view class="subtitle-text ss-line-1">{{ state.goodsInfo.introduction }}</view>
</view>
<!-- 功能卡片 -->
<view class="detail-cell-card detail-card ss-flex-col">
<!-- 规格 -->
<detail-cell-sku :sku="state.selectedSku" @tap="state.showSelectSku = true" />
</view>
<!-- 参团列表 -->
<groupon-card-list v-model="state.activity" @join="onJoinGroupon" />
<!-- 规格与数量弹框 -->
<s-select-groupon-sku
:show="state.showSelectSku"
:goodsInfo="state.goodsInfo"
:grouponAction="state.grouponAction"
:grouponNum="state.grouponNum"
@buy="onBuy"
@change="onSkuChange"
@close="onSkuClose"
/>
</view>
<!-- 评价 -->
<detail-comment-card class="detail-comment-selector" :goodsId="state.goodsId" />
<!-- 详情 -->
<detail-content-card class="detail-content-selector" :content="state.goodsInfo.description" />
<!-- 商品tabbar -->
<detail-tabbar v-model="state.goodsInfo">
<view class="buy-box ss-flex ss-col-center ss-p-r-20">
<button
class="ss-reset-button origin-price-btn ss-flex-col"
@tap="sheep.$router.go('/pages/goods/index', { id: state.goodsInfo.id })"
>
<view class="btn-price">{{ fen2yuan(state.goodsInfo.marketPrice) }}</view>
<view>原价购买</view>
</button>
<button
class="ss-reset-button btn-tox ss-flex-col"
@tap="onCreateGroupon"
:class="
state.activity.status === 0 && state.goodsInfo.stock !== 0
? 'check-btn-box'
: 'disabled-btn-box'
"
:disabled="state.goodsInfo.stock === 0 || state.activity.status !== 0"
>
<view class="btn-price">{{
fen2yuan(
state.selectedSku.price * state.selectedSku.count ||
state.activity.price * state.selectedSku.count ||
state.goodsInfo.price * state.selectedSku.count ||
state.goodsInfo.price,
)
}}</view>
<view v-if="state.activity.startTime > new Date().getTime()">未开始</view>
<view v-else-if="state.activity.endTime <= new Date().getTime()">已结束</view>
<view v-else>
<view v-if="state.goodsInfo.stock === 0">已售罄</view>
<view v-else>立即开团</view>
</view>
</button>
</view>
</detail-tabbar>
</block>
</s-layout>
</template>
<script setup>
import { reactive, computed } from 'vue';
import { onLoad, onPageScroll } from '@dcloudio/uni-app';
import sheep from '@/sheep';
import { isEmpty } from 'lodash-es';
import detailNavbar from './components/detail/detail-navbar.vue';
import detailCellSku from './components/detail/detail-cell-sku.vue';
import detailTabbar from './components/detail/detail-tabbar.vue';
import detailSkeleton from './components/detail/detail-skeleton.vue';
import detailCommentCard from './components/detail/detail-comment-card.vue';
import detailContentCard from './components/detail/detail-content-card.vue';
import grouponCardList from './components/groupon/groupon-card-list.vue';
import { useDurationTime, formatGoodsSwiper, fen2yuan } from '@/sheep/hooks/useGoods';
import CombinationApi from '@/sheep/api/promotion/combination';
import SpuApi from '@/sheep/api/product/spu';
import { SharePageEnum } from '@/sheep/helper/const';
const headerBg = sheep.$url.css('/static/img/shop/goods/groupon-bg.png');
const btnBg = sheep.$url.css('/static/img/shop/goods/groupon-btn.png');
const disabledBtnBg = sheep.$url.css('/static/img/shop/goods/activity-btn-disabled.png');
const grouponBg = sheep.$url.css('/static/img/shop/goods/groupon-tip-bg.png');
onPageScroll(() => {});
const state = reactive({
skeletonLoading: true, // 骨架屏
goodsId: 0, // 商品ID
goodsInfo: {}, // 商品信息
goodsSwiper: [], // 商品轮播图
showSelectSku: false, // 显示规格弹框
selectedSku: {}, // 选中的规格属性
activity: {}, // 团购活动
grouponId: 0, // 团购ID
grouponNum: 0, // 团购人数
grouponAction: 'create', // 团购操作
combinationHeadId: null, // 拼团团长编号
});
// 倒计时
const endTime = computed(() => {
return useDurationTime(state.activity.endTime);
});
// 规格变更
function onSkuChange(e) {
state.selectedSku = e;
}
function onSkuClose() {
state.showSelectSku = false;
}
// 发起拼团
function onCreateGroupon() {
state.grouponAction = 'create';
state.grouponId = 0;
state.showSelectSku = true;
}
/**
* 去参团
*
* @param record 团长的团购记录
*/
function onJoinGroupon(record) {
state.grouponAction = 'join';
state.grouponId = record.activityId;
state.combinationHeadId = record.id;
state.grouponNum = record.userSize;
state.showSelectSku = true;
}
// 立即购买
function onBuy(sku) {
sheep.$router.go('/pages/order/confirm', {
data: JSON.stringify({
order_type: 'goods',
combinationActivityId: state.activity.id,
combinationHeadId: state.combinationHeadId,
items: [
{
skuId: sku.id,
count: sku.count,
},
],
}),
});
}
// 分享信息
const shareInfo = computed(() => {
if (isEmpty(state.activity)) return {};
return sheep.$platform.share.getShareInfo(
{
title: state.activity.name,
image: sheep.$url.cdn(state.goodsInfo.picUrl),
params: {
page: SharePageEnum.GROUPON.value,
query: state.activity.id,
},
},
{
type: 'goods', // 商品海报
title: state.activity.name, // 商品标题
image: sheep.$url.cdn(state.goodsInfo.picUrl), // 商品主图
price: fen2yuan(state.goodsInfo.price), // 商品价格
marketPrice: fen2yuan(state.goodsInfo.marketPrice), // 商品原价
},
);
});
onLoad(async (options) => {
// 非法参数
if (!options.id) {
state.goodsInfo = null;
state.skeletonLoading = false;
return;
}
state.grouponId = options.id;
// 加载活动信息
const { code, data: activity } = await CombinationApi.getCombinationActivity(state.grouponId);
if (code !== 0) {
state.goodsInfo = null;
state.skeletonLoading = false;
return;
}
state.activity = activity;
// 加载商品信息
const { data: spu } = await SpuApi.getSpuDetail(activity.spuId);
if (code !== 0) {
state.goodsInfo = null;
state.skeletonLoading = false;
return;
}
state.goodsId = spu.id;
// 默认显示最低价
spu.price = activity.products.reduce((min, product) => {
return Math.min(min, product.combinationPrice || Infinity);
}, Infinity);
// 价格、库存使用活动的
spu.skus.forEach((sku) => {
const product = activity.products.find((product) => product.skuId === sku.id);
if (product) {
sku.price = product.combinationPrice;
} else {
// 找不到可能是没配置,则不能发起秒杀
sku.stock = 0;
}
});
// 关闭骨架屏
state.skeletonLoading = false;
if (code === 0) {
state.goodsInfo = spu;
state.grouponNum = activity.userSize;
state.goodsSwiper = formatGoodsSwiper(state.goodsInfo.sliderPicUrls);
} else {
// 未找到商品
state.goodsInfo = null;
}
});
</script>
<style lang="scss" scoped>
.detail-card {
background-color: $white;
margin: 14rpx 20rpx;
border-radius: 10rpx;
overflow: hidden;
}
// 价格标题卡片
.title-card {
width: 710rpx;
box-sizing: border-box;
// height: 320rpx;
background-size: 100% 100%;
border-radius: 10rpx;
background-image: v-bind(headerBg);
background-repeat: no-repeat;
.price-box {
.price-text {
font-size: 30rpx;
font-weight: 500;
color: #fff;
line-height: normal;
font-family: OPPOSANS;
&::before {
content: '¥';
font-size: 30rpx;
}
}
}
.origin-price {
font-size: 24rpx;
font-weight: 400;
color: #fff;
opacity: 0.7;
.origin-price-text {
text-decoration: line-through;
font-family: OPPOSANS;
&::before {
content: '¥';
}
}
}
.tig {
border: 2rpx solid #ffffff;
border-radius: 4rpx;
width: 126rpx;
height: 38rpx;
.tig-icon {
margin-left: -2rpx;
width: 40rpx;
height: 40rpx;
background: #ffffff;
border-radius: 4rpx 0 0 4rpx;
.groupon-tag {
width: 32rpx;
height: 32rpx;
}
}
.tig-title {
font-size: 24rpx;
font-weight: 500;
line-height: normal;
color: #ffffff;
width: 86rpx;
display: flex;
justify-content: center;
align-items: center;
}
}
.countdown-title {
font-size: 26rpx;
font-weight: 500;
color: #ffffff;
}
.countdown-time {
font-size: 26rpx;
font-weight: 500;
color: #ffffff;
.countdown-h {
font-size: 24rpx;
font-family: OPPOSANS;
font-weight: 500;
color: #ffffff;
padding: 0 4rpx;
height: 40rpx;
background: rgba(#000000, 0.1);
border-radius: 6rpx;
}
.countdown-num {
font-size: 24rpx;
font-family: OPPOSANS;
font-weight: 500;
color: #ffffff;
width: 40rpx;
height: 40rpx;
background: rgba(#000000, 0.1);
border-radius: 6rpx;
}
}
.title-text {
font-size: 30rpx;
font-weight: bold;
line-height: 42rpx;
color: #fff;
}
.subtitle-text {
font-size: 26rpx;
font-weight: 400;
color: #ffffff;
line-height: 42rpx;
opacity: 0.9;
}
}
// 购买
.buy-box {
.disabled-btn-box[disabled] {
background-color: transparent;
}
.check-btn-box {
width: 248rpx;
height: 80rpx;
font-size: 24rpx;
font-weight: 600;
margin-left: -36rpx;
background-image: v-bind(btnBg);
background-repeat: no-repeat;
background-size: 100% 100%;
color: #ffffff;
line-height: normal;
border-radius: 0px 40rpx 40rpx 0px;
}
.disabled-btn-box {
width: 248rpx;
height: 80rpx;
font-size: 24rpx;
font-weight: 600;
margin-left: -36rpx;
background-image: v-bind(disabledBtnBg);
background-repeat: no-repeat;
background-size: 100% 100%;
color: #999999;
line-height: normal;
border-radius: 0px 40rpx 40rpx 0px;
}
.origin-price-btn {
width: 236rpx;
height: 80rpx;
background: rgba(#ff5651, 0.1);
color: #ff6000;
border-radius: 40rpx 0px 0px 40rpx;
line-height: normal;
font-size: 24rpx;
font-weight: 500;
.btn-title {
font-size: 28rpx;
}
}
.btn-price {
font-family: OPPOSANS;
&::before {
content: '¥';
}
}
.more-item-box {
.more-item {
width: 156rpx;
height: 58rpx;
font-size: 26rpx;
font-weight: 500;
color: #999999;
border-radius: 10rpx;
}
.more-item-hover {
background: rgba(#ffefe5, 0.32);
color: #ff6000;
}
}
}
.groupon-box {
background: v-bind(grouponBg) no-repeat;
background-size: 100% 100%;
}
//活动卡片
.activity-box {
width: 100%;
height: 80rpx;
box-sizing: border-box;
margin-bottom: 10rpx;
.activity-title {
font-size: 26rpx;
font-weight: 500;
color: #ffffff;
line-height: 42rpx;
.activity-icon {
width: 38rpx;
height: 38rpx;
}
}
.activity-go {
width: 70rpx;
height: 32rpx;
background: #ffffff;
border-radius: 16rpx;
font-weight: 500;
color: #ff6000;
font-size: 24rpx;
line-height: normal;
}
}
.model-box {
.title {
font-size: 36rpx;
font-weight: bold;
color: #333333;
}
.subtitle {
font-size: 26rpx;
font-weight: 500;
color: #333333;
}
}
image {
width: 100%;
height: 100%;
}
</style>
+672
View File
@@ -0,0 +1,672 @@
<template>
<view>
<s-layout :onShareAppMessage="shareInfo" navbar="goods">
<!-- 标题栏 -->
<detailNavbar />
<!-- 骨架屏 -->
<detailSkeleton v-if="state.skeletonLoading" />
<!-- 下架/售罄提醒 -->
<s-empty
v-else-if="state.goodsInfo === null"
text="商品不存在或已下架"
icon="/static/soldout-empty.png"
showAction
actionText="再逛逛"
actionUrl="/pages/goods/list"
/>
<block v-else>
<view class="detail-swiper-selector">
<!-- 商品轮播图 -->
<su-swiper
class="ss-m-b-14"
isPreview
:list="formatGoodsSwiper(state.goodsInfo.sliderPicUrls)"
otStyle="tag"
imageMode="widthFix"
dotCur="bg-mask-40"
:seizeHeight="750"
/>
<!-- 限时折扣/会员价的优惠信息 -->
<view
class="discount"
v-if="
state.settlementSku && state.settlementSku.id && state.settlementSku.promotionPrice
"
>
<image class="disImg" :src="sheep.$url.static('/static/img/shop/goods/dis.png')" />
<view class="discountCont">
<view class="disContT">
<view class="disContT1">
<view class="disContT1P">
¥{{ fen2yuan(state.settlementSku.promotionPrice) }}
</view>
<view class="disContT1End">
直降¥
{{ fen2yuan(state.settlementSku.price - state.settlementSku.promotionPrice) }}
</view>
</view>
<view class="disContT2" v-if="state.settlementSku.promotionType === 4">
限时折扣
</view>
<view class="disContT2" v-else-if="state.settlementSku.promotionType === 6">
会员折扣
</view>
</view>
<view class="disContB">
<view class="disContB1">
价格:¥{{ fen2yuan(state.settlementSku.price) }} 丨 剩余:
{{ state.settlementSku.stock }}
</view>
<view class="disContB2" v-if="state.settlementSku.promotionEndTime > 0">
距结束仅剩
<s-count-down
:tipText="' '"
:bgColor="bgColor"
:dayText="':'"
:hourText="':'"
:minuteText="':'"
:secondText="' '"
:datatime="state.settlementSku.promotionEndTime / 1000"
:isDay="false"
/>
</view>
</view>
</view>
</view>
<!-- 价格+标题 -->
<view class="title-card detail-card ss-p-y-30 ss-p-x-20">
<!-- 没有限时折扣/会员价的优惠信息时,展示的价格信息 -->
<view
class="ss-flex ss-row-between ss-col-center ss-m-b-26"
v-if="!state.settlementSku.promotionPrice"
>
<view class="price-box ss-flex ss-col-bottom">
<view class="price-text ss-m-r-16">
{{ fen2yuan(state.selectedSku.price || state.goodsInfo.price) }}
</view>
<view
class="origin-price-text"
v-if="state.goodsInfo.marketPrice > state.goodsInfo.price"
>
{{ fen2yuan(state.selectedSku.marketPrice || state.goodsInfo.marketPrice) }}
</view>
</view>
<view class="sales-text">
{{ formatSales('exact', state.goodsInfo.salesCount) }}
</view>
</view>
<view class="discounts-box ss-flex ss-row-between ss-m-b-28">
<!-- 查看优惠劵的描述 -->
<view
class="tag ss-m-r-10"
v-for="coupon in state.couponInfo.slice(0, 1)"
:key="coupon.id"
@tap="onOpenActivity"
>
[劵]满{{ fen2yuanSimple(coupon.usePrice) }}元{{
coupon.discountType === 1
? '减' + fen2yuanSimple(coupon.discountPrice) + '元'
: '打' + formatDiscountPercent(coupon.discountPercent) + '折'
}}
</view>
<!-- 查看满减送的描述 -->
<div class="tag-content">
<view class="tag-box ss-flex">
<!-- 最多打印 3 条,所以需要扣除优惠劵已打印的 -->
<view
v-for="item in getRewardActivityRuleItemDescriptions(
state.rewardActivity,
).slice(0, 3 - state.couponInfo.slice(0, 1).length)"
:key="item"
class="tag ss-m-r-10"
@tap="onOpenActivity"
>
<text>{{ item }}</text>
</view>
</view>
</div>
<!-- 领取优惠劵的按钮 -->
<view
class="get-coupon-box ss-flex ss-col-center ss-m-l-20"
@tap="onOpenActivity"
v-if="state.couponInfo.length"
>
<view class="discounts-title ss-m-r-8">领券</view>
<text class="cicon-forward"></text>
</view>
</view>
<view class="title-text ss-line-2 ss-m-b-6">{{ state.goodsInfo.name }}</view>
<view class="subtitle-text ss-line-1">{{ state.goodsInfo.introduction }}</view>
</view>
<!-- 功能卡片 -->
<view class="detail-cell-card detail-card ss-flex-col">
<detail-cell-sku
v-model="state.selectedSku.goods_sku_text"
:sku="state.selectedSku"
@tap="state.showSelectSku = true"
/>
</view>
<!-- 规格与数量弹框 -->
<s-select-sku
:goodsInfo="state.goodsInfo"
:show="state.showSelectSku"
@addCart="onAddCart"
@buy="onBuy"
@change="onSkuChange"
@close="state.showSelectSku = false"
/>
</view>
<!-- 评价 -->
<detail-comment-card class="detail-comment-selector" :goodsId="state.goodsId" />
<!-- 详情 -->
<detail-content-card
class="detail-content-selector"
:content="state.goodsInfo.description"
/>
<!-- 活动跳转:拼团/秒杀/砍价活动 -->
<detail-activity-tip
v-if="state.activityList.length > 0"
:activity-list="state.activityList"
/>
<!-- 详情 tabbar -->
<detail-tabbar v-model="state.goodsInfo">
<view class="buy-box ss-flex ss-col-center ss-p-r-20" v-if="state.goodsInfo.stock > 0">
<button
class="ss-reset-button add-btn ui-Shadow-Main"
@tap="state.showSelectSku = true"
>
加入购物车
</button>
<button
class="ss-reset-button buy-btn ui-Shadow-Main"
@tap="state.showSelectSku = true"
>
立即购买
</button>
</view>
<view class="buy-box ss-flex ss-col-center ss-p-r-20" v-else>
<button class="ss-reset-button disabled-btn" disabled> 已售罄 </button>
</view>
</detail-tabbar>
<!-- 满减送/限时折扣活动弹窗 -->
<s-activity-pop
v-model="state"
:show="state.showActivityModel"
@close="state.showActivityModel = false"
@get="onTakeCoupon"
/>
</block>
</s-layout>
</view>
</template>
<script setup>
import { reactive, computed } from 'vue';
import { onLoad, onPageScroll } from '@dcloudio/uni-app';
import sheep from '@/sheep';
import CouponApi from '@/sheep/api/promotion/coupon';
import ActivityApi from '@/sheep/api/promotion/activity';
import FavoriteApi from '@/sheep/api/product/favorite';
import RewardActivityApi from '@/sheep/api/promotion/rewardActivity';
import {
formatSales,
formatGoodsSwiper,
fen2yuan,
fen2yuanSimple,
formatDiscountPercent,
getRewardActivityRuleItemDescriptions,
} from '@/sheep/hooks/useGoods';
import detailNavbar from './components/detail/detail-navbar.vue';
import detailCellSku from './components/detail/detail-cell-sku.vue';
import detailTabbar from './components/detail/detail-tabbar.vue';
import detailSkeleton from './components/detail/detail-skeleton.vue';
import detailCommentCard from './components/detail/detail-comment-card.vue';
import detailContentCard from './components/detail/detail-content-card.vue';
import detailActivityTip from './components/detail/detail-activity-tip.vue';
import { isEmpty } from 'lodash-es';
import SpuApi from '@/sheep/api/product/spu';
onPageScroll(() => {});
import OrderApi from '@/sheep/api/trade/order';
import { SharePageEnum } from '@/sheep/helper/const';
const bgColor = {
bgColor: '#E93323',
Color: '#fff',
width: '44rpx',
timeTxtwidth: '16rpx',
isDay: true,
};
const isLogin = computed(() => sheep.$store('user').isLogin);
const state = reactive({
goodsId: 0,
skeletonLoading: true, // SPU 加载中
goodsInfo: {}, // SPU 信息
showSelectSku: false, // 是否展示 SKU 选择弹窗
selectedSku: {}, // 选中的 SKU
settlementSku: {}, // 结算的 SKU:由于 selectedSku 不进行默认选中,所以初始使用结算价格最低的 SKU 作为基础展示
showModel: false, // 是否展示 Coupon 优惠劵的弹窗
couponInfo: [], // 可领取的 Coupon 优惠劵的列表
showActivityModel: false, // 【满减送/限时折扣】是否展示 Activity 营销活动的弹窗
rewardActivity: {}, // 【满减送】活动
activityList: [], // 【秒杀/拼团/砍价】可参与的 Activity 营销活动的列表
});
// 规格变更
function onSkuChange(e) {
state.selectedSku = e;
state.settlementSku = e;
}
// 添加购物车
function onAddCart(e) {
if (!e.id) {
sheep.$helper.toast('请选择商品规格');
return;
}
sheep.$store('cart').add(e);
}
// 立即购买
function onBuy(e) {
if (!e.id) {
sheep.$helper.toast('请选择商品规格');
return;
}
sheep.$router.go('/pages/order/confirm', {
data: JSON.stringify({
items: [
{
skuId: e.id,
count: e.goods_num,
categoryId: state.goodsInfo.categoryId,
},
],
}),
});
}
// 打开营销弹窗
function onOpenActivity() {
state.showActivityModel = true;
}
// 立即领取优惠劵
async function onTakeCoupon(id) {
const { code } = await CouponApi.takeCoupon(id);
if (code !== 0) {
return;
}
uni.showToast({
title: '领取成功',
});
setTimeout(() => {
getCoupon();
}, 1000);
}
const shareInfo = computed(() => {
if (isEmpty(state.goodsInfo)) return {};
return sheep.$platform.share.getShareInfo(
{
title: state.goodsInfo.name,
image: sheep.$url.cdn(state.goodsInfo.picUrl),
desc: state.goodsInfo.introduction,
params: {
page: SharePageEnum.GOODS.value,
query: state.goodsInfo.id,
},
},
{
type: 'goods', // 商品海报
title: state.goodsInfo.name, // 商品名称
image: sheep.$url.cdn(state.goodsInfo.picUrl), // 商品主图
price: fen2yuan(state.goodsInfo.price), // 商品价格
original_price: fen2yuan(state.goodsInfo.marketPrice), // 商品原价
},
);
});
async function getCoupon() {
const { code, data } = await CouponApi.getCouponTemplateList(state.goodsId, 2, 10);
if (code === 0) {
state.couponInfo = data;
}
}
async function getSettlementByIds(ids) {
let { data, code } = await OrderApi.getSettlementProduct(ids);
if (code !== 0 || data.length !== 1) {
return;
}
data = data[0];
// 补充 SKU 的价格信息
state.goodsInfo.skus.forEach((sku) => {
data.skus.forEach((item) => {
if (sku.id === item.id) {
sku.promotionType = item.promotionType;
sku.promotionPrice = item.promotionPrice;
sku.promotionId = item.promotionId;
sku.promotionEndTime = item.promotionEndTime;
}
});
});
// 选择有 promotionPrice 且最小的
state.settlementSku = state.goodsInfo.skus
.filter((sku) => sku.stock > 0 && sku.promotionPrice > 0)
.reduce((prev, curr) => (prev.promotionPrice < curr.promotionPrice ? prev : curr), []);
// 设置满减送活动
if (data.rewardActivity) {
state.rewardActivity = data.rewardActivity;
//获取活动时间
getActivityTime(state.rewardActivity.id);
}
}
//获取活动时间
async function getActivityTime(id) {
const { code, data } = await RewardActivityApi.getRewardActivity(id);
if (code === 0) {
// console.log('获取到的活动 数据', data)
state.rewardActivity.startTime = data.startTime;
state.rewardActivity.endTime = data.endTime;
}
}
onLoad((options) => {
// 非法参数
if (!options.id) {
state.goodsInfo = null;
state.skeletonLoading = false;
return;
}
state.goodsId = options.id;
// 1. 加载商品信息
SpuApi.getSpuDetail(state.goodsId).then((res) => {
if (res.code !== 0 || !res.data) {
state.goodsInfo = null;
state.skeletonLoading = false;
return;
}
// 加载到商品
state.skeletonLoading = false;
state.goodsInfo = res.data;
// 获取结算信息
getSettlementByIds(state.goodsId);
// 加载是否收藏
if (isLogin.value) {
FavoriteApi.isFavoriteExists(state.goodsId, 'goods').then((res) => {
if (res.code !== 0) {
return;
}
state.goodsInfo.favorite = res.data;
});
}
});
// 2. 加载优惠劵信息
getCoupon();
// 3. 加载营销活动信息
ActivityApi.getActivityListBySpuId(state.goodsId).then((res) => {
if (res.code !== 0) {
return;
}
state.activityList = res.data;
});
});
</script>
<style lang="scss" scoped>
.detail-card {
background-color: #ffff;
margin: 14rpx 20rpx;
border-radius: 10rpx;
overflow: hidden;
}
// 价格标题卡片
.title-card {
.price-box {
.price-text {
font-size: 42rpx;
font-weight: 500;
color: #ff3000;
line-height: 30rpx;
font-family: OPPOSANS;
&::before {
content: '¥';
font-size: 30rpx;
}
}
.origin-price-text {
font-size: 26rpx;
font-weight: 400;
text-decoration: line-through;
color: $gray-c;
font-family: OPPOSANS;
&::before {
content: '¥';
}
}
}
.sales-text {
font-size: 26rpx;
font-weight: 500;
color: $gray-c;
}
.discounts-box {
.tag-content {
flex: 1;
min-width: 0;
white-space: nowrap;
}
.tag-box {
overflow: hidden;
text-overflow: ellipsis;
}
.tag {
flex-shrink: 0;
padding: 4rpx 10rpx;
font-size: 24rpx;
font-weight: 500;
border-radius: 4rpx;
color: var(--ui-BG-Main);
background: var(--ui-BG-Main-tag);
}
.discounts-title {
font-size: 24rpx;
font-weight: 500;
color: var(--ui-BG-Main);
line-height: normal;
}
.cicon-forward {
color: var(--ui-BG-Main);
font-size: 24rpx;
line-height: normal;
margin-top: 4rpx;
}
}
.title-text {
font-size: 30rpx;
font-weight: bold;
line-height: 42rpx;
}
.subtitle-text {
font-size: 26rpx;
font-weight: 400;
color: $dark-9;
line-height: 42rpx;
}
}
// 购买
.buy-box {
.add-btn {
width: 214rpx;
height: 72rpx;
font-weight: 500;
font-size: 28rpx;
border-radius: 40rpx 0 0 40rpx;
background-color: var(--ui-BG-Main-light);
color: var(--ui-BG-Main);
}
.buy-btn {
width: 214rpx;
height: 72rpx;
font-weight: 500;
font-size: 28rpx;
border-radius: 0 40rpx 40rpx 0;
background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
color: $white;
}
.disabled-btn {
width: 428rpx;
height: 72rpx;
border-radius: 40rpx;
background: #999999;
color: $white;
}
}
.model-box {
height: 60vh;
.model-content {
height: 56vh;
}
.title {
font-size: 36rpx;
font-weight: bold;
color: #333333;
}
.subtitle {
font-size: 26rpx;
font-weight: 500;
color: #333333;
}
}
// 限时折扣
.discount {
width: 750rpx;
height: 100rpx;
// background-color: red;
overflow: hidden;
position: relative;
}
.disImg {
width: 750rpx;
height: 100rpx;
position: absolute;
top: 0;
z-index: -1;
}
.discountCont {
width: 680rpx;
height: 90rpx;
margin: 10rpx auto 0 auto;
// background-color: gold;
}
.disContT {
width: 680rpx;
height: 50rpx;
display: flex;
justify-content: space-between;
}
.disContT1 {
width: 400rpx;
height: 50rpx;
// background-color: green;
display: flex;
justify-content: flex-start;
align-items: center;
}
.disContT2 {
width: 200rpx;
height: 50rpx;
line-height: 50rpx;
// background-color: gold;
font-size: 30rpx;
text-align: end;
color: white;
font-weight: bolder;
font-style: oblique 20deg;
letter-spacing: 0.1rem;
}
.disContT1P {
color: white;
font-weight: bold;
font-size: 28rpx;
}
.disContT1End {
// width: 180rpx;
padding: 0 10rpx;
height: 30rpx;
line-height: 28rpx;
text-align: center;
font-weight: bold;
background-color: white;
color: #ff3000;
font-size: 23rpx;
border-radius: 20rpx;
margin-left: 10rpx;
}
.disContB {
width: 680rpx;
height: 40rpx;
display: flex;
justify-content: space-between;
font-size: 20rpx;
color: white;
align-items: center;
}
.disContB1 {
width: 300rpx;
height: 40rpx;
line-height: 40rpx;
}
.disContB2 {
width: 300rpx;
height: 40rpx;
line-height: 40rpx;
display: flex;
justify-content: flex-end;
}
</style>
+407
View File
@@ -0,0 +1,407 @@
<template>
<s-layout
navbar="normal"
:leftWidth="0"
:rightWidth="0"
tools="search"
:defaultSearch="state.keyword"
@search="onSearch"
>
<!-- 筛选 -->
<su-sticky bgColor="#fff">
<view class="ss-flex">
<view class="ss-flex-1">
<su-tabs
:list="state.tabList"
:scrollable="false"
@change="onTabsChange"
:current="state.currentTab"
/>
</view>
<view class="list-icon" @tap="state.iconStatus = !state.iconStatus">
<text v-if="state.iconStatus" class="sicon-goods-list" />
<text v-else class="sicon-goods-card" />
</view>
</view>
</su-sticky>
<!-- 弹窗 -->
<su-popup
:show="state.showFilter"
type="top"
round="10"
:space="sys_navBar + 38"
backgroundColor="#F6F6F6"
:zIndex="10"
@close="state.showFilter = false"
>
<view class="filter-list-box">
<view
class="filter-item"
v-for="(item, index) in state.tabList[state.currentTab].list"
:key="item.value"
:class="[{ 'filter-item-active': index === state.curFilter }]"
@tap="onFilterItem(index)"
>
{{ item.label }}
</view>
</view>
</su-popup>
<!-- 情况一单列布局 -->
<view v-if="state.iconStatus && state.pagination.total > 0" class="goods-list ss-m-t-20">
<view
class="ss-p-l-20 ss-p-r-20 ss-m-b-20"
v-for="item in state.pagination.list"
:key="item.id"
>
<s-goods-column
class=""
size="lg"
:data="item"
:topRadius="10"
:bottomRadius="10"
@click="sheep.$router.go('/pages/goods/index', { id: item.id })"
/>
</view>
</view>
<!-- 情况二双列布局 -->
<view
v-if="!state.iconStatus && state.pagination.total > 0"
class="ss-flex ss-flex-wrap ss-p-x-20 ss-m-t-20 ss-col-top"
>
<view class="goods-list-box">
<view class="left-list" v-for="item in state.leftGoodsList" :key="item.id">
<s-goods-column
class="goods-md-box"
size="md"
:data="item"
:topRadius="10"
:bottomRadius="10"
@click="sheep.$router.go('/pages/goods/index', { id: item.id })"
@getHeight="mountMasonry($event, 'left')"
>
<template v-slot:cart>
<button class="ss-reset-button cart-btn" />
</template>
</s-goods-column>
</view>
</view>
<view class="goods-list-box">
<view class="right-list" v-for="item in state.rightGoodsList" :key="item.id">
<s-goods-column
class="goods-md-box"
size="md"
:topRadius="10"
:bottomRadius="10"
:data="item"
@click="sheep.$router.go('/pages/goods/index', { id: item.id })"
@getHeight="mountMasonry($event, 'right')"
>
<template v-slot:cart>
<button class="ss-reset-button cart-btn" />
</template>
</s-goods-column>
</view>
</view>
</view>
<uni-load-more
v-if="state.pagination.total > 0"
:status="state.loadStatus"
:content-text="{
contentdown: '上拉加载更多',
}"
@tap="loadMore"
/>
<s-empty v-if="state.pagination.total === 0" icon="/static/soldout-empty.png" text="暂无商品" />
</s-layout>
</template>
<script setup>
import { reactive, ref } from 'vue';
import { onLoad, onReachBottom } from '@dcloudio/uni-app';
import sheep from '@/sheep';
import _ from 'lodash-es';
import { resetPagination } from '@/sheep/helper/utils';
import SpuApi from '@/sheep/api/product/spu';
import OrderApi from '@/sheep/api/trade/order';
import { appendSettlementProduct } from '@/sheep/hooks/useGoods';
const sys_navBar = sheep.$platform.navbar;
const emits = defineEmits(['close', 'change']);
const state = reactive({
pagination: {
list: [],
total: 0,
pageNo: 1,
pageSize: 6,
},
currentSort: undefined,
currentOrder: undefined,
currentTab: 0, // 当前选中的 tab
curFilter: 0, // 当前选中的 list 筛选项
showFilter: false,
iconStatus: false, // true - 单列布局;false - 双列布局
keyword: '',
categoryId: 0,
tabList: [
{
name: '综合推荐',
list: [
{
label: '综合推荐',
},
{
label: '价格升序',
sort: 'price',
order: true,
},
{
label: '价格降序',
sort: 'price',
order: false,
},
],
},
{
name: '销量',
sort: 'salesCount',
order: false,
},
{
name: '新品优先',
value: 'createTime',
order: false,
},
],
loadStatus: '',
leftGoodsList: [], // 双列布局 - 左侧商品
rightGoodsList: [], // 双列布局 - 右侧商品
});
// 加载瀑布流
let count = 0;
let leftHeight = 0;
let rightHeight = 0;
// 处理双列布局 leftGoodsList + rightGoodsList
function mountMasonry(height = 0, where = 'left') {
if (where === 'left') {
leftHeight += height;
} else {
rightHeight += height;
}
if (!state.pagination.list[count]) {
return;
}
if (leftHeight <= rightHeight) {
state.leftGoodsList.push(state.pagination.list[count]);
} else {
state.rightGoodsList.push(state.pagination.list[count]);
}
count++;
}
// 清空列表
function emptyList() {
resetPagination(state.pagination);
state.leftGoodsList = [];
state.rightGoodsList = [];
count = 0;
leftHeight = 0;
rightHeight = 0;
}
// 搜索
function onSearch(e) {
state.keyword = e;
emptyList();
getList(state.currentSort, state.currentOrder);
}
// 点击
function onTabsChange(e) {
// 如果点击的是【综合推荐】,则直接展开或者收起筛选项
if (state.tabList[e.index].list) {
state.currentTab = e.index;
state.showFilter = !state.showFilter;
return;
}
state.showFilter = false;
// 如果点击的是【销量】或者【新品优先】,则直接切换 tab
if (e.index === state.currentTab) {
return;
}
state.currentTab = e.index;
state.currentSort = e.sort;
state.currentOrder = e.order;
emptyList();
getList(e.sort, e.order);
}
// 点击 tab 的 list 筛选项
const onFilterItem = (val) => {
// 如果点击的是当前的筛选项,则直接收起筛选项,不要加载数据
// 这里选择 tabList[0] 的原因,是目前只有它有 list
if (
state.currentSort === state.tabList[0].list[val].sort &&
state.currentOrder === state.tabList[0].list[val].order
) {
state.showFilter = false;
return;
}
state.showFilter = false;
// 设置筛选条件
state.curFilter = val;
state.tabList[0].name = state.tabList[0].list[val].label;
state.currentSort = state.tabList[0].list[val].sort;
state.currentOrder = state.tabList[0].list[val].order;
// 清空 + 加载数据
emptyList();
getList();
};
async function getList() {
state.loadStatus = 'loading';
const { code, data } = await SpuApi.getSpuPage({
pageNo: state.pagination.pageNo,
pageSize: state.pagination.pageSize,
sortField: state.currentSort,
sortAsc: state.currentOrder,
categoryId: state.categoryId,
keyword: state.keyword,
});
if (code !== 0) {
return;
}
// 拼接结算信息(营销)
await OrderApi.getSettlementProduct(data.list.map((item) => item.id).join(',')).then((res) => {
if (res.code !== 0) {
return;
}
appendSettlementProduct(data.list, res.data);
});
state.pagination.list = _.concat(state.pagination.list, data.list);
state.pagination.total = data.total;
state.loadStatus = state.pagination.list.length < state.pagination.total ? 'more' : 'noMore';
mountMasonry();
}
// 加载更多
function loadMore() {
if (state.loadStatus === 'noMore') {
return;
}
state.pagination.pageNo++;
getList(state.currentSort, state.currentOrder);
}
onLoad((options) => {
state.categoryId = options.categoryId;
state.keyword = options.keyword;
getList(state.currentSort, state.currentOrder);
});
// 上拉加载更多
onReachBottom(() => {
loadMore();
});
</script>
<style lang="scss" scoped>
.goods-list-box {
width: 50%;
box-sizing: border-box;
.left-list {
margin-right: 10rpx;
margin-bottom: 20rpx;
}
.right-list {
margin-left: 10rpx;
margin-bottom: 20rpx;
}
}
.goods-box {
&:nth-last-of-type(1) {
margin-bottom: 0 !important;
}
&:nth-child(2n) {
margin-right: 0;
}
}
.list-icon {
width: 80rpx;
.sicon-goods-card {
font-size: 40rpx;
}
.sicon-goods-list {
font-size: 40rpx;
}
}
.goods-card {
margin-left: 20rpx;
}
.list-filter-tabs {
background-color: #fff;
}
.filter-list-box {
padding: 28rpx 52rpx;
.filter-item {
font-size: 28rpx;
font-weight: 500;
color: #333333;
line-height: normal;
margin-bottom: 24rpx;
&:nth-last-child(1) {
margin-bottom: 0;
}
}
.filter-item-active {
color: var(--ui-BG-Main);
}
}
.tab-item {
height: 50px;
position: relative;
z-index: 11;
.tab-title {
font-size: 30rpx;
}
.cur-tab-title {
font-weight: $font-weight-bold;
}
.tab-line {
width: 60rpx;
height: 6rpx;
border-radius: 6rpx;
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: 10rpx;
background-color: var(--ui-BG-Main);
z-index: 12;
}
}
</style>
+483
View File
@@ -0,0 +1,483 @@
<!-- 秒杀商品详情 -->
<template>
<s-layout :onShareAppMessage="shareInfo" navbar="goods">
<!-- 标题栏 -->
<detailNavbar />
<!-- 骨架屏 -->
<detailSkeleton v-if="state.skeletonLoading" />
<!-- 下架/售罄提醒 -->
<s-empty
v-else-if="
state.goodsInfo === null ||
state.goodsInfo.activity_type !== PromotionActivityTypeEnum.POINT.type
"
text="活动不存在或已结束"
icon="/static/soldout-empty.png"
showAction
actionText="再逛逛"
actionUrl="/pages/goods/list"
/>
<block v-else>
<view class="detail-swiper-selector">
<!-- 商品图轮播 -->
<su-swiper
class="ss-m-b-14"
isPreview
:list="state.goodsSwiper"
dotStyle="tag"
imageMode="widthFix"
dotCur="bg-mask-40"
:seizeHeight="750"
/>
<!-- 价格+标题 -->
<view class="title-card detail-card ss-p-y-40 ss-p-x-20">
<view class="ss-flex ss-row-between ss-col-center ss-m-b-18">
<view class="price-box ss-flex ss-col-bottom">
<image
:src="sheep.$url.static('/static/img/shop/goods/score1.svg')"
class="point-img"
></image>
<text class="point-text ss-m-r-16">
{{ getShowPrice.point }}
{{
!getShowPrice.price || getShowPrice.price === 0 ? '' : `+¥${getShowPrice.price}`
}}
</text>
</view>
<view class="sales-text">
{{ formatExchange(state.goodsInfo.sales_show_type, state.goodsInfo.sales) }}
</view>
</view>
<view class="origin-price-text ss-m-b-60" v-if="state.goodsInfo.marketPrice">
原价:¥{{ fen2yuan(state.selectedSku.marketPrice || state.goodsInfo.marketPrice) }}
</view>
<view class="title-text ss-line-2 ss-m-b-6">{{ state.goodsInfo.name || '' }}</view>
<view class="subtitle-text ss-line-1">{{ state.goodsInfo.introduction }}</view>
</view>
<!-- 功能卡片 -->
<view class="detail-cell-card detail-card ss-flex-col">
<detail-cell-sku :sku="state.selectedSku" @tap="state.showSelectSku = true" />
</view>
<!-- 规格与数量弹框 -->
<s-select-seckill-sku
v-model="state.goodsInfo"
:show="state.showSelectSku"
:single-limit-count="activity.singleLimitCount"
@buy="onBuy"
@change="onSkuChange"
@close="state.showSelectSku = false"
/>
</view>
<!-- 评价 -->
<detail-comment-card class="detail-comment-selector" :goodsId="state.goodsInfo.id" />
<!-- 详情 -->
<detail-content-card class="detail-content-selector" :content="state.goodsInfo.description" />
<!-- 详情tabbar -->
<detail-tabbar v-model="state.goodsInfo">
<view class="buy-box ss-flex ss-col-center ss-p-r-20">
<button
class="ss-reset-button origin-price-btn ss-flex-col"
v-if="state.goodsInfo.marketPrice"
@tap="sheep.$router.go('/pages/goods/index', { id: state.goodsInfo.id })"
>
<view>
<view class="btn-price">{{ fen2yuan(state.goodsInfo.marketPrice) }}</view>
<view>原价购买</view>
</view>
</button>
<button
class="ss-reset-button btn-box ss-flex-col"
@tap="state.showSelectSku = true"
:class="state.goodsInfo.stock != 0 ? 'check-btn-box' : 'disabled-btn-box'"
:disabled="state.goodsInfo.stock === 0"
>
<view class="price-box ss-flex">
<image
:src="sheep.$url.static('/static/img/shop/goods/score1.svg')"
style="width: 36rpx; height: 36rpx; margin: 0 4rpx"
></image>
<text class="point-text ss-m-r-16">
{{ getShowPrice.point }}
{{
!getShowPrice.price || getShowPrice.price === 0 ? '' : `+¥${getShowPrice.price}`
}}
</text>
</view>
<view v-if="state.goodsInfo.stock === 0">已售罄</view>
<view v-else>立即兑换</view>
</button>
</view>
</detail-tabbar>
</block>
</s-layout>
</template>
<script setup>
import { computed, reactive, ref, unref } from 'vue';
import { onLoad, onPageScroll } from '@dcloudio/uni-app';
import sheep from '@/sheep';
import { isEmpty } from 'lodash-es';
import { fen2yuan, formatExchange, formatGoodsSwiper } from '@/sheep/hooks/useGoods';
import detailNavbar from './components/detail/detail-navbar.vue';
import detailCellSku from './components/detail/detail-cell-sku.vue';
import detailTabbar from './components/detail/detail-tabbar.vue';
import detailSkeleton from './components/detail/detail-skeleton.vue';
import detailCommentCard from './components/detail/detail-comment-card.vue';
import detailContentCard from './components/detail/detail-content-card.vue';
import SpuApi from '@/sheep/api/product/spu';
import { PromotionActivityTypeEnum, SharePageEnum } from '@/sheep/helper/const';
import PointApi from '@/sheep/api/promotion/point';
const headerBg = sheep.$url.css('/static/img/shop/goods/score-bg.png');
const btnBg = sheep.$url.css('/static/img/shop/goods/seckill-btn.png');
const disabledBtnBg = sheep.$url.css('/static/img/shop/goods/activity-btn-disabled.png');
const seckillBg = sheep.$url.css('/static/img/shop/goods/seckill-tip-bg.png');
const grouponBg = sheep.$url.css('/static/img/shop/goods/groupon-tip-bg.png');
onPageScroll(() => {});
const state = reactive({
skeletonLoading: true,
goodsInfo: {},
showSelectSku: false,
goodsSwiper: [],
selectedSku: {},
showModel: false,
total: 0,
price: '',
});
// 规格变更
function onSkuChange(e) {
state.selectedSku = e;
}
// 立即购买
function onBuy(sku) {
sheep.$router.go('/pages/order/confirm', {
data: JSON.stringify({
order_type: 'goods',
buy_type: 'point',
pointActivityId: activity.value.id,
items: [
{
skuId: sku.id,
count: sku.count,
},
],
}),
});
}
// 分享信息
const shareInfo = computed(() => {
if (isEmpty(unref(activity))) return {};
return sheep.$platform.share.getShareInfo(
{
title: activity.value.name,
image: sheep.$url.cdn(state.goodsInfo.picUrl),
params: {
page: SharePageEnum.POINT.value,
query: activity.value.id,
},
},
{
type: 'goods', // 商品海报
title: activity.value.name, // 商品标题
image: sheep.$url.cdn(state.goodsInfo.picUrl), // 商品主图
price: (getShowPrice.value.price || 0) + ` + ${getShowPrice.value.point} 积分`, // 积分价格
marketPrice: fen2yuan(state.goodsInfo.marketPrice), // 商品原价
},
);
});
const activity = ref();
const getShowPrice = computed(() => {
if (!isEmpty(state.selectedSku)) {
const sku = state.selectedSku;
return {
point: sku.point,
price: !sku.pointPrice ? '' : fen2yuan(sku.pointPrice),
};
}
return {
point: activity.value.point,
price: !activity.value.price ? '' : fen2yuan(activity.value.price),
};
});
// 查询活动
const getActivity = async (id) => {
const { data } = await PointApi.getPointActivity(id);
if (!data) {
state.goodsInfo = null;
state.skeletonLoading = false;
return;
}
activity.value = data;
// 查询商品
await getSpu(data.spuId);
};
// 查询商品
const getSpu = async (id) => {
const { data } = await SpuApi.getSpuDetail(id);
if (!data) {
state.goodsInfo = null;
state.skeletonLoading = false;
return;
}
data.activity_type = PromotionActivityTypeEnum.POINT.type;
state.goodsInfo = data;
state.goodsInfo.stock = Math.min(data.stock, activity.value.stock);
// 处理轮播图
state.goodsSwiper = formatGoodsSwiper(state.goodsInfo.sliderPicUrls);
// 价格、库存使用活动的
data.skus.forEach((sku) => {
const product = activity.value.products.find((product) => product.skuId === sku.id);
if (product) {
sku.point = product.point;
sku.pointPrice = product.price;
sku.stock = Math.min(sku.stock, product.stock);
// 设置限购数量
sku.limitCount = product.count;
} else {
// 找不到可能是没配置
sku.stock = 0;
}
});
state.skeletonLoading = false;
};
onLoad((options) => {
// 非法参数
if (!options.id) {
state.goodsInfo = null;
state.skeletonLoading = false;
return;
}
// 查询活动
getActivity(options.id);
});
</script>
<style lang="scss" scoped>
.disabled-btn-box[disabled] {
background-color: transparent;
}
.detail-card {
background-color: $white;
margin: 14rpx 20rpx;
border-radius: 10rpx;
overflow: hidden;
}
// 价格标题卡片
.title-card {
width: 710rpx;
box-sizing: border-box;
background-size: 100% 100%;
border-radius: 10rpx;
background-image: v-bind(headerBg);
background-repeat: no-repeat;
.price-box {
.point-img {
width: 36rpx;
height: 36rpx;
margin: 0 4rpx;
}
.point-text {
font-size: 42rpx;
font-weight: 500;
color: #ff3000;
line-height: 36rpx;
font-family: OPPOSANS;
}
.price-text {
font-size: 42rpx;
font-weight: 500;
color: #ff3000;
line-height: 36rpx;
font-family: OPPOSANS;
}
}
.origin-price-text {
font-size: 26rpx;
font-weight: 400;
text-decoration: line-through;
color: $gray-c;
font-family: OPPOSANS;
}
.sales-text {
font-size: 26rpx;
font-weight: 500;
color: $gray-c;
}
.discounts-box {
.discounts-tag {
padding: 4rpx 10rpx;
font-size: 24rpx;
font-weight: 500;
border-radius: 4rpx;
color: var(--ui-BG-Main);
// background: rgba(#2aae67, 0.05);
background: var(--ui-BG-Main-tag);
}
.discounts-title {
font-size: 24rpx;
font-weight: 500;
color: var(--ui-BG-Main);
line-height: normal;
}
.cicon-forward {
color: var(--ui-BG-Main);
font-size: 24rpx;
line-height: normal;
margin-top: 4rpx;
}
}
.title-text {
font-size: 30rpx;
font-weight: bold;
line-height: 42rpx;
}
.subtitle-text {
font-size: 26rpx;
font-weight: 400;
color: $dark-9;
line-height: 42rpx;
}
}
// 购买
.buy-box {
.check-btn-box {
width: 248rpx;
height: 80rpx;
font-size: 24rpx;
font-weight: 600;
margin-left: -36rpx;
background-image: v-bind(btnBg);
background-repeat: no-repeat;
background-size: 100% 100%;
color: #ffffff;
line-height: normal;
border-radius: 0px 40rpx 40rpx 0px;
}
.disabled-btn-box {
width: 248rpx;
height: 80rpx;
font-size: 24rpx;
font-weight: 600;
margin-left: -36rpx;
background-image: v-bind(disabledBtnBg);
background-repeat: no-repeat;
background-size: 100% 100%;
color: #999999;
line-height: normal;
border-radius: 0px 40rpx 40rpx 0px;
}
.btn-price {
font-family: OPPOSANS;
&::before {
content: '¥';
}
}
.origin-price-btn {
width: 236rpx;
height: 80rpx;
background: rgba(#ff5651, 0.1);
color: #ff6000;
border-radius: 40rpx 0px 0px 40rpx;
line-height: normal;
font-size: 24rpx;
font-weight: 500;
.no-original {
font-size: 28rpx;
}
.btn-title {
font-size: 28rpx;
}
}
}
//秒杀卡片
.seckill-box {
background: v-bind(seckillBg) no-repeat;
background-size: 100% 100%;
}
.groupon-box {
background: v-bind(grouponBg) no-repeat;
background-size: 100% 100%;
}
//活动卡片
.activity-box {
width: 100%;
height: 80rpx;
box-sizing: border-box;
margin-bottom: 10rpx;
.activity-title {
font-size: 26rpx;
font-weight: 500;
color: #ffffff;
line-height: 42rpx;
.activity-icon {
width: 38rpx;
height: 38rpx;
}
}
.activity-go {
width: 70rpx;
height: 32rpx;
background: #ffffff;
border-radius: 16rpx;
font-weight: 500;
color: #ff6000;
font-size: 24rpx;
line-height: normal;
}
}
.model-box {
.title {
font-size: 36rpx;
font-weight: bold;
color: #333333;
}
.subtitle {
font-size: 26rpx;
font-weight: 500;
color: #333333;
}
}
</style>
+575
View File
@@ -0,0 +1,575 @@
<!-- 秒杀商品详情 -->
<template>
<s-layout :onShareAppMessage="shareInfo" navbar="goods">
<!-- 标题栏 -->
<detailNavbar />
<!-- 骨架屏 -->
<detailSkeleton v-if="state.skeletonLoading" />
<!-- 下架/售罄提醒 -->
<s-empty
v-else-if="
state.goodsInfo === null || state.goodsInfo.activity_type !== 'seckill' || endTime.ms <= 0
"
text="活动不存在或已结束"
icon="/static/soldout-empty.png"
showAction
actionText="再逛逛"
actionUrl="/pages/goods/list"
/>
<block v-else>
<view class="detail-swiper-selector">
<!-- 商品图轮播 -->
<su-swiper
class="ss-m-b-14"
isPreview
:list="state.goodsSwiper"
dotStyle="tag"
imageMode="widthFix"
dotCur="bg-mask-40"
:seizeHeight="750"
/>
<!-- 价格+标题 -->
<view class="title-card ss-m-y-14 ss-m-x-20 ss-p-x-20 ss-p-y-34">
<view class="price-box ss-flex ss-row-between ss-m-b-18">
<view class="ss-flex">
<view class="price-text ss-m-r-16">
{{ fen2yuan(state.selectedSku.price || state.goodsInfo.price) }}
</view>
<view class="tig ss-flex ss-col-center">
<view class="tig-icon ss-flex ss-col-center ss-row-center">
<text class="cicon-alarm"></text>
</view>
<view class="tig-title">秒杀价</view>
</view>
</view>
<view class="countdown-box" v-if="endTime.ms > 0">
<view class="countdown-title ss-m-b-20">距结束仅剩</view>
<view class="ss-flex countdown-time">
<view class="ss-flex countdown-h">{{ endTime.h }}</view>
<view class="ss-m-x-4">:</view>
<view class="countdown-num ss-flex ss-row-center">{{ endTime.m }}</view>
<view class="ss-m-x-4">:</view>
<view class="countdown-num ss-flex ss-row-center">{{ endTime.s }}</view>
</view>
</view>
<view class="countdown-title" v-else> 活动已结束 </view>
</view>
<view class="ss-flex ss-row-between ss-m-b-60">
<view class="origin-price ss-flex ss-col-center" v-if="state.goodsInfo.marketPrice">
原价
<view class="origin-price-text">
{{ fen2yuan(state.selectedSku.marketPrice || state.goodsInfo.marketPrice) }}
</view>
</view>
<detail-progress :percent="state.percent" />
</view>
<view class="title-text ss-line-2 ss-m-b-6">{{ state.goodsInfo.name || '' }}</view>
<view class="subtitle-text ss-line-1">{{ state.goodsInfo.introduction }}</view>
</view>
<!-- 功能卡片 -->
<view class="detail-cell-card detail-card ss-flex-col">
<detail-cell-sku :sku="state.selectedSku" @tap="state.showSelectSku = true" />
</view>
<!-- 规格与数量弹框 -->
<s-select-seckill-sku
v-model="state.goodsInfo"
:show="state.showSelectSku"
:single-limit-count="activity.singleLimitCount"
@buy="onBuy"
@change="onSkuChange"
@close="state.showSelectSku = false"
/>
</view>
<!-- 评价 -->
<detail-comment-card class="detail-comment-selector" :goodsId="state.goodsInfo.id" />
<!-- 详情 -->
<detail-content-card class="detail-content-selector" :content="state.goodsInfo.description" />
<!-- 详情tabbar -->
<detail-tabbar v-model="state.goodsInfo">
<!-- TODO: 缺货中 已售罄 判断 设计-->
<view class="buy-box ss-flex ss-col-center ss-p-r-20">
<button
class="ss-reset-button origin-price-btn ss-flex-col"
v-if="state.goodsInfo.marketPrice"
@tap="sheep.$router.go('/pages/goods/index', { id: state.goodsInfo.id })"
>
<view>
<view class="btn-price">{{ fen2yuan(state.goodsInfo.marketPrice) }}</view>
<view>原价购买</view>
</view>
</button>
<button v-else class="ss-reset-button origin-price-btn ss-flex-col">
<view
class="no-original"
:class="
state.goodsInfo.stock === 0 || timeStatusEnum !== TimeStatusEnum.STARTED ? '' : ''
"
>
秒杀价
</view>
</button>
<button
class="ss-reset-button btn-box ss-flex-col"
@tap="state.showSelectSku = true"
:class="
timeStatusEnum === TimeStatusEnum.STARTED && state.goodsInfo.stock != 0
? 'check-btn-box'
: 'disabled-btn-box'
"
:disabled="state.goodsInfo.stock === 0 || timeStatusEnum !== TimeStatusEnum.STARTED"
>
<view class="btn-price">{{ fen2yuan(state.goodsInfo.price) }}</view>
<view v-if="timeStatusEnum === TimeStatusEnum.STARTED">
<view v-if="state.goodsInfo.stock === 0">已售罄</view>
<view v-else>立即秒杀</view>
</view>
<view v-else>{{ timeStatusEnum }}</view>
</button>
</view>
</detail-tabbar>
</block>
</s-layout>
</template>
<script setup>
import { computed, reactive, ref, unref } from 'vue';
import { onLoad, onPageScroll } from '@dcloudio/uni-app';
import sheep from '@/sheep';
import { isEmpty, min } from 'lodash-es';
import { fen2yuan, formatGoodsSwiper, useDurationTime } from '@/sheep/hooks/useGoods';
import detailNavbar from './components/detail/detail-navbar.vue';
import detailCellSku from './components/detail/detail-cell-sku.vue';
import detailTabbar from './components/detail/detail-tabbar.vue';
import detailSkeleton from './components/detail/detail-skeleton.vue';
import detailCommentCard from './components/detail/detail-comment-card.vue';
import detailContentCard from './components/detail/detail-content-card.vue';
import detailProgress from './components/detail/detail-progress.vue';
import SeckillApi from '@/sheep/api/promotion/seckill';
import SpuApi from '@/sheep/api/product/spu';
import { getTimeStatusEnum, SharePageEnum, TimeStatusEnum } from '@/sheep/helper/const';
const headerBg = sheep.$url.css('/static/img/shop/goods/seckill-bg.png');
const btnBg = sheep.$url.css('/static/img/shop/goods/seckill-btn.png');
const disabledBtnBg = sheep.$url.css('/static/img/shop/goods/activity-btn-disabled.png');
const seckillBg = sheep.$url.css('/static/img/shop/goods/seckill-tip-bg.png');
const grouponBg = sheep.$url.css('/static/img/shop/goods/groupon-tip-bg.png');
onPageScroll(() => {});
const state = reactive({
skeletonLoading: true,
goodsInfo: {},
showSelectSku: false,
goodsSwiper: [],
selectedSku: {},
showModel: false,
total: 0,
percent: 0,
price: '',
});
const endTime = computed(() => {
return useDurationTime(activity.value.endTime);
});
// 规格变更
function onSkuChange(e) {
state.selectedSku = e;
}
// 立即购买
function onBuy(sku) {
sheep.$router.go('/pages/order/confirm', {
data: JSON.stringify({
order_type: 'goods',
buy_type: 'seckill',
seckillActivityId: activity.value.id,
items: [
{
skuId: sku.id,
count: sku.count,
},
],
}),
});
}
// 分享信息
const shareInfo = computed(() => {
if (isEmpty(unref(activity))) return {};
return sheep.$platform.share.getShareInfo(
{
title: activity.value.name,
image: sheep.$url.cdn(state.goodsInfo.picUrl),
params: {
page: SharePageEnum.SECKILL.value,
query: activity.value.id,
},
},
{
type: 'goods', // 商品海报
title: activity.value.name, // 商品标题
image: sheep.$url.cdn(state.goodsInfo.picUrl), // 商品主图
price: fen2yuan(state.goodsInfo.price), // 商品价格
marketPrice: fen2yuan(state.goodsInfo.marketPrice), // 商品原价
},
);
});
const activity = ref();
const timeStatusEnum = ref('');
// 查询活动
const getActivity = async (id) => {
const { data } = await SeckillApi.getSeckillActivity(id);
if (!data) {
state.goodsInfo = null;
state.skeletonLoading = false;
return;
}
activity.value = data;
timeStatusEnum.value = getTimeStatusEnum(activity.value.startTime, activity.value.endTime);
state.percent = 100 - (data.stock / data.totalStock) * 100;
// 查询商品
await getSpu(data.spuId);
};
// 查询商品
const getSpu = async (id) => {
const { data } = await SpuApi.getSpuDetail(id);
if (!data) {
state.goodsInfo = null;
state.skeletonLoading = false;
return;
}
data.activity_type = 'seckill';
state.goodsInfo = data;
// 处理轮播图
state.goodsSwiper = formatGoodsSwiper(state.goodsInfo.sliderPicUrls);
// 默认显示最低价
state.goodsInfo.price = min([
state.goodsInfo.price,
...activity.value.products.map((spu) => spu.seckillPrice),
]);
// 价格、库存使用活动的
data.skus.forEach((sku) => {
const product = activity.value.products.find((product) => product.skuId === sku.id);
if (product) {
sku.price = product.seckillPrice;
sku.stock = Math.min(sku.stock, product.stock);
} else {
// 找不到可能是没配置,则不能发起秒杀
sku.stock = 0;
}
// 设置限购数量
if (activity.value.totalLimitCount > 0 && activity.value.singleLimitCount > 0) {
sku.limitCount = Math.min(activity.value.totalLimitCount, activity.value.singleLimitCount);
} else if (activity.value.totalLimitCount > 0) {
sku.limitCount = activity.value.totalLimitCount;
} else if (activity.value.singleLimitCount > 0) {
sku.limitCount = activity.value.singleLimitCount;
}
});
state.skeletonLoading = false;
};
onLoad((options) => {
// 非法参数
if (!options.id) {
state.goodsInfo = null;
state.skeletonLoading = false;
return;
}
// 查询活动
getActivity(options.id);
});
</script>
<style lang="scss" scoped>
.disabled-btn-box[disabled] {
background-color: transparent;
}
.detail-card {
background-color: $white;
margin: 14rpx 20rpx;
border-radius: 10rpx;
overflow: hidden;
}
// 价格标题卡片
.title-card {
width: 710rpx;
box-sizing: border-box;
// height: 320rpx;
background-size: 100% 100%;
border-radius: 10rpx;
background-image: v-bind(headerBg);
background-repeat: no-repeat;
.price-box {
.price-text {
font-size: 30rpx;
font-weight: 500;
color: #fff;
line-height: normal;
font-family: OPPOSANS;
&::before {
content: '¥';
font-size: 30rpx;
}
}
}
.origin-price {
font-size: 24rpx;
font-weight: 400;
color: #fff;
opacity: 0.7;
.origin-price-text {
text-decoration: line-through;
font-family: OPPOSANS;
&::before {
content: '¥';
}
}
}
.tig {
border: 2rpx solid #ffffff;
border-radius: 4rpx;
width: 126rpx;
height: 38rpx;
.tig-icon {
width: 40rpx;
height: 40rpx;
margin-left: -2rpx;
background: #ffffff;
border-radius: 4rpx 0 0 4rpx;
.cicon-alarm {
font-size: 32rpx;
color: #fc6e6f;
}
}
.tig-title {
width: 86rpx;
font-size: 24rpx;
font-weight: 500;
line-height: normal;
color: #ffffff;
display: flex;
justify-content: center;
align-items: center;
}
}
.countdown-title {
font-size: 26rpx;
font-weight: 500;
color: #ffffff;
}
.countdown-time {
font-size: 26rpx;
font-weight: 500;
color: #ffffff;
.countdown-h {
font-size: 24rpx;
font-family: OPPOSANS;
font-weight: 500;
color: #ffffff;
padding: 0 4rpx;
height: 40rpx;
background: rgba(#000000, 0.1);
border-radius: 6rpx;
}
.countdown-num {
font-size: 24rpx;
font-family: OPPOSANS;
font-weight: 500;
color: #ffffff;
width: 40rpx;
height: 40rpx;
background: rgba(#000000, 0.1);
border-radius: 6rpx;
}
}
.discounts-box {
.discounts-tag {
padding: 4rpx 10rpx;
font-size: 24rpx;
font-weight: 500;
border-radius: 4rpx;
color: var(--ui-BG-Main);
// background: rgba(#2aae67, 0.05);
background: var(--ui-BG-Main-tag);
}
.discounts-title {
font-size: 24rpx;
font-weight: 500;
color: var(--ui-BG-Main);
line-height: normal;
}
.cicon-forward {
color: var(--ui-BG-Main);
font-size: 24rpx;
line-height: normal;
margin-top: 4rpx;
}
}
.title-text {
font-size: 30rpx;
font-weight: bold;
line-height: 42rpx;
color: #fff;
}
.subtitle-text {
font-size: 26rpx;
font-weight: 400;
color: #ffffff;
line-height: 42rpx;
opacity: 0.9;
}
}
// 购买
.buy-box {
.check-btn-box {
width: 248rpx;
height: 80rpx;
font-size: 24rpx;
font-weight: 600;
margin-left: -36rpx;
background-image: v-bind(btnBg);
background-repeat: no-repeat;
background-size: 100% 100%;
color: #ffffff;
line-height: normal;
border-radius: 0px 40rpx 40rpx 0px;
}
.disabled-btn-box {
width: 248rpx;
height: 80rpx;
font-size: 24rpx;
font-weight: 600;
margin-left: -36rpx;
background-image: v-bind(disabledBtnBg);
background-repeat: no-repeat;
background-size: 100% 100%;
color: #999999;
line-height: normal;
border-radius: 0px 40rpx 40rpx 0px;
}
.btn-price {
font-family: OPPOSANS;
&::before {
content: '¥';
}
}
.origin-price-btn {
width: 236rpx;
height: 80rpx;
background: rgba(#ff5651, 0.1);
color: #ff6000;
border-radius: 40rpx 0px 0px 40rpx;
line-height: normal;
font-size: 24rpx;
font-weight: 500;
.no-original {
font-size: 28rpx;
}
.btn-title {
font-size: 28rpx;
}
}
}
//秒杀卡片
.seckill-box {
background: v-bind(seckillBg) no-repeat;
background-size: 100% 100%;
}
.groupon-box {
background: v-bind(grouponBg) no-repeat;
background-size: 100% 100%;
}
//活动卡片
.activity-box {
width: 100%;
height: 80rpx;
box-sizing: border-box;
margin-bottom: 10rpx;
.activity-title {
font-size: 26rpx;
font-weight: 500;
color: #ffffff;
line-height: 42rpx;
.activity-icon {
width: 38rpx;
height: 38rpx;
}
}
.activity-go {
width: 70rpx;
height: 32rpx;
background: #ffffff;
border-radius: 16rpx;
font-weight: 500;
color: #ff6000;
font-size: 24rpx;
line-height: normal;
}
}
.model-box {
.title {
font-size: 36rpx;
font-weight: bold;
color: #333333;
}
.subtitle {
font-size: 26rpx;
font-weight: 500;
color: #333333;
}
}
image {
width: 100%;
height: 100%;
}
</style>
+315
View File
@@ -0,0 +1,315 @@
<template>
<s-layout :bgStyle="{ color: '#fff' }" tabbar="/pages/index/cart" title="购物车">
<s-empty
v-if="state.list.length === 0"
icon="/static/cart-empty.png"
text="购物车空空如也,快去逛逛吧~"
/>
<!-- 头部 -->
<view v-if="state.list.length" class="cart-box ss-flex ss-flex-col ss-row-between">
<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="onChangeEditMode(false)">
取消
</button>
<button v-else class="ss-reset-button ui-TC-Main" @tap="onChangeEditMode(true)">
编辑
</button>
</view>
</view>
<!-- 内容 -->
<view class="cart-content ss-flex-1 ss-p-x-30 ss-m-b-40">
<view v-for="item in state.list" :key="item.id" class="goods-box ss-r-10 ss-m-b-14">
<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>
<view v-if="item.spu?.status !== 1 && !state.editMode" class="down-box">
该商品已下架
</view>
<view v-else-if="item.spu?.stock <= 0 && !state.editMode" class="down-box">
该商品无库存
</view>
<s-goods-item
: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
"
:title="item.spu.name"
:titleWidth="400"
priceColor="#FF3000"
>
<template v-if="!state.editMode" v-slot:tool>
<su-number-box
v-model="item.count"
:max="item.sku.stock"
:min="0"
:step="1"
@change="onNumberChange($event, item)"
/>
</template>
</s-goods-item>
</view>
</view>
</view>
<!-- 底部 -->
<su-fixed v-if="state.list.length > 0" :isInset="false" :val="48" bottom placeholder>
<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 { onShow } from '@dcloudio/uni-app';
import SpuApi from '@/sheep/api/product/spu';
import { computed, reactive } from 'vue';
import { fen2yuan } from '@/sheep/hooks/useGoods';
import { isEmpty } from '@/sheep/helper/utils';
// 隐藏原生tabBar
uni.hideTabBar({
fail: () => {},
});
const sys_navBar = sheep.$platform.navbar;
const cart = sheep.$store('cart');
const state = reactive({
editMode: computed(() => cart.editMode),
list: computed(() => cart.list),
selectedList: [],
selectedIds: computed(() => cart.selectedIds),
isAllSelected: computed(() => cart.isAllSelected),
totalPriceSelected: computed(() => cart.totalPriceSelected),
});
// 单选选中
function onSelectSingle(id) {
cart.selectSingle(id);
}
// 编辑、取消
function onChangeEditMode(flag) {
cart.onChangeEditMode(flag);
}
// 全选
function onSelectAll() {
cart.selectAll(!state.isAllSelected);
}
// 结算
async function onConfirm() {
const items = [];
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,
});
});
if (isEmpty(items)) {
sheep.$helper.toast('请先选择商品');
return;
}
await validateDeliveryType(state.selectedList.map((item) => item.spu).map((spu) => spu.id));
sheep.$router.go('/pages/order/confirm', {
data: JSON.stringify({
items,
}),
});
}
/**
* 校验配送方式冲突
*
* @param {string[]} spuIds - 商品ID数组
* @returns {Promise<void>}
* @throws {Error} 当配送方式冲突或获取商品信息失败时抛出错误
*/
async function validateDeliveryType(spuIds) {
// 获取商品信息
const { data: spuList } = await SpuApi.getSpuListByIds(spuIds.join(','));
if (isEmpty(spuList)) {
sheep.$helper.toast('未找到商品信息');
throw new Error('未找到商品信息');
}
// 获取所有商品的配送方式列表
const deliveryTypesList = spuList.map((item) => item.deliveryTypes);
// 检查配送方式冲突
const hasConflict = checkDeliveryConflicts(deliveryTypesList);
if (hasConflict) {
sheep.$helper.toast('选中商品支持的配送方式冲突,不允许提交');
throw new Error('选中商品支持的配送方式冲突,不允许提交');
}
}
/**
* 检查配送方式列表中是否存在冲突
* @description
* 示例场景:
* A 商品支持:[快递, 自提]
* B 商品支持:[快递]
* C 商品支持:[自提]
*
* 对比结果:
* A 和 B:不冲突 (有交集:快递)
* A 和 C:不冲突 (有交集:自提)
* B 和 C:冲突 (无交集)
* @param {Array<Array<number>>} deliveryTypesList - 配送方式列表的数组
* @returns {boolean} 是否存在冲突
*/
function checkDeliveryConflicts(deliveryTypesList) {
for (let i = 0; i < deliveryTypesList.length - 1; i++) {
const currentTypes = deliveryTypesList[i];
for (let j = i + 1; j < deliveryTypesList.length; j++) {
const nextTypes = deliveryTypesList[j];
// 检查是否没有交集(即冲突)
const hasNoIntersection = !currentTypes.some((type) => nextTypes.includes(type));
if (hasNoIntersection) {
return true;
}
}
}
return false;
}
function onNumberChange(e, cartItem) {
if (e === 0) {
cart.delete(cartItem.id);
return;
}
if (cartItem.goods_num === e) return;
cartItem.goods_num = e;
cart.update({
goods_id: cartItem.id,
goods_num: e,
goods_sku_price_id: cartItem.goods_sku_price_id,
});
}
async function onDelete() {
cart.delete(state.selectedIds);
}
function getCartList() {
cart.getList();
}
onShow(() => {
getCartList();
});
</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;
position: relative;
}
// 下架商品
.down-box {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(#fff, 0.8);
z-index: 2;
display: flex;
justify-content: center;
align-items: center;
color: #999;
font-size: 32rpx;
}
}
}
</style>
+240
View File
@@ -0,0 +1,240 @@
<!-- 商品分类列表 -->
<template>
<s-layout :bgStyle="{ color: '#fff' }" tabbar="/pages/index/category" title="分类">
<view class="s-category">
<view class="three-level-wrap ss-flex ss-col-top">
<!-- 商品分类 -->
<view class="side-menu-wrap" :style="[{ top: Number(statusBarHeight + 88) + 'rpx' }]">
<scroll-view 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>
</view>
<!-- 商品分类 -->
<view class="goods-list-box" v-if="state.categoryList?.length">
<scroll-view scroll-y :style="[{ height: pageHeight + 'px' }]">
<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>
</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 } from '@dcloudio/uni-app';
import { computed, reactive } from 'vue';
import _ from 'lodash-es';
import { handleTree } from '@/sheep/helper/utils';
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);
const statusBarHeight = sheep.$platform.device.statusBarHeight * 2;
// 加载商品分类
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 === Number(params.id));
// 如果找到则调用 onMenu 自动勾选相应分类,否则调用 onMenu(0) 勾选第一个分类
onMenu(foundCategory ? state.categoryList.indexOf(foundCategory) : 0);
});
function handleScrollToLower() {
loadMore();
}
</script>
<style lang="scss" scoped>
.s-category {
:deep() {
.side-menu-wrap {
width: 200rpx;
height: 100%;
padding-left: 12rpx;
background-color: #f6f6f6;
position: fixed;
left: 0;
.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 - 200rpx);
padding: 10px;
margin-left: 200rpx;
}
.banner-img {
width: calc(100vw - 130px);
border-radius: 5px;
margin-bottom: 20rpx;
}
}
}
</style>
+26
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>
+66
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.name }}</view>
<view class="goods-price">{{ fen2yuan(item.price) }}</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import sheep from '@/sheep';
import { fen2yuan } from '@/sheep/hooks/useGoods';
const props = defineProps({
pagination: Object,
});
</script>
<style lang="scss" scoped>
.goods-box {
width: calc((100% - 20rpx) / 2);
margin-bottom: 20rpx;
.goods-img {
width: 100%;
height: 246rpx;
border-radius: 10rpx 10rpx 0px 0px;
}
.goods-content {
width: 100%;
background: #ffffff;
box-shadow: 0px 0px 20rpx 4rpx rgba(199, 199, 199, 0.22);
padding: 20rpx 0 32rpx 16rpx;
box-sizing: border-box;
border-radius: 0 0 10rpx 10rpx;
.goods-title {
font-size: 26rpx;
font-weight: bold;
color: #333333;
}
.goods-price {
font-size: 24rpx;
font-family: OPPOSANS;
font-weight: 500;
color: #e1212b;
}
}
&:nth-child(2n + 1) {
margin-right: 20rpx;
}
}
</style>
+80
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>
+93
View File
@@ -0,0 +1,93 @@
<!-- 首页支持店铺装修 -->
<template>
<view v-if="template">
<s-layout
title="首页"
navbar="custom"
tabbar="/pages/index/index"
:bgStyle="template.page"
:navbarStyle="template.navigationBar"
onShareAppMessage
>
<s-block
v-for="(item, index) in template.components"
:key="index"
:styles="item.property.style"
>
<s-block-item :type="item.id" :data="item.property" :styles="item.property.style" />
</s-block>
</s-layout>
</view>
</template>
<script setup>
import { computed } from 'vue';
import { onLoad, onPageScroll, onPullDownRefresh } from '@dcloudio/uni-app';
import sheep from '@/sheep';
import $share from '@/sheep/platform/share';
// 隐藏原生tabBar
uni.hideTabBar({
fail: () => {},
});
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
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
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
View File
@@ -0,0 +1,119 @@
<!-- 搜索界面 -->
<template>
<s-layout :bgStyle="{ color: '#FFF' }" class="set-wrap" title="搜索">
<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>
+47
View File
@@ -0,0 +1,47 @@
<!-- 个人中心支持装修 -->
<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({
fail: () => {},
});
const template = computed(() => sheep.$store('app').template.user);
onShow(() => {
sheep.$store('user').updateUserData();
});
onPullDownRefresh(() => {
sheep.$store('user').updateUserData();
setTimeout(function () {
uni.stopPullDownRefresh();
}, 800);
});
onPageScroll(() => {});
</script>
<style></style>
+207
View File
@@ -0,0 +1,207 @@
<template>
<view class="login-page-wrapper">
<view class="mask" />
<view class="login-ctn">
<image class="login-logo-image" src="/static/imgs/login-logo.png" />
<view class="formItem-box mobile-formItem">
<view class="prefix">+86</view>
<uni-easyinput
placeholder="请输入手机号码"
v-model="state.model.mobile"
:inputBorder="false"
type="number"
/>
</view>
<view class="formItem-box code-formItem">
<uni-easyinput
placeholder="请输入验证码"
v-model="state.model.code"
:inputBorder="false"
type="number"
maxlength="4"
/>
<view class="suffix" @click="checkAgreementAndGetSmsCode">
{{ getSmsTimer('smsLogin') }}
</view>
</view>
<view class="agreement-text">
<checkbox-group @change="handleAgreementChange">
<label>
<checkbox :value="true" fontSize="12" />
</label>
</checkbox-group>
已同意用户条款隐私政策
</view>
<view class="login-btn" @click="smsLoginSubmit">登录</view>
</view>
</view>
</template>
<script setup>
import { ref, reactive, unref } from 'vue';
import { code, mobile } from '@/sheep/validate/form';
import { showAuthModal, closeAuthModal, getSmsCode, getSmsTimer } from '@/sheep/hooks/useModal';
import sheep from '@/sheep';
import AuthUtil from '@/sheep/api/member/auth';
const agreeStatus = ref(false);
// 数据
const state = reactive({
model: {
mobile: '', // 手机号
code: '', // 验证码
},
rules: {
code,
mobile,
},
});
const handleAgreementChange = (e) => {
const values = e.detail.value;
agreeStatus.value = values[0] || false;
};
// 检查协议并获取验证码
function checkAgreementAndGetSmsCode() {
if (!state.model.mobile) {
sheep.$helper.toast('请输入手机号码');
return;
}
if (!agreeStatus.value) {
sheep.$helper.toast('请选择是否同意协议');
return;
}
getSmsCode('smsLogin', state.model.mobile);
}
// 短信登录
async function smsLoginSubmit() {
if (!state.model.mobile) {
sheep.$helper.toast('请输入手机号码');
return;
}
if (!state.model.code) {
sheep.$helper.toast('请输入验证码');
return;
}
if (!agreeStatus.value) {
sheep.$helper.toast('请选择是否同意协议');
return;
}
// 提交数据
const { code } = await AuthUtil.smsLogin(state.model);
if (code === 0) {
sheep.$router.go('/pages/index/index', {}, { redirect: true });
}
}
</script>
<style lang="scss" scoped>
.login-page-wrapper {
width: 100%;
height: 100%;
box-sizing: border-box;
background-image: url('/static/imgs/login-bg.jpg');
background-size: cover;
background-position: center;
position: relative;
.mask {
width: 100%;
height: 100%;
background: rgba(18, 19, 20, 0.43);
}
.login-ctn {
width: calc(100% - 110rpx);
height: 500rpx;
background: #fff;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
.login-logo-image {
width: 118rpx;
height: 70rpx;
position: absolute;
left: calc(50% - 59rpx);
top: 16rpx;
}
.formItem-box {
width: calc(100% - 110rpx);
height: 80rpx;
border: 2rpx solid #000;
}
.mobile-formItem {
position: absolute;
left: 50%;
top: 116rpx;
transform: translateX(-50%);
display: flex;
align-items: center;
.prefix {
width: 100rpx;
flex-shrink: 0;
text-align: center;
border-right: 1rpx solid #ccc;
margin-right: 20rpx;
}
}
.code-formItem {
position: absolute;
left: 50%;
top: 212rpx;
transform: translateX(-50%);
display: flex;
align-items: center;
padding: 0 20rpx;
box-sizing: border-box;
.suffix {
width: 100rpx;
flex-shrink: 0;
text-align: center;
border-left: 1rpx solid #ccc;
padding-left: 20rpx;
}
}
.agreement-text {
width: 100%;
color: #c4c4c4;
font-size: 24rpx;
position: absolute;
left: 50%;
top: 310rpx;
transform: translateX(-50%);
display: flex;
justify-content: center;
align-items: center;
}
.login-btn {
width: calc(100% - 400rpx);
height: 80rpx;
line-height: 80rpx;
text-align: center;
background: #000;
color: #fff;
font-size: 28rpx;
position: absolute;
left: 50%;
bottom: 32rpx;
transform: translateX(-50%);
}
}
}
</style>
+282
View File
@@ -0,0 +1,282 @@
<!-- 下单界面收货地址 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-if="state.deliveryType === 2"
@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/img/shop/line.png')" />
</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>
+349
View File
@@ -0,0 +1,349 @@
<!-- 售后申请 -->
<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="客官~请描述您遇到的问题,建议上传照片"
/>
<view class="upload-img">
<s-uploader
v-model:url="formData.applyPicUrls"
fileMediatype="image"
limit="9"
mode="grid"
:imageStyles="{ width: '168rpx', height: '168rpx' }"
/>
</view>
</view>
</view>
</uni-forms>
<!-- 底部按钮 -->
<su-fixed bottom placeholder>
<view class="foot-wrap">
<view class="foot_box ss-flex ss-col-center ss-row-between ss-p-x-30">
<button class="ss-reset-button contcat-btn" @tap="sheep.$router.go('/pages/chat/index')">
联系客服
</button>
<button class="ss-reset-button ui-BG-Main-Gradient sub-btn" @tap="submit">提交</button>
</view>
</view>
</su-fixed>
<!-- 申请原因弹窗 -->
<su-popup :show="state.showModal" round="10" :showClose="true" @close="state.showModal = false">
<view class="modal-box page_box">
<view class="modal-head item-title head_box ss-flex ss-row-center ss-col-center">
申请原因
</view>
<view class="modal-content content_box">
<radio-group @change="onChange">
<label class="radio ss-flex ss-col-center" v-for="item in state.reasonList" :key="item">
<view class="ss-flex-1 ss-p-20">{{ item }}</view>
<radio
:value="item"
color="var(--ui-BG-Main)"
:checked="item === state.currentValue"
/>
</label>
</radio-group>
</view>
<view class="modal-foot foot_box ss-flex ss-row-center ss-col-center">
<button class="ss-reset-button close-btn ui-BG-Main-Gradient" @tap="onReason">
确定
</button>
</view>
</view>
</su-popup>
</s-layout>
</template>
<script setup>
import sheep from '@/sheep';
import { onLoad } from '@dcloudio/uni-app';
import { reactive, ref } from 'vue';
import OrderApi from '@/sheep/api/trade/order';
import TradeConfigApi from '@/sheep/api/trade/config';
import { fen2yuan } from '@/sheep/hooks/useGoods';
import AfterSaleApi from '@/sheep/api/trade/afterSale';
const form = ref(null);
const state = reactive({
orderId: 0, // 订单编号
itemId: 0, // 订单项编号
order: {}, // 订单
item: {}, // 订单项
config: {}, // 交易配置
// 售后类型
wayList: [
{
text: '仅退款',
value: '10',
},
{
text: '退款退货',
value: '20',
},
],
reasonList: [], // 可选的申请原因数组
showModal: false, // 是否显示申请原因弹窗
currentValue: '', // 当前选择的售后原因
});
let formData = reactive({
way: '',
applyReason: '',
applyDescription: '',
applyPicUrls: [],
});
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.redirect('/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.getOrderDetail(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>
+379
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>
+209
View File
@@ -0,0 +1,209 @@
<!-- 售后列表 -->
<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">
<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/helper/utils';
const state = reactive({
currentTab: 0,
showApply: false,
pagination: {
list: [],
total: 0,
pageNo: 1,
pageSize: 10,
},
loadStatus: '',
});
const tabMaps = [
{
name: '全部',
value: [],
},
{
name: '申请中',
value: [10],
},
{
name: '处理中',
value: [20, 30, 40],
},
{
name: '已完成',
value: [50],
},
{
name: '已拒绝',
value: [61, 62, 63],
},
];
// 切换选项卡
function onTabsChange(e) {
resetPagination(state.pagination);
state.currentTab = e.index;
getOrderList();
}
// 获取售后列表
async function getOrderList() {
state.loadStatus = 'loading';
let { data, code } = await AfterSaleApi.getAfterSalePage({
pageNo: state.pagination.pageNo,
pageSize: state.pagination.pageSize,
statuses: tabMaps[state.currentTab].value.join(','),
});
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>
+83
View File
@@ -0,0 +1,83 @@
<!-- 售后日志的每一项展示 -->
<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;
}
:deep() {
image {
display: block;
}
}
</style>
+38
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>
+195
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>
+529
View File
@@ -0,0 +1,529 @@
<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
v-if="state.orderPayload.pointActivityId"
class="order-item ss-flex ss-col-center ss-row-between"
>
<view class="item-title">兑换积分</view>
<view class="ss-flex ss-col-center">
<image
:src="sheep.$url.static('/static/img/shop/goods/score1.svg')"
class="score-img"
/>
<text class="item-value ss-m-r-24">
{{ state.orderInfo.usePoint }}
</text>
</view>
</view>
<view
class="order-item ss-flex ss-col-center ss-row-between"
v-if="state.orderInfo.type === 0 || state.orderPayload.pointActivityId"
>
<view class="item-title">积分抵扣</view>
<view class="ss-flex ss-col-center">
{{ state.pointStatus || state.orderPayload.pointActivityId ? '剩余积分' : '当前积分' }}
<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.orderPayload.pointActivityId
? state.orderInfo.totalPoint - state.orderInfo.usePoint
: state.orderInfo.totalPoint || 0
}}
</text>
<checkbox-group @change="changeIntegral" v-if="!state.orderPayload.pointActivityId">
<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.filter((coupon) => coupon.match).length > 0
? 'text-red'
: 'text-disabled'
"
v-else
>
{{
state.couponInfo.filter((coupon) => coupon.match).length > 0
? state.couponInfo.filter((coupon) => coupon.match).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">
<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"
/>
<!-- 满额折扣弹框 -->
<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 TradeConfigApi from '@/sheep/api/trade/config';
import { fen2yuan } from '@/sheep/hooks/useGoods';
import { DeliveryTypeEnum } from '@/sheep/helper/const';
const state = reactive({
orderPayload: {},
orderInfo: {
items: [], // 商品项列表
price: {}, // 价格信息
},
showCoupon: false, // 是否展示优惠劵
couponInfo: [], // 优惠劵列表
showDiscount: false, // 是否展示营销活动
// ========== 积分 ==========
pointStatus: false, //是否使用积分
});
const addressState = ref({
addressInfo: {}, // 选择的收货地址
deliveryType: undefined, // 收货方式:1-快递配送,2-门店自提
isPickUp: true, // 门店自提是否开启
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,
pointActivityId: state.orderPayload.pointActivityId,
});
if (code !== 0) {
return;
}
// 更新购物车列表,如果来自购物车
if (state.orderPayload.items[0].cartId > 0) {
sheep.$store('cart').getList();
}
// 跳转到支付页面
if (data.payOrderId && data.payOrderId > 0) {
sheep.$router.redirect('/pages/pay/index', {
id: data.payOrderId,
});
} else {
sheep.$router.redirect('/pages/order/detail', {
id: data.id,
});
}
}
// 检查库存 & 计算订单价格
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,
pointActivityId: state.orderPayload.pointActivityId,
});
if (code !== 0) {
return code;
}
state.orderInfo = data;
state.couponInfo = data.coupons || [];
// 设置收货地址
if (state.orderInfo.address) {
addressState.value.addressInfo = state.orderInfo.address;
}
return code;
}
onLoad(async (options) => {
// 解析参数
if (!options.data) {
sheep.$helper.toast('参数不正确,请检查!');
return;
}
state.orderPayload = JSON.parse(options.data);
// 获取交易配置
const { data, code } = await TradeConfigApi.getTradeConfig();
if (code === 0) {
addressState.value.isPickUp = data.deliveryPickUpEnabled;
}
// 价格计算
// 情况一:先自动选择“快递物流”
addressState.value.deliveryType = DeliveryTypeEnum.EXPRESS.type;
let orderCode = await getOrderInfo();
if (orderCode === 0) {
return;
}
// 情况二:失败,再自动选择“门店自提”
if (addressState.value.isPickUp) {
addressState.value.deliveryType = DeliveryTypeEnum.PICK_UP.type;
let orderCode = await getOrderInfo();
if (orderCode === 0) {
return;
}
}
// 情况三:都失败,则不选择
addressState.value.deliveryType = undefined;
await getOrderInfo();
});
// 使用 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>
+671
View File
@@ -0,0 +1,671 @@
<!-- 订单详情 -->
<template>
<s-layout title="订单详情" class="index-wrap" navbar="inner">
<view
class="state-box ss-flex-col ss-col-center ss-row-right"
:style="[
{
marginTop: '-' + Number(statusBarHeight + 88) + 'rpx',
paddingTop: Number(statusBarHeight + 88) + 'rpx',
},
]"
>
<view class="ss-flex ss-m-t-32 ss-m-b-20">
<!-- 待支付 -->
<image
v-if="state.orderInfo.status === 0"
class="state-img"
:src="sheep.$url.static('/static/img/shop/order/no_pay.png')"
/>
<!-- 待发货 -->
<image
v-if="state.orderInfo.status === 10"
class="state-img"
:src="sheep.$url.static('/static/img/shop/order/order_loading.png')"
/>
<!-- 已完成 -->
<image
v-else-if="state.orderInfo.status === 30"
class="state-img"
:src="sheep.$url.static('/static/img/shop/order/order_success.png')"
/>
<!-- 已关闭 -->
<image
v-else-if="state.orderInfo.status === 40"
class="state-img"
:src="sheep.$url.static('/static/img/shop/order/order_close.png')"
/>
<!-- 已发货 -->
<image
v-else-if="state.orderInfo.status === 20"
class="state-img"
:src="sheep.$url.static('/static/img/shop/order/order_express.png')"
/>
<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, onShow } 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: 芋艿:【微信物流】待接入微信 https://gitee.com/sheepjs/shopro-uniapp/commit/a6bbba49b84dd418b84c5fefc8b7463df8f4901f
// 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;
}
uni.showModal({
title: '提示',
content: '确认收货吗?',
success: async function (res) {
if (!res.confirm) {
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.getOrderDetail(id, {
merchant_trade_no: state.merchantTradeNo,
});
} else {
res = await OrderApi.getOrderDetail(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();
}
}
onShow(async () => {
//onShow中获取订单列表,保证跳转后页面为最新状态
await getOrderDetail(state.orderInfo.id);
});
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;
}
state.orderInfo.id = 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>
+233
View File
@@ -0,0 +1,233 @@
<!-- 物流追踪 -->
<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-desc ss-m-b-16">
<highlight-number :content="item.content" @phone-click="handlePhoneClick" />
<!-- <rich-text :nodes="item.content"></rich-text>-->
</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';
import HighlightNumber from '@/pages/components/HighlightNumberText.vue';
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;
}
async function getOrderDetail(id) {
const { data } = await OrderApi.getOrderDetail(id);
state.info = data;
}
onLoad((options) => {
getExpressDetail(options.id);
getOrderDetail(options.id);
});
function handlePhoneClick(data) {
handleClick(data);
}
function handleClick(data) {
const phoneNumber = data.phoneNumber;
if (!phoneNumber) return;
// 获取当前平台
const platform = uni.getSystemInfoSync().platform.toLowerCase();
if (platform === 'devtools') {
uni.showToast({ title: '真机才可拨打电话', icon: 'none' });
handleCopy(phoneNumber);
return;
}
if (platform === 'wechat') {
uni.showToast({ title: '请手动拨打', icon: 'none' });
handleCopy(phoneNumber);
return;
}
uni.makePhoneCall({
phoneNumber: phoneNumber,
success: () => {
console.log('拨打电话成功');
},
fail: (err) => {
console.error('拨打电话失败', err);
uni.showToast({ title: '拨号失败,请手动拨打', icon: 'none' });
handleCopy(phoneNumber);
},
});
}
function handleCopy(text) {
uni.setClipboardData({
data: text,
success: () => {
uni.showToast({ title: '已复制到剪贴板', icon: 'success' });
},
fail: () => {
uni.showToast({ title: '复制失败', icon: 'none' });
},
});
}
</script>
<style lang="scss" scoped>
.highlight {
color: red; /* 高亮颜色 */
font-weight: bold;
}
.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: #000000;
}
}
}
</style>
+504
View File
@@ -0,0 +1,504 @@
<!-- 订单列表 -->
<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/helper/utils';
// 数据
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,
});
}
// 确认收货
async function onConfirm(order, ignore = false) {
// 需开启确认收货组件
// todo: 芋艿:【微信物流】需要后续接入微信收货组件 https://gitee.com/sheepjs/shopro-uniapp/commit/a6bbba49b84dd418b84c5fefc8b7463df8f4901f
// 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;
}
uni.showModal({
title: '提示',
content: '确认收货吗?',
success: async function (res) {
if (!res.confirm) {
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>
+260
View File
@@ -0,0 +1,260 @@
<template>
<view class="order-details">
<!-- 自提商品核销 -->
<view v-if="orderInfo.deliveryType === 2 && orderInfo.payStatus" class="writeOff borRadius14">
<view class="title">核销信息</view>
<view class="grayBg flex-center">
<view class="pictrue">
<image
v-if="!!painterImageUrl"
:src="painterImageUrl"
:style="{ width: `${state.qrcodeSize}px`, height: `${state.qrcodeSize}px` }"
:show-menu-by-longpress="true"
/>
</view>
</view>
<view class="gear">
<image :src="sheep.$url.static('/static/img/shop/writeOff.png')"></image>
</view>
<view class="num">{{ orderInfo.pickUpVerifyCode }}</view>
<view class="rules">
<view class="item">
<view class="rulesTitle flex flex-wrap align-center"> 核销时间 </view>
<view class="info">
每日
<text class="time">{{ systemStore.openingTime }} - {{ systemStore.closingTime }}</text>
</view>
</view>
<view class="item">
<view class="rulesTitle flex flex-wrap align-center">
<text class="iconfont icon-shuoming1"></text>
使用说明
</view>
<view class="info">可将二维码出示给店员扫描或提供数字核销码</view>
</view>
</view>
</view>
<view
v-if="orderInfo.deliveryType === 2"
class="map flex flex-wrap align-center ss-row-between borRadius14"
>
<view>自提地址信息</view>
<view class="place cart-color flex flex-wrap flex-center" @tap="showMaoLocation">
查看位置
</view>
</view>
<!-- 海报画板默认隐藏只用来生成海报生成方式为主动调用 -->
<l-painter
v-if="showPainter"
isCanvasToTempFilePath
pathType="url"
@success="setPainterImageUrl"
hidden
ref="painterRef"
/>
</view>
</template>
<script setup>
import sheep from '@/sheep';
import { reactive, ref } from 'vue';
const props = defineProps({
orderInfo: {
type: Object,
default() {},
},
systemStore: {
type: Object,
default() {},
},
});
const state = reactive({
qrcodeSize: 145,
});
/**
* 打开地图
*/
const showMaoLocation = () => {
if (!props.systemStore.latitude || !props.systemStore.longitude) {
sheep.$helper.toast('缺少经纬度信息无法查看地图!');
return;
}
uni.openLocation({
latitude: props.systemStore.latitude,
longitude: props.systemStore.longitude,
scale: 8,
name: props.systemStore.name,
address: props.systemStore.areaName + props.systemStore.detailAddress,
});
};
/**
* 拨打电话
*/
const makePhone = () => {
uni.makePhoneCall({
phoneNumber: props.systemStore.phone,
});
};
const painterRef = ref(); // 海报画板
const painterImageUrl = ref(); // 海报 url
const showPainter = ref(true);
// 渲染海报
const renderPoster = async (poster) => {
await painterRef.value.render(poster);
};
// 获得生成的图片
const setPainterImageUrl = (path) => {
painterImageUrl.value = path;
showPainter.value = false;
};
/**
* 生成核销二维码
*/
const markCode = (text) => {
renderPoster({
css: {
width: `${state.qrcodeSize}px`,
height: `${state.qrcodeSize}px`,
},
views: [
{
type: 'qrcode',
text: text,
css: {
width: `${state.qrcodeSize}px`,
height: `${state.qrcodeSize}px`,
},
},
],
});
};
defineExpose({
markCode,
});
</script>
<style scoped lang="scss">
.borRadius14 {
border-radius: 14rpx !important;
}
.cart-color {
color: #e93323 !important;
border: 1px solid #e93323 !important;
}
.order-details {
border-radius: 10rpx;
margin: 0 20rpx 20rpx 20rpx;
}
.order-details .writeOff {
background-color: #fff;
margin-top: 15rpx;
padding-bottom: 50rpx;
}
.order-details .writeOff .title {
font-size: 30rpx;
color: #282828;
height: 87rpx;
border-bottom: 1px solid #f0f0f0;
padding: 0 24rpx;
line-height: 87rpx;
}
.order-details .writeOff .grayBg {
background-color: #f2f5f7;
width: 590rpx;
height: 384rpx;
border-radius: 20rpx 20rpx 0 0;
margin: 50rpx auto 0 auto;
padding-top: 55rpx;
}
.order-details .writeOff .grayBg .pictrue {
width: 290rpx;
height: 290rpx;
}
.order-details .writeOff .grayBg .pictrue image {
width: 100%;
height: 100%;
}
.order-details .writeOff .gear {
width: 590rpx;
height: 30rpx;
margin: 0 auto;
}
.order-details .writeOff .gear image {
width: 100%;
height: 100%;
}
.order-details .writeOff .num {
background-color: #f0c34c;
width: 590rpx;
height: 84rpx;
color: #282828;
font-size: 48rpx;
margin: 0 auto;
border-radius: 0 0 20rpx 20rpx;
text-align: center;
padding-top: 4rpx;
}
.order-details .writeOff .rules {
margin: 46rpx 30rpx 0 30rpx;
border-top: 1px solid #f0f0f0;
padding-top: 10rpx;
}
.order-details .writeOff .rules .item {
margin-top: 20rpx;
}
.order-details .writeOff .rules .item .rulesTitle {
font-size: 28rpx;
color: #282828;
}
.order-details .writeOff .rules .item .rulesTitle .iconfont {
font-size: 30rpx;
color: #333;
margin-right: 8rpx;
margin-top: 5rpx;
}
.order-details .writeOff .rules .item .info {
font-size: 28rpx;
color: #999;
margin-top: 7rpx;
}
.order-details .writeOff .rules .item .info .time {
margin-left: 20rpx;
}
.order-details .map {
height: 86rpx;
font-size: 30rpx;
color: #282828;
line-height: 86rpx;
border-bottom: 1px solid #f0f0f0;
margin-top: 15rpx;
background-color: #fff;
padding: 0 24rpx;
}
.order-details .map .place {
font-size: 26rpx;
width: 176rpx;
height: 50rpx;
border-radius: 25rpx;
line-height: 50rpx;
text-align: center;
}
</style>
+305
View File
@@ -0,0 +1,305 @@
<!-- 收银台 -->
<template>
<s-layout title="收银台">
<view class="bg-white ss-modal-box ss-flex-col">
<!-- 订单信息 -->
<view class="modal-header ss-flex-col ss-col-center ss-row-center">
<view class="money-box ss-m-b-20">
<text class="money-text">{{ fen2yuan(state.orderInfo.price) }}</text>
</view>
<view class="time-text">
<text>{{ payDescText }}</text>
</view>
</view>
<!-- 支付方式 -->
<view class="modal-content ss-flex-1">
<view class="pay-title ss-p-l-30 ss-m-y-30">选择支付方式</view>
<radio-group @change="onTapPay">
<label class="pay-type-item" v-for="item in state.payMethods" :key="item.title">
<view
class="pay-item ss-flex ss-col-center ss-row-between ss-p-x-30 border-bottom"
:class="{ 'disabled-pay-item': item.disabled }"
>
<view class="ss-flex ss-col-center">
<image
class="pay-icon"
v-if="item.disabled"
:src="sheep.$url.static('/static/img/shop/pay/cod_disabled.png')"
mode="aspectFit"
/>
<image
class="pay-icon"
v-else
:src="sheep.$url.static(item.icon)"
mode="aspectFit"
/>
<text class="pay-title">{{ item.title }}</text>
</view>
<view class="check-box ss-flex ss-col-center ss-p-l-10">
<view class="userInfo-money ss-m-r-10" v-if="item.value === 'wallet'">
余额: {{ fen2yuan(userWallet.balance) }}
</view>
<radio
:value="item.value"
color="var(--ui-BG-Main)"
style="transform: scale(0.8)"
:disabled="item.disabled"
:checked="state.payment === item.value"
/>
</view>
</view>
</label>
</radio-group>
</view>
<!-- 工具 -->
<view class="modal-footer ss-flex ss-row-center ss-col-center ss-m-t-80 ss-m-b-40">
<button v-if="state.payStatus === 0" class="ss-reset-button past-due-btn">
检测支付环境中
</button>
<button v-else-if="state.payStatus === -1" class="ss-reset-button past-due-btn" disabled>
支付已过期
</button>
<button
v-else
class="ss-reset-button save-btn"
@tap="onPay"
:disabled="state.payStatus !== 1"
:class="{ 'disabled-btn': state.payStatus !== 1 }"
>
立即支付
</button>
</view>
</view>
</s-layout>
</template>
<script setup>
import { computed, reactive } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import sheep from '@/sheep';
import { fen2yuan, useDurationTime } from '@/sheep/hooks/useGoods';
import PayOrderApi from '@/sheep/api/pay/order';
import PayChannelApi from '@/sheep/api/pay/channel';
import { getPayMethods, goPayResult } from '@/sheep/platform/pay';
const userWallet = computed(() => sheep.$store('user').userWallet);
// 检测支付环境
const state = reactive({
orderType: 'goods', // 订单类型; goods - 商品订单, recharge - 充值订单
orderInfo: {}, // 支付单信息
payStatus: 0, // 0=检测支付环境, -2=未查询到支付单信息, -1=支付已过期, 1=待支付,2=订单已支付
payMethods: [], // 可选的支付方式
payment: '', // 选中的支付方式
});
const onPay = () => {
if (state.payment === '') {
sheep.$helper.toast('请选择支付方式');
return;
}
if (state.payment === 'wallet') {
uni.showModal({
title: '提示',
content: '确定要支付吗?',
success: function (res) {
if (res.confirm) {
sheep.$platform.pay(state.payment, state.orderType, state.orderInfo.id);
}
},
});
} else {
sheep.$platform.pay(state.payment, state.orderType, state.orderInfo.id);
}
};
// 支付文案提示
const payDescText = computed(() => {
if (state.payStatus === 2) {
return '该订单已支付';
}
if (state.payStatus === 1) {
const time = useDurationTime(state.orderInfo.expireTime);
if (time.ms <= 0) {
state.payStatus = -1;
return '';
}
return `剩余支付时间 ${time.h}:${time.m}:${time.s} `;
}
if (state.payStatus === -2) {
return '未查询到支付单信息';
}
return '';
});
// 状态转换:payOrder.status => payStatus
function checkPayStatus() {
if (state.orderInfo.status === 10 || state.orderInfo.status === 20) {
// 支付成功
state.payStatus = 2;
// 跳转回支付成功页
uni.showModal({
title: '提示',
content: '订单已支付',
showCancel: false,
success: function () {
goPayResult(state.orderInfo.id, state.orderType);
},
});
return;
}
if (state.orderInfo.status === 30) {
// 支付关闭
state.payStatus = -1;
return;
}
state.payStatus = 1; // 待支付
}
// 切换支付方式
function onTapPay(e) {
state.payment = e.detail.value;
}
// 设置支付订单信息
async function setOrder(id) {
// 获得支付订单信息
const { data, code } = await PayOrderApi.getOrder(id, true);
if (code !== 0 || !data) {
state.payStatus = -2;
return;
}
state.orderInfo = data;
// 设置支付状态
checkPayStatus();
// 获得支付方式
await setPayMethods();
}
// 获得支付方式
async function setPayMethods() {
const { data, code } = await PayChannelApi.getEnableChannelCodeList(state.orderInfo.appId);
if (code !== 0) {
return;
}
state.payMethods = getPayMethods(data);
state.payMethods.find((item) => {
if (item.value && !item.disabled) {
state.payment = item.value;
return true;
}
});
}
onLoad((options) => {
if (
sheep.$platform.name === 'WechatOfficialAccount' &&
sheep.$platform.os === 'ios' &&
!sheep.$platform.landingPage.includes('pages/pay/index')
) {
location.reload();
return;
}
// 获得支付订单信息
let id = options.id;
if (options.orderType) {
state.orderType = options.orderType;
}
setOrder(id);
// 刷新钱包的缓存
sheep.$store('user').getWallet();
});
</script>
<style lang="scss" scoped>
.pay-icon {
width: 36rpx;
height: 36rpx;
margin-right: 26rpx;
}
.ss-modal-box {
// max-height: 1000rpx;
.modal-header {
position: relative;
padding: 60rpx 20rpx 40rpx;
.money-text {
color: $red;
font-size: 46rpx;
font-weight: bold;
font-family: OPPOSANS;
&::before {
content: '¥';
font-size: 30rpx;
}
}
.time-text {
font-size: 26rpx;
color: $gray-b;
}
.close-icon {
position: absolute;
top: 10rpx;
right: 20rpx;
font-size: 46rpx;
opacity: 0.2;
}
}
.modal-content {
overflow-y: auto;
.pay-title {
font-size: 26rpx;
font-weight: 500;
color: #333333;
}
.pay-tip {
font-size: 26rpx;
color: #bbbbbb;
}
.pay-item {
height: 86rpx;
}
.disabled-pay-item {
.pay-title {
color: #999999;
}
}
.userInfo-money {
font-size: 26rpx;
color: #bbbbbb;
line-height: normal;
}
}
.save-btn {
width: 710rpx;
height: 80rpx;
border-radius: 40rpx;
background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
color: $white;
}
.disabled-btn {
background: #e5e5e5;
color: #999999;
}
.past-due-btn {
width: 710rpx;
height: 80rpx;
border-radius: 40rpx;
background-color: #999;
color: #fff;
}
}
</style>
+167
View File
@@ -0,0 +1,167 @@
<!-- 充值记录 -->
<template>
<s-layout class="widthdraw-log-wrap" title="充值记录">
<!-- 记录卡片 -->
<view class="wallet-log-box ss-p-b-30">
<view class="log-list" v-for="item in state.pagination.list" :key="item">
<view class="head ss-flex ss-col-center ss-row-between">
<view class="title">充值金额</view>
<view class="num" :class="item.refundStatus === 10 ? 'danger-color' : 'success-color'">
{{ fen2yuan(item.payPrice) }}
<text v-if="item.bonusPrice > 0">赠送 {{ fen2yuan(item.bonusPrice) }} </text>
</view>
</view>
<view class="status-box item ss-flex ss-col-center ss-row-between">
<view class="item-title">支付状态</view>
<view
class="status-text"
:class="item.refundStatus === 10 ? 'danger-color' : 'success-color'"
>
{{ item.refundStatus === 10 ? '已退款' : '已支付' }}
</view>
</view>
<view class="time-box item ss-flex ss-col-center ss-row-between">
<text class="item-title">充值渠道</text>
<view class="time ss-ellipsis-1">{{ item.payChannelName }}</view>
</view>
<view class="time-box item ss-flex ss-col-center ss-row-between">
<text class="item-title">充值单号</text>
<view class="time"> {{ item.payOrderChannelOrderNo }} </view>
</view>
<view class="time-box item ss-flex ss-col-center ss-row-between">
<text class="item-title">充值时间</text>
<view class="time">
{{ sheep.$helper.timeFormat(item.payTime, 'yyyy-mm-dd hh:MM:ss') }}</view
>
</view>
</view>
</view>
<s-empty
v-if="state.pagination.total === 0"
icon="/static/comment-empty.png"
text="暂无充值记录"
/>
<uni-load-more
v-if="state.pagination.total > 0"
:status="state.loadStatus"
:content-text="{
contentdown: '上拉加载更多',
}"
@tap="loadMore"
/>
</s-layout>
</template>
<script setup>
import { reactive } from 'vue';
import { onLoad, onReachBottom } from '@dcloudio/uni-app';
import _ from 'lodash-es';
import PayWalletApi from '@/sheep/api/pay/wallet';
import sheep from '@/sheep';
import { fen2yuan } from '../../sheep/hooks/useGoods';
const state = reactive({
pagination: {
list: [],
total: 0,
pageNo: 1,
pageSize: 5,
},
loadStatus: '',
});
async function getLogList(page = 1, list_rows = 5) {
const { code, data } = await PayWalletApi.getWalletRechargePage({
pageNo: page,
pageSize: list_rows,
});
if (code !== 0) {
return;
}
state.pagination.list = _.concat(state.pagination.list, data.list);
state.pagination.total = data.total;
state.loadStatus = state.pagination.list.length < state.pagination.total ? 'more' : 'noMore';
}
// 加载更多
function loadMore() {
if (state.loadStatus === 'noMore') {
return;
}
state.pagination.pageNo++;
getLogList();
}
onLoad(() => {
getLogList();
});
onReachBottom(() => {
loadMore();
});
</script>
<style lang="scss" scoped>
// 记录卡片
.log-list {
min-height: 213rpx;
background: $white;
margin-bottom: 10rpx;
padding-bottom: 10rpx;
.head {
padding: 0 35rpx;
height: 80rpx;
border-bottom: 1rpx solid $gray-e;
margin-bottom: 20rpx;
.title {
font-size: 28rpx;
font-weight: 500;
color: $dark-3;
}
.num {
font-size: 28rpx;
font-weight: 500;
}
}
.item {
padding: 0 30rpx 10rpx;
.item-icon {
color: $gray-d;
font-size: 36rpx;
margin-right: 8rpx;
}
.item-title {
width: 180rpx;
font-size: 24rpx;
font-weight: 400;
color: #666666;
}
.status-text {
font-size: 24rpx;
font-weight: 500;
}
.time {
font-size: 24rpx;
font-weight: 400;
color: #c0c0c0;
}
}
}
.warning-color {
color: #faad14;
}
.danger-color {
color: #ff4d4f;
}
.success-color {
color: #67c23a;
}
</style>
+277
View File
@@ -0,0 +1,277 @@
<!-- 充值界面 -->
<template>
<s-layout title="充值" class="withdraw-wrap" navbar="inner">
<view
class="wallet-num-box ss-flex ss-col-center ss-row-between"
:style="[
{
marginTop: '-' + Number(statusBarHeight + 88) + 'rpx',
paddingTop: Number(statusBarHeight + 108) + 'rpx',
},
]"
>
<view class="">
<view class="num-title">当前余额</view>
<view class="wallet-num">{{ fen2yuan(userWallet.balance) }}</view>
</view>
<button class="ss-reset-button log-btn" @tap="sheep.$router.go('/pages/pay/recharge-log')">
充值记录
</button>
</view>
<view class="recharge-box">
<view class="recharge-card-box">
<view class="input-label ss-m-b-50">充值金额</view>
<view class="input-box ss-flex border-bottom ss-p-b-20">
<view class="unit"></view>
<uni-easyinput
v-model="state.recharge_money"
type="digit"
placeholder="请输入充值金额"
:inputBorder="false"
/>
</view>
<view class="face-value-box ss-flex ss-flex-wrap ss-m-y-40">
<button
class="ss-reset-button face-value-btn"
v-for="item in state.packageList"
:key="item.money"
:class="[{ 'btn-active': state.recharge_money === fen2yuan(item.payPrice) }]"
@tap="onCard(item.payPrice)"
>
<text class="face-value-title">{{ fen2yuan(item.payPrice) }}</text>
<view v-if="item.bonusPrice" class="face-value-tag">
{{ fen2yuan(item.bonusPrice) }}
</view>
</button>
</view>
<button
class="ss-reset-button save-btn ui-BG-Main-Gradient ss-m-t-60 ui-Shadow-Main"
@tap="onConfirm"
>
确认充值
</button>
</view>
</view>
</s-layout>
</template>
<script setup>
import { computed, reactive } from 'vue';
import sheep from '@/sheep';
import { onLoad } from '@dcloudio/uni-app';
import { fen2yuan } from '@/sheep/hooks/useGoods';
import PayWalletApi from '@/sheep/api/pay/wallet';
import { WxaSubscribeTemplate } from '@/sheep/helper/const';
const userWallet = computed(() => sheep.$store('user').userWallet);
const statusBarHeight = sheep.$platform.device.statusBarHeight * 2;
const headerBg = sheep.$url.css('/static/img/shop/user/withdraw_bg.png');
const state = reactive({
recharge_money: '', // 输入的充值金额
packageList: [],
});
// 点击卡片,选择充值金额
function onCard(e) {
state.recharge_money = fen2yuan(e);
}
// 获得钱包充值套餐列表
async function getRechargeTabs() {
const { code, data } = await PayWalletApi.getWalletRechargePackageList();
if (code !== 0) {
return;
}
state.packageList = data;
}
// 发起支付
async function onConfirm() {
const { code, data } = await PayWalletApi.createWalletRecharge({
packageId: state.packageList.find((item) => fen2yuan(item.payPrice) === state.recharge_money)
?.id,
payPrice: state.recharge_money * 100,
});
if (code !== 0) {
return;
}
// #ifdef MP
sheep.$platform
.useProvider('wechat')
.subscribeMessage(WxaSubscribeTemplate.PAY_WALLET_RECHARGER_SUCCESS);
// #endif
sheep.$router.go('/pages/pay/index', {
id: data.payOrderId,
orderType: 'recharge',
});
}
onLoad(() => {
getRechargeTabs();
});
</script>
<style lang="scss" scoped>
:deep() {
.uni-input-input {
font-family: OPPOSANS !important;
}
}
.wallet-num-box {
padding: 0 40rpx 80rpx;
background: var(--ui-BG-Main) v-bind(headerBg) center/750rpx 100% no-repeat;
border-radius: 0 0 5% 5%;
.num-title {
font-size: 26rpx;
font-weight: 500;
color: $white;
margin-bottom: 20rpx;
}
.wallet-num {
font-size: 60rpx;
font-weight: 500;
color: $white;
font-family: OPPOSANS;
}
.log-btn {
width: 170rpx;
height: 60rpx;
line-height: 60rpx;
border: 1rpx solid $white;
border-radius: 30rpx;
padding: 0;
font-size: 26rpx;
font-weight: 500;
color: $white;
}
}
.recharge-box {
position: relative;
padding: 0 30rpx;
margin-top: -60rpx;
}
.save-btn {
width: 620rpx;
height: 86rpx;
border-radius: 44rpx;
font-size: 30rpx;
}
.recharge-card-box {
width: 690rpx;
background: var(--ui-BG);
border-radius: 20rpx;
padding: 30rpx;
box-sizing: border-box;
.input-label {
font-size: 30rpx;
font-weight: 500;
color: #333;
}
.unit {
display: flex;
align-items: center;
font-size: 48rpx;
font-weight: 500;
}
.uni-easyinput__placeholder-class {
font-size: 30rpx;
height: 60rpx;
display: flex;
align-items: center;
}
:deep(.uni-easyinput__content-input) {
font-size: 48rpx;
}
.face-value-btn {
width: 200rpx;
height: 144rpx;
border: 1px solid var(--ui-BG-Main);
border-radius: 10rpx;
position: relative;
z-index: 1;
margin-bottom: 15rpx;
margin-right: 15rpx;
&:nth-of-type(3n) {
margin-right: 0;
}
.face-value-title {
font-size: 36rpx;
font-weight: 500;
color: var(--ui-BG-Main);
font-family: OPPOSANS;
&::after {
content: '元';
font-size: 24rpx;
margin-left: 6rpx;
}
}
.face-value-tag {
position: absolute;
z-index: 2;
height: 40rpx;
line-height: 40rpx;
background: var(--ui-BG-Main);
opacity: 0.8;
border-radius: 10rpx 0 20rpx 0;
top: 0;
left: -2rpx;
padding: 0 16rpx;
font-size: 22rpx;
color: $white;
font-family: OPPOSANS;
}
&::before {
position: absolute;
content: ' ';
width: 100%;
height: 100%;
background: var(--ui-BG-Main);
opacity: 0.1;
z-index: 0;
left: 0;
top: 0;
}
}
.btn-active {
z-index: 1;
&::before {
content: '';
background: var(--ui-BG-Main);
opacity: 1;
}
.face-value-title {
color: $white;
position: relative;
z-index: 1;
font-family: OPPOSANS;
}
.face-value-tag {
background: $white;
color: var(--ui-BG-Main);
font-family: OPPOSANS;
}
}
}
</style>
+339
View File
@@ -0,0 +1,339 @@
<!-- 支付结果页面 -->
<template>
<s-layout :bgStyle="{ color: '#FFF' }" title="支付结果">
<view class="pay-result-box ss-flex-col ss-row-center ss-col-center">
<!-- 信息展示 -->
<view class="pay-waiting ss-m-b-30" v-if="payResult === 'waiting'" />
<image
class="pay-img ss-m-b-30"
v-if="payResult === 'success'"
:src="sheep.$url.static('/static/img/shop/order/order_pay_success.gif')"
/>
<image
class="pay-img ss-m-b-30"
v-if="['failed', 'closed'].includes(payResult)"
:src="sheep.$url.static('/static/img/shop/order/order_paty_fail.gif')"
/>
<view class="tip-text ss-m-b-30" v-if="payResult === 'success'">支付成功</view>
<view class="tip-text ss-m-b-30" v-if="payResult === 'failed'">支付失败</view>
<view class="tip-text ss-m-b-30" v-if="payResult === 'closed'">该订单已关闭</view>
<view class="tip-text ss-m-b-30" v-if="payResult === 'waiting'">检测支付结果...</view>
<view class="pay-total-num ss-flex" v-if="payResult === 'success'">
<view>{{ fen2yuan(state.orderInfo.price) }}</view>
</view>
<!-- 操作区 -->
<view class="btn-box ss-flex ss-row-center ss-m-t-50">
<button class="back-btn ss-reset-button" @tap="sheep.$router.go('/pages/index/index')">
返回首页
</button>
<button
class="check-btn ss-reset-button"
v-if="payResult === 'failed'"
@tap="
sheep.$router.redirect('/pages/pay/index', { id: state.id, orderType: state.orderType })
"
>
重新支付
</button>
<button class="check-btn ss-reset-button" v-if="payResult === 'success'" @tap="onOrder">
查看订单
</button>
<button
class="check-btn ss-reset-button"
v-if="payResult === 'success' && state.tradeOrder.type === 3"
@tap="sheep.$router.redirect('/pages/activity/groupon/order')"
>
我的拼团
</button>
</view>
<!-- #ifdef MP -->
<view
class="subscribe-box ss-flex ss-m-t-44"
v-if="showSubscribeBtn && state.orderType === 'goods'"
>
<image class="subscribe-img" :src="sheep.$url.static('/static/img/shop/order/cargo.png')" />
<view class="subscribe-title ss-m-r-48 ss-m-l-16">获取实时发货信息与订单状态</view>
<view class="subscribe-start" @tap="subscribeMessage">立即订阅</view>
</view>
<!-- #endif -->
</view>
</s-layout>
</template>
<script setup>
import { onHide, onLoad, onShow } from '@dcloudio/uni-app';
import { computed, reactive, ref } from 'vue';
import { isEmpty } from 'lodash-es';
import sheep from '@/sheep';
import PayOrderApi from '@/sheep/api/pay/order';
import { fen2yuan } from '@/sheep/hooks/useGoods';
import OrderApi from '@/sheep/api/trade/order';
import { WxaSubscribeTemplate } from '@/sheep/helper/const';
const state = reactive({
id: 0, // 支付单号
orderType: 'goods', // 订单类型
result: 'unpaid', // 支付状态
orderInfo: {}, // 支付订单信息
tradeOrder: {}, // 商品订单信息,只有在 orderType 为 goods 才会请求。目的:【我的拼团】按钮的展示
counter: 0, // 获取结果次数
});
// 支付结果 result => payResult
const payResult = computed(() => {
if (state.result === 'unpaid') {
return 'waiting';
}
if (state.result === 'paid') {
return 'success';
}
if (state.result === 'failed') {
return 'failed';
}
if (state.result === 'closed') {
return 'closed';
}
});
function showRepayModal() {
if (state.result !== 'failed') return;
uni.showModal({
title: '确认支付',
content: '未检测到您的支付结果,请确认您是否已经支付完成?',
cancelText: '取消',
confirmText: '我已支付',
success: function (res) {
if (res.confirm) {
state.counter = 0;
setTimeout(() => {
getOrderInfo(state.id);
}, 100);
}
},
});
}
// 获得订单信息
async function getOrderInfo(id) {
state.counter++;
// 1. 加载订单信息
const { data, code } = await PayOrderApi.getOrder(id);
if (code === 0) {
state.orderInfo = data;
if (!state.orderInfo || state.orderInfo.status === 30) {
// 支付关闭
state.result = 'closed';
return;
}
if (state.orderInfo.status !== 0) {
// 非待支付,可能是已支付,可能是已退款
state.result = 'paid';
// #ifdef MP
uni.showModal({
title: '支付结果',
showCancel: false, // 不要取消按钮
content: '支付成功',
success: () => {
// 订阅只能由用户主动触发,只能包一层 showModal 诱导用户点击
autoSubscribeMessage();
},
});
// #endif
// 特殊:获得商品订单信息
if (state.orderType === 'goods') {
const { data, code } = await OrderApi.getOrderDetail(
state.orderInfo.merchantOrderId,
true,
);
if (code === 0) {
state.tradeOrder = data;
}
}
return;
}
}
// 2.1 情况三一:未支付,且轮询次数小于五次,则继续轮询
if (state.counter < 5 && state.result === 'unpaid') {
setTimeout(() => {
getOrderInfo(id);
}, 2000);
}
// 2.2 情况二:超过五次检测才判断为支付失败
if (state.counter >= 5) {
state.result = 'failed';
showRepayModal();
}
}
function onOrder() {
if (state.orderType === 'recharge') {
sheep.$router.redirect('/pages/pay/recharge-log');
} else {
sheep.$router.redirect('/pages/order/list');
}
}
// #ifdef MP
const showSubscribeBtn = ref(false); // 默认隐藏
const SUBSCRIBE_BTN_STATUS_STORAGE_KEY = 'subscribe_btn_status';
function subscribeMessage() {
if (state.orderType !== 'goods') {
return;
}
const event = [WxaSubscribeTemplate.TRADE_ORDER_DELIVERY];
if (state.tradeOrder.type === 3) {
event.push(WxaSubscribeTemplate.PROMOTION_COMBINATION_SUCCESS);
}
sheep.$platform.useProvider('wechat').subscribeMessage(event, () => {
// 订阅后记录一下订阅状态
uni.removeStorageSync(SUBSCRIBE_BTN_STATUS_STORAGE_KEY);
uni.setStorageSync(SUBSCRIBE_BTN_STATUS_STORAGE_KEY, '已订阅');
// 隐藏订阅按钮
showSubscribeBtn.value = false;
});
}
async function autoSubscribeMessage() {
// 1. 校验是否手动订阅过
const subscribeBtnStatus = uni.getStorageSync(SUBSCRIBE_BTN_STATUS_STORAGE_KEY);
if (!subscribeBtnStatus) {
showSubscribeBtn.value = true;
return;
}
// 2. 订阅消息
subscribeMessage();
}
// #endif
onLoad(async (options) => {
// 支付订单号
if (options.id) {
state.id = options.id;
}
// 订单类型
if (options.orderType) {
state.orderType = options.orderType;
}
// 支付结果传值过来是失败,则直接显示失败界面
if (options.payState === 'fail') {
state.result = 'failed';
} else {
// 轮询五次检测订单支付结果
await getOrderInfo(state.id);
}
});
onShow(() => {
if (isEmpty(state.orderInfo)) {
return;
}
getOrderInfo(state.id);
});
onHide(() => {
state.result = 'unpaid';
state.counter = 0;
});
</script>
<style lang="scss" scoped>
@keyframes rotation {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.score-img {
width: 36rpx;
height: 36rpx;
margin: 0 4rpx;
}
.pay-result-box {
padding: 60rpx 0;
.pay-waiting {
margin-top: 20rpx;
width: 60rpx;
height: 60rpx;
border: 10rpx solid rgb(233, 231, 231);
border-bottom-color: rgb(204, 204, 204);
border-radius: 50%;
display: inline-block;
// -webkit-animation: rotation 1s linear infinite;
animation: rotation 1s linear infinite;
}
.pay-img {
width: 130rpx;
height: 130rpx;
}
.tip-text {
font-size: 30rpx;
font-weight: bold;
color: #333333;
}
.pay-total-num {
font-size: 36rpx;
font-weight: 500;
color: #333333;
font-family: OPPOSANS;
}
.btn-box {
width: 100%;
.back-btn {
width: 190rpx;
height: 70rpx;
font-size: 28rpx;
border: 2rpx solid #dfdfdf;
border-radius: 35rpx;
font-weight: 400;
color: #595959;
}
.check-btn {
width: 190rpx;
height: 70rpx;
font-size: 28rpx;
border: 2rpx solid #dfdfdf;
border-radius: 35rpx;
font-weight: 400;
color: #595959;
margin-left: 32rpx;
}
}
.subscribe-box {
.subscribe-img {
width: 44rpx;
height: 44rpx;
}
.subscribe-title {
font-weight: 500;
font-size: 32rpx;
line-height: 36rpx;
color: #434343;
}
.subscribe-start {
color: var(--ui-BG-Main);
font-weight: 700;
font-size: 32rpx;
line-height: 36rpx;
}
}
}
</style>
+60
View File
@@ -0,0 +1,60 @@
<!-- 错误界面 -->
<template>
<view class="error-page">
<s-empty
v-if="errCode === 'NetworkError'"
icon="/static/internet-empty.png"
text="网络连接失败"
showAction
actionText="重新连接"
@clickAction="onReconnect"
buttonColor="#ff3000"
/>
<s-empty
v-else-if="errCode === 'TemplateError'"
icon="/static/internet-empty.png"
text="未找到模板,请前往后台启用对应模板"
showAction
actionText="重新加载"
@clickAction="onReconnect"
buttonColor="#ff3000"
/>
<s-empty
v-else-if="errCode !== ''"
icon="/static/internet-empty.png"
:text="errMsg"
showAction
actionText="重新加载"
@clickAction="onReconnect"
buttonColor="#ff3000"
/>
</view>
</template>
<script setup>
import { onLoad } from '@dcloudio/uni-app';
import { ref } from 'vue';
import { ShoproInit } from '@/sheep';
const errCode = ref('');
const errMsg = ref('');
onLoad((options) => {
errCode.value = options.errCode;
errMsg.value = options.errMsg;
});
// 重新连接
async function onReconnect() {
uni.reLaunch({
url: '/pages/index/index',
});
await ShoproInit();
}
</script>
<style lang="scss" scoped>
.error-page {
width: 100%;
}
</style>
+118
View File
@@ -0,0 +1,118 @@
<!-- FAQ 常见问题 -->
<template>
<s-layout :bgStyle="{ color: '#FFF' }" class="set-wrap" title="常见问题">
<uni-collapse>
<uni-collapse-item v-for="(item, index) in state.list" :key="item">
<template v-slot:title>
<view class="ss-flex ss-col-center header">
<view class="ss-m-l-20 ss-m-r-20 icon">
<view class="rectangle">
<view class="num ss-flex ss-row-center ss-col-center">
{{ index + 1 < 10 ? '0' + (index + 1) : index + 1 }}
</view>
</view>
<view class="triangle"> </view>
</view>
<view class="title ss-m-t-36 ss-m-b-36">
{{ item.title }}
</view>
</view>
</template>
<view class="content ss-p-l-78 ss-p-r-40 ss-p-b-50 ss-p-t-20">
<text class="text">{{ item.content }}</text>
</view>
</uni-collapse-item>
</uni-collapse>
<s-empty
v-if="state.list.length === 0 && !state.loading"
text="暂无常见问题"
icon="/static/collect-empty.png"
/>
</s-layout>
</template>
<script setup>
import { onLoad } from '@dcloudio/uni-app';
import { reactive } from 'vue';
import sheep from '@/sheep';
const state = reactive({
list: [],
loading: true,
});
async function getFaqList() {
const { error, data } = await sheep.$api.data.faq();
if (error === 0) {
state.list = data;
state.loading = false;
}
}
onLoad(() => {
// TODO 芋艿:【文章】目前简单做,使用营销文章,作为 faq
if (true) {
sheep.$router.go('/pages/public/richtext', {
title: '常见问题',
})
return;
}
getFaqList();
});
</script>
<style lang="scss" scoped>
.header {
.title {
font-size: 28rpx;
font-weight: 500;
color: #333333;
line-height: 30rpx;
max-width: 688rpx;
}
.icon {
position: relative;
width: 40rpx;
height: 40rpx;
.rectangle {
position: absolute;
left: 0;
top: 0;
width: 40rpx;
height: 36rpx;
background: var(--ui-BG-Main);
border-radius: 4px;
.num {
width: 100%;
height: 100%;
font-size: 24rpx;
font-weight: 500;
color: var(--ui-BG);
line-height: 32rpx;
}
}
.triangle {
width: 0;
height: 0;
border-left: 4rpx solid transparent;
border-right: 4rpx solid transparent;
border-top: 8rpx solid var(--ui-BG-Main);
position: absolute;
left: 16rpx;
bottom: -4rpx;
}
}
}
.content {
border-bottom: 1rpx solid #dfdfdf;
.text {
font-size: 26rpx;
color: #666666;
}
}
</style>
+54
View File
@@ -0,0 +1,54 @@
<!-- 文章展示 -->
<template>
<s-layout :bgStyle="{ color: '#FFF' }" :title="state.title" class="set-wrap">
<view class="ss-p-30 richtext"><mp-html :content="state.content"></mp-html></view>
</s-layout>
</template>
<script setup>
import { onLoad } from '@dcloudio/uni-app';
import { reactive } from 'vue';
import ArticleApi from '@/sheep/api/promotion/article';
const state = reactive({
title: '',
content: '',
});
async function getRichTextContent(id, title) {
const { code, data } = await ArticleApi.getArticle(id, title);
if (code !== 0) {
return;
}
state.content = data.content;
// 标题不一致时,修改标题
if (state.title !== data.title) {
state.title = data.title;
uni.setNavigationBarTitle({
title: state.title,
});
}
}
onLoad((options) => {
if (options.title) {
state.title = options.title;
uni.setNavigationBarTitle({
title: state.title,
});
}
getRichTextContent(options.id, options.title);
});
</script>
<style lang="scss" scoped>
.set-title {
margin: 0 30rpx;
}
:deep() {
image {
display: block;
}
}
</style>
+236
View File
@@ -0,0 +1,236 @@
<template>
<s-layout :bgStyle="{ color: '#fff' }" class="set-wrap" title="系统设置">
<view class="header-box ss-flex-col ss-row-center ss-col-center">
<image
class="logo-img ss-m-b-46"
:src="sheep.$url.cdn(appInfo.logo)"
mode="aspectFit"
></image>
<view class="name ss-m-b-24">{{ appInfo.name }}</view>
</view>
<view class="container-list">
<uni-list :border="false">
<uni-list-item
title="当前版本"
:rightText="appInfo.version"
showArrow
clickable
:border="false"
class="list-border"
@tap="onCheckUpdate"
/>
<uni-list-item
title="本地缓存"
:rightText="storageSize"
showArrow
:border="false"
class="list-border"
/>
<uni-list-item
title="关于我们"
showArrow
clickable
:border="false"
class="list-border"
@tap="
sheep.$router.go('/pages/public/richtext', {
title: '关于我们'
})
"
/>
<!-- 为了过审 只有 iOS-App 有注销账号功能 -->
<uni-list-item
v-if="isLogin && sheep.$platform.os === 'ios' && sheep.$platform.name === 'App'"
title="注销账号"
rightText=""
showArrow
clickable
:border="false"
class="list-border"
@click="onLogoff"
/>
</uni-list>
</view>
<view class="set-footer ss-flex-col ss-row-center ss-col-center">
<view class="agreement-box ss-flex ss-col-center ss-m-b-40">
<view class="ss-flex ss-col-center ss-m-b-10">
<view
class="tcp-text"
@tap="
sheep.$router.go('/pages/public/richtext', {
title: '用户协议'
})
"
>
用户协议
</view>
<view class="agreement-text"></view>
<view
class="tcp-text"
@tap="
sheep.$router.go('/pages/public/richtext', {
title: '隐私协议'
})
"
>
隐私协议
</view>
</view>
</view>
<view class="copyright-text ss-m-b-10">{{ appInfo.copyright }}</view>
<view class="copyright-text">{{ appInfo.copytime }}</view>
</view>
<su-fixed bottom placeholder>
<view class="ss-p-x-20 ss-p-b-40">
<button
class="loginout-btn ss-reset-button ui-BG-Main ui-Shadow-Main"
@tap="onLogout"
v-if="isLogin"
>
退出登录
</button>
</view>
</su-fixed>
</s-layout>
</template>
<script setup>
import sheep from '@/sheep';
import { computed, reactive } from 'vue';
import AuthUtil from '@/sheep/api/member/auth';
const appInfo = computed(() => sheep.$store('app').info);
const isLogin = computed(() => sheep.$store('user').isLogin);
const storageSize = uni.getStorageInfoSync().currentSize + 'Kb';
const state = reactive({
showModal: false,
});
function onCheckUpdate() {
sheep.$platform.checkUpdate();
// 小程序初始化时已检查更新
// H5实时更新无需检查
// App 1.跳转应用市场更新 2.手动热更新 3.整包更新
}
// 注销账号
function onLogoff() {
uni.showModal({
title: '提示',
content: '确认注销账号?',
success: async function (res) {
if (!res.confirm) {
return;
}
const { code } = await AuthUtil.logout();
if (code !== 0) {
return;
}
sheep.$store('user').logout();
sheep.$router.go('/pages/index/user');
},
});
}
// 退出账号
function onLogout() {
uni.showModal({
title: '提示',
content: '确认退出账号?',
success: async function (res) {
if (!res.confirm) {
return;
}
const { code } = await AuthUtil.logout();
if (code !== 0) {
return;
}
sheep.$store('user').logout();
sheep.$router.go('/pages/index/user');
},
});
}
</script>
<style lang="scss" scoped>
.container-list {
width: 100%;
}
.set-title {
margin: 0 30rpx;
}
.header-box {
padding: 100rpx 0;
.logo-img {
width: 160rpx;
height: 160rpx;
border-radius: 50%;
}
.name {
font-size: 42rpx;
font-weight: 400;
color: $dark-3;
}
.version {
font-size: 32rpx;
font-weight: 500;
line-height: 32rpx;
color: $gray-b;
}
}
.set-footer {
margin: 100rpx 0 0 0;
.copyright-text {
font-size: 22rpx;
font-weight: 500;
color: $gray-c;
line-height: 30rpx;
}
.agreement-box {
font-size: 26rpx;
font-weight: 500;
.tcp-text {
color: var(--ui-BG-Main);
}
.agreement-text {
color: $dark-9;
}
}
}
.loginout-btn {
width: 100%;
height: 80rpx;
border-radius: 40rpx;
font-size: 30rpx;
}
.list-border {
font-size: 28rpx;
font-weight: 400;
color: #333333;
border-bottom: 2rpx solid #eeeeee;
}
:deep(.uni-list-item__content-title) {
font-size: 28rpx;
font-weight: 500;
color: #333;
}
:deep(.uni-list-item__extra-text) {
color: #bbbbbb;
font-size: 28rpx;
}
</style>
+18
View File
@@ -0,0 +1,18 @@
<!-- 网页加载 -->
<template>
<view>
<web-view :src="url" />
</view>
</template>
<script setup>
import { onLoad } from '@dcloudio/uni-app';
import { ref } from 'vue';
const url = ref('');
onLoad((options) => {
url.value = decodeURIComponent(options.url);
});
</script>
<style lang="scss" scoped></style>
+305
View File
@@ -0,0 +1,305 @@
<!-- 收货地址的新增/编辑 -->
<template>
<s-layout :title="state.model.id ? '编辑地址' : '新增地址'">
<uni-forms
ref="addressFormRef"
v-model="state.model"
:rules="rules"
validateTrigger="bind"
labelWidth="160"
labelAlign="left"
border
:labelStyle="{ fontWeight: 'bold' }"
>
<view class="bg-white form-box ss-p-x-30">
<uni-forms-item name="name" label="收货人" class="form-item">
<uni-easyinput
v-model="state.model.name"
placeholder="请填写收货人姓名"
:inputBorder="false"
placeholderStyle="color:#BBBBBB;font-size:30rpx;font-weight:400;line-height:normal"
/>
</uni-forms-item>
<uni-forms-item name="mobile" label="手机号" class="form-item">
<uni-easyinput
v-model="state.model.mobile"
type="number"
placeholder="请输入手机号"
:inputBorder="false"
placeholderStyle="color:#BBBBBB;font-size:30rpx;font-weight:400;line-height:normal"
>
</uni-easyinput>
</uni-forms-item>
<uni-forms-item
name="areaName"
label="省市区"
@tap="state.showRegion = true"
class="form-item"
>
<uni-easyinput
v-model="state.model.areaName"
disabled
:inputBorder="false"
:styles="{ disableColor: '#fff', color: '#333' }"
placeholderStyle="color:#BBBBBB;font-size:30rpx;font-weight:400;line-height:normal"
placeholder="请选择省市区"
>
<template v-slot:right>
<uni-icons type="right" />
</template>
</uni-easyinput>
</uni-forms-item>
<uni-forms-item
name="detailAddress"
label="详细地址"
:formItemStyle="{ alignItems: 'flex-start' }"
:labelStyle="{ lineHeight: '5em' }"
class="textarea-item"
>
<uni-easyinput
:inputBorder="false"
type="textarea"
v-model="state.model.detailAddress"
placeholderStyle="color:#BBBBBB;font-size:30rpx;font-weight:400;line-height:normal"
placeholder="请输入详细地址"
clearable
/>
</uni-forms-item>
</view>
<view class="ss-m-y-20 bg-white ss-p-x-30 ss-flex ss-row-between ss-col-center default-box">
<view class="default-box-title"> 设为默认地址 </view>
<su-switch style="transform: scale(0.8)" v-model="state.model.defaultStatus" />
</view>
</uni-forms>
<su-fixed bottom :opacity="false" bg="" placeholder :noFixed="false" :index="10">
<view class="footer-box ss-flex-col ss-row-between ss-p-20">
<view class="ss-m-b-20">
<button class="ss-reset-button save-btn ui-Shadow-Main" @tap="onSave">保存</button>
</view>
<button v-if="state.model.id" class="ss-reset-button cancel-btn" @tap="onDelete">
删除
</button>
</view>
</su-fixed>
<!-- 省市区弹窗 -->
<su-region-picker
:show="state.showRegion"
@cancel="state.showRegion = false"
@confirm="onRegionConfirm"
/>
</s-layout>
</template>
<script setup>
import { ref, reactive, unref } from 'vue';
import sheep from '@/sheep';
import { onLoad } from '@dcloudio/uni-app';
import _ from 'lodash-es';
import { mobile } from '@/sheep/validate/form';
import AreaApi from '@/sheep/api/system/area';
import AddressApi from '@/sheep/api/member/address';
const addressFormRef = ref(null);
const state = reactive({
showRegion: false,
model: {
name: '',
mobile: '',
detailAddress: '',
defaultStatus: false,
areaName: '',
},
rules: {},
});
const rules = {
name: {
rules: [
{
required: true,
errorMessage: '请输入收货人姓名',
},
],
},
mobile,
detailAddress: {
rules: [
{
required: true,
errorMessage: '请输入详细地址',
},
],
},
areaName: {
rules: [
{
required: true,
errorMessage: '请选择您的位置',
},
],
},
};
// 确认选择地区
const onRegionConfirm = (e) => {
state.model.areaName = `${e.province_name} ${e.city_name} ${e.district_name}`;
state.model.areaId = e.district_id;
state.showRegion = false;
};
// 获得地区数据
const getAreaData = () => {
if (_.isEmpty(uni.getStorageSync('areaData'))) {
AreaApi.getAreaTree().then((res) => {
if (res.code === 0) {
uni.setStorageSync('areaData', res.data);
}
});
}
};
// 保存收货地址
const onSave = async () => {
// 参数校验
const validate = await unref(addressFormRef)
.validate()
.catch((error) => {
console.log('error: ', error);
});
if (!validate) {
return;
}
// 提交请求
const formData = {
...state.model,
};
const { code } =
state.model.id > 0
? await AddressApi.updateAddress(formData)
: await AddressApi.createAddress(formData);
if (code === 0) {
sheep.$router.back();
}
};
// 删除收货地址
const onDelete = () => {
uni.showModal({
title: '提示',
content: '确认删除此收货地址吗?',
success: async function (res) {
if (!res.confirm) {
return;
}
const { code } = await AddressApi.deleteAddress(state.model.id);
if (code === 0) {
sheep.$router.back();
}
},
});
};
onLoad(async (options) => {
// 获得地区数据
getAreaData();
// 情况一:基于 id 获得收件地址
if (options.id) {
let { code, data } = await AddressApi.getAddress(options.id);
if (code !== 0) {
return;
}
state.model = data;
}
// 情况二:微信导入
if (options.data) {
let data = JSON.parse(options.data);
const areaData = uni.getStorageSync('areaData');
const findAreaByName = (areas, name) => areas.find((item) => item.name === name);
let provinceObj = findAreaByName(areaData, data.province_name);
let cityObj = provinceObj ? findAreaByName(provinceObj.children, data.city_name) : undefined;
let districtObj = cityObj ? findAreaByName(cityObj.children, data.district_name) : undefined;
let areaId = (districtObj || cityObj || provinceObj).id;
state.model = {
...state.model,
areaId,
areaName: [data.province_name, data.city_name, data.district_name]
.filter(Boolean)
.join(' '),
defaultStatus: false,
detailAddress: data.address,
mobile: data.mobile,
name: data.consignee,
};
}
});
</script>
<style lang="scss" scoped>
:deep() {
.uni-forms-item__label .label-text {
font-size: 28rpx !important;
color: #333333 !important;
line-height: normal !important;
}
.uni-easyinput__content-input {
font-size: 28rpx !important;
color: #333333 !important;
line-height: normal !important;
padding-left: 0 !important;
}
.uni-easyinput__content-textarea {
font-size: 28rpx !important;
color: #333333 !important;
line-height: normal !important;
margin-top: 8rpx !important;
}
.uni-icons {
font-size: 40rpx !important;
}
.is-textarea-icon {
margin-top: 22rpx;
}
.is-disabled {
color: #333333;
}
}
.default-box {
width: 100%;
box-sizing: border-box;
height: 100rpx;
.default-box-title {
font-size: 28rpx;
color: #333333;
line-height: normal;
}
}
.footer-box {
.save-btn {
width: 710rpx;
height: 80rpx;
border-radius: 40rpx;
background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
color: $white;
}
.cancel-btn {
width: 710rpx;
height: 80rpx;
border-radius: 40rpx;
background: var(--ui-BG);
}
}
</style>
+165
View File
@@ -0,0 +1,165 @@
<!-- 收件地址列表 -->
<template>
<s-layout :bgStyle="{ color: '#FFF' }" title="收货地址">
<view v-if="state.list.length">
<s-address-item
hasBorderBottom
v-for="item in state.list"
:key="item.id"
:item="item"
@tap="onSelect(item)"
/>
</view>
<su-fixed bottom placeholder>
<view class="footer-box ss-flex ss-row-between ss-p-20">
<!-- 微信小程序和微信H5 -->
<button
v-if="['WechatMiniProgram', 'WechatOfficialAccount'].includes(sheep.$platform.name)"
@tap="importWechatAddress"
class="border ss-reset-button sync-wxaddress ss-m-20 ss-flex ss-row-center ss-col-center"
>
<text class="cicon-weixin ss-p-r-10" style="color: #09bb07; font-size: 40rpx"></text>
导入微信地址
</button>
<button
class="add-btn ss-reset-button ui-Shadow-Main"
@tap="sheep.$router.go('/pages/user/address/edit')"
>
新增收货地址
</button>
</view>
</su-fixed>
<s-empty
v-if="state.list.length === 0 && !state.loading"
text="暂无收货地址"
icon="/static/data-empty.png"
/>
</s-layout>
</template>
<script setup>
import { onBeforeMount, reactive } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
import sheep from '@/sheep';
import { isEmpty } from 'lodash-es';
import AreaApi from '@/sheep/api/system/area';
import AddressApi from '@/sheep/api/member/address';
const state = reactive({
list: [], // 地址列表
loading: true,
openType: '', // 页面打开类型
});
// 选择收货地址
const onSelect = (addressInfo) => {
if (state.openType !== 'select'){ // 不作为选择组件时阻断操作
return
}
uni.$emit('SELECT_ADDRESS', {
addressInfo,
});
sheep.$router.back();
};
// 导入微信地址
function importWechatAddress() {
let wechatAddress = {};
// #ifdef MP
uni.chooseAddress({
success: (res) => {
wechatAddress = {
consignee: res.userName,
mobile: res.telNumber,
province_name: res.provinceName,
city_name: res.cityName,
district_name: res.countyName,
address: res.detailInfo,
region: '',
is_default: false,
};
if (!isEmpty(wechatAddress)) {
sheep.$router.go('/pages/user/address/edit', {
data: JSON.stringify(wechatAddress),
});
}
},
fail: (err) => {
console.log('%cuni.chooseAddress,调用失败', 'color:green;background:yellow');
},
});
// #endif
// #ifdef H5
sheep.$platform.useProvider('wechat').jssdk.openAddress({
success: (res) => {
wechatAddress = {
consignee: res.userName,
mobile: res.telNumber,
province_name: res.provinceName,
city_name: res.cityName,
district_name: res.countryName,
address: res.detailInfo,
region: '',
is_default: false,
};
if (!isEmpty(wechatAddress)) {
sheep.$router.go('/pages/user/address/edit', {
data: JSON.stringify(wechatAddress),
});
}
},
});
// #endif
}
onLoad((option) => {
if (option.type) {
state.openType = option.type;
}
});
onShow(async () => {
state.list = (await AddressApi.getAddressList()).data;
state.loading = false;
});
onBeforeMount(() => {
if (!!uni.getStorageSync('areaData')) {
return;
}
// 提前加载省市区数据
AreaApi.getAreaTree().then((res) => {
if (res.code === 0) {
uni.setStorageSync('areaData', res.data);
}
});
});
</script>
<style lang="scss" scoped>
.footer-box {
.add-btn {
flex: 1;
background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
border-radius: 80rpx;
font-size: 30rpx;
font-weight: 500;
line-height: 80rpx;
color: $white;
position: relative;
z-index: 1;
}
.sync-wxaddress {
flex: 1;
line-height: 80rpx;
background: $white;
border-radius: 80rpx;
font-size: 30rpx;
font-weight: 500;
color: $dark-6;
margin-right: 18rpx;
}
}
</style>
+233
View File
@@ -0,0 +1,233 @@
<!-- 我的商品收藏 -->
<template>
<s-layout title="商品收藏">
<view class="cart-box ss-flex ss-flex-col ss-row-between">
<!-- 头部 -->
<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.pagination.total }}</text> 件商品
</view>
<view class="header-right">
<button
v-if="state.editMode && state.pagination.total"
class="ss-reset-button"
@tap="state.editMode = false"
>
取消
</button>
<button
v-if="!state.editMode && state.pagination.total"
class="ss-reset-button ui-TC-Main"
@tap="state.editMode = true"
>
编辑
</button>
</view>
</view>
<!-- 内容 -->
<view class="cart-content">
<view
class="goods-box ss-r-10 ss-m-b-14"
v-for="item in state.pagination.list"
:key="item.id"
>
<view class="ss-flex ss-col-center">
<label
class="check-box ss-flex ss-col-center ss-p-l-10"
v-if="state.editMode"
@tap="onSelect(item.spuId)"
>
<radio
:checked="state.selectedCollectList.includes(item.spuId)"
color="var(--ui-BG-Main)"
style="transform: scale(0.8)"
@tap.stop="onSelect(item.spuId)"
/>
</label>
<s-goods-item
:title="item.spuName"
:img="item.picUrl"
:price="item.price"
priceColor="#FF3000"
:titleWidth="400"
@tap="
sheep.$router.go('/pages/goods/index', {
id: item.spuId,
})
"
/>
</view>
</view>
</view>
<!-- 底部 -->
<su-fixed bottom :val="0" placeholder v-show="state.editMode">
<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.selectAll"
color="var(--ui-BG-Main)"
style="transform: scale(0.7)"
@tap.stop="onSelectAll"
/>
<view> 全选 </view>
</label>
</view>
<view class="footer-right">
<button
class="ss-reset-button ui-BG-Main-Gradient pay-btn ss-font-28 ui-Shadow-Main"
@tap="onCancel"
>
取消收藏
</button>
</view>
</view>
</su-fixed>
</view>
<uni-load-more
v-if="state.pagination.total > 0"
:status="state.loadStatus"
:content-text="{
contentdown: '上拉加载更多',
}"
@tap="loadMore"
/>
<s-empty v-if="state.pagination.total === 0" text="暂无收藏" icon="/static/collect-empty.png" />
</s-layout>
</template>
<script setup>
import sheep from '@/sheep';
import { reactive } from 'vue';
import { onLoad, onReachBottom } from '@dcloudio/uni-app';
import _ from 'lodash-es';
import FavoriteApi from '@/sheep/api/product/favorite';
import { resetPagination } from '@/sheep/helper/utils';
const sys_navBar = sheep.$platform.navbar;
const state = reactive({
pagination: {
list: [],
total: 0,
pageNo: 1,
pageSize: 6,
},
loadStatus: '',
editMode: false,
selectedCollectList: [], // 选中的 SPU 数组
selectAll: false,
});
async function getData() {
state.loadStatus = 'loading';
const { code, data } = await FavoriteApi.getFavoritePage({
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';
}
// 单选选中
const onSelect = (spuId) => {
if (!state.selectedCollectList.includes(spuId)) {
state.selectedCollectList.push(spuId);
} else {
state.selectedCollectList.splice(state.selectedCollectList.indexOf(spuId), 1);
}
state.selectAll = state.selectedCollectList.length === state.pagination.list.length;
};
// 全选
const onSelectAll = () => {
state.selectAll = !state.selectAll;
if (!state.selectAll) {
state.selectedCollectList = [];
} else {
state.selectedCollectList = state.pagination.list.map((item) => item.spuId);
}
};
async function onCancel() {
if (!state.selectedCollectList) {
return;
}
// 取消收藏
for (const spuId of state.selectedCollectList) {
await FavoriteApi.deleteFavorite(spuId);
}
// 清空选择 + 重新加载
state.editMode = false;
state.selectedCollectList = [];
state.selectAll = false;
resetPagination(state.pagination);
await getData();
}
// 加载更多
function loadMore() {
if (state.loadStatus === 'noMore') {
return;
}
state.pagination.pageNo++;
getData();
}
onReachBottom(() => {
loadMore();
});
onLoad(() => {
getData();
});
</script>
<style lang="scss" scoped>
.cart-box {
.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 {
height: 80rpx;
line-height: 80rpx;
border-radius: 40rpx;
padding: 0 40rpx;
min-width: 200rpx;
}
}
.cart-content {
width: 100%;
margin-top: 70rpx;
padding: 0 20rpx;
box-sizing: border-box;
.goods-box {
background-color: #fff;
&:last-child {
margin-bottom: 40rpx;
}
}
}
}
</style>
+308
View File
@@ -0,0 +1,308 @@
<!-- 商品浏览记录 -->
<template>
<s-layout :bgStyle="{ color: '#f2f2f2' }" title="我的足迹">
<view class="cart-box ss-flex ss-flex-col ss-row-between">
<!-- 头部 -->
<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.pagination.total }}
</text>
件商品
</view>
<view class="header-right">
<button
v-if="state.editMode && state.pagination.total"
class="ss-reset-button"
@tap="state.editMode = false"
>
取消
</button>
<button
v-if="!state.editMode && state.pagination.total"
class="ss-reset-button ui-TC-Main"
@tap="state.editMode = true"
>
编辑
</button>
</view>
</view>
<!-- 内容 -->
<view class="cart-content">
<view
class="goods-box ss-r-10 ss-m-b-14"
v-for="item in state.pagination.list"
:key="item.id"
>
<view class="ss-flex ss-col-center">
<label
class="check-box ss-flex ss-col-center ss-p-l-10"
v-if="state.editMode"
@tap="onSelect(item.spuId)"
>
<radio
:checked="state.selectedSpuIdList.includes(item.spuId)"
color="var(--ui-BG-Main)"
style="transform: scale(0.8)"
@tap.stop="onSelect(item.spuId)"
/>
</label>
<s-goods-item
:title="item.spuName"
:img="item.picUrl"
:price="item.price"
:skuText="item.introduction"
priceColor="#FF3000"
:titleWidth="400"
@tap="
sheep.$router.go('/pages/goods/index', {
id: item.spuId,
})
"
>
</s-goods-item>
</view>
</view>
</view>
<!-- 底部 -->
<su-fixed bottom :val="0" placeholder v-show="state.editMode">
<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.selectAll"
color="var(--ui-BG-Main)"
style="transform: scale(0.7)"
@tap.stop="onSelectAll"
/>
<view>全选</view>
</label>
</view>
<view class="footer-right ss-flex">
<button
:class="[
'ss-reset-button pay-btn ss-font-28 ',
{
'ui-BG-Main-Gradient': state.selectedSpuIdList.length > 0,
'ui-Shadow-Main': state.selectedSpuIdList.length > 0,
},
]"
@tap="onDelete"
>
删除足迹
</button>
<button
class="ss-reset-button ui-BG-Main-Gradient pay-btn ss-font-28 ui-Shadow-Main ml-2"
@tap="onClean"
>
清空
</button>
</view>
</view>
</su-fixed>
</view>
<uni-load-more
v-if="state.pagination.total > 0"
:status="state.loadStatus"
:content-text="{
contentdown: '上拉加载更多',
}"
@tap="loadMore"
/>
<s-empty
v-if="state.pagination.total === 0"
text="暂无浏览记录"
icon="/static/collect-empty.png"
/>
</s-layout>
</template>
<script setup>
import sheep from '@/sheep';
import { reactive } from 'vue';
import { onLoad, onReachBottom } from '@dcloudio/uni-app';
import _ from 'lodash-es';
import SpuHistoryApi from '@/sheep/api/product/history';
import { cloneDeep } from '@/sheep/helper/utils';
const sys_navBar = sheep.$platform.navbar;
const pagination = {
list: [],
pageNo: 1,
total: 1,
pageSize: 10,
};
const state = reactive({
pagination: cloneDeep(pagination),
loadStatus: '',
editMode: false,
selectedSpuIdList: [],
selectAll: false,
});
async function getList() {
state.loadStatus = 'loading';
const { code, data } = await SpuHistoryApi.getBrowseHistoryPage({
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';
}
// 单选选中
const onSelect = (id) => {
if (!state.selectedSpuIdList.includes(id)) {
state.selectedSpuIdList.push(id);
} else {
state.selectedSpuIdList.splice(state.selectedSpuIdList.indexOf(id), 1);
}
state.selectAll = state.selectedSpuIdList.length === state.pagination.list.length;
};
// 全选
const onSelectAll = () => {
state.selectAll = !state.selectAll;
if (!state.selectAll) {
state.selectedSpuIdList = [];
} else {
state.pagination.list.forEach((item) => {
if (state.selectedSpuIdList.includes(item.spuId)) {
state.selectedSpuIdList.splice(state.selectedSpuIdList.indexOf(item.spuId), 1);
}
state.selectedSpuIdList.push(item.spuId);
});
}
};
// 删除足迹
async function onDelete() {
if (state.selectedSpuIdList.length <= 0) {
return;
}
const { code } = await SpuHistoryApi.deleteBrowseHistory(state.selectedSpuIdList);
if (code === 0) {
reload();
}
}
// 清空
async function onClean() {
const { code } = await SpuHistoryApi.cleanBrowseHistory();
if (code === 0) {
reload();
}
}
function reload() {
state.editMode = false;
state.selectedSpuIdList = [];
state.selectAll = false;
state.pagination = pagination;
getList();
}
// 加载更多
function loadMore() {
if (state.loadStatus !== 'noMore') {
state.pagination.pageNo += 1;
getList();
}
}
onReachBottom(() => {
loadMore();
});
onLoad(() => {
getList();
});
</script>
<style lang="scss" scoped>
.cart-box {
.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 {
height: 80rpx;
line-height: 80rpx;
border-radius: 40rpx;
padding: 0 40rpx;
min-width: 200rpx;
}
}
.cart-content {
width: 100%;
padding: 0 20rpx;
box-sizing: border-box;
margin-top: 70rpx;
.goods-box {
background-color: #fff;
&:last-child {
margin-bottom: 40rpx;
}
}
}
}
.title-card {
padding: 36rpx 0 46rpx 20rpx;
.img-box {
width: 164rpx;
height: 164rpx;
.order-img {
width: 164rpx;
height: 164rpx;
}
}
.check-box {
height: 100%;
}
.title-text {
font-size: 28rpx;
font-weight: 500;
color: #333333;
}
.params-box {
.params-title {
height: 38rpx;
background: #f4f4f4;
border-radius: 2rpx;
font-size: 24rpx;
font-weight: 400;
color: #666666;
}
}
.price-text {
color: $red;
font-family: OPPOSANS;
}
}
</style>
+282
View File
@@ -0,0 +1,282 @@
<template>
<s-layout :bgStyle="{ color: '#FFF' }" title="选择自提门店">
<view class="storeBox" ref="container">
<view
class="storeBox-box"
v-for="(item, index) in state.storeList"
:key="index"
@tap="checked(item)"
>
<view class="store-img">
<image :src="item.logo" class="img" />
</view>
<view class="store-cent-left">
<view class="store-name">{{ item.name }}</view>
<view class="store-address line1">
{{ item.areaName }}{{ ', ' + item.detailAddress }}
</view>
</view>
<view class="row-right ss-flex-col ss-col-center">
<view>
<!-- #ifdef H5 -->
<a class="store-phone" :href="'tel:' + item.phone">
<view class="iconfont">
<view class="ss-rest-button">
<text class="_icon-forward" />
</view>
</view>
</a>
<!-- #endif -->
<!-- #ifdef MP -->
<view class="store-phone" @click="call(item.phone)">
<view class="iconfont">
<view class="ss-rest-button">
<text class="_icon-forward" />
</view>
</view>
</view>
<!-- #endif -->
</view>
<view class="store-distance ss-flex ss-row-center" @tap.stop="showMaoLocation(item)">
<text class="addressTxt" v-if="item.distance">
距离{{ item.distance.toFixed(2) }}千米
</text>
<text class="addressTxt" v-else>查看地图</text>
<view class="iconfont">
<view class="ss-rest-button">
<text class="_icon-forward" />
</view>
</view>
</view>
</view>
</view>
</view>
</s-layout>
</template>
<script setup>
import DeliveryApi from '@/sheep/api/trade/delivery';
import { onMounted, reactive } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import sheep from '@/sheep';
const LONGITUDE = 'user_longitude';
const LATITUDE = 'user_latitude';
const state = reactive({
loaded: false,
loading: false,
storeList: [],
system_store: {},
locationShow: false,
user_latitude: 0,
user_longitude: 0,
});
const call = (phone) => {
uni.makePhoneCall({
phoneNumber: phone,
});
};
const selfLocation = () => {
// #ifdef H5
const jsWxSdk = sheep.$platform.useProvider('wechat').jsWxSdk;
if (jsWxSdk.isWechat()) {
jsWxSdk.getLocation((res) => {
state.user_latitude = res.latitude;
state.user_longitude = res.longitude;
uni.setStorageSync(LATITUDE, res.latitude);
uni.setStorageSync(LONGITUDE, res.longitude);
getList();
});
} else {
// #endif
uni.getLocation({
type: 'gcj02',
success: (res) => {
try {
state.user_latitude = res.latitude;
state.user_longitude = res.longitude;
uni.setStorageSync(LATITUDE, res.latitude);
uni.setStorageSync(LONGITUDE, res.longitude);
} catch (e) {
console.error(e);
}
getList();
},
complete: () => {
getList();
},
});
// #ifdef H5
}
// #endif
};
const showMaoLocation = (e) => {
// #ifdef H5
const jsWxSdk = sheep.$platform.useProvider('wechat').jsWxSdk;
if (jsWxSdk.isWechat()) {
jsWxSdk.openLocation({
latitude: Number(e.latitude),
longitude: Number(e.longitude),
name: e.name,
address: `${e.areaName}-${e.detailAddress}`,
});
} else {
// #endif
uni.openLocation({
latitude: Number(e.latitude),
longitude: Number(e.longitude),
name: e.name,
address: `${e.areaName}-${e.detailAddress}`,
success: function () {
console.log('success');
},
});
// #ifdef H5
}
// #endif
};
/**
* 选中门店
*/
const checked = (addressInfo) => {
uni.$emit('SELECT_PICK_UP_INFO', {
addressInfo,
});
sheep.$router.back();
};
/**
* 获取门店列表数据
*/
const getList = async () => {
if (state.loading || state.loaded) {
return;
}
state.loading = true;
const { data, code } = await DeliveryApi.getDeliveryPickUpStoreList({
latitude: state.user_latitude,
longitude: state.user_longitude,
});
if (code !== 0) {
return;
}
state.loading = false;
state.storeList = data;
};
onMounted(() => {
if (state.user_latitude && state.user_longitude) {
getList();
} else {
selfLocation();
getList();
}
});
onLoad(() => {
try {
state.user_latitude = uni.getStorageSync(LATITUDE);
state.user_longitude = uni.getStorageSync(LONGITUDE);
} catch (e) {
console.error(e);
}
});
</script>
<style lang="scss" scoped>
.line1 {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.geoPage {
position: fixed;
width: 100%;
height: 100%;
top: 0;
z-index: 10000;
}
.storeBox {
width: 100%;
background-color: #fff;
padding: 0 30rpx;
}
.storeBox-box {
width: 100%;
height: auto;
display: flex;
align-items: center;
padding: 23rpx 0;
justify-content: space-between;
border-bottom: 1px solid #eee;
}
.store-cent {
display: flex;
align-items: center;
width: 80%;
}
.store-cent-left {
//width: 45%;
flex: 2;
}
.store-img {
flex: 1;
width: 120rpx;
height: 120rpx;
border-radius: 6rpx;
margin-right: 22rpx;
}
.store-img .img {
width: 100%;
height: 100%;
}
.store-name {
color: #282828;
font-size: 30rpx;
margin-bottom: 22rpx;
font-weight: 800;
}
.store-address {
color: #666666;
font-size: 24rpx;
}
.store-phone {
width: 50rpx;
height: 50rpx;
color: #fff;
border-radius: 50%;
display: block;
text-align: center;
line-height: 48rpx;
background-color: #e83323;
margin-bottom: 22rpx;
text-decoration: none;
}
.store-distance {
font-size: 22rpx;
color: #e83323;
}
.iconfont {
font-size: 20rpx;
}
.row-right {
flex: 2;
//display: flex;
//flex-direction: column;
//align-items: flex-end;
//width: 33.5%;
}
</style>
+467
View File
@@ -0,0 +1,467 @@
<!-- 用户信息 -->
<template>
<s-layout title="用户信息" class="set-userinfo-wrap">
<uni-forms
:model="state.model"
:rules="state.rules"
labelPosition="left"
border
class="form-box"
>
<!-- 头像 -->
<view class="ss-flex ss-row-center ss-col-center ss-p-t-60 ss-p-b-0 bg-white">
<view class="header-box-content">
<su-image
class="content-img"
isPreview
:current="0"
:src="state.model?.avatar || sheep.$url.static('/static/img/shop/default_avatar.png')"
:height="160"
:width="160"
:radius="80"
mode="scaleToFill"
/>
<view class="avatar-action">
<!-- #ifdef MP -->
<button
class="ss-reset-button avatar-action-btn"
open-type="chooseAvatar"
@chooseavatar="onChooseAvatar"
>
修改
</button>
<!-- #endif -->
<!-- #ifndef MP -->
<button class="ss-reset-button avatar-action-btn" @tap="onChangeAvatar">修改</button>
<!-- #endif -->
</view>
</view>
</view>
<view class="bg-white ss-p-x-30">
<!-- 昵称 + 性别 -->
<uni-forms-item name="nickname" label="昵称">
<uni-easyinput
v-model="state.model.nickname"
type="nickname"
placeholder="设置昵称"
:inputBorder="false"
:placeholderStyle="placeholderStyle"
/>
</uni-forms-item>
<uni-forms-item name="sex" label="性别">
<view class="ss-flex ss-col-center ss-h-100">
<radio-group @change="onChangeGender" class="ss-flex ss-col-center">
<label class="radio" v-for="item in sexRadioMap" :key="item.value">
<view class="ss-flex ss-col-center ss-m-r-32">
<radio
:value="item.value"
color="var(--ui-BG-Main)"
style="transform: scale(0.8)"
:checked="parseInt(item.value) === state.model?.sex"
/>
<view class="gender-name">{{ item.name }}</view>
</view>
</label>
</radio-group>
</view>
</uni-forms-item>
<uni-forms-item name="mobile" label="手机号" @tap="onChangeMobile">
<uni-easyinput
v-model="userInfo.mobile"
placeholder="请绑定手机号"
:inputBorder="false"
disabled
:styles="{ disableColor: '#fff' }"
:placeholderStyle="placeholderStyle"
:clearable="false"
>
<template v-slot:right>
<view class="ss-flex ss-col-center">
<su-radio v-if="userInfo.verification?.mobile" :modelValue="true" />
<button v-else class="ss-reset-button ss-flex ss-col-center ss-row-center">
<text class="_icon-forward" style="color: #bbbbbb; font-size: 26rpx"></text>
</button>
</view>
</template>
</uni-easyinput>
</uni-forms-item>
<uni-forms-item name="password" label="登录密码" @tap="onSetPassword">
<uni-easyinput
v-model="userInfo.password"
placeholder="点击修改登录密码"
:inputBorder="false"
:styles="{ disableColor: '#fff' }"
disabled
placeholderStyle="color:#BBBBBB;font-size:28rpx;line-height:normal"
:clearable="false"
>
<template v-slot:right>
<view class="ss-flex ss-col-center">
<su-radio
class="ss-flex"
v-if="userInfo.verification?.password"
:modelValue="true"
/>
<button v-else class="ss-reset-button ss-flex ss-col-center ss-row-center">
<text class="_icon-forward" style="color: #bbbbbb; font-size: 26rpx" />
</button>
</view>
</template>
</uni-easyinput>
</uni-forms-item>
</view>
<view class="bg-white ss-m-t-14">
<uni-list>
<uni-list-item
clickable
@tap="sheep.$router.go('/pages/user/address/list')"
title="地址管理"
showArrow
:border="false"
class="list-border"
/>
</uni-list>
</view>
</uni-forms>
<!-- 当前社交平台的绑定关系只处理 wechat 微信场景 -->
<view v-if="sheep.$platform.name !== 'H5'">
<view class="title-box ss-p-l-30">第三方账号绑定</view>
<view class="account-list ss-flex ss-row-between">
<view v-if="'WechatOfficialAccount' === sheep.$platform.name" class="ss-flex ss-col-center">
<image
class="list-img"
:src="sheep.$url.static('/static/img/shop/platform/WechatOfficialAccount.png')"
/>
<text class="list-name">微信公众号</text>
</view>
<view v-if="'WechatMiniProgram' === sheep.$platform.name" class="ss-flex ss-col-center">
<image
class="list-img"
:src="sheep.$url.static('/static/img/shop/platform/WechatMiniProgram.png')"
/>
<text class="list-name">微信小程序</text>
</view>
<view v-if="'App' === sheep.$platform.name" class="ss-flex ss-col-center">
<image
class="list-img"
:src="sheep.$url.static('/static/img/shop/platform/wechat.png')"
/>
<text class="list-name">微信开放平台</text>
</view>
<view class="ss-flex ss-col-center">
<view class="info ss-flex ss-col-center" v-if="state.thirdInfo">
<image class="avatar ss-m-r-20" :src="sheep.$url.cdn(state.thirdInfo.avatar)" />
<text class="name">{{ state.thirdInfo.nickname }}</text>
</view>
<view class="bind-box ss-m-l-20">
<button
v-if="state.thirdInfo.openid"
class="ss-reset-button relieve-btn"
@tap="unBindThirdOauth"
>
解绑
</button>
<button v-else class="ss-reset-button bind-btn" @tap="bindThirdOauth">绑定</button>
</view>
</view>
</view>
</view>
<su-fixed bottom placeholder bg="none">
<view class="footer-box ss-p-20">
<button class="ss-rest-button logout-btn ui-Shadow-Main" @tap="onSubmit">保存</button>
</view>
</su-fixed>
</s-layout>
</template>
<script setup>
import { computed, reactive, onBeforeMount } from 'vue';
import sheep from '@/sheep';
import { clone } from 'lodash-es';
import { showAuthModal } from '@/sheep/hooks/useModal';
import UserApi from '@/sheep/api/member/user';
import {
chooseAndUploadFile,
uploadFilesFromPath,
} from '@/sheep/components/s-uploader/choose-and-upload-file';
const state = reactive({
model: {}, // 个人信息
rules: {},
thirdInfo: {}, // 社交用户的信息
});
const placeholderStyle = 'color:#BBBBBB;font-size:28rpx;line-height:normal';
const sexRadioMap = [
{
name: '男',
value: '1',
},
{
name: '女',
value: '2',
},
];
const userInfo = computed(() => sheep.$store('user').userInfo);
// 选择性别
function onChangeGender(e) {
state.model.sex = e.detail.value;
}
// 修改手机号
const onChangeMobile = () => {
showAuthModal('changeMobile');
};
// 选择微信的头像,进行上传
async function onChooseAvatar(e) {
debugger;
const tempUrl = e.detail.avatarUrl || '';
if (!tempUrl) return;
const files = await uploadFilesFromPath(tempUrl);
if (files.length > 0) {
state.model.avatar = files[0].url;
}
}
// 手动选择头像,进行上传
async function onChangeAvatar() {
const files = await chooseAndUploadFile({ type: 'image' });
if (files.length > 0) {
state.model.avatar = files[0].url;
}
}
// 修改密码
function onSetPassword() {
showAuthModal('changePassword');
}
// 绑定第三方账号
async function bindThirdOauth() {
let result = await sheep.$platform.useProvider('wechat').bind();
if (result) {
await getUserInfo();
}
}
// 解绑第三方账号
function unBindThirdOauth() {
uni.showModal({
title: '解绑提醒',
content: '解绑后您将无法通过微信登录此账号',
cancelText: '再想想',
confirmText: '确定',
success: async function (res) {
if (!res.confirm) {
return;
}
const result = await sheep.$platform.useProvider('wechat').unbind(state.thirdInfo.openid);
if (result) {
await getUserInfo();
}
},
});
}
// 保存信息
async function onSubmit() {
const { code } = await UserApi.updateUser({
avatar: state.model.avatar,
nickname: state.model.nickname,
sex: state.model.sex,
});
if (code === 0) {
await getUserInfo();
}
}
// 获得用户信息
const getUserInfo = async () => {
// 个人信息
const userInfo = await sheep.$store('user').getInfo();
state.model = clone(userInfo);
// 获得社交用户的信息
if (sheep.$platform.name !== 'H5') {
const result = await sheep.$platform.useProvider('wechat').getInfo();
state.thirdInfo = result || {};
}
};
onBeforeMount(() => {
getUserInfo();
});
</script>
<style lang="scss" scoped>
:deep() {
.uni-file-picker {
border-radius: 50%;
}
.uni-file-picker__container {
margin: -14rpx -12rpx;
}
.file-picker__progress {
height: 0 !important;
}
.uni-list-item__content-title {
font-size: 28rpx !important;
color: #333333 !important;
line-height: normal !important;
}
.uni-icons {
font-size: 40rpx !important;
}
.is-disabled {
color: #333333;
}
}
:deep(.disabled) {
opacity: 1;
}
.gender-name {
font-size: 28rpx;
font-weight: 500;
line-height: normal;
color: #333333;
}
.title-box {
font-size: 28rpx;
font-weight: 500;
color: #666666;
line-height: 100rpx;
}
.logout-btn {
width: 710rpx;
height: 80rpx;
background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
border-radius: 40rpx;
font-size: 30rpx;
font-weight: 500;
color: $white;
}
.radio-dark {
filter: grayscale(100%);
filter: gray;
opacity: 0.4;
}
.content-img {
border-radius: 50%;
}
.header-box-content {
position: relative;
width: 160rpx;
height: 160rpx;
overflow: hidden;
border-radius: 50%;
}
.avatar-action {
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: 0;
z-index: 1;
width: 160rpx;
height: 46rpx;
background: rgba(#000000, 0.3);
.avatar-action-btn {
width: 160rpx;
height: 46rpx;
font-weight: 500;
font-size: 24rpx;
color: #ffffff;
}
}
// 绑定项
.account-list {
background-color: $white;
height: 100rpx;
padding: 0 20rpx;
.list-img {
width: 40rpx;
height: 40rpx;
margin-right: 10rpx;
}
.list-name {
font-size: 28rpx;
color: #333333;
}
.info {
.avatar {
width: 38rpx;
height: 38rpx;
border-radius: 50%;
overflow: hidden;
}
.name {
font-size: 28rpx;
font-weight: 400;
color: $dark-9;
}
}
.bind-box {
width: 100rpx;
height: 50rpx;
line-height: normal;
display: flex;
justify-content: center;
align-items: center;
font-size: 24rpx;
.bind-btn {
width: 100%;
height: 100%;
border-radius: 25rpx;
background: #f4f4f4;
color: #999999;
}
.relieve-btn {
width: 100%;
height: 100%;
border-radius: 25rpx;
background: var(--ui-BG-Main-opacity-1);
color: var(--ui-BG-Main);
}
}
}
.list-border {
font-size: 28rpx;
font-weight: 400;
color: #333333;
border-bottom: 2rpx solid #eeeeee;
}
image {
width: 100%;
height: 100%;
}
</style>
+379
View File
@@ -0,0 +1,379 @@
<!-- 我的钱包 -->
<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-64">
<view class="money-num">{{
state.showMoney ? fen2yuan(userWallet.balance) : '*****'
}}</view>
<button class="ss-reset-button topup-btn" @tap="sheep.$router.go('/pages/pay/recharge')">
充值
</button>
</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.data"
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"></text>
</button>
</uni-datetime-picker>
<view class="total-box">
<view class="ss-m-b-10">总收入{{ fen2yuan(state.summary.totalIncome) }}</view>
<view>总支出{{ fen2yuan(state.summary.totalExpense) }}</view>
</view>
</view>
<su-tabs
:list="tabMaps"
@change="onChange"
:scrollable="false"
:current="state.currentTab"
></su-tabs>
</su-sticky>
<s-empty v-if="state.pagination.total === 0" text="暂无数据" icon="/static/data-empty.png" />
<!-- 钱包记录 -->
<view v-if="state.pagination.total > 0">
<view
class="wallet-list ss-flex border-bottom"
v-for="item in state.pagination.list"
:key="item.id"
>
<view class="list-content">
<view class="title-box ss-flex ss-row-between ss-m-b-20">
<text class="title ss-line-1">
{{ item.title }}
</text>
<view class="money">
<text v-if="item.price >= 0" class="add">+{{ fen2yuan(item.price) }}</text>
<text v-else class="minus">{{ fen2yuan(item.price) }}</text>
</view>
</view>
<text class="time">
{{ sheep.$helper.timeFormat(state.createTime, 'yyyy-mm-dd hh:MM:ss') }}
</text>
</view>
</view>
</view>
<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 PayWalletApi from '@/sheep/api/pay/wallet';
import { fen2yuan } from '@/sheep/hooks/useGoods';
import { resetPagination } from '@/sheep/helper/utils';
const headerBg = sheep.$url.css('/static/img/shop/user/wallet_card_bg.png');
// 数据
const state = reactive({
showMoney: false,
date: [], // 筛选的时间段
currentTab: 0,
pagination: {
list: [],
total: 0,
pageNo: 1,
pageSize: 8,
},
summary: {
totalIncome: 0,
totalExpense: 0,
},
loadStatus: '',
today: '',
});
const tabMaps = [
{
name: '全部',
value: '',
},
{
name: '收入',
value: '1',
},
{
name: '支出',
value: '2',
},
];
const userWallet = computed(() => sheep.$store('user').userWallet);
// 格式化时间段
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';
const { data, code } = await PayWalletApi.getWalletTransactionPage({
pageNo: state.pagination.pageNo,
pageSize: state.pagination.pageSize,
type: 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';
}
// 获得钱包统计
async function getSummary() {
const { data, code } = await PayWalletApi.getWalletTransactionSummary({
createTime: [state.date[0] + ' 00:00:00', state.date[1] + ' 23:59:59'],
});
if (code !== 0) {
return;
}
state.summary = data;
}
onLoad(() => {
state.today = dayjs().format('YYYY-MM-DD');
state.date = [state.today, state.today];
getLogList();
getSummary();
// 刷新钱包的缓存
sheep.$store('user').getWallet();
});
// 处理 tab 切换
function onChange(e) {
state.currentTab = e.index;
// 重新加载列表
resetPagination(state.pagination);
getLogList();
getSummary();
}
// 处理时间筛选
function onChangeTime(e) {
state.date[0] = e[0];
state.date[1] = e[e.length - 1];
// 重新加载列表
resetPagination(state.pagination);
getLogList();
getSummary();
}
onReachBottom(() => {
if (state.loadStatus === 'noMore') {
return;
}
state.pagination.pageNo++;
getLogList();
});
</script>
<style lang="scss" scoped>
// 钱包
.header-box {
background-color: $white;
padding: 30rpx;
.card-box {
width: 100%;
min-height: 300rpx;
padding: 40rpx;
background-size: 100% 100%;
border-radius: 30rpx;
overflow: hidden;
position: relative;
z-index: 1;
box-sizing: border-box;
&::after {
content: '';
display: block;
width: 100%;
height: 100%;
z-index: 2;
position: absolute;
top: 0;
left: 0;
background: v-bind(headerBg) no-repeat;
pointer-events: none;
}
.card-head {
color: $white;
font-size: 30rpx;
}
.ss-eye-icon {
font-size: 40rpx;
color: $white;
}
.money-num {
font-size: 70rpx;
line-height: 70rpx;
font-weight: 500;
color: $white;
font-family: OPPOSANS;
}
.reduce-num {
font-size: 26rpx;
font-weight: 400;
color: $white;
}
.topup-btn {
width: 120rpx;
height: 60rpx;
line-height: 60rpx;
border-radius: 30px;
font-size: 26rpx;
font-weight: 500;
background-color: $white;
color: var(--ui-BG-Main);
}
}
}
// 筛选
.filter-box {
height: 114rpx;
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;
}
}
}
.tabs-box {
background: $white;
border-bottom: 2rpx solid #eeeeee;
}
// 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;
}
}
}
</style>
+282
View File
@@ -0,0 +1,282 @@
<!-- 我的积分 -->
<template>
<s-layout class="wallet-wrap" title="我的积分" navbar="inner">
<view
class="header-box ss-flex ss-flex-col ss-row-center ss-col-center"
:style="[
{
marginTop: '-' + Number(statusBarHeight + 88) + 'rpx',
paddingTop: Number(statusBarHeight + 88) + 'rpx',
},
]"
>
<view class="header-bg">
<view class="bg" />
</view>
<view class="score-box ss-flex-col ss-row-center ss-col-center">
<view class="ss-m-b-30">
<text class="all-title ss-m-r-8">当前积分</text>
</view>
<text class="all-num">{{ userInfo.point || 0 }}</text>
</view>
</view>
<!-- tab -->
<su-sticky :customNavHeight="sys_navBar">
<!-- 统计 -->
<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"></text>
</button>
</uni-datetime-picker>
<!-- TODO 芋艿钱包-可优化展示一下 -->
<!-- <view class="total-box">-->
<!-- <view class="ss-m-b-10">总收入{{ state.pagination.income }}</view>-->
<!-- <view>总支出{{ -state.pagination.expense }}</view>-->
<!-- </view>-->
</view>
<su-tabs
:list="tabMaps"
@change="onChange"
:scrollable="false"
:current="state.currentTab"
></su-tabs>
</su-sticky>
<!-- list -->
<view class="list-box">
<view v-if="state.pagination.total > 0">
<view
class="list-item ss-flex ss-col-center ss-row-between"
v-for="item in state.pagination.list"
:key="item.id"
>
<view class="ss-flex-col">
<view class="name"
>{{ item.title }}{{ item.description ? ' - ' + item.description : '' }}</view
>
<view class="time">{{
sheep.$helper.timeFormat(item.createTime, 'yyyy-mm-dd hh:MM:ss')
}}</view>
</view>
<view class="add" v-if="item.point > 0">+{{ item.point }}</view>
<view class="minus" v-else>{{ item.point }}</view>
</view>
</view>
<s-empty v-else text="暂无数据" icon="/static/data-empty.png" />
</view>
<uni-load-more
v-if="state.pagination.total > 0"
:status="state.loadStatus"
:content-text="{
contentdown: '上拉加载更多',
}"
@tap="onLoadMore"
/>
</s-layout>
</template>
<script setup>
import sheep from '@/sheep';
import { onLoad, onReachBottom } from '@dcloudio/uni-app';
import { computed, reactive } from 'vue';
import _ from 'lodash-es';
import dayjs from 'dayjs';
import PointApi from '@/sheep/api/member/point';
import { resetPagination } from '@/sheep/helper/utils';
const statusBarHeight = sheep.$platform.device.statusBarHeight * 2;
const userInfo = computed(() => sheep.$store('user').userInfo);
const sys_navBar = sheep.$platform.navbar;
const state = reactive({
currentTab: 0,
pagination: {
list: 0,
total: 0,
pageSize: 6,
pageNo: 1,
},
loadStatus: '',
date: [],
today: '',
});
const tabMaps = [
{
name: '全部',
value: 'all',
},
{
name: '收入',
value: 'true',
},
{
name: '支出',
value: 'false',
},
];
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 PointApi.getPointRecordPage({
pageNo: state.pagination.pageNo,
pageSize: state.pagination.pageSize,
addStatus: state.currentTab > 0 ? tabMaps[state.currentTab].value : undefined,
'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';
}
onLoad(() => {
state.today = dayjs().format('YYYY-MM-DD');
state.date = [state.today, state.today];
getLogList();
});
function onChange(e) {
state.currentTab = e.index;
resetPagination(state.pagination);
getLogList();
}
function onChangeTime(e) {
state.date[0] = e[0];
state.date[1] = e[e.length - 1];
resetPagination(state.pagination);
getLogList();
}
function onLoadMore() {
if (state.loadStatus === 'noMore') {
return;
}
state.pagination.pageNo++;
getLogList();
}
onReachBottom(() => {
onLoadMore();
});
</script>
<style lang="scss" scoped>
.header-box {
width: 100%;
background: linear-gradient(180deg, var(--ui-BG-Main) 0%, var(--ui-BG-Main-gradient) 100%)
no-repeat;
background-size: 750rpx 100%;
padding: 0 0 120rpx 0;
box-sizing: border-box;
.score-box {
height: 100%;
.all-num {
font-size: 50rpx;
font-weight: bold;
color: #fff;
font-family: OPPOSANS;
}
.all-title {
font-size: 26rpx;
font-weight: 500;
color: #fff;
}
.cicon-help-o {
color: #fff;
font-size: 28rpx;
}
}
}
// 筛选
.filter-box {
height: 114rpx;
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;
}
}
}
.list-box {
.list-item {
background: #fff;
border-bottom: 1rpx solid #dfdfdf;
padding: 30rpx;
.name {
font-size: 28rpx;
font-weight: 500;
color: rgba(102, 102, 102, 1);
line-height: 28rpx;
margin-bottom: 20rpx;
}
.time {
font-size: 24rpx;
font-weight: 500;
color: rgba(196, 196, 196, 1);
line-height: 24px;
}
.add {
font-size: 30rpx;
font-weight: 500;
color: #e6b873;
}
.minus {
font-size: 30rpx;
font-weight: 500;
color: $dark-3;
}
}
}
</style>