Merge branch 'feat/appointment-goods'

This commit is contained in:
pikachu1995@126.com
2026-10-21 08:25:10 +08:00
94 changed files with 6672 additions and 6293 deletions

View File

@@ -1,8 +1,8 @@
<template>
<view :style="themeStyle" class="content">
<view class="portrait-box">
<image src="/static/pointTrade/point_bg_1.png" mode=""></image>
<image class="point-img" src="/static/pointTrade/tradehall.png" />
<image src="/pages/mine/static/point_bg_1.png" mode=""></image>
<image class="point-img" src="/pages/mine/static/tradehall.png" />
<view class="position-point">

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 742 B

View File

@@ -35,7 +35,7 @@
<view class="select-view">
<view class="select-cell" v-if="applyInfo.returnGoods" @click="onSelect(1)">
<view class="select-image">
<image class="select-icon" src="/static/order/t1.png"></image>
<image class="select-icon" src="/pages/order/static/t1.png"></image>
</view>
<view class="cell-body">
<view class="select-title">退货</view>
@@ -45,7 +45,7 @@
</view>
<view class="select-cell" v-if="applyInfo.returnMoney" @click="onSelect(3)">
<view class="select-image">
<image class="select-icon" src="/static/order/t3.png"></image>
<image class="select-icon" src="/pages/order/static/t3.png"></image>
</view>
<view class="cell-body">
<view class="select-title">退款</view>

BIN
pages/order/static/t1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 952 B

BIN
pages/order/static/t3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 617 B

View File

