Initial commit: 积分兑换电商平台多商户版 MER-2.2

Made-with: Cursor
This commit is contained in:
apple
2026-03-08 20:07:52 +08:00
commit de02c8a3e1
4954 changed files with 703009 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
<template>
<view class="bottom_close" :style="topStyle" @click="close"><view class="iconfont icon-ic_close f-s-24"></view></view>
</template>
<script>
export default {
name: "close-icon",
props:{
topStyle:{
type: String,
default: ''
}
},
methods: {
close(){
this.$emit('handle-close')
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,246 @@
<template>
<view class="easy-loadimage" :style="[boxStyle]" :id="uid">
<image class="origin-img" :style="[imageRadius]" :src="imageSrc" :mode="mode" v-if="loadImg&&!isLoadError"
v-show="showImg" :class="{'no-transition':!openTransition,'show-transition':showTransition&&openTransition}"
@load="handleImgLoad" @error="handleImgError">
</image>
<view class="loadfail-img" v-else-if="isLoadError"
:style="{'background-image': `url(${urlDomain}crmebimage/presets/loadfail.png) no-repeat center`}"></view>
<view :class="['loading-img','spin-circle',loadingMode,mode]" v-show="!showImg&&!isLoadError"></view>
</view>
</template>
<script>
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
import {
throttle
} from '@/utils/validate.js'
// 生成全局唯一id
function generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
let r = Math.random() * 16 | 0,
v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
})
}
export default {
name: 'easyLoadimage',
props: {
imageSrc: {
type: String || null,
default () {
return '';
}
},
mode: {
type: String,
default: "aspectFill"
},
loadingMode: {
type: String,
default: 'looming-gray'
},
openTransition: {
type: Boolean,
default: true,
},
viewHeight: {
type: Number,
default () {
return uni.getSystemInfoSync().windowHeight;
}
},
width: {
type: String,
default: ''
},
height: {
type: String,
default: ''
},
radius: {
type: String,
default: ''
},
},
data() {
const that = this;
return {
urlDomain: this.$Cache.get("imgHost"),
uid: 'uid-' + generateUUID(),
loadImg: false,
showImg: false,
isLoadError: false,
borderLoaded: 0,
showTransition: false,
scrollFn: throttle(function() {
// 加载img时才执行滚动监听判断是否可加载
if (that.loadImg || that.isLoadError) return;
const id = that.uid
const query = uni.createSelectorQuery().in(that);
query.select('#' + id).boundingClientRect(data => {
if (!data) return;
if (data.top - that.viewHeight < 0) {
that.loadImg = !!that.imageSrc;
that.isLoadError = !that.loadImg;
}
}).exec()
}, 200)
}
},
computed: {
boxStyle() {
return {
width: this.width,
height: this.height,
borderRadius: this.radius
}
},
imageRadius() {
if (this.radius) {
return {
'border-radius': this.radius
}
}
}
},
methods: {
init() {
this.$nextTick(this.onScroll)
},
handleBorderLoad() {
this.borderLoaded = 1;
},
handleBorderError() {
this.borderLoaded = 2;
},
handleImgLoad(e) {
this.showImg = true;
setTimeout(() => {
this.showTransition = true
}, 50)
},
handleImgError(e) {
this.isLoadError = true;
},
onScroll() {
this.scrollFn();
},
},
mounted() {
this.init()
uni.$on('scroll', this.scrollFn);
this.onScroll()
},
beforeDestroy() {
uni.$off('scroll', this.scrollFn);
}
}
</script>
<style scoped lang="scss">
.easy-loadimage {
width: 100%;
height: 100%;
overflow: hidden;
margin: auto;
display: flex;
justify-content: center;
}
.widthFix {
min-height: 172px;
}
/* 官方优化图片tips */
image {
will-change: transform;
overflow: hidden;
object-fit: cover;
}
/* 渐变过渡效果处理 */
image.origin-img {
width: 100%;
height: 100%;
opacity: 0.3;
}
image.origin-img.show-transition {
transition: opacity .5s;
opacity: 1;
}
image.origin-img.no-transition {
opacity: 1;
}
/* 加载失败、加载中的占位图样式控制 */
.loadfail-img {
height: 100%;
background-size: 50%;
}
.loading-img {
width: 100%;
height: 100%;
}
/* 动态灰色若隐若现 */
.looming-gray {
animation: looming-gray 1s infinite linear;
background-color: #e3e3e3;
}
@keyframes looming-gray {
0% {
background-color: #e3e3e3aa;
}
50% {
background-color: #e3e3e3;
}
100% {
background-color: #e3e3e3aa;
}
}
/* 骨架屏1 */
.skeleton-1 {
background-color: #e3e3e3;
background-image: linear-gradient(100deg, rgba(255, 255, 255, 0), rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0) 80%);
background-size: 100rpx 100%;
background-repeat: repeat-y;
background-position: 0 0;
animation: skeleton-1 .6s infinite;
}
@keyframes skeleton-1 {
to {
background-position: 200% 0;
}
}
/* 骨架屏2 */
.skeleton-2 {
background-image: linear-gradient(-90deg, #fefefe 0%, #e6e6e6 50%, #fefefe 100%);
background-size: 400% 400%;
background-position: 0 0;
animation: skeleton-2 1.2s ease-in-out infinite;
}
@keyframes skeleton-2 {
to {
background-position: -135% 0;
}
}
</style>

View File

@@ -0,0 +1,173 @@
<template>
<view class='recommend'>
<block v-if="tempArr.length">
<title-box v-if="isShowTitle" title="热门推荐"></title-box>
<view class='recommendList borderPad' :class="isShowTitle?'':'mt30'">
<WaterfallsFlow :wfList='tempArr' :type="1" :isStore="1">
<template slot-scope="{item}">{{item.name}}
<WaterfallsFlowItem :item="item" :type="1" :isStore="1"/>
</template>
</WaterfallsFlow>
</view>
<view class='loadingicon acea-row row-center-wrapper' :hidden='loading==false'>
<text class='loading iconfont icon-jiazai'></text>
</view>
<view class="mores-txt flex" v-if="goodScroll">
<text>到底了~~</text>
</view>
</block>
</view>
</template>
<script>
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
import {
mapGetters
} from "vuex";
import animationType from '@/utils/animationType.js'
import {
getProductHot
} from '@/api/product.js';
import WaterfallsFlow from '@/components/WaterfallsFlow/WaterfallsFlow.vue';
import WaterfallsFlowItem from '@/components/WaterfallsFlowItem/WaterfallsFlowItem.vue';
import TitleBox from '@/components/titleBox/index.vue';
let app = getApp();
export default {
name: 'recommend',
computed: mapGetters(['uid']),
components: {
WaterfallsFlow,
TitleBox
},
props: {
categoryId: {
type: Number,
default: function() {
return 0;
}
},
//是否显示头部
isShowTitle: {
type: Boolean,
default: function() {
return true;
}
},
//是否使用本页面的请求数据
isDefault: {
type: Boolean,
default: function() {
return true;
}
},
//使用的页面中调用数据传来的商品列表isDefault为false时使用
recommendList: {
type: Array,
default: function() {
return [];
}
}
},
data() {
return {
urlDomain: this.$Cache.get("imgHost"),
theme: app.globalData.theme,
goodScroll: false,
params: { //精品推荐分页
page: 1,
limit: 10,
cid: 0
},
loading: false,
tempArr: []
};
},
watch: {
categoryId: function(val) { //监听props中的属性
if (!this.isDefault) {
this.params.page = 1;
this.tempArr = [];
this.goodScroll = false;
this.get_host_product()
}
}
},
mounted() {
if (this.isDefault) {
this.params.page = 1;
this.goodScroll = false;
this.tempArr = [];
this.get_host_product()
}else{
this.tempArr = this.recommendList
};
},
methods: {
/**
* 获取我的推荐
*/
get_host_product: function() {
if (this.goodScroll) return;
this.loading = true
this.params.cid = this.categoryId;
getProductHot(
this.params
).then((res) => {
this.$set(this.params, 'page', this.params.page + 1);
this.goodScroll = this.params.page > res.data.totalPage;
this.tempArr = this.tempArr.concat(res.data.list || []);
this.$emit('getRecommendLength', this.tempArr.length);
this.loading = false
}).catch(err => {
this.loading = false
});
}
},
}
</script>
<style scoped lang="scss">
.mores-txt {
width: 100%;
align-items: center;
justify-content: center;
height: 70rpx;
color: #999;
font-size: 24rpx;
.iconfont {
margin-top: 2rpx;
font-size: 20rpx;
}
}
.recommend {
.title {
height: 120rpx;
line-height: 120rpx;
font-size: 32rpx;
color: #333333;
.iconfont {
font-size: 170rpx;
color: #454545;
}
}
.name {
margin: 0 28rpx;
}
}
.recommendList{
background-color: #f5f5f5;
}
</style>

View File

@@ -0,0 +1,199 @@
<template>
<view v-if="show"
:style="{width: systemInfo.width + 'px', height: systemInfo.height + 'px', backgroundColor: bgcolor, position: 'absolute', left: 0, top: 0, zIndex: 9998}">
<view v-for="(item,rect_idx) in skeletonRectLists" :key="rect_idx + 'rect'"
:class="[loading == 'chiaroscuro' ? 'chiaroscuro' : '']"
:style="{width: item.width + 'px', height: item.height + 'px', backgroundColor: 'rgb(194, 207, 214,.3)', position: 'absolute', left: item.left + 'px', top: item.top + 'px'}">
</view>
<view v-for="(item,circle_idx) in skeletonCircleLists" :key="circle_idx + 'circle'"
:class="loading == 'chiaroscuro' ? 'chiaroscuro' : ''"
:style="{width: item.width + 'px', height: item.height + 'px', backgroundColor: 'rgb(194, 207, 214,.3)', borderRadius: item.width + 'px', position: 'absolute', left: item.left + 'px', top: item.top + 'px'}">
</view>
<view class="spinbox" v-if="loading == 'spin'">
<view class="spin"></view>
</view>
</view>
</template>
<script>
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
export default {
name: "skeleton",
props: {
bgcolor: {
type: String,
value: '#FFF'
},
selector: {
type: String,
value: 'skeleton'
},
loading: {
type: String,
value: 'spin'
},
show: {
type: Boolean,
value: false
},
isNodes: {
type: Number,
value: false
} //控制什么时候开始抓取元素节点,只要数值改变就重新抓取
},
data() {
return {
loadingAni: ['spin', 'chiaroscuro'],
systemInfo: {},
skeletonRectLists: [],
skeletonCircleLists: []
}
},
watch: {
isNodes(val) {
this.readyAction();
}
},
mounted() {
this.attachedAction();
},
methods: {
attachedAction: function() {
//默认的首屏宽高,防止内容闪现
const systemInfo = uni.getSystemInfoSync();
this.systemInfo = {
width: systemInfo.windowWidth,
height: systemInfo.windowHeight
};
this.loading = this.loadingAni.includes(this.loading) ? this.loading : 'spin';
},
readyAction: function() {
const that = this;
//绘制背景
uni.createSelectorQuery().selectAll(`.${this.selector}`).boundingClientRect().exec(function(res) {
if(res[0].length>0)
that.systemInfo.height = res[0][0].height + res[0][0].top;
});
//绘制矩形
this.rectHandle();
//绘制圆形
this.radiusHandle();
},
rectHandle: function() {
const that = this;
//绘制不带样式的节点
uni.createSelectorQuery().selectAll(`.${this.selector}-rect`).boundingClientRect().exec(function(res) {
that.skeletonRectLists = res[0];
});
},
radiusHandle() {
const that = this;
uni.createSelectorQuery().selectAll(`.${this.selector}-radius`).boundingClientRect().exec(function(res) {
that.skeletonCircleLists = res[0];
});
}
}
}
</script>
<style>
.spinbox {
position: fixed;
display: flex;
justify-content: center;
align-items: center;
height: 100%;
width: 100%;
z-index: 9999
}
.spin {
display: inline-block;
width: 64rpx;
height: 64rpx;
}
.spin:after {
content: " ";
display: block;
width: 46rpx;
height: 46rpx;
margin: 1rpx;
border-radius: 50%;
border: 5rpx solid #409eff;
border-color: #409eff transparent #409eff transparent;
animation: spin 1.2s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.chiaroscuro {
width: 100%;
height: 100%;
background: rgb(194, 207, 214);
animation-duration: 2s;
animation-name: blink;
animation-iteration-count: infinite;
}
@keyframes blink {
0% {
opacity: .4;
}
50% {
opacity: 1;
}
100% {
opacity: .4;
}
}
@keyframes flush {
0% {
left: -100%;
}
50% {
left: 0;
}
100% {
left: 100%;
}
}
.shine {
animation: flush 2s linear infinite;
position: absolute;
top: 0;
bottom: 0;
width: 100%;
background: linear-gradient(to left,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, .85) 50%,
rgba(255, 255, 255, 0) 100%)
}
</style>

View File

@@ -0,0 +1,108 @@
<template>
<view @touchmove.stop.prevent>
<view class="tui-popup-class tui-bottom-popup" :class="{ 'tui-popup-show': show, 'tui-popup-radius': radius }" :style="{ background: backgroundColor, height: height ? height + 'rpx' : 'auto', zIndex: zIndex,transform:`translate3d(0, ${show?translateY:'100%'}, 0)`}">
<slot></slot>
</view>
<view class="tui-popup-mask" :class="[show ? 'tui-mask-show' : '']" :style="{ zIndex: maskZIndex }" v-if="mask" @tap="handleClose"></view>
</view>
</template>
<script>
export default {
name: 'tuiBottomPopup',
emits: ['close'],
props: {
//是否需要mask
mask: {
type: Boolean,
default: true
},
//控制显示
show: {
type: Boolean,
default: false
},
//背景颜色
backgroundColor: {
type: String,
default: '#fff'
},
//高度 rpx
height: {
type: Number,
default: 0
},
//设置圆角
radius: {
type: Boolean,
default: true
},
zIndex: {
type: [Number, String],
default: 997
},
maskZIndex: {
type: [Number, String],
default: 996
},
//弹层显示时,垂直方向移动的距离
translateY: {
type: String,
default: '0'
}
},
methods: {
handleClose() {
if (!this.show) {
return;
}
this.$emit('close', {});
}
}
};
</script>
<style scoped lang="scss">
.tui-bottom-popup {
width: 750rpx;
position: fixed;
left: 0;
right: 0;
bottom: 0;
opacity: 0;
transform: translate3d(0, 100%, 0);
transform-origin: center;
transition: all 0.3s ease-in-out;
min-height: 20rpx;
padding-bottom: env(safe-area-inset-bottom);
}
.tui-popup-radius {
border-top-left-radius: 40rpx;
border-top-right-radius: 40rpx;
overflow: hidden;
}
.tui-popup-show {
opacity: 1;
/* transform: translate3d(0, 0, 0); */
}
.tui-popup-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.6);
transition: all 0.3s ease-in-out;
opacity: 0;
visibility: hidden;
}
.tui-mask-show {
opacity: 1;
visibility: visible;
}
</style>

