feat(uniapp): 首页楼层模板支持装修样式字段,修复 tpl_notice 样式缺少闭合括号

This commit is contained in:
pikachu1995@126.com
2026-10-20 14:17:21 +08:00
parent a41936dc00
commit 986625c516
26 changed files with 3915 additions and 774 deletions

View File

@@ -1,5 +1,5 @@
<template> <template>
<view class="home-page" :style="themeStyle"> <view class="home-page" :style="[themeStyle, pageBgStyle]">
<view <view
v-for="(item, index) in pageData.list" v-for="(item, index) in pageData.list"
:key="`${item.type}-${index}`" :key="`${item.type}-${index}`"
@@ -105,6 +105,29 @@ const enableLoad = ref(false)
const loading = ref(false) const loading = ref(false)
const searchBarWidth = ref(0) const searchBarWidth = ref(0)
// 页面设置-样式:画布背景(背景色 / 背景图 + 填充方式)
const pageBgStyle = computed(() => {
const data: any = pageData.value
if (data?.bgType === 'image' && data?.bgImage) {
const size =
data.bgFill === 'contain'
? 'contain'
: data.bgFill === 'cover'
? 'cover'
: '100% 100%'
return {
backgroundImage: `url(${data.bgImage})`,
backgroundSize: size,
backgroundRepeat: 'no-repeat',
backgroundColor: data.bgColor || '#f9f9f9',
}
}
if (data?.bgColor) {
return { backgroundColor: data.bgColor }
}
return {}
})
const searchBarStyle = computed(() => { const searchBarStyle = computed(() => {
if (searchBarWidth.value) { if (searchBarWidth.value) {
return { width: `${searchBarWidth.value}px` } return { width: `${searchBarWidth.value}px` }
@@ -148,6 +171,10 @@ function init() {
pageData.value = Array.isArray(result?.list) pageData.value = Array.isArray(result?.list)
? { ...result, list: result.list.filter((i: any) => !i?.hidden) } ? { ...result, list: result.list.filter((i: any) => !i?.hidden) }
: { list: [] } : { list: [] }
// 页面设置-内容:页面标题同步到导航栏
if (result?.title) {
uni.setNavigationBarTitle({ title: result.title })
}
const lastItem = pageData.value.list[pageData.value.list.length - 1] const lastItem = pageData.value.list[pageData.value.list.length - 1]
enableLoad.value = enableLoad.value =
lastItem?.type === 'goods' || lastItem?.model === 'goods' lastItem?.type === 'goods' || lastItem?.model === 'goods'

View File

@@ -243,3 +243,35 @@ function seacnCode() {
}, },
}); });
} }
/** 广告魔方各布局共用的样式字段(装修面板配置;老数据无字段时返回空对象,保持原样) */
export function cubeWrapperStyle(res) {
if (!res || res.paddingTop === undefined) return {};
const r = (v) => `${Number(v) || 0}rpx`;
const bg =
!res.bgColor2 || res.bgColor1 === res.bgColor2
? res.bgColor1
: `linear-gradient(180deg, ${res.bgColor1}, ${res.bgColor2})`;
return {
padding: `${r(res.paddingTop)} ${r(res.paddingX)} ${r(res.paddingBottom)}`,
background: bg,
borderRadius: r(res.borderRadius),
marginBottom: r(res.pageGap),
overflow: Number(res.borderRadius) > 0 ? "hidden" : "visible",
};
}
/** 内容圆角(作用于单张图片容器) */
export function cubeImgStyle(res) {
if (!res || res.imgRadius === undefined) return {};
return {
borderRadius: `${Number(res.imgRadius) || 0}rpx`,
overflow: "hidden",
};
}
/** 内容间距(作用于图片布局容器,flex gap) */
export function cubeGapStyle(res) {
if (!res || res.contentGap === undefined) return {};
return { gap: `${Number(res.contentGap) || 0}rpx` };
}

View File