@@ -113,7 +113,7 @@
<u-icon v-if="item.title != 'APPLE'" color="#fff" size="42" :name="item.icon"
@click="navigateLogin(item)">
</u-icon>
<u-image v-else src="/static/appleidButton@2x.png" :lazy-load="false" @click="navigateLogin(item)"
<u-image v-else src="/pages/passport/static/appleidButton@2x.png" :lazy-load="false" @click="navigateLogin(item)"
width="80" height="80" />
</div>
</template>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -303,7 +303,7 @@ page {
.star-item {
width: 28rpx;
height: 28rpx;
background: url("/static/star.png");
background: url("/pages/product/static/star.png");
background-size: 100%;
opacity: 0.25;

View File

@@ -1,5 +1,5 @@
.group-wrapper {
background: url("/static/exchange.png");
background: url("/pages/product/static/exchange.png");
background-size: cover;
}
.u-group-row {

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -27,7 +27,7 @@
</u-navbar>
<!-- 顶部栏 -->
<view class="header-wraper">
<image class="header-banner" mode="aspectFit" src="/static/join-buy.png"></image>
<image class="header-banner" mode="aspectFit" src="/pages/promotion/static/join-buy.png"></image>
</view>
<!-- 商品栏 -->
<div class="swiper">
@@ -156,7 +156,7 @@ function GET_AssembleGoods() {
}
.header-wraper {
background: url('/static/bg.png') no-repeat center center;
background: url('/pages/promotion/static/bg.png') no-repeat center center;
background-size: cover;
height: 200rpx;
display: flex;

View File

@@ -32,7 +32,7 @@ async function init() {
padding: 0 20rpx;
height: 300rpx;
flex-shrink: 0;
background: url("/static/point-bg.png") no-repeat;
background: url("/pages/promotion/static/point-bg.png") no-repeat;
background-size: 100%;
display: flex;
align-items: center;

View File

@@ -9,7 +9,7 @@
@left-click="back"
></u-navbar>
<view class="header-wraper">
<image class="header-banner" mode="aspectFit" src="/static/seckill.png"></image>
<image class="header-banner" mode="aspectFit" src="/pages/promotion/static/seckill.png"></image>
</view>
<scroll-view scroll-x>
<view class="index-navs">
@@ -176,7 +176,7 @@ function clickNavigateTime(type: number) {
}
.header-wraper {
background: url('/static/bg.png') no-repeat center center;
background: url('/pages/promotion/static/bg.png') no-repeat center center;
background-size: cover;
height: 200rpx;
display: flex;

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -1,208 +1,262 @@
<template>
<view class="home-page" :style="themeStyle">
<view
v-for="(item, index) in pageData.list"
:key="`${item.type}-${index}`"
class="floor-block"
>
<u-navbar
class="navbar"
v-if="item.type == 'search'"
:auto-back="false"
:fixed="index === 1 ? false : true"
:placeholder="index === 1 ? false : true"
left-icon=""
title=""
bg-color="#ffffff"
>
<template #left>
<view class="navbar-search-wrap" :style="searchBarStyle" @click.stop>
<search :res="item.options" />
</view>
</template>
</u-navbar>
<carousel v-if="item.type == 'carousel'" :res="item.options" />
<titleLayout v-if="item.type == 'title'" :res="item.options" />
<leftOneRightTwo v-if="item.type == 'leftOneRightTwo'" :res="item.options" />
<leftTwoRightOne v-if="item.type == 'leftTwoRightOne'" :res="item.options" />
<topOneBottomTwo v-if="item.type == 'topOneBottomTwo'" :res="item.options" />
<topTwoBottomOne v-if="item.type == 'topTwoBottomOne'" :res="item.options" />
<flexThree v-if="item.type == 'flexThree'" :res="item.options" />
<flexFive v-if="item.type == 'flexFive'" :res="item.options" />
<flexFour v-if="item.type == 'flexFour'" :res="item.options" />
<flexTwo v-if="item.type == 'flexTwo'" :res="item.options" />
<textPicture v-if="item.type == 'textPicture'" :res="item.options" />
<menuLayout v-if="item.type == 'menu'" :res="item.options" />
<flexOne v-if="item.type == 'flexOne'" :res="item.options" />
<goods
v-if="item.type == 'goods'"
:enableBottomLoad="enableLoad"
:res="item.options"
/>
<group v-if="item.type == 'group'" :res="item.options" />
<notice v-if="item.type == 'notice'" :res="item.options" />
<promotions v-if="item.type == 'promotionDetail'" :res="item.options" />
<videoLayout v-if="item.type == 'video'" :res="item.options" />
</view>
<fetchCoupon ref="couponRef" />
</view>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import {
onShow,
onReady,
onReachBottom,
onPullDownRefresh,
} from '@dcloudio/uni-app'
import { useStore } from '@/store'
import { getThemeStyle } from '@/utils/theme'
import carousel from '@/pages/tabbar/home/template/tpl_banner.vue'
import titleLayout from '@/pages/tabbar/home/template/tpl_title.vue'
import leftOneRightTwo from '@/pages/tabbar/home/template/tpl_left_one_right_two.vue'
import leftTwoRightOne from '@/pages/tabbar/home/template/tpl_left_two_right_one.vue'
import topOneBottomTwo from '@/pages/tabbar/home/template/tpl_top_one_bottom_two.vue'
import topTwoBottomOne from '@/pages/tabbar/home/template/tpl_top_two_bottom_one.vue'
import flexOne from '@/pages/tabbar/home/template/tpl_flex_one.vue'
import flexTwo from '@/pages/tabbar/home/template/tpl_flex_two.vue'
import flexThree from '@/pages/tabbar/home/template/tpl_flex_three.vue'
import flexFive from '@/pages/tabbar/home/template/tpl_flex_five.vue'
import flexFour from '@/pages/tabbar/home/template/tpl_flex_four.vue'
import textPicture from '@/pages/tabbar/home/template/tpl_text_picture.vue'
import menuLayout from '@/pages/tabbar/home/template/tpl_menu.vue'
import search from '@/pages/tabbar/home/template/tpl_search.vue'
import group from '@/pages/tabbar/home/template/tpl_group.vue'
import videoLayout from '@/pages/tabbar/home/template/tpl_video.vue'
import goods from '@/pages/tabbar/home/template/tpl_goods.vue'
import notice from '@/pages/tabbar/home/template/tpl_notice.vue'
import promotions from '@/pages/tabbar/home/template/tpl_promotions_detail.vue'
import fetchCoupon from '@/pages/tabbar/home/template/fetch_coupon.vue'
import { getFloorData } from '@/api/home'
const HOME_LAYOUT_PADDING_RPX = 16
const store = useStore()
const themeStyle = computed(() => getThemeStyle(store.state.theme))
const couponRef = ref<InstanceType<typeof fetchCoupon>>()
const pageData = ref<{ list: any[] }>({ list: [] })
const enableLoad = ref(false)
const loading = ref(false)
const searchBarWidth = ref(0)
const searchBarStyle = computed(() => {
if (searchBarWidth.value) {
return { width: `${searchBarWidth.value}px` }
}
return { width: '100%' }
})
function setCapsuleInset() {
try {
const { windowWidth } = uni.getWindowInfo()
const leftPadding = uni.upx2px(HOME_LAYOUT_PADDING_RPX)
const rightPadding = uni.upx2px(HOME_LAYOUT_PADDING_RPX)
// #ifdef MP-WEIXIN
const menuButton = uni.getMenuButtonBoundingClientRect()
searchBarWidth.value = windowWidth - (windowWidth - menuButton.left) - leftPadding
// #endif
// #ifndef MP-WEIXIN
searchBarWidth.value = windowWidth - leftPadding - rightPadding
// #endif
} catch (e) {
searchBarWidth.value = 0
}
}
function fetchCouponFn() {
couponRef.value?.firstGetAuto()
}
function init() {
if (loading.value) return
loading.value = true
getFloorData()
.then((res) => {
const payload = res?.data != null ? res.data : res
const pageDataStr = payload?.result?.pageData
const isSuccess = payload?.success === true || payload?.code === 200
if (!isSuccess || !pageDataStr) return
try {
const result = JSON.parse(pageDataStr)
pageData.value = Array.isArray(result?.list) ? result : { list: [] }
const lastItem = pageData.value.list[pageData.value.list.length - 1]
enableLoad.value =
lastItem?.type === 'goods' || lastItem?.model === 'goods'
} catch (parseError) {
console.error('parse pageData failed', parseError)
}
})
.catch((err) => {
console.error('getFloorData failed', err)
})
.finally(() => {
loading.value = false
})
}
onReady(() => {
setCapsuleInset()
init()
// #ifdef MP-WEIXIN
uni.showShareMenu({ withShareTicket: true })
// #endif
})
onShow(() => {
setTimeout(() => {
fetchCouponFn()
}, 1000)
})
onReachBottom(() => {
uni.$emit('onReachBottom', true)
})
onPullDownRefresh(() => {
init()
uni.stopPullDownRefresh()
})
</script>
<style scoped lang="scss">
@import '@/pages/tabbar/home/template/tpl.scss';
.home-page {
width: 100%;
min-height: 100%;
background: #f9f9f9;
}
.floor-block {
width: 100%;
display: block;
}
.navbar {
width: 100%;
:deep(.u-navbar__content) {
width: 100%;
}
:deep(.u-navbar__content__left) {
flex: 1;
width: auto !important;
max-width: 100%;
padding: 0 0 0 $home-layout-padding !important;
}
}
.navbar-search-wrap {
box-sizing: border-box;
flex: 1;
min-width: 0;
}
</style>
<template>
<view class="home-page" :style="[themeStyle, pageBgStyle]">
<view
v-for="(item, index) in pageData.list"
:key="`${item.type}-${index}`"
class="floor-block"
>
<u-navbar
class="navbar"
v-if="item.type == 'search'"
:auto-back="false"
:fixed="index === 1 ? false : true"
:placeholder="index === 1 ? false : true"
left-icon=""
title=""
:bg-color="topColor"
>
<template #left>
<view class="navbar-search-wrap" :style="searchBarStyle" @click.stop>
<search :res="item.options" />
</view>
</template>
</u-navbar>
<homeComb v-if="item.type == 'homeComb'" :res="item.options" />
<carousel v-if="item.type == 'carousel'" :res="item.options" />
<titleLayout v-if="item.type == 'title'" :res="item.options" />
<leftOneRightTwo v-if="item.type == 'leftOneRightTwo'" :res="item.options" />
<leftTwoRightOne v-if="item.type == 'leftTwoRightOne'" :res="item.options" />
<topOneBottomTwo v-if="item.type == 'topOneBottomTwo'" :res="item.options" />
<topTwoBottomOne v-if="item.type == 'topTwoBottomOne'" :res="item.options" />
<flexThree v-if="item.type == 'flexThree'" :res="item.options" />
<flexFive v-if="item.type == 'flexFive'" :res="item.options" />
<flexFour v-if="item.type == 'flexFour'" :res="item.options" />
<flexTwo v-if="item.type == 'flexTwo'" :res="item.options" />
<textPicture v-if="item.type == 'textPicture'" :res="item.options" />
<menuLayout v-if="item.type == 'menu'" :res="item.options" />
<flexOne v-if="item.type == 'flexOne'" :res="item.options" />
<goods
v-if="item.type == 'goods'"
:enableBottomLoad="enableLoad"
:res="item.options"
/>
<group v-if="item.type == 'group'" :res="item.options" />
<notice v-if="item.type == 'notice'" :res="item.options" />
<promotions v-if="item.type == 'promotionDetail'" :res="item.options" />
<videoLayout v-if="item.type == 'video'" :res="item.options" />
<blank v-if="item.type == 'blank'" :res="item.options" />
<lineLayout v-if="item.type == 'line'" :res="item.options" />
<richText v-if="item.type == 'richText'" :res="item.options" />
<integral v-if="item.type == 'integral'" :res="item.options" />
<coupon v-if="item.type == 'coupon'" :res="item.options" />
<articleList v-if="item.type == 'article'" :res="item.options" />
<shopList v-if="item.type == 'shop'" :res="item.options" />
</view>
<fetchCoupon ref="couponRef" />
</view>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import {
onShow,
onReady,
onReachBottom,
onPullDownRefresh,
} from '@dcloudio/uni-app'
import { useStore } from '@/store'
import { getThemeStyle } from '@/utils/theme'
import carousel from '@/pages/tabbar/home/template/tpl_banner.vue'
import titleLayout from '@/pages/tabbar/home/template/tpl_title.vue'
import leftOneRightTwo from '@/pages/tabbar/home/template/tpl_left_one_right_two.vue'
import leftTwoRightOne from '@/pages/tabbar/home/template/tpl_left_two_right_one.vue'
import topOneBottomTwo from '@/pages/tabbar/home/template/tpl_top_one_bottom_two.vue'
import topTwoBottomOne from '@/pages/tabbar/home/template/tpl_top_two_bottom_one.vue'
import flexOne from '@/pages/tabbar/home/template/tpl_flex_one.vue'
import flexTwo from '@/pages/tabbar/home/template/tpl_flex_two.vue'
import flexThree from '@/pages/tabbar/home/template/tpl_flex_three.vue'
import flexFive from '@/pages/tabbar/home/template/tpl_flex_five.vue'
import flexFour from '@/pages/tabbar/home/template/tpl_flex_four.vue'
import textPicture from '@/pages/tabbar/home/template/tpl_text_picture.vue'
import menuLayout from '@/pages/tabbar/home/template/tpl_menu.vue'
import search from '@/pages/tabbar/home/template/tpl_search.vue'
import group from '@/pages/tabbar/home/template/tpl_group.vue'
import videoLayout from '@/pages/tabbar/home/template/tpl_video.vue'
import goods from '@/pages/tabbar/home/template/tpl_goods.vue'
import notice from '@/pages/tabbar/home/template/tpl_notice.vue'
import promotions from '@/pages/tabbar/home/template/tpl_promotions_detail.vue'
import fetchCoupon from '@/pages/tabbar/home/template/fetch_coupon.vue'
import blank from '@/pages/tabbar/home/template/tpl_blank.vue'
import lineLayout from '@/pages/tabbar/home/template/tpl_line.vue'
import richText from '@/pages/tabbar/home/template/tpl_rich_text.vue'
import integral from '@/pages/tabbar/home/template/tpl_integral.vue'
import coupon from '@/pages/tabbar/home/template/tpl_coupon.vue'
import articleList from '@/pages/tabbar/home/template/tpl_article.vue'
import shopList from '@/pages/tabbar/home/template/tpl_shop.vue'
import homeComb from '@/pages/tabbar/home/template/tpl_home_comb.vue'
import { getFloorData } from '@/api/home'
const HOME_LAYOUT_PADDING_RPX = 16
const store = useStore()
const themeStyle = computed(() => getThemeStyle(store.state.theme))
const couponRef = ref<InstanceType<typeof fetchCoupon>>()
const pageData = ref<Record<string, any>>({ list: [] })
const enableLoad = ref(false)
const loading = ref(false)
const searchBarWidth = ref(0)
const topColor = computed(() => pageData.value?.topColor || '#ffffff')
// 页面设置-样式:画布背景(背景色 / 背景图 + 填充方式)
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` }
}
return { width: '100%' }
})
function setCapsuleInset() {
try {
const { windowWidth } = uni.getWindowInfo()
const leftPadding = uni.upx2px(HOME_LAYOUT_PADDING_RPX)
const rightPadding = uni.upx2px(HOME_LAYOUT_PADDING_RPX)
// #ifdef MP-WEIXIN
const menuButton = uni.getMenuButtonBoundingClientRect()
searchBarWidth.value = windowWidth - (windowWidth - menuButton.left) - leftPadding
// #endif
// #ifndef MP-WEIXIN
searchBarWidth.value = windowWidth - leftPadding - rightPadding
// #endif
} catch (e) {
searchBarWidth.value = 0
}
}
function fetchCouponFn() {
couponRef.value?.firstGetAuto()
}
function init() {
if (loading.value) return
loading.value = true
getFloorData()
.then((res) => {
const payload = res?.data != null ? res.data : res
const pageDataStr = payload?.result?.pageData
const isSuccess = payload?.success === true || payload?.code === 200
if (!isSuccess || !pageDataStr) return
try {
const result = JSON.parse(pageDataStr)
pageData.value = Array.isArray(result?.list)
? { ...result, list: result.list.filter((i: any) => !i?.hidden) }
: { list: [] }
// 页面设置-内容:页面标题同步到导航栏
if (result?.title) {
uni.setNavigationBarTitle({ title: result.title })
}
// 页面设置-样式:顶部颜色与标题颜色同步到导航栏
if (result?.topColor || result?.titleColor) {
uni.setNavigationBarColor({
frontColor: result?.titleColor === 'white' ? '#ffffff' : '#000000',
backgroundColor: result?.topColor || '#ffffff',
})
}
const lastItem = pageData.value.list[pageData.value.list.length - 1]
enableLoad.value =
lastItem?.type === 'goods' || lastItem?.model === 'goods'
} catch (parseError) {
console.error('parse pageData failed', parseError)
}
})
.catch((err) => {
console.error('getFloorData failed', err)
})
.finally(() => {
loading.value = false
})
}
onReady(() => {
setCapsuleInset()
init()
// #ifdef MP-WEIXIN
uni.showShareMenu({ withShareTicket: true })
// #endif
})
onShow(() => {
setTimeout(() => {
fetchCouponFn()
}, 1000)
})
onReachBottom(() => {
uni.$emit('onReachBottom', true)
})
onPullDownRefresh(() => {
init()
uni.stopPullDownRefresh()
})
</script>
<style scoped lang="scss">
@import '@/pages/tabbar/home/template/tpl.scss';
.home-page {
width: 100%;
min-height: 100%;
background: #f9f9f9;
}
.floor-block {
width: 100%;
display: block;
}
.navbar {
width: 100%;
:deep(.u-navbar__content) {
width: 100%;
}
:deep(.u-navbar__content__left) {
flex: 1;
width: auto !important;
max-width: 100%;
padding: 0 0 0 $home-layout-padding !important;
}
}
.navbar-search-wrap {
box-sizing: border-box;
flex: 1;
min-width: 0;
}
</style>

View File

@@ -18,7 +18,7 @@
<view style="width: 120rpx; height: 120rpx; float: right">
<image
@click="enableShowCoupon = false"
src="/static/cpauto1.png"
src="https://lilishop-uniapp.oss-cn-beijing.aliyuncs.com/cpauto1.png"
style="width: 100%; height: 100%"
></image>
</view>

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

@@ -0,0 +1,104 @@
<template>
<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"
v-for="item in articleList"
:key="item.id"
@click="goDetail(item)"
>
<view class="article-text">
<view class="article-name" :style="nameStyle">{{ item.title }}</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from "vue";
import { getArticleList } from "@/api/article.js";
const props = defineProps<{ res: any }>();
const articleList = ref<any[]>([]);
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 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 };
if (categoryId.value) params.categoryId = categoryId.value;
getArticleList(params).then((res) => {
if (res.data.success && res.data.result) {
articleList.value = (res.data.result.records || []).slice(0, count.value);
}
});
});
function goDetail(item: any) {
uni.navigateTo({ url: `/pages/passport/article?id=${item.id}` });
}
</script>
<style lang="scss" scoped>
.article-title {
font-size: 30rpx;
font-weight: 600;
padding: 8rpx 24rpx 20rpx;
}
.article-item {
display: flex;
align-items: center;
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;
}
.article-name {
font-size: 26rpx;
color: #333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>

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

@@ -0,0 +1,24 @@
<template>
<view class="tpl-blank" :style="blankStyle"></view>
</template>
<script setup lang="ts">
import { computed } from "vue";
const props = defineProps<{ res: any }>();
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

@@ -0,0 +1,511 @@
<template>
<view class="tpl-coupon" :class="{ styled: styled }" :style="wrapperStyle" v-if="couponList.length || !styled">
<view class="coupon-title" v-if="title">{{ title }}</view>
<!-- 老数据保持原样 -->
<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="cp1-item"
v-for="item in couponList"
:key="item.id"
:style="itemGapStyle"
@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="cp1-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>
<!-- 样式二:白底小卡,底部去领取按钮 -->
<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"
:style="itemGapStyle"
@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"
:style="itemGapStyle"
@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"
:style="itemGapStyle"
@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>
<script setup lang="ts">
import { ref, computed, onMounted } from "vue";
import { getAllCoupons } from "@/api/promotions.js";
import { receiveCoupons } from "@/api/members.js";
import { isLogin, navigateToLogin, unitPrice } from "@/utils/filters.js";
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 || 5);
const layoutStyle = computed(() => (props.res && props.res.layoutStyle) || "1");
// 面板保存过样式字段才启用自定义样式,老数据保持原样
const styled = computed(() => props.res && props.res.paddingTop !== undefined);
const itemGapStyle = computed(() => {
if (!styled.value) return {};
const gap = props.res?.contentGap !== undefined ? Number(props.res.contentGap) || 0 : 5;
return { marginRight: `${gap}rpx` };
});
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) => {
if (res.data.code == 200 && res.data.result) {
couponList.value = (res.data.result.records || []).slice(0, count.value);
}
});
});
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");
return;
}
receiveCoupons(item.id).then((res) => {
if (res.data.code == 200) {
uni.showToast({ title: "领取成功", icon: "none" });
} else {
uni.showToast({ title: res.data.message, icon: "none" });
}
});
}
</script>
<style lang="scss" scoped>
.tpl-coupon {
font-size: 26rpx;
}
.coupon-title {
font-size: 30rpx;
font-weight: 600;
padding: 8rpx 24rpx 20rpx;
}
.coupon-item {
display: flex;
margin: 0 20rpx 20rpx;
border-radius: 12rpx;
overflow: hidden;
background: linear-gradient(90deg, #ffece8, #fff5f4);
}
.tpl-coupon.styled .coupon-item {
margin: 0;
}
.coupon-left {
width: 180rpx;
padding: 24rpx 0;
text-align: center;
color: var(--theme-primary, #e1251b);
border-right: 2rpx dashed #f0c0bb;
}
.coupon-value {
font-size: 40rpx;
font-weight: 700;
}
.coupon-symbol {
font-size: 24rpx;
}
.coupon-threshold {
font-size: 22rpx;
}
.coupon-right {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24rpx;
}
.coupon-name {
color: #333;
}
.coupon-btn {
background: var(--theme-primary, #e1251b);
color: #fff;
font-size: 24rpx;
padding: 8rpx 20rpx;
border-radius: 24rpx;
}
/* 参考 CRMEB 优惠券组件 */
.tpl-coupon.styled .coupon-title {
padding: 0 0 16rpx;
}
.cp-scroll {
white-space: nowrap;
}
.cp1-item:last-child,
.cp2-item:last-child,
.cp3-item:last-child,
.cp4-item:last-child {
margin-right: 0 !important;
}
.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,6 +1,6 @@
<template>
<view class="layout">
<view class="flex-one">
<view class="layout" :style="wrapperStyle">
<view class="flex-one" :style="contentStyle">
<u-image
v-if="!hasHotZone"
@click="modelNavigateTo(res.list[0])"
@@ -25,6 +25,27 @@ const hasHotZone = computed(() => {
const zoneInfo = props.res?.list?.[0]?.zoneInfo;
return Array.isArray(zoneInfo) ? zoneInfo.length > 0 : !!zoneInfo;
});
const wrapperStyle = computed(() => {
const r = props.res || {};
const bg =
r.bgColor1 !== undefined
? !r.bgColor2 || r.bgColor1 === r.bgColor2
? r.bgColor1
: `linear-gradient(180deg, ${r.bgColor1}, ${r.bgColor2})`
: r.bgColor ?? "transparent";
return {
background: bg,
padding: `${r.paddingTop ?? 0}rpx ${r.paddingX ?? 0}rpx ${r.paddingBottom ?? 0}rpx`,
marginBottom: `${r.pageGap ?? 0}rpx`,
borderRadius: `${r.borderRadius ?? 0}rpx`,
overflow: (r.borderRadius ?? 0) > 0 ? "hidden" : "visible",
};
});
const contentStyle = computed(() => ({
borderRadius: `${props.res?.imgRadius ?? 0}rpx`,
}));
</script>
<style lang="scss" scoped>
@import "./tpl.scss";
@@ -33,4 +54,4 @@ const hasHotZone = computed(() => {
display: block;
overflow: hidden;
}
</style>
</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,704 @@
<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}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)}`,
marginBottom: rpx(res.pageGap),
borderRadius: rpx(res.borderRadius),
overflow: Number(res.borderRadius) > 0 ? "hidden" : "visible",
};
});
const listStyle = computed(() => {
const gap = rpx(props.res?.contentGap ?? 10);
return { gap, "--g-gap": gap };
});
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) {
// 指定商品:按选择顺序排列(兼容 SKU id 与 goodsId)
if (first.value.dataMode === "goods") {
const byId: Record<string, any> = {};
mapped.forEach((g: any) => {
if (g.id) byId[g.id] = g;
if (g.goodsId) byId[g.goodsId] = g;
});
newGoodsData.value = (first.value.goodsList || [])
.map((g: any) => byId[g.id] || byId[g.goodsId])
.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: 48rpx;
height: 48rpx;
border-radius: 50%;
}
.cart-plus-label {
font-size: 30rpx;
line-height: 1;
}
.cart-text {
border-radius: 24rpx;
padding: 6rpx 20rpx;
}
.cart-text-label {
font-size: 24rpx;
}
/* 卡片:左图右文(参考 CRMEB 详细列表,左侧 224rpx 缩略图,右侧标题置顶、价格销量置底) */
.mode-card {
flex-direction: column;
.g-item {
flex-direction: row;
align-items: stretch;
padding: 20rpx;
}
.g-imgbox {
width: 224rpx;
height: 224rpx;
margin-right: 24rpx;
border-radius: 12rpx;
overflow: hidden;
}
.g-info {
min-height: 224rpx;
justify-content: space-between;
}
.g-name {
font-size: 28rpx;
font-weight: 500;
line-height: 40rpx;
margin-bottom: 12rpx;
}
.g-price {
font-size: 32rpx;
font-weight: 700;
margin-top: auto;
margin-bottom: 8rpx;
}
.g-bottom {
margin-top: 0;
}
.g-sales {
font-size: 24rpx;
}
.g-cart {
align-self: center;
}
}
/* 列表:两列网格 */
.mode-list {
.g-item {
width: calc((100% - var(--g-gap, 10rpx)) / 2);
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((100% - var(--g-gap, 10rpx) * 2) / 3);
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

@@ -0,0 +1,497 @@
<template>
<view class="tpl-home-comb" :style="wrapperStyle">
<!-- 高斯模糊轮播图背景 + 底部渐变遮罩 -->
<view class="comb-bg">
<image
v-if="currentBannerImg"
class="comb-bg-img"
:src="currentBannerImg"
mode="aspectFill"
/>
<view class="comb-mask"></view>
</view>
<!-- 顶部状态栏占位(小程序/App) -->
<view v-if="statusBarHeight" :style="{ height: statusBarHeight + 'px' }"></view>
<!-- 1. 搜索栏 -->
<view class="comb-search-row" :style="searchRowStyle">
<image
v-if="res.searType === 'logo' && res.logoUrl"
class="comb-logo"
:src="res.logoUrl"
mode="aspectFit"
/>
<view class="comb-search-box" :style="searchBoxStyle" @click="onSearchTap">
<u-icon name="search" :color="res.textColor || '#303133'" size="28rpx" />
<text class="comb-search-text">{{ displayHotWord }}</text>
</view>
</view>
<!-- 2. 分类选项卡 -->
<view v-if="res.showTabs !== false" class="comb-tabs-wrap">
<view class="comb-tabs">
<scroll-view scroll-x class="comb-tabs-scroll" :show-scrollbar="false">
<view class="comb-tabs-inner">
<view
v-for="(tab, idx) in displayTabs"
:key="idx"
class="comb-tab-item"
:class="{ active: activeTabIndex === idx }"
:style="{
color:
activeTabIndex === idx
? res.tabActiveColor || '#E93323'
: res.tabColor || '#FFFFFF',
}"
@click="onTabTap(tab, idx)"
>
<text>{{ tab.title || "未命名" }}</text>
<view
v-if="activeTabIndex === idx"
class="comb-tab-line"
:style="{ background: res.tabActiveColor || '#E93323' }"
></view>
</view>
</view>
</scroll-view>
<view class="comb-tab-toggle" @click="showMoreTabs = !showMoreTabs">
<u-icon
:name="showMoreTabs ? 'arrow-up' : 'arrow-down'"
:color="res.tabColor || '#FFFFFF'"
size="28rpx"
/>
</view>
</view>
<!-- 下拉展开全部分类面板 -->
<view v-if="showMoreTabs" class="comb-tabs-dropdown">
<view
v-for="(tab, idx) in displayTabs"
:key="idx"
class="dropdown-chip"
:class="{ active: activeTabIndex === idx }"
:style="
activeTabIndex === idx
? {
color: res.tabActiveColor || '#E93323',
borderColor: res.tabActiveColor || '#E93323',
}
: {}
"
@click="onTabTap(tab, idx)"
>
{{ tab.title || "未命名" }}
</view>
</view>
</view>
<!-- 3. 轮播图区域 -->
<view
v-if="bannerList.length"
class="comb-banner"
:class="{ 'no-tabs': res.showTabs === false }"
>
<swiper
class="comb-swiper"
autoplay
:interval="4500"
:duration="500"
circular
:previous-margin="cardMode ? '48rpx' : '0rpx'"
:next-margin="cardMode ? '48rpx' : '0rpx'"
@change="onSwiperChange"
>
<swiper-item v-for="(item, i) in bannerList" :key="i">
<view
class="comb-slide-item"
:class="{ 'is-card': cardMode, active: i === currentSlide }"
@click="onBannerTap(item)"
>
<image
class="comb-slide-img"
:style="bannerImgStyle"
:src="item.img"
mode="scaleToFill"
/>
</view>
</swiper-item>
</swiper>
<!-- 指示器 -->
<view
v-if="res.indicatorStyle !== 'none' && bannerList.length > 1"
class="comb-indicator"
:class="'pos-' + (res.indicatorPosition || 'center')"
>
<view
v-for="(item, i) in bannerList"
:key="i"
class="dot"
:class="[res.indicatorStyle || 'round', { active: i === currentSlide }]"
:style="
i === currentSlide
? { background: res.indicatorColor || '#E93323' }
: {}
"
></view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted, onBeforeUnmount } from "vue";
import { modelNavigateTo } from "./tpl";
const props = defineProps<{ res: any }>();
const toRpx = (v: any) => `${Number(v) || 0}rpx`;
// 状态栏与胶囊避让
const statusBarHeight = ref(0);
const capsuleRightPad = ref(0);
onMounted(() => {
try {
const info = uni.getWindowInfo();
// #ifdef MP-WEIXIN || APP-PLUS
statusBarHeight.value = info.statusBarHeight || 0;
// #endif
// #ifdef MP-WEIXIN
const menu = uni.getMenuButtonBoundingClientRect();
capsuleRightPad.value = Math.max(0, info.windowWidth - menu.left + 8);
// #endif
} catch (e) {
statusBarHeight.value = 0;
}
});
// 搜索热词轮换
const hotIndex = ref(0);
let hotTimer: ReturnType<typeof setInterval> | null = null;
const validHotWords = computed(() =>
(props.res?.hotWords || []).filter((w: string) => w && String(w).trim())
);
const displayHotWord = computed(() => {
if (validHotWords.value.length) {
return validHotWords.value[hotIndex.value % validHotWords.value.length];
}
return props.res?.placeWords || "搜索商品名称";
});
function stopHotTimer() {
if (hotTimer) {
clearInterval(hotTimer);
hotTimer = null;
}
}
watch(
() => [validHotWords.value.length, props.res?.hotInterval],
() => {
stopHotTimer();
hotIndex.value = 0;
if (validHotWords.value.length > 1) {
hotTimer = setInterval(() => {
hotIndex.value = (hotIndex.value + 1) % validHotWords.value.length;
}, (Number(props.res?.hotInterval) || 3) * 1000);
}
},
{ immediate: true }
);
onBeforeUnmount(stopHotTimer);
// 选项卡
const activeTabIndex = ref(0);
const showMoreTabs = ref(false);
const displayTabs = computed(() => {
const custom = Array.isArray(props.res?.tabList) ? props.res.tabList : [];
return [{ title: "首页", isHome: true }, ...custom];
});
function onTabTap(tab: any, idx: number) {
activeTabIndex.value = idx;
showMoreTabs.value = false;
if (tab.isHome || !tab.value) return;
if (tab.dataType === "special") {
uni.navigateTo({
url: `/pages/tabbar/special/special?id=${tab.value}`,
});
} else if (tab.dataType === "category") {
uni.navigateTo({
url: `/pages/navigation/search/searchPage?category=${tab.value}`,
});
}
}
// 轮播图
const currentSlide = ref(0);
const bannerList = computed(() =>
Array.isArray(props.res?.list) ? props.res.list : []
);
const cardMode = computed(() => (props.res?.swiperStyle || "card") === "card");
const currentBannerImg = computed(() => {
const list = bannerList.value;
if (!list.length) return "";
const item = list[currentSlide.value % list.length] || list[0];
return item?.img || "";
});
function onSwiperChange(e: any) {
currentSlide.value = e?.detail?.current ?? 0;
}
function onBannerTap(item: any) {
if (item) modelNavigateTo(item);
}
function onSearchTap() {
const keyword = validHotWords.value.length ? displayHotWord.value : "";
const query = keyword ? `?keyword=${encodeURIComponent(keyword)}` : "";
uni.navigateTo({
url: `/pages/navigation/search/searchPage${query}`,
});
}
// 样式计算
const wrapperStyle = computed(() => ({
marginBottom: toRpx(props.res?.pageGap ?? 0),
}));
const searchRowStyle = computed(() =>
capsuleRightPad.value ? { paddingRight: `${capsuleRightPad.value}px` } : {}
);
const searchBoxStyle = computed(() => {
const r = props.res || {};
const align = r.textAlign || "left";
return {
background: r.boxColor || "#FFFFFF",
color: r.textColor || "#303133",
borderRadius: toRpx(r.searchRadius ?? 30),
justifyContent:
align === "center"
? "center"
: align === "right"
? "flex-end"
: "flex-start",
};
});
const bannerImgStyle = computed(() => ({
borderRadius: toRpx(props.res?.bannerRadius ?? 14),
}));
</script>
<style lang="scss" scoped>
.tpl-home-comb {
position: relative;
overflow: hidden;
padding: 16rpx 20rpx 20rpx;
box-sizing: border-box;
}
.comb-bg {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 0;
overflow: hidden;
background: linear-gradient(180deg, #e93323 0%, #f5f5f5 100%);
.comb-bg-img {
width: 100%;
height: 100%;
transform: scale(1.25);
filter: blur(40rpx);
display: block;
}
.comb-mask {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 180rpx;
background: linear-gradient(180deg, rgba(245, 245, 245, 0) 0%, #f5f5f5 100%);
}
}
.comb-search-row {
position: relative;
z-index: 2;
display: flex;
align-items: center;
gap: 16rpx;
}
.comb-logo {
width: 138rpx;
height: 48rpx;
flex-shrink: 0;
}
.comb-search-box {
flex: 1;
min-width: 0;
height: 60rpx;
padding: 0 20rpx;
display: flex;
align-items: center;
gap: 8rpx;
font-size: 24rpx;
box-sizing: border-box;
overflow: hidden;
}
.comb-search-text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.comb-tabs-wrap {
position: relative;
z-index: 3;
margin-top: 8rpx;
}
.comb-tabs {
display: flex;
align-items: center;
height: 72rpx;
}
.comb-tabs-scroll {
flex: 1;
min-width: 0;
white-space: nowrap;
}
.comb-tabs-inner {
display: inline-flex;
align-items: center;
gap: 32rpx;
}
.comb-tab-item {
position: relative;
display: inline-flex;
align-items: center;
height: 68rpx;
font-size: 26rpx;
white-space: nowrap;
&.active {
font-size: 28rpx;
font-weight: 600;
}
.comb-tab-line {
position: absolute;
left: 50%;
bottom: 4rpx;
transform: translateX(-50%);
width: 36rpx;
height: 4rpx;
border-radius: 4rpx;
}
}
.comb-tab-toggle {
flex-shrink: 0;
padding-left: 16rpx;
display: flex;
align-items: center;
}
.comb-tabs-dropdown {
background: #ffffff;
border-radius: 16rpx;
padding: 20rpx;
margin-top: 8rpx;
display: flex;
flex-wrap: wrap;
gap: 16rpx;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.1);
}
.dropdown-chip {
padding: 8rpx 24rpx;
background: #f5f5f5;
border: 2rpx solid transparent;
border-radius: 8rpx;
font-size: 24rpx;
color: #333333;
}
.comb-banner {
position: relative;
z-index: 2;
margin-top: 12rpx;
height: 300rpx;
&.no-tabs {
margin-top: 20rpx;
}
}
.comb-swiper {
width: 100%;
height: 300rpx;
}
.comb-slide-item {
width: 100%;
height: 100%;
box-sizing: border-box;
&.is-card {
padding: 0 8rpx;
transform: scale(0.92);
transition: transform 0.35s ease;
&.active {
transform: scale(1);
}
}
}
.comb-slide-img {
width: 100%;
height: 100%;
display: block;
overflow: hidden;
}
.comb-indicator {
position: absolute;
bottom: 16rpx;
display: flex;
align-items: center;
z-index: 3;
&.pos-left {
left: 32rpx;
}
&.pos-center {
left: 50%;
transform: translateX(-50%);
}
&.pos-right {
right: 32rpx;
}
.dot {
background: rgba(255, 255, 255, 0.65);
transition: all 0.25s ease;
&.round {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
margin: 0 6rpx;
}
&.line {
width: 24rpx;
height: 6rpx;
border-radius: 4rpx;
margin: 0 4rpx;
}
}
}
</style>

View File

@@ -1,54 +1,188 @@
<template>
<div class="layout" v-if="goodsList.length">
<div class="join-list">
<div class="join-title" @click="goMore">
<div class="join-title-name">{{ res.list[0].title }}</div>
<div class="join-title-more">更多</div>
</div>
<div class="join-box">
<div
class="join-item"
@click="goDetail(item)"
v-for="(item, index) in goodsList"
:key="index"
>
<div class="item-img-box">
<u-image
class="item-img"
width="150rpx"
height="150rpx"
:src="item.goodsImage || item.thumbnail"
/>
</div>
<div class="item-price">
<span class="price-text">{{ item.points != null ? item.points : 0 }}积分</span>
</div>
<div class="item-line-through" v-if="item.originalPrice">
<span class="price-text">¥{{ item.originalPrice }}</span>
</div>
</div>
</div>
</div>
</div>
<view class="layout" :style="wrapperStyle" v-if="goodsList.length">
<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>
<!-- 样式一:两列大卡片 -->
<view v-else-if="layoutStyle === '1'" class="ig-grid2" :style="gapStyle">
<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" :style="nameStyle">{{ 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" :style="gapStyle">
<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" :style="nameStyle">{{ item.goodsName || item.promotionName || '' }}</view>
</view>
</view>
<!-- 样式三:三列,仅积分价 -->
<view v-else class="ig-grid3" :style="gapStyle">
<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>
</template>
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { ref, computed, onMounted } from "vue";
import * as API_Promotions from "@/api/promotions";
const props = defineProps<{ res: any }>();
const goodsList = ref<any[]>([]);
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 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})`,
backgroundSize: "cover",
backgroundPosition: "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 nameStyle = computed(() =>
styled.value && props.res.nameColor ? { color: props.res.nameColor } : {}
);
const gapStyle = computed(() => {
if (!styled.value) return {};
const gap = `${Number(props.res.contentGap ?? 10) || 0}rpx`;
return { gap, "--ig-gap": gap };
});
const imgRadiusStyle = computed(() =>
styled.value ? { borderRadius: `${Number(props.res.imgRadius) || 0}rpx`, overflow: "hidden" } : {}
);
onMounted(() => {
API_Promotions.getPointsGoods({
pageNumber: 1,
pageSize: props.res.list[0].count || 3,
pageSize: count.value,
}).then((res) => {
if (res.data.success && res.data.result && res.data.result.records) {
goodsList.value = res.data.result.records.slice(
0,
props.res.list[0].count || 3
);
goodsList.value = res.data.result.records.slice(0, count.value);
}
});
});
@@ -63,12 +197,39 @@ 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;
}
.item-price {
.price-text {
font-size: 15px;
font-size: 30rpx;
font-weight: 500;
color: $main-color;
}
@@ -77,36 +238,116 @@ function goDetail(item: any) {
flex: 1;
text-align: center;
}
.item-img {
width: 75px;
height: 75px;
margin: 0 auto;
display: block;
}
.item-img-box {
position: relative;
}
.item-line-through {
.price-text {
font-size: 10px;
font-size: 20rpx;
font-weight: 400;
text-decoration: line-through;
color: #999;
}
}
.join-title {
/* 积分商品卡片(参考 CRMEB 积分商城) */
.ig-img {
width: 100%;
height: 100%;
display: block;
}
.ig-grid2 {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
}
.ig-g2-item {
width: calc((100% - var(--ig-gap, 10rpx)) / 2);
background: #fff;
height: 50px;
.join-title-name {
font-size: 16px;
font-weight: bold;
}
.join-title-more {
font-size: 12px;
color: #999;
}
border-radius: 16rpx;
overflow: hidden;
padding-bottom: 16rpx;
box-sizing: border-box;
}
.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;
}
.ig-g3-item {
width: calc((100% - var(--ig-gap, 10rpx) * 2) / 3);
background: #fff;
border-radius: 16rpx;
overflow: hidden;
padding-bottom: 16rpx;
box-sizing: border-box;
}
.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

@@ -0,0 +1,30 @@
<template>
<view class="tpl-line" :style="wrapperStyle">
<view :style="lineStyle"></view>
</view>
</template>
<script setup lang="ts">
import { computed } from "vue";
const props = defineProps<{ res: any }>();
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"}`,
}));
</script>

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

