feat(pc-decorate): PC端楼层装修组件体系规范默认行高与各楼层组件重构

This commit is contained in:
pikachu1995@126.com
2026-10-29 17:06:08 +08:00
parent 5973d43b9d
commit 8739811b04
45 changed files with 7340 additions and 1856 deletions

View File

@@ -27,6 +27,8 @@
:component-style="comp.componentStyle || {}"
:row-style="row.rowStyle || {}"
/>
<div v-else class="unknown-component-placeholder">
@@ -93,7 +95,9 @@ const rowWrapperStyle = computed(() => {
if (s.height !== undefined && s.height !== '') {
const h = String(s.height).trim();
if (h && Number(h) > 0) {
style.minHeight = /^\d+$/.test(h) ? `${h}px` : h;
const hVal = /^\d+$/.test(h) ? `${h}px` : h;
style.height = hVal;
style.minHeight = hVal;
}
}
@@ -136,7 +140,7 @@ const rowInnerFlexStyle = computed(() => {
const s = props.row.rowStyle || {};
return {
const styleObj = {
display: 'flex',
@@ -150,10 +154,23 @@ const rowInnerFlexStyle = computed(() => {
width: '100%',
height: '100%',
gap: `${s.gap || 0}px`
};
if (s.height !== undefined && s.height !== '') {
const h = String(s.height).trim();
if (h && Number(h) > 0) {
const hVal = /^\d+$/.test(h) ? `${h}px` : h;
styleObj.height = hVal;
styleObj.minHeight = hVal;
}
}
return styleObj;
});
@@ -163,6 +180,15 @@ function getComponentCellStyle(style, rowStyle = {}, totalComps = 1) {
if (!style) return { flex: '1 1 0%' };
const s = {};
if (rowStyle?.height !== undefined && rowStyle?.height !== '') {
const rh = String(rowStyle.height).trim();
if (rh && Number(rh) > 0) {
const rhVal = /^\d+$/.test(rh) ? `${rh}px` : rh;
s.height = rhVal;
s.minHeight = rhVal;
}
}
const gap = Number(rowStyle?.gap) || 0;
const totalGap = (totalComps > 1 && gap > 0) ? gap * (totalComps - 1) : 0;
@@ -275,12 +301,22 @@ function getComponentCellStyle(style, rowStyle = {}, totalComps = 1) {
box-sizing: border-box;
display: flex;
flex-direction: column;
.row-inner-flex {
box-sizing: border-box;
flex: 1;
height: 100%;
min-height: 0;
.component-cell {
@@ -295,6 +331,13 @@ function getComponentCellStyle(style, rowStyle = {}, totalComps = 1) {
height: 100%;
> * {
flex: 1;
height: 100%;
width: 100%;
box-sizing: border-box;
}
}
}

View File

@@ -7,9 +7,10 @@
:style="customStyle"
>
<el-carousel
height="100%"
:autoplay="options.autoPlay !== false"
:interval="Number(options.interval) || 4000"
:arrow="options.showArrow ? 'hover' : 'never'"
:arrow="options.showArrow !== false ? 'hover' : 'never'"
:indicator-position="options.showIndicator !== false ? '' : 'none'"
class="custom-carousel"
>
@@ -37,6 +38,10 @@ const props = defineProps({
componentStyle: {
type: Object,
default: () => ({})
},
rowStyle: {
type: Object,
default: () => ({})
}
});
@@ -55,48 +60,32 @@ const slidesList = computed(() => {
}));
}
return [
{ title: '出游囤好货', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/carousel/banner_1.png', url: '/goodsList?seckill=1' },
{ title: '科技新品', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/carousel/banner_2.png', url: '/goodsList?category=1' },
{ title: '原产地直采', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/carousel/banner_3.png', url: '/goodsList?category=2' },
{ title: '大牌美妆', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/carousel/banner_4.png', url: '/goodsList?category=3' }
{ title: '潮流数码', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/carousel/banner_1.png', url: '/goodsList?category=1' },
{ title: '奢宠美妆', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/carousel/banner_2.png', url: '/goodsList?category=2' },
{ title: '运动潮流', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/carousel/banner_3.png', url: '/goodsList?category=3' },
{ title: '居家生活', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/carousel/banner_4.png', url: '/goodsList?category=4' }
];
});
function getAspectRatioCss(ratio) {
if (ratio === '16:9') return '16 / 9';
if (ratio === '2:1') return '2 / 1';
if (ratio === '3:1') return '3 / 1';
if (ratio === 'custom') return null;
return '2.5 / 1';
}
const customStyle = computed(() => {
const s = props.componentStyle || {};
const fit = props.options.fitMode || 'contain';
const fit = props.options.fitMode || 'cover';
const hasRowHeight = Boolean(props.rowStyle?.height && Number(props.rowStyle.height) > 0);
const styleObj = {
borderRadius: Array.isArray(s.radius)
borderRadius: Array.isArray(s.radius) && s.radius.some(r => r > 0)
? `${s.radius[0]}px ${s.radius[1]}px ${s.radius[2]}px ${s.radius[3]}px`
: '8px',
backgroundColor: s.backgroundColor || '#ffffff',
'--carousel-fit': fit
backgroundColor: (s.backgroundColor && s.backgroundColor !== 'transparent') ? s.backgroundColor : '#ffffff',
'--carousel-fit': fit,
height: '100%',
minHeight: hasRowHeight ? '0' : '420px'
};
if (props.options.height) {
if (props.options.aspectRatio === 'custom' && props.options.height) {
styleObj.height = `${props.options.height}px`;
styleObj.aspectRatio = 'auto';
} else {
const ratio = props.options.aspectRatio || '2.5:1';
const ratioCss = getAspectRatioCss(ratio);
if (ratioCss) {
styleObj.aspectRatio = ratioCss;
styleObj['--carousel-ratio'] = ratioCss;
} else {
styleObj.height = `${s.height || 420}px`;
styleObj.aspectRatio = 'auto';
}
styleObj.minHeight = `${props.options.height}px`;
}
return styleObj;
});
</script>
@@ -104,6 +93,7 @@ const customStyle = computed(() => {
<style lang="scss" scoped>
.decor-carousel {
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
background: #ffffff;
@@ -111,10 +101,13 @@ const customStyle = computed(() => {
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
box-sizing: border-box;
display: flex;
flex-direction: column;
.custom-carousel {
width: 100%;
height: 100%;
flex: 1;
:deep(.el-carousel__container) {
height: 100% !important;
@@ -122,25 +115,29 @@ const customStyle = computed(() => {
}
:deep(.el-carousel__indicators--horizontal) {
bottom: 14px;
bottom: 12px;
z-index: 10;
background: rgba(0, 0, 0, 0.18);
border-radius: 10px;
padding: 3px 8px;
backdrop-filter: blur(2px);
.el-carousel__indicator {
padding: 4px;
padding: 3px;
.el-carousel__button {
width: 8px;
height: 8px;
width: 6px;
height: 6px;
border-radius: 50%;
background-color: rgba(0, 0, 0, 0.35);
background-color: rgba(255, 255, 255, 0.6);
opacity: 1;
transition: all 0.25s;
}
&.is-active .el-carousel__button {
width: 22px;
border-radius: 5px;
background-color: #ea580c;
width: 16px;
border-radius: 3px;
background-color: #409EFF;
}
}
}
@@ -158,52 +155,15 @@ const customStyle = computed(() => {
height: 100%;
max-width: 100%;
max-height: 100%;
object-fit: var(--carousel-fit, contain) !important;
object-position: center;
object-fit: var(--carousel-fit, cover);
display: block;
}
}
}
&.is-compact {
:deep(.el-carousel__arrow) {
width: 26px;
height: 26px;
font-size: 12px;
}
&.is-mini {
:deep(.el-carousel__indicators--horizontal) {
bottom: 8px;
.el-carousel__indicator {
padding: 2px;
.el-carousel__button {
width: 6px;
height: 6px;
}
&.is-active .el-carousel__button {
width: 14px;
}
}
}
}
&.is-mini {
:deep(.el-carousel__arrow) {
width: 20px;
height: 20px;
font-size: 10px;
}
:deep(.el-carousel__indicators--horizontal) {
bottom: 4px;
.el-carousel__indicator {
padding: 1px;
.el-carousel__button {
width: 4px;
height: 4px;
}
&.is-active .el-carousel__button {
width: 10px;
}
}
}
}
}

View File

@@ -1,33 +1,95 @@
<template>
<div class="decor-category-mega-menu" :style="customStyle">
<!-- 顶部标题:分类(同万米设计,纯净大方) -->
<div
class="decor-category-mega-menu"
:class="{ 'is-hovering': panelVisible && activeChildren.length }"
:style="customStyle"
@mouseleave="handleMouseLeave"
>
<!-- 顶部标题:分类 / 商品分类 -->
<div class="menu-header">
<span class="header-title">{{ options.title || '分类' }}</span>
<span class="header-title">{{ options.title || '商品分类' }}</span>
</div>
<!-- 垂直一级类目列表(8项均匀铺满整个高度) -->
<!-- 垂直一级类目列表 -->
<div class="category-list">
<router-link
v-for="(cat, idx) in categoriesList"
:key="idx"
:to="cat.url || `/goodsList?categoryId=${cat.id || ''}`"
<div
v-for="(cat, idx) in displayCategories"
:key="cat.id || idx"
class="cat-item"
:class="{ 'is-active': activeIndex === idx && panelVisible }"
:style="{
'--text-color': options.textColor || '#334155',
'--active-color': options.activeColor || '#409EFF',
'--font-size': `${options.fontSize || 13}px`
}"
@mouseenter="handleMouseEnter(idx, cat)"
@click="goGoodsList(cat.id)"
>
<span class="cat-name">{{ cat.name }}</span>
<el-icon class="arrow-icon"><ArrowRight /></el-icon>
</router-link>
</div>
</div>
<!-- 右侧悬浮超级大面板 (Mega Menu) -->
<transition name="fade-flyout">
<div
v-if="panelVisible && activeChildren.length"
class="mega-flyout-panel"
@mouseenter="handlePanelEnter"
@mouseleave="handleMouseLeave"
>
<!-- 顶部快捷推荐标签(展示前8个二级分类) -->
<div class="flyout-top-tags" v-if="activeChildren && activeChildren.length">
<span
v-for="(sub, sIdx) in activeChildren.slice(0, 8)"
:key="sub.id || sIdx"
class="top-tag-pill"
@click.stop="goGoodsList(sub.id, sub.parentId)"
>
{{ sub.name }}
<el-icon class="pill-arrow"><ArrowRight /></el-icon>
</span>
</div>
<!-- 详细分类二级与三级平铺网格 -->
<div class="flyout-content-scroll">
<div
v-for="(sub, sIdx) in activeChildren"
:key="sub.id || sIdx"
class="cat-row"
>
<!-- 二级分类标题 -->
<div
class="lvl2-title"
@click.stop="goGoodsList(sub.id, sub.parentId)"
>
<span class="lvl2-name">{{ sub.name }}</span>
<el-icon class="lvl2-arrow"><ArrowRight /></el-icon>
</div>
<!-- 三级分类细项 -->
<div class="lvl3-tags-wrap">
<span
v-for="(leaf, lIdx) in (sub.children || [])"
:key="leaf.id || lIdx"
class="lvl3-tag"
@click.stop="goGoodsList(leaf.id, sub.id, sub.parentId)"
>
{{ leaf.name }}
</span>
</div>
</div>
</div>
</div>
</transition>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { ref, computed, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { ArrowRight } from '@element-plus/icons-vue';
import { getCategory } from '@/api/goods';
const props = defineProps({
options: {
@@ -37,41 +99,214 @@ const props = defineProps({
componentStyle: {
type: Object,
default: () => ({})
},
rowStyle: {
type: Object,
default: () => ({})
}
});
const defaultCategories = [
{ id: '1', name: '潮流数码' },
{ id: '2', name: '奢宠美妆' },
{ id: '3', name: '智能家电' },
{ id: '4', name: '服饰鞋包' },
{ id: '5', name: '运动潮流' },
{ id: '6', name: '精品生鲜' },
{ id: '7', name: '居家生活' },
{ id: '8', name: '母婴玩具' }
const router = useRouter();
// 默认真实类目备选数据(防止弱网下未加载出接口时空白)
const fallbackCategories = [
{
id: '1348576427264204941',
name: '电脑办公',
children: [
{ id: '1-1', name: '电脑整机', parentId: '1348576427264204941', children: [{ id: '1-1-1', name: '笔记本' }, { id: '1-1-2', name: '游戏本' }, { id: '1-1-3', name: '台式机' }, { id: '1-1-4', name: '平板电脑' }] },
{ id: '1-2', name: '电脑配件', parentId: '1348576427264204941', children: [{ id: '1-2-1', name: '显示器' }, { id: '1-2-2', name: '显卡' }, { id: '1-2-3', name: '机械键盘' }, { id: '1-2-4', name: '鼠标' }] },
{ id: '1-3', name: '手机通讯', parentId: '1348576427264204941', children: [{ id: '1-3-1', name: '5G智能手机' }, { id: '1-3-2', name: '拍照手机' }, { id: '1-3-3', name: '蓝牙耳机' }] }
]
},
{
id: '1348576427268399553',
name: '家用电器',
children: [
{ id: '2-1', name: '大家电', parentId: '1348576427268399553', children: [{ id: '2-1-1', name: '平板电视' }, { id: '2-1-2', name: '风冷冰箱' }, { id: '2-1-3', name: '滚筒洗衣机' }, { id: '2-1-4', name: '变频空调' }] },
{ id: '2-2', name: '生活电器', parentId: '1348576427268399553', children: [{ id: '2-2-1', name: '空气净化器' }, { id: '2-2-2', name: '扫地机器人' }, { id: '2-2-3', name: '吸尘器' }] },
{ id: '2-3', name: '厨房小电', parentId: '1348576427268399553', children: [{ id: '2-3-1', name: '电饭煲' }, { id: '2-3-2', name: '复古咖啡机' }, { id: '2-3-3', name: '破壁料理机' }] }
]
},
{
id: '1348576427260010496',
name: '服装鞋包',
children: [
{ id: '3-1', name: '女装流行', parentId: '1348576427260010496', children: [{ id: '3-1-1', name: '连衣裙' }, { id: '3-1-2', name: '羊绒大衣' }, { id: '3-1-3', name: '早秋衬衫' }, { id: '3-1-4', name: '休闲裤' }] },
{ id: '3-2', name: '男装精选', parentId: '1348576427260010496', children: [{ id: '3-2-1', name: '商务夹克' }, { id: '3-2-2', name: '休闲T恤' }, { id: '3-2-3', name: '免烫衬衫' }, { id: '3-2-4', name: '牛仔裤' }] }
]
},
{
id: '1348576427268399363',
name: '食品生鲜',
children: [
{ id: '4-1', name: '生鲜水果', parentId: '1348576427268399363', children: [{ id: '4-1-1', name: '进口车厘子' }, { id: '4-1-2', name: '红富士苹果' }, { id: '4-1-3', name: '佳沛奇异果' }] },
{ id: '4-2', name: '休闲零食', parentId: '1348576427268399363', children: [{ id: '4-2-1', name: '每日坚果' }, { id: '4-2-2', name: '烘焙饼干' }, { id: '4-2-3', name: '巧克力糖果' }] }
]
},
{
id: '1348576427268399172',
name: '精品箱包',
children: [
{ id: '5-1', name: '潮流女包', parentId: '1348576427268399172', children: [{ id: '5-1-1', name: '真皮单肩包' }, { id: '5-1-2', name: '托特大包' }, { id: '5-1-3', name: '斜挎小方包' }] },
{ id: '5-2', name: '商务男包', parentId: '1348576427268399172', children: [{ id: '5-2-1', name: '双肩电脑包' }, { id: '5-2-2', name: '公文手提包' }] }
]
},
{
id: '1348576427268399318',
name: '美妆个护',
children: [
{ id: '6-1', name: '面部护肤', parentId: '1348576427268399318', children: [{ id: '6-1-1', name: '修护精华' }, { id: '6-1-2', name: '补水面膜' }, { id: '6-1-3', name: '爽肤水' }, { id: '6-1-4', name: '眼霜' }] },
{ id: '6-2', name: '彩妆香水', parentId: '1348576427268399318', children: [{ id: '6-2-1', name: '大牌口红' }, { id: '6-2-2', name: '持久气垫' }, { id: '6-2-3', name: '沙龙香氛' }] }
]
},
{
id: '1348576427268399274',
name: '运动户外',
children: [
{ id: '7-1', name: '运动鞋服', parentId: '1348576427268399274', children: [{ id: '7-1-1', name: '缓震跑鞋' }, { id: '7-1-2', name: '专业篮球鞋' }, { id: '7-1-3', name: '运动速干衣' }] },
{ id: '7-2', name: '户外装备', parentId: '1348576427268399274', children: [{ id: '7-2-1', name: '露营帐篷' }, { id: '7-2-2', name: '登山包' }, { id: '7-2-3', name: '运动手表' }] }
]
},
{
id: '1348576427268399217',
name: '家居家装',
children: [
{ id: '8-1', name: '家纺布艺', parentId: '1348576427268399217', children: [{ id: '8-1-1', name: '四件套' }, { id: '8-1-2', name: '乳胶枕' }, { id: '8-1-3', name: '羽绒被' }] },
{ id: '8-2', name: '现代家具', parentId: '1348576427268399217', children: [{ id: '8-2-1', name: '人体工学椅' }, { id: '8-2-2', name: '升降电脑桌' }, { id: '8-2-3', name: '真皮沙发' }] }
]
}
];
const categoriesList = computed(() => {
if (Array.isArray(props.options?.categories) && props.options.categories.length) {
return props.options.categories.map(c => {
const rawName = c.name || c.title || '';
const cleanName = rawName.includes('/') ? rawName.split('/')[0].trim() : rawName;
return {
...c,
name: cleanName
};
});
const systemCategories = ref([]);
const activeIndex = ref(-1);
const activeCategory = ref(null);
const panelVisible = ref(false);
const isCustomSource = computed(() => {
// 1. 如果明确指定为系统分类,始终按系统商品分类处理
if (props.options?.source === 'system') return false;
// 2. 如果配置为 custom,但分类条目依然是初始未编辑的 4 个占位条目,依然展示真实系统分类
if (props.options?.source === 'custom') {
if (Array.isArray(props.options?.categories) && props.options.categories.length === 4) {
const names = props.options.categories.map(c => c.name || c.title || '');
const isPlaceholder = names.some(n => n.includes('精选')) &&
names.some(n => n.includes('新品')) &&
names.some(n => n.includes('企业')) &&
names.some(n => n.includes('领券'));
if (isPlaceholder) return false;
}
return true;
}
return defaultCategories;
// 3. 其它默认情况均默认展示系统商品分类
return false;
});
const displayCategories = computed(() => {
if (isCustomSource.value && Array.isArray(props.options?.categories) && props.options.categories.length) {
return props.options.categories.map(c => ({
...c,
name: c.name || c.title || ''
}));
}
if (systemCategories.value && systemCategories.value.length) {
return systemCategories.value;
}
return fallbackCategories;
});
const activeChildren = computed(() => {
if (!activeCategory.value) return [];
if (Array.isArray(activeCategory.value.children) && activeCategory.value.children.length) {
return activeCategory.value.children;
}
if (Array.isArray(activeCategory.value.subCategories) && activeCategory.value.subCategories.length) {
return activeCategory.value.subCategories.map((s, idx) => ({
id: s.id || `sub_${idx}`,
name: s.name,
parentId: activeCategory.value.id,
children: (s.items || []).map((it, iIdx) => ({
id: `leaf_${idx}_${iIdx}`,
name: typeof it === 'string' ? it : it.name,
parentId: s.id
}))
}));
}
return [];
});
function handleMouseEnter(idx, cat) {
activeIndex.value = idx;
activeCategory.value = cat;
if (cat.children && cat.children.length) {
panelVisible.value = true;
} else if (cat.subCategories && cat.subCategories.length) {
panelVisible.value = true;
} else {
panelVisible.value = false;
}
}
function handlePanelEnter() {
panelVisible.value = true;
}
function handleMouseLeave() {
panelVisible.value = false;
activeIndex.value = -1;
activeCategory.value = null;
}
function goGoodsList(id, secondId, firstId) {
const arr = [firstId, secondId, id];
if (!arr[1]) arr.splice(0, 2);
if (!arr[0]) arr.shift();
router.push({
path: '/goodsList',
query: { categoryId: arr.filter(Boolean).join(',') }
});
}
function loadCategories() {
// 1. 优先读取 localStorage 缓存快速渲染
try {
const cached = localStorage.getItem('category');
const exp = localStorage.getItem('category_expiration_time');
if (cached && exp && new Date() <= Number(exp)) {
const parsed = JSON.parse(cached);
if (Array.isArray(parsed) && parsed.length) {
systemCategories.value = parsed;
}
}
} catch (e) {}
// 2. 调接口拉取最新分类
getCategory(0).then(res => {
if (res.success && Array.isArray(res.result) && res.result.length) {
systemCategories.value = res.result;
const expTime = new Date().setHours(new Date().getHours() + 1);
localStorage.setItem('category_expiration_time', expTime);
localStorage.setItem('category', JSON.stringify(res.result));
}
}).catch(err => {
console.warn('获取分类失败', err);
});
}
onMounted(() => {
loadCategories();
});
const customStyle = computed(() => {
const s = props.componentStyle || {};
const hasRowHeight = Boolean(props.rowStyle?.height && Number(props.rowStyle.height) > 0);
return {
backgroundColor: s.backgroundColor || '#ffffff',
borderRadius: Array.isArray(s.radius)
backgroundColor: (s.backgroundColor && s.backgroundColor !== 'transparent') ? s.backgroundColor : '#ffffff',
borderRadius: Array.isArray(s.radius) && s.radius.some(r => r > 0)
? `${s.radius[0]}px ${s.radius[1]}px ${s.radius[2]}px ${s.radius[3]}px`
: '8px'
: '8px',
height: '100%',
minHeight: hasRowHeight ? '0' : '420px'
};
});
</script>
@@ -80,7 +315,6 @@ const customStyle = computed(() => {
.decor-category-mega-menu {
width: 100%;
height: 100%;
min-height: 420px;
box-sizing: border-box;
background: #ffffff;
border: 1px solid #e2e8f0;
@@ -88,15 +322,23 @@ const customStyle = computed(() => {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
overflow: visible;
&.is-hovering {
z-index: 1050;
}
.menu-header {
padding: 14px 16px 6px;
height: 42px;
padding: 0 16px;
display: flex;
align-items: center;
border-bottom: 1px solid #f1f5f9;
flex-shrink: 0;
.header-title {
font-size: 15px;
font-size: 14px;
font-weight: 700;
color: #1e293b;
letter-spacing: 0.5px;
@@ -107,14 +349,17 @@ const customStyle = computed(() => {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 2px 0 10px;
justify-content: space-evenly;
padding: 4px 0 8px;
box-sizing: border-box;
min-height: 0;
overflow-y: auto;
.cat-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 16px;
padding: 6px 16px;
cursor: pointer;
text-decoration: none;
transition: all 0.15s ease;
@@ -129,14 +374,15 @@ const customStyle = computed(() => {
}
.arrow-icon {
font-size: 12px;
font-size: 11px;
color: #94a3b8;
transition: transform 0.15s ease, color 0.15s ease;
flex-shrink: 0;
}
&:hover {
background: #fff7ed;
&:hover,
&.is-active {
background: #ecf5ff;
.cat-name {
color: var(--active-color, #409EFF);
@@ -150,5 +396,148 @@ const customStyle = computed(() => {
}
}
}
.mega-flyout-panel {
position: absolute;
top: 0;
left: 100%;
width: 780px;
min-height: 100%;
max-height: 520px;
background: #ffffff;
z-index: 1100;
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.12), 0 2px 8px rgba(0, 0, 0, 0.04);
border: 1px solid #e2e8f0;
border-left: none;
border-radius: 0 8px 8px 0;
padding: 16px 20px;
box-sizing: border-box;
display: flex;
flex-direction: column;
.flyout-top-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding-bottom: 12px;
border-bottom: 1px solid #f1f5f9;
margin-bottom: 12px;
flex-shrink: 0;
.top-tag-pill {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 10px;
background: #f8fafc;
border-radius: 14px;
font-size: 12px;
font-weight: 600;
color: #334155;
cursor: pointer;
transition: all 0.15s;
.pill-arrow {
font-size: 10px;
color: #94a3b8;
}
&:hover {
background: #ecf5ff;
color: #409EFF;
.pill-arrow {
color: #409EFF;
}
}
}
}
.flyout-content-scroll {
flex: 1;
overflow-y: auto;
padding-right: 6px;
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 4px;
}
.cat-row {
display: flex;
align-items: flex-start;
padding: 10px 0;
border-bottom: 1px dashed #f1f5f9;
&:last-child {
border-bottom: none;
}
.lvl2-title {
width: 96px;
flex-shrink: 0;
display: inline-flex;
align-items: center;
gap: 3px;
cursor: pointer;
color: #1e293b;
font-weight: 600;
font-size: 12px;
padding-top: 1px;
.lvl2-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.lvl2-arrow {
font-size: 10px;
color: #94a3b8;
}
&:hover {
color: #409EFF;
.lvl2-arrow {
color: #409EFF;
}
}
}
.lvl3-tags-wrap {
flex: 1;
display: flex;
flex-wrap: wrap;
gap: 6px 14px;
.lvl3-tag {
font-size: 12px;
color: #64748b;
cursor: pointer;
transition: color 0.15s;
line-height: 1.6;
&:hover {
color: #409EFF;
}
}
}
}
}
}
}
.fade-flyout-enter-active,
.fade-flyout-leave-active {
transition: opacity 0.15s ease, transform 0.15s ease;
}
.fade-flyout-enter-from,
.fade-flyout-leave-to {
opacity: 0;
transform: translateX(-4px);
}
</style>

View File

@@ -98,8 +98,21 @@
<script setup>
import { computed } from 'vue';
import { useRouter } from 'vue-router';
import { ElMessage } from 'element-plus';
const router = useRouter();
function handleNavigate(url) {
const target = url ? String(url).trim() : '/goods';
if (!target) return;
if (/^https?:\/\//i.test(target)) {
window.open(target, '_blank');
} else {
router.push(target);
}
}
const props = defineProps({
component: {
type: Object,
@@ -479,10 +492,10 @@ function addToCart(item) {
flex: 1;
.goods-name {
font-size: 12px;
font-size: 14px;
font-weight: 500;
line-height: 1.4;
height: 34px;
height: 40px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;

View File

@@ -7,9 +7,9 @@
<h3 class="grid-main-title">{{ options.title }}</h3>
<span class="grid-sub-title" v-if="options.subtitle">{{ options.subtitle }}</span>
</div>
<router-link :to="options.moreUrl || '/goods'" class="grid-more" v-if="options.showMoreLink !== false">
<a href="javascript:;" class="grid-more" v-if="options.showMoreLink !== false" @click="handleNavigate(options.moreUrl || '/goods')">
{{ options.moreText || '查看全部' }} &gt;
</router-link>
</a>
</div>
<!-- 紧凑严谨的商品九宫格/矩阵网格 (Bordered Matrix Grid) -->
@@ -27,13 +27,13 @@
<span class="rank-num">TOP {{ idx + 1 }}</span>
</div>
<router-link :to="`/goods/${item.id || 1}`" class="cell-thumb-box">
<a href="javascript:;" class="cell-thumb-box" @click="handleNavigate(item.url || `/goods/${item.id || 1}`)">
<img :src="item.thumbnail || defaultThumb" :alt="item.goodsName" class="cell-thumb" />
</router-link>
</a>
<div class="cell-body">
<h4 class="cell-name">
<router-link :to="`/goods/${item.id || 1}`">{{ item.goodsName }}</router-link>
<a href="javascript:;" @click="handleNavigate(item.url || `/goods/${item.id || 1}`)">{{ item.goodsName }}</a>
</h4>
<div class="cell-meta">
<span class="badge-label">热卖好物</span>
@@ -54,6 +54,7 @@
<script setup>
import { computed } from 'vue';
import { useRouter } from 'vue-router';
import { ElMessage } from 'element-plus';
const props = defineProps({
@@ -255,10 +256,10 @@ function addToCart(item) {
flex: 1;
.cell-name {
font-size: 13px;
font-size: 14px;
font-weight: 500;
line-height: 1.4;
height: 36px;
height: 40px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;

View File

@@ -6,7 +6,11 @@
<h3 class="main-title">{{ options.title }}</h3>
<span class="sub-title" v-if="options.subtitle">{{ options.subtitle }}</span>
</div>
<router-link :to="options.moreUrl || '/goods'" class="more-link" v-if="options.showMoreLink !== false">
<router-link
:to="options.moreUrl || (options.categoryId ? `/goodsList?categoryId=${options.categoryId}` : '/goodsList')"
class="more-link"
v-if="options.showMoreLink !== false"
>
{{ options.moreText || '查看更多' }} &gt;
</router-link>
</div>
@@ -22,21 +26,21 @@
:key="g.id || idx"
class="goods-flow-card"
>
<router-link :to="`/goods/${g.id || 1}`" class="card-thumb-wrap">
<router-link
:to="{ path: '/goodsDetail', query: { skuId: g.id, goodsId: g.goodsId || g.id } }"
class="card-thumb-wrap"
>
<img :src="g.thumbnail || defaultThumb" :alt="g.goodsName" class="thumb-img" />
<span class="self-tag" v-if="idx % 3 === 0">自营</span>
</router-link>
<div class="card-body">
<h4 class="goods-name" v-if="options.showTitle !== false">
<router-link :to="`/goods/${g.id || 1}`">{{ g.goodsName }}</router-link>
<router-link :to="{ path: '/goodsDetail', query: { skuId: g.id, goodsId: g.goodsId || g.id } }">
{{ g.goodsName }}
</router-link>
</h4>
<div class="meta-row" v-if="options.showSales !== false">
<span class="sales-text">已售 {{ 120 + idx * 85 }}+ 件</span>
<span class="rate-text">99%好评</span>
</div>
<div class="price-row">
<div class="price-box">
<span class="symbol">¥</span>
@@ -60,8 +64,37 @@
</template>
<script setup>
import { computed } from 'vue';
import { ref, computed, onMounted, watch } from 'vue';
import { useRouter } from 'vue-router';
import { ElMessage } from 'element-plus';
import { goodsList } from '@/api/goods';
const router = useRouter();
function handleNavigateGoods(g) {
if (g.url) {
const url = String(g.url).trim();
if (/^https?:\/\//i.test(url)) {
window.open(url, '_blank');
return;
}
router.push(url);
return;
}
router.push({
path: '/goodsDetail',
query: { skuId: g.id, goodsId: g.goodsId || g.id }
});
}
function handleNavigateMore() {
const url = props.options?.moreUrl || (props.options?.categoryId ? `/goodsList?categoryId=${props.options.categoryId}` : '/goodsList');
if (/^https?:\/\//i.test(url)) {
window.open(url, '_blank');
} else {
router.push(url);
}
}
const props = defineProps({
component: {
@@ -95,6 +128,75 @@ const defaultMockGoods = [
{ id: '12', goodsName: '航空级铝镁合金静音万向轮拉杆登机箱 20英寸防刮耐磨', price: '499.00', originalPrice: '699.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_7.png' }
];
const categoryFetchedGoods = ref([]);
const isFetching = ref(false);
async function fetchGoodsByCategory() {
if (props.options.dataSource !== 'category') return;
const count = Number(props.options.goodsCount) || (props.component?.type === 'goodsCard' ? 1 : 6);
const params = {
pageNumber: 1,
pageSize: count
};
if (props.options.categoryId) {
const rawCat = String(props.options.categoryId);
const catParts = rawCat.split(',');
params.categoryId = catParts[catParts.length - 1];
}
// 排序参数映射
if (props.options.sortType === 'sales') {
params.sort = 'buyCount';
params.order = 'desc';
} else if (props.options.sortType === 'price_asc') {
params.sort = 'price';
params.order = 'asc';
} else if (props.options.sortType === 'price_desc') {
params.sort = 'price';
params.order = 'desc';
} else if (props.options.sortType === 'new') {
params.sort = 'releaseTime';
params.order = 'desc';
}
try {
isFetching.value = true;
const res = await goodsList(params);
const records = res?.data?.result?.records || res?.result?.records || res?.data?.records || [];
if (Array.isArray(records) && records.length > 0) {
categoryFetchedGoods.value = records.map(item => ({
id: item.id || item.goodsId || item.sn,
goodsId: item.goodsId || item.id,
goodsName: item.goodsName || item.name,
price: item.price,
originalPrice: item.originalPrice || item.price,
thumbnail: item.thumbnail || item.pic || defaultThumb
}));
}
} catch (err) {
console.warn('Fetch goods by category failed:', err);
} finally {
isFetching.value = false;
}
}
onMounted(() => {
fetchGoodsByCategory();
});
watch(
() => [props.options.dataSource, props.options.categoryId, props.options.goodsCount, props.options.sortType],
() => {
fetchGoodsByCategory();
}
);
const isGoodsCard = computed(() => {
return props.component?.type === 'goodsCard' || props.options?.type === 'goodsCard';
});
const actualColumns = computed(() => {
let baseCols = Number(props.options?.columns) || 6;
const ratio = props.componentStyle?.widthRatio || '100%';
@@ -127,6 +229,10 @@ const customStyle = computed(() => {
});
const sampleGoods = computed(() => {
if (props.options.dataSource === 'category' && categoryFetchedGoods.value.length > 0) {
const targetCount = Number(props.options.goodsCount) || (props.component?.type === 'goodsCard' ? 1 : 6);
return categoryFetchedGoods.value.slice(0, targetCount);
}
if (Array.isArray(props.options.goods) && props.options.goods.length > 0) {
return props.options.goods;
}
@@ -170,6 +276,7 @@ function addToCart(item) {
}
.more-link {
cursor: pointer;
font-size: 13px;
color: #64748b;
text-decoration: none;
@@ -210,6 +317,7 @@ function addToCart(item) {
}
.card-thumb-wrap {
cursor: pointer;
display: block;
position: relative;
width: 100%;
@@ -245,10 +353,10 @@ function addToCart(item) {
flex: 1;
.goods-name {
font-size: 12px;
font-size: 14px;
font-weight: 500;
line-height: 1.4;
height: 34px;
height: 40px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
@@ -324,4 +432,4 @@ function addToCart(item) {
}
}
}
</style>
</style>

View File

@@ -22,9 +22,9 @@
{{ tag }}
</router-link>
</div>
<router-link :to="options.moreUrl || '/goods'" class="more-link">
<a href="javascript:;" class="more-link" @click="handleNavigate(options.moreUrl || '/goods')">
查看更多 &gt;
</router-link>
</a>
</div>
</div>
@@ -32,7 +32,7 @@
<div class="floor-main-body" :class="{ 'no-poster': !showPoster }">
<!-- 左侧营销大促海报 -->
<div class="floor-left-poster" v-if="showPoster">
<router-link :to="options.posterUrl || '/goods'" class="poster-link">
<div class="poster-link" @click="handleNavigate(options.bannerUrl || options.posterUrl || '/goods')">
<img :src="posterImg" alt="floor poster" class="poster-img" />
<div class="poster-overlay">
<div class="poster-top-badge">{{ options.floorNum || '1F' }} 甄选</div>
@@ -45,7 +45,7 @@
<span>立即抢购 &gt;</span>
</div>
</div>
</router-link>
</div>
</div>
<!-- 右侧商品网格 (2行 x N列) -->
@@ -59,14 +59,14 @@
:key="g.id || idx"
class="floor-goods-card"
>
<router-link :to="`/goods/${g.id || 1}`" class="card-thumb-wrap">
<a href="javascript:;" class="card-thumb-wrap" @click="handleNavigate(g.url || `/goods/${g.id || 1}`)">
<img :src="g.thumbnail || defaultThumb" :alt="g.goodsName" class="goods-img" />
<span class="corner-badge" v-if="idx < 3">HOT</span>
</router-link>
</a>
<div class="card-detail">
<h4 class="goods-name-line">
<router-link :to="`/goods/${g.id || 1}`">{{ g.goodsName }}</router-link>
<a href="javascript:;" @click="handleNavigate(g.url || `/goods/${g.id || 1}`)">{{ g.goodsName }}</a>
</h4>
<div class="goods-tag-row">
<span class="mini-tag">官方直营</span>
@@ -98,8 +98,29 @@
<script setup>
import { ref, computed } from 'vue';
import { useRouter } from 'vue-router';
import { ElMessage } from 'element-plus';
const router = useRouter();
function handleNavigate(url) {
const target = url ? String(url).trim() : '/goods';
if (!target) return;
if (/^https?:\/\//i.test(target)) {
window.open(target, '_blank');
} else {
router.push(target);
}
}
function handleNavigateGoods(g) {
if (g.url) {
handleNavigate(g.url);
return;
}
router.push(`/goods/${g.id || 1}`);
}
const props = defineProps({
component: {
type: Object,
@@ -268,6 +289,7 @@ function addToCart(item) {
}
.more-link {
cursor: pointer;
font-size: 13px;
color: #64748b;
text-decoration: none;
@@ -303,6 +325,7 @@ function addToCart(item) {
background: #1e293b;
.poster-link {
cursor: pointer;
display: block;
position: relative;
width: 100%;
@@ -456,10 +479,10 @@ function addToCart(item) {
flex: 1;
.goods-name-line {
font-size: 13px;
font-size: 14px;
font-weight: 500;
line-height: 1.4;
height: 36px;
height: 40px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;

View File

@@ -2,10 +2,9 @@
<div class="decor-hot-zone" :style="customStyle">
<div class="hot-zone-wrap" v-if="displayImageUrl">
<img :src="displayImageUrl" alt="hotzone" class="base-image" />
<router-link
<div
v-for="(zone, idx) in displayZones"
:key="idx"
:to="zone.url || '/'"
class="zone-link"
:style="{
top: `${zone.top || 0}%`,
@@ -14,6 +13,7 @@
height: `${zone.height || 10}%`
}"
:title="zone.title"
@click="handleClickZone(zone)"
/>
</div>
</div>
@@ -21,6 +21,9 @@
<script setup>
import { computed } from 'vue';
import { useRouter } from 'vue-router';
const router = useRouter();
const props = defineProps({
options: {
@@ -63,6 +66,16 @@ const customStyle = computed(() => {
overflow: 'hidden'
};
});
function handleClickZone(zone) {
const url = zone?.url ? String(zone.url).trim() : '';
if (!url) return;
if (/^https?:\/\//i.test(url)) {
window.open(url, '_blank');
} else {
router.push(url);
}
}
</script>
<style lang="scss" scoped>
@@ -79,21 +92,23 @@ const customStyle = computed(() => {
overflow: hidden;
.base-image {
width: 100%;
display: block;
border-radius: inherit;
object-fit: cover;
width: 100%;
height: auto;
user-select: none;
pointer-events: none;
}
.zone-link {
position: absolute;
display: block;
cursor: pointer;
border-radius: 6px;
transition: background 0.2s ease;
z-index: 2;
transition: background 0.2s, box-shadow 0.2s;
&:hover {
background: rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.12);
box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.4);
}
}
}

View File

@@ -10,7 +10,13 @@
>
<!-- 左侧快讯徽章 -->
<div class="notice-badge-box">
<div class="badge-icon">
<img
v-if="options.badgeImg || options.icon"
:src="options.badgeImg || options.icon"
class="badge-custom-img"
alt="icon"
/>
<div v-else class="badge-icon">
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
<path d="M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-7 12h-2v-2h2v2zm0-4h-2V6h2v4z"/>
</svg>
@@ -33,9 +39,13 @@
<span class="notice-item-tag" :class="getTagClass(item.tag)">
{{ item.tag || '最新' }}
</span>
<router-link :to="item.url || item.href || '/article'" class="notice-item-link">
<a
href="javascript:;"
class="notice-item-link"
@click="handleNavigate(item.url || item.href || '/article')"
>
{{ item.title || item.text }}
</router-link>
</a>
<span class="notice-item-time" v-if="item.time && !isMini && !isCompact">
{{ item.time }}
</span>
@@ -45,15 +55,22 @@
<!-- 右侧更多链接 -->
<div class="notice-more-action" v-if="options.showMore !== false && !isMini">
<router-link :to="options.moreUrl || '/article'" class="more-link">
<a
href="javascript:;"
class="more-link"
@click="handleNavigate(options.moreUrl || '/article')"
>
{{ options.moreText || '更多' }} &gt;
</router-link>
</a>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue';
import { useRouter } from 'vue-router';
const router = useRouter();
const props = defineProps({
component: {
@@ -70,6 +87,15 @@ const props = defineProps({
}
});
function handleNavigate(url) {
if (!url) return;
if (/^https?:\/\//i.test(url)) {
window.open(url, '_blank');
} else {
router.push(url);
}
}
const isMini = computed(() => {
const r = props.componentStyle?.widthRatio;
return r === '16.67%' || r === '16.6%' || r === '16.7%';
@@ -149,101 +175,143 @@ const customStyle = computed(() => {
display: flex;
align-items: center;
box-sizing: border-box;
border: 1px solid #eef2f6;
background: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 8px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.02);
gap: 12px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
overflow: hidden;
&.is-mini {
gap: 6px;
padding: 0 8px !important;
.notice-badge-box {
.badge-title, .badge-divider {
display: none;
}
}
}
&.is-compact {
gap: 8px;
padding: 0 10px !important;
.notice-badge-box {
.badge-divider {
display: none;
}
}
}
.notice-badge-box {
display: flex;
align-items: center;
gap: 6px;
gap: 8px;
flex-shrink: 0;
.badge-icon {
color: #ef4444;
color: #f97316;
display: flex;
align-items: center;
justify-content: center;
}
.badge-custom-img {
width: 18px;
height: 18px;
object-fit: contain;
flex-shrink: 0;
}
.badge-title {
font-size: 14px;
font-weight: 800;
font-size: 13px;
font-weight: 700;
color: #0f172a;
white-space: nowrap;
letter-spacing: -0.2px;
}
.badge-divider {
width: 1px;
height: 14px;
background: #e2e8f0;
margin-left: 6px;
background: #cbd5e1;
margin-left: 4px;
}
}
.notice-content-viewport {
flex: 1;
min-width: 0;
height: 40px;
overflow: hidden;
position: relative;
min-width: 0;
.notice-items-strip {
transition: transform 0.4s ease-in-out;
transition: transform 0.5s cubic-bezier(0.25, 1, 0.5, 1);
display: flex;
flex-direction: column;
.notice-strip-item {
height: 40px;
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
box-sizing: border-box;
.notice-item-tag {
font-size: 10px;
font-weight: 700;
font-size: 11px;
padding: 1px 6px;
border-radius: 3px;
border-radius: 4px;
font-weight: 600;
flex-shrink: 0;
&.tag-new {
background: #fee2e2;
color: #dc2626;
}
&.tag-promo {
background: #ffedd5;
color: #ea580c;
}
&.tag-notice {
background: #eff6ff;
color: #2563eb;
border: 1px solid #bfdbfe;
}
&.tag-promo {
background: #fef2f2;
color: #ef4444;
border: 1px solid #fecaca;
}
&.tag-notice {
background: #fffbeb;
color: #d97706;
border: 1px solid #fde68a;
}
&.tag-hot {
background: #fdf2f8;
color: #db2777;
border: 1px solid #fbcfe8;
}
}
.notice-item-link {
flex: 1;
min-width: 0;
font-size: 13px;
color: #334155;
text-decoration: none;
overflow: hidden;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
transition: color 0.2s;
transition: color 0.15s ease;
cursor: pointer;
&:hover {
color: #ef4444;
color: #f97316;
}
}
.notice-item-time {
font-size: 11px;
color: #94a3b8;
white-space: nowrap;
flex-shrink: 0;
margin-right: 4px;
}
}
}
@@ -256,56 +324,11 @@ const customStyle = computed(() => {
font-size: 12px;
color: #64748b;
text-decoration: none;
transition: color 0.2s;
transition: color 0.15s ease;
cursor: pointer;
&:hover {
color: #ef4444;
}
}
}
&.is-compact {
gap: 8px;
.notice-badge-box {
.badge-title {
font-size: 13px;
}
.badge-divider {
margin-left: 2px;
}
}
.notice-content-viewport {
.notice-strip-item {
.notice-item-link {
font-size: 12px;
}
}
}
}
&.is-mini {
gap: 4px;
padding: 0 8px !important;
.notice-badge-box {
.badge-title {
display: none;
}
.badge-divider {
display: none;
}
}
.notice-content-viewport {
.notice-strip-item {
.notice-item-tag {
display: none;
}
.notice-item-link {
font-size: 11px;
}
color: #f97316;
}
}
}

View File

@@ -4,25 +4,28 @@
class="promo-grid"
:style="{ gridTemplateColumns: `repeat(${actualColumns}, 1fr)` }"
>
<router-link
<div
v-for="(card, idx) in cardsList"
:key="idx"
:to="card.url || '/goods'"
class="promo-card"
:style="card.bgImg ? { backgroundImage: `url(${card.bgImg})` } : {}"
@click="handleClickCard(card)"
>
<div class="card-inner">
<h3 class="card-title">{{ card.title }}</h3>
<p class="card-sub">{{ card.subtitle }}</p>
<span class="action-btn">立即查看 ></span>
<span class="action-btn">立即查看 &gt;</span>
</div>
</router-link>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { useRouter } from 'vue-router';
const router = useRouter();
const props = defineProps({
options: {
@@ -75,6 +78,16 @@ const customStyle = computed(() => {
: '10px 0'
};
});
function handleClickCard(card) {
const url = card?.url ? String(card.url).trim() : '';
if (!url) return;
if (/^https?:\/\//i.test(url)) {
window.open(url, '_blank');
} else {
router.push(url);
}
}
</script>
<style lang="scss" scoped>
@@ -98,6 +111,7 @@ const customStyle = computed(() => {
flex-direction: column;
justify-content: center;
color: #ffffff;
cursor: pointer;
transition: all 0.25s ease;
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08);

View File

@@ -22,14 +22,49 @@
<!-- 左侧精选特大卡片 (Featured Hero Card) -->
<div class="featured-big-card" v-if="featuredGoods">
<div class="big-card-badge">为你特别甄选</div>
<router-link :to="`/goods/${featuredGoods.id || 1}`" class="big-card-thumb">
<router-link :to="{ path: '/goodsDetail', query: { skuId: featuredGoods.id, goodsId: featuredGoods.goodsId || featuredGoods.id } }" class="big-card-thumb">
<img :src="featuredGoods.thumbnail || defaultThumb" :alt="featuredGoods.goodsName" />
</router-link>
<div class="big-card-info">
<h4 class="big-title">
<router-link :to="`/goods/${featuredGoods.id || 1}`">{{ featuredGoods.goodsName }}</router-link>
<router-link :to="{ path: '/goodsDetail', query: { skuId: featuredGoods.id, goodsId: featuredGoods.goodsId || featuredGoods.id } }">
{{ featuredGoods.goodsName }}
</router-link>
</h4>
<div class="big-reason">99.8% 高好评 · 猜你对这款高人气科技数码感兴趣</div>
<div class="big-reason">官方甄选 · 猜你对这款高人气商品感兴趣</div>
<!-- 核心特色标签 -->
<div class="big-tags-row">
<span class="feat-tag highlight">🔥 镇店爆品</span>
<span class="feat-tag">🛡️ 官方正品</span>
<span class="feat-tag">⚡ 极速发货</span>
<span class="feat-tag">🏷️ 售后无忧</span>
</div>
<!-- 权威热度指标 -->
<div class="big-stats-bar">
<div class="stat-cell">
<span class="stat-val">100%</span>
<span class="stat-lbl">品质正品</span>
</div>
<div class="stat-line"></div>
<div class="stat-cell">
<span class="stat-val">极速达</span>
<span class="stat-lbl">顺丰速运</span>
</div>
<div class="stat-line"></div>
<div class="stat-cell">
<span class="stat-val">TOP 1</span>
<span class="stat-lbl">品类热榜</span>
</div>
</div>
<!-- 服务保障权益 -->
<div class="big-guarantees">
<div class="guar-item"><span class="guar-icon">✓</span> 7天无理由退换货</div>
<div class="guar-item"><span class="guar-icon">✓</span> 赠运费险 · 破损包赔</div>
</div>
<div class="big-price-row">
<div class="big-price">
<span class="sym">¥</span>
@@ -48,13 +83,13 @@
:key="item.id || idx"
class="recommend-card"
>
<router-link :to="`/goods/${item.id || 1}`" class="card-thumb">
<router-link :to="{ path: '/goodsDetail', query: { skuId: item.id, goodsId: item.goodsId || item.id } }" class="card-thumb">
<img :src="item.thumbnail || defaultThumb" :alt="item.goodsName" />
<span class="rec-tag">推荐</span>
</router-link>
<div class="card-body">
<h4 class="goods-title">
<router-link :to="`/goods/${item.id || 1}`">{{ item.goodsName }}</router-link>
<router-link :to="{ path: '/goodsDetail', query: { skuId: item.id, goodsId: item.goodsId || item.id } }">{{ item.goodsName }}</router-link>
</h4>
<div class="recommend-meta">
<span class="reason-tag">同品类热度榜</span>
@@ -83,17 +118,16 @@
:key="item.id || idx"
class="recommend-card"
>
<router-link :to="`/goods/${item.id || 1}`" class="card-thumb">
<router-link :to="{ path: '/goodsDetail', query: { skuId: item.id, goodsId: item.goodsId || item.id } }" class="card-thumb">
<img :src="item.thumbnail || defaultThumb" :alt="item.goodsName" />
<span class="rec-tag">为你推荐</span>
</router-link>
<div class="card-body">
<h4 class="goods-title">
<router-link :to="`/goods/${item.id || 1}`">{{ item.goodsName }}</router-link>
<router-link :to="{ path: '/goodsDetail', query: { skuId: item.id, goodsId: item.goodsId || item.id } }">{{ item.goodsName }}</router-link>
</h4>
<div class="recommend-meta">
<span class="sales-num">已售 {{ 200 + idx * 70 }}+</span>
<span class="rate-num">98%好评</span>
<span class="reason-tag">甄选推荐</span>
</div>
<div class="price-row">
<div class="price-box">
@@ -333,10 +367,92 @@ function handleRefresh() {
background: #fff7ed;
padding: 4px 8px;
border-radius: 4px;
margin-bottom: 12px;
margin-bottom: 10px;
line-height: 1.3;
}
.big-tags-row {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 10px;
.feat-tag {
font-size: 11px;
color: #475569;
background: #f1f5f9;
padding: 2px 7px;
border-radius: 4px;
font-weight: 500;
&.highlight {
color: #dc2626;
background: #fee2e2;
font-weight: 600;
}
}
}
.big-stats-bar {
display: flex;
align-items: center;
justify-content: space-around;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 6px;
padding: 8px 6px;
margin-bottom: 10px;
.stat-cell {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
.stat-val {
font-size: 13px;
font-weight: 700;
color: #0f172a;
}
.stat-lbl {
font-size: 10px;
color: #64748b;
}
}
.stat-line {
width: 1px;
height: 20px;
background: #cbd5e1;
}
}
.big-guarantees {
display: flex;
flex-direction: column;
gap: 4px;
padding: 6px 10px;
background: #fbfbfb;
border-left: 3px solid #ef4444;
border-radius: 0 4px 4px 0;
margin-bottom: 12px;
.guar-item {
font-size: 11px;
color: #475569;
display: flex;
align-items: center;
gap: 4px;
.guar-icon {
color: #16a34a;
font-weight: 700;
font-size: 11px;
}
}
}
.big-price-row {
margin-top: auto;
display: flex;
@@ -459,9 +575,9 @@ function handleRefresh() {
flex: 1;
.goods-title {
font-size: 12px;
font-size: 14px;
line-height: 1.4;
height: 34px;
height: 40px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;

View File

@@ -6,18 +6,40 @@
isMini ? 'is-mini' : (isCompact ? 'is-compact' : '')
]"
>
<div class="title-wrap">
<h2 class="main-title">{{ options.mainTitle || '楼层标题' }}</h2>
<p class="subtitle" v-if="options.subtitle">{{ options.subtitle }}</p>
<div class="title-wrap" :class="{ 'with-icon': options.titleImg || options.icon }">
<img
v-if="options.titleImg || options.icon"
:src="options.titleImg || options.icon"
class="title-custom-icon"
alt="icon"
/>
<div class="title-text-group">
<h2
class="main-title"
:class="{ 'clickable': Boolean(options.titleUrl) }"
@click="options.titleUrl && handleNavigate(options.titleUrl)"
>
{{ options.mainTitle || '楼层标题' }}
</h2>
<p class="subtitle" v-if="options.subtitle">{{ options.subtitle }}</p>
</div>
</div>
<router-link :to="options.moreUrl || '/goods'" class="more-link" v-if="options.showMore">
<a
href="javascript:;"
class="more-link"
v-if="options.showMore"
@click="handleNavigate(options.moreUrl || '/goods')"
>
{{ options.moreText || '查看更多 >' }}
</router-link>
</a>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { useRouter } from 'vue-router';
const router = useRouter();
const props = defineProps({
options: {
@@ -30,6 +52,15 @@ const props = defineProps({
}
});
function handleNavigate(url) {
if (!url) return;
if (/^https?:\/\//i.test(url)) {
window.open(url, '_blank');
} else {
router.push(url);
}
}
const isMini = computed(() => props.componentStyle?.widthRatio === '16.67%');
const isCompact = computed(() => props.componentStyle?.widthRatio === '33.33%');
</script>
@@ -57,21 +88,43 @@ const isCompact = computed(() => props.componentStyle?.widthRatio === '33.33%');
.title-wrap {
min-width: 0;
display: flex;
align-items: center;
gap: 10px;
.main-title {
font-size: 26px;
font-weight: 800;
color: #0f172a;
margin: 0;
line-height: 1.2;
word-break: break-word;
.title-custom-icon {
width: 28px;
height: 28px;
object-fit: contain;
flex-shrink: 0;
}
.subtitle {
font-size: 14px;
color: #94a3b8;
margin: 6px 0 0 0;
word-break: break-word;
.title-text-group {
min-width: 0;
.main-title {
font-size: 26px;
font-weight: 800;
color: #0f172a;
margin: 0;
line-height: 1.2;
word-break: break-word;
&.clickable {
cursor: pointer;
transition: color 0.15s ease;
&:hover {
color: #f97316;
}
}
}
.subtitle {
font-size: 14px;
color: #94a3b8;
margin: 6px 0 0 0;
word-break: break-word;
}
}
}
@@ -82,6 +135,7 @@ const isCompact = computed(() => props.componentStyle?.widthRatio === '33.33%');
transition: color 0.2s;
white-space: nowrap;
flex-shrink: 0;
cursor: pointer;
&:hover {
color: #e11d48;
@@ -90,12 +144,19 @@ const isCompact = computed(() => props.componentStyle?.widthRatio === '33.33%');
&.is-compact {
.title-wrap {
.main-title {
font-size: 20px;
.title-custom-icon {
width: 22px;
height: 22px;
}
.subtitle {
font-size: 13px;
margin-top: 4px;
.title-text-group {
.main-title {
font-size: 20px;
}
.subtitle {
font-size: 13px;
margin-top: 4px;
}
}
}
.more-link {
@@ -119,12 +180,19 @@ const isCompact = computed(() => props.componentStyle?.widthRatio === '33.33%');
}
.title-wrap {
.main-title {
font-size: 16px;
.title-custom-icon {
width: 18px;
height: 18px;
}
.subtitle {
font-size: 12px;
margin-top: 2px;
.title-text-group {
.main-title {
font-size: 16px;
}
.subtitle {
font-size: 12px;
margin-top: 2px;
}
}
}

View File

@@ -24,84 +24,53 @@
<!-- 2. 登录 / 注册按钮组 -->
<div class="action-btn-row">
<router-link :to="options.loginUrl || '/login'" class="action-btn login-btn">
<a href="javascript:;" class="action-btn login-btn" @click="handleNavigate(options.loginUrl || '/login')">
{{ options.loginText || '登录' }}
</router-link>
<router-link :to="options.registerUrl || '/signUp'" class="action-btn register-btn">
</a>
<a href="javascript:;" class="action-btn register-btn" @click="handleNavigate(options.registerUrl || '/signUp')">
{{ options.registerText || '注册' }}
</router-link>
</a>
</div>
<!-- 3. 指标数据卡 (购物车 / 优惠券 / 订单) -->
<div class="metrics-cards-row">
<router-link :to="options.cartUrl || '/cart'" class="metric-card">
<a href="javascript:;" class="metric-card" v-if="options.showCart !== false" @click="handleNavigate(options.cartUrl || '/cart')">
<div class="metric-val">0</div>
<div class="metric-label">{{ options.cartText || '购物车' }}</div>
</router-link>
<router-link :to="options.couponUrl || '/member/coupon'" class="metric-card">
</a>
<a href="javascript:;" class="metric-card" v-if="options.showCoupons !== false" @click="handleNavigate(options.couponUrl || '/member/coupon')">
<div class="metric-val">0</div>
<div class="metric-label">{{ options.couponText || '优惠券' }}</div>
</router-link>
<router-link :to="options.orderUrl || '/member/order'" class="metric-card">
</a>
<a href="javascript:;" class="metric-card" v-if="options.showOrders !== false" @click="handleNavigate(options.orderUrl || '/member/order')">
<div class="metric-val">0</div>
<div class="metric-label">{{ options.orderText || '订单' }}</div>
</router-link>
</a>
</div>
<!-- 4. 分隔线 -->
<div class="section-divider"></div>
<!-- 5. 热点快报(万米魔方标准组件底部模块) -->
<div class="news-section">
<div class="news-header">
<div class="news-title-wrap">
<span class="news-title">热点快报</span>
<span class="hot-badge">HOT</span>
</div>
<router-link to="/article" class="news-more">查看全部 &gt;</router-link>
</div>
<div class="news-list">
<router-link to="/article" class="news-item" title="精选优质好物 官方正品保障">
<span class="dot">·</span>
<span class="news-text">精选优质好物 官方正品保障</span>
</router-link>
<router-link to="/article" class="news-item" title="双11狂欢 跨店每满300减50">
<span class="dot">·</span>
<span class="news-text">双11狂欢 跨店每满300减50</span>
</router-link>
<router-link to="/article" class="news-item" title="新人专享好礼 注册立领大礼包">
<span class="dot">·</span>
<span class="news-text">新人专享好礼 注册立领大礼包</span>
</router-link>
</div>
</div>
<!-- 6. 快捷便民服务入口(底部图标排) -->
<!-- 4. 快捷便民服务入口(底部图标排) -->
<div class="quick-service-row">
<router-link to="/member/favorites" class="service-item">
<el-icon class="service-icon"><Star /></el-icon>
<span class="service-name">收藏</span>
</router-link>
<router-link to="/member/footprint" class="service-item">
<el-icon class="service-icon"><Clock /></el-icon>
<span class="service-name">足迹</span>
</router-link>
<router-link to="/couponCenter" class="service-item">
<el-icon class="service-icon"><Present /></el-icon>
<span class="service-name">红包</span>
</router-link>
<router-link to="/article" class="service-item">
<el-icon class="service-icon"><Service /></el-icon>
<span class="service-name">客服</span>
</router-link>
<a
v-for="(srv, sIdx) in displayServices"
:key="sIdx"
href="javascript:;"
class="service-item"
@click="handleNavigate(srv.url)"
>
<el-icon class="service-icon"><component :is="getServiceIcon(srv.icon)" /></el-icon>
<span class="service-name">{{ srv.name }}</span>
</a>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { useRouter } from 'vue-router';
import { Star, Clock, Present, Service } from '@element-plus/icons-vue';
const router = useRouter();
const props = defineProps({
options: {
type: Object,
@@ -110,16 +79,57 @@ const props = defineProps({
componentStyle: {
type: Object,
default: () => ({})
},
rowStyle: {
type: Object,
default: () => ({})
}
});
function handleNavigate(url) {
if (!url) return;
if (/^https?:\/\//i.test(url)) {
window.open(url, '_blank');
} else {
router.push(url);
}
}
const defaultServices = [
{ name: '收藏', url: '/member/favorites', icon: 'Star' },
{ name: '足迹', url: '/member/footprint', icon: 'Clock' },
{ name: '红包', url: '/couponCenter', icon: 'Present' },
{ name: '客服', url: '/article', icon: 'Service' }
];
const iconMap = {
Star,
Clock,
Present,
Service
};
function getServiceIcon(iconName) {
return iconMap[iconName] || Service;
}
const displayServices = computed(() => {
if (Array.isArray(props.options?.services) && props.options.services.length > 0) {
return props.options.services;
}
return defaultServices;
});
const customStyle = computed(() => {
const s = props.componentStyle || {};
const hasRowHeight = Boolean(props.rowStyle?.height && Number(props.rowStyle.height) > 0);
return {
backgroundColor: s.backgroundColor || '#ffffff',
borderRadius: Array.isArray(s.radius)
backgroundColor: (s.backgroundColor && s.backgroundColor !== 'transparent') ? s.backgroundColor : '#ffffff',
borderRadius: Array.isArray(s.radius) && s.radius.some(r => r > 0)
? `${s.radius[0]}px ${s.radius[1]}px ${s.radius[2]}px ${s.radius[3]}px`
: '8px'
: '8px',
height: '100%',
minHeight: hasRowHeight ? '0' : '420px'
};
});
</script>
@@ -128,17 +138,18 @@ const customStyle = computed(() => {
.decor-user-benefits {
width: 100%;
height: 100%;
min-height: 420px;
box-sizing: border-box;
background: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
padding: 14px 12px 10px;
padding: 24px 16px 20px;
gap: 12px;
display: flex;
flex-direction: column;
justify-content: space-between;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
overflow: hidden;
.user-main-info {
display: flex;
@@ -146,8 +157,8 @@ const customStyle = computed(() => {
gap: 8px;
.avatar-box {
width: 40px;
height: 40px;
width: 48px;
height: 48px;
border-radius: 50%;
overflow: hidden;
flex-shrink: 0;
@@ -178,7 +189,7 @@ const customStyle = computed(() => {
.badge-tag {
align-self: flex-start;
padding: 1px 6px;
background: #fff2e8;
background: #ecf5ff;
color: #409EFF;
font-size: 10px;
font-weight: 700;
@@ -211,25 +222,25 @@ const customStyle = computed(() => {
.action-btn-row {
display: flex;
gap: 8px;
margin-top: 10px;
margin-top: 8px;
.action-btn {
flex: 1;
height: 28px;
line-height: 28px;
height: 32px;
line-height: 32px;
font-size: 12px;
font-weight: 500;
text-align: center;
border-radius: 14px;
cursor: pointer;
box-sizing: border-box;
text-decoration: none;
box-sizing: border-box;
transition: all 0.15s ease;
&.login-btn {
background: #409EFF;
color: #ffffff;
box-shadow: 0 2px 6px rgba(255, 96, 0, 0.25);
box-shadow: 0 2px 6px rgba(64, 158, 255, 0.25);
&:hover {
background: #66b1ff;
@@ -242,7 +253,7 @@ const customStyle = computed(() => {
border: 1px solid #409EFF;
&:hover {
background: #fff7ed;
background: #ecf5ff;
}
}
}
@@ -252,22 +263,23 @@ const customStyle = computed(() => {
display: flex;
align-items: center;
justify-content: space-around;
padding: 8px 0;
margin-top: 10px;
padding: 12px 0;
margin-top: 6px;
background: #f8fafc;
border-radius: 6px;
.metric-card {
text-align: center;
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
text-decoration: none;
cursor: pointer;
.metric-val {
font-size: 15px;
font-weight: 700;
color: #0f172a;
line-height: 1.1;
color: #409EFF;
line-height: 1.2;
}
.metric-label {
@@ -275,131 +287,42 @@ const customStyle = computed(() => {
color: #64748b;
margin-top: 2px;
}
&:hover {
.metric-val, .metric-label {
color: #409EFF;
}
}
}
}
.section-divider {
height: 1px;
background: #f1f5f9;
margin: 8px 0 6px;
}
.news-section {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
padding: 0 2px;
.news-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
.news-title-wrap {
display: flex;
align-items: center;
gap: 4px;
.news-title {
font-size: 12px;
font-weight: 700;
color: #1e293b;
}
.hot-badge {
background: #ef4444;
color: #ffffff;
font-size: 9px;
font-weight: 700;
padding: 0 3px;
border-radius: 2px;
line-height: 1.2;
}
}
.news-more {
font-size: 11px;
color: #94a3b8;
text-decoration: none;
cursor: pointer;
&:hover {
color: #409EFF;
}
}
}
.news-list {
display: flex;
flex-direction: column;
gap: 5px;
.news-item {
display: flex;
align-items: center;
gap: 4px;
font-size: 11px;
color: #475569;
text-decoration: none;
cursor: pointer;
line-height: 1.3;
transition: color 0.15s;
.dot {
color: #409EFF;
font-weight: 700;
}
.news-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&:hover {
color: #409EFF;
}
}
}
}
.quick-service-row {
display: flex;
align-items: center;
justify-content: space-around;
padding-top: 8px;
align-items: center;
padding-top: 14px;
border-top: 1px dashed #f1f5f9;
margin-top: 4px;
margin-top: 6px;
.service-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
text-decoration: none;
cursor: pointer;
color: #64748b;
transition: color 0.15s;
text-decoration: none;
transition: color 0.15s ease;
.service-icon {
font-size: 14px;
font-size: 20px;
transition: transform 0.15s ease;
}
.service-name {
font-size: 10px;
line-height: 1;
font-size: 11px;
}
&:hover {
color: #409EFF;
.service-icon {
transform: translateY(-2px);
}
}
}
}

View File

@@ -1,5 +1,10 @@
<template>
<div class="decor-video-player" :style="customStyle">
<div
class="decor-video-player"
:class="{ 'has-link': !!options.url }"
:style="customStyle"
@click="handleClickPlaceholder"
>
<video
v-if="options.videoUrl"
:src="options.videoUrl"
@@ -21,14 +26,29 @@
>
<div class="video-play-hint">
<span class="play-icon">▶</span>
<span class="hint-text">官方品牌宣传片</span>
<span class="hint-text">{{ options.title || '官方品牌宣传片' }}</span>
</div>
</div>
<!-- 跳转行动点徽标(如果配置了链接) -->
<div
v-if="options.url"
class="video-cta-badge"
@click.stop="handleNavigate"
title="点击查看详情"
>
<span class="cta-dot"></span>
<span class="cta-text">查看活动详情</span>
<span class="cta-arrow">&gt;</span>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { useRouter } from 'vue-router';
const router = useRouter();
const props = defineProps({
options: {
@@ -49,14 +69,37 @@ const customStyle = computed(() => {
: '8px'
};
});
function handleNavigate() {
const url = props.options?.url ? String(props.options.url).trim() : '';
if (!url) return;
if (/^https?:\/\//i.test(url)) {
window.open(url, '_blank');
} else {
router.push(url);
}
}
function handleClickPlaceholder() {
if (!props.options?.videoUrl && props.options?.url) {
handleNavigate();
}
}
</script>
<style lang="scss" scoped>
.decor-video-player {
width: 100%;
position: relative;
overflow: hidden;
box-sizing: border-box;
&.has-link {
.video-placeholder {
cursor: pointer;
}
}
.video-element {
width: 100%;
object-fit: cover;
@@ -87,6 +130,7 @@ const customStyle = computed(() => {
border-radius: 12px;
backdrop-filter: blur(4px);
border: 1px solid rgba(255, 255, 255, 0.15);
transition: transform 0.2s ease, background 0.2s ease;
.play-icon {
width: 48px;
@@ -109,6 +153,61 @@ const customStyle = computed(() => {
letter-spacing: 0.5px;
}
}
&:hover .video-play-hint {
transform: scale(1.04);
background: rgba(15, 23, 42, 0.85);
}
}
.video-cta-badge {
position: absolute;
top: 16px;
right: 16px;
display: flex;
align-items: center;
gap: 6px;
padding: 6px 14px;
background: rgba(15, 23, 42, 0.8);
border: 1px solid rgba(255, 255, 255, 0.25);
border-radius: 20px;
backdrop-filter: blur(8px);
color: #ffffff;
font-size: 12px;
font-weight: 500;
cursor: pointer;
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.25);
transition: all 0.2s ease;
z-index: 10;
.cta-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: #e11d48;
box-shadow: 0 0 8px #e11d48;
}
.cta-text {
letter-spacing: 0.3px;
}
.cta-arrow {
font-size: 11px;
opacity: 0.8;
transition: transform 0.2s ease;
}
&:hover {
background: rgba(225, 29, 72, 0.9);
border-color: rgba(255, 255, 255, 0.4);
transform: translateY(-1px);
box-shadow: 0 6px 18px rgba(225, 29, 72, 0.35);
.cta-arrow {
transform: translateX(2px);
}
}
}
}
</style>

View File

@@ -22,7 +22,8 @@ export const DOUBLE11_PAGE_DOCUMENT = {
0,
0,
0
]
],
"height": 40
},
"components": [
{
@@ -77,7 +78,8 @@ export const DOUBLE11_PAGE_DOCUMENT = {
0,
12,
0
]
],
"height": 120
},
"components": [
{
@@ -99,7 +101,10 @@ export const DOUBLE11_PAGE_DOCUMENT = {
"华为Mate 70",
"五粮液普五",
"耐克碳板跑鞋"
]
],
"showSearchTypes": true,
"defaultSearchType": "goods",
"storePlaceholder": "请输入店铺名称"
}
}
]
@@ -120,7 +125,8 @@ export const DOUBLE11_PAGE_DOCUMENT = {
0,
0,
0
]
],
"height": 48
},
"components": [
{
@@ -193,7 +199,8 @@ export const DOUBLE11_PAGE_DOCUMENT = {
],
"justifyContent": "space-between",
"alignItems": "stretch",
"gap": 12
"gap": 12,
"height": 320
},
"components": [
{
@@ -366,7 +373,8 @@ export const DOUBLE11_PAGE_DOCUMENT = {
4,
12
],
"borderRadius": "8px"
"borderRadius": "8px",
"height": 40
},
"components": [
{
@@ -427,7 +435,8 @@ export const DOUBLE11_PAGE_DOCUMENT = {
16,
14
],
"borderRadius": "8px"
"borderRadius": "8px",
"height": 120
},
"components": [
{
@@ -532,7 +541,8 @@ export const DOUBLE11_PAGE_DOCUMENT = {
0,
0,
0
]
],
"height": 48
},
"components": [
{
@@ -569,7 +579,8 @@ export const DOUBLE11_PAGE_DOCUMENT = {
0,
0,
0
]
],
"height": 160
},
"components": [
{
@@ -634,7 +645,8 @@ export const DOUBLE11_PAGE_DOCUMENT = {
0,
0,
0
]
],
"height": 750
},
"components": [
{
@@ -711,7 +723,8 @@ export const DOUBLE11_PAGE_DOCUMENT = {
0,
0,
0
]
],
"height": 180
},
"components": [
{
@@ -771,7 +784,8 @@ export const DOUBLE11_PAGE_DOCUMENT = {
0,
0
],
"justifyContent": "space-between"
"justifyContent": "space-between",
"height": 360
},
"components": [
{
@@ -874,7 +888,8 @@ export const DOUBLE11_PAGE_DOCUMENT = {
14,
20
],
"borderRadius": "8px"
"borderRadius": "8px",
"height": 450
},
"components": [
{
@@ -924,7 +939,8 @@ export const DOUBLE11_PAGE_DOCUMENT = {
0,
0,
0
]
],
"height": 48
},
"components": [
{
@@ -962,7 +978,8 @@ export const DOUBLE11_PAGE_DOCUMENT = {
16,
16
],
"borderRadius": "8px"
"borderRadius": "8px",
"height": 560
},
"components": [
{
@@ -1043,7 +1060,8 @@ export const DOUBLE11_PAGE_DOCUMENT = {
16,
16
],
"borderRadius": "8px"
"borderRadius": "8px",
"height": 560
},
"components": [
{
@@ -1121,7 +1139,8 @@ export const DOUBLE11_PAGE_DOCUMENT = {
16,
16
],
"borderRadius": "8px"
"borderRadius": "8px",
"height": 620
},
"components": [
{
@@ -1190,7 +1209,8 @@ export const DOUBLE11_PAGE_DOCUMENT = {
0,
0,
0
]
],
"height": 48
},
"components": [
{
@@ -1203,7 +1223,7 @@ export const DOUBLE11_PAGE_DOCUMENT = {
},
"options": {
"title": "双11爆款必买热销榜",
"subtitle": "TOP RANKINGS · 亿万买家真实下单好评热选",
"subtitle": "TOP RANKINGS · 平台高人气甄选热卖",
"tag": "实时榜单",
"moreText": "查看完整榜单 >",
"moreUrl": "/goodsList"
@@ -1228,7 +1248,8 @@ export const DOUBLE11_PAGE_DOCUMENT = {
16,
16
],
"borderRadius": "8px"
"borderRadius": "8px",
"height": 420
},
"components": [
{
@@ -1343,7 +1364,8 @@ export const DOUBLE11_PAGE_DOCUMENT = {
0,
0,
0
]
],
"height": 48
},
"components": [
{
@@ -1381,7 +1403,8 @@ export const DOUBLE11_PAGE_DOCUMENT = {
16,
16
],
"borderRadius": "8px"
"borderRadius": "8px",
"height": 360
},
"components": [
{
@@ -1395,6 +1418,7 @@ export const DOUBLE11_PAGE_DOCUMENT = {
"options": {
"videoUrl": "https://vjs.zencdn.net/v/oceans.mp4",
"coverUrl": "https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/video/video_cover.png",
"url": "/goodsList?brand=1",
"title": "双11全球狂欢发布盛典 · 年度旗舰好物真实开箱测评",
"autoplay": false
}
@@ -1417,7 +1441,8 @@ export const DOUBLE11_PAGE_DOCUMENT = {
0,
0,
0
]
],
"height": 48
},
"components": [
{
@@ -1455,7 +1480,8 @@ export const DOUBLE11_PAGE_DOCUMENT = {
16,
16
],
"borderRadius": "8px"
"borderRadius": "8px",
"height": 220
},
"components": [
{
@@ -1513,7 +1539,8 @@ export const DOUBLE11_PAGE_DOCUMENT = {
0,
0,
0
]
],
"height": 48
},
"components": [
{
@@ -1551,7 +1578,8 @@ export const DOUBLE11_PAGE_DOCUMENT = {
20,
24
],
"borderRadius": "8px"
"borderRadius": "8px",
"height": ""
},
"components": [
{
@@ -1588,7 +1616,8 @@ export const DOUBLE11_PAGE_DOCUMENT = {
0,
0,
0
]
],
"height": 48
},
"components": [
{
@@ -1626,7 +1655,8 @@ export const DOUBLE11_PAGE_DOCUMENT = {
16,
16
],
"borderRadius": "8px"
"borderRadius": "8px",
"height": 980
},
"components": [
{
@@ -1740,7 +1770,8 @@ export const DOUBLE11_PAGE_DOCUMENT = {
0,
0,
0
]
],
"height": 48
},
"components": [
{
@@ -1778,7 +1809,8 @@ export const DOUBLE11_PAGE_DOCUMENT = {
16,
16
],
"borderRadius": "8px"
"borderRadius": "8px",
"height": 1270
},
"components": [
{
@@ -1893,7 +1925,8 @@ export const DOUBLE11_PAGE_DOCUMENT = {
0,
24,
0
]
],
"height": 450
},
"components": [
{