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>
<view class="home-page" :style="themeStyle">
<view class="home-page" :style="[themeStyle, pageBgStyle]">
<view
v-for="(item, index) in pageData.list"
:key="`${item.type}-${index}`"
@@ -105,6 +105,29 @@ const enableLoad = ref(false)
const loading = ref(false)
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(() => {
if (searchBarWidth.value) {
return { width: `${searchBarWidth.value}px` }
@@ -148,6 +171,10 @@ function init() {
pageData.value = Array.isArray(result?.list)
? { ...result, list: result.list.filter((i: any) => !i?.hidden) }
: { list: [] }
// 页面设置-内容:页面标题同步到导航栏
if (result?.title) {
uni.setNavigationBarTitle({ title: result.title })
}
const lastItem = pageData.value.list[pageData.value.list.length - 1]
enableLoad.value =
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>
<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-item"
@@ -8,7 +8,7 @@
@click="goDetail(item)"
>
<view class="article-text">
<view class="article-name">{{ item.title }}</view>
<view class="article-name" :style="nameStyle">{{ item.title }}</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 categoryId = computed(() => props.res?.list?.[0]?.categoryId || "");
const wrapperStyle = computed(() => ({
background: props.res?.bgColor || "#FFFFFF",
padding: `20rpx ${props.res?.paddingX ?? 0}rpx`,
marginBottom: `${props.res?.pageGap ?? 10}rpx`,
borderRadius: `${props.res?.borderRadius ?? 0}rpx`,
overflow: "hidden",
}));
// 面板保存过样式字段才启用自定义样式,老数据保持原样
const styled = computed(() => props.res && props.res.paddingTop !== undefined);
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 {
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(() => {
const params: any = { pageNumber: 1, pageSize: count.value };
@@ -61,6 +86,10 @@ function goDetail(item: any) {
padding: 16rpx 24rpx;
border-top: 2rpx solid #f5f5f5;
}
.tpl-article.styled .article-item {
border-top: none;
padding: 0;
}
.article-text {
flex: 1;
min-width: 0;

View File

@@ -1,55 +1,190 @@
<template>
<view class="layout">
<view class="banner-box">
<u-swiper
v-if="res && res.list && res.list.length"
class="home-banner-swiper"
:list="res.list"
keyName="img"
height="350rpx"
imgMode="scaleToFill"
interval="5000"
duration="500"
circular
indicator
@click="clickSwiper"
>
<template #loading><u-loading-icon></u-loading-icon></template>
</u-swiper>
</view>
</view>
</template>
<script setup lang="ts">
import { modelNavigateTo } from "./tpl";
const props = defineProps<{ res: any }>();
function clickSwiper(index: number) {
modelNavigateTo(props.res.list[index]);
}
</script>
<style lang="scss" scoped>
@import "./tpl.scss";
.banner-box {
width: 100%;
overflow: hidden;
}
: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>
<template>
<view class="layout" :style="wrapperStyle">
<view class="banner-box" :style="cardMode ? {} : boxStyle">
<u-swiper
v-if="!cardMode && res && res.list && res.list.length"
class="home-banner-swiper"
:list="res.list"
keyName="img"
height="350rpx"
imgMode="scaleToFill"
interval="5000"
duration="500"
circular
:indicator="!customIndicator"
@change="onChange"
@click="clickSwiper"
>
<template #loading><u-loading-icon></u-loading-icon></template>
</u-swiper>
<swiper
v-if="cardMode && res && res.list && res.list.length"
class="card-swiper"
autoplay
:interval="5000"
:duration="500"
circular
previous-margin="68rpx"
next-margin="68rpx"
@change="onChange"
>
<swiper-item v-for="(item, i) in res.list" :key="i">
<view class="card-item" :class="{ active: i === current }" @click="clickSwiper(i)">
<image class="card-img" :style="boxStyle" :src="item.img" mode="scaleToFill" />
</view>
</swiper-item>
</swiper>
<view
v-if="customIndicator && res.indicatorStyle !== 'none' && res.list.length > 1"
class="indicator"
:class="'pos-' + (res.indicatorPosition || 'center')"
>
<view
v-for="(item, i) in res.list"
:key="i"
class="dot"
:class="[res.indicatorStyle || 'round', { active: i === current }]"
:style="i === current ? { background: indicatorColor } : {}"
></view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
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 blankStyle = computed(() => ({
background: props.res?.bgColor || "#f5f5f5",
height: `${props.res?.height ?? 10}rpx`,
margin: `0 ${props.res?.paddingX ?? 0}rpx ${props.res?.pageGap ?? 10}rpx`,
}));
const blankStyle = computed(() => {
const res = props.res || {};
const bg =
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>

View File

@@ -1,32 +1,163 @@
<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-item"
v-for="item in couponList"
:key="item.id"
@click="receive(item)"
<!-- 老数据保持原样 -->
<block v-if="!styled">
<view
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 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
class="cp1-item"
v-for="item in couponList"
:key="item.id"
@click="receive(item)"
>
<view class="cp1-left">
<view class="cp1-amount">
<template v-if="item.couponType == 'DISCOUNT'">
<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 class="coupon-threshold">满{{ unitPrice(item.consumeThreshold) }}元可用</view>
</view>
<view class="coupon-right">
<view class="coupon-name">
{{ item.storeName == 'platform' ? '全平台通用' : item.storeName + '店铺' }}
<view class="cp1-right">
<text>领</text><text>取</text>
</view>
<view class="coupon-btn">立即领取</view>
<view class="cp-notch cp-notch-t"></view>
<view class="cp-notch cp-notch-b"></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>
</template>
@@ -41,15 +172,35 @@ const props = defineProps<{ res: any }>();
const couponList = ref<any[]>([]);
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",
padding: `20rpx ${props.res?.paddingX ?? 0}rpx`,
marginBottom: `${props.res?.pageGap ?? 10}rpx`,
borderRadius: `${props.res?.borderRadius ?? 0}rpx`,
overflow: "hidden",
}));
// 面板保存过样式字段才启用自定义样式,老数据保持原样
const styled = computed(() => props.res && props.res.paddingTop !== undefined);
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 {
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(() => {
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) {
if (!isLogin("auth")) {
navigateToLogin("redirectTo");
@@ -90,6 +246,9 @@ function receive(item: any) {
overflow: hidden;
background: linear-gradient(90deg, #ffece8, #fff5f4);
}
.tpl-coupon.styled .coupon-item {
margin: 0;
}
.coupon-left {
width: 180rpx;
padding: 24rpx 0;
@@ -124,4 +283,214 @@ function receive(item: any) {
padding: 8rpx 20rpx;
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>

View File

@@ -1,16 +1,21 @@
<template>
<div class="layout">
<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="">
<div class="layout" :style="[wrapperStyle, gapStyle]">
<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>
</u-image>
</div>
</template>
<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>
<style lang="scss" scoped>
@import "./tpl.scss";
@@ -20,4 +25,8 @@ defineProps<{ res: any }>();
justify-content: center;
background-size: cover;
}
</style>
.flex-img {
flex: 1;
min-width: 0;
}
</style>

View File

@@ -1,24 +1,33 @@
<template>
<div class="layout">
<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">
<div class="layout" :style="[wrapperStyle, gapStyle]">
<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>
</u-image>
</div>
</template>
<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>
<style lang="scss" scoped>
@import "./tpl.scss";
.layout {
display: flex;
align-items: center;
justify-content: center;
background-size: cover;
}
</style>
.flex-img {
flex: 1;
min-width: 0;
}
</style>

View File

@@ -1,16 +1,21 @@
<template>
<div class="layout">
<u-image @click="modelNavigateTo(item)" height="240rpx" width="240rpx" class="image-mode" :src="item.img" v-for="(item, index) in res.list" :key="index">
<div class="layout" :style="[wrapperStyle, gapStyle]">
<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>
</u-image>
</div>
</template>
<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>
<style lang="scss" scoped>
@import "./tpl.scss";
@@ -22,4 +27,8 @@ defineProps<{ res: any }>();
justify-content: center;
background-size: cover;
}
</style>
.flex-img {
flex: 1;
min-width: 0;
}
</style>

View File

@@ -1,12 +1,12 @@
<template>
<view class="layout">
<view class="flex-two">
<view class="flex-item" @click="modelNavigateTo(res.list[0])">
<view class="layout" :style="wrapperStyle">
<view class="flex-two" :style="gapStyle">
<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>
<template #loading><u-loading-icon></u-loading-icon></template>
</u-image>
</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>
<template #loading><u-loading-icon></u-loading-icon></template>
</u-image>
@@ -15,9 +15,14 @@
</view>
</template>
<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>
<style lang="scss" scoped>
@import "./tpl.scss";
@@ -27,6 +32,7 @@ defineProps<{ res: any }>();
overflow: hidden;
}
.flex-item {
width: 50%;
flex: 1;
min-width: 0;
}
</style>
</style>

View File

@@ -1,265 +1,678 @@
<template>
<view class="layout">
<u-sticky>
<view class="goods-cell-title">
<view
class="goods-item-title"
:class="{ 'selected-title': selected.index == index }"
@click="handleClickTitle(title, index)"
v-for="(title, index) in res.list[0].titleWay"
:key="index"
>
<h4 class="h4">{{ title.title }}</h4>
<view class="title-desc">{{ title.desc }}</view>
</view>
</view>
</u-sticky>
<view class="goods-list">
<template v-for="(item, item_index) in res.list[0].listWay" :key="item_index">
<view
v-if="
item.___index != undefined
? selected.index == item.___index
: selected.val == item.type
"
@click="handleClick(item)"
class="goods-item"
>
<view class="goods-img">
<u-image
:src="item.img"
height="350rpx"
mode="aspectFit"
width="100%"
>
<template #loading><u-loading-icon></u-loading-icon></template>
</u-image>
</view>
<view class="goods-desc">
<view class="goods-title">
{{ item.title }}
</view>
<view class="goods-bottom">
<view class="goods-price">
¥<span class="price-int"
>{{ goodsFormatPrice(item.price)[0] }} </span
>.{{ goodsFormatPrice(item.price)[1] }}
</view>
</view>
</view>
</view>
</template>
<template
v-if="currentTitleWay?.bindCategory && goodsData.length"
>
<view
v-for="(item, index) in goodsData"
:key="index"
class="goods-item"
@click="handleClick(item)"
>
<view class="goods-img">
<u-image
:src="item.thumbnail"
height="350rpx"
mode="aspectFit"
width="100%"
>
<template #loading><u-loading-icon></u-loading-icon></template>
</u-image>
</view>
<view class="goods-desc">
<view class="goods-title">
{{ item.goodsName }}
</view>
<view class="goods-bottom">
<view class="goods-price">
¥<span class="price-int"
>{{ goodsFormatPrice(item.price)[0] }} </span
>.{{ goodsFormatPrice(item.price)[1] }}
</view>
</view>
</view>
</view>
</template>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, reactive, watch, onMounted, onUnmounted, computed } from "vue";
import { getGoodsList } from "@/api/goods.js";
import { goodsFormatPrice } from "@/utils/filters.js";
const props = defineProps<{ res: any; enableBottomLoad?: boolean }>();
const selected = reactive({
index: 0,
val: "",
});
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
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 &&
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);
console.log(goodsData.value);
}
}
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%;
.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: 158px;
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>
<template>
<!-- 新版商品列表 -->
<view v-if="isNew" class="goods-new" :style="wrapperStyle">
<view v-if="displayGoods.length" class="g-list" :class="'mode-' + layout" :style="listStyle">
<view
class="g-item"
v-for="(g, i) in displayGoods"
:key="i"
:style="itemStyle"
@click="goDetail(g)"
>
<view class="g-imgbox">
<image :src="g.img" mode="aspectFill" class="g-img" />
</view>
<view class="g-info">
<view v-if="showName" class="g-name" :style="nameStyle">{{ g.name }}</view>
<view v-if="showPrice" class="g-price" :style="priceStyle">
¥{{ goodsFormatPrice(g.price)[0] }}.{{ goodsFormatPrice(g.price)[1] }}
</view>
<view class="g-bottom">
<text
v-if="showSales && g.sales != null"
class="g-sales"
:style="salesStyle"
>已售{{ g.sales }}件</text
>
<view
v-if="cartShow"
class="g-cart"
:class="'cart-' + cartStyle"
:style="{ background: priceColor }"
@click.stop="onCartBtn(g)"
>
<text v-if="cartStyle === 'text'" class="cart-text-label">加购</text>
<text v-else-if="cartStyle === 'plus'" class="cart-plus-label">+</text>
<u-icon v-else name="shopping-cart" color="#ffffff" size="28rpx"></u-icon>
</view>
</view>
</view>
</view>
</view>
<view v-else class="g-empty">暂无商品</view>
</view>
<!-- 旧版:分类标签 + 绑定商品 -->
<view v-else class="layout">
<u-sticky>
<view class="goods-cell-title">
<view
class="goods-item-title"
:class="{ 'selected-title': selected.index == index }"
@click="handleClickTitle(title, index)"
v-for="(title, index) in res.list[0].titleWay"
:key="index"
>
<h4 class="h4">{{ title.title }}</h4>
<view class="title-desc">{{ title.desc }}</view>
</view>
</view>
</u-sticky>
<view class="goods-list">
<template v-for="(item, item_index) in res.list[0].listWay" :key="item_index">
<view
v-if="
item.___index != undefined
? selected.index == item.___index
: selected.val == item.type
"
@click="handleClick(item)"
class="goods-item"
>
<view class="goods-img">
<u-image
:src="item.img"
height="350rpx"
mode="aspectFit"
width="100%"
>
<template #loading><u-loading-icon></u-loading-icon></template>
</u-image>
</view>
<view class="goods-desc">
<view class="goods-title">
{{ item.title }}
</view>
<view class="goods-bottom">
<view class="goods-price">
¥<span class="price-int"
>{{ goodsFormatPrice(item.price)[0] }} </span
>.{{ goodsFormatPrice(item.price)[1] }}
</view>
</view>
</view>
</view>
</template>
<template
v-if="currentTitleWay?.bindCategory && goodsData.length"
>
<view
v-for="(item, index) in goodsData"
:key="index"
class="goods-item"
@click="handleClick(item)"
>
<view class="goods-img">
<u-image
:src="item.thumbnail"
height="350rpx"
mode="aspectFit"
width="100%"
>
<template #loading><u-loading-icon></u-loading-icon></template>
</u-image>
</view>
<view class="goods-desc">
<view class="goods-title">
{{ item.goodsName }}
</view>
<view class="goods-bottom">
<view class="goods-price">
¥<span class="price-int"
>{{ goodsFormatPrice(item.price)[0] }} </span
>.{{ goodsFormatPrice(item.price)[1] }}
</view>
</view>
</view>
</view>
</template>
</view>
</view>
</template>
<script setup lang="ts">
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";
const props = defineProps<{ res: any; enableBottomLoad?: boolean }>();
const selected = reactive({
index: 0,
val: "",
});
// ---------- 新版商品列表 ----------
const first = computed(() => props.res?.list?.[0] || {});
const isNew = computed(() => first.value.layout !== undefined);
const layout = computed(() => first.value.layout || "list");
const showName = computed(() => first.value.showName !== false);
const showPrice = computed(() => first.value.showPrice !== false);
const showSales = computed(() => first.value.showSales !== false);
const cartShow = computed(() => first.value.cartShow !== false);
const cartStyle = computed(() => first.value.cartStyle || "plus");
const priceColor = computed(() =>
props.res?.toneMode === "custom" && props.res?.priceColor
? props.res.priceColor
: "var(--theme-primary, #ff3c2a)"
);
const priceStyle = computed(() => ({ color: priceColor.value }));
const nameStyle = computed(() =>
props.res?.titleColor ? { color: props.res.titleColor } : {}
);
const salesStyle = computed(() =>
props.res?.salesColor ? { color: props.res.salesColor } : {}
);
const rpx = (v: any) => `${(Number(v) || 0) * 2}rpx`;
const wrapperStyle = computed(() => {
const res = props.res || {};
const bg =
!res.bgColor2 || res.bgColor1 === res.bgColor2
? res.bgColor1 || "#ffffff"
: `linear-gradient(180deg, ${res.bgColor1}, ${res.bgColor2})`;
return {
background: bg,
padding: `${rpx(res.paddingTop)} ${rpx(res.paddingX)} ${rpx(res.paddingBottom)}`,
borderRadius: rpx(res.borderRadius),
overflow: Number(res.borderRadius) > 0 ? "hidden" : "visible",
};
});
const listStyle = computed(() => ({ gap: rpx(props.res?.contentGap ?? 10) }));
const itemStyle = computed(() => ({
borderRadius: rpx(props.res?.contentRadius ?? 7),
overflow: Number(props.res?.contentRadius ?? 7) > 0 ? "hidden" : "visible",
}));
const newGoodsData = ref<any[]>([]);
const displayGoods = computed(() => {
if (!isNew.value) return [];
return newGoodsData.value;
});
// 触底加载(新版)
const newParams = reactive({ pageNumber: 1 });
const newTotal = ref(0);
function buildQuery() {
const item = first.value;
const params: Record<string, any> = {
pageNumber: newParams.pageNumber,
pageSize: item.count || 6,
};
if (item.dataMode === "brand" && item.brandId) params.brandId = item.brandId;
if (item.dataMode === "store" && item.storeId) params.storeId = item.storeId;
if (item.dataMode === "category" && item.categoryId) params.categoryId = item.categoryId;
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;
}
if (item.sortType === "sales") {
params.sort = "buyCount";
params.order = "desc";
} else if (item.sortType === "price") {
params.sort = "price";
params.order = "asc";
}
return params;
}
async function loadNewGoods(reset = true) {
const params = buildQuery();
if (!params) {
newGoodsData.value = [];
newTotal.value = 0;
return;
}
try {
const res = await getGoodsList(params);
if (!res.data.success) return;
const result = res.data.result || {};
const records = result.records || [];
const mapped = records.map((g: any) => ({
id: g.id,
goodsId: g.goodsId,
name: g.goodsName,
price: g.price,
img: g.thumbnail || g.small,
sales: g.buyCount != null ? g.buyCount : null,
}));
if (reset) {
// 指定商品:按选择顺序排列
if (first.value.dataMode === "goods") {
const byGoodsId: Record<string, any> = {};
mapped.forEach((g: any) => {
byGoodsId[g.goodsId] = g;
});
newGoodsData.value = (first.value.goodsList || [])
.map((g: any) => byGoodsId[g.id])
.filter(Boolean);
} else {
newGoodsData.value = mapped;
}
} else {
newGoodsData.value.push(...mapped);
}
newTotal.value = result.totalElements ?? newGoodsData.value.length;
} catch (e) {
// 静默失败,保持已有数据
}
}
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>
<view class="layout" :style="wrapperStyle" v-if="goodsList.length">
<view class="join-list">
<view class="join-title" @click="goMore">
<view class="join-title-name">{{ title }}</view>
<view class="join-title-more">更多</view>
<view class="integral-head" :style="headStyle" @click="goMore">
<image
v-if="styled && options.titleType === 'image' && options.titleImage"
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 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 v-else-if="layoutStyle === '1'" class="ig-grid2">
<view
class="ig-g2-item"
v-for="(item, index) in goodsList"
:key="index"
@click="goDetail(item)"
>
<view class="ig-g2-img-box" :style="imgRadiusStyle">
<image class="ig-img" :src="item.goodsImage || item.thumbnail" mode="aspectFill" />
</view>
<view class="ig-g2-name">{{ item.goodsName || item.promotionName || '' }}</view>
<view class="ig-price-row">
<view class="ig-price" :style="pointStyle">
<text class="ig-coin">¥</text>{{ item.points != null ? item.points : 0 }}积分
</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>
@@ -39,16 +98,76 @@ const props = defineProps<{ res: any }>();
const goodsList = ref<any[]>([]);
const title = computed(() => props.res?.list?.[0]?.title || "积分商城");
const count = computed(() => props.res?.list?.[0]?.count || 3);
const options = computed(() => props.res || {});
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",
padding: `0 ${props.res?.paddingX ?? 0}rpx`,
marginBottom: `${props.res?.pageGap ?? 10}rpx`,
borderRadius: `${props.res?.borderRadius ?? 0}rpx`,
overflow: "hidden",
}));
// 面板保存过样式字段才启用新样式,老数据保持原样
const styled = computed(() => props.res && props.res.paddingTop !== undefined);
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 {
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(() => {
API_Promotions.getPointsGoods({
@@ -71,6 +190,33 @@ function goDetail(item: any) {
</script>
<style lang="scss" scoped>
@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 {
display: flex;
}
@@ -96,20 +242,109 @@ function goDetail(item: any) {
color: #999;
}
}
.join-title {
/* 积分商品卡片(参考 CRMEB 积分商城) */
.ig-img {
width: 100%;
height: 100%;
display: block;
}
.ig-grid2 {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
}
.ig-g2-item {
width: 49%;
background: #fff;
height: 100rpx;
padding: 0 20rpx;
.join-title-name {
font-size: 32rpx;
font-weight: bold;
}
.join-title-more {
font-size: 24rpx;
color: #999;
}
border-radius: 16rpx;
overflow: hidden;
padding-bottom: 16rpx;
box-sizing: border-box;
margin-bottom: 16rpx;
}
.ig-g2-img-box {
width: 100%;
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>

View File

@@ -1,19 +1,19 @@
<template>
<view class="layout">
<view class="view-height-150" @click="modelNavigateTo(res.list[0])">
<view class="layout" :style="[wrapperStyle, gapStyle]">
<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">
<template #loading><u-loading-icon></u-loading-icon></template>
</u-image>
</view>
<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>
<template #loading><u-loading-icon></u-loading-icon></template>
</u-image>
</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>
<template #loading><u-loading-icon></u-loading-icon></template>
</u-image>
@@ -22,9 +22,14 @@
</view>
</template>
<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>
<style lang="scss" scoped>
@import "./tpl.scss";
@@ -35,4 +40,7 @@ defineProps<{ res: any }>();
background-size: cover;
}
</style>
.view-height-150 {
min-width: 0;
}
</style>

View File

@@ -1,24 +1,29 @@
<template>
<div class="layout ">
<div class="layout " :style="[wrapperStyle, gapStyle]">
<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>
</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>
</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>
</div>
</div>
</template>
<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>
<style lang="scss" scoped>
@import "./tpl.scss";
@@ -30,5 +35,8 @@ defineProps<{ res: any }>();
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 wrapperStyle = computed(() => ({
background: props.res?.bgColor || "#FFFFFF",
padding: `0 ${props.res?.paddingX ?? 0}rpx`,
marginBottom: `${props.res?.pageGap ?? 10}rpx`,
}));
const wrapperStyle = computed(() => {
const res = props.res || {};
const bg =
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(() => ({
borderTop: `${props.res?.lineHeight ?? 1}rpx ${props.res?.lineStyle || "dashed"} ${props.res?.lineColor || "#979797"}`,

View File

@@ -1,31 +1,61 @@
<template>
<view class="layout">
<view class="menu-list">
<view class="layout" :style="wrapperStyle">
<view
class="menu-list"
:class="{ styled: styled, single: isSingle }"
:style="listStyle"
>
<view
class="menu-item"
:style="itemStyle"
@click="modelNavigateTo(item)"
v-for="(item, index) in res.list"
v-for="(item, index) in visibleList"
:key="index"
>
<view class="menu-img-wrap">
<u-image
width="88rpx"
height="88rpx"
class="menu-img"
:src="item.img"
>
<view class="menu-img-wrap" :style="imgStyle">
<u-image width="88rpx" height="88rpx" class="menu-img" :src="item.img">
<template #loading><u-loading-icon></u-loading-icon></template>
</u-image>
</view>
<view class="menu-title">{{ item.title }}</view>
<view class="menu-title" :style="titleStyle">{{ item.title }}</view>
</view>
</view>
</view>
</template>
<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>
<style lang="scss" scoped>
@import "./tpl.scss";
@@ -34,17 +64,28 @@ defineProps<{ res: any }>();
flex-wrap: wrap;
align-items: flex-start;
&.single {
flex-wrap: nowrap;
overflow: hidden;
}
> .menu-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
width: 20%;
flex: 0 0 20%;
width: 25%;
flex: 0 0 25%;
box-sizing: border-box;
margin: 20rpx 0;
padding: 0 4rpx;
}
&.styled > .menu-item,
&.single > .menu-item {
margin: 0;
width: auto;
}
}
.menu-img-wrap {
@@ -72,4 +113,4 @@ defineProps<{ res: any }>();
width: 100%;
text-align: center;
}
</style>
</style>

View File

@@ -1,55 +1,103 @@
<template>
<div class="layout" v-if="list.length">
<div class="background">
<u-notice-bar
direction="column"
:bg-color="noticeStyle.bk_color"
:color="noticeStyle.color"
:text="list"
></u-notice-bar>
</div>
</div>
<view class="layout" :class="{ styled: styled }" v-if="texts.length" :style="wrapperStyle">
<view class="notice-bar">
<image
v-if="styled && item.img"
class="notice-icon"
:src="item.img"
mode="aspectFit"
/>
<view class="notice-body" :style="{ textAlign: alignValue }">
<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>
<script setup lang="ts">
import { computed } from "vue";
import { modelNavigateTo } from "./tpl";
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;
if (!Array.isArray(titles)) return [];
return titles
.map((item: { context?: string }) => (item?.context == null ? "" : String(item.context)))
.filter((text: string) => text.length > 0);
// 面板保存过样式字段才启用自定义样式,老数据保持原样
const styled = computed(() => props.res && props.res.paddingX !== undefined);
const wrapperStyle = computed(() => {
const res = props.res || {};
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>
<style lang="scss" scoped>
@import "./tpl.scss";
.background {
position: absolute;
z-index: 2;
width: 100%;
height: 84rpx;
text-align: left;
font-size: 20rpx;
background-size: cover;
}
.layout {
text-align: center;
position: relative;
height: 84rpx;
display: flex;
align-items: center;
justify-content: center;
background: #ffffff;
}
.title {
line-height: 84rpx;
font-size: 20px;
font-weight: bold;
}
@import "./tpl.scss";
.layout {
display: flex;
align-items: center;
height: 84rpx;
background: #ffffff;
}
.layout.styled {
height: auto;
}
.notice-bar {
width: 100%;
display: flex;
align-items: center;
}
.notice-icon {
width: 130rpx;
height: 36rpx;
flex-shrink: 0;
margin: 0 16rpx;
}
.notice-body {
flex: 1;
min-width: 0;
}
</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 wrapperStyle = computed(() => ({
background: props.res?.bgColor || "#FFFFFF",
padding: `0 ${props.res?.paddingX ?? 12}rpx`,
marginBottom: `${props.res?.pageGap ?? 10}rpx`,
borderRadius: `${props.res?.borderRadius ?? 0}rpx`,
overflow: "hidden",
}));
const wrapperStyle = computed(() => {
const res = props.res || {};
const bg =
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 ?? 12}rpx`,
marginBottom: `${res.pageGap ?? 10}rpx`,
borderRadius: `${res.borderRadius ?? 0}rpx`,
overflow: "hidden",
};
});
// 防止富文本内图片超宽撑破布局
const processedHtml = computed(() => {

View File

@@ -1,57 +1,123 @@
<template>
<view class="layout">
<view class="search" @click="handleSearch">
<u-icon name="search"></u-icon>
<text class="search-text">{{ res.list[0].title }}</text>
</view>
</view>
</template>
<script setup lang="ts">
const props = defineProps<{ res: any; storeId?: string }>();
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>
<template>
<view class="layout" :style="wrapperStyle">
<view class="search" :style="boxStyle" @click="handleSearch">
<u-icon name="search"></u-icon>
<text class="search-text">{{ displayText }}</text>
</view>
</view>
</template>
<script setup lang="ts">
import { computed, ref, watch, onBeforeUnmount } from "vue";
const props = defineProps<{ res: any; storeId?: string }>();
// 面板保存过样式字段才启用自定义样式,老数据保持原样
const styled = computed(() => props.res && props.res.paddingX !== undefined);
// 搜索热词轮播:有热词时按切换时间轮换展示,否则展示提示文字
const hotIndex = ref(0);
let hotTimer: ReturnType<typeof setInterval> | null = null;
const validWords = computed(() =>
(props.res.hotWords || []).filter((w: string) => w && w.trim())
);
const displayText = computed(() => {
if (validWords.value.length) {
return validWords.value[hotIndex.value % validWords.value.length];
}
return props.res.list?.[0]?.title || "";
});
function stopHot() {
if (hotTimer) {
clearInterval(hotTimer);
hotTimer = null;
}
}
watch(
() => [validWords.value.length, props.res.hotInterval],
() => {
stopHot();
hotIndex.value = 0;
if (validWords.value.length > 1) {
hotTimer = setInterval(() => {
hotIndex.value = (hotIndex.value + 1) % validWords.value.length;
}, (Number(props.res.hotInterval) || 3) * 1000);
}
},
{ immediate: true }
);
onBeforeUnmount(stopHot);
const wrapperStyle = computed(() => {
const res = props.res || {};
if (!styled.value) return {};
const bg =
!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>
<view class="tpl-shop" :style="wrapperStyle" v-if="shopList.length">
<view class="shop-title" v-if="title">{{ title }}</view>
<view class="shop-grid">
<view class="shop-head" v-if="headIcon || title || moreUrl">
<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
class="shop-item"
v-for="item in shopList"
:key="item.id"
@click="goShop(item)"
>
<image class="shop-logo" :src="item.storeLogo" mode="aspectFill" />
<view class="shop-name">{{ item.storeName }}</view>
<image
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>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from "vue";
import { ref, computed, watch } from "vue";
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 shopList = ref<any[]>([]);
const title = computed(() => props.res?.list?.[0]?.title || "");
const count = computed(() => props.res?.list?.[0]?.count || 4);
const first = computed(() => props.res?.list?.[0] || {});
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",
padding: `20rpx ${props.res?.paddingX ?? 0}rpx`,
marginBottom: `${props.res?.pageGap ?? 10}rpx`,
borderRadius: `${props.res?.borderRadius ?? 0}rpx`,
overflow: "hidden",
}));
// 面板保存过样式字段才启用自定义样式,老数据保持原样
const styled = computed(() => props.res && props.res.paddingTop !== undefined);
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 {
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(() => {
getStoreList({ pageNumber: 1, pageSize: count.value }).then((res) => {
async function loadShops() {
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) {
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) {
uni.navigateTo({ url: `/pages/product/shopPage?id=${item.id}` });
}
function goMore() {
modelNavigateTo({ url: moreUrl.value });
}
</script>
<style lang="scss" scoped>
.shop-title {
font-size: 30rpx;
font-weight: 600;
.shop-head {
display: flex;
align-items: center;
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 {
display: flex;
flex-wrap: wrap;
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 {
width: 50%;
@@ -68,10 +363,11 @@ function goShop(item: any) {
width: 72rpx;
height: 72rpx;
border-radius: 50%;
background: #f0f0f0;
flex-shrink: 0;
}
.shop-name {
flex: 1;
min-width: 0;
font-size: 26rpx;
color: #333;
margin-left: 16rpx;

View File

@@ -1,30 +1,30 @@
<template>
<div class="layout">
<div class="view-list">
<div class="layout" :style="[wrapperStyle, gapStyle]">
<div class="view-list" :style="gapStyle">
<div class="view-item" @click="modelNavigateTo(res.list[0])">
<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>
</div>
</div>
<div class="view-item" @click="modelNavigateTo(res.list[1])">
<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>
</div>
</div>
</div>
<div class="view-list">
<div class="view-list" :style="gapStyle">
<div class="view-item" @click="modelNavigateTo(res.list[2])">
<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>
</div>
</div>
<div class="view-item" @click="modelNavigateTo(res.list[3])">
<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>
</div>
</div>
@@ -33,9 +33,14 @@
</template>
<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>
<style lang="scss" scoped>
@import "./tpl.scss";
@@ -48,15 +53,15 @@ defineProps<{ res: any }>();
padding: 0;
}
.-item-image {
padding: 10px;
padding: 20rpx;
}
.-item-tilte {
background: $aider-light-color;
height: 60rpx;
color: #fff;
font-size: 14px;
font-size: 28rpx;
text-align: center;
line-height: 30px;
line-height: 60rpx;
}
.view-list {
@@ -66,10 +71,11 @@ defineProps<{ res: any }>();
background: #fff;
border-top-left-radius: 0.8em;
border-top-right-radius: 0.8em;
border: 1px solid #ededed;
border: 2rpx solid #ededed;
> .view-item {
width: 50%;
flex: 1;
min-width: 0;
}
> .view-item:nth-of-type(1) {
> .-item-tilte {
@@ -82,4 +88,4 @@ defineProps<{ res: any }>();
}
}
}
</style>
</style>

View File

@@ -1,50 +1,149 @@
<template>
<div class="layout" :style="{textAlign: res.list[0].textAlign}" @click="modelNavigateTo(res.list[0])" >
<div class="background" :style="{ backgroundColor: res.list[0].bk_color}">
<div class="title" :style="{ color: res.list[0].color }">
{{ res.list[0].title }}
</div>
<div style="position: absolute;right: 10px;top:2px;color: #fff;line-height: 42px;font-size: 10px">
<a :style="{ color: res.list[0].color1 }" style="text-decoration: none">{{ res.list[0].title1 }}</a>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { modelNavigateTo } from "./tpl";
defineProps<{ res: any }>();
</script>
<style lang="scss" scoped>
@import "./tpl.scss";
.background {
// background: url("/static/title.png") no-repeat;
position: absolute;
z-index: 2;
width: 100%;
height: 84rpx;
background-position-x: center;
background-position-y: center;
background-size: cover;
}
.layout {
text-align: center;
position: relative;
height: 84rpx;
display: flex;
align-items: center;
justify-content: center;
background: #ffffff;
}
.title {
line-height: 84rpx;
font-size: 30rpx;
font-weight: bold;
margin-left: 8rpx;
}
</style>
<template>
<view class="layout" :style="wrapperStyle">
<!-- 新样式:左标题+副标题,右侧更多 -->
<view v-if="styled" class="title-bar" :style="barStyle">
<view class="title-left">
<text class="title-main" :style="mainStyle">{{ item.title }}</text>
<text class="title-sub" :style="subStyle" v-if="item.title1">{{ item.title1 }}</text>
</view>
<view
v-if="item.showMore"
class="title-more"
:style="moreStyle"
@click="goMore"
>
{{ item.moreText || "更多" }} &gt;
</view>
</view>
<!-- 老数据:保持原样 -->
<view
v-else
class="background"
:style="{ backgroundColor: item.bk_color, textAlign: item.textAlign }"
@click="modelNavigateTo(item)"
>
<view class="title" :style="{ color: item.color }">{{ item.title }}</view>
<view class="legacy-sub">
<a :style="{ color: item.color1 }" style="text-decoration: none">{{ item.title1 }}</a>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { computed } from "vue";
import { modelNavigateTo } from "./tpl";
const props = defineProps<{ res: any }>();
const item = computed(() => props.res?.list?.[0] || {});
// 面板保存过样式字段才启用新样式,老数据保持原样
const styled = computed(() => props.res && props.res.paddingTop !== undefined);
const wrapperStyle = computed(() => {
const res = props.res || {};
if (!styled.value) return {};
return {
padding: `${Number(res.paddingTop) || 0}rpx ${Number(res.paddingX) || 0}rpx ${Number(res.paddingBottom) || 0}rpx`,
marginBottom: `${Number(res.pageGap) || 0}rpx`,
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>
<div class="layout">
<div class="view-width-100" @click="modelNavigateTo(res.list[0])">
<div class="layout" :style="[wrapperStyle, gapStyle]">
<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">
<template #loading><u-loading-icon></u-loading-icon></template>
</u-image>
</div>
<div class="view-width-100" @click="modelNavigateTo(res.list[1])">
<div class="view-height-85">
<div class="view-width-100" :style="gapStyle" @click="modelNavigateTo(res.list[1])">
<div class="view-height-85" :style="imgStyle">
<u-image class="image-mode" width="100%" height="170rpx" :src="res.list[1].img">
<template #loading><u-loading-icon></u-loading-icon></template>
</u-image>
</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">
<template #loading><u-loading-icon></u-loading-icon></template>
</u-image>
@@ -22,9 +22,14 @@
</template>
<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>
<style lang="scss" scoped>
@import "./tpl.scss";
@@ -41,16 +46,17 @@ defineProps<{ res: any }>();
// height: 85px !important;
// }
.view-width-100 {
padding: 1px 0;
padding: 2rpx 0;
display: flex;
height: 200rpx;
width: 100%;
}
.view-height-85 {
padding: 0 1px;
width: 50%;
padding: 0 2rpx;
flex: 1;
min-width: 0;
}
.image-mode{
width: 100%;
}
</style>
</style>

View File

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

View File

@@ -91,7 +91,26 @@ const item = computed(() => {
return data;
});
// 面板保存过样式字段(options 级)才启用新样式,老数据走 item.style 体系
const styled = computed(() => props.res && props.res.paddingTop !== undefined);
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;
return {
margin: `${style.margin || 0}px`,
@@ -102,6 +121,14 @@ const wrapperStyle = 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 box: Record<string, string> = {
borderRadius: `${style.bgRadius || 0}px`,