View File

@@ -0,0 +1,248 @@
<template>
<view class="tui-skeleton-cmomon tui-skeleton-box" :style="{width: winWidth+'px', height:winHeight+'px', backgroundColor:backgroundColor}">
<view class="tui-skeleton-cmomon" v-for="(item,index) in skeletonElements" :key="index" :style="{width: item.width+'px', height:item.height+'px', left: item.left+'px', top: item.top+'px',backgroundColor: skeletonBgColor,borderRadius:getRadius(item.skeletonType,borderRadius)}"></view>
<view class="tui-loading" :class="[getLoadingType(loadingType)]" v-if="isLoading"></view>
</view>
</template>
<script>
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
export default {
name: "tuiSkeleton",
props: {
//选择器(外层容器)
selector: {
type: String,
default: "tui-skeleton"
},
//外层容器背景颜色
backgroundColor: {
type: String,
default: "#fff"
},
//骨架元素背景颜色
skeletonBgColor: {
type: String,
default: "#e9e9e9"
},
//骨架元素类型:矩形,圆形,带圆角矩形["rect","circular","fillet"]
//默认所有,根据页面情况进行传值
//页面对应元素class为tui-skeleton-recttui-skeleton-circulartui-skeleton-fillet
//如果传入的值不在下列数组中则为自定义class值默认按矩形渲染
skeletonType: {
type: Array,
default () {
return ["rect", "circular", "fillet"]
}
},
//圆角值skeletonType=fillet时生效
borderRadius: {
type: String,
default: "16rpx"
},
//骨架屏预生成数据:提前生成好的数据,当传入该属性值时,则不会再次查找子节点信息
preloadData: {
type: Array,
default () {
return []
}
},
//是否需要loading
isLoading: {
type: Boolean,
default: false
},
//loading类型[1-10]
loadingType: {
type: Number,
default: 1
}
},
created() {
const res = uni.getSystemInfoSync();
this.winWidth = res.windowWidth;
this.winHeight = res.windowHeight;
//如果有预生成数据,则直接使用
this.isPreload(true)
},
mounted() {
this.$nextTick(() => {
this.nodesRef(`.${this.selector}`).then((res) => {
if(res && res[0]){
this.winHeight = res[0].height + Math.abs(res[0].top)
}
});
!this.isPreload() && this.selectorQuery()
})
},
data() {
return {
winWidth: 375,
winHeight: 800,
skeletonElements: []
};
},
methods: {
getLoadingType: function(type) {
let value = 1
if (type && type > 0 && type < 11) {
value = type
}
return 'tui-loading-' + value
},
getRadius: function(type, val) {
let radius = "0"
if (type == "circular") {
radius = "50%"
} else if (type == "fillet") {
radius = val
}
return radius;
},
isPreload(init) {
let preloadData = this.preloadData || []
if (preloadData.length) {
init && (this.skeletonElements = preloadData)
return true
}
return false
},
async selectorQuery() {
let skeletonType = this.skeletonType || []
let nodes = []
for (let item of skeletonType) {
let className = '';
// #ifndef MP-WEIXIN
className = `.${item}`;
if (~'rect_circular_fillet'.indexOf(item)) {
className = `.${this.selector}-${item}`;
}
// #endif
// #ifdef MP-WEIXIN
className = `.${this.selector} >>> .${item}`;
if (~'rect_circular_fillet'.indexOf(item)) {
className = `.${this.selector} >>> .${this.selector}-${item}`;
}
// #endif
await this.nodesRef(className).then((res) => {
res.map(d => {
d.skeletonType = item
})
nodes = nodes.concat(res)
})
}
this.skeletonElements = nodes
},
async nodesRef(className) {
return await new Promise((resolve, reject) => {
uni.createSelectorQuery().selectAll(className).boundingClientRect((res) => {
if (res) {
resolve(res);
} else {
reject(res)
}
}).exec();
})
}
}
}
</script>
<style scoped>
.tui-skeleton-cmomon {
position: absolute;
z-index: 99999;
}
.tui-skeleton-box {
left: 0;
top: 0;
}
.tui-loading {
display: inline-block;
vertical-align: middle;
width: 40rpx;
height: 40rpx;
background: 0 0;
border-radius: 50%;
border: 2px solid;
animation: tui-rotate 0.7s linear infinite;
position: fixed;
z-index: 999999;
left: 50%;
top: 50%;
margin-left: -20rpx;
margin-top: -20rpx;
}
.tui-loading-1 {
border-color: #e5e5e5 #e5e5e5 #e5e5e5 #5677fc;
}
.tui-loading-2 {
border-color: #e5e5e5 #e5e5e5 #e5e5e5 #8f8d8e;
}
.tui-loading-3 {
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) #fff;
}
.tui-loading-4 {
border-color: #e5e5e5 #e5e5e5 #e5e5e5 #35b06a;
}
.tui-loading-5 {
border-color: #e5e5e5 #e5e5e5 #e5e5e5 #fc872d;
}
.tui-loading-6 {
border-color: #e5e5e5 #e5e5e5 #e5e5e5 #eb0909;
}
.tui-loading-7 {
border-color: #5677fc transparent #5677fc transparent;
}
.tui-loading-8 {
border-color: #35b06a transparent #35b06a transparent;
}
.tui-loading-9 {
border-color: #fc872d transparent #fc872d transparent;
}
.tui-loading-10 {
border-color: #eb0909 transparent #eb0909 transparent;
}
@-webkit-keyframes tui-rotate {
0% {
transform: rotate(0);
}
100% {
transform: rotate(360deg);
}
}
@keyframes tui-rotate {
0% {
transform: rotate(0);
}
100% {
transform: rotate(360deg);
}
}
</style>