Merge branch 'dev' into 'master'

【修复】(订单提交):使用优惠券

See merge request jianweie/coreshoppro!37
This commit is contained in:
花城
2024-10-16 09:23:17 +00:00
19 changed files with 344 additions and 109 deletions

View File

@@ -1,4 +1,5 @@
.banner-box {
margin-bottom: 20rpx;
.img {
width: 100%;
}

View File

@@ -0,0 +1,53 @@
<template>
<view class="layout-page-box page-bg" :style="props.customStyle">
<coreshop-navbar :isBack="props.isBack" :bgColor="props.bgColor" :mode="props.mode" :title="props.title"
:titleColor="props.titleColor" :handleCustomRouteJump="props.handleCustomRouteJump"></coreshop-navbar>
<view class="layout-page-content"
:style="{ 'padding-top': `${props.isShowStatusBarHeight ? statusBarHeight : 0}px`, ...props.contentStyle }">
<slot></slot>
</view>
<template v-if="props.showLoginModalDom">
<coreshop-login-modal :show="_useLoginStore.showLoginModalTogglePop"
@handleChangePopup="handleChangeLoginPopup" @getUserInfo="_useLoginStore.getUserInfo()"></coreshop-login-modal>
</template>
</view>
</template>
<script setup lang="ts">
import { useSystemInfo } from '@/core/hooks';
import { useLoginStore } from '@/core/store';
// 获取自定义导航栏高度
const { statusBarHeight } = useSystemInfo();
const _useLoginStore = useLoginStore();
const props = withDefaults(defineProps<{
isBack : boolean,
bgColor : string,
titleColor : string,
mode : string,
title : string,
isShowStatusBarHeight : boolean;
customStyle : any;
contentStyle : any;
handleCustomRouteJump : () => void | null,
showLoginModalDom : boolean;
}>(), {
isBack: true,
bgColor: '#EEF3F7',
titleColor: '#000',
mode: 'center',
title: '',
isShowStatusBarHeight: true,
customStyle: {},
contentStyle: {},
handleCustomRouteJump: null,
showLoginModalDom: false
});
/** 打开获取关闭login弹框 */
const handleChangeLoginPopup = (isShow : boolean) => {
_useLoginStore.setShowLoginModalTogglePop(isShow);
}
</script>
<style lang="scss" scoped>
</style>

View File

@@ -1,36 +1,33 @@
<template>
<view class="layout-page-box page-bg" :style="props.customStyle">
<coreshop-navbar :isBack="props.isBack" :bgColor="props.bgColor" :mode="props.mode" :title="props.title"
:titleColor="props.titleColor" :handleCustomRouteJump="props.handleCustomRouteJump"></coreshop-navbar>
<view class="layout-page-content"
:style="{ 'padding-top': `${props.isShowStatusBarHeight ? statusBarHeight : 0}px`, ...props.contentStyle }">
<slot></slot>
</view>
<template v-if="props.showLoginModalDom">
<coreshop-login-modal :show="_useLoginStore.showLoginModalTogglePop"
@handleChangePopup="handleChangeLoginPopup" @getUserInfo="_useLoginStore.getUserInfo()"></coreshop-login-modal>
</template>
</view>
<template v-if="props.needSkeleton">
<uv-skeletons :skeleton="props.skeleton" :loading="props.skeletonLoading">
<coreshop-page-content v-bind="props">
<slot name="default" />
</coreshop-page-content>
</uv-skeletons>
</template>
<template v-else>
<coreshop-page-content v-bind="props">
<slot name="default" />
</coreshop-page-content>
</template>
</template>
<script setup lang="ts">
import { useSystemInfo } from '@/core/hooks';
import { useLoginStore } from '@/core/store';
// 获取自定义导航栏高度
const { statusBarHeight } = useSystemInfo();
const _useLoginStore = useLoginStore();
const props = withDefaults(defineProps<{
isBack : boolean,
bgColor : string,
titleColor : string,
mode : string,
title : string,
title : string;
isShowStatusBarHeight : boolean;
customStyle : any;
contentStyle : any;
handleCustomRouteJump : () => void | null,
showLoginModalDom : boolean;
needSkeleton : boolean;
skeleton ?: Array<object>;
skeletonLoading : boolean;
}>(), {
isBack: true,
bgColor: '#EEF3F7',
@@ -41,13 +38,16 @@
customStyle: {},
contentStyle: {},
handleCustomRouteJump: null,
showLoginModalDom: false
showLoginModalDom: false,
needSkeleton: false,
skeleton: [{
type: 'line',
num: 3,
gap: '20rpx',
style: ['width: 200rpx;marginBottom: 50rpx;', 'height: 100rpx;', 'width: 500rpx;'],
}],
skeletonLoading: false
});
/** 打开获取关闭login弹框 */
const handleChangeLoginPopup = (isShow : boolean) => {
_useLoginStore.setShowLoginModalTogglePop(isShow);
}
</script>
<style lang="scss" scoped>
</style>

View File

@@ -1,16 +1,16 @@
// 不同的环境变量配置
const development = {
requestBaseUrl: 'https://api.test.pro.coreshop.cn',
requestBaseUrl: 'https://api.pro.demo.corecms.cn',
appid: '',
}
const test = {
requestBaseUrl: 'https://api.test.pro.coreshop.cn',
requestBaseUrl: 'https://api.pro.demo.corecms.cn',
appid: '',
}
const production = {
requestBaseUrl: 'https://api.test.pro.coreshop.cn',
requestBaseUrl: 'https://api.pro.demo.corecms.cn',
appid: '',
}

View File

@@ -0,0 +1,2 @@
/** 监听首页onshow需要的常量字段 */
export const onHomePageShow = "onHomePageShow";

View File

@@ -1,6 +1,9 @@
/** 配置 */
export * from './config';
/** 首页 */
export * from './home';
/** 广告位 */
export * from './advertPosition';

View File

@@ -1,2 +1,3 @@
/** 系统配置 */
export * from './systemInfo';
export * from './use-loading';

View File

@@ -0,0 +1,11 @@
import { Ref } from "vue";
export function useLoadingFn<T extends Array<unknown>, R>(fn : (...args : T) => Promise<R>, loading : Ref<boolean>) {
function wrapper(this, ...args : T) {
loading.value = true;
return new Promise<R>((resolve, reject) =>
Promise.resolve(fn.apply(this, args)).then(resolve).catch(reject)
).finally(() => loading.value = false);
}
return wrapper;
}

View File

@@ -50,7 +50,7 @@
"quickapp" : {},
/* */
"mp-weixin" : {
"appid" : "wx3f290252193d7627",
"appid" : "wx6a73f5a508da7af2",
"setting" : {
"urlCheck" : false,
"checkSiteMap" : false,

View File

@@ -94,8 +94,8 @@
position: absolute;
bottom: 0;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
max-width: 750rpx;
.classify-left {
position: relative;
width: 200rpx;

View File

@@ -42,7 +42,7 @@
<scroll-view class="scroll-view" enable-flex
:style="{ 'height': `${props.height - state.bigClassifyH - state.rightTabH}px` }" :scroll-y="true"
@scrolltolower="handleScrolltolower">
<view class="advert-box radius-15 m-b-20">
<view class="advert-box radius-15">
<coreshop-advert :code="advertPosition.goodsClassifyBanner"></coreshop-advert>
</view>
<view class="data-box" v-if="state.goodsList.length > 0">

View File

@@ -1,7 +1,8 @@
.classify-box {
max-width: 750rpx;
position: relative;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
.classify-left {
position: relative;

View File

@@ -15,7 +15,7 @@
</view>
<scroll-view class="scroll-view" :style="{ 'height': `${props.height - 50}px` }" :scroll-y="true"
@scrolltolower="handleScrolltolower">
<view class="advert-box radius-15 m-b-20">
<view class="advert-box radius-15">
<coreshop-advert :code="advertPosition.goodsClassifyBanner"></coreshop-advert>
</view>
<view class="data-box" v-if="state.goodsList.length > 0">

View File

@@ -47,20 +47,19 @@
.classify-right {
position: relative;
flex: 1;
padding: 20rpx;
padding: 0 20rpx;
.scroll-view {
position: absolute;
left: 20rpx;
top: 20rpx;
top: 0;
right: 20rpx;
width: calc(100% - 40rpx);
height: calc(100% - 40rpx);
height: calc(100% - 20rpx);
}
.data-box {
display: flex;
align-items: center;
flex-wrap: wrap;
margin-top: 10rpx;
background-color: #f7f7f7;
border-radius: 10rpx;
padding: 20rpx;

View File

@@ -3,15 +3,18 @@
[props.data?.style?.align || 'left']: 0,
top: `${props.data?.style?.top || 20}%`
}">
<image class="img" :src="state.recordObj?.avatar"></image>
<image class="img" :src="state.recordObj?.avatar || handleStaticResources('/static/images/empty/noImage.jpg')">
</image>
<text class="tit">{{state.recordObj?.nickname}}{{state.recordObj?.createTime}}{{state.recordObj?.desc}}</text>
</view>
</template>
<script setup lang="ts">
import { reactive ,onMounted } from 'vue';
import { reactive, onMounted } from 'vue';
import { queryRecord } from '@/core/api';
import type { Response, RecordType } from '@/core/models'
import { handleStaticResources } from '@/core/utils'
import { onHide } from '@dcloudio/uni-app';
import { onHomePageShow } from '@/core/consts';
const props = withDefaults(defineProps<{
data : any,
@@ -27,13 +30,16 @@
times: "",
})
onMounted(() => {
query();
})
onHide(() => {
clearInterval(state.times);
})
// 获取日志
const getRecord = async () => {
const record : Response<RecordType> = await queryRecord({
type: 'home',
value: 0,
});
if (record.status && record.data) {
state.recordObj = record?.data;
};
}
const query = () => {
getRecord();
@@ -42,16 +48,17 @@
}, 3000)
}
// 获取日志
const getRecord = async () => {
const record : Response<RecordType> = await queryRecord({
type: 'home',
value: 0,
onMounted(() => {
query();
/** 监听页面onshow事件 */
uni.$on(onHomePageShow, () => {
query();
});
if(record.status && record.data){
state.recordObj = record?.data;
};
}
})
onHide(() => {
clearInterval(state.times);
})
</script>
<style lang="scss" scoped>
@import './home-record.scss';

View File

@@ -1,5 +1,5 @@
<template>
<template v-for="(item, index) in props.coreshopData" :key="index">
<template v-for="item in props.coreshopData" :key="item.id">
<!-- 轮播图 -->
<HomeSwiper v-if="item.widgetCode === WidgetCodeEnum.imgSlide" :data="item.parameters"></HomeSwiper>
<!--分类-->
@@ -36,7 +36,7 @@
<!--空格-->
<HomeBlank v-if="item.widgetCode === WidgetCodeEnum.blank" :data="item.parameters"></HomeBlank>
<!-- 下单记录 -->
<HomeRecord v-if="item.widgetCode === WidgetCodeEnum.record" :data="item.parameters"></HomeRecord>
<HomeRecord v-if="item.widgetCode === WidgetCodeEnum.record" :data="item.parameters"></HomeRecord>
<!--文章-->
<HomeArticle v-if="item.widgetCode === WidgetCodeEnum.article" :data="item.parameters"></HomeArticle>
<!--文章分类-->

View File

@@ -11,8 +11,7 @@
</uv-navbar>
<view class="content-box p-25" :style="{ 'padding-top': `${statusBarHeight + 10}px` }">
<CustomPage :coreshopData="state.coreshopData">
</CustomPage>
<CustomPage ref="homePage" :coreshopData="state.coreshopData"></CustomPage>
</view>
<!-- 备案信息 -->
@@ -25,15 +24,15 @@
</template>
<script setup lang="ts">
import { onPageScroll } from '@dcloudio/uni-app';
import { onMounted, reactive } from 'vue';
import { onMounted, reactive, ref } from 'vue';
import { onPageScroll, onShow } from '@dcloudio/uni-app';
import { queryPageConfig, queryUserInfo } from '@/core/api';
import type { Response, PageConfigType, PageConfigItemsType, UserInfoType } from '@/core/models';
import CustomPage from '@/pages/components/custom-page/index.vue';
import HomeAdpop from '@/pages/components/custom-page/components/home-adpop/home-adpop.vue';
import { handleStaticResources, handleRouteNavigateTo } from '@/core/utils';
import { useSystemInfo } from '@/core/hooks';
import { UserToken } from '@/core/consts';
import { useSystemInfo, useLoadingFn } from '@/core/hooks';
import { UserToken, onHomePageShow } from '@/core/consts';
import { useUserInfoStore, useShopConfigStore } from '@/core/store';
/** 获取项目配置 */
@@ -45,32 +44,39 @@
/** 获取 用户数据 */
const userInfoStore = useUserInfoStore();
const loading = ref(true);
const handleuQueryPageConfig = useLoadingFn(getPageConfig, loading);
const state = reactive<{
coreshopData : Array<PageConfigItemsType>;
isScrollTop : boolean;
showPage : boolean;
}>({
coreshopData: [],
isScrollTop: false,
showPage: false,
})
const getPageConfig = async () => {
onShow(() => {
/** 触发自定义onshow事件让后代组件监听页面是都进入 */
uni.$emit(onHomePageShow);
})
onMounted(() => {
handleuQueryPageConfig()
if (uni.getStorageSync(UserToken)) {
getUserInfo();
}
})
async function getPageConfig() {
const pageConfig : Response<PageConfigType> = await queryPageConfig({ code: 'mobile_home' });
state.coreshopData = pageConfig.data?.items;
}
onPageScroll((e : any) => {
if (e.scrollTop > 10) {
state.isScrollTop = true;
} else {
state.isScrollTop = false;
}
})
onMounted(() => {
getPageConfig();
if (uni.getStorageSync(UserToken)) {
getUserInfo();
}
state.isScrollTop = e.scrollTop > 10;
})
/** 获取用户信息 */

View File

@@ -230,7 +230,93 @@
}
}
}
.coupon-box{
margin-top: 30rpx;
border-radius: 15rpx;
overflow: hidden;
padding: 30rpx 25rpx;
background-color: #fff;
.title-box{
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20rpx;
.title{
font-size: 28rpx;
}
.desc{
font-size: 22rpx;
padding: 8rpx 15rpx;
border-radius: 20px;
background-color: #d33123;
color: #fff;
}
}
.list-box{
.coupon-scroll{
height: 160rpx;
position: relative;
white-space: nowrap;
width: 100%;
}
.coupon-item{
position: relative;
display: inline-block;
background:#f9f9f9;
padding: 20rpx;
margin-right: 20rpx;
border-radius: 15rpx;
max-width: 370rpx;
.select{
position: absolute;
right: 0;
top: 0;
width: 80rpx;
height: 80rpx;
&:before{
display: block;
content: "";
position: absolute;
right: 0;
top: 0;
border-top: 80rpx solid #eaeaea;
border-left: 80rpx solid transparent;
}
.icon{
position: absolute;
right: 2rpx;
top: 2rpx;
}
}
.on{
&:before{
border-top: 80rpx solid #d33123;
}
}
.name{
font-size: 27rpx;
margin-bottom: 15rpx;
max-width: 300rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.expression{
font-size: 24rpx;
color: #aaaaaa;
margin-bottom: 15rpx;
max-width: 300rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.time{
font-size: 22rpx;
color: #d33123;
}
}
}
}
.invoice-point {
margin-top: 30rpx;
border-radius: 15rpx;

View File

@@ -133,6 +133,29 @@
</view>
</view>
</view>
<!-- 优惠券 -->
<view class="coupon-box"
v-if="state.userCouponsList.length > 0 && state.orderType === PaymentTypeEnum.common && shopConfigStore.config.showCoupon === ShowCouponEnum.yes">
<view class="title-box">
<view class="title">优惠券</view>
<view class="desc" v-if="state.couponCodeList.length > 0" @click="handleCancelSelectCoupop">取消选择</view>
</view>
<view class="list-box">
<scroll-view class="coupon-scroll" scroll-x>
<view class="coupon-item" v-for="item in state.userCouponsList" :key="item.couponCode"
@click="handleSelectCoupop(item)">
<view class="name">{{item.couponName}}</view>
<view class="expression">{{item.expression2}}</view>
<view class="time">有效期{{ item.stime + ' 至 ' + item.etime }}</view>
<view :class="['select',{'on':item.checked}]">
<view class="icon">
<uv-icon name="checkbox-mark" color="#fff" size="22"></uv-icon>
</view>
</view>
</view>
</scroll-view>
</view>
</view>
<!-- 发票 -->
<view class="invoice-point">
<view class="point-box"
@@ -215,7 +238,7 @@
import {
PaymentTypeEnum, OrderDistributionEnum, ShowStoresSwitchEnum,
OpenPointEnum, PointExchangeModelEnum, ShowPointExchangePriceEnum,
PointsGiveModeEnum, ShowPointsGiveMsgEnum, OrderPayStatusEnum
PointsGiveModeEnum, ShowPointsGiveMsgEnum, OrderPayStatusEnum, ShowCouponEnum
} from '@/core/enum';
import { queryOrderDistributionModel, getSubscriptionTmplIds, queryCreateOrder, queryUserDefaultShip, queryDefaultStore, queryCartList, queryUserPoint, queryCartCoupon } from '@/core/api';
import type { Response, AddressType, ShopConfigStoreType, StoreListType } from '@/core/models';
@@ -255,7 +278,9 @@
mobile : string;
};
invoice : any; // 发票信息
store : StoreListType; // 默认门店
store : StoreListType; // 默认门店
userCouponsList : Array<any>; // 可用优惠券列表
couponCodeList : Array<string>; // 选中的优惠券code
}>({
orderType: PaymentTypeEnum.common,
tabList: [
@@ -278,6 +303,8 @@
},
invoice: {},
store: {},
userCouponsList: [],
couponCodeList: [],
});
@@ -364,7 +391,7 @@
});
/** 获取商品详情 */
const getCartList = async () => {
const getCartList = async (callBack ?: (data : any) => void) => {
uni.showLoading({
title: '加载中'
});
@@ -375,23 +402,28 @@
objectId: state.objectId,
point: state.isUsePoint ? state.userPointData?.availablePoint : 0,
type: state.orderType,
couponCode: "",
couponCode: state.couponCodeList.length > 0 ? state.couponCodeList.join(',') : '',
});
state.cartData = cartList?.data;
uni.hideLoading();
if (cartList.status) {
uni.hideLoading();
state.cartData = cartList?.data;
/** 判断是否有库存 */
let noStockGood = cartList?.data?.list?.filter((item : any) => !item.isSelect);;
if (noStockGood.length > 0 || cartList?.data?.list?.length == 0) {
uni.showModal({
title: '提示',
content: `所挑选的商品已售罄,请重新添加哦`,
showCancel: false
});
}
/** 判断是否开启积分抵扣 并且 没有勾选积分使用 */
if (shopConfigStore.config.pointSwitch === OpenPointEnum.yes && !state.isUsePoint) {
getUserPoint(cartList?.data?.amount)
/** 判断是否有库存 */
let noStockGood = cartList?.data?.list?.filter((item : any) => !item.isSelect);;
if (noStockGood.length > 0 || cartList?.data?.list?.length == 0) {
uni.showModal({
title: '提示',
content: `所挑选的商品已售罄,请重新添加哦`,
showCancel: false
});
}
/** 判断是否开启积分抵扣 并且 没有勾选积分使用 */
if (shopConfigStore.config.pointSwitch === OpenPointEnum.yes && !state.isUsePoint) {
getUserPoint(cartList?.data?.amount)
}
} else {
uni.hideLoading();
callBack(cartList);
}
}
@@ -438,10 +470,47 @@
/** 获取用户的可用优惠券信息 */
const getUserCounpons = async () => {
const cartCoupon : Response<AddressType> = await queryCartCoupon({
const cartCoupon : Response<any> = await queryCartCoupon({
display: 'no_used',
ids: state.params.ids
});
if (cartCoupon.status && cartCoupon.data?.list) {
let nowTime = Math.round(new Date().getTime() / 1000).toString();
state.userCouponsList = cartCoupon.data.list.map((item : any) => {
item['checked'] = false;
item['disabled'] = item.startTime > nowTime ? true : false;
return item;
})
}
}
/** 取消使用优惠券 */
const handleCancelSelectCoupop = () => {
state.userCouponsList.forEach((item : any) => {
item.checked = false;
})
state.couponCodeList = [];
getCartList();
}
/** 选择使用优惠券 */
const handleSelectCoupop = (item : any) => {
if(!item.disabled){
handleShowToast('请在有效时间内使用');
return ;
}
if(state.couponCodeList.includes(item.couponCode)){
item.checked = false;
state.couponCodeList.splice(state.couponCodeList.findIndex((cell) => cell == item.couponCode), 1);
}else{
item.checked = true;
state.couponCodeList.push(item.couponCode);
}
getCartList((data : any) => {
handleShowToast(data.msg);
state.couponCodeList.splice(state.couponCodeList.findIndex((cell) => cell == item.couponCode), 1);
item.checked = false;
});
}
/** 获取默认店铺 */
@@ -450,11 +519,7 @@
if (defaultStore.status) {
state.store = defaultStore.data;
} else {
uni.showToast({
title: '商家未配置默认自提店铺!',
duration: 2000,
icon: "none"
});
handleShowToast('商家未配置默认自提店铺!');
}
}
@@ -525,7 +590,7 @@
...delivery,
cartIds: state.params.ids,
memo: state.leaveMsg,
couponCode: state.params.couponCode,
couponCode: state.couponCodeList.length > 0 ? state.couponCodeList.join(',') : '',
point: state.params.point,
receiptType: state.tabSelectType,
objectId: state.objectId,