mirror of
https://gitee.com/beijing_hongye_huicheng/lilishop-ui.git
synced 2026-11-08 01:48:56 +08:00
feat(pc-decorate): PC端楼层装修组件体系规范默认行高与各楼层组件重构
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 || '查看全部' }} >
|
||||
</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;
|
||||
|
||||
@@ -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 || '查看更多' }} >
|
||||
</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>
|
||||
@@ -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')">
|
||||
查看更多 >
|
||||
</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>立即抢购 ></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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 || '更多' }} >
|
||||
</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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">立即查看 ></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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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">查看全部 ></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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">></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>
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user