@@ -0,0 +1,35 @@
<template>
<view class="tpl-rich-text" :style="wrapperStyle">
<rich-text :nodes="processedHtml"></rich-text>
</view>
</template>
<script setup lang="ts">
import { computed } from "vue";
const props = defineProps<{ res: any }>();
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(() => {
const html = props.res?.content;
if (!html) return "";
return String(html).replace(/<img/gi, '<img style="max-width:100%;height:auto;"');
});
</script>

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

@@ -0,0 +1,378 @@
<template>
<view class="tpl-shop" :style="wrapperStyle" v-if="shopList.length">
<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
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, 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 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 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` } : {}
);
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-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%;
display: flex;
align-items: center;
margin-bottom: 20rpx;
}
.shop-logo {
width: 72rpx;
height: 72rpx;
border-radius: 50%;
flex-shrink: 0;
}
.shop-name {
flex: 1;
min-width: 0;
font-size: 26rpx;
color: #333;
margin-left: 16rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>

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`,

View File

@@ -1,9 +1,9 @@
<template>
<div class="wrapper">
<div class="wrapper" :style="pageBgStyle">
<!-- uni 中不能使用 vue component 所以用if判断每个组件 -->
<div v-for="(item, index) in pageData.list" :key="index">
<!-- 搜索栏,如果在楼层装修顶部则会自动浮动,否则不浮动 -->
<div class="navbar" v-if="item.type == 'search'">
<div class="navbar" v-if="item.type == 'search'" :style="{ background: topColor }">
<search style="width: 100%" :res="item.options" />
<!-- #ifndef H5 -->
<!-- 扫码功能 不兼容h5 详情文档: https://uniapp.dcloud.io/api/system/barcode?id=scancode -->
@@ -12,6 +12,7 @@
</div>
<!-- #endif -->
</div>
<homeComb v-if="item.type == 'homeComb'" :res="item.options" />
<carousel v-if="item.type == 'carousel'" :res="item.options" />
<titleLayout v-if="item.type == 'title'" :res="item.options" />
<leftOneRightTwo
@@ -41,16 +42,22 @@
<group v-if="item.type == 'group'" :res="item.options" />
<notice v-if="item.type == 'notice'" :res="item.options" />
<promotions v-if="item.type == 'promotionDetail'" :res="item.options" />
<!-- <integral v-if="item.type == 'integral'" :res="item.options" /> -->
<integral v-if="item.type == 'integral'" :res="item.options" />
<!-- <spike v-if="item.type == 'spike'" :res="item.options" /> -->
<videoLayout v-if="item.type == 'video'" :res="item.options" />
<blank v-if="item.type == 'blank'" :res="item.options" />
<lineLayout v-if="item.type == 'line'" :res="item.options" />
<richText v-if="item.type == 'richText'" :res="item.options" />
<coupon v-if="item.type == 'coupon'" :res="item.options" />
<articleList v-if="item.type == 'article'" :res="item.options" />
<shopList v-if="item.type == 'shop'" :res="item.options" />
</div>
<u-no-network></u-no-network>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import carousel from '@/pages/tabbar/home/template/tpl_banner'
import titleLayout from '@/pages/tabbar/home/template/tpl_title'
@@ -71,6 +78,14 @@ import videoLayout from '@/pages/tabbar/home/template/tpl_video'
import goods from '@/pages/tabbar/home/template/tpl_goods'
import notice from '@/pages/tabbar/home/template/tpl_notice'
import promotions from '@/pages/tabbar/home/template/tpl_promotions_detail'
import blank from '@/pages/tabbar/home/template/tpl_blank'
import lineLayout from '@/pages/tabbar/home/template/tpl_line'
import richText from '@/pages/tabbar/home/template/tpl_rich_text'
import integral from '@/pages/tabbar/home/template/tpl_integral'
import coupon from '@/pages/tabbar/home/template/tpl_coupon'
import articleList from '@/pages/tabbar/home/template/tpl_article'
import shopList from '@/pages/tabbar/home/template/tpl_shop'
import homeComb from '@/pages/tabbar/home/template/tpl_home_comb.vue'
import { toSpecial, getSpecial } from '@/api/home'
import permision from '@/js_sdk/wa-permission/permission.js'
import config from '@/config/config'
@@ -80,6 +95,42 @@ const pageData = ref<any>('')
const bodyParam = ref('')
const isIos = ref('')
const topColor = computed(() => pageData.value?.topColor || '#ffffff')
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 {}
})
function applyPageSettings(parsed: any) {
if (!parsed) return
if (parsed.title) {
uni.setNavigationBarTitle({ title: parsed.title })
}
if (parsed.topColor || parsed.titleColor) {
uni.setNavigationBarColor({
frontColor: parsed.titleColor === 'white' ? '#ffffff' : '#000000',
backgroundColor: parsed.topColor || '#ffffff',
})
}
}
onMounted(() => {
init()
// #ifdef MP-WEIXIN
@@ -98,13 +149,23 @@ function init() {
if (bodyParam.value) {
toSpecial({ body: bodyParam.value }).then((res) => {
if (res.data.success) {
pageData.value = JSON.parse(res.data.result.pageData)
const parsed = JSON.parse(res.data.result.pageData)
if (parsed && Array.isArray(parsed.list)) {
parsed.list = parsed.list.filter((i: any) => !i?.hidden)
}
pageData.value = parsed
applyPageSettings(parsed)
}
})
} else {
getSpecial(id.value).then((res) => {
if (res.data.success) {
pageData.value = JSON.parse(res.data.result.pageData)
const parsed = JSON.parse(res.data.result.pageData)
if (parsed && Array.isArray(parsed.list)) {
parsed.list = parsed.list.filter((i: any) => !i?.hidden)
}
pageData.value = parsed
applyPageSettings(parsed)
}
})
}
@@ -190,4 +251,4 @@ async function scan() {
display: flex;
align-items: center;
}
</style>
</style>

View File

@@ -181,7 +181,7 @@ body {
background-size: cover;
border-bottom-left-radius: 30rpx;
border-bottom-right-radius: 30rpx;
background-image: url("/static/img/main-bg.png");
background-image: url("https://lilishop-uniapp.oss-cn-beijing.aliyuncs.com/main-bg.png");
background-position: bottom;
background-repeat: no-repeat;
color: #ffffff;