@@ -1,5 +1,5 @@
<template> <template>
<view class="tpl-article" :style="wrapperStyle" v-if="articleList.length"> <view class="tpl-article" :class="{ styled: styled }" :style="wrapperStyle" v-if="articleList.length">
<view class="article-title" v-if="title">{{ title }}</view> <view class="article-title" v-if="title">{{ title }}</view>
<view <view
class="article-item" class="article-item"
@@ -8,7 +8,7 @@
@click="goDetail(item)" @click="goDetail(item)"
> >
<view class="article-text"> <view class="article-text">
<view class="article-name">{{ item.title }}</view> <view class="article-name" :style="nameStyle">{{ item.title }}</view>
</view> </view>
</view> </view>
</view> </view>
@@ -26,13 +26,38 @@ const title = computed(() => props.res?.list?.[0]?.title || "");
const count = computed(() => props.res?.list?.[0]?.count || 3); const count = computed(() => props.res?.list?.[0]?.count || 3);
const categoryId = computed(() => props.res?.list?.[0]?.categoryId || ""); const categoryId = computed(() => props.res?.list?.[0]?.categoryId || "");
const wrapperStyle = computed(() => ({ // 面板保存过样式字段才启用自定义样式,老数据保持原样
background: props.res?.bgColor || "#FFFFFF", const styled = computed(() => props.res && props.res.paddingTop !== undefined);
padding: `20rpx ${props.res?.paddingX ?? 0}rpx`, const wrapperStyle = computed(() => {
marginBottom: `${props.res?.pageGap ?? 10}rpx`, const res = props.res || {};
borderRadius: `${props.res?.borderRadius ?? 0}rpx`, if (styled.value) {
overflow: "hidden", const bg =
})); !res.bgColor2 || res.bgColor1 === res.bgColor2
? res.bgColor1
: `linear-gradient(180deg, ${res.bgColor1}, ${res.bgColor2})`;
const radius = Number(res.borderRadius) || 0;
return {
background: bg,
padding: `${Number(res.paddingTop) || 0}rpx ${Number(res.paddingX) || 0}rpx ${Number(res.paddingBottom) || 0}rpx`,
marginBottom: `${Number(res.pageGap) || 0}rpx`,
borderRadius: `${radius}rpx`,
overflow: radius > 0 ? "hidden" : "visible",
display: "flex",
flexDirection: "column",
rowGap: `${Number(res.contentGap) || 0}rpx`,
};
}
return {
background: res.bgColor || "#FFFFFF",
padding: `20rpx ${res.paddingX ?? 0}rpx`,
marginBottom: `${res.pageGap ?? 10}rpx`,
borderRadius: `${res.borderRadius ?? 0}rpx`,
overflow: "hidden",
};
});
const nameStyle = computed(() =>
styled.value && props.res.titleColor ? { color: props.res.titleColor } : {}
);
onMounted(() => { onMounted(() => {
const params: any = { pageNumber: 1, pageSize: count.value }; const params: any = { pageNumber: 1, pageSize: count.value };
@@ -61,6 +86,10 @@ function goDetail(item: any) {
padding: 16rpx 24rpx; padding: 16rpx 24rpx;
border-top: 2rpx solid #f5f5f5; border-top: 2rpx solid #f5f5f5;
} }
.tpl-article.styled .article-item {
border-top: none;
padding: 0;
}
.article-text { .article-text {
flex: 1; flex: 1;
min-width: 0; min-width: 0;

View File

@@ -1,55 +1,190 @@
<template> <template>
<view class="layout"> <view class="layout" :style="wrapperStyle">
<view class="banner-box"> <view class="banner-box" :style="cardMode ? {} : boxStyle">
<u-swiper <u-swiper
v-if="res && res.list && res.list.length" v-if="!cardMode && res && res.list && res.list.length"
class="home-banner-swiper" class="home-banner-swiper"
:list="res.list" :list="res.list"
keyName="img" keyName="img"
height="350rpx" height="350rpx"
imgMode="scaleToFill" imgMode="scaleToFill"
interval="5000" interval="5000"
duration="500" duration="500"
circular circular
indicator :indicator="!customIndicator"
@click="clickSwiper" @change="onChange"
> @click="clickSwiper"
<template #loading><u-loading-icon></u-loading-icon></template> >
</u-swiper> <template #loading><u-loading-icon></u-loading-icon></template>
</view> </u-swiper>
</view> <swiper
</template> v-if="cardMode && res && res.list && res.list.length"
class="card-swiper"
<script setup lang="ts"> autoplay
import { modelNavigateTo } from "./tpl"; :interval="5000"
:duration="500"
const props = defineProps<{ res: any }>(); circular
previous-margin="68rpx"
function clickSwiper(index: number) { next-margin="68rpx"
modelNavigateTo(props.res.list[index]); @change="onChange"
} >
</script> <swiper-item v-for="(item, i) in res.list" :key="i">
<style lang="scss" scoped> <view class="card-item" :class="{ active: i === current }" @click="clickSwiper(i)">
@import "./tpl.scss"; <image class="card-img" :style="boxStyle" :src="item.img" mode="scaleToFill" />
</view>
.banner-box { </swiper-item>
width: 100%; </swiper>
overflow: hidden; <view
} v-if="customIndicator && res.indicatorStyle !== 'none' && res.list.length > 1"
class="indicator"
:deep(.home-banner-swiper), :class="'pos-' + (res.indicatorPosition || 'center')"
:deep(.u-swiper), >
:deep(.u-swiper__wrapper) { <view
width: 100%; v-for="(item, i) in res.list"
} :key="i"
class="dot"
:deep(.u-swiper__wrapper__item), :class="[res.indicatorStyle || 'round', { active: i === current }]"
:deep(.u-swiper__wrapper__item__wrapper) { :style="i === current ? { background: indicatorColor } : {}"
width: 100%; ></view>
} </view>
</view>
:deep(.u-swiper__wrapper__item__wrapper__image) { </view>
width: 100% !important; </template>
display: block;
} <script setup lang="ts">
</style> import { computed, ref } from "vue";
import { modelNavigateTo } from "./tpl";
const props = defineProps<{ res: any }>();
const current = ref(0);
// 装修面板保存过样式字段才启用自定义样式/指示器,老数据保持原样
const customIndicator = computed(() => props.res && props.res.indicatorStyle !== undefined);
const cardMode = computed(() => props.res && props.res.swiperStyle === "card");
const indicatorColor = computed(() =>
props.res.indicatorColorMode === "custom" && props.res.indicatorColor
? props.res.indicatorColor
: "var(--theme-primary, #ff3c2a)"
);
const toRpx = (val: any) => `${Number(val) || 0}rpx`;
const wrapperStyle = computed(() => {
const res = props.res || {};
if (res.paddingTop === undefined) return {};
const bg =
!res.bgColor2 || res.bgColor1 === res.bgColor2
? res.bgColor1
: `linear-gradient(180deg, ${res.bgColor1}, ${res.bgColor2})`;
return {
padding: `${toRpx(res.paddingTop)} ${toRpx(res.paddingX)} ${toRpx(res.paddingBottom)}`,
background: bg,
borderRadius: toRpx(res.borderRadius),
marginBottom: toRpx(res.pageGap),
};
});
const boxStyle = computed(() => {
const res = props.res || {};
if (res.imgRadius === undefined) return {};
return { borderRadius: toRpx(res.imgRadius) };
});
function onChange(e: any) {
current.value = typeof e === "number" ? e : e.current;
}
function clickSwiper(index: number) {
modelNavigateTo(props.res.list[index]);
}
</script>
<style lang="scss" scoped>
@import "./tpl.scss";
.banner-box {
width: 100%;
overflow: hidden;
position: relative;
}
.card-swiper {
width: 100%;
height: 350rpx;
}
.card-item {
width: 100%;
height: 100%;
padding: 0 8rpx;
box-sizing: border-box;
transform: scale(0.92);
transition: transform 0.4s ease;
}
.card-item.active {
transform: scale(1);
}
.card-img {
width: 100%;
height: 100%;
display: block;
overflow: hidden;
}
.indicator {
position: absolute;
bottom: 16rpx;
display: flex;
align-items: center;
z-index: 2;
&.pos-left {
left: 24rpx;
}
&.pos-center {
left: 50%;
transform: translateX(-50%);
}
&.pos-right {
right: 24rpx;
}
.dot {
background: rgba(255, 255, 255, 0.7);
transition: background 0.3s ease;
&.round {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
margin: 0 6rpx;
}
&.line {
width: 24rpx;
height: 6rpx;
border-radius: 3rpx;
margin: 0 4rpx;
}
}
}
:deep(.home-banner-swiper),
:deep(.u-swiper),
:deep(.u-swiper__wrapper) {
width: 100%;
}
:deep(.u-swiper__wrapper__item),
:deep(.u-swiper__wrapper__item__wrapper) {
width: 100%;
}
:deep(.u-swiper__wrapper__item__wrapper__image) {
width: 100% !important;
display: block;
}
</style>

View File

@@ -7,9 +7,18 @@ import { computed } from "vue";
const props = defineProps<{ res: any }>(); const props = defineProps<{ res: any }>();
const blankStyle = computed(() => ({ const blankStyle = computed(() => {
background: props.res?.bgColor || "#f5f5f5", const res = props.res || {};
height: `${props.res?.height ?? 10}rpx`, const bg =
margin: `0 ${props.res?.paddingX ?? 0}rpx ${props.res?.pageGap ?? 10}rpx`, res.bgColor1 !== undefined
})); ? !res.bgColor2 || res.bgColor1 === res.bgColor2
? res.bgColor1
: `linear-gradient(180deg, ${res.bgColor1}, ${res.bgColor2})`
: res.bgColor || "#f5f5f5";
return {
background: bg,
height: `${res.height ?? 10}rpx`,
margin: `0 ${res.paddingX ?? 0}rpx ${res.pageGap ?? 10}rpx`,
};
});
</script> </script>

View File

@@ -1,32 +1,163 @@
<template> <template>
<view class="tpl-coupon" :style="wrapperStyle" v-if="couponList.length"> <view class="tpl-coupon" :class="{ styled: styled }" :style="wrapperStyle" v-if="couponList.length || !styled">
<view class="coupon-title" v-if="title">{{ title }}</view> <view class="coupon-title" v-if="title">{{ title }}</view>
<view
class="coupon-item" <!-- 老数据保持原样 -->
v-for="item in couponList" <block v-if="!styled">
:key="item.id" <view
@click="receive(item)" class="coupon-item"
v-for="item in couponList"
:key="item.id"
@click="receive(item)"
>
<view class="coupon-left">
<view class="coupon-amount">
<template v-if="item.couponType == 'DISCOUNT'">
<text class="coupon-value">{{ item.couponDiscount }}</text>
<text class="coupon-symbol">折</text>
</template>
<template v-else>
<text class="coupon-symbol">¥</text>
<text class="coupon-value">{{ unitPrice(item.price) }}</text>
</template>
</view>
<view class="coupon-threshold">满{{ unitPrice(item.consumeThreshold) }}元可用</view>
</view>
<view class="coupon-right">
<view class="coupon-name">
{{ item.storeName == 'platform' ? '全平台通用' : item.storeName + '店铺' }}
</view>
<view class="coupon-btn">立即领取</view>
</view>
</view>
</block>
<!-- 样式一:红色大卡横滑,右侧竖排领取 -->
<scroll-view
v-else-if="layoutStyle === '1'"
scroll-x
class="cp-scroll"
:show-scrollbar="false"
> >
<view class="coupon-left"> <view
<view class="coupon-amount"> class="cp1-item"
<template v-if="item.couponType == 'DISCOUNT'"> v-for="item in couponList"
<text class="coupon-value">{{ item.couponDiscount }}</text> :key="item.id"
<text class="coupon-symbol">折</text> @click="receive(item)"
</template> >
<template v-else> <view class="cp1-left">
<text class="coupon-symbol">¥</text> <view class="cp1-amount">
<text class="coupon-value">{{ unitPrice(item.price) }}</text> <template v-if="item.couponType == 'DISCOUNT'">
</template> <text class="cp1-value">{{ item.couponDiscount }}</text>
<text class="cp1-symbol">折</text>
</template>
<template v-else>
<text class="cp1-symbol">¥</text>
<text class="cp1-value">{{ unitPrice(item.price) }}</text>
</template>
</view>
<view class="cp1-threshold">{{ thresholdText(item) }}</view>
</view> </view>
<view class="coupon-threshold">满{{ unitPrice(item.consumeThreshold) }}元可用</view> <view class="cp1-right">
</view> <text>领</text><text>取</text>
<view class="coupon-right">
<view class="coupon-name">
{{ item.storeName == 'platform' ? '全平台通用' : item.storeName + '店铺' }}
</view> </view>
<view class="coupon-btn">立即领取</view> <view class="cp-notch cp-notch-t"></view>
<view class="cp-notch cp-notch-b"></view>
</view> </view>
</view> </scroll-view>
<!-- 样式二:白底小卡,底部去领取按钮 -->
<scroll-view
v-else-if="layoutStyle === '2'"
scroll-x
class="cp-scroll"
:show-scrollbar="false"
>
<view
class="cp2-item"
v-for="item in couponList"
:key="item.id"
@click="receive(item)"
>
<view class="cp2-top">
<view class="cp2-amount">
<template v-if="item.couponType == 'DISCOUNT'">
<text class="cp2-value">{{ item.couponDiscount }}</text>
<text class="cp2-symbol">折</text>
</template>
<template v-else>
<text class="cp2-symbol">¥</text>
<text class="cp2-value">{{ unitPrice(item.price) }}</text>
</template>
</view>
<view class="cp2-threshold">{{ thresholdText(item) }}</view>
</view>
<view class="cp2-btn">去领取</view>
</view>
</scroll-view>
<!-- 样式三:浅粉虚线卡,右侧竖排立即领取 -->
<scroll-view
v-else-if="layoutStyle === '3'"
scroll-x
class="cp-scroll"
:show-scrollbar="false"
>
<view
class="cp3-item"
v-for="item in couponList"
:key="item.id"
@click="receive(item)"
>
<view class="cp3-left">
<view class="cp3-amount">
<template v-if="item.couponType == 'DISCOUNT'">
<text class="cp3-value">{{ item.couponDiscount }}</text>
<text class="cp3-symbol">折</text>
</template>
<template v-else>
<text class="cp3-symbol">¥</text>
<text class="cp3-value">{{ unitPrice(item.price) }}</text>
</template>
</view>
<view class="cp3-threshold">{{ thresholdText(item) }}</view>
</view>
<view class="cp3-right">
<text>立</text><text>即</text><text>领</text><text>取</text>
</view>
<view class="cp-notch cp-notch-t"></view>
<view class="cp-notch cp-notch-b"></view>
</view>
</scroll-view>
<!-- 样式四:浅粉渐变 + 右侧渐变竖条领取 -->
<scroll-view v-else scroll-x class="cp-scroll" :show-scrollbar="false">
<view
class="cp4-item"
v-for="item in couponList"
:key="item.id"
@click="receive(item)"
>
<view class="cp4-left">
<view class="cp4-amount">
<template v-if="item.couponType == 'DISCOUNT'">
<text class="cp4-value">{{ item.couponDiscount }}</text>
<text class="cp4-symbol">折</text>
</template>
<template v-else>
<text class="cp4-symbol">¥</text>
<text class="cp4-value">{{ unitPrice(item.price) }}</text>
</template>
</view>
<view class="cp4-threshold">{{ thresholdText(item) }}</view>
</view>
<view class="cp4-right">
<text>领</text><text>取</text>
</view>
<view class="cp-notch cp-notch-t"></view>
<view class="cp-notch cp-notch-b"></view>
</view>
</scroll-view>
</view> </view>
</template> </template>
@@ -41,15 +172,35 @@ const props = defineProps<{ res: any }>();
const couponList = ref<any[]>([]); const couponList = ref<any[]>([]);
const title = computed(() => props.res?.list?.[0]?.title || ""); const title = computed(() => props.res?.list?.[0]?.title || "");
const count = computed(() => props.res?.list?.[0]?.count || 3); const count = computed(() => props.res?.list?.[0]?.count || 5);
const layoutStyle = computed(() => (props.res && props.res.layoutStyle) || "1");
const wrapperStyle = computed(() => ({ // 面板保存过样式字段才启用自定义样式,老数据保持原样
background: props.res?.bgColor || "#FFFFFF", const styled = computed(() => props.res && props.res.paddingTop !== undefined);
padding: `20rpx ${props.res?.paddingX ?? 0}rpx`, const wrapperStyle = computed(() => {
marginBottom: `${props.res?.pageGap ?? 10}rpx`, const res = props.res || {};
borderRadius: `${props.res?.borderRadius ?? 0}rpx`, if (styled.value) {
overflow: "hidden", const bg =
})); !res.bgColor2 || res.bgColor1 === res.bgColor2
? res.bgColor1
: `linear-gradient(180deg, ${res.bgColor1}, ${res.bgColor2})`;
const radius = Number(res.borderRadius) || 0;
return {
background: bg,
padding: `${Number(res.paddingTop) || 0}rpx ${Number(res.paddingX) || 0}rpx ${Number(res.paddingBottom) || 0}rpx`,
marginBottom: `${Number(res.pageGap) || 0}rpx`,
borderRadius: `${radius}rpx`,
overflow: "hidden",
};
}
return {
background: res.bgColor || "#FFFFFF",
padding: `20rpx ${res.paddingX ?? 0}rpx`,
marginBottom: `${res.pageGap ?? 10}rpx`,
borderRadius: `${res.borderRadius ?? 0}rpx`,
overflow: "hidden",
};
});
onMounted(() => { onMounted(() => {
getAllCoupons({ pageNumber: 1, pageSize: count.value }).then((res) => { getAllCoupons({ pageNumber: 1, pageSize: count.value }).then((res) => {
@@ -59,6 +210,11 @@ onMounted(() => {
}); });
}); });
function thresholdText(item: any) {
const t = Number(unitPrice(item.consumeThreshold));
return t > 0 ? `满${unitPrice(item.consumeThreshold)}元可用` : "无门槛使用";
}
function receive(item: any) { function receive(item: any) {
if (!isLogin("auth")) { if (!isLogin("auth")) {
navigateToLogin("redirectTo"); navigateToLogin("redirectTo");
@@ -90,6 +246,9 @@ function receive(item: any) {
overflow: hidden; overflow: hidden;
background: linear-gradient(90deg, #ffece8, #fff5f4); background: linear-gradient(90deg, #ffece8, #fff5f4);
} }
.tpl-coupon.styled .coupon-item {
margin: 0;
}
.coupon-left { .coupon-left {
width: 180rpx; width: 180rpx;
padding: 24rpx 0; padding: 24rpx 0;
@@ -124,4 +283,214 @@ function receive(item: any) {
padding: 8rpx 20rpx; padding: 8rpx 20rpx;
border-radius: 24rpx; border-radius: 24rpx;
} }
/* 参考 CRMEB 优惠券组件 */
.tpl-coupon.styled .coupon-title {
padding: 0 0 16rpx;
}
.cp-scroll {
white-space: nowrap;
}
.cp-notch {
position: absolute;
right: 60rpx;
width: 16rpx;
height: 16rpx;
border-radius: 50%;
background: #fff;
}
.cp-notch-t {
top: -8rpx;
}
.cp-notch-b {
bottom: -8rpx;
}
/* 样式一:红色大卡 */
.cp1-item {
position: relative;
display: inline-flex;
width: 264rpx;
height: 132rpx;
margin-right: 16rpx;
border-radius: 12rpx;
background: linear-gradient(135deg, #ff5a36, #e1251b);
color: #fff;
vertical-align: top;
}
.cp1-left {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
justify-content: center;
padding-left: 24rpx;
}
.cp1-value {
font-size: 40rpx;
font-weight: 700;
}
.cp1-symbol {
font-size: 22rpx;
}
.cp1-threshold {
font-size: 20rpx;
color: rgba(255, 255, 255, 0.85);
margin-top: 4rpx;
}
.cp1-right {
width: 60rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 24rpx;
border-left: 2rpx dashed rgba(255, 255, 255, 0.6);
}
.cp1-item .cp-notch {
right: 52rpx;
}
/* 样式二:白底小卡 */
.cp2-item {
display: inline-flex;
flex-direction: column;
width: 148rpx;
margin-right: 16rpx;
border-radius: 12rpx;
background: #fff;
border-left: 4rpx solid #f5483a;
border-right: 4rpx solid #f5483a;
overflow: hidden;
vertical-align: top;
}
.cp2-top {
padding: 16rpx 0 12rpx;
text-align: center;
}
.cp2-value {
font-size: 34rpx;
font-weight: 700;
color: #e1251b;
}
.cp2-symbol {
font-size: 20rpx;
color: #e1251b;
}
.cp2-threshold {
font-size: 18rpx;
color: #999;
margin-top: 2rpx;
}
.cp2-btn {
background: linear-gradient(90deg, #ff6a3d, #e1251b);
color: #fff;
font-size: 20rpx;
text-align: center;
padding: 8rpx 0;
}
/* 样式三:浅粉虚线卡 */
.cp3-item {
position: relative;
display: inline-flex;
width: 276rpx;
height: 120rpx;
margin-right: 16rpx;
border-radius: 12rpx;
background: #fdf1ec;
border: 2rpx dashed #f5483a;
vertical-align: top;
}
.cp3-left {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
justify-content: center;
padding-left: 24rpx;
}
.cp3-value {
font-size: 38rpx;
font-weight: 700;
color: #e1251b;
}
.cp3-symbol {
font-size: 20rpx;
color: #e1251b;
}
.cp3-threshold {
font-size: 20rpx;
color: #e1251b;
margin-top: 2rpx;
}
.cp3-right {
width: 64rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 22rpx;
color: #e1251b;
border-left: 2rpx dashed #f5483a;
}
.cp3-item .cp-notch {
right: 56rpx;
}
/* 样式四:浅粉渐变 + 渐变竖条 */
.cp4-item {
position: relative;
display: inline-flex;
width: 232rpx;
height: 108rpx;
margin-right: 16rpx;
border-radius: 12rpx;
overflow: hidden;
vertical-align: top;
}
.cp4-left {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
justify-content: center;
padding-left: 20rpx;
background: linear-gradient(135deg, #fff3ec, #ffe0d3);
}
.cp4-value {
font-size: 34rpx;
font-weight: 700;
color: #e1251b;
}
.cp4-symbol {
font-size: 20rpx;
color: #e1251b;
}
.cp4-threshold {
font-size: 18rpx;
color: #e1251b;
margin-top: 2rpx;
}
.cp4-right {
width: 52rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 22rpx;
color: #fff;
background: linear-gradient(180deg, #ff6a3d, #e1251b);
}
.cp4-item .cp-notch {
right: 44rpx;
width: 12rpx;
height: 12rpx;
}
.cp4-item .cp-notch-t {
top: -6rpx;
}
.cp4-item .cp-notch-b {
bottom: -6rpx;
}
</style> </style>

View File

@@ -1,16 +1,21 @@
<template> <template>
<div class="layout"> <div class="layout" :style="[wrapperStyle, gapStyle]">
<u-image width="140rpx" mode="aspectFit" height="140rpx" @click="modelNavigateTo(item)" class="image-mode" v-for="(item,index) in res.list" :key="index" :src="item.img" alt=""> <u-image width="100%" mode="aspectFit" height="140rpx" @click="modelNavigateTo(item)" class="image-mode flex-img" :style="imgStyle" v-for="(item,index) in res.list" :key="index" :src="item.img" alt="">
<template #loading><u-loading-icon></u-loading-icon></template> <template #loading><u-loading-icon></u-loading-icon></template>
</u-image> </u-image>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { modelNavigateTo } from "./tpl"; import { computed } from "vue";
import { modelNavigateTo, cubeWrapperStyle, cubeImgStyle, cubeGapStyle } from "./tpl";
defineProps<{ res: any }>(); const props = defineProps<{ res: any }>();
const wrapperStyle = computed(() => cubeWrapperStyle(props.res));
const imgStyle = computed(() => cubeImgStyle(props.res));
const gapStyle = computed(() => cubeGapStyle(props.res));
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "./tpl.scss"; @import "./tpl.scss";
@@ -20,4 +25,8 @@ defineProps<{ res: any }>();
justify-content: center; justify-content: center;
background-size: cover; background-size: cover;
} }
</style> .flex-img {
flex: 1;
min-width: 0;
}
</style>

View File

@@ -1,24 +1,33 @@
<template> <template>
<div class="layout"> <div class="layout" :style="[wrapperStyle, gapStyle]">
<u-image height="175rpx" mode="aspectFit" width="175rpx" @click="modelNavigateTo(item)" class="image-mode" :src="item.img" v-for="(item,index) in res.list" :key="index"> <u-image height="175rpx" mode="aspectFit" width="100%" @click="modelNavigateTo(item)" class="image-mode flex-img" :style="imgStyle" :src="item.img" v-for="(item,index) in res.list" :key="index">
<template #loading><u-loading-icon></u-loading-icon></template> <template #loading><u-loading-icon></u-loading-icon></template>
</u-image> </u-image>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { modelNavigateTo } from "./tpl"; import { computed } from "vue";
import { modelNavigateTo, cubeWrapperStyle, cubeImgStyle, cubeGapStyle } from "./tpl";
defineProps<{ res: any }>(); const props = defineProps<{ res: any }>();
const wrapperStyle = computed(() => cubeWrapperStyle(props.res));
const imgStyle = computed(() => cubeImgStyle(props.res));
const gapStyle = computed(() => cubeGapStyle(props.res));
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "./tpl.scss"; @import "./tpl.scss";
.layout { .layout {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background-size: cover; background-size: cover;
} }
</style> .flex-img {
flex: 1;
min-width: 0;
}
</style>

View File

@@ -1,16 +1,21 @@
<template> <template>
<div class="layout"> <div class="layout" :style="[wrapperStyle, gapStyle]">
<u-image @click="modelNavigateTo(item)" height="240rpx" width="240rpx" class="image-mode" :src="item.img" v-for="(item, index) in res.list" :key="index"> <u-image @click="modelNavigateTo(item)" height="240rpx" width="100%" class="image-mode flex-img" :style="imgStyle" :src="item.img" v-for="(item, index) in res.list" :key="index">
<template #loading><u-loading-icon></u-loading-icon></template> <template #loading><u-loading-icon></u-loading-icon></template>
</u-image> </u-image>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { modelNavigateTo } from "./tpl"; import { computed } from "vue";
import { modelNavigateTo, cubeWrapperStyle, cubeImgStyle, cubeGapStyle } from "./tpl";
defineProps<{ res: any }>(); const props = defineProps<{ res: any }>();
const wrapperStyle = computed(() => cubeWrapperStyle(props.res));
const imgStyle = computed(() => cubeImgStyle(props.res));
const gapStyle = computed(() => cubeGapStyle(props.res));
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "./tpl.scss"; @import "./tpl.scss";
@@ -22,4 +27,8 @@ defineProps<{ res: any }>();
justify-content: center; justify-content: center;
background-size: cover; background-size: cover;
} }
</style> .flex-img {
flex: 1;
min-width: 0;
}
</style>

View File

@@ -1,12 +1,12 @@
<template> <template>
<view class="layout"> <view class="layout" :style="wrapperStyle">
<view class="flex-two"> <view class="flex-two" :style="gapStyle">
<view class="flex-item" @click="modelNavigateTo(res.list[0])"> <view class="flex-item" :style="imgStyle" @click="modelNavigateTo(res.list[0])">
<u-image height="250rpx" width="100%" mode="scaleToFill" :src="res.list[0].img" alt> <u-image height="250rpx" width="100%" mode="scaleToFill" :src="res.list[0].img" alt>
<template #loading><u-loading-icon></u-loading-icon></template> <template #loading><u-loading-icon></u-loading-icon></template>
</u-image> </u-image>
</view> </view>
<view class="flex-item" @click="modelNavigateTo(res.list[1])"> <view class="flex-item" :style="imgStyle" @click="modelNavigateTo(res.list[1])">
<u-image height="250rpx" width="100%" mode="scaleToFill" :src="res.list[1].img" alt> <u-image height="250rpx" width="100%" mode="scaleToFill" :src="res.list[1].img" alt>
<template #loading><u-loading-icon></u-loading-icon></template> <template #loading><u-loading-icon></u-loading-icon></template>
</u-image> </u-image>
@@ -15,9 +15,14 @@
</view> </view>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { modelNavigateTo } from "./tpl"; import { computed } from "vue";
import { modelNavigateTo, cubeWrapperStyle, cubeImgStyle, cubeGapStyle } from "./tpl";
defineProps<{ res: any }>(); const props = defineProps<{ res: any }>();
const wrapperStyle = computed(() => cubeWrapperStyle(props.res));
const imgStyle = computed(() => cubeImgStyle(props.res));
const gapStyle = computed(() => cubeGapStyle(props.res));
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "./tpl.scss"; @import "./tpl.scss";
@@ -27,6 +32,7 @@ defineProps<{ res: any }>();
overflow: hidden; overflow: hidden;
} }
.flex-item { .flex-item {
width: 50%; flex: 1;
min-width: 0;
} }
</style> </style>

View File

@@ -1,265 +1,678 @@
<template> <template>
<view class="layout"> <!-- 新版商品列表 -->
<u-sticky> <view v-if="isNew" class="goods-new" :style="wrapperStyle">
<view class="goods-cell-title"> <view v-if="displayGoods.length" class="g-list" :class="'mode-' + layout" :style="listStyle">
<view <view
class="goods-item-title" class="g-item"
:class="{ 'selected-title': selected.index == index }" v-for="(g, i) in displayGoods"
@click="handleClickTitle(title, index)" :key="i"
v-for="(title, index) in res.list[0].titleWay" :style="itemStyle"
:key="index" @click="goDetail(g)"
> >
<h4 class="h4">{{ title.title }}</h4> <view class="g-imgbox">
<view class="title-desc">{{ title.desc }}</view> <image :src="g.img" mode="aspectFill" class="g-img" />
</view> </view>
</view> <view class="g-info">
</u-sticky> <view v-if="showName" class="g-name" :style="nameStyle">{{ g.name }}</view>
<view class="goods-list"> <view v-if="showPrice" class="g-price" :style="priceStyle">
<template v-for="(item, item_index) in res.list[0].listWay" :key="item_index"> ¥{{ goodsFormatPrice(g.price)[0] }}.{{ goodsFormatPrice(g.price)[1] }}
<view </view>
v-if=" <view class="g-bottom">
item.___index != undefined <text
? selected.index == item.___index v-if="showSales && g.sales != null"
: selected.val == item.type class="g-sales"
" :style="salesStyle"
@click="handleClick(item)" >已售{{ g.sales }}件</text
class="goods-item" >
> <view
<view class="goods-img"> v-if="cartShow"
<u-image class="g-cart"
:src="item.img" :class="'cart-' + cartStyle"
height="350rpx" :style="{ background: priceColor }"
mode="aspectFit" @click.stop="onCartBtn(g)"
width="100%" >
> <text v-if="cartStyle === 'text'" class="cart-text-label">加购</text>
<template #loading><u-loading-icon></u-loading-icon></template> <text v-else-if="cartStyle === 'plus'" class="cart-plus-label">+</text>
</u-image> <u-icon v-else name="shopping-cart" color="#ffffff" size="28rpx"></u-icon>
</view> </view>
<view class="goods-desc"> </view>
<view class="goods-title"> </view>
{{ item.title }} </view>
</view> </view>
<view class="goods-bottom"> <view v-else class="g-empty">暂无商品</view>
<view class="goods-price"> </view>
¥<span class="price-int"
>{{ goodsFormatPrice(item.price)[0] }} </span <!-- 旧版:分类标签 + 绑定商品 -->
>.{{ goodsFormatPrice(item.price)[1] }} <view v-else class="layout">
</view> <u-sticky>
</view> <view class="goods-cell-title">
</view> <view
</view> class="goods-item-title"
</template> :class="{ 'selected-title': selected.index == index }"
@click="handleClickTitle(title, index)"
<template v-for="(title, index) in res.list[0].titleWay"
v-if="currentTitleWay?.bindCategory && goodsData.length" :key="index"
> >
<view <h4 class="h4">{{ title.title }}</h4>
v-for="(item, index) in goodsData" <view class="title-desc">{{ title.desc }}</view>
:key="index" </view>
class="goods-item" </view>
@click="handleClick(item)" </u-sticky>
> <view class="goods-list">
<view class="goods-img"> <template v-for="(item, item_index) in res.list[0].listWay" :key="item_index">
<u-image <view
:src="item.thumbnail" v-if="
height="350rpx" item.___index != undefined
mode="aspectFit" ? selected.index == item.___index
width="100%" : selected.val == item.type
> "
<template #loading><u-loading-icon></u-loading-icon></template> @click="handleClick(item)"
</u-image> class="goods-item"
</view> >
<view class="goods-desc"> <view class="goods-img">
<view class="goods-title"> <u-image
{{ item.goodsName }} :src="item.img"
</view> height="350rpx"
<view class="goods-bottom"> mode="aspectFit"
<view class="goods-price"> width="100%"
¥<span class="price-int" >
>{{ goodsFormatPrice(item.price)[0] }} </span <template #loading><u-loading-icon></u-loading-icon></template>
>.{{ goodsFormatPrice(item.price)[1] }} </u-image>
</view> </view>
</view> <view class="goods-desc">
</view> <view class="goods-title">
</view> {{ item.title }}
</template> </view>
</view> <view class="goods-bottom">
</view> <view class="goods-price">
</template> ¥<span class="price-int"
<script setup lang="ts"> >{{ goodsFormatPrice(item.price)[0] }} </span
import { ref, reactive, watch, onMounted, onUnmounted, computed } from "vue"; >.{{ goodsFormatPrice(item.price)[1] }}
import { getGoodsList } from "@/api/goods.js"; </view>
import { goodsFormatPrice } from "@/utils/filters.js"; </view>
</view>
const props = defineProps<{ res: any; enableBottomLoad?: boolean }>(); </view>
</template>
const selected = reactive({
index: 0, <template
val: "", v-if="currentTitleWay?.bindCategory && goodsData.length"
}); >
<view
const currentTitleWay = computed(() => { v-for="(item, index) in goodsData"
return props.res?.list?.[0]?.titleWay?.[selected.index] :key="index"
}) class="goods-item"
const params = reactive({ @click="handleClick(item)"
pageNumber: 1, >
pageSize: 100, <view class="goods-img">
categoryId: "", <u-image
}); :src="item.thumbnail"
const goodsData = ref<any[]>([]); height="350rpx"
const goodsResult = ref<any>(""); mode="aspectFit"
width="100%"
watch( >
() => props.res, <template #loading><u-loading-icon></u-loading-icon></template>
(val) => { </u-image>
if (!val?.list?.[0]) return </view>
const firstListWay = val.list[0].listWay?.[0] <view class="goods-desc">
selected.val = firstListWay?.type || '' <view class="goods-title">
const firstTitleWay = val.list[0].titleWay?.[0] {{ item.goodsName }}
if (firstTitleWay?.bindCategory) { </view>
initGoods(firstTitleWay) <view class="goods-bottom">
} <view class="goods-price">
}, ¥<span class="price-int"
{ immediate: true } >{{ goodsFormatPrice(item.price)[0] }} </span
) >.{{ goodsFormatPrice(item.price)[1] }}
</view>
onMounted(() => { </view>
uni.$on("onReachBottom", () => { </view>
if ( </view>
props.enableBottomLoad && </template>
goodsResult.value.totalElements >= </view>
params.pageNumber * params.pageSize </view>
) { </template>
params.pageNumber++; <script setup lang="ts">
initGoods(props.res.list[0].titleWay[selected.index]); import { ref, reactive, watch, onMounted, onUnmounted, computed } from "vue";
} import { getGoodsList } from "@/api/goods.js";
}); import { addToCart } from "@/api/trade.js";
}); import { goodsFormatPrice } from "@/utils/filters.js";
import storage from "@/utils/storage.js";
onUnmounted(() => {
uni.$off("onReachBottom"); const props = defineProps<{ res: any; enableBottomLoad?: boolean }>();
});
const selected = reactive({
function handleClick(item: any) { index: 0,
if (!item?.id || !item?.goodsId) return val: "",
uni.navigateTo({ });
url: `/pages/product/goods?id=${item.id}&goodsId=${item.goodsId}`,
}) // ---------- 新版商品列表 ----------
} const first = computed(() => props.res?.list?.[0] || {});
const isNew = computed(() => first.value.layout !== undefined);
async function initGoods(val: any) { const layout = computed(() => first.value.layout || "list");
if (props.enableBottomLoad) params.pageSize = 20; const showName = computed(() => first.value.showName !== false);
val ? (params.categoryId = val.bindCategory.id) : ""; const showPrice = computed(() => first.value.showPrice !== false);
const res = await getGoodsList(params); const showSales = computed(() => first.value.showSales !== false);
if (res.data.success) { const cartShow = computed(() => first.value.cartShow !== false);
goodsResult.value = res.data.result; const cartStyle = computed(() => first.value.cartStyle || "plus");
const result = res.data.result.records; const priceColor = computed(() =>
goodsData.value.push(...result); props.res?.toneMode === "custom" && props.res?.priceColor
console.log(goodsData.value); ? props.res.priceColor
} : "var(--theme-primary, #ff3c2a)"
} );
const priceStyle = computed(() => ({ color: priceColor.value }));
function handleClickTitle(val: any, index: number) { const nameStyle = computed(() =>
selected.index = index; props.res?.titleColor ? { color: props.res.titleColor } : {}
selected.val = val.title; );
if (val.bindCategory) { const salesStyle = computed(() =>
params.pageNumber = 1; props.res?.salesColor ? { color: props.res.salesColor } : {}
goodsData.value = []; );
initGoods(val); const rpx = (v: any) => `${(Number(v) || 0) * 2}rpx`;
} const wrapperStyle = computed(() => {
} const res = props.res || {};
</script> const bg =
<style lang="scss" scoped> !res.bgColor2 || res.bgColor1 === res.bgColor2
$w_94: 94%; ? res.bgColor1 || "#ffffff"
: `linear-gradient(180deg, ${res.bgColor1}, ${res.bgColor2})`;
.layout { return {
padding: 8px 0; background: bg,
background: #f9f9f9; padding: `${rpx(res.paddingTop)} ${rpx(res.paddingX)} ${rpx(res.paddingBottom)}`,
} borderRadius: rpx(res.borderRadius),
overflow: Number(res.borderRadius) > 0 ? "hidden" : "visible",
.selected-title { };
.h4 { });
font-size: 30rpx; const listStyle = computed(() => ({ gap: rpx(props.res?.contentGap ?? 10) }));
color: #000 !important; const itemStyle = computed(() => ({
} borderRadius: rpx(props.res?.contentRadius ?? 7),
overflow: Number(props.res?.contentRadius ?? 7) > 0 ? "hidden" : "visible",
.title-desc { }));
font-weight: bold;
color: $main-color !important; const newGoodsData = ref<any[]>([]);
}
} const displayGoods = computed(() => {
if (!isNew.value) return [];
.goods-cell-title { return newGoodsData.value;
background: #f9f9f9; });
padding: 10px;
transition: 0.35s; // 触底加载(新版)
display: flex; const newParams = reactive({ pageNumber: 1 });
const newTotal = ref(0);
> .goods-item-title {
flex: 1; function buildQuery() {
text-align: center; const item = first.value;
const params: Record<string, any> = {
.h4 { pageNumber: newParams.pageNumber,
font-size: 32rpx; pageSize: item.count || 6,
} };
if (item.dataMode === "brand" && item.brandId) params.brandId = item.brandId;
.title-desc { if (item.dataMode === "store" && item.storeId) params.storeId = item.storeId;
color: #999; if (item.dataMode === "category" && item.categoryId) params.categoryId = item.categoryId;
font-size: 24rpx; if (item.dataMode === "goods") {
} const ids = (item.goodsList || []).map((g: any) => g.id).filter(Boolean);
} if (!ids.length) return null;
} params.ids = ids.join(",");
params.pageSize = ids.length;
.goods-list { }
width: 100%; if (item.sortType === "sales") {
display: flex; params.sort = "buyCount";
flex-wrap: wrap; params.order = "desc";
} } else if (item.sortType === "price") {
params.sort = "price";
.goods-item { params.order = "asc";
width: 50%; }
margin-bottom: 10px; return params;
border-radius: 0.4em; }
overflow: hidden;
} async function loadNewGoods(reset = true) {
const params = buildQuery();
.goods-img { if (!params) {
position: relative; newGoodsData.value = [];
margin: 0 auto; newTotal.value = 0;
// width: 158px; return;
width: $w_94; }
border-top-left-radius: 20rpx; try {
border-top-right-radius: 20rpx; const res = await getGoodsList(params);
overflow: hidden; if (!res.data.success) return;
} const result = res.data.result || {};
const records = result.records || [];
.goods-desc { const mapped = records.map((g: any) => ({
border-bottom-left-radius: 20rpx; id: g.id,
border-bottom-right-radius: 20rpx; goodsId: g.goodsId,
width: $w_94; name: g.goodsName,
background: #fff; price: g.price,
padding: 8rpx 0 8rpx 8rpx; img: g.thumbnail || g.small,
margin: 0 auto; sales: g.buyCount != null ? g.buyCount : null,
> .goods-title { }));
font-size: 24rpx; if (reset) {
height: 67rpx; // 指定商品:按选择顺序排列
display: -webkit-box; if (first.value.dataMode === "goods") {
font-weight: 500; const byGoodsId: Record<string, any> = {};
-webkit-box-orient: vertical; mapped.forEach((g: any) => {
-webkit-line-clamp: 2; byGoodsId[g.goodsId] = g;
overflow: hidden; });
} newGoodsData.value = (first.value.goodsList || [])
.map((g: any) => byGoodsId[g.id])
> .goods-bottom { .filter(Boolean);
display: flex; } else {
font-weight: bold; newGoodsData.value = mapped;
> .goods-price { }
line-height: 2; } else {
color: $main-color; newGoodsData.value.push(...mapped);
.price-int { }
font-size: 42rpx; newTotal.value = result.totalElements ?? newGoodsData.value.length;
} } catch (e) {
} // 静默失败,保持已有数据
} }
} }
</style>
function goDetail(g: any) {
if (!g?.id || !g?.goodsId) return;
uni.navigateTo({
url: `/pages/product/goods?id=${g.id}&goodsId=${g.goodsId}`,
});
}
function onCartBtn(g: any) {
if (first.value.cartAction !== "add") {
goDetail(g);
return;
}
if (!g?.id) return;
if (!storage.getAccessToken()) {
// #ifdef MP-WEIXIN
uni.navigateTo({ url: "/pages/passport/wechatMPLogin" });
// #endif
// #ifndef MP-WEIXIN
uni.navigateTo({ url: "/pages/passport/login" });
// #endif
return;
}
addToCart({ skuId: g.id, num: 1 }).then((res: any) => {
uni.showToast({
title: res.data.success ? "已加入购物车" : res.data.message || "加购失败",
icon: "none",
});
});
}
watch(
() => [
first.value.dataMode,
first.value.categoryId,
first.value.brandId,
first.value.storeId,
first.value.count,
first.value.sortType,
first.value.goodsList,
],
() => {
if (!isNew.value) return;
newParams.pageNumber = 1;
loadNewGoods(true);
},
{ immediate: true, deep: true }
);
// ---------- 旧版逻辑 ----------
const currentTitleWay = computed(() => {
return props.res?.list?.[0]?.titleWay?.[selected.index]
})
const params = reactive({
pageNumber: 1,
pageSize: 100,
categoryId: "",
});
const goodsData = ref<any[]>([]);
const goodsResult = ref<any>("");
watch(
() => props.res,
(val) => {
if (!val?.list?.[0]) return
if (val.list[0].layout !== undefined) return
const firstListWay = val.list[0].listWay?.[0]
selected.val = firstListWay?.type || ''
const firstTitleWay = val.list[0].titleWay?.[0]
if (firstTitleWay?.bindCategory) {
initGoods(firstTitleWay)
}
},
{ immediate: true }
)
onMounted(() => {
uni.$on("onReachBottom", () => {
if (!props.enableBottomLoad) return;
if (isNew.value) {
// 新版:仅分类/品牌/商户模式支持触底加载
if (first.value.dataMode === "goods") return;
if (newTotal.value > newGoodsData.value.length) {
newParams.pageNumber++;
loadNewGoods(false);
}
return;
}
if (
goodsResult.value.totalElements >=
params.pageNumber * params.pageSize
) {
params.pageNumber++;
initGoods(props.res.list[0].titleWay[selected.index]);
}
});
});
onUnmounted(() => {
uni.$off("onReachBottom");
});
function handleClick(item: any) {
if (!item?.id || !item?.goodsId) return
uni.navigateTo({
url: `/pages/product/goods?id=${item.id}&goodsId=${item.goodsId}`,
})
}
async function initGoods(val: any) {
if (props.enableBottomLoad) params.pageSize = 20;
val ? (params.categoryId = val.bindCategory.id) : "";
const res = await getGoodsList(params);
if (res.data.success) {
goodsResult.value = res.data.result;
const result = res.data.result.records;
goodsData.value.push(...result);
}
}
function handleClickTitle(val: any, index: number) {
selected.index = index;
selected.val = val.title;
if (val.bindCategory) {
params.pageNumber = 1;
goodsData.value = [];
initGoods(val);
}
}
</script>
<style lang="scss" scoped>
$w_94: 94%;
// ---------- 新版 ----------
.goods-new {
background: #fff;
}
.g-empty {
padding: 60rpx 0;
text-align: center;
color: #999;
font-size: 24rpx;
}
.g-list {
display: flex;
flex-wrap: wrap;
}
.g-item {
background: #fff;
display: flex;
box-sizing: border-box;
}
.g-imgbox {
flex-shrink: 0;
background: #f5f5f5;
overflow: hidden;
}
.g-img {
width: 100%;
height: 100%;
display: block;
}
.g-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
}
.g-name {
font-size: 26rpx;
color: #282828;
line-height: 1.4;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
.g-bottom {
display: flex;
align-items: center;
margin-top: auto;
}
.g-price {
color: $main-color;
font-size: 30rpx;
font-weight: 600;
margin-bottom: 4rpx;
}
.g-sales {
flex: 1;
min-width: 0;
font-size: 22rpx;
color: #666;
}
.g-cart {
flex-shrink: 0;
margin-left: auto;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
background: $main-color;
}
.cart-plus,
.cart-cart {
width: 36rpx;
height: 36rpx;
border-radius: 50%;
}
.cart-plus-label {
font-size: 28rpx;
line-height: 1;
}
.cart-text {
border-radius: 18rpx;
padding: 4rpx 18rpx;
}
.cart-text-label {
font-size: 22rpx;
}
/* 卡片:左图右文 */
.mode-card {
flex-direction: column;
.g-item {
flex-direction: row;
align-items: center;
padding: 16rpx;
}
.g-imgbox {
width: 140rpx;
height: 140rpx;
margin-right: 16rpx;
}
.g-name {
margin-bottom: 8rpx;
}
.g-cart {
align-self: center;
}
}
/* 列表:两列网格 */
.mode-list {
.g-item {
width: calc(50% - 10rpx);
flex-direction: column;
}
.g-imgbox {
width: 100%;
height: 330rpx;
}
.g-info {
padding: 12rpx;
min-height: 120rpx;
}
.g-name {
margin-bottom: 8rpx;
}
}
/* 三列 */
.mode-three {
.g-item {
width: calc(33.33% - 12rpx);
flex-direction: column;
}
.g-imgbox {
width: 100%;
height: 220rpx;
}
.g-info {
padding: 10rpx;
min-height: 96rpx;
}
.g-name {
-webkit-line-clamp: 1;
font-size: 24rpx;
margin-bottom: 6rpx;
}
.g-price {
font-size: 26rpx;
}
.g-sales {
font-size: 20rpx;
}
}
/* 大图:一行一个 */
.mode-big {
flex-direction: column;
.g-item {
width: 100%;
flex-direction: column;
}
.g-imgbox {
width: 100%;
height: 420rpx;
}
.g-info {
padding: 16rpx;
min-height: 110rpx;
}
.g-name {
margin-bottom: 8rpx;
}
}
// ---------- 旧版 ----------
.layout {
padding: 8px 0;
background: #f9f9f9;
}
.selected-title {
.h4 {
font-size: 30rpx;
color: #000 !important;
}
.title-desc {
font-weight: bold;
color: $main-color !important;
}
}
.goods-cell-title {
background: #f9f9f9;
padding: 10px;
transition: 0.35s;
display: flex;
> .goods-item-title {
flex: 1;
text-align: center;
.h4 {
font-size: 32rpx;
}
.title-desc {
color: #999;
font-size: 24rpx;
}
}
}
.goods-list {
width: 100%;
display: flex;
flex-wrap: wrap;
}
.goods-item {
width: 50%;
margin-bottom: 10px;
border-radius: 0.4em;
overflow: hidden;
}
.goods-img {
position: relative;
margin: 0 auto;
width: $w_94;
border-top-left-radius: 20rpx;
border-top-right-radius: 20rpx;
overflow: hidden;
}
.goods-desc {
border-bottom-left-radius: 20rpx;
border-bottom-right-radius: 20rpx;
width: $w_94;
background: #fff;
padding: 8rpx 0 8rpx 8rpx;
margin: 0 auto;
> .goods-title {
font-size: 24rpx;
height: 67rpx;
display: -webkit-box;
font-weight: 500;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
> .goods-bottom {
display: flex;
font-weight: bold;
> .goods-price {
line-height: 2;
color: $main-color;
.price-int {
font-size: 42rpx;
}
}
}
}
</style>

View File

@@ -1,31 +1,90 @@
<template> <template>
<view class="layout" :style="wrapperStyle" v-if="goodsList.length"> <view class="layout" :style="wrapperStyle" v-if="goodsList.length">
<view class="join-list"> <view class="integral-head" :style="headStyle" @click="goMore">
<view class="join-title" @click="goMore"> <image
<view class="join-title-name">{{ title }}</view> v-if="styled && options.titleType === 'image' && options.titleImage"
<view class="join-title-more">更多</view> class="integral-head-img"
:src="options.titleImage"
mode="aspectFit"
/>
<view v-else class="integral-head-title" :style="headTitleStyle">{{ title }}</view>
<view class="integral-head-space"></view>
<view class="integral-head-more" :style="moreStyle">更多 &gt;</view>
</view>
<!-- 老数据:原始四列样式 -->
<view v-if="!styled" class="join-box">
<view
class="join-item"
@click="goDetail(item)"
v-for="(item, index) in goodsList"
:key="index"
>
<view class="item-img-box">
<u-image
class="item-img"
width="150rpx"
height="150rpx"
:src="item.goodsImage || item.thumbnail"
/>
</view>
<view class="item-price">
<span class="price-text">{{ item.points != null ? item.points : 0 }}积分</span>
</view>
<view class="item-line-through" v-if="item.originalPrice">
<span class="price-text">¥{{ item.originalPrice }}</span>
</view>
</view> </view>
<view class="join-box"> </view>
<view <!-- 样式一:两列大卡片 -->
class="join-item" <view v-else-if="layoutStyle === '1'" class="ig-grid2">
@click="goDetail(item)" <view
v-for="(item, index) in goodsList" class="ig-g2-item"
:key="index" v-for="(item, index) in goodsList"
> :key="index"
<view class="item-img-box"> @click="goDetail(item)"
<u-image >
class="item-img" <view class="ig-g2-img-box" :style="imgRadiusStyle">
width="150rpx" <image class="ig-img" :src="item.goodsImage || item.thumbnail" mode="aspectFill" />
height="150rpx" </view>
:src="item.goodsImage || item.thumbnail" <view class="ig-g2-name">{{ item.goodsName || item.promotionName || '' }}</view>
/> <view class="ig-price-row">
</view> <view class="ig-price" :style="pointStyle">
<view class="item-price"> <text class="ig-coin">¥</text>{{ item.points != null ? item.points : 0 }}积分
<span class="price-text">{{ item.points != null ? item.points : 0 }}积分</span>
</view>
<view class="item-line-through" v-if="item.originalPrice">
<span class="price-text">¥{{ item.originalPrice }}</span>
</view> </view>
<text class="ig-original" v-if="item.originalPrice">¥{{ item.originalPrice }}</text>
</view>
</view>
</view>
<!-- 样式二:三列,积分价在名称上方 -->
<view v-else-if="layoutStyle === '2'" class="ig-grid3">
<view
class="ig-g3-item"
v-for="(item, index) in goodsList"
:key="index"
@click="goDetail(item)"
>
<view class="ig-g3-img-box" :style="imgRadiusStyle">
<image class="ig-img" :src="item.goodsImage || item.thumbnail" mode="aspectFill" />
</view>
<view class="ig-price ig-price-sm" :style="pointStyle">
<text class="ig-coin ig-coin-sm">¥</text>{{ item.points != null ? item.points : 0 }}积分
</view>
<view class="ig-g3-name">{{ item.goodsName || item.promotionName || '' }}</view>
</view>
</view>
<!-- 样式三:三列,仅积分价 -->
<view v-else class="ig-grid3">
<view
class="ig-g3-item"
v-for="(item, index) in goodsList"
:key="index"
@click="goDetail(item)"
>
<view class="ig-g3-img-box" :style="imgRadiusStyle">
<image class="ig-img" :src="item.goodsImage || item.thumbnail" mode="aspectFill" />
</view>
<view class="ig-price ig-price-sm" :style="pointStyle">
<text class="ig-coin ig-coin-sm">¥</text>{{ item.points != null ? item.points : 0 }}积分
</view> </view>
</view> </view>
</view> </view>
@@ -39,16 +98,76 @@ const props = defineProps<{ res: any }>();
const goodsList = ref<any[]>([]); const goodsList = ref<any[]>([]);
const title = computed(() => props.res?.list?.[0]?.title || "积分商城"); const options = computed(() => props.res || {});
const count = computed(() => props.res?.list?.[0]?.count || 3); const title = computed(() => props.res?.list?.[0]?.title || "积分兑好礼");
const count = computed(() => props.res?.list?.[0]?.count || 6);
const layoutStyle = computed(() => props.res?.layoutStyle || "1");
const wrapperStyle = computed(() => ({ // 面板保存过样式字段才启用新样式,老数据保持原样
background: props.res?.bgColor || "#FFFFFF", const styled = computed(() => props.res && props.res.paddingTop !== undefined);
padding: `0 ${props.res?.paddingX ?? 0}rpx`,
marginBottom: `${props.res?.pageGap ?? 10}rpx`, const wrapperStyle = computed(() => {
borderRadius: `${props.res?.borderRadius ?? 0}rpx`, const res = props.res || {};
overflow: "hidden", if (styled.value) {
})); const bg =
!res.bgColor2 || res.bgColor1 === res.bgColor2
? res.bgColor1
: `linear-gradient(180deg, ${res.bgColor1}, ${res.bgColor2})`;
const radius = Number(res.borderRadius) || 0;
return {
background: bg,
padding: `${Number(res.paddingTop) || 0}rpx ${Number(res.paddingX) || 0}rpx ${Number(res.paddingBottom) || 0}rpx`,
marginBottom: `${Number(res.pageGap) || 0}rpx`,
borderRadius: `${radius}rpx`,
overflow: radius > 0 ? "hidden" : "visible",
};
}
return {
background: res.bgColor || "#FFFFFF",
padding: `0 ${res.paddingX ?? 0}rpx`,
marginBottom: `${res.pageGap ?? 10}rpx`,
borderRadius: `${res.borderRadius ?? 0}rpx`,
overflow: "hidden",
};
});
const headStyle = computed(() => {
if (!styled.value) return {};
const res = props.res;
if (res.headBgType === "image" && res.headBgImage) {
return {
backgroundImage: `url(${res.headBgImage});background-size:cover;background-position:center;`,
};
}
const bg =
!res.headBg2 || res.headBg1 === res.headBg2
? res.headBg1
: `linear-gradient(90deg, ${res.headBg1}, ${res.headBg2})`;
return { background: bg };
});
const headTitleStyle = computed(() => {
if (!styled.value) return {};
const res = props.res;
return {
color: res.titleColor || "#FFFFFF",
fontWeight: res.titleStyle === "bold" ? "bold" : "normal",
fontStyle: res.titleStyle === "italic" ? "italic" : "normal",
};
});
const moreStyle = computed(() =>
styled.value ? { color: props.res.moreColor || "#999999" } : {}
);
const pointStyle = computed(() =>
styled.value && props.res.pointColor ? { color: props.res.pointColor } : {}
);
const gapStyle = computed(() =>
styled.value ? { gap: `${Number(props.res.contentGap) || 0}rpx` } : {}
);
const imgRadiusStyle = computed(() =>
styled.value ? { borderRadius: `${Number(props.res.imgRadius) || 0}rpx`, overflow: "hidden" } : {}
);
onMounted(() => { onMounted(() => {
API_Promotions.getPointsGoods({ API_Promotions.getPointsGoods({
@@ -71,6 +190,33 @@ function goDetail(item: any) {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "./tpl.scss"; @import "./tpl.scss";
.integral-head {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
min-height: 96rpx;
padding: 0 24rpx;
box-sizing: border-box;
margin-bottom: 20rpx;
border-radius: 12rpx;
}
.integral-head-img {
width: 154rpx;
height: 32rpx;
}
.integral-head-title {
font-size: 32rpx;
font-weight: bold;
}
.integral-head-space {
flex: 1;
}
.integral-head-more {
flex-shrink: 0;
font-size: 24rpx;
color: #999;
}
.join-box { .join-box {
display: flex; display: flex;
} }
@@ -96,20 +242,109 @@ function goDetail(item: any) {
color: #999; color: #999;
} }
} }
.join-title {
/* 积分商品卡片(参考 CRMEB 积分商城) */
.ig-img {
width: 100%;
height: 100%;
display: block;
}
.ig-grid2 {
display: flex; display: flex;
flex-wrap: wrap;
justify-content: space-between; justify-content: space-between;
align-items: center; }
.ig-g2-item {
width: 49%;
background: #fff; background: #fff;
height: 100rpx; border-radius: 16rpx;
padding: 0 20rpx; overflow: hidden;
.join-title-name { padding-bottom: 16rpx;
font-size: 32rpx; box-sizing: border-box;
font-weight: bold; margin-bottom: 16rpx;
} }
.join-title-more { .ig-g2-img-box {
font-size: 24rpx; width: 100%;
color: #999; height: 300rpx;
} background: #f5f5f5;
}
.ig-g2-name {
margin: 16rpx 16rpx 0;
font-size: 26rpx;
color: #282828;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ig-price-row {
display: flex;
align-items: baseline;
margin: 10rpx 16rpx 0;
min-width: 0;
}
.ig-price {
display: flex;
align-items: center;
font-size: 28rpx;
font-weight: bold;
color: #f5483a;
white-space: nowrap;
}
.ig-price-sm {
font-size: 24rpx;
margin: 10rpx 16rpx 0;
}
.ig-coin {
width: 28rpx;
height: 28rpx;
border-radius: 50%;
background: linear-gradient(135deg, #ffe29a, #f5a623);
color: #fff;
font-size: 18rpx;
font-weight: bold;
display: inline-flex;
align-items: center;
justify-content: center;
margin-right: 6rpx;
flex-shrink: 0;
}
.ig-coin-sm {
width: 24rpx;
height: 24rpx;
font-size: 16rpx;
}
.ig-original {
font-size: 22rpx;
color: #999;
margin-left: 10rpx;
white-space: nowrap;
text-decoration: line-through;
}
.ig-grid3 {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.ig-g3-item {
width: 32.5%;
background: #fff;
border-radius: 16rpx;
overflow: hidden;
padding-bottom: 16rpx;
box-sizing: border-box;
margin-bottom: 16rpx;
}
.ig-g3-img-box {
width: 100%;
height: 184rpx;
background: #f5f5f5;
}
.ig-g3-name {
margin: 8rpx 16rpx 0;
font-size: 24rpx;
color: #282828;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} }
</style> </style>

View File

@@ -1,19 +1,19 @@
<template> <template>
<view class="layout"> <view class="layout" :style="[wrapperStyle, gapStyle]">
<view class="view-height-150" @click="modelNavigateTo(res.list[0])"> <view class="view-height-150" :style="imgStyle" @click="modelNavigateTo(res.list[0])">
<u-image width="100%" height="340rpx" class="image-mode" :src="res.list[0].img"> <u-image width="100%" height="340rpx" class="image-mode" :src="res.list[0].img">
<template #loading><u-loading-icon></u-loading-icon></template> <template #loading><u-loading-icon></u-loading-icon></template>
</u-image> </u-image>
</view> </view>
<view class="view-height-150"> <view class="view-height-150">
<view class="view-height-75" @click="modelNavigateTo(res.list[1])"> <view class="view-height-75" :style="imgStyle" @click="modelNavigateTo(res.list[1])">
<u-image width="100%" height="170rpx" class="image-mode" :src="res.list[1].img" alt> <u-image width="100%" height="170rpx" class="image-mode" :src="res.list[1].img" alt>
<template #loading><u-loading-icon></u-loading-icon></template> <template #loading><u-loading-icon></u-loading-icon></template>
</u-image> </u-image>
</view> </view>
<view class="view-height-75" @click="modelNavigateTo(res.list[2])"> <view class="view-height-75" :style="imgStyle" @click="modelNavigateTo(res.list[2])">
<u-image width="100%" height="170rpx" class="image-mode" :src="res.list[2].img" alt> <u-image width="100%" height="170rpx" class="image-mode" :src="res.list[2].img" alt>
<template #loading><u-loading-icon></u-loading-icon></template> <template #loading><u-loading-icon></u-loading-icon></template>
</u-image> </u-image>
@@ -22,9 +22,14 @@
</view> </view>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { modelNavigateTo } from "./tpl"; import { computed } from "vue";
import { modelNavigateTo, cubeWrapperStyle, cubeImgStyle, cubeGapStyle } from "./tpl";
defineProps<{ res: any }>(); const props = defineProps<{ res: any }>();
const wrapperStyle = computed(() => cubeWrapperStyle(props.res));
const imgStyle = computed(() => cubeImgStyle(props.res));
const gapStyle = computed(() => cubeGapStyle(props.res));
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "./tpl.scss"; @import "./tpl.scss";
@@ -35,4 +40,7 @@ defineProps<{ res: any }>();
background-size: cover; background-size: cover;
} }
</style> .view-height-150 {
min-width: 0;
}
</style>

View File

@@ -1,24 +1,29 @@
<template> <template>
<div class="layout "> <div class="layout " :style="[wrapperStyle, gapStyle]">
<div class="view-height-150"> <div class="view-height-150">
<div class="view-height-75" @click="modelNavigateTo(res.list[0])"> <div class="view-height-75" :style="imgStyle" @click="modelNavigateTo(res.list[0])">
<u-image class="image-mode" width="100%" height="150rpx" :src="res.list[0].img" ></u-image> <u-image class="image-mode" width="100%" height="150rpx" :src="res.list[0].img" ></u-image>
</div> </div>
<div class="view-height-75" @click="modelNavigateTo(res.list[1])"> <div class="view-height-75" :style="imgStyle" @click="modelNavigateTo(res.list[1])">
<u-image class="image-mode" width="100%" height="150rpx" :src="res.list[1].img" ></u-image> <u-image class="image-mode" width="100%" height="150rpx" :src="res.list[1].img" ></u-image>
</div> </div>
</div> </div>
<div class="view-height-150" @click="modelNavigateTo(res.list[2])"> <div class="view-height-150" :style="imgStyle" @click="modelNavigateTo(res.list[2])">
<u-image class="image-mode" width="100%" height="300rpx" :src="res.list[2].img" ></u-image> <u-image class="image-mode" width="100%" height="300rpx" :src="res.list[2].img" ></u-image>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { modelNavigateTo } from "./tpl"; import { computed } from "vue";
import { modelNavigateTo, cubeWrapperStyle, cubeImgStyle, cubeGapStyle } from "./tpl";
defineProps<{ res: any }>(); const props = defineProps<{ res: any }>();
const wrapperStyle = computed(() => cubeWrapperStyle(props.res));
const imgStyle = computed(() => cubeImgStyle(props.res));
const gapStyle = computed(() => cubeGapStyle(props.res));
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "./tpl.scss"; @import "./tpl.scss";
@@ -30,5 +35,8 @@ defineProps<{ res: any }>();
background-size: cover; background-size: cover;
} }
.view-height-150 {
min-width: 0;
}
</style> </style>

View File

@@ -9,11 +9,20 @@ import { computed } from "vue";
const props = defineProps<{ res: any }>(); const props = defineProps<{ res: any }>();
const wrapperStyle = computed(() => ({ const wrapperStyle = computed(() => {
background: props.res?.bgColor || "#FFFFFF", const res = props.res || {};
padding: `0 ${props.res?.paddingX ?? 0}rpx`, const bg =
marginBottom: `${props.res?.pageGap ?? 10}rpx`, res.bgColor1 !== undefined
})); ? !res.bgColor2 || res.bgColor1 === res.bgColor2
? res.bgColor1
: `linear-gradient(180deg, ${res.bgColor1}, ${res.bgColor2})`
: res.bgColor || "#FFFFFF";
return {
background: bg,
padding: `0 ${res.paddingX ?? 0}rpx`,
marginBottom: `${res.pageGap ?? 10}rpx`,
};
});
const lineStyle = computed(() => ({ const lineStyle = computed(() => ({
borderTop: `${props.res?.lineHeight ?? 1}rpx ${props.res?.lineStyle || "dashed"} ${props.res?.lineColor || "#979797"}`, borderTop: `${props.res?.lineHeight ?? 1}rpx ${props.res?.lineStyle || "dashed"} ${props.res?.lineColor || "#979797"}`,

View File

@@ -1,31 +1,61 @@
<template> <template>
<view class="layout"> <view class="layout" :style="wrapperStyle">
<view class="menu-list"> <view
class="menu-list"
:class="{ styled: styled, single: isSingle }"
:style="listStyle"
>
<view <view
class="menu-item" class="menu-item"
:style="itemStyle"
@click="modelNavigateTo(item)" @click="modelNavigateTo(item)"
v-for="(item, index) in res.list" v-for="(item, index) in visibleList"
:key="index" :key="index"
> >
<view class="menu-img-wrap"> <view class="menu-img-wrap" :style="imgStyle">
<u-image <u-image width="88rpx" height="88rpx" class="menu-img" :src="item.img">
width="88rpx"
height="88rpx"
class="menu-img"
:src="item.img"
>
<template #loading><u-loading-icon></u-loading-icon></template> <template #loading><u-loading-icon></u-loading-icon></template>
</u-image> </u-image>
</view> </view>
<view class="menu-title">{{ item.title }}</view> <view class="menu-title" :style="titleStyle">{{ item.title }}</view>
</view> </view>
</view> </view>
</view> </view>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { modelNavigateTo } from "./tpl"; import { computed } from "vue";
import { modelNavigateTo, cubeWrapperStyle, cubeImgStyle } from "./tpl";
defineProps<{ res: any }>(); const props = defineProps<{ res: any }>();
// 面板保存过样式字段才启用自定义样式,老数据保持原样
const styled = computed(() => props.res && props.res.paddingTop !== undefined);
const wrapperStyle = computed(() => cubeWrapperStyle(props.res));
const imgStyle = computed(() => cubeImgStyle(props.res));
const listStyle = computed(() =>
styled.value ? { rowGap: `${Number(props.res.contentGap) || 0}rpx` } : {}
);
const titleStyle = computed(() =>
styled.value && props.res.textColor ? { color: props.res.textColor } : {}
);
// 展示设置:单行/多行、行数、每行个数,超出部分不展示
const isSingle = computed(
() => styled.value && props.res.menuDisplay === "single"
);
const cols = computed(() =>
styled.value ? Number(props.res.menuCols) || 4 : 4
);
const rows = computed(() => Number(props.res.menuRows) || 3);
const itemStyle = computed(() =>
styled.value ? { flex: `0 0 ${100 / cols.value}%` } : {}
);
const visibleList = computed(() => {
const list = props.res.list || [];
if (!styled.value) return list;
const limit = isSingle.value ? cols.value : rows.value * cols.value;
return list.slice(0, limit);
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "./tpl.scss"; @import "./tpl.scss";
@@ -34,17 +64,28 @@ defineProps<{ res: any }>();
flex-wrap: wrap; flex-wrap: wrap;
align-items: flex-start; align-items: flex-start;
&.single {
flex-wrap: nowrap;
overflow: hidden;
}
> .menu-item { > .menu-item {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: flex-start; justify-content: flex-start;
width: 20%; width: 25%;
flex: 0 0 20%; flex: 0 0 25%;
box-sizing: border-box; box-sizing: border-box;
margin: 20rpx 0; margin: 20rpx 0;
padding: 0 4rpx; padding: 0 4rpx;
} }
&.styled > .menu-item,
&.single > .menu-item {
margin: 0;
width: auto;
}
} }
.menu-img-wrap { .menu-img-wrap {
@@ -72,4 +113,4 @@ defineProps<{ res: any }>();
width: 100%; width: 100%;
text-align: center; text-align: center;
} }
</style> </style>

View File

@@ -1,55 +1,103 @@
<template> <template>
<div class="layout" v-if="list.length"> <view class="layout" :class="{ styled: styled }" v-if="texts.length" :style="wrapperStyle">
<div class="background"> <view class="notice-bar">
<u-notice-bar <image
direction="column" v-if="styled && item.img"
:bg-color="noticeStyle.bk_color" class="notice-icon"
:color="noticeStyle.color" :src="item.img"
:text="list" mode="aspectFit"
></u-notice-bar> />
</div> <view class="notice-body" :style="{ textAlign: alignValue }">
</div> <u-notice-bar
:direction="directionValue"
:bg-color="styled ? 'transparent' : item.bk_color"
:color="item.color"
:text="texts"
@click="clickNotice"
></u-notice-bar>
</view>
</view>
</view>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from "vue"; import { computed } from "vue";
import { modelNavigateTo } from "./tpl";
const props = defineProps<{ res: any }>(); const props = defineProps<{ res: any }>();
const noticeStyle = computed(() => props.res?.list?.[0] || {}); const item = computed(() => props.res?.list?.[0] || {});
const list = computed(() => { // 面板保存过样式字段才启用自定义样式,老数据保持原样
const titles = props.res?.list?.[0]?.title; const styled = computed(() => props.res && props.res.paddingX !== undefined);
if (!Array.isArray(titles)) return [];
return titles const wrapperStyle = computed(() => {
.map((item: { context?: string }) => (item?.context == null ? "" : String(item.context))) const res = props.res || {};
.filter((text: string) => text.length > 0); if (!styled.value) return {};
const bg =
!res.bgColor2 || res.bgColor1 === res.bgColor2
? res.bgColor1
: `linear-gradient(180deg, ${res.bgColor1}, ${res.bgColor2})`;
const radius = Number(res.borderRadius) || 0;
return {
padding: `${Number(res.paddingTop) || 0}rpx ${Number(res.paddingX) || 0}rpx ${Number(res.paddingBottom) || 0}rpx`,
background: bg,
borderRadius: `${radius}rpx`,
marginBottom: `${Number(res.pageGap) || 0}rpx`,
overflow: radius > 0 ? "hidden" : "visible",
};
}); });
const directionValue = computed(() =>
styled.value && item.value.direction === "horizontal" ? "row" : "column"
);
const alignValue = computed(() =>
styled.value ? item.value.textAlign || "left" : "left"
);
const titleList = computed(() => {
const titles = item.value?.title;
if (!Array.isArray(titles)) return [];
return titles.filter(
(t: any) => t && t.context != null && String(t.context).length > 0
);
});
const texts = computed(() =>
titleList.value.map((t: any) => String(t.context))
);
function clickNotice(index: number) {
const target = titleList.value[index];
if (target && target.url) {
modelNavigateTo(target);
}
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "./tpl.scss"; @import "./tpl.scss";
.background { .layout {
position: absolute; display: flex;
z-index: 2; align-items: center;
width: 100%; height: 84rpx;
height: 84rpx; background: #ffffff;
text-align: left; }
font-size: 20rpx; .layout.styled {
background-size: cover; height: auto;
} }
.layout { .notice-bar {
text-align: center; width: 100%;
position: relative; display: flex;
height: 84rpx; align-items: center;
display: flex; }
align-items: center; .notice-icon {
justify-content: center; width: 130rpx;
height: 36rpx;
background: #ffffff; flex-shrink: 0;
} margin: 0 16rpx;
.title { }
line-height: 84rpx; .notice-body {
font-size: 20px; flex: 1;
font-weight: bold; min-width: 0;
} }
</style> </style>

File diff suppressed because it is too large Load Diff

View File

@@ -9,13 +9,22 @@ import { computed } from "vue";
const props = defineProps<{ res: any }>(); const props = defineProps<{ res: any }>();
const wrapperStyle = computed(() => ({ const wrapperStyle = computed(() => {
background: props.res?.bgColor || "#FFFFFF", const res = props.res || {};
padding: `0 ${props.res?.paddingX ?? 12}rpx`, const bg =
marginBottom: `${props.res?.pageGap ?? 10}rpx`, res.bgColor1 !== undefined
borderRadius: `${props.res?.borderRadius ?? 0}rpx`, ? !res.bgColor2 || res.bgColor1 === res.bgColor2
overflow: "hidden", ? res.bgColor1
})); : `linear-gradient(180deg, ${res.bgColor1}, ${res.bgColor2})`
: res.bgColor || "#FFFFFF";
return {
background: bg,
padding: `0 ${res.paddingX ?? 12}rpx`,
marginBottom: `${res.pageGap ?? 10}rpx`,
borderRadius: `${res.borderRadius ?? 0}rpx`,
overflow: "hidden",
};
});
// 防止富文本内图片超宽撑破布局 // 防止富文本内图片超宽撑破布局
const processedHtml = computed(() => { const processedHtml = computed(() => {

View File

@@ -1,57 +1,123 @@
<template> <template>
<view class="layout"> <view class="layout" :style="wrapperStyle">
<view class="search" @click="handleSearch"> <view class="search" :style="boxStyle" @click="handleSearch">
<u-icon name="search"></u-icon> <u-icon name="search"></u-icon>
<text class="search-text">{{ res.list[0].title }}</text> <text class="search-text">{{ displayText }}</text>
</view> </view>
</view> </view>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const props = defineProps<{ res: any; storeId?: string }>(); import { computed, ref, watch, onBeforeUnmount } from "vue";
function handleSearch() { const props = defineProps<{ res: any; storeId?: string }>();
if (props.storeId) {
uni.navigateTo({ // 面板保存过样式字段才启用自定义样式,老数据保持原样
url: `/pages/navigation/search/searchPage?storeId=${props.storeId}`, const styled = computed(() => props.res && props.res.paddingX !== undefined);
});
} else { // 搜索热词轮播:有热词时按切换时间轮换展示,否则展示提示文字
uni.navigateTo({ const hotIndex = ref(0);
url: `/pages/navigation/search/searchPage`, let hotTimer: ReturnType<typeof setInterval> | null = null;
}); const validWords = computed(() =>
} (props.res.hotWords || []).filter((w: string) => w && w.trim())
} );
</script> const displayText = computed(() => {
<style lang="scss" scoped> if (validWords.value.length) {
@import "./tpl.scss"; return validWords.value[hotIndex.value % validWords.value.length];
}
.search { return props.res.list?.[0]?.title || "";
width: 100%; });
height: 64rpx;
border-radius: 10rpx; function stopHot() {
display: flex; if (hotTimer) {
align-items: center; clearInterval(hotTimer);
justify-content: flex-start; hotTimer = null;
padding: 0 10rpx; }
background: #ededed; }
color: #999;
overflow: hidden; watch(
box-sizing: border-box; () => [validWords.value.length, props.res.hotInterval],
} () => {
stopHot();
.search-text { hotIndex.value = 0;
margin-left: 12rpx; if (validWords.value.length > 1) {
font-size: 26rpx; hotTimer = setInterval(() => {
overflow: hidden; hotIndex.value = (hotIndex.value + 1) % validWords.value.length;
text-overflow: ellipsis; }, (Number(props.res.hotInterval) || 3) * 1000);
white-space: nowrap; }
} },
{ immediate: true }
.layout { );
display: flex;
align-items: center; onBeforeUnmount(stopHot);
width: 100%;
background: transparent; const wrapperStyle = computed(() => {
padding: 0; const res = props.res || {};
box-sizing: border-box; if (!styled.value) return {};
} const bg =
</style> !res.bgColor2 || res.bgColor1 === res.bgColor2
? res.bgColor1
: `linear-gradient(180deg, ${res.bgColor1}, ${res.bgColor2})`;
return {
padding: `0 ${Number(res.paddingX) || 0}rpx`,
background: bg,
borderRadius: `${Number(res.borderRadius) || 0}rpx`,
overflow: Number(res.borderRadius) > 0 ? "hidden" : "visible",
};
});
const boxStyle = computed(() => {
const res = props.res || {};
if (!styled.value) return {};
return {
background: res.boxColor || "#EDEDED",
color: res.textColor || "#999999",
borderRadius: `${Number(res.boxRadius) || 0}rpx`,
justifyContent: res.textAlign === "center" ? "center" : "flex-start",
};
});
function handleSearch() {
if (props.storeId) {
uni.navigateTo({
url: `/pages/navigation/search/searchPage?storeId=${props.storeId}`,
});
} else {
uni.navigateTo({
url: `/pages/navigation/search/searchPage`,
});
}
}
</script>
<style lang="scss" scoped>
@import "./tpl.scss";
.search {
width: 100%;
height: 64rpx;
border-radius: 10rpx;
display: flex;
align-items: center;
justify-content: flex-start;
padding: 0 10rpx;
background: #ededed;
color: #999;
overflow: hidden;
box-sizing: border-box;
}
.search-text {
margin-left: 12rpx;
font-size: 26rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.layout {
display: flex;
align-items: center;
width: 100%;
background: transparent;
padding: 0;
box-sizing: border-box;
}
</style>

View File

@@ -1,62 +1,357 @@
<template> <template>
<view class="tpl-shop" :style="wrapperStyle" v-if="shopList.length"> <view class="tpl-shop" :style="wrapperStyle" v-if="shopList.length">
<view class="shop-title" v-if="title">{{ title }}</view> <view class="shop-head" v-if="headIcon || title || moreUrl">
<view class="shop-grid"> <image v-if="headIcon" class="shop-head-icon" :src="headIcon" mode="aspectFit" />
<view class="shop-title" :style="titleStyle">{{ title }}</view>
<view v-if="moreUrl" class="shop-more" :style="moreStyle" @click="goMore">更多 &gt;</view>
</view>
<!-- 老数据:两列网格 -->
<view v-if="!styled" class="shop-grid-legacy">
<view <view
class="shop-item" class="shop-item"
v-for="item in shopList" v-for="item in shopList"
:key="item.id" :key="item.id"
@click="goShop(item)" @click="goShop(item)"
> >
<image class="shop-logo" :src="item.storeLogo" mode="aspectFill" /> <image
<view class="shop-name">{{ item.storeName }}</view> v-if="showLogo"
class="shop-logo"
:src="item.storeLogo"
mode="aspectFill"
/>
<view v-if="showName" class="shop-name">{{ item.storeName }}</view>
</view>
</view>
<!-- 样式一:店铺卡片(商品图背景 + Logo + 名称 + 类型标签) -->
<view v-else-if="shopStyle === 'one'" class="shop-grid">
<view
class="shop-card"
v-for="item in shopList"
:key="item.id"
@click="goShop(item)"
>
<image
v-if="(item.goodsImages && item.goodsImages.length) || item.storeBanner"
class="shop-card-bg"
:style="logoStyle"
:src="item.goodsImages && item.goodsImages.length ? item.goodsImages[0] : item.storeBanner"
mode="aspectFill"
/>
<view v-else class="shop-card-bg shop-card-bg-empty" :style="logoStyle"></view>
<view class="shop-card-body">
<image
v-if="showLogo"
class="shop-card-logo"
:src="item.storeLogo"
mode="aspectFit"
/>
<view v-if="showName" class="shop-card-name" :style="nameStyle">{{ item.storeName }}</view>
</view>
</view>
</view>
<!-- 样式二:店铺卡片 + 商品列表 -->
<view v-else class="shop-street-list">
<view
class="shop-street-card"
v-for="item in shopList"
:key="item.id"
@click="goShop(item)"
>
<view class="shop-street-head">
<image v-if="showLogo" class="shop-street-logo" :src="item.storeLogo" mode="aspectFit" />
<view v-if="showName" class="shop-street-name" :style="nameStyle">{{ item.storeName }}</view>
<image
v-if="(item.goods && item.goods.length) || item.storeBanner"
class="shop-street-banner"
:style="logoStyle"
:src="item.goods && item.goods.length ? item.goods[0].img : item.storeBanner"
mode="aspectFill"
/>
</view>
<view class="shop-street-goods">
<view
class="street-goods-item"
v-for="(g, gi) in item.goods || []"
:key="gi"
>
<image class="street-goods-img" :src="g.img" mode="aspectFill" />
<view class="street-goods-name">{{ g.name }}</view>
<view class="street-goods-price">
¥{{ goodsFormatPrice(g.price)[0] }}.{{ goodsFormatPrice(g.price)[1] }}
</view>
</view>
</view>
</view> </view>
</view> </view>
</view> </view>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted } from "vue"; import { ref, computed, watch } from "vue";
import { getStoreList } from "@/api/store.js"; import { getStoreList } from "@/api/store.js";
import { getGoodsList } from "@/api/goods.js";
import { goodsFormatPrice } from "@/utils/filters.js";
import { modelNavigateTo } from "./tpl";
const props = defineProps<{ res: any }>(); const props = defineProps<{ res: any }>();
const shopList = ref<any[]>([]); const shopList = ref<any[]>([]);
const title = computed(() => props.res?.list?.[0]?.title || ""); const first = computed(() => props.res?.list?.[0] || {});
const count = computed(() => props.res?.list?.[0]?.count || 4); const title = computed(() => first.value.title || "");
const count = computed(() => first.value.count || 4);
const headIcon = computed(() => first.value.icon || "");
const moreUrl = computed(() => first.value.moreUrl || "");
const showLogo = computed(() => first.value.showLogo !== false);
const showName = computed(() => first.value.showName !== false);
const shopStyle = computed(() => first.value.shopStyle || "one");
const wrapperStyle = computed(() => ({ // 面板保存过样式字段才启用自定义样式,老数据保持原样
background: props.res?.bgColor || "#FFFFFF", const styled = computed(() => props.res && props.res.paddingTop !== undefined);
padding: `20rpx ${props.res?.paddingX ?? 0}rpx`, const wrapperStyle = computed(() => {
marginBottom: `${props.res?.pageGap ?? 10}rpx`, const res = props.res || {};
borderRadius: `${props.res?.borderRadius ?? 0}rpx`, if (styled.value) {
overflow: "hidden", const bg =
})); !res.bgColor2 || res.bgColor1 === res.bgColor2
? res.bgColor1
: `linear-gradient(180deg, ${res.bgColor1}, ${res.bgColor2})`;
const radius = Number(res.borderRadius) || 0;
return {
background: bg,
padding: `${Number(res.paddingTop) || 0}rpx ${Number(res.paddingX) || 0}rpx ${Number(res.paddingBottom) || 0}rpx`,
marginBottom: `${Number(res.pageGap) || 0}rpx`,
borderRadius: `${radius}rpx`,
overflow: radius > 0 ? "hidden" : "visible",
};
}
return {
background: res.bgColor || "#FFFFFF",
padding: `20rpx ${res.paddingX ?? 0}rpx`,
marginBottom: `${res.pageGap ?? 10}rpx`,
borderRadius: `${res.borderRadius ?? 0}rpx`,
overflow: "hidden",
};
});
const titleStyle = computed(() =>
styled.value && props.res.titleColor ? { color: props.res.titleColor } : {}
);
const nameStyle = computed(() =>
styled.value && props.res.nameColor ? { color: props.res.nameColor } : {}
);
const moreStyle = computed(() =>
styled.value && props.res.moreColor ? { color: props.res.moreColor } : {}
);
const logoStyle = computed(() =>
styled.value ? { borderRadius: `${Number(props.res.imgRadius) || 0}rpx` } : {}
);
onMounted(() => { async function loadShops() {
getStoreList({ pageNumber: 1, pageSize: count.value }).then((res) => { const item = first.value;
if (item.dataMode === "custom" && Array.isArray(item.shopList) && item.shopList.length) {
shopList.value = item.shopList;
} else {
const res = await getStoreList({ pageNumber: 1, pageSize: count.value });
if (res.data.success && res.data.result) { if (res.data.success && res.data.result) {
shopList.value = (res.data.result.records || []).slice(0, count.value); shopList.value = (res.data.result.records || []).slice(0, count.value);
} }
}
loadGoodsImages();
}
// 样式二:按店铺拉取商品(图片、名称、价格)
function loadGoodsImages() {
if (shopStyle.value !== "two") return;
shopList.value.forEach((shop: any, i: number) => {
getGoodsList({ storeId: shop.id, pageNumber: 1, pageSize: 3 }).then((res: any) => {
const records = res?.data?.result?.records || res?.data?.result || [];
const goods = (Array.isArray(records) ? records : [])
.map((g: any) => ({
name: g.goodsName,
price: g.price,
img: g.thumbnail || g.small || g.goodsImage,
}))
.filter((g: any) => g.img);
shopList.value[i].goods = goods;
shopList.value[i].goodsImages = goods.map((g: any) => g.img);
}).catch(() => {});
}); });
}); }
watch(
() => [first.value.dataMode, first.value.shopList, count.value],
() => loadShops(),
{ immediate: true, deep: true }
);
function goShop(item: any) { function goShop(item: any) {
uni.navigateTo({ url: `/pages/product/shopPage?id=${item.id}` }); uni.navigateTo({ url: `/pages/product/shopPage?id=${item.id}` });
} }
function goMore() {
modelNavigateTo({ url: moreUrl.value });
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.shop-title { .shop-head {
font-size: 30rpx; display: flex;
font-weight: 600; align-items: center;
padding: 8rpx 24rpx 20rpx; padding: 8rpx 24rpx 20rpx;
} }
.shop-head-icon {
width: 124rpx;
height: 32rpx;
flex-shrink: 0;
margin-right: 12rpx;
}
.shop-title {
flex: 1;
min-width: 0;
font-size: 30rpx;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.shop-more {
flex-shrink: 0;
font-size: 24rpx;
color: #999;
}
// 样式一:三列店铺卡片
.shop-grid { .shop-grid {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
padding: 0 24rpx; padding: 0 24rpx;
gap: 16rpx;
}
.shop-card {
width: calc((100% - 32rpx) / 3);
border-radius: 12rpx;
overflow: hidden;
background: #f7f7f7;
}
.shop-card-bg {
width: 100%;
height: 140rpx;
display: block;
background: #eee;
}
.shop-card-bg-empty {
background: linear-gradient(135deg, #f5f5f5, #e8e8e8);
}
.shop-card-body {
padding: 8rpx;
text-align: center;
}
.shop-card-logo {
position: relative;
z-index: 1;
display: block;
width: 96rpx;
height: 32rpx;
margin: -16rpx auto 0;
background: #fff;
}
.shop-card-name {
font-size: 24rpx;
color: #333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-top: 6rpx;
}
// 样式二:店铺卡片 + 商品列表
.shop-street-list {
padding: 0 24rpx;
display: flex;
flex-direction: column;
gap: 16rpx;
}
.shop-street-card {
border-radius: 12rpx;
overflow: hidden;
background: #f7f7f7;
}
.shop-street-head {
position: relative;
display: flex;
align-items: center;
height: 68rpx;
padding: 0 16rpx;
box-sizing: border-box;
background: var(--theme-primary, #ff3c2a);
overflow: hidden;
}
.shop-street-logo {
flex-shrink: 0;
width: 88rpx;
height: 32rpx;
padding: 2rpx 6rpx;
box-sizing: border-box;
background: #fff;
border-radius: 4rpx;
}
.shop-street-name {
max-width: calc(100% - 220rpx);
margin-left: 12rpx;
color: #fff;
font-size: 26rpx;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.shop-street-banner {
position: absolute;
top: 0;
right: 0;
width: 160rpx;
height: 68rpx;
}
.shop-street-goods {
display: flex;
gap: 12rpx;
padding: 12rpx;
box-sizing: border-box;
}
.street-goods-item {
flex: 1;
min-width: 0;
border-radius: 8rpx;
overflow: hidden;
background: #fff;
}
.street-goods-img {
width: 100%;
height: 116rpx;
display: block;
background: #eee;
}
.street-goods-name {
padding: 8rpx 8rpx 0;
color: #282828;
font-size: 22rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.street-goods-price {
padding: 4rpx 8rpx 10rpx;
color: var(--theme-primary, #ff3c2a);
font-size: 22rpx;
font-weight: 600;
}
// 老数据两列网格(未配置样式时)
.shop-grid-legacy {
display: flex;
flex-wrap: wrap;
padding: 0 24rpx;
} }
.shop-item { .shop-item {
width: 50%; width: 50%;
@@ -68,10 +363,11 @@ function goShop(item: any) {
width: 72rpx; width: 72rpx;
height: 72rpx; height: 72rpx;
border-radius: 50%; border-radius: 50%;
background: #f0f0f0;
flex-shrink: 0; flex-shrink: 0;
} }
.shop-name { .shop-name {
flex: 1;
min-width: 0;
font-size: 26rpx; font-size: 26rpx;
color: #333; color: #333;
margin-left: 16rpx; margin-left: 16rpx;

View File

@@ -1,30 +1,30 @@
<template> <template>
<div class="layout"> <div class="layout" :style="[wrapperStyle, gapStyle]">
<div class="view-list"> <div class="view-list" :style="gapStyle">
<div class="view-item" @click="modelNavigateTo(res.list[0])"> <div class="view-item" @click="modelNavigateTo(res.list[0])">
<div class="-item-tilte">{{res.list[0].title}}</div> <div class="-item-tilte">{{res.list[0].title}}</div>
<div class="-item-image"> <div class="-item-image" :style="imgStyle">
<u-image width="120rpx" mode="scaleToFill" height="120rpx" :src="res.list[0].img" alt ></u-image> <u-image width="120rpx" mode="scaleToFill" height="120rpx" :src="res.list[0].img" alt ></u-image>
</div> </div>
</div> </div>
<div class="view-item" @click="modelNavigateTo(res.list[1])"> <div class="view-item" @click="modelNavigateTo(res.list[1])">
<div class="-item-tilte">{{res.list[1].title}}</div> <div class="-item-tilte">{{res.list[1].title}}</div>
<div class="-item-image"> <div class="-item-image" :style="imgStyle">
<u-image width="120rpx" mode="scaleToFill" height="120rpx" :src="res.list[1].img" alt ></u-image> <u-image width="120rpx" mode="scaleToFill" height="120rpx" :src="res.list[1].img" alt ></u-image>
</div> </div>
</div> </div>
</div> </div>
<div class="view-list"> <div class="view-list" :style="gapStyle">
<div class="view-item" @click="modelNavigateTo(res.list[2])"> <div class="view-item" @click="modelNavigateTo(res.list[2])">
<div class="-item-tilte">{{res.list[2].title}}</div> <div class="-item-tilte">{{res.list[2].title}}</div>
<div class="-item-image"> <div class="-item-image" :style="imgStyle">
<u-image width="120rpx" mode="scaleToFill" height="120rpx" :src="res.list[2].img" alt ></u-image> <u-image width="120rpx" mode="scaleToFill" height="120rpx" :src="res.list[2].img" alt ></u-image>
</div> </div>
</div> </div>
<div class="view-item" @click="modelNavigateTo(res.list[3])"> <div class="view-item" @click="modelNavigateTo(res.list[3])">
<div class="-item-tilte">{{res.list[3].title}}</div> <div class="-item-tilte">{{res.list[3].title}}</div>
<div class="-item-image"> <div class="-item-image" :style="imgStyle">
<u-image width="120rpx" mode="scaleToFill" height="120rpx" :src="res.list[3].img" alt ></u-image> <u-image width="120rpx" mode="scaleToFill" height="120rpx" :src="res.list[3].img" alt ></u-image>
</div> </div>
</div> </div>
@@ -33,9 +33,14 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { modelNavigateTo } from "./tpl"; import { computed } from "vue";
import { modelNavigateTo, cubeWrapperStyle, cubeImgStyle, cubeGapStyle } from "./tpl";
defineProps<{ res: any }>(); const props = defineProps<{ res: any }>();
const wrapperStyle = computed(() => cubeWrapperStyle(props.res));
const imgStyle = computed(() => cubeImgStyle(props.res));
const gapStyle = computed(() => cubeGapStyle(props.res));
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "./tpl.scss"; @import "./tpl.scss";
@@ -48,15 +53,15 @@ defineProps<{ res: any }>();
padding: 0; padding: 0;
} }
.-item-image { .-item-image {
padding: 10px; padding: 20rpx;
} }
.-item-tilte { .-item-tilte {
background: $aider-light-color; background: $aider-light-color;
height: 60rpx; height: 60rpx;
color: #fff; color: #fff;
font-size: 14px; font-size: 28rpx;
text-align: center; text-align: center;
line-height: 30px; line-height: 60rpx;
} }
.view-list { .view-list {
@@ -66,10 +71,11 @@ defineProps<{ res: any }>();
background: #fff; background: #fff;
border-top-left-radius: 0.8em; border-top-left-radius: 0.8em;
border-top-right-radius: 0.8em; border-top-right-radius: 0.8em;
border: 1px solid #ededed; border: 2rpx solid #ededed;
> .view-item { > .view-item {
width: 50%; flex: 1;
min-width: 0;
} }
> .view-item:nth-of-type(1) { > .view-item:nth-of-type(1) {
> .-item-tilte { > .-item-tilte {
@@ -82,4 +88,4 @@ defineProps<{ res: any }>();
} }
} }
} }
</style> </style>

View File

@@ -1,50 +1,149 @@
<template> <template>
<div class="layout" :style="{textAlign: res.list[0].textAlign}" @click="modelNavigateTo(res.list[0])" > <view class="layout" :style="wrapperStyle">
<div class="background" :style="{ backgroundColor: res.list[0].bk_color}"> <!-- 新样式:左标题+副标题,右侧更多 -->
<div class="title" :style="{ color: res.list[0].color }"> <view v-if="styled" class="title-bar" :style="barStyle">
{{ res.list[0].title }} <view class="title-left">
</div> <text class="title-main" :style="mainStyle">{{ item.title }}</text>
<div style="position: absolute;right: 10px;top:2px;color: #fff;line-height: 42px;font-size: 10px"> <text class="title-sub" :style="subStyle" v-if="item.title1">{{ item.title1 }}</text>
<a :style="{ color: res.list[0].color1 }" style="text-decoration: none">{{ res.list[0].title1 }}</a> </view>
</div> <view
</div> v-if="item.showMore"
</div> class="title-more"
</template> :style="moreStyle"
@click="goMore"
>
<script setup lang="ts"> {{ item.moreText || "更多" }} &gt;
import { modelNavigateTo } from "./tpl"; </view>
</view>
defineProps<{ res: any }>(); <!-- 老数据:保持原样 -->
</script> <view
<style lang="scss" scoped> v-else
@import "./tpl.scss"; class="background"
.background { :style="{ backgroundColor: item.bk_color, textAlign: item.textAlign }"
// background: url("/static/title.png") no-repeat; @click="modelNavigateTo(item)"
position: absolute; >
z-index: 2; <view class="title" :style="{ color: item.color }">{{ item.title }}</view>
width: 100%; <view class="legacy-sub">
height: 84rpx; <a :style="{ color: item.color1 }" style="text-decoration: none">{{ item.title1 }}</a>
background-position-x: center; </view>
background-position-y: center; </view>
background-size: cover; </view>
} </template>
.layout { <script setup lang="ts">
text-align: center; import { computed } from "vue";
position: relative; import { modelNavigateTo } from "./tpl";
height: 84rpx;
display: flex; const props = defineProps<{ res: any }>();
align-items: center;
justify-content: center; const item = computed(() => props.res?.list?.[0] || {});
background: #ffffff; // 面板保存过样式字段才启用新样式,老数据保持原样
} const styled = computed(() => props.res && props.res.paddingTop !== undefined);
.title { const wrapperStyle = computed(() => {
line-height: 84rpx; const res = props.res || {};
font-size: 30rpx; if (!styled.value) return {};
font-weight: bold; return {
margin-left: 8rpx; padding: `${Number(res.paddingTop) || 0}rpx ${Number(res.paddingX) || 0}rpx ${Number(res.paddingBottom) || 0}rpx`,
} marginBottom: `${Number(res.pageGap) || 0}rpx`,
</style> borderRadius: `${Number(res.radiusTop) || 0}rpx ${Number(res.radiusTop) || 0}rpx ${Number(res.radiusBottom) || 0}rpx ${Number(res.radiusBottom) || 0}rpx`,
overflow:
Number(res.radiusTop) > 0 || Number(res.radiusBottom) > 0 ? "hidden" : "visible",
background: "#ffffff",
boxSizing: "border-box",
};
});
const barStyle = computed(() => {
const it = item.value;
if (it.bgType === "image" && it.bgImage) {
return {
backgroundImage: `url(${it.bgImage})`,
backgroundSize: "cover",
backgroundPosition: "center",
};
}
return { backgroundColor: it.bk_color || "#FFFFFF" };
});
const mainStyle = computed(() => ({
color: item.value.color || "#282828",
fontSize: `${(Number(item.value.fontSize) || 20) * 2}rpx`,
fontWeight: item.value.textStyle === "bold" ? "bold" : "normal",
fontStyle: item.value.textStyle === "italic" ? "italic" : "normal",
}));
const subStyle = computed(() => ({
color: item.value.color1 || "#282828",
fontSize: `${(Number(item.value.subFontSize) || 14) * 2}rpx`,
}));
const moreStyle = computed(() => ({
color: item.value.moreColor || "#282828",
fontSize: `${(Number(item.value.moreFontSize) || 12) * 2}rpx`,
}));
function goMore() {
if (item.value.moreUrl) {
modelNavigateTo({ url: item.value.moreUrl });
}
}
</script>
<style lang="scss" scoped>
@import "./tpl.scss";
.layout {
background: #ffffff;
}
.title-bar {
width: 100%;
min-height: 84rpx;
display: flex;
align-items: center;
padding: 0 24rpx;
box-sizing: border-box;
}
.title-left {
flex: 1;
min-width: 0;
display: flex;
align-items: baseline;
overflow: hidden;
}
.title-main {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.title-sub {
margin-left: 12rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.title-more {
flex-shrink: 0;
}
.background {
position: relative;
z-index: 2;
width: 100%;
height: 84rpx;
background-size: cover;
display: flex;
align-items: center;
justify-content: center;
}
.legacy-sub {
position: absolute;
right: 20rpx;
top: 4rpx;
color: #fff;
line-height: 84rpx;
font-size: 20rpx;
}
.title {
line-height: 84rpx;
font-size: 30rpx;
font-weight: bold;
margin-left: 8rpx;
}
</style>

View File

@@ -1,18 +1,18 @@
<template> <template>
<div class="layout"> <div class="layout" :style="[wrapperStyle, gapStyle]">
<div class="view-width-100" @click="modelNavigateTo(res.list[0])"> <div class="view-width-100" :style="imgStyle" @click="modelNavigateTo(res.list[0])">
<u-image class="image-mode" width="100%" height="200rpx" :src="res.list[0].img"> <u-image class="image-mode" width="100%" height="200rpx" :src="res.list[0].img">
<template #loading><u-loading-icon></u-loading-icon></template> <template #loading><u-loading-icon></u-loading-icon></template>
</u-image> </u-image>
</div> </div>
<div class="view-width-100" @click="modelNavigateTo(res.list[1])"> <div class="view-width-100" :style="gapStyle" @click="modelNavigateTo(res.list[1])">
<div class="view-height-85"> <div class="view-height-85" :style="imgStyle">
<u-image class="image-mode" width="100%" height="170rpx" :src="res.list[1].img"> <u-image class="image-mode" width="100%" height="170rpx" :src="res.list[1].img">
<template #loading><u-loading-icon></u-loading-icon></template> <template #loading><u-loading-icon></u-loading-icon></template>
</u-image> </u-image>
</div> </div>
<div class="view-height-85" @click="modelNavigateTo(res.list[2])"> <div class="view-height-85" :style="imgStyle" @click="modelNavigateTo(res.list[2])">
<u-image class="image-mode" width="100%" height="170rpx" :src="res.list[2].img"> <u-image class="image-mode" width="100%" height="170rpx" :src="res.list[2].img">
<template #loading><u-loading-icon></u-loading-icon></template> <template #loading><u-loading-icon></u-loading-icon></template>
</u-image> </u-image>
@@ -22,9 +22,14 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { modelNavigateTo } from "./tpl"; import { computed } from "vue";
import { modelNavigateTo, cubeWrapperStyle, cubeImgStyle, cubeGapStyle } from "./tpl";
defineProps<{ res: any }>(); const props = defineProps<{ res: any }>();
const wrapperStyle = computed(() => cubeWrapperStyle(props.res));
const imgStyle = computed(() => cubeImgStyle(props.res));
const gapStyle = computed(() => cubeGapStyle(props.res));
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "./tpl.scss"; @import "./tpl.scss";
@@ -41,16 +46,17 @@ defineProps<{ res: any }>();
// height: 85px !important; // height: 85px !important;
// } // }
.view-width-100 { .view-width-100 {
padding: 1px 0; padding: 2rpx 0;
display: flex; display: flex;
height: 200rpx; height: 200rpx;
width: 100%; width: 100%;
} }
.view-height-85 { .view-height-85 {
padding: 0 1px; padding: 0 2rpx;
width: 50%; flex: 1;
min-width: 0;
} }
.image-mode{ .image-mode{
width: 100%; width: 100%;
} }
</style> </style>

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="layout"> <div class="layout" :style="[wrapperStyle, gapStyle]">
<div class="view-width-100"> <div class="view-width-100" :style="gapStyle">
<div class="view-height-85" @click="modelNavigateTo(res.list[0])"> <div class="view-height-85" :style="imgStyle" @click="modelNavigateTo(res.list[0])">
<u-image <u-image
class="image-mode" class="image-mode"
height="170rpx" height="170rpx"
@@ -12,7 +12,7 @@
<template #loading><u-loading-icon></u-loading-icon></template> <template #loading><u-loading-icon></u-loading-icon></template>
</u-image> </u-image>
</div> </div>
<div class="view-height-85" @click="modelNavigateTo(res.list[1])"> <div class="view-height-85" :style="imgStyle" @click="modelNavigateTo(res.list[1])">
<u-image <u-image
class="image-mode" class="image-mode"
height="170rpx" height="170rpx"
@@ -24,7 +24,7 @@
</u-image> </u-image>
</div> </div>
</div> </div>
<div class="view-width-100" @click="modelNavigateTo(res.list[2])"> <div class="view-width-100" :style="imgStyle" @click="modelNavigateTo(res.list[2])">
<u-image <u-image
class="image-mode" class="image-mode"
height="200rpx" height="200rpx"
@@ -38,9 +38,14 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { modelNavigateTo } from "./tpl"; import { computed } from "vue";
import { modelNavigateTo, cubeWrapperStyle, cubeImgStyle, cubeGapStyle } from "./tpl";
defineProps<{ res: any }>(); const props = defineProps<{ res: any }>();
const wrapperStyle = computed(() => cubeWrapperStyle(props.res));
const imgStyle = computed(() => cubeImgStyle(props.res));
const gapStyle = computed(() => cubeGapStyle(props.res));
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "./tpl.scss"; @import "./tpl.scss";
@@ -57,14 +62,15 @@ defineProps<{ res: any }>();
// height: 85px !important; // height: 85px !important;
// } // }
.view-width-100 { .view-width-100 {
padding: 1px 0; padding: 2rpx 0;
display: flex; display: flex;
height: 200rpx; height: 200rpx;
width: 100%; width: 100%;
} }
.view-height-85 { .view-height-85 {
padding: 0 1px; padding: 0 2rpx;
width: 50%; flex: 1;
min-width: 0;
} }
.image-mode{ .image-mode{
width: 100%; width: 100%;

View File

@@ -91,7 +91,26 @@ const item = computed(() => {
return data; return data;
}); });
// 面板保存过样式字段(options 级)才启用新样式,老数据走 item.style 体系
const styled = computed(() => props.res && props.res.paddingTop !== undefined);
const wrapperStyle = computed(() => { const wrapperStyle = computed(() => {
const res = props.res || {};
if (styled.value) {
const bg =
!res.bgColor2 || res.bgColor1 === res.bgColor2
? res.bgColor1
: `linear-gradient(180deg, ${res.bgColor1}, ${res.bgColor2})`;
const radius = Number(res.borderRadius) || 0;
return {
padding: `${Number(res.paddingTop) || 0}rpx ${Number(res.paddingX) || 0}rpx ${Number(res.paddingBottom) || 0}rpx`,
background: bg,
borderRadius: `${radius}rpx`,
marginBottom: `${Number(res.pageGap) || 0}rpx`,
overflow: radius > 0 ? "hidden" : "visible",
boxSizing: "border-box",
};
}
const style = item.value.style; const style = item.value.style;
return { return {
margin: `${style.margin || 0}px`, margin: `${style.margin || 0}px`,
@@ -102,6 +121,14 @@ const wrapperStyle = computed(() => {
}); });
const boxStyle = computed(() => { const boxStyle = computed(() => {
if (styled.value) {
return {
borderRadius: `${Number(props.res.imgRadius) || 0}rpx`,
overflow: "hidden",
position: "relative",
width: "100%",
};
}
const style = item.value.style; const style = item.value.style;
const box: Record<string, string> = { const box: Record<string, string> = {
borderRadius: `${style.bgRadius || 0}px`, borderRadius: `${style.bgRadius || 0}px`,