mirror of
https://gitee.com/beijing_hongye_huicheng/lilishop-ui.git
synced 2026-11-06 17:09:08 +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": [
|
||||
{
|
||||
|
||||
@@ -214,7 +214,7 @@ if (props.options.showIndicator === undefined) props.options.showIndicator = tru
|
||||
if (!props.options.effect) props.options.effect = 'fade';
|
||||
if (!props.options.fitMode) props.options.fitMode = 'contain';
|
||||
if (!props.options.aspectRatio) props.options.aspectRatio = '2.5:1';
|
||||
if (props.options.height === undefined) props.options.height = 380;
|
||||
if (props.options.height === undefined) props.options.height = 420;
|
||||
if (!Array.isArray(props.options.list)) {
|
||||
props.options.list = [
|
||||
{ title: '出游囤好货', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/carousel/banner_1.png', url: '/goodsList?seckill=1' },
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<div class="title-with-tip">
|
||||
<span>内容设置</span>
|
||||
<el-tooltip
|
||||
content="设置类目弹层的文字颜色及显示字号"
|
||||
content="设置商品分类的文字颜色及显示字号"
|
||||
placement="top"
|
||||
>
|
||||
<el-icon class="help-icon"><QuestionFilled /></el-icon>
|
||||
@@ -69,14 +69,26 @@
|
||||
<el-input
|
||||
v-model="options.activeColor"
|
||||
size="small"
|
||||
placeholder="#ea580c"
|
||||
placeholder="#409EFF"
|
||||
class="color-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分类项列表 -->
|
||||
<div class="categories-sub-section mt-20">
|
||||
<!-- 数据来源 -->
|
||||
<div class="form-row flex-col mt-16">
|
||||
<span class="row-label">数据来源</span>
|
||||
<el-radio-group v-model="options.source" size="small">
|
||||
<el-radio-button label="system">系统分类 (自动)</el-radio-button>
|
||||
<el-radio-button label="custom">手动自定义</el-radio-button>
|
||||
</el-radio-group>
|
||||
<span class="form-tip mt-6" v-if="options.source !== 'custom'">
|
||||
已启用系统商品分类,前后台自动拉取商城最新分类及二级、三级悬浮大菜单。
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 分类项列表 (仅自定义模式可见) -->
|
||||
<div class="categories-sub-section mt-20" v-if="options.source === 'custom'">
|
||||
<div class="sub-header flex-between">
|
||||
<span class="sub-title">分类条目</span>
|
||||
<span class="sub-count">共 {{ (options.categories || []).length }} 项</span>
|
||||
@@ -153,6 +165,7 @@ const props = defineProps({
|
||||
});
|
||||
|
||||
// 初始化默认字段
|
||||
if (!props.options.source) props.options.source = 'system';
|
||||
if (!props.options.title) props.options.title = '分类';
|
||||
if (!props.options.textColor) props.options.textColor = '#111927';
|
||||
if (!props.options.fontSize) props.options.fontSize = 14;
|
||||
|
||||
@@ -3,46 +3,142 @@
|
||||
<!-- 1. 标题区设置 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">标题区设置</div>
|
||||
<div class="form-item">
|
||||
<div class="form-item flex-row">
|
||||
<span class="item-label">展示标题区</span>
|
||||
<el-switch v-model="options.showTitle" size="small" />
|
||||
</div>
|
||||
<template v-if="options.showTitle !== false">
|
||||
<div class="form-item">
|
||||
<span class="item-label">主标题</span>
|
||||
<el-input v-model="options.title" size="small" placeholder="如:场景频道" />
|
||||
<el-input v-model="options.title" size="small" placeholder="如:场景频道" clearable />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">副标题</span>
|
||||
<el-input v-model="options.subtitle" size="small" placeholder="如:主视觉 + 商品组合楼层" />
|
||||
<el-input v-model="options.subtitle" size="small" placeholder="如:主视觉 + 商品组合楼层" clearable />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">“更多”跳转链接 (选填)</span>
|
||||
<div class="link-picker-wrap">
|
||||
<el-input
|
||||
v-model="options.moreUrl"
|
||||
size="small"
|
||||
placeholder="默认跟随主视觉链接,如 /goods"
|
||||
clearable
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon class="link-input-icon"><Link /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
size="small"
|
||||
class="action-picker-btn link-btn"
|
||||
@click="handleSelectMoreLink"
|
||||
>
|
||||
<el-icon class="mr-4"><Link /></el-icon>
|
||||
<span>选择链接</span>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="link-current-tag" v-if="options.moreUrl">
|
||||
<span class="tag-label">当前链接:</span>
|
||||
<span class="tag-val" :title="options.moreUrl">{{ options.moreUrl }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 2. 主视觉设置 (Hero Section) -->
|
||||
<!-- 2. 场景主视觉 (Hero Section) -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">场景主视觉</div>
|
||||
|
||||
<!-- 主视觉图片 -->
|
||||
<div class="form-item">
|
||||
<span class="item-label">主视觉图片</span>
|
||||
<el-input v-model="options.heroImage" size="small" placeholder="建议尺寸: 520x560px" clearable />
|
||||
<span class="item-label">主视觉图片 (建议尺寸 520x560px)</span>
|
||||
<div class="picker-flex-wrap">
|
||||
<div
|
||||
class="thumb-preview-box"
|
||||
@click="handleSelectHeroImage"
|
||||
:title="options.heroImage ? '点击更换主视觉图片' : '点击选择图片'"
|
||||
>
|
||||
<template v-if="options.heroImage">
|
||||
<img :src="options.heroImage" alt="主视觉" class="preview-img" />
|
||||
<span class="thumb-clear-btn" @click.stop="options.heroImage = ''" title="清除图片">
|
||||
<el-icon><Close /></el-icon>
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-icon class="thumb-empty-icon"><Picture /></el-icon>
|
||||
<span class="thumb-empty-tip">选图</span>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="input-with-action flex-1">
|
||||
<el-input
|
||||
v-model="options.heroImage"
|
||||
size="small"
|
||||
placeholder="请输入或选择主视觉图片"
|
||||
clearable
|
||||
/>
|
||||
<el-button
|
||||
size="small"
|
||||
class="action-picker-btn"
|
||||
@click="handleSelectHeroImage"
|
||||
>
|
||||
<el-icon class="mr-4"><Picture /></el-icon>
|
||||
<span>选择图片</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
|
||||
<div class="form-item mt-10">
|
||||
<span class="item-label">主视觉大标题</span>
|
||||
<el-input v-model="options.heroTitle" size="small" placeholder="如:品质生活馆" />
|
||||
<el-input v-model="options.heroTitle" size="small" placeholder="如:品质生活馆" clearable />
|
||||
</div>
|
||||
|
||||
<div class="form-item">
|
||||
<span class="item-label">主视觉副标题</span>
|
||||
<el-input v-model="options.heroSubtitle" size="small" placeholder="如:精选场景好物,一站购齐" />
|
||||
<el-input v-model="options.heroSubtitle" size="small" placeholder="如:精选场景好物,一站购齐" clearable />
|
||||
</div>
|
||||
|
||||
<div class="form-item">
|
||||
<span class="item-label">行动按钮文案</span>
|
||||
<el-input v-model="options.heroButtonText" size="small" placeholder="如:进入频道" />
|
||||
<el-input v-model="options.heroButtonText" size="small" placeholder="如:进入频道" clearable />
|
||||
</div>
|
||||
|
||||
<!-- 按钮跳转链接 -->
|
||||
<div class="form-item">
|
||||
<span class="item-label">按钮跳转链接</span>
|
||||
<el-input v-model="options.heroHref" size="small" placeholder="如:/goods" />
|
||||
<div class="link-picker-wrap">
|
||||
<el-input
|
||||
v-model="options.heroHref"
|
||||
size="small"
|
||||
placeholder="如 /goods 或活动页面链接"
|
||||
clearable
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon class="link-input-icon"><Link /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
size="small"
|
||||
class="action-picker-btn link-btn"
|
||||
@click="handleSelectHeroLink"
|
||||
>
|
||||
<el-icon class="mr-4"><Link /></el-icon>
|
||||
<span>选择链接</span>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="link-current-tag" v-if="options.heroHref">
|
||||
<span class="tag-label">当前链接:</span>
|
||||
<span class="tag-val" :title="options.heroHref">{{ options.heroHref }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
|
||||
<div class="form-item flex-row mt-10">
|
||||
<span class="item-label">主视觉视觉风格</span>
|
||||
<el-radio-group v-model="options.heroStyle" size="small">
|
||||
<el-radio-button label="gradient">主题渐变</el-radio-button>
|
||||
@@ -54,38 +150,113 @@
|
||||
<!-- 3. 活动入口 (Promos) -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title flex-between">
|
||||
<span>活动入口 ({{ options.promos?.length || 0 }} 个)</span>
|
||||
<span>专属活动入口 ({{ options.promos?.length || 0 }} 个)</span>
|
||||
<el-button link type="primary" size="small" @click="handleAddPromo">
|
||||
<el-icon><Plus /></el-icon> 添加活动
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="promos-list-editor">
|
||||
<div class="promos-list-editor" v-if="Array.isArray(options.promos) && options.promos.length">
|
||||
<div v-for="(p, pIdx) in options.promos" :key="pIdx" class="promo-editor-card">
|
||||
<div class="promo-row-1">
|
||||
<el-input v-model="p.tag" size="small" placeholder="标签(限时/新品)" style="width: 100px" />
|
||||
<el-input v-model="p.title" size="small" placeholder="活动主标题" style="flex: 1" />
|
||||
<el-button link type="danger" size="small" @click="options.promos.splice(pIdx, 1)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
<!-- 顶部标签与排序按钮 -->
|
||||
<div class="promo-card-header">
|
||||
<span class="promo-idx-badge">入口 {{ pIdx + 1 }}{{ p.title ? ` · ${p.title}` : '' }}</span>
|
||||
<div class="promo-actions">
|
||||
<el-button
|
||||
link
|
||||
size="small"
|
||||
:disabled="pIdx === 0"
|
||||
@click="movePromo(pIdx, -1)"
|
||||
title="上移"
|
||||
>
|
||||
<el-icon><Top /></el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
size="small"
|
||||
:disabled="pIdx === options.promos.length - 1"
|
||||
@click="movePromo(pIdx, 1)"
|
||||
title="下移"
|
||||
>
|
||||
<el-icon><Bottom /></el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="options.promos.splice(pIdx, 1)"
|
||||
title="删除入口"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="promo-row-2">
|
||||
<el-input v-model="p.desc" size="small" placeholder="活动描述(如: 领券最高减100)" />
|
||||
|
||||
<!-- 属性输入 -->
|
||||
<div class="promo-row-1">
|
||||
<div class="sub-col" style="width: 100px;">
|
||||
<span class="micro-label">角标标签</span>
|
||||
<el-input v-model="p.tag" size="small" placeholder="限时/新品" clearable />
|
||||
</div>
|
||||
<div class="sub-col flex-1">
|
||||
<span class="micro-label">活动标题</span>
|
||||
<el-input v-model="p.title" size="small" placeholder="如:大牌补贴" clearable />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="promo-row-2 mt-6">
|
||||
<span class="micro-label">活动描述</span>
|
||||
<el-input v-model="p.desc" size="small" placeholder="如:领券最高减100" clearable />
|
||||
</div>
|
||||
|
||||
<!-- 活动链接选择器 -->
|
||||
<div class="promo-row-3 mt-6">
|
||||
<span class="micro-label">跳转链接</span>
|
||||
<div class="link-picker-wrap">
|
||||
<el-input
|
||||
v-model="p.url"
|
||||
size="small"
|
||||
placeholder="如 /goods 或活动专题页"
|
||||
clearable
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon class="link-input-icon"><Link /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
size="small"
|
||||
class="action-picker-btn link-btn"
|
||||
@click="handleSelectPromoLink(p)"
|
||||
>
|
||||
<el-icon class="mr-4"><Link /></el-icon>
|
||||
<span>选择链接</span>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="link-current-tag" v-if="p.url">
|
||||
<span class="tag-label">当前链接:</span>
|
||||
<span class="tag-val" :title="p.url">{{ p.url }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="empty-tip" v-else>
|
||||
暂无活动入口,点击右上角“添加活动”
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 4. 场景商品选择 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title flex-between">
|
||||
<span>场景商品位 ({{ options.goods?.length || 0 }} 件)</span>
|
||||
<el-button type="primary" size="small" @click="showGoodsModal = true">
|
||||
选择真实商品
|
||||
</el-button>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<el-button v-if="options.goods?.length" link type="danger" size="small" @click="options.goods = []">清空</el-button>
|
||||
<el-button type="primary" size="small" @click="handleOpenGoodsPicker">选择商品</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-item">
|
||||
<div class="form-item flex-row">
|
||||
<span class="item-label">展示商品数量</span>
|
||||
<el-input-number v-model="options.goodsCount" :min="1" :max="8" size="small" controls-position="right" />
|
||||
</div>
|
||||
@@ -104,34 +275,32 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 商品选择弹窗 -->
|
||||
<el-dialog v-model="showGoodsModal" title="选择场景商品" width="600px" append-to-body>
|
||||
<div class="modal-goods-pool">
|
||||
<div
|
||||
v-for="item in mockPool"
|
||||
:key="item.id"
|
||||
class="pool-item"
|
||||
:class="{ selected: isSelected(item.id) }"
|
||||
@click="toggleSelect(item)"
|
||||
>
|
||||
<img :src="item.thumbnail" class="pool-thumb" alt="" />
|
||||
<div class="pool-meta">
|
||||
<div class="pool-name">{{ item.goodsName }}</div>
|
||||
<div class="pool-price">¥{{ item.price }}</div>
|
||||
</div>
|
||||
<el-icon v-if="isSelected(item.id)" class="check-icon"><Check /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 商品选择弹窗 (兜底) -->
|
||||
<el-dialog
|
||||
v-if="!openGoodsPicker && showGoodsModal"
|
||||
v-model="showGoodsModal"
|
||||
title="从商品库选择商品"
|
||||
width="1160px"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
>
|
||||
<goods-dialog
|
||||
:selectedWay="tempSelectedGoods"
|
||||
@selected="(val) => { tempSelectedGoods = val; }"
|
||||
/>
|
||||
<template #footer>
|
||||
<el-button @click="showGoodsModal = false">完成</el-button>
|
||||
<el-button @click="showGoodsModal = false">取消</el-button>
|
||||
<el-button type="primary" @click="confirmGoodsSelected">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { Plus, Delete, Check } from '@element-plus/icons-vue';
|
||||
import { ref, inject } from 'vue';
|
||||
import GoodsDialog from '@/components/lili-dialog/goods-dialog.vue';
|
||||
import { Plus, Delete, Link, Picture, Close, Top, Bottom } from '@element-plus/icons-vue';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
@@ -146,7 +315,14 @@ const props = defineProps({
|
||||
|
||||
const defaultThumb = 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_1.png';
|
||||
const showGoodsModal = ref(false);
|
||||
const tempSelectedGoods = ref([]);
|
||||
|
||||
// 注入来自 pc/index.vue 的全局选择器
|
||||
const openImagePicker = inject('openImagePicker', null);
|
||||
const openLinkPicker = inject('openLinkPicker', null);
|
||||
const openGoodsPicker = inject('openGoodsPicker', null);
|
||||
|
||||
// 默认值兜底
|
||||
if (props.options.showTitle === undefined) props.options.showTitle = true;
|
||||
if (!props.options.title) props.options.title = '场景频道';
|
||||
if (!props.options.subtitle) props.options.subtitle = '主视觉 + 商品组合楼层';
|
||||
@@ -174,15 +350,70 @@ if (!props.options.goods) {
|
||||
];
|
||||
}
|
||||
|
||||
const mockPool = [
|
||||
{ id: '1', goodsName: '旗舰 5G 智能手机 12GB+256GB 超光影影像系统', price: '3999.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_1.png' },
|
||||
{ id: '2', goodsName: '无线头戴式深度主动降噪蓝牙耳机 空间音频Hi-Res', price: '1099.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_2.png' },
|
||||
{ id: '3', goodsName: '14英寸 全面屏轻薄商务笔记本电脑 酷睿i7 高刷防眩光', price: '5699.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_3.png' },
|
||||
{ id: '4', goodsName: '运动健康专业监测智能手表 心率血氧长续航50米防水', price: '899.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_4.png' },
|
||||
{ id: '5', goodsName: '意式半自动家用复古咖啡机 20Bar高压恒温萃取蒸汽奶泡', price: '899.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_6.png' },
|
||||
{ id: '6', goodsName: '奢宠酵母焕活修护精华露 50ml 紧致淡纹抗皱滋养', price: '680.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_5.png' }
|
||||
];
|
||||
// 1. 选择主视觉图片
|
||||
function handleSelectHeroImage() {
|
||||
if (openImagePicker) {
|
||||
openImagePicker((url) => {
|
||||
if (url) props.options.heroImage = url;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入主视觉图片 URL 地址:', '选择主视觉图片', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: props.options.heroImage || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '图片地址不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
props.options.heroImage = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 选择主视觉按钮链接
|
||||
function handleSelectHeroLink() {
|
||||
if (openLinkPicker) {
|
||||
openLinkPicker(({ url }) => {
|
||||
if (url) props.options.heroHref = url;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入主视觉跳转链接(如 /goods 或活动网址):', '设置主视觉链接', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: props.options.heroHref || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '链接不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
props.options.heroHref = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 选择“探索更多”链接
|
||||
function handleSelectMoreLink() {
|
||||
if (openLinkPicker) {
|
||||
openLinkPicker(({ url }) => {
|
||||
if (url) props.options.moreUrl = url;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入“更多”跳转链接:', '设置更多跳转链接', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: props.options.moreUrl || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '链接不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
props.options.moreUrl = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 活动入口增删与排序
|
||||
function handleAddPromo() {
|
||||
if (!Array.isArray(props.options.promos)) props.options.promos = [];
|
||||
props.options.promos.push({
|
||||
@@ -193,17 +424,62 @@ function handleAddPromo() {
|
||||
});
|
||||
}
|
||||
|
||||
function isSelected(id) {
|
||||
return props.options.goods?.some(g => String(g.id) === String(id));
|
||||
function movePromo(index, step) {
|
||||
const target = index + step;
|
||||
if (target < 0 || target >= props.options.promos.length) return;
|
||||
const item = props.options.promos.splice(index, 1)[0];
|
||||
props.options.promos.splice(target, 0, item);
|
||||
}
|
||||
|
||||
function toggleSelect(item) {
|
||||
if (!Array.isArray(props.options.goods)) props.options.goods = [];
|
||||
const idx = props.options.goods.findIndex(g => String(g.id) === String(item.id));
|
||||
if (idx > -1) {
|
||||
props.options.goods.splice(idx, 1);
|
||||
// 5. 选择活动入口链接
|
||||
function handleSelectPromoLink(p) {
|
||||
if (openLinkPicker) {
|
||||
openLinkPicker(({ url, title }) => {
|
||||
if (url) p.url = url;
|
||||
if (!p.title && title) p.title = title;
|
||||
});
|
||||
} else {
|
||||
props.options.goods.push({ ...item });
|
||||
ElMessageBox.prompt('请输入活动入口跳转链接:', '设置活动链接', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: p.url || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '链接不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
p.url = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 商品选择
|
||||
function handleOpenGoodsPicker() {
|
||||
if (typeof openGoodsPicker === 'function') {
|
||||
openGoodsPicker((selectedGoods) => {
|
||||
applySelectedGoods(selectedGoods);
|
||||
}, props.options.goods || []);
|
||||
} else {
|
||||
tempSelectedGoods.value = Array.isArray(props.options.goods) ? [...props.options.goods] : [];
|
||||
showGoodsModal.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
function confirmGoodsSelected() {
|
||||
applySelectedGoods(tempSelectedGoods.value);
|
||||
showGoodsModal.value = false;
|
||||
}
|
||||
|
||||
function applySelectedGoods(selectedGoods) {
|
||||
if (Array.isArray(selectedGoods)) {
|
||||
props.options.goods = selectedGoods.map(g => ({
|
||||
id: g.id || g.skuId || g.goodsId,
|
||||
goodsId: g.goodsId,
|
||||
goodsName: g.goodsName || g.name,
|
||||
price: g.price,
|
||||
originalPrice: g.originalPrice || g.price,
|
||||
thumbnail: g.thumbnail || g.pic || defaultThumb
|
||||
}));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -217,6 +493,12 @@ function toggleSelect(item) {
|
||||
padding-bottom: 14px;
|
||||
border-bottom: 1px dashed #e2e8f0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
@@ -232,38 +514,223 @@ function toggleSelect(item) {
|
||||
|
||||
.form-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
&.flex-row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.item-label {
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
margin-right: 8px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mt-6 {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.mt-10 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.mr-4 {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.flex-1 {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.micro-label {
|
||||
font-size: 11px;
|
||||
color: #64748b;
|
||||
margin-bottom: 2px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.picker-flex-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.thumb-preview-box {
|
||||
position: relative;
|
||||
width: 58px;
|
||||
height: 40px;
|
||||
border-radius: 4px;
|
||||
border: 1px dashed #cbd5e1;
|
||||
background: #ffffff;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
transition: border-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #409eff;
|
||||
}
|
||||
|
||||
.preview-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.thumb-empty-icon {
|
||||
font-size: 15px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.thumb-empty-tip {
|
||||
font-size: 10px;
|
||||
color: #94a3b8;
|
||||
line-height: 1;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.thumb-clear-btn {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
background: rgba(0, 0, 0, 0.65);
|
||||
color: #ffffff;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: #ef4444;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input-with-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.action-picker-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link-picker-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.link-input-icon {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.action-picker-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 0 10px;
|
||||
|
||||
&.link-btn {
|
||||
background: #ecf5ff;
|
||||
border-color: #b3d8ff;
|
||||
color: #409eff;
|
||||
|
||||
&:hover {
|
||||
background: #409eff;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link-current-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 11px;
|
||||
background: #eff6ff;
|
||||
border: 1px solid #dbeafe;
|
||||
border-radius: 4px;
|
||||
padding: 2px 8px;
|
||||
margin-top: 2px;
|
||||
overflow: hidden;
|
||||
|
||||
.tag-label {
|
||||
color: #3b82f6;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tag-val {
|
||||
color: #1e40af;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.promos-list-editor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
gap: 10px;
|
||||
|
||||
.promo-editor-card {
|
||||
background: #f8fafc;
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e2e8f0;
|
||||
|
||||
.promo-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 1px solid #edf2f7;
|
||||
|
||||
.promo-idx-badge {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.promo-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.promo-row-1 {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-bottom: 6px;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
text-align: center;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.goods-list-preview {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
@@ -310,59 +777,4 @@ function toggleSelect(item) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-goods-pool {
|
||||
max-height: 360px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.pool-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #4f46e5;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: #eef2ff;
|
||||
border-color: #4f46e5;
|
||||
}
|
||||
|
||||
.pool-thumb {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.pool-meta {
|
||||
flex: 1;
|
||||
.pool-name {
|
||||
font-size: 13px;
|
||||
color: #1e293b;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.pool-price {
|
||||
font-size: 12px;
|
||||
color: #e11d48;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
color: #4f46e5;
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,26 +5,58 @@
|
||||
<div class="section-title">宫格标题设置</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">主标题</span>
|
||||
<el-input v-model="options.title" size="small" placeholder="如:爆款热卖榜" />
|
||||
<el-input v-model="options.title" size="small" placeholder="如:爆款热卖榜" clearable />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">副标题</span>
|
||||
<el-input v-model="options.subtitle" size="small" placeholder="如:近30天爆卖好物精选" />
|
||||
<el-input v-model="options.subtitle" size="small" placeholder="如:近30天爆卖好物精选" clearable />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<div class="form-item flex-row">
|
||||
<span class="item-label">展示查看更多</span>
|
||||
<el-switch v-model="options.showMoreLink" size="small" />
|
||||
</div>
|
||||
<div class="form-item" v-if="options.showMoreLink !== false">
|
||||
<span class="item-label">更多文案</span>
|
||||
<el-input v-model="options.moreText" size="small" placeholder="如:查看全部" />
|
||||
</div>
|
||||
<template v-if="options.showMoreLink !== false">
|
||||
<div class="form-item">
|
||||
<span class="item-label">更多文案</span>
|
||||
<el-input v-model="options.moreText" size="small" placeholder="如:查看全部" clearable />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">更多跳转链接</span>
|
||||
<div class="link-picker-wrap">
|
||||
<el-input
|
||||
v-model="options.moreUrl"
|
||||
size="small"
|
||||
placeholder="如:/goodsList 或专题页面"
|
||||
clearable
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon class="link-input-icon"><Link /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
size="small"
|
||||
class="action-picker-btn link-btn"
|
||||
@click="handleSelectMoreLink"
|
||||
>
|
||||
<el-icon class="mr-4"><Link /></el-icon>
|
||||
<span>选择链接</span>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="link-current-tag" v-if="options.moreUrl">
|
||||
<span class="tag-label">当前链接:</span>
|
||||
<span class="tag-val" :title="options.moreUrl">{{ options.moreUrl }}</span>
|
||||
<el-icon class="url-clear-icon" title="清除链接" @click.stop="options.moreUrl = ''"><Close /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 2. 宫格排版设置 (网格与列数) -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">宫格排版与线条</div>
|
||||
<div class="form-item">
|
||||
<div class="form-item flex-row">
|
||||
<span class="item-label">一行展示几个 (列数)</span>
|
||||
<el-radio-group v-model="options.columns" size="small">
|
||||
<el-radio-button :label="4">4 列</el-radio-button>
|
||||
@@ -32,7 +64,7 @@
|
||||
<el-radio-button :label="6">6 列</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<div class="form-item flex-row">
|
||||
<span class="item-label">矩阵实线网格分割线</span>
|
||||
<el-switch v-model="options.showBorder" size="small" />
|
||||
</div>
|
||||
@@ -42,54 +74,119 @@
|
||||
<div class="panel-section">
|
||||
<div class="section-title flex-between">
|
||||
<span>宫格商品数据 ({{ options.goods?.length || 0 }} 件)</span>
|
||||
<el-button type="primary" size="small" @click="showModal = true">
|
||||
选择真实商品
|
||||
</el-button>
|
||||
<div class="btn-group">
|
||||
<el-button v-if="options.goods?.length" link type="danger" size="small" @click="options.goods = []">
|
||||
清空
|
||||
</el-button>
|
||||
<el-button type="primary" size="small" @click="handleOpenGoodsPicker">
|
||||
选择商品
|
||||
</el-button>
|
||||
<el-button size="small" @click="handleAddCustomGoods">
|
||||
<el-icon class="mr-4"><Plus /></el-icon> 自定义加品
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="goods-grid-preview" v-if="options.goods?.length">
|
||||
<div v-for="(g, idx) in options.goods" :key="g.id || idx" class="grid-item-row">
|
||||
<span class="idx-badge">{{ idx + 1 }}</span>
|
||||
<img :src="g.thumbnail || defaultThumb" class="g-thumb" alt="" />
|
||||
<div class="g-info">
|
||||
<span class="g-name">{{ g.goodsName }}</span>
|
||||
<span class="g-price">¥{{ g.price }}</span>
|
||||
<div class="thumb-wrap-mini" @click="handleSelectGoodsThumb(g)" title="点击更换商品图片">
|
||||
<img :src="g.thumbnail || defaultThumb" class="g-thumb" alt="" />
|
||||
<div class="thumb-tip">换图</div>
|
||||
</div>
|
||||
<el-button link type="danger" size="small" @click="options.goods.splice(idx, 1)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
<div class="g-info">
|
||||
<el-input
|
||||
v-model="g.goodsName"
|
||||
size="small"
|
||||
placeholder="商品名称"
|
||||
class="g-name-input"
|
||||
/>
|
||||
<div class="g-meta-row">
|
||||
<div class="price-input-wrap">
|
||||
<span class="price-sym">¥</span>
|
||||
<el-input v-model="g.price" size="small" placeholder="价格" class="price-mini-input" />
|
||||
</div>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="handleSelectGoodsLink(g)"
|
||||
:title="g.url || '设置跳转链接'"
|
||||
>
|
||||
<el-icon><Link /></el-icon>
|
||||
<span style="font-size: 11px; margin-left: 2px;">{{ g.url ? '已配链接' : '选链接' }}</span>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="item-url-badge" v-if="g.url">
|
||||
<span class="url-label">🔗</span>
|
||||
<span class="url-text" :title="g.url">{{ g.url }}</span>
|
||||
<el-icon class="url-clear-icon" title="清除链接" @click.stop="g.url = ''"><Close /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-btn-col">
|
||||
<el-button
|
||||
link
|
||||
size="small"
|
||||
:disabled="idx === 0"
|
||||
@click="handleMoveGoods(idx, -1)"
|
||||
title="上移"
|
||||
>
|
||||
<el-icon><Top /></el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
size="small"
|
||||
:disabled="idx === options.goods.length - 1"
|
||||
@click="handleMoveGoods(idx, 1)"
|
||||
title="下移"
|
||||
>
|
||||
<el-icon><Bottom /></el-icon>
|
||||
</el-button>
|
||||
<el-button link type="danger" size="small" @click="options.goods.splice(idx, 1)" title="删除">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="goods-empty-tip" v-else>
|
||||
<span>暂未选择商品,前台将展示推荐占位</span>
|
||||
<div class="empty-btn-row">
|
||||
<el-button type="primary" link size="small" @click="handleOpenGoodsPicker">
|
||||
+ 从商品库选择
|
||||
</el-button>
|
||||
<el-button type="primary" link size="small" @click="handleAddCustomGoods">
|
||||
+ 自定义加品
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 商品选择弹窗 -->
|
||||
<el-dialog v-model="showModal" title="选择宫格商品" width="600px" append-to-body>
|
||||
<div class="modal-goods-pool">
|
||||
<div
|
||||
v-for="item in mockPool"
|
||||
:key="item.id"
|
||||
class="pool-item"
|
||||
:class="{ selected: isSelected(item.id) }"
|
||||
@click="toggleSelect(item)"
|
||||
>
|
||||
<img :src="item.thumbnail" class="pool-thumb" alt="" />
|
||||
<div class="pool-meta">
|
||||
<div class="pool-name">{{ item.goodsName }}</div>
|
||||
<div class="pool-price">¥{{ item.price }}</div>
|
||||
</div>
|
||||
<el-icon v-if="isSelected(item.id)" class="check-icon"><Check /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 兜底商品选择弹窗(当未注入全局 openGoodsPicker 时生效) -->
|
||||
<el-dialog
|
||||
v-if="!openGoodsPicker && fallbackModal"
|
||||
v-model="fallbackModal"
|
||||
title="从商品库选择商品"
|
||||
width="1160px"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
>
|
||||
<goods-dialog
|
||||
:selectedWay="tempSelectedGoods"
|
||||
@selected="(val) => { tempSelectedGoods = val; }"
|
||||
/>
|
||||
<template #footer>
|
||||
<el-button @click="showModal = false">完成</el-button>
|
||||
<el-button @click="fallbackModal = false">取消</el-button>
|
||||
<el-button type="primary" @click="confirmFallbackSelected">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { Delete, Check } from '@element-plus/icons-vue';
|
||||
import { ref, inject } from 'vue';
|
||||
import { Delete, Link, Picture, Plus, Top, Bottom, Close } from '@element-plus/icons-vue';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import GoodsDialog from '@/components/lili-dialog/goods-dialog.vue';
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
@@ -103,51 +200,139 @@ const props = defineProps({
|
||||
});
|
||||
|
||||
const defaultThumb = 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_1.png';
|
||||
const showModal = ref(false);
|
||||
|
||||
if (!props.options.title) props.options.title = '爆款热卖榜';
|
||||
if (!props.options.subtitle) props.options.subtitle = '近30天爆卖好物精选';
|
||||
if (props.options.showMoreLink === undefined) props.options.showMoreLink = true;
|
||||
if (!props.options.moreText) props.options.moreText = '查看全部';
|
||||
if (!props.options.columns) props.options.columns = 5;
|
||||
if (props.options.showBorder === undefined) props.options.showBorder = true;
|
||||
if (!props.options.goods) {
|
||||
props.options.goods = [
|
||||
{ id: '1', goodsName: '旗舰 5G 智能手机 12GB+256GB 超光影影像系统', price: '3999.00', originalPrice: '4599.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_1.png' },
|
||||
{ id: '2', goodsName: '无线头戴式深度主动降噪蓝牙耳机 空间音频Hi-Res', price: '1099.00', originalPrice: '1499.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_2.png' },
|
||||
{ id: '3', goodsName: '14英寸 全面屏轻薄商务笔记本电脑 酷睿i7 高刷防眩光', price: '5699.00', originalPrice: '6299.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_3.png' },
|
||||
{ id: '4', goodsName: '运动健康专业监测智能手表 心率血氧长续航50米防水', price: '899.00', originalPrice: '1199.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_4.png' },
|
||||
{ id: '5', goodsName: '奢宠酵母焕活修护精华露 50ml 紧致淡纹抗皱滋养', price: '680.00', originalPrice: '890.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_5.png' },
|
||||
{ id: '6', goodsName: '意式半自动家用复古咖啡机 20Bar高压恒温萃取蒸汽奶泡', price: '899.00', originalPrice: '1280.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_6.png' },
|
||||
{ id: '7', goodsName: '碳板专业竞速轻量缓震跑步鞋 透气防滑男女同款潮鞋', price: '459.00', originalPrice: '699.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_7.png' },
|
||||
{ id: '8', goodsName: '极简静音无雾负离子香薰加湿器 大容量客厅卧室通用', price: '299.00', originalPrice: '399.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_8.png' },
|
||||
{ id: '9', goodsName: '智能降噪AI翻译蓝牙墨镜 超轻防紫外线偏光太阳镜', price: '599.00', originalPrice: '799.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_1.png' },
|
||||
{ id: '10', goodsName: '15.6英寸 4K超高清便携式显示器 Type-C一线直连高色域', price: '1299.00', originalPrice: '1699.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_3.png' }
|
||||
];
|
||||
}
|
||||
// 注入来自 pc/index.vue 的全局选择器
|
||||
const openImagePicker = inject('openImagePicker', null);
|
||||
const openLinkPicker = inject('openLinkPicker', null);
|
||||
const openGoodsPicker = inject('openGoodsPicker', null);
|
||||
|
||||
const mockPool = [
|
||||
{ id: '1', goodsName: '旗舰 5G 智能手机 12GB+256GB 超光影影像系统', price: '3999.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_1.png' },
|
||||
{ id: '2', goodsName: '无线头戴式深度主动降噪蓝牙耳机 空间音频Hi-Res', price: '1099.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_2.png' },
|
||||
{ id: '3', goodsName: '14英寸 全面屏轻薄商务笔记本电脑 酷睿i7 高刷防眩光', price: '5699.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_3.png' },
|
||||
{ id: '4', goodsName: '运动健康专业监测智能手表 心率血氧长续航50米防水', price: '899.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_4.png' },
|
||||
{ id: '5', goodsName: '奢宠酵母焕活修护精华露 50ml 紧致淡纹抗皱滋养', price: '680.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_5.png' },
|
||||
{ id: '6', goodsName: '意式半自动家用复古咖啡机 20Bar高压恒温萃取蒸汽奶泡', price: '899.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_6.png' },
|
||||
{ id: '7', goodsName: '碳板专业竞速轻量缓震跑步鞋 透气防滑男女同款潮鞋', price: '459.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_7.png' },
|
||||
{ id: '8', goodsName: '极简静音无雾负离子香薰加湿器 大容量客厅卧室通用', price: '299.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_8.png' }
|
||||
];
|
||||
const fallbackModal = ref(false);
|
||||
const tempSelectedGoods = ref([]);
|
||||
|
||||
function isSelected(id) {
|
||||
return props.options.goods?.some(g => String(g.id) === String(id));
|
||||
}
|
||||
|
||||
function toggleSelect(item) {
|
||||
if (!Array.isArray(props.options.goods)) props.options.goods = [];
|
||||
const idx = props.options.goods.findIndex(g => String(g.id) === String(item.id));
|
||||
if (idx > -1) {
|
||||
props.options.goods.splice(idx, 1);
|
||||
// 1. 选择更多链接
|
||||
function handleSelectMoreLink() {
|
||||
if (openLinkPicker) {
|
||||
openLinkPicker(({ url }) => {
|
||||
if (url) props.options.moreUrl = url;
|
||||
});
|
||||
} else {
|
||||
props.options.goods.push({ ...item });
|
||||
ElMessageBox.prompt('请输入“查看更多”跳转链接:', '设置更多链接', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: props.options.moreUrl || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '链接不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
props.options.moreUrl = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 更换商品图片
|
||||
function handleSelectGoodsThumb(g) {
|
||||
if (openImagePicker) {
|
||||
openImagePicker((url) => {
|
||||
if (url) g.thumbnail = url;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入商品图片 URL 地址:', '更换商品图片', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: g.thumbnail || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '图片地址不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
g.thumbnail = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 配置商品专属跳转链接
|
||||
function handleSelectGoodsLink(g) {
|
||||
if (openLinkPicker) {
|
||||
openLinkPicker(({ url, title }) => {
|
||||
if (url) g.url = url;
|
||||
if (!g.goodsName && title) g.goodsName = title;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入商品跳转链接(如 /goodsDetail?goodsId=xxx 或外部网址):', '设置商品链接', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: g.url || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '链接不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
g.url = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 自定义加品
|
||||
function handleAddCustomGoods() {
|
||||
if (!Array.isArray(props.options.goods)) {
|
||||
props.options.goods = [];
|
||||
}
|
||||
const nextIdx = props.options.goods.length + 1;
|
||||
const newItem = {
|
||||
id: `custom_${Date.now()}_${nextIdx}`,
|
||||
goodsName: `爆款精选好物 ${nextIdx}`,
|
||||
price: '99.00',
|
||||
originalPrice: '199.00',
|
||||
thumbnail: defaultThumb,
|
||||
url: ''
|
||||
};
|
||||
props.options.goods.push(newItem);
|
||||
if (openImagePicker) {
|
||||
openImagePicker((url) => {
|
||||
if (url) newItem.thumbnail = url;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 排序调整
|
||||
function handleMoveGoods(idx, direction) {
|
||||
const list = props.options.goods;
|
||||
const target = idx + direction;
|
||||
if (target < 0 || target >= list.length) return;
|
||||
const temp = list[idx];
|
||||
list[idx] = list[target];
|
||||
list[target] = temp;
|
||||
}
|
||||
|
||||
// 6. 从商品库选择
|
||||
function handleOpenGoodsPicker() {
|
||||
if (typeof openGoodsPicker === 'function') {
|
||||
openGoodsPicker((selectedGoods) => {
|
||||
applySelectedGoods(selectedGoods);
|
||||
}, props.options.goods || []);
|
||||
} else {
|
||||
tempSelectedGoods.value = Array.isArray(props.options.goods) ? [...props.options.goods] : [];
|
||||
fallbackModal.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
function confirmFallbackSelected() {
|
||||
applySelectedGoods(tempSelectedGoods.value);
|
||||
fallbackModal.value = false;
|
||||
}
|
||||
|
||||
function applySelectedGoods(selectedGoods) {
|
||||
if (Array.isArray(selectedGoods)) {
|
||||
props.options.goods = selectedGoods.map(g => ({
|
||||
id: g.id || g.skuId || g.goodsId,
|
||||
goodsId: g.goodsId,
|
||||
goodsName: g.goodsName || g.name,
|
||||
price: g.price,
|
||||
originalPrice: g.originalPrice || g.price,
|
||||
thumbnail: g.thumbnail || g.pic || defaultThumb,
|
||||
url: g.url || ''
|
||||
}));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -176,125 +361,278 @@ function toggleSelect(item) {
|
||||
|
||||
.form-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
&.flex-row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.item-label {
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
margin-right: 8px;
|
||||
white-space: nowrap;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mr-4 {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.link-picker-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.link-input-icon {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.action-picker-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 0 10px;
|
||||
|
||||
&.link-btn {
|
||||
background: #ecf5ff;
|
||||
border-color: #b3d8ff;
|
||||
color: #409eff;
|
||||
|
||||
&:hover {
|
||||
background: #409eff;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link-current-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 11px;
|
||||
background: #eff6ff;
|
||||
border: 1px solid #dbeafe;
|
||||
border-radius: 4px;
|
||||
padding: 3px 8px;
|
||||
margin-top: 4px;
|
||||
overflow: hidden;
|
||||
|
||||
.tag-label {
|
||||
color: #3b82f6;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tag-val {
|
||||
color: #1e40af;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.url-clear-icon {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
color: #ef4444;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.goods-grid-preview {
|
||||
max-height: 240px;
|
||||
max-height: 380px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
gap: 8px;
|
||||
|
||||
.grid-item-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: #f8fafc;
|
||||
padding: 6px 8px;
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e2e8f0;
|
||||
transition: background 0.15s ease;
|
||||
|
||||
.idx-badge {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #94a3b8;
|
||||
width: 16px;
|
||||
text-align: center;
|
||||
&:hover {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
.g-thumb {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
.idx-badge {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: #e2e8f0;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
color: #475569;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.thumb-wrap-mini {
|
||||
position: relative;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
border: 1px solid #cbd5e1;
|
||||
|
||||
.g-thumb {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.thumb-tip {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(15, 23, 42, 0.7);
|
||||
color: #ffffff;
|
||||
font-size: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
&:hover .thumb-tip {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.g-info {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
.g-name {
|
||||
font-size: 12px;
|
||||
color: #1e293b;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
.g-name-input {
|
||||
:deep(.el-input__inner) {
|
||||
font-size: 12px;
|
||||
padding: 0 6px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.g-price {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #e11d48;
|
||||
.g-meta-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
|
||||
.price-input-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
font-size: 11px;
|
||||
color: #ef4444;
|
||||
font-weight: 600;
|
||||
|
||||
.price-mini-input {
|
||||
width: 70px;
|
||||
:deep(.el-input__inner) {
|
||||
font-size: 11px;
|
||||
padding: 0 4px;
|
||||
height: 22px;
|
||||
color: #ef4444;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-url-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 10px;
|
||||
background: #eff6ff;
|
||||
border: 1px solid #dbeafe;
|
||||
border-radius: 3px;
|
||||
padding: 2px 6px;
|
||||
color: #2563eb;
|
||||
|
||||
.url-label {
|
||||
font-size: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.url-text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.url-clear-icon {
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
|
||||
&:hover {
|
||||
color: #ef4444;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-btn-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.el-button {
|
||||
margin: 0;
|
||||
padding: 2px;
|
||||
height: auto;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-goods-pool {
|
||||
max-height: 360px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.pool-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
.goods-empty-tip {
|
||||
padding: 18px 12px;
|
||||
border: 1px dashed #cbd5e1;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
background: #f8fafc;
|
||||
|
||||
&:hover {
|
||||
border-color: #f97316;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: #fff7ed;
|
||||
border-color: #f97316;
|
||||
}
|
||||
|
||||
.pool-thumb {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.pool-meta {
|
||||
flex: 1;
|
||||
.pool-name {
|
||||
font-size: 13px;
|
||||
color: #1e293b;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.pool-price {
|
||||
font-size: 12px;
|
||||
color: #e11d48;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
color: #f97316;
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
.empty-btn-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,23 +3,54 @@
|
||||
<!-- 1. 楼层标题设置 (Floor Header) -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">楼层大标题设置</div>
|
||||
<div class="form-item">
|
||||
<div class="form-item flex-row">
|
||||
<span class="item-label">展示楼层标题</span>
|
||||
<el-switch v-model="options.showFloorTitle" size="small" />
|
||||
</div>
|
||||
<div class="form-row-2">
|
||||
<div class="form-item">
|
||||
<span class="item-label">楼层编号</span>
|
||||
<el-input v-model="options.floorNum" size="small" placeholder="如:1F" style="width: 90px;" />
|
||||
<el-input v-model="options.floorNum" size="small" placeholder="如:1F" style="width: 90px;" clearable />
|
||||
</div>
|
||||
<div class="form-item flex-1">
|
||||
<span class="item-label">楼层名称</span>
|
||||
<el-input v-model="options.title" size="small" placeholder="如:潮流数码" />
|
||||
<el-input v-model="options.title" size="small" placeholder="如:潮流数码" clearable />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">英文/副标题</span>
|
||||
<el-input v-model="options.subtitle" size="small" placeholder="如:DIGITAL & TECH" />
|
||||
<el-input v-model="options.subtitle" size="small" placeholder="如:DIGITAL & TECH" clearable />
|
||||
</div>
|
||||
|
||||
<!-- 更多链接选择器 -->
|
||||
<div class="form-item">
|
||||
<span class="item-label">“查看更多”跳转链接</span>
|
||||
<div class="link-picker-wrap">
|
||||
<el-input
|
||||
v-model="options.moreUrl"
|
||||
size="small"
|
||||
placeholder="如:/goodsList 或分类活动页面"
|
||||
clearable
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon class="link-input-icon"><Link /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
size="small"
|
||||
class="action-picker-btn link-btn"
|
||||
@click="handleSelectMoreLink"
|
||||
>
|
||||
<el-icon class="mr-4"><Link /></el-icon>
|
||||
<span>选择链接</span>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="link-current-tag" v-if="options.moreUrl">
|
||||
<span class="tag-label">当前链接:</span>
|
||||
<span class="tag-val" :title="options.moreUrl">{{ options.moreUrl }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -33,7 +64,7 @@
|
||||
</div>
|
||||
<div class="tags-editor-wrap">
|
||||
<div v-for="(tag, tIdx) in options.subCategories" :key="tIdx" class="tag-edit-row">
|
||||
<el-input v-model="options.subCategories[tIdx]" size="small" placeholder="分类名称" />
|
||||
<el-input v-model="options.subCategories[tIdx]" size="small" placeholder="分类名称" clearable />
|
||||
<el-button link type="danger" size="small" @click="options.subCategories.splice(tIdx, 1)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
@@ -41,29 +72,89 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3. 左侧专属营销竖版海报 -->
|
||||
<!-- 3. 左侧专属营销竖版海报 / 横幅 (横幅楼层 bannerFloor 与宫格楼层通用) -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">左侧专属大促海报</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">展示左侧海报</span>
|
||||
<div class="section-title">左侧营销海报 / 横幅设置</div>
|
||||
<div class="form-item flex-row">
|
||||
<span class="item-label">展示左侧海报横幅</span>
|
||||
<el-switch v-model="options.showPoster" size="small" />
|
||||
</div>
|
||||
<template v-if="options.showPoster !== false">
|
||||
<div class="form-item">
|
||||
<span class="item-label">海报图片 URL</span>
|
||||
<el-input v-model="options.posterImg" size="small" placeholder="图片链接" clearable />
|
||||
<span class="item-label">海报横幅图片</span>
|
||||
<div class="picker-flex-wrap">
|
||||
<div
|
||||
class="thumb-preview-box"
|
||||
@click="handleSelectPosterImg"
|
||||
:title="currentPosterImg ? '点击更换海报' : '点击选择图片'"
|
||||
>
|
||||
<template v-if="currentPosterImg">
|
||||
<img :src="currentPosterImg" alt="海报" class="preview-img" />
|
||||
<span class="thumb-clear-btn" @click.stop="handleClearPosterImg" title="清除图片">
|
||||
<el-icon><Close /></el-icon>
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-icon class="thumb-empty-icon"><Picture /></el-icon>
|
||||
<span class="thumb-empty-tip">选图</span>
|
||||
</template>
|
||||
</div>
|
||||
<div class="input-with-action flex-1">
|
||||
<el-input
|
||||
v-model="currentPosterImg"
|
||||
size="small"
|
||||
placeholder="请输入或选择海报横幅图地址"
|
||||
clearable
|
||||
/>
|
||||
<el-button
|
||||
size="small"
|
||||
class="action-picker-btn"
|
||||
@click="handleSelectPosterImg"
|
||||
>
|
||||
<el-icon class="mr-4"><Picture /></el-icon>
|
||||
<span>选择图片</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
|
||||
<div class="form-item mt-10">
|
||||
<span class="item-label">海报主标题</span>
|
||||
<el-input v-model="options.posterTitle" size="small" placeholder="如:数码潮电 酷玩季" />
|
||||
<el-input v-model="options.posterTitle" size="small" placeholder="如:数码潮电 酷玩季" clearable />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">海报副标题</span>
|
||||
<el-input v-model="options.posterSub" size="small" placeholder="如:领券最高立减 600 元" />
|
||||
<el-input v-model="options.posterSub" size="small" placeholder="如:领券最高立减 600 元" clearable />
|
||||
</div>
|
||||
|
||||
<div class="form-item">
|
||||
<span class="item-label">跳转链接</span>
|
||||
<el-input v-model="options.posterUrl" size="small" placeholder="如:/goods" />
|
||||
<span class="item-label">海报跳转链接</span>
|
||||
<div class="link-picker-wrap">
|
||||
<el-input
|
||||
v-model="currentPosterUrl"
|
||||
size="small"
|
||||
placeholder="如:/goods 或活动专题"
|
||||
clearable
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon class="link-input-icon"><Link /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
size="small"
|
||||
class="action-picker-btn link-btn"
|
||||
@click="handleSelectPosterLink"
|
||||
>
|
||||
<el-icon class="mr-4"><Link /></el-icon>
|
||||
<span>选择链接</span>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="link-current-tag" v-if="currentPosterUrl">
|
||||
<span class="tag-label">当前链接:</span>
|
||||
<span class="tag-val" :title="currentPosterUrl">{{ currentPosterUrl }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -71,7 +162,7 @@
|
||||
<!-- 4. 右侧商品列数与排版 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">右侧商品排版</div>
|
||||
<div class="form-item">
|
||||
<div class="form-item flex-row">
|
||||
<span class="item-label">单行展示列数</span>
|
||||
<el-radio-group v-model="options.columns" size="small">
|
||||
<el-radio-button :label="3">3 列</el-radio-button>
|
||||
@@ -85,17 +176,27 @@
|
||||
<div class="panel-section">
|
||||
<div class="section-title flex-between">
|
||||
<span>楼层商品数据 ({{ options.goods?.length || 0 }} 件)</span>
|
||||
<el-button type="primary" size="small" @click="showGoodsModal = true">
|
||||
选择真实商品
|
||||
</el-button>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<el-button v-if="options.goods?.length" link type="danger" size="small" @click="options.goods = []">清空</el-button>
|
||||
<el-button type="primary" size="small" @click="handleOpenGoodsPicker">选择商品</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="goods-list-preview" v-if="options.goods?.length">
|
||||
<div v-for="(g, idx) in options.goods" :key="g.id || idx" class="goods-item-row">
|
||||
<img :src="g.thumbnail || defaultThumb" class="g-thumb" alt="" />
|
||||
<div class="thumb-wrap-mini" @click="handleSelectGoodsThumb(g)" title="点击更换商品图片">
|
||||
<img :src="g.thumbnail || defaultThumb" class="g-thumb" alt="" />
|
||||
<div class="thumb-tip">换图</div>
|
||||
</div>
|
||||
<div class="g-info">
|
||||
<span class="g-name">{{ g.goodsName }}</span>
|
||||
<span class="g-price">¥{{ g.price }}</span>
|
||||
<span class="g-name" :title="g.goodsName">{{ g.goodsName }}</span>
|
||||
<div class="g-meta-row">
|
||||
<span class="g-price">¥{{ g.price }}</span>
|
||||
<el-button link type="primary" size="small" @click="handleSelectGoodsLink(g)" :title="g.url || '设置商品链接'">
|
||||
<el-icon><Link /></el-icon>
|
||||
<span style="font-size: 11px; margin-left: 2px;">{{ g.url ? '已配链接' : '选链接' }}</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-button link type="danger" size="small" @click="options.goods.splice(idx, 1)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
@@ -104,34 +205,32 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 商品选择弹窗 -->
|
||||
<el-dialog v-model="showGoodsModal" title="选择楼层商品" width="600px" append-to-body>
|
||||
<div class="modal-goods-pool">
|
||||
<div
|
||||
v-for="item in mockPool"
|
||||
:key="item.id"
|
||||
class="pool-item"
|
||||
:class="{ selected: isSelected(item.id) }"
|
||||
@click="toggleSelect(item)"
|
||||
>
|
||||
<img :src="item.thumbnail" class="pool-thumb" alt="" />
|
||||
<div class="pool-meta">
|
||||
<div class="pool-name">{{ item.goodsName }}</div>
|
||||
<div class="pool-price">¥{{ item.price }}</div>
|
||||
</div>
|
||||
<el-icon v-if="isSelected(item.id)" class="check-icon"><Check /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 商品选择弹窗 (兜底) -->
|
||||
<el-dialog
|
||||
v-if="!openGoodsPicker && showGoodsModal"
|
||||
v-model="showGoodsModal"
|
||||
title="从商品库选择商品"
|
||||
width="1160px"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
>
|
||||
<goods-dialog
|
||||
:selectedWay="tempSelectedGoods"
|
||||
@selected="(val) => { tempSelectedGoods = val; }"
|
||||
/>
|
||||
<template #footer>
|
||||
<el-button @click="showGoodsModal = false">完成</el-button>
|
||||
<el-button @click="showGoodsModal = false">取消</el-button>
|
||||
<el-button type="primary" @click="confirmGoodsSelected">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { Plus, Delete, Check } from '@element-plus/icons-vue';
|
||||
import { ref, computed, inject } from 'vue';
|
||||
import GoodsDialog from '@/components/lili-dialog/goods-dialog.vue';
|
||||
import { Plus, Delete, Link, Picture, Close } from '@element-plus/icons-vue';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
@@ -146,6 +245,12 @@ const props = defineProps({
|
||||
|
||||
const defaultThumb = 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_1.png';
|
||||
const showGoodsModal = ref(false);
|
||||
const tempSelectedGoods = ref([]);
|
||||
|
||||
// 注入来自 pc/index.vue 的全局选择器
|
||||
const openImagePicker = inject('openImagePicker', null);
|
||||
const openLinkPicker = inject('openLinkPicker', null);
|
||||
const openGoodsPicker = inject('openGoodsPicker', null);
|
||||
|
||||
// 初始化缺省 options
|
||||
if (props.options.showFloorTitle === undefined) props.options.showFloorTitle = true;
|
||||
@@ -155,9 +260,33 @@ if (!props.options.subtitle) props.options.subtitle = 'DIGITAL & TECH';
|
||||
if (!props.options.subCategories) {
|
||||
props.options.subCategories = ['旗舰手机', '轻薄笔电', '降噪耳机', '智能穿戴', '数码配件'];
|
||||
}
|
||||
if (!props.options.posterImg) {
|
||||
props.options.posterImg = 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/banner-floor/floor_banner_1.png';
|
||||
|
||||
// 兼容 bannerFloor 与 gridFloor 的字段名双向打通
|
||||
const currentPosterImg = computed({
|
||||
get() {
|
||||
return props.options.bannerImg || props.options.posterImg || 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/banner-floor/floor_banner_1.png';
|
||||
},
|
||||
set(val) {
|
||||
props.options.posterImg = val;
|
||||
props.options.bannerImg = val;
|
||||
}
|
||||
});
|
||||
|
||||
const currentPosterUrl = computed({
|
||||
get() {
|
||||
return props.options.bannerUrl || props.options.posterUrl || '';
|
||||
},
|
||||
set(val) {
|
||||
props.options.posterUrl = val;
|
||||
props.options.bannerUrl = val;
|
||||
}
|
||||
});
|
||||
|
||||
function handleClearPosterImg() {
|
||||
props.options.posterImg = '';
|
||||
props.options.bannerImg = '';
|
||||
}
|
||||
|
||||
if (!props.options.posterTitle) props.options.posterTitle = '数码潮电 酷玩季';
|
||||
if (!props.options.posterSub) props.options.posterSub = '领券最高立减 600 元';
|
||||
if (!props.options.columns) props.options.columns = 4;
|
||||
@@ -174,35 +303,150 @@ if (!props.options.goods) {
|
||||
];
|
||||
}
|
||||
|
||||
const mockPool = [
|
||||
{ id: '101', goodsName: '旗舰 5G 智能手机 12GB+256GB 超光影影像系统', price: '3999.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_1.png' },
|
||||
{ id: '102', goodsName: '无线头戴式深度主动降噪蓝牙耳机 空间音频Hi-Res', price: '1099.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_2.png' },
|
||||
{ id: '103', goodsName: '14英寸 全面屏轻薄商务笔记本电脑 酷睿i7 高刷防眩光', price: '5699.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_3.png' },
|
||||
{ id: '104', goodsName: '运动健康专业监测智能手表 心率血氧长续航50米防水', price: '899.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_4.png' },
|
||||
{ id: '105', goodsName: '奢宠酵母焕活修护精华露 50ml 紧致淡纹抗皱滋养', price: '680.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_5.png' },
|
||||
{ id: '106', goodsName: '意式半自动家用复古咖啡机 20Bar高压恒温萃取蒸汽奶泡', price: '899.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_6.png' },
|
||||
{ id: '107', goodsName: '碳板专业竞速轻量缓震跑步鞋 透气防滑男女同款潮鞋', price: '459.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_7.png' },
|
||||
{ id: '108', goodsName: '极简静音无雾负离子香薰加湿器 大容量客厅卧室通用', price: '299.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_8.png' }
|
||||
];
|
||||
|
||||
function handleAddTag() {
|
||||
if (!Array.isArray(props.options.subCategories)) {
|
||||
props.options.subCategories = [];
|
||||
}
|
||||
props.options.subCategories.push('新分类');
|
||||
props.options.subCategories.push('新标签');
|
||||
}
|
||||
|
||||
function isSelected(id) {
|
||||
return props.options.goods?.some(g => String(g.id) === String(id));
|
||||
}
|
||||
|
||||
function toggleSelect(item) {
|
||||
if (!Array.isArray(props.options.goods)) props.options.goods = [];
|
||||
const idx = props.options.goods.findIndex(g => String(g.id) === String(item.id));
|
||||
if (idx > -1) {
|
||||
props.options.goods.splice(idx, 1);
|
||||
// 1. 选择更多链接
|
||||
function handleSelectMoreLink() {
|
||||
if (openLinkPicker) {
|
||||
openLinkPicker(({ url }) => {
|
||||
if (url) props.options.moreUrl = url;
|
||||
});
|
||||
} else {
|
||||
props.options.goods.push({ ...item });
|
||||
ElMessageBox.prompt('请输入“查看更多”跳转链接:', '设置更多链接', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: props.options.moreUrl || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '链接不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
props.options.moreUrl = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 选择海报横幅图片
|
||||
function handleSelectPosterImg() {
|
||||
if (openImagePicker) {
|
||||
openImagePicker((url) => {
|
||||
if (url) {
|
||||
currentPosterImg.value = url;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入海报图片 URL 地址:', '选择海报图片', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: currentPosterImg.value || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '图片地址不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
currentPosterImg.value = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 选择海报跳转链接
|
||||
function handleSelectPosterLink() {
|
||||
if (openLinkPicker) {
|
||||
openLinkPicker(({ url }) => {
|
||||
if (url) {
|
||||
currentPosterUrl.value = url;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入海报跳转链接:', '设置海报链接', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: currentPosterUrl.value || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '链接不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
currentPosterUrl.value = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 更换单个商品图片
|
||||
function handleSelectGoodsThumb(g) {
|
||||
if (openImagePicker) {
|
||||
openImagePicker((url) => {
|
||||
if (url) g.thumbnail = url;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入商品图片 URL 地址:', '更换商品图片', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: g.thumbnail || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '图片地址不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
g.thumbnail = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 配置商品专属链接
|
||||
function handleSelectGoodsLink(g) {
|
||||
if (openLinkPicker) {
|
||||
openLinkPicker(({ url, title }) => {
|
||||
if (url) g.url = url;
|
||||
if (!g.goodsName && title) g.goodsName = title;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入商品跳转链接(如 /goodsDetail?goodsId=xxx):', '设置商品链接', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: g.url || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '链接不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
g.url = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
function handleOpenGoodsPicker() {
|
||||
if (typeof openGoodsPicker === 'function') {
|
||||
openGoodsPicker((selectedGoods) => {
|
||||
applySelectedGoods(selectedGoods);
|
||||
}, props.options.goods || []);
|
||||
} else {
|
||||
tempSelectedGoods.value = Array.isArray(props.options.goods) ? [...props.options.goods] : [];
|
||||
showGoodsModal.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
function confirmGoodsSelected() {
|
||||
applySelectedGoods(tempSelectedGoods.value);
|
||||
showGoodsModal.value = false;
|
||||
}
|
||||
|
||||
function applySelectedGoods(selectedGoods) {
|
||||
if (Array.isArray(selectedGoods)) {
|
||||
props.options.goods = selectedGoods.map(g => ({
|
||||
id: g.id || g.skuId || g.goodsId,
|
||||
goodsId: g.goodsId,
|
||||
goodsName: g.goodsName || g.name,
|
||||
price: g.price,
|
||||
originalPrice: g.originalPrice || g.price,
|
||||
thumbnail: g.thumbnail || g.pic || defaultThumb,
|
||||
url: g.url || ''
|
||||
}));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -231,43 +475,184 @@ function toggleSelect(item) {
|
||||
|
||||
.form-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
&.flex-row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.item-label {
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
margin-right: 8px;
|
||||
white-space: nowrap;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.form-row-2 {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.flex-1 {
|
||||
flex: 1;
|
||||
.tags-editor-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
|
||||
.tag-edit-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tags-editor-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
.mr-4 {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.tag-edit-row {
|
||||
.mt-10 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.flex-1 {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.picker-flex-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.thumb-preview-box {
|
||||
position: relative;
|
||||
width: 58px;
|
||||
height: 40px;
|
||||
border-radius: 4px;
|
||||
border: 1px dashed #cbd5e1;
|
||||
background: #ffffff;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
transition: border-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #409eff;
|
||||
}
|
||||
|
||||
.preview-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.thumb-empty-icon {
|
||||
font-size: 15px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.thumb-empty-tip {
|
||||
font-size: 10px;
|
||||
color: #94a3b8;
|
||||
line-height: 1;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.thumb-clear-btn {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
background: rgba(0, 0, 0, 0.65);
|
||||
color: #ffffff;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: #ef4444;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input-with-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
|
||||
.action-picker-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link-picker-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.link-input-icon {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.action-picker-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 0 10px;
|
||||
|
||||
&.link-btn {
|
||||
background: #ecf5ff;
|
||||
border-color: #b3d8ff;
|
||||
color: #409eff;
|
||||
|
||||
&:hover {
|
||||
background: #409eff;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link-current-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 11px;
|
||||
background: #eff6ff;
|
||||
border: 1px solid #dbeafe;
|
||||
border-radius: 4px;
|
||||
padding: 3px 8px;
|
||||
margin-top: 4px;
|
||||
overflow: hidden;
|
||||
|
||||
.tag-label {
|
||||
color: #3b82f6;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tag-val {
|
||||
color: #1e40af;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.goods-list-preview {
|
||||
max-height: 240px;
|
||||
max-height: 260px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -281,12 +666,41 @@ function toggleSelect(item) {
|
||||
background: #f8fafc;
|
||||
padding: 6px 8px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e2e8f0;
|
||||
|
||||
.g-thumb {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
.thumb-wrap-mini {
|
||||
position: relative;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
border: 1px solid #cbd5e1;
|
||||
|
||||
.g-thumb {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.thumb-tip {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(15, 23, 42, 0.7);
|
||||
color: #ffffff;
|
||||
font-size: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
&:hover .thumb-tip {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.g-info {
|
||||
@@ -294,6 +708,7 @@ function toggleSelect(item) {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
|
||||
.g-name {
|
||||
font-size: 12px;
|
||||
@@ -303,68 +718,19 @@ function toggleSelect(item) {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.g-price {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #e11d48;
|
||||
.g-meta-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.g-price {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #e11d48;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-goods-pool {
|
||||
max-height: 360px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.pool-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: #eff6ff;
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
.pool-thumb {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.pool-meta {
|
||||
flex: 1;
|
||||
.pool-name {
|
||||
font-size: 13px;
|
||||
color: #1e293b;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.pool-price {
|
||||
font-size: 12px;
|
||||
color: #e11d48;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
color: #3b82f6;
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,48 +1,219 @@
|
||||
<template>
|
||||
<div class="panel-hot-zone">
|
||||
<!-- 1. 热区底图设置 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">热区底图</div>
|
||||
|
||||
<!-- URL 输入与选择图片按钮 -->
|
||||
<div class="form-item">
|
||||
<span class="item-label">大图 URL 地址</span>
|
||||
<el-input v-model="options.imageUrl" size="small" placeholder="请输入热区底图 URL" clearable />
|
||||
<div class="input-with-action">
|
||||
<el-input
|
||||
v-model="options.imageUrl"
|
||||
size="small"
|
||||
placeholder="请输入或选择热区大图 URL"
|
||||
clearable
|
||||
/>
|
||||
<el-button
|
||||
size="small"
|
||||
class="action-picker-btn"
|
||||
@click="handleSelectImage"
|
||||
>
|
||||
<el-icon class="mr-4"><Picture /></el-icon>
|
||||
<span>选择图片</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hotzone-preview" v-if="options.imageUrl">
|
||||
|
||||
<!-- 底图预览框 -->
|
||||
<div class="hotzone-preview-box" v-if="options.imageUrl">
|
||||
<img :src="options.imageUrl" alt="hotzone" class="img-preview" />
|
||||
<div class="preview-actions-bar">
|
||||
<el-button size="small" type="primary" plain @click="handleSelectImage">
|
||||
<el-icon class="mr-4"><Picture /></el-icon>
|
||||
更换底图
|
||||
</el-button>
|
||||
<el-button size="small" type="danger" plain @click="options.imageUrl = ''">
|
||||
<el-icon class="mr-4"><Close /></el-icon>
|
||||
移除底图
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. 热区列表与链接选择 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title flex-between">
|
||||
<span>热区列表 ({{ options.zones?.length || 0 }})</span>
|
||||
<el-button link type="primary" size="small" @click="handleAddZone">
|
||||
<el-icon><Plus /></el-icon> 添加热区
|
||||
</el-button>
|
||||
<div class="header-action-group">
|
||||
<el-button link type="primary" size="small" @click="handleAddZone">
|
||||
<el-icon><Plus /></el-icon> 添加热区
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="zones-list">
|
||||
<div class="zones-list" v-if="Array.isArray(options.zones) && options.zones.length">
|
||||
<div
|
||||
v-for="(zone, idx) in options.zones"
|
||||
:key="idx"
|
||||
class="zone-card"
|
||||
class="zone-card-item"
|
||||
>
|
||||
<!-- 热区卡片头部 -->
|
||||
<div class="zone-header">
|
||||
<span>热区 {{ idx + 1 }}</span>
|
||||
<el-button link type="danger" size="small" @click="options.zones.splice(idx, 1)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
<span class="zone-idx-title">
|
||||
<span class="zone-num-badge">{{ idx + 1 }}</span>
|
||||
<span>{{ zone.title || `热区 ${idx + 1}` }}</span>
|
||||
</span>
|
||||
<div class="zone-header-btns">
|
||||
<el-button
|
||||
link
|
||||
size="small"
|
||||
:disabled="idx === 0"
|
||||
@click="moveZone(idx, -1)"
|
||||
title="上移"
|
||||
>
|
||||
<el-icon><Top /></el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
size="small"
|
||||
:disabled="idx === options.zones.length - 1"
|
||||
@click="moveZone(idx, 1)"
|
||||
title="下移"
|
||||
>
|
||||
<el-icon><Bottom /></el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="options.zones.splice(idx, 1)"
|
||||
title="删除热区"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="zone-inputs">
|
||||
<el-input v-model="zone.title" size="small" placeholder="热区标题" />
|
||||
<el-input v-model="zone.url" size="small" placeholder="跳转路径,如 /goods/1" />
|
||||
|
||||
<!-- 热区标题与链接设置 -->
|
||||
<div class="zone-body-inputs">
|
||||
<!-- 热区标题 -->
|
||||
<div class="field-item">
|
||||
<span class="field-lbl">热区标题</span>
|
||||
<el-input
|
||||
v-model="zone.title"
|
||||
size="small"
|
||||
placeholder="热区名称(如:领券中心 / 潮电专场)"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 跳转链接 + 链接选择器 -->
|
||||
<div class="field-item">
|
||||
<span class="field-lbl">跳转链接</span>
|
||||
<div class="link-picker-row">
|
||||
<el-input
|
||||
v-model="zone.url"
|
||||
size="small"
|
||||
placeholder="跳转链接(如 /goodsList?category=1)"
|
||||
clearable
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon class="link-prefix-icon"><Link /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
size="small"
|
||||
class="action-picker-btn link-btn"
|
||||
@click="handleSelectLink(zone)"
|
||||
>
|
||||
<el-icon class="mr-4"><Link /></el-icon>
|
||||
<span>选择链接</span>
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 当前链接高亮标识 -->
|
||||
<div class="link-current-tag" v-if="zone.url">
|
||||
<span class="tag-label">当前链接:</span>
|
||||
<span class="tag-val" :title="zone.url">{{ zone.url }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 热区位置百分比 (X, Y, W, H) -->
|
||||
<div class="field-item">
|
||||
<span class="field-lbl">热区位置 (坐标与尺寸 %)</span>
|
||||
<div class="coord-grid">
|
||||
<div class="coord-cell">
|
||||
<span class="coord-lbl">X</span>
|
||||
<el-input-number
|
||||
v-model="zone.left"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:step="1"
|
||||
:precision="1"
|
||||
size="small"
|
||||
controls-position="right"
|
||||
class="coord-input"
|
||||
/>
|
||||
</div>
|
||||
<div class="coord-cell">
|
||||
<span class="coord-lbl">Y</span>
|
||||
<el-input-number
|
||||
v-model="zone.top"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:step="1"
|
||||
:precision="1"
|
||||
size="small"
|
||||
controls-position="right"
|
||||
class="coord-input"
|
||||
/>
|
||||
</div>
|
||||
<div class="coord-cell">
|
||||
<span class="coord-lbl">宽</span>
|
||||
<el-input-number
|
||||
v-model="zone.width"
|
||||
:min="1"
|
||||
:max="100"
|
||||
:step="1"
|
||||
:precision="1"
|
||||
size="small"
|
||||
controls-position="right"
|
||||
class="coord-input"
|
||||
/>
|
||||
</div>
|
||||
<div class="coord-cell">
|
||||
<span class="coord-lbl">高</span>
|
||||
<el-input-number
|
||||
v-model="zone.height"
|
||||
:min="1"
|
||||
:max="100"
|
||||
:step="1"
|
||||
:precision="1"
|
||||
size="small"
|
||||
controls-position="right"
|
||||
class="coord-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="empty-zones-box" v-else>
|
||||
<span>暂无热区,请点击右上角“添加热区”</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Plus, Delete } from '@element-plus/icons-vue';
|
||||
import { inject } from 'vue';
|
||||
import { Plus, Delete, Link, Picture, Close, Top, Bottom } from '@element-plus/icons-vue';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
@@ -55,15 +226,76 @@ const props = defineProps({
|
||||
}
|
||||
});
|
||||
|
||||
// 注入来自 pc/index.vue 的全局选择器
|
||||
const openImagePicker = inject('openImagePicker', null);
|
||||
const openLinkPicker = inject('openLinkPicker', null);
|
||||
|
||||
// 1. 选择大图
|
||||
function handleSelectImage() {
|
||||
if (openImagePicker) {
|
||||
openImagePicker((url) => {
|
||||
if (url) props.options.imageUrl = url;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入热区大图 URL 地址:', '选择热区底图', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: props.options.imageUrl || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '图片地址不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
props.options.imageUrl = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 选择热区跳转链接
|
||||
function handleSelectLink(zone) {
|
||||
if (openLinkPicker) {
|
||||
openLinkPicker(({ url, title, raw }) => {
|
||||
if (url) zone.url = url;
|
||||
if (!zone.title && title) {
|
||||
zone.title = title;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入跳转链接地址(如 /goodsList?category=1 或外部网址):', '设置跳转链接', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: zone.url || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '链接地址不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
zone.url = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 上移/下移热区
|
||||
function moveZone(index, direction) {
|
||||
const newIdx = index + direction;
|
||||
if (!Array.isArray(props.options.zones)) return;
|
||||
if (newIdx < 0 || newIdx >= props.options.zones.length) return;
|
||||
const temp = props.options.zones[index];
|
||||
props.options.zones[index] = props.options.zones[newIdx];
|
||||
props.options.zones[newIdx] = temp;
|
||||
}
|
||||
|
||||
// 4. 添加热区
|
||||
function handleAddZone() {
|
||||
if (!Array.isArray(props.options.zones)) props.options.zones = [];
|
||||
const count = props.options.zones.length;
|
||||
props.options.zones.push({
|
||||
title: `热区 ${props.options.zones.length + 1}`,
|
||||
url: '/',
|
||||
top: 10,
|
||||
left: 10,
|
||||
width: 20,
|
||||
height: 20
|
||||
title: `热区 ${count + 1}`,
|
||||
url: '/goodsList',
|
||||
top: Math.min(10 + count * 5, 60),
|
||||
left: Math.min(10 + (count % 3) * 30, 70),
|
||||
width: 25,
|
||||
height: 35
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@@ -99,45 +331,219 @@ function handleAddZone() {
|
||||
}
|
||||
}
|
||||
|
||||
.hotzone-preview {
|
||||
margin-top: 10px;
|
||||
max-height: 140px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 6px;
|
||||
.input-with-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.img-preview {
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
.action-picker-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.zones-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
.hotzone-preview-box {
|
||||
margin-top: 10px;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
background: #f8fafc;
|
||||
|
||||
.zone-card {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
.img-preview {
|
||||
width: 100%;
|
||||
max-height: 140px;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.zone-header {
|
||||
.preview-actions-bar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 10px;
|
||||
background: #f1f5f9;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.zones-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
.zone-card-item {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #cbd5e1;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.zone-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #edf2f7;
|
||||
|
||||
.zone-idx-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
margin-bottom: 6px;
|
||||
font-weight: 600;
|
||||
color: #334155;
|
||||
|
||||
.zone-num-badge {
|
||||
background: #409EFF;
|
||||
color: #ffffff;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.zone-inputs {
|
||||
.zone-header-btns {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.zone-body-inputs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
.field-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
.field-lbl {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: #64748b;
|
||||
}
|
||||
}
|
||||
|
||||
.link-picker-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.link-prefix-icon {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.action-picker-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 0 10px;
|
||||
|
||||
&.link-btn {
|
||||
background: #ecf5ff;
|
||||
border-color: #b3d8ff;
|
||||
color: #409EFF;
|
||||
|
||||
&:hover {
|
||||
background: #409EFF;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link-current-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 11px;
|
||||
background: #eff6ff;
|
||||
border: 1px solid #dbeafe;
|
||||
border-radius: 4px;
|
||||
padding: 2px 8px;
|
||||
margin-top: 2px;
|
||||
overflow: hidden;
|
||||
|
||||
.tag-label {
|
||||
color: #3b82f6;
|
||||
flex-shrink: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tag-val {
|
||||
color: #1e40af;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-family: monospace;
|
||||
}
|
||||
}
|
||||
|
||||
.coord-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 6px;
|
||||
|
||||
.coord-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 4px;
|
||||
padding: 2px 6px;
|
||||
|
||||
.coord-lbl {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #64748b;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.coord-input {
|
||||
width: 100%;
|
||||
|
||||
:deep(.el-input__wrapper) {
|
||||
padding: 0 2px;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
:deep(.el-input__inner) {
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-zones-box {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
border: 1px dashed #cbd5e1;
|
||||
border-radius: 6px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.mr-4 {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,29 +1,82 @@
|
||||
<template>
|
||||
<div class="panel-notice-bar">
|
||||
<!-- 1. 基础配置 -->
|
||||
<!-- 1. 基础配置与图标设置 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">快讯标语与更多</div>
|
||||
<div class="section-title">快讯标语与图标</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">前缀标语</span>
|
||||
<el-input v-model="options.title" size="small" placeholder="如:公告快讯 / 今日热点" />
|
||||
<el-input v-model="options.title" size="small" placeholder="如:公告快讯 / 今日热点" clearable />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">展示更多入口</span>
|
||||
|
||||
<!-- 徽标图标 / 图片选择器 -->
|
||||
<div class="form-item mt-10">
|
||||
<span class="item-label">前缀徽标图标 (可选自定义图片)</span>
|
||||
<div class="badge-icon-picker-row">
|
||||
<div class="icon-thumb-preview" @click="handleSelectBadgeImg" title="点击选择/更换徽标图片">
|
||||
<img v-if="options.badgeImg || options.icon" :src="options.badgeImg || options.icon" class="badge-img" alt="" />
|
||||
<div v-else class="icon-placeholder">
|
||||
<el-icon><Picture /></el-icon>
|
||||
<span>选图</span>
|
||||
</div>
|
||||
<div class="hover-overlay" v-if="options.badgeImg || options.icon">换图</div>
|
||||
</div>
|
||||
<div class="icon-input-wrap">
|
||||
<div class="input-btn-row">
|
||||
<el-input
|
||||
v-model="badgeImgVal"
|
||||
size="small"
|
||||
placeholder="留空展示默认喇叭图标"
|
||||
clearable
|
||||
/>
|
||||
<el-button size="small" class="action-picker-btn" @click="handleSelectBadgeImg">
|
||||
<el-icon class="mr-4"><Picture /></el-icon>
|
||||
<span>选择图片</span>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="tip-sub-text">建议尺寸 24x24 或 32x32px 正方形小图标</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-item flex-row mt-10">
|
||||
<span class="item-label">展示“更多”入口</span>
|
||||
<el-switch v-model="options.showMore" size="small" />
|
||||
</div>
|
||||
|
||||
<template v-if="options.showMore !== false">
|
||||
<div class="form-item">
|
||||
<span class="item-label">更多文案</span>
|
||||
<el-input v-model="options.moreText" size="small" placeholder="如:更多" />
|
||||
<div class="form-item mt-10">
|
||||
<span class="item-label">“更多”文案</span>
|
||||
<el-input v-model="options.moreText" size="small" placeholder="如:更多" clearable />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">跳转链接</span>
|
||||
<el-input v-model="options.moreUrl" size="small" placeholder="如:/article" />
|
||||
<div class="form-item mt-10">
|
||||
<span class="item-label">“更多”跳转链接</span>
|
||||
<div class="link-picker-wrap">
|
||||
<el-input v-model="options.moreUrl" size="small" placeholder="如:/article 或文章列表" clearable>
|
||||
<template #prefix>
|
||||
<el-icon class="link-input-icon"><Link /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
size="small"
|
||||
class="action-picker-btn link-btn"
|
||||
@click="handleSelectMoreLink"
|
||||
>
|
||||
<el-icon class="mr-4"><Link /></el-icon>
|
||||
<span>选择链接</span>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="link-current-tag" v-if="options.moreUrl">
|
||||
<span class="tag-label">当前链接:</span>
|
||||
<span class="tag-val" :title="options.moreUrl">{{ options.moreUrl }}</span>
|
||||
<el-icon class="url-clear-icon" title="清除链接" @click.stop="options.moreUrl = ''"><Close /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 2. 快讯条目列表 (匹配万米 Mg 结构化列表) -->
|
||||
<!-- 2. 快讯条目列表 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title flex-between">
|
||||
<span>快讯条目 ({{ itemsList?.length || 0 }} 条)</span>
|
||||
@@ -39,7 +92,7 @@
|
||||
class="notice-card-item"
|
||||
>
|
||||
<div class="item-top-row">
|
||||
<el-select v-model="item.tag" size="small" style="width: 80px">
|
||||
<el-select v-model="item.tag" size="small" style="width: 86px" filterable allow-create placeholder="标签">
|
||||
<el-option label="最新" value="最新" />
|
||||
<el-option label="活动" value="活动" />
|
||||
<el-option label="通知" value="通知" />
|
||||
@@ -71,12 +124,33 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item-title-row">
|
||||
<el-input v-model="item.title" size="small" placeholder="快讯内容标题" />
|
||||
<div class="item-title-row mt-6">
|
||||
<el-input v-model="item.title" size="small" placeholder="快讯内容标题" clearable />
|
||||
</div>
|
||||
|
||||
<div class="item-link-row">
|
||||
<el-input v-model="item.url" size="small" placeholder="跳转路径(如: /article/101)" />
|
||||
<div class="item-link-row mt-6">
|
||||
<div class="link-picker-wrap">
|
||||
<el-input v-model="item.url" size="small" placeholder="跳转路径(如: /article/101 或文章链接)" clearable>
|
||||
<template #prefix>
|
||||
<el-icon class="link-input-icon"><Link /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
size="small"
|
||||
class="action-picker-btn link-btn"
|
||||
@click="handleSelectItemLink(item)"
|
||||
>
|
||||
<el-icon class="mr-4"><Link /></el-icon>
|
||||
<span>选链接</span>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="link-current-tag" v-if="item.url">
|
||||
<span class="tag-label">当前链接:</span>
|
||||
<span class="tag-val" :title="item.url">{{ item.url }}</span>
|
||||
<el-icon class="url-clear-icon" title="清除链接" @click.stop="item.url = ''"><Close /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -85,8 +159,9 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { Plus, Delete, Top, Bottom } from '@element-plus/icons-vue';
|
||||
import { computed, inject } from 'vue';
|
||||
import { Plus, Delete, Top, Bottom, Link, Picture, Close } from '@element-plus/icons-vue';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
@@ -99,11 +174,25 @@ const props = defineProps({
|
||||
}
|
||||
});
|
||||
|
||||
// 全局选择器注入
|
||||
const openImagePicker = inject('openImagePicker', null);
|
||||
const openLinkPicker = inject('openLinkPicker', null);
|
||||
|
||||
if (!props.options.title) props.options.title = '公告快讯';
|
||||
if (props.options.showMore === undefined) props.options.showMore = true;
|
||||
if (!props.options.moreText) props.options.moreText = '更多';
|
||||
if (!props.options.moreUrl) props.options.moreUrl = '/article';
|
||||
|
||||
const badgeImgVal = computed({
|
||||
get() {
|
||||
return props.options.badgeImg || props.options.icon || '';
|
||||
},
|
||||
set(v) {
|
||||
props.options.badgeImg = v;
|
||||
props.options.icon = v;
|
||||
}
|
||||
});
|
||||
|
||||
// 统一 items 数据源
|
||||
if (!props.options.items && !props.options.notices) {
|
||||
props.options.items = [
|
||||
@@ -125,6 +214,74 @@ const itemsList = computed(() => {
|
||||
return props.options.items;
|
||||
});
|
||||
|
||||
// 1. 选择前缀徽标图片
|
||||
function handleSelectBadgeImg() {
|
||||
if (openImagePicker) {
|
||||
openImagePicker((url) => {
|
||||
if (url) {
|
||||
props.options.badgeImg = url;
|
||||
props.options.icon = url;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入快讯徽标图片 URL:', '选择快讯图标', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: props.options.badgeImg || props.options.icon || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '图片地址不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
props.options.badgeImg = value.trim();
|
||||
props.options.icon = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 选择“更多”链接
|
||||
function handleSelectMoreLink() {
|
||||
if (openLinkPicker) {
|
||||
openLinkPicker(({ url }) => {
|
||||
if (url) props.options.moreUrl = url;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入“更多”跳转链接:', '设置跳转链接', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: props.options.moreUrl || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '链接不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
props.options.moreUrl = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 选择单条快讯链接
|
||||
function handleSelectItemLink(item) {
|
||||
if (openLinkPicker) {
|
||||
openLinkPicker(({ url, title }) => {
|
||||
if (url) item.url = url;
|
||||
if (!item.title && title) item.title = title;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入快讯跳转链接:', '设置快讯链接', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: item.url || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '链接不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
item.url = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
function handleAddItem() {
|
||||
props.options.items.push({
|
||||
tag: '最新',
|
||||
@@ -148,8 +305,8 @@ function moveItem(index, step) {
|
||||
padding: 8px 4px;
|
||||
|
||||
.panel-section {
|
||||
margin-bottom: 18px;
|
||||
padding-bottom: 14px;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px dashed #e2e8f0;
|
||||
|
||||
.section-title {
|
||||
@@ -167,15 +324,168 @@ function moveItem(index, step) {
|
||||
|
||||
.form-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
|
||||
&.flex-row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.item-label {
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
margin-right: 8px;
|
||||
white-space: nowrap;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mt-6 {
|
||||
margin-top: 6px;
|
||||
}
|
||||
.mt-10 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.mr-4 {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.badge-icon-picker-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
|
||||
.icon-thumb-preview {
|
||||
position: relative;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #cbd5e1;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
|
||||
.badge-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.icon-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #e2e8f0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
color: #64748b;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.hover-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(15, 23, 42, 0.65);
|
||||
color: #ffffff;
|
||||
font-size: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
&:hover .hover-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-input-wrap {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
.input-btn-row {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.tip-sub-text {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link-picker-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.link-input-icon {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.action-picker-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 0 10px;
|
||||
|
||||
&.link-btn {
|
||||
background: #ecf5ff;
|
||||
border-color: #b3d8ff;
|
||||
color: #409eff;
|
||||
|
||||
&:hover {
|
||||
background: #409eff;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link-current-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 11px;
|
||||
background: #eff6ff;
|
||||
border: 1px solid #dbeafe;
|
||||
border-radius: 4px;
|
||||
padding: 3px 8px;
|
||||
margin-top: 4px;
|
||||
overflow: hidden;
|
||||
|
||||
.tag-label {
|
||||
color: #3b82f6;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tag-val {
|
||||
color: #1e40af;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.url-clear-icon {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
color: #ef4444;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -183,32 +493,28 @@ function moveItem(index, step) {
|
||||
.notice-items-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
gap: 10px;
|
||||
max-height: 420px;
|
||||
overflow-y: auto;
|
||||
|
||||
.notice-card-item {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 10px;
|
||||
|
||||
.item-top-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
gap: 8px;
|
||||
|
||||
.item-actions {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.item-title-row, .item-link-row {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<div class="panel-promo-group">
|
||||
<!-- 1. 排版列数 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">排版列数</div>
|
||||
<div class="form-item">
|
||||
@@ -12,40 +13,156 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. 活动卡片列表 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title flex-between">
|
||||
<span>活动卡片列表</span>
|
||||
<span>活动卡片列表 ({{ options.cards?.length || 0 }})</span>
|
||||
<el-button link type="primary" size="small" @click="handleAddCard">
|
||||
<el-icon><Plus /></el-icon> 添加卡片
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="cards-list">
|
||||
<div class="cards-list" v-if="Array.isArray(options.cards) && options.cards.length">
|
||||
<div
|
||||
v-for="(card, idx) in options.cards"
|
||||
:key="idx"
|
||||
class="promo-card-item"
|
||||
>
|
||||
<!-- 顶部序号与工具按钮 -->
|
||||
<div class="card-header">
|
||||
<span>卡片 {{ idx + 1 }}</span>
|
||||
<el-button link type="danger" size="small" @click="options.cards.splice(idx, 1)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
<span class="card-idx-badge">卡片 {{ idx + 1 }}{{ card.title ? ` · ${card.title}` : '' }}</span>
|
||||
<div class="card-actions">
|
||||
<el-button
|
||||
link
|
||||
size="small"
|
||||
:disabled="idx === 0"
|
||||
@click="moveCard(idx, -1)"
|
||||
title="上移"
|
||||
>
|
||||
<el-icon><Top /></el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
size="small"
|
||||
:disabled="idx === options.cards.length - 1"
|
||||
@click="moveCard(idx, 1)"
|
||||
title="下移"
|
||||
>
|
||||
<el-icon><Bottom /></el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="options.cards.splice(idx, 1)"
|
||||
title="删除卡片"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 卡片属性输入 -->
|
||||
<div class="card-inputs">
|
||||
<el-input v-model="card.title" size="small" placeholder="主标题(如:今日秒杀)" />
|
||||
<el-input v-model="card.subtitle" size="small" placeholder="副标题(如:抢神券立减50)" />
|
||||
<el-input v-model="card.bgImg" size="small" placeholder="卡片底图 URL" />
|
||||
<el-input v-model="card.url" size="small" placeholder="活动落地页链接" />
|
||||
<!-- 标题行 -->
|
||||
<div class="input-row">
|
||||
<span class="field-label">主标题</span>
|
||||
<el-input v-model="card.title" size="small" placeholder="如:限时秒杀" clearable />
|
||||
</div>
|
||||
|
||||
<!-- 副标题行 -->
|
||||
<div class="input-row">
|
||||
<span class="field-label">副标题</span>
|
||||
<el-input v-model="card.subtitle" size="small" placeholder="如:抢券立减50元 · 爆款直降" clearable />
|
||||
</div>
|
||||
|
||||
<!-- 卡片底图 (图片选择器) -->
|
||||
<div class="input-row">
|
||||
<span class="field-label">卡片底图</span>
|
||||
<div class="picker-flex-wrap">
|
||||
<!-- 缩略图预览与快捷替换 -->
|
||||
<div
|
||||
class="thumb-preview-box"
|
||||
@click="handleSelectImage(card)"
|
||||
:title="card.bgImg ? '点击更换底图' : '点击上传底图'"
|
||||
>
|
||||
<template v-if="card.bgImg">
|
||||
<img :src="card.bgImg" alt="底图" class="preview-img" />
|
||||
<span class="thumb-clear-btn" @click.stop="card.bgImg = ''" title="清除图片">
|
||||
<el-icon><Close /></el-icon>
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-icon class="thumb-empty-icon"><Picture /></el-icon>
|
||||
<span class="thumb-empty-tip">选图</span>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- URL 输入框与选择图片按钮 -->
|
||||
<div class="input-with-action">
|
||||
<el-input
|
||||
v-model="card.bgImg"
|
||||
size="small"
|
||||
placeholder="底图 URL,可直接粘贴或选择"
|
||||
clearable
|
||||
/>
|
||||
<el-button
|
||||
size="small"
|
||||
class="action-picker-btn"
|
||||
@click="handleSelectImage(card)"
|
||||
>
|
||||
<el-icon class="mr-4"><Picture /></el-icon>
|
||||
<span>选择图片</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 跳转链接 (链接选择器) -->
|
||||
<div class="input-row">
|
||||
<span class="field-label">跳转链接</span>
|
||||
<div class="link-picker-wrap">
|
||||
<el-input
|
||||
v-model="card.url"
|
||||
size="small"
|
||||
placeholder="跳转链接(如 /goodsList?seckill=1)"
|
||||
clearable
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon class="link-input-icon"><Link /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
size="small"
|
||||
class="action-picker-btn link-btn"
|
||||
@click="handleSelectLink(card)"
|
||||
>
|
||||
<el-icon class="mr-4"><Link /></el-icon>
|
||||
<span>选择链接</span>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="link-current-tag" v-if="card.url">
|
||||
<span class="tag-label">当前链接:</span>
|
||||
<span class="tag-val" :title="card.url">{{ card.url }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="empty-cards-tip" v-else>
|
||||
<span>暂无活动卡片,请点击上方“添加卡片”</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Plus, Delete } from '@element-plus/icons-vue';
|
||||
import { inject } from 'vue';
|
||||
import { Plus, Delete, Link, Picture, Close, Top, Bottom } from '@element-plus/icons-vue';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
@@ -58,13 +175,73 @@ const props = defineProps({
|
||||
}
|
||||
});
|
||||
|
||||
// 注入来自 pc/index.vue 的全局选择器
|
||||
const openImagePicker = inject('openImagePicker', null);
|
||||
const openLinkPicker = inject('openLinkPicker', null);
|
||||
|
||||
// 1. 选择卡片背景图片
|
||||
function handleSelectImage(card) {
|
||||
if (openImagePicker) {
|
||||
openImagePicker((url) => {
|
||||
if (url) card.bgImg = url;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入卡片背景图 URL 地址:', '选择卡片底图', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: card.bgImg || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '图片地址不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
card.bgImg = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 选择跳转链接
|
||||
function handleSelectLink(card) {
|
||||
if (openLinkPicker) {
|
||||
openLinkPicker(({ url, title, raw }) => {
|
||||
if (url) card.url = url;
|
||||
if (!card.title && title) {
|
||||
card.title = title;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入跳转链接(如 /goodsList?seckill=1 或完整网址):', '设置跳转链接', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: card.url || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '链接不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
card.url = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 上移/下移卡片
|
||||
function moveCard(index, direction) {
|
||||
const newIdx = index + direction;
|
||||
if (!Array.isArray(props.options.cards)) return;
|
||||
if (newIdx < 0 || newIdx >= props.options.cards.length) return;
|
||||
const temp = props.options.cards[index];
|
||||
props.options.cards[index] = props.options.cards[newIdx];
|
||||
props.options.cards[newIdx] = temp;
|
||||
}
|
||||
|
||||
// 4. 添加卡片
|
||||
function handleAddCard() {
|
||||
if (!Array.isArray(props.options.cards)) props.options.cards = [];
|
||||
props.options.cards.push({
|
||||
title: '大促专场',
|
||||
subtitle: '限量特惠抢购',
|
||||
bgImg: '',
|
||||
url: '/goods'
|
||||
url: '/goodsList'
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@@ -99,33 +276,209 @@ function handleAddCard() {
|
||||
color: #475569;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cards-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
.cards-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
|
||||
.promo-card-item {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
.promo-card-item {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
&:hover {
|
||||
border-color: #cbd5e1;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #edf2f7;
|
||||
|
||||
.card-idx-badge {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.card-inputs {
|
||||
.card-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-inputs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
.input-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
.field-label {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: #64748b;
|
||||
}
|
||||
}
|
||||
|
||||
.picker-flex-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.thumb-preview-box {
|
||||
position: relative;
|
||||
width: 58px;
|
||||
height: 40px;
|
||||
border-radius: 4px;
|
||||
border: 1px dashed #cbd5e1;
|
||||
background: #ffffff;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
transition: border-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #409EFF;
|
||||
}
|
||||
|
||||
.preview-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.thumb-empty-icon {
|
||||
font-size: 15px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.thumb-empty-tip {
|
||||
font-size: 10px;
|
||||
color: #94a3b8;
|
||||
line-height: 1;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.thumb-clear-btn {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
background: rgba(0, 0, 0, 0.65);
|
||||
color: #ffffff;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: #ef4444;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input-with-action {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.action-picker-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link-picker-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.link-input-icon {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.action-picker-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 0 10px;
|
||||
|
||||
&.link-btn {
|
||||
background: #ecf5ff;
|
||||
border-color: #b3d8ff;
|
||||
color: #409EFF;
|
||||
|
||||
&:hover {
|
||||
background: #409EFF;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link-current-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 11px;
|
||||
background: #eff6ff;
|
||||
border: 1px solid #dbeafe;
|
||||
border-radius: 4px;
|
||||
padding: 2px 8px;
|
||||
margin-top: 2px;
|
||||
overflow: hidden;
|
||||
|
||||
.tag-label {
|
||||
color: #3b82f6;
|
||||
flex-shrink: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tag-val {
|
||||
color: #1e40af;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-family: monospace;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-cards-tip {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
border: 1px dashed #cbd5e1;
|
||||
border-radius: 6px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.mr-4 {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -46,9 +46,14 @@
|
||||
<div class="panel-section">
|
||||
<div class="section-title flex-between">
|
||||
<span>推荐商品池 ({{ options.goods?.length || 0 }} 件)</span>
|
||||
<el-button type="primary" size="small" @click="showModal = true">
|
||||
选择真实商品
|
||||
</el-button>
|
||||
<div class="btn-group">
|
||||
<el-button v-if="options.goods?.length" link type="danger" size="small" @click="options.goods = []">
|
||||
清空
|
||||
</el-button>
|
||||
<el-button type="primary" size="small" @click="handleOpenGoodsPicker">
|
||||
选择商品
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="goods-list-preview" v-if="options.goods?.length">
|
||||
@@ -56,7 +61,7 @@
|
||||
<span class="idx-tag">{{ idx === 0 ? '精选' : idx + 1 }}</span>
|
||||
<img :src="g.thumbnail || defaultThumb" class="g-thumb" alt="" />
|
||||
<div class="g-info">
|
||||
<span class="g-name">{{ g.goodsName }}</span>
|
||||
<span class="g-name" :title="g.goodsName">{{ g.goodsName }}</span>
|
||||
<span class="g-price">¥{{ g.price }}</span>
|
||||
</div>
|
||||
<el-button link type="danger" size="small" @click="options.goods.splice(idx, 1)">
|
||||
@@ -64,36 +69,39 @@
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="goods-empty-tip" v-else>
|
||||
<span>暂未选择特定商品,前台将展示系统推荐商品</span>
|
||||
<el-button type="primary" link size="small" @click="handleOpenGoodsPicker">
|
||||
+ 点击添加商品
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 商品选择弹窗 -->
|
||||
<el-dialog v-model="showModal" title="选择推荐商品" width="600px" append-to-body>
|
||||
<div class="modal-goods-pool">
|
||||
<div
|
||||
v-for="item in mockPool"
|
||||
:key="item.id"
|
||||
class="pool-item"
|
||||
:class="{ selected: isSelected(item.id) }"
|
||||
@click="toggleSelect(item)"
|
||||
>
|
||||
<img :src="item.thumbnail" class="pool-thumb" alt="" />
|
||||
<div class="pool-meta">
|
||||
<div class="pool-name">{{ item.goodsName }}</div>
|
||||
<div class="pool-price">¥{{ item.price }}</div>
|
||||
</div>
|
||||
<el-icon v-if="isSelected(item.id)" class="check-icon"><Check /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 兜底商品选择弹窗(当未注入全局 openGoodsPicker 时生效) -->
|
||||
<el-dialog
|
||||
v-if="!openGoodsPicker && showModal"
|
||||
v-model="showModal"
|
||||
title="从商品库选择商品"
|
||||
width="1160px"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
>
|
||||
<goods-dialog
|
||||
:selectedWay="tempSelectedGoods"
|
||||
@selected="(val) => { tempSelectedGoods = val; }"
|
||||
/>
|
||||
<template #footer>
|
||||
<el-button @click="showModal = false">完成</el-button>
|
||||
<el-button @click="showModal = false">取消</el-button>
|
||||
<el-button type="primary" @click="confirmGoodsSelected">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { Delete, Check } from '@element-plus/icons-vue';
|
||||
import { ref, inject } from 'vue';
|
||||
import GoodsDialog from '@/components/lili-dialog/goods-dialog.vue';
|
||||
import { Delete } from '@element-plus/icons-vue';
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
@@ -108,6 +116,8 @@ const props = defineProps({
|
||||
|
||||
const defaultThumb = 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_1.png';
|
||||
const showModal = ref(false);
|
||||
const openGoodsPicker = inject('openGoodsPicker', null);
|
||||
const tempSelectedGoods = ref([]);
|
||||
|
||||
if (!props.options.title) props.options.title = '猜你喜欢';
|
||||
if (!props.options.subtitle) props.options.subtitle = '真实推荐商品流';
|
||||
@@ -128,26 +138,32 @@ if (!props.options.goods) {
|
||||
];
|
||||
}
|
||||
|
||||
const mockPool = [
|
||||
{ id: '1', goodsName: '旗舰 5G 智能手机 12GB+256GB 超光影影像系统', price: '3999.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_1.png' },
|
||||
{ id: '2', goodsName: '无线头戴式深度主动降噪蓝牙耳机 空间音频Hi-Res', price: '1099.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_2.png' },
|
||||
{ id: '3', goodsName: '14英寸 全面屏轻薄商务笔记本电脑 酷睿i7 高刷防眩光', price: '5699.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_3.png' },
|
||||
{ id: '4', goodsName: '运动健康专业监测智能手表 心率血氧长续航50米防水', price: '899.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_4.png' },
|
||||
{ id: '5', goodsName: '奢宠酵母焕活修护精华露 50ml 紧致淡纹抗皱滋养', price: '680.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_5.png' },
|
||||
{ id: '6', goodsName: '意式半自动家用复古咖啡机 20Bar高压恒温萃取蒸汽奶泡', price: '899.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_6.png' }
|
||||
];
|
||||
|
||||
function isSelected(id) {
|
||||
return props.options.goods?.some(g => String(g.id) === String(id));
|
||||
function handleOpenGoodsPicker() {
|
||||
if (typeof openGoodsPicker === 'function') {
|
||||
openGoodsPicker((selectedGoods) => {
|
||||
applySelectedGoods(selectedGoods);
|
||||
}, props.options.goods || []);
|
||||
} else {
|
||||
tempSelectedGoods.value = Array.isArray(props.options.goods) ? [...props.options.goods] : [];
|
||||
showModal.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSelect(item) {
|
||||
if (!Array.isArray(props.options.goods)) props.options.goods = [];
|
||||
const idx = props.options.goods.findIndex(g => String(g.id) === String(item.id));
|
||||
if (idx > -1) {
|
||||
props.options.goods.splice(idx, 1);
|
||||
} else {
|
||||
props.options.goods.push({ ...item });
|
||||
function confirmGoodsSelected() {
|
||||
applySelectedGoods(tempSelectedGoods.value);
|
||||
showModal.value = false;
|
||||
}
|
||||
|
||||
function applySelectedGoods(selectedGoods) {
|
||||
if (Array.isArray(selectedGoods)) {
|
||||
props.options.goods = selectedGoods.map(g => ({
|
||||
id: g.id || g.skuId || g.goodsId,
|
||||
goodsId: g.goodsId,
|
||||
goodsName: g.goodsName || g.name,
|
||||
price: g.price,
|
||||
originalPrice: g.originalPrice || g.price,
|
||||
thumbnail: g.thumbnail || g.pic || defaultThumb
|
||||
}));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -172,6 +188,12 @@ function toggleSelect(item) {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-item {
|
||||
@@ -190,7 +212,7 @@ function toggleSelect(item) {
|
||||
}
|
||||
|
||||
.goods-list-preview {
|
||||
max-height: 200px;
|
||||
max-height: 260px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -204,14 +226,16 @@ function toggleSelect(item) {
|
||||
background: #f8fafc;
|
||||
padding: 6px 8px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #f1f5f9;
|
||||
|
||||
.idx-tag {
|
||||
font-size: 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
background: #fee2e2;
|
||||
color: #dc2626;
|
||||
padding: 2px 4px;
|
||||
color: #64748b;
|
||||
background: #e2e8f0;
|
||||
padding: 1px 5px;
|
||||
border-radius: 3px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.g-thumb {
|
||||
@@ -243,60 +267,18 @@ function toggleSelect(item) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-goods-pool {
|
||||
max-height: 360px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.pool-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
.goods-empty-tip {
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
background: #f8fafc;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #ef4444;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: #fef2f2;
|
||||
border-color: #ef4444;
|
||||
}
|
||||
|
||||
.pool-thumb {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.pool-meta {
|
||||
flex: 1;
|
||||
.pool-name {
|
||||
font-size: 13px;
|
||||
color: #1e293b;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.pool-price {
|
||||
font-size: 12px;
|
||||
color: #e11d48;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
color: #ef4444;
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
}
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,15 +5,77 @@
|
||||
|
||||
<div class="form-item">
|
||||
<span class="item-label">主标题文案</span>
|
||||
<el-input v-model="options.mainTitle" size="small" placeholder="如:限时特卖" />
|
||||
<el-input v-model="options.mainTitle" size="small" placeholder="如:限时特卖" clearable />
|
||||
</div>
|
||||
|
||||
<div class="form-item">
|
||||
<!-- 主标题前缀图标 / 图片 -->
|
||||
<div class="form-item mt-10">
|
||||
<span class="item-label">标题图标 / 徽标图片 (可选)</span>
|
||||
<div class="title-icon-picker-row">
|
||||
<div class="icon-thumb-preview" @click="handleSelectTitleImg" title="点击选择/更换标题图标">
|
||||
<img v-if="titleImgVal" :src="titleImgVal" class="title-img" alt="" />
|
||||
<div v-else class="icon-placeholder">
|
||||
<el-icon><Picture /></el-icon>
|
||||
<span>选图</span>
|
||||
</div>
|
||||
<div class="hover-overlay" v-if="titleImgVal">换图</div>
|
||||
</div>
|
||||
<div class="icon-input-wrap">
|
||||
<div class="input-btn-row">
|
||||
<el-input
|
||||
v-model="titleImgVal"
|
||||
size="small"
|
||||
placeholder="请输入或选择标题前缀图标"
|
||||
clearable
|
||||
/>
|
||||
<el-button size="small" class="action-picker-btn" @click="handleSelectTitleImg">
|
||||
<el-icon class="mr-4"><Picture /></el-icon>
|
||||
<span>选择图片</span>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="tip-sub-text">建议尺寸 32x32 或 48x48px,展示在标题前缀</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主标题跳转链接 -->
|
||||
<div class="form-item mt-10">
|
||||
<span class="item-label">主标题点击跳转链接 (可选)</span>
|
||||
<div class="link-picker-wrap">
|
||||
<el-input
|
||||
v-model="options.titleUrl"
|
||||
size="small"
|
||||
placeholder="如 /goods 或活动专区,留空不可点击"
|
||||
clearable
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon class="link-input-icon"><Link /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
size="small"
|
||||
class="action-picker-btn link-btn"
|
||||
@click="handleSelectTitleLink"
|
||||
>
|
||||
<el-icon class="mr-4"><Link /></el-icon>
|
||||
<span>选择链接</span>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="link-current-tag" v-if="options.titleUrl">
|
||||
<span class="tag-label">当前链接:</span>
|
||||
<span class="tag-val" :title="options.titleUrl">{{ options.titleUrl }}</span>
|
||||
<el-icon class="url-clear-icon" title="清除链接" @click.stop="options.titleUrl = ''"><Close /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-item mt-10">
|
||||
<span class="item-label">副标题文字</span>
|
||||
<el-input v-model="options.subtitle" size="small" placeholder="如:精选爆款 每日早十点上新" />
|
||||
<el-input v-model="options.subtitle" size="small" placeholder="如:精选爆款 每日早十点上新" clearable />
|
||||
</div>
|
||||
|
||||
<div class="form-item flex-row">
|
||||
<div class="form-item flex-row mt-10">
|
||||
<span class="item-label">对齐方式</span>
|
||||
<el-radio-group v-model="options.align" size="small">
|
||||
<el-radio-button label="left">靠左对齐</el-radio-button>
|
||||
@@ -21,25 +83,57 @@
|
||||
</el-radio-group>
|
||||
</div>
|
||||
|
||||
<div class="form-item flex-row">
|
||||
<div class="form-item flex-row mt-10">
|
||||
<span class="item-label">显示右侧“查看更多”</span>
|
||||
<el-switch v-model="options.showMore" size="small" />
|
||||
</div>
|
||||
|
||||
<div class="form-item" v-if="options.showMore">
|
||||
<span class="item-label">更多链接文案</span>
|
||||
<el-input v-model="options.moreText" size="small" placeholder="查看更多 >" />
|
||||
</div>
|
||||
<template v-if="options.showMore">
|
||||
<div class="form-item mt-10">
|
||||
<span class="item-label">更多链接文案</span>
|
||||
<el-input v-model="options.moreText" size="small" placeholder="查看更多 >" clearable />
|
||||
</div>
|
||||
|
||||
<div class="form-item" v-if="options.showMore">
|
||||
<span class="item-label">更多跳转地址</span>
|
||||
<el-input v-model="options.moreUrl" size="small" placeholder="如 /goods" />
|
||||
</div>
|
||||
<div class="form-item mt-10">
|
||||
<span class="item-label">更多跳转地址</span>
|
||||
<div class="link-picker-wrap">
|
||||
<el-input
|
||||
v-model="options.moreUrl"
|
||||
size="small"
|
||||
placeholder="如 /goods 或活动专题"
|
||||
clearable
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon class="link-input-icon"><Link /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
size="small"
|
||||
class="action-picker-btn link-btn"
|
||||
@click="handleSelectMoreLink"
|
||||
>
|
||||
<el-icon class="mr-4"><Link /></el-icon>
|
||||
<span>选择链接</span>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="link-current-tag" v-if="options.moreUrl">
|
||||
<span class="tag-label">当前链接:</span>
|
||||
<span class="tag-val" :title="options.moreUrl">{{ options.moreUrl }}</span>
|
||||
<el-icon class="url-clear-icon" title="清除链接" @click.stop="options.moreUrl = ''"><Close /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, inject } from 'vue';
|
||||
import { Link, Picture, Close } from '@element-plus/icons-vue';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
@@ -50,10 +144,93 @@ const props = defineProps({
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
// 注入来自 pc/index.vue 的全局选择器
|
||||
const openImagePicker = inject('openImagePicker', null);
|
||||
const openLinkPicker = inject('openLinkPicker', null);
|
||||
|
||||
const titleImgVal = computed({
|
||||
get() {
|
||||
return props.options.titleImg || props.options.icon || '';
|
||||
},
|
||||
set(v) {
|
||||
props.options.titleImg = v;
|
||||
props.options.icon = v;
|
||||
}
|
||||
});
|
||||
|
||||
// 1. 选择前缀图片
|
||||
function handleSelectTitleImg() {
|
||||
if (openImagePicker) {
|
||||
openImagePicker((url) => {
|
||||
if (url) {
|
||||
props.options.titleImg = url;
|
||||
props.options.icon = url;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入标题图标图片 URL:', '选择标题图标', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: props.options.titleImg || props.options.icon || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '图片地址不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
props.options.titleImg = value.trim();
|
||||
props.options.icon = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 选择主标题跳转链接
|
||||
function handleSelectTitleLink() {
|
||||
if (openLinkPicker) {
|
||||
openLinkPicker(({ url }) => {
|
||||
if (url) props.options.titleUrl = url;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入主标题跳转链接:', '设置标题链接', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: props.options.titleUrl || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '链接不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
props.options.titleUrl = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 选择“查看更多”链接
|
||||
function handleSelectMoreLink() {
|
||||
if (openLinkPicker) {
|
||||
openLinkPicker(({ url }) => {
|
||||
if (url) props.options.moreUrl = url;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入“查看更多”跳转链接:', '设置更多链接', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: props.options.moreUrl || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '链接不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
props.options.moreUrl = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.panel-title-bar {
|
||||
padding: 8px 4px;
|
||||
|
||||
.panel-section {
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 16px;
|
||||
@@ -69,7 +246,6 @@ const props = defineProps({
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
&.flex-row {
|
||||
flex-direction: row;
|
||||
@@ -80,6 +256,153 @@ const props = defineProps({
|
||||
.item-label {
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mt-10 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.mr-4 {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.title-icon-picker-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
|
||||
.icon-thumb-preview {
|
||||
position: relative;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #cbd5e1;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
|
||||
.title-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.icon-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #e2e8f0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
color: #64748b;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.hover-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(15, 23, 42, 0.65);
|
||||
color: #ffffff;
|
||||
font-size: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
&:hover .hover-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-input-wrap {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
.input-btn-row {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.tip-sub-text {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link-picker-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.link-input-icon {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.action-picker-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 0 10px;
|
||||
|
||||
&.link-btn {
|
||||
background: #ecf5ff;
|
||||
border-color: #b3d8ff;
|
||||
color: #409eff;
|
||||
|
||||
&:hover {
|
||||
background: #409eff;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link-current-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 11px;
|
||||
background: #eff6ff;
|
||||
border: 1px solid #dbeafe;
|
||||
border-radius: 4px;
|
||||
padding: 3px 8px;
|
||||
margin-top: 4px;
|
||||
overflow: hidden;
|
||||
|
||||
.tag-label {
|
||||
color: #3b82f6;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tag-val {
|
||||
color: #1e40af;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.url-clear-icon {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
color: #ef4444;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="panel-user-benefits">
|
||||
<!-- 用户问候与标语 -->
|
||||
<!-- 1. 用户问候与头像设置 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">问候信息</div>
|
||||
<div class="section-title">问候与头像设置</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">状态标签</span>
|
||||
<el-input v-model="options.badgeText" size="small" placeholder="如:请登录" clearable />
|
||||
@@ -15,34 +15,103 @@
|
||||
<span class="item-label">副标语提示</span>
|
||||
<el-input v-model="options.subtitle" size="small" placeholder="如:登录后查看专属权益" clearable />
|
||||
</div>
|
||||
|
||||
<!-- 头像图片选择器 -->
|
||||
<div class="form-item mt-10">
|
||||
<span class="item-label">自定义头像 URL (留空展示默认头像)</span>
|
||||
<el-input v-model="options.avatar" size="small" placeholder="请输入头像图片地址" clearable />
|
||||
<span class="item-label">用户头像</span>
|
||||
<div class="avatar-upload-box">
|
||||
<div class="avatar-preview-wrap" @click="handleSelectAvatar" title="点击选择/更换头像">
|
||||
<img v-if="options.avatar" :src="options.avatar" alt="avatar" class="avatar-img" />
|
||||
<div v-else class="avatar-empty">
|
||||
<el-icon><Picture /></el-icon>
|
||||
<span>选头像</span>
|
||||
</div>
|
||||
<div class="avatar-hover-tip" v-if="options.avatar">换图</div>
|
||||
</div>
|
||||
<div class="avatar-right-controls">
|
||||
<div class="avatar-input-row">
|
||||
<el-input
|
||||
v-model="options.avatar"
|
||||
size="small"
|
||||
placeholder="留空展示系统默认头像"
|
||||
clearable
|
||||
/>
|
||||
<el-button size="small" class="action-picker-btn" @click="handleSelectAvatar">
|
||||
<el-icon class="mr-4"><Picture /></el-icon>
|
||||
<span>选择图片</span>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="sub-tip-text">推荐比例 1:1 圆形头像,建议尺寸 100x100px</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 登录与注册按钮 -->
|
||||
<!-- 2. 登录与注册按钮 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">按钮配置</div>
|
||||
<div class="section-title">登录 / 注册按钮配置</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">登录按钮文案</span>
|
||||
<el-input v-model="options.loginText" size="small" placeholder="登录" />
|
||||
</div>
|
||||
<div class="form-item mt-10">
|
||||
<span class="item-label">登录跳转路径</span>
|
||||
<el-input v-model="options.loginUrl" size="small" placeholder="/login" />
|
||||
<span class="item-label">登录跳转链接</span>
|
||||
<div class="link-picker-wrap">
|
||||
<el-input v-model="options.loginUrl" size="small" placeholder="/login" clearable>
|
||||
<template #prefix>
|
||||
<el-icon class="link-input-icon"><Link /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
size="small"
|
||||
class="action-picker-btn link-btn"
|
||||
@click="handleSelectLink('loginUrl', '登录跳转链接')"
|
||||
>
|
||||
<el-icon class="mr-4"><Link /></el-icon>
|
||||
<span>选择链接</span>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="link-current-tag" v-if="options.loginUrl">
|
||||
<span class="tag-label">当前链接:</span>
|
||||
<span class="tag-val" :title="options.loginUrl">{{ options.loginUrl }}</span>
|
||||
<el-icon class="url-clear-icon" title="清除链接" @click.stop="options.loginUrl = ''"><Close /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-item mt-10">
|
||||
<span class="item-label">注册按钮文案</span>
|
||||
<el-input v-model="options.registerText" size="small" placeholder="注册" />
|
||||
</div>
|
||||
<div class="form-item mt-10">
|
||||
<span class="item-label">注册跳转路径</span>
|
||||
<el-input v-model="options.registerUrl" size="small" placeholder="/signUp" />
|
||||
<span class="item-label">注册跳转链接</span>
|
||||
<div class="link-picker-wrap">
|
||||
<el-input v-model="options.registerUrl" size="small" placeholder="/signUp" clearable>
|
||||
<template #prefix>
|
||||
<el-icon class="link-input-icon"><Link /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
size="small"
|
||||
class="action-picker-btn link-btn"
|
||||
@click="handleSelectLink('registerUrl', '注册跳转链接')"
|
||||
>
|
||||
<el-icon class="mr-4"><Link /></el-icon>
|
||||
<span>选择链接</span>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="link-current-tag" v-if="options.registerUrl">
|
||||
<span class="tag-label">当前链接:</span>
|
||||
<span class="tag-val" :title="options.registerUrl">{{ options.registerUrl }}</span>
|
||||
<el-icon class="url-clear-icon" title="清除链接" @click.stop="options.registerUrl = ''"><Close /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3项指标节点 (购物车 / 优惠券 / 订单) -->
|
||||
<!-- 3. 指标节点展示 (购物车 / 优惠券 / 订单) -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">指标节点展示</div>
|
||||
|
||||
@@ -58,8 +127,29 @@
|
||||
<el-input v-model="options.cartText" size="small" placeholder="购物车" />
|
||||
</div>
|
||||
<div class="sub-field mt-6">
|
||||
<span class="sub-label">跳转路径</span>
|
||||
<el-input v-model="options.cartUrl" size="small" placeholder="/cart" />
|
||||
<span class="sub-label">跳转链接</span>
|
||||
<div class="link-picker-wrap">
|
||||
<el-input v-model="options.cartUrl" size="small" placeholder="/cart" clearable>
|
||||
<template #prefix>
|
||||
<el-icon class="link-input-icon"><Link /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
size="small"
|
||||
class="action-picker-btn link-btn"
|
||||
@click="handleSelectLink('cartUrl', '购物车跳转链接')"
|
||||
>
|
||||
<el-icon class="mr-4"><Link /></el-icon>
|
||||
<span>选择链接</span>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="link-current-tag" v-if="options.cartUrl">
|
||||
<span class="tag-label">当前链接:</span>
|
||||
<span class="tag-val" :title="options.cartUrl">{{ options.cartUrl }}</span>
|
||||
<el-icon class="url-clear-icon" title="清除链接" @click.stop="options.cartUrl = ''"><Close /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -76,8 +166,29 @@
|
||||
<el-input v-model="options.couponText" size="small" placeholder="优惠券" />
|
||||
</div>
|
||||
<div class="sub-field mt-6">
|
||||
<span class="sub-label">跳转路径</span>
|
||||
<el-input v-model="options.couponUrl" size="small" placeholder="/member/coupon" />
|
||||
<span class="sub-label">跳转链接</span>
|
||||
<div class="link-picker-wrap">
|
||||
<el-input v-model="options.couponUrl" size="small" placeholder="/member/coupon" clearable>
|
||||
<template #prefix>
|
||||
<el-icon class="link-input-icon"><Link /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
size="small"
|
||||
class="action-picker-btn link-btn"
|
||||
@click="handleSelectLink('couponUrl', '优惠券跳转链接')"
|
||||
>
|
||||
<el-icon class="mr-4"><Link /></el-icon>
|
||||
<span>选择链接</span>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="link-current-tag" v-if="options.couponUrl">
|
||||
<span class="tag-label">当前链接:</span>
|
||||
<span class="tag-val" :title="options.couponUrl">{{ options.couponUrl }}</span>
|
||||
<el-icon class="url-clear-icon" title="清除链接" @click.stop="options.couponUrl = ''"><Close /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -94,8 +205,72 @@
|
||||
<el-input v-model="options.orderText" size="small" placeholder="订单" />
|
||||
</div>
|
||||
<div class="sub-field mt-6">
|
||||
<span class="sub-label">跳转路径</span>
|
||||
<el-input v-model="options.orderUrl" size="small" placeholder="/member/order" />
|
||||
<span class="sub-label">跳转链接</span>
|
||||
<div class="link-picker-wrap">
|
||||
<el-input v-model="options.orderUrl" size="small" placeholder="/member/order" clearable>
|
||||
<template #prefix>
|
||||
<el-icon class="link-input-icon"><Link /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
size="small"
|
||||
class="action-picker-btn link-btn"
|
||||
@click="handleSelectLink('orderUrl', '订单跳转链接')"
|
||||
>
|
||||
<el-icon class="mr-4"><Link /></el-icon>
|
||||
<span>选择链接</span>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="link-current-tag" v-if="options.orderUrl">
|
||||
<span class="tag-label">当前链接:</span>
|
||||
<span class="tag-val" :title="options.orderUrl">{{ options.orderUrl }}</span>
|
||||
<el-icon class="url-clear-icon" title="清除链接" @click.stop="options.orderUrl = ''"><Close /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 4. 底部快捷便民服务入口 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">底部便民服务入口</div>
|
||||
<div class="services-list">
|
||||
<div v-for="(srv, sIdx) in ensureServices" :key="sIdx" class="service-config-item">
|
||||
<div class="service-header-row">
|
||||
<span class="service-idx">{{ sIdx + 1 }}.</span>
|
||||
<el-input v-model="srv.name" size="small" placeholder="名称" style="width: 80px;" />
|
||||
<el-select v-model="srv.icon" size="small" style="width: 90px;">
|
||||
<el-option label="收藏" value="Star" />
|
||||
<el-option label="足迹" value="Clock" />
|
||||
<el-option label="红包" value="Present" />
|
||||
<el-option label="客服" value="Service" />
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="service-link-row mt-6">
|
||||
<div class="link-picker-wrap flex-1">
|
||||
<el-input v-model="srv.url" size="small" placeholder="跳转路径" clearable>
|
||||
<template #prefix>
|
||||
<el-icon class="link-input-icon"><Link /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
size="small"
|
||||
class="action-picker-btn link-btn"
|
||||
@click="handleSelectServiceLink(srv)"
|
||||
>
|
||||
<el-icon class="mr-4"><Link /></el-icon>
|
||||
<span>选链接</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="link-current-tag" v-if="srv.url">
|
||||
<span class="tag-label">当前链接:</span>
|
||||
<span class="tag-val" :title="srv.url">{{ srv.url }}</span>
|
||||
<el-icon class="url-clear-icon" title="清除链接" @click.stop="srv.url = ''"><Close /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -104,6 +279,10 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, inject } from 'vue';
|
||||
import { Picture, Link, Close } from '@element-plus/icons-vue';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
@@ -115,6 +294,10 @@ const props = defineProps({
|
||||
}
|
||||
});
|
||||
|
||||
// 全局选择器注入
|
||||
const openImagePicker = inject('openImagePicker', null);
|
||||
const openLinkPicker = inject('openLinkPicker', null);
|
||||
|
||||
// 初始化缺省配置
|
||||
if (props.options.badgeText === undefined) props.options.badgeText = '请登录';
|
||||
if (props.options.welcomeText === undefined) props.options.welcomeText = '欢迎来到商城';
|
||||
@@ -135,10 +318,89 @@ if (props.options.orderText === undefined) props.options.orderText = '订单';
|
||||
if (props.options.cartUrl === undefined) props.options.cartUrl = '/cart';
|
||||
if (props.options.couponUrl === undefined) props.options.couponUrl = '/member/coupon';
|
||||
if (props.options.orderUrl === undefined) props.options.orderUrl = '/member/order';
|
||||
|
||||
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 ensureServices = computed(() => {
|
||||
if (!Array.isArray(props.options.services) || props.options.services.length === 0) {
|
||||
props.options.services = JSON.parse(JSON.stringify(defaultServices));
|
||||
}
|
||||
return props.options.services;
|
||||
});
|
||||
|
||||
// 1. 选择头像
|
||||
function handleSelectAvatar() {
|
||||
if (openImagePicker) {
|
||||
openImagePicker((url) => {
|
||||
if (url) props.options.avatar = url;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入用户头像图片 URL 地址:', '选择头像', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: props.options.avatar || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '图片地址不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
props.options.avatar = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 通用选择链接
|
||||
function handleSelectLink(key, title) {
|
||||
if (openLinkPicker) {
|
||||
openLinkPicker(({ url }) => {
|
||||
if (url) props.options[key] = url;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt(`请输入${title}(如 /member 或外部网址):`, `设置${title}`, {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: props.options[key] || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '链接不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
props.options[key] = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 服务项链接选择
|
||||
function handleSelectServiceLink(srv) {
|
||||
if (openLinkPicker) {
|
||||
openLinkPicker(({ url }) => {
|
||||
if (url) srv.url = url;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt(`请输入【${srv.name}】跳转链接:`, '设置便民服务链接', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: srv.url || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '链接不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
srv.url = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.panel-user-benefits {
|
||||
padding: 8px 4px;
|
||||
|
||||
.panel-section {
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 16px;
|
||||
@@ -148,7 +410,7 @@ if (props.options.orderUrl === undefined) props.options.orderUrl = '/member/orde
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
margin-bottom: 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
@@ -159,49 +421,225 @@ if (props.options.orderUrl === undefined) props.options.orderUrl = '/member/orde
|
||||
.item-label {
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mt-6 {
|
||||
margin-top: 6px;
|
||||
}
|
||||
.mt-10 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.mr-4 {
|
||||
margin-right: 4px;
|
||||
}
|
||||
.flex-1 {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.avatar-upload-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
padding: 8px 10px;
|
||||
|
||||
.avatar-preview-wrap {
|
||||
position: relative;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
border: 2px solid #cbd5e1;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
|
||||
.avatar-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.avatar-empty {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #e2e8f0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #64748b;
|
||||
font-size: 10px;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.avatar-hover-tip {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(15, 23, 42, 0.65);
|
||||
color: #ffffff;
|
||||
font-size: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
&:hover .avatar-hover-tip {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.metric-config-card {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
padding: 10px 12px;
|
||||
.avatar-right-controls {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
.metric-config-head {
|
||||
.avatar-input-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
|
||||
.metric-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #334155;
|
||||
.action-picker-btn {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.metric-inputs {
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px dashed #e2e8f0;
|
||||
.sub-tip-text {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sub-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
.link-picker-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.sub-label {
|
||||
width: 56px;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.link-input-icon {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.action-picker-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 0 10px;
|
||||
|
||||
&.link-btn {
|
||||
background: #ecf5ff;
|
||||
border-color: #b3d8ff;
|
||||
color: #409eff;
|
||||
|
||||
&:hover {
|
||||
background: #409eff;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mt-6 { margin-top: 6px; }
|
||||
.mt-10 { margin-top: 10px; }
|
||||
.link-current-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 11px;
|
||||
background: #eff6ff;
|
||||
border: 1px solid #dbeafe;
|
||||
border-radius: 4px;
|
||||
padding: 3px 8px;
|
||||
margin-top: 4px;
|
||||
overflow: hidden;
|
||||
|
||||
.tag-label {
|
||||
color: #3b82f6;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tag-val {
|
||||
color: #1e40af;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.url-clear-icon {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
color: #ef4444;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.metric-config-card {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
|
||||
.metric-config-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
|
||||
.metric-title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
}
|
||||
|
||||
.metric-inputs {
|
||||
.sub-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
.sub-label {
|
||||
font-size: 11px;
|
||||
color: #64748b;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.services-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.service-config-item {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
padding: 8px 10px;
|
||||
|
||||
.service-header-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.service-idx {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #64748b;
|
||||
width: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,20 +1,136 @@
|
||||
<template>
|
||||
<div class="panel-video">
|
||||
<!-- 1. 媒体资源设置 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">视频播放设置</div>
|
||||
<div class="section-title">视频源与媒体文件</div>
|
||||
|
||||
<!-- 视频文件选择 -->
|
||||
<div class="form-item">
|
||||
<span class="item-label">视频文件 URL (.mp4)</span>
|
||||
<el-input v-model="options.videoUrl" size="small" placeholder="请输入视频在线地址" clearable />
|
||||
<div class="input-with-action">
|
||||
<el-input
|
||||
v-model="options.videoUrl"
|
||||
size="small"
|
||||
placeholder="请输入在线视频地址或点击右侧选择"
|
||||
clearable
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon class="field-icon"><VideoCamera /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
class="action-picker-btn"
|
||||
@click="handleSelectVideo"
|
||||
>
|
||||
<el-icon class="mr-4"><VideoCamera /></el-icon>
|
||||
<span>选择视频</span>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="source-status-tag" v-if="options.videoUrl">
|
||||
<span class="status-dot"></span>
|
||||
<span class="status-text" :title="options.videoUrl">已绑定视频: {{ getMediaName(options.videoUrl) }}</span>
|
||||
<span class="status-clear-btn" @click="options.videoUrl = ''" title="清除视频">清空</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 视频封面图选择 -->
|
||||
<div class="form-item mt-14">
|
||||
<span class="item-label">视频封面图片 (海报/底图)</span>
|
||||
<div class="picker-flex-wrap">
|
||||
<!-- 缩略图预览与快捷替换 -->
|
||||
<div
|
||||
class="thumb-preview-box"
|
||||
@click="handleSelectCover"
|
||||
:title="options.coverUrl ? '点击更换封面图' : '点击选择封面图'"
|
||||
>
|
||||
<template v-if="options.coverUrl">
|
||||
<img :src="options.coverUrl" alt="封面" class="preview-img" />
|
||||
<span class="thumb-clear-btn" @click.stop="options.coverUrl = ''" title="清除封面">
|
||||
<el-icon><Close /></el-icon>
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-icon class="thumb-empty-icon"><Picture /></el-icon>
|
||||
<span class="thumb-empty-tip">选图</span>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- URL 输入框与选择图片按钮 -->
|
||||
<div class="input-with-action flex-1">
|
||||
<el-input
|
||||
v-model="options.coverUrl"
|
||||
size="small"
|
||||
placeholder="请输入封面图 URL 或点击选择"
|
||||
clearable
|
||||
/>
|
||||
<el-button
|
||||
size="small"
|
||||
class="action-picker-btn"
|
||||
@click="handleSelectCover"
|
||||
>
|
||||
<el-icon class="mr-4"><Picture /></el-icon>
|
||||
<span>选择图片</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. 点击跳转链接 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">跳转与行动点 (CTA)</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">视频封面图片 URL</span>
|
||||
<el-input v-model="options.coverUrl" size="small" placeholder="请输入视频封面图地址" clearable />
|
||||
<span class="item-label">跳转链接 (选填,点击视频或行动点跳转)</span>
|
||||
<div class="link-picker-wrap">
|
||||
<el-input
|
||||
v-model="options.url"
|
||||
size="small"
|
||||
placeholder="如 /goodsList?seckill=1 或完整网址"
|
||||
clearable
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon class="link-input-icon"><Link /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
size="small"
|
||||
class="action-picker-btn link-btn"
|
||||
@click="handleSelectLink"
|
||||
>
|
||||
<el-icon class="mr-4"><Link /></el-icon>
|
||||
<span>选择链接</span>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="link-current-tag" v-if="options.url">
|
||||
<span class="tag-label">当前链接:</span>
|
||||
<span class="tag-val" :title="options.url">{{ options.url }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3. 播放与展示控制 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">播放与展示控制</div>
|
||||
|
||||
<div class="form-item flex-row">
|
||||
<span class="item-label">播放器高度 (px)</span>
|
||||
<el-input-number
|
||||
v-model="options.height"
|
||||
:min="200"
|
||||
:max="800"
|
||||
:step="20"
|
||||
size="small"
|
||||
controls-position="right"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-item flex-row">
|
||||
<span class="item-label">播放高度 (px)</span>
|
||||
<el-input-number v-model="options.height" :min="200" :max="800" size="small" controls-position="right" />
|
||||
<span class="item-label">显示播放控制条</span>
|
||||
<el-switch v-model="options.controls" size="small" />
|
||||
</div>
|
||||
|
||||
<div class="form-item flex-row">
|
||||
@@ -31,6 +147,10 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject } from 'vue';
|
||||
import { VideoCamera, Picture, Link, Close } from '@element-plus/icons-vue';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
@@ -41,6 +161,86 @@ const props = defineProps({
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
// 注入来自 pc/index.vue 的全局选择器
|
||||
const openImagePicker = inject('openImagePicker', null);
|
||||
const openVideoPicker = inject('openVideoPicker', null);
|
||||
const openLinkPicker = inject('openLinkPicker', null);
|
||||
|
||||
// 提取 URL 中的文件名
|
||||
function getMediaName(url) {
|
||||
if (!url) return '';
|
||||
try {
|
||||
const cleanUrl = url.split('?')[0];
|
||||
const parts = cleanUrl.split('/');
|
||||
return parts[parts.length - 1] || url;
|
||||
} catch (e) {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 选择视频文件 (.mp4)
|
||||
function handleSelectVideo() {
|
||||
if (openVideoPicker) {
|
||||
openVideoPicker((url) => {
|
||||
if (url) props.options.videoUrl = url;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入在线视频 (.mp4) URL 地址:', '选择视频文件', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: props.options.videoUrl || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '视频地址不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
props.options.videoUrl = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 选择封面图片
|
||||
function handleSelectCover() {
|
||||
if (openImagePicker) {
|
||||
openImagePicker((url) => {
|
||||
if (url) props.options.coverUrl = url;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入视频封面图 URL 地址:', '选择视频封面', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: props.options.coverUrl || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '封面图地址不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
props.options.coverUrl = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 选择跳转链接
|
||||
function handleSelectLink() {
|
||||
if (openLinkPicker) {
|
||||
openLinkPicker(({ url }) => {
|
||||
if (url) props.options.url = url;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入跳转链接(如 /goodsList?seckill=1 或完整网址):', '设置跳转链接', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: props.options.url || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '链接不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
props.options.url = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -50,6 +250,12 @@ const props = defineProps({
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px dashed #e2e8f0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
@@ -57,6 +263,10 @@ const props = defineProps({
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.mt-14 {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -72,8 +282,192 @@ const props = defineProps({
|
||||
.item-label {
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mr-4 {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.flex-1 {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.field-icon {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.input-with-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
|
||||
.action-picker-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.source-status-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 11px;
|
||||
background: #f0fdf4;
|
||||
border: 1px solid #bbf7d0;
|
||||
color: #166534;
|
||||
border-radius: 4px;
|
||||
padding: 3px 8px;
|
||||
margin-top: 2px;
|
||||
overflow: hidden;
|
||||
|
||||
.status-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: #22c55e;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status-clear-btn {
|
||||
color: #dc2626;
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
padding-left: 4px;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.picker-flex-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.thumb-preview-box {
|
||||
position: relative;
|
||||
width: 58px;
|
||||
height: 40px;
|
||||
border-radius: 4px;
|
||||
border: 1px dashed #cbd5e1;
|
||||
background: #ffffff;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
transition: border-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #409eff;
|
||||
}
|
||||
|
||||
.preview-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.thumb-empty-icon {
|
||||
font-size: 15px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.thumb-empty-tip {
|
||||
font-size: 10px;
|
||||
color: #94a3b8;
|
||||
line-height: 1;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.thumb-clear-btn {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
background: rgba(0, 0, 0, 0.65);
|
||||
color: #ffffff;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: #ef4444;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link-picker-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.link-input-icon {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.action-picker-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 0 10px;
|
||||
|
||||
&.link-btn {
|
||||
background: #ecf5ff;
|
||||
border-color: #b3d8ff;
|
||||
color: #409eff;
|
||||
|
||||
&:hover {
|
||||
background: #409eff;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link-current-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 11px;
|
||||
background: #eff6ff;
|
||||
border: 1px solid #dbeafe;
|
||||
border-radius: 4px;
|
||||
padding: 3px 8px;
|
||||
margin-top: 4px;
|
||||
overflow: hidden;
|
||||
|
||||
.tag-label {
|
||||
color: #3b82f6;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tag-val {
|
||||
color: #1e40af;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
type="text"
|
||||
v-model="row.rowStyle.height"
|
||||
class="dim-input"
|
||||
placeholder="40"
|
||||
placeholder="默认"
|
||||
/>
|
||||
</div>
|
||||
<div class="dimension-item">
|
||||
|
||||
@@ -211,6 +211,20 @@
|
||||
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
import {
|
||||
House, Compass, Picture, ShoppingBag, Ticket, Document, Menu, Bottom as BottomIcon,
|
||||
List, Grid, Fold, PictureRounded, Medal, Aim, FullScreen, Files, VideoCamera,
|
||||
Goods, Postcard, Reading, DArrowRight, CollectionTag, Star, Edit, Bell,
|
||||
Search, User, Rank
|
||||
} from '@element-plus/icons-vue';
|
||||
|
||||
const iconMap = {
|
||||
House, Compass, Picture, ShoppingBag, Ticket, Document, Menu, Bottom: BottomIcon,
|
||||
List, Grid, Fold, PictureRounded, Medal, Aim, FullScreen, Files, VideoCamera,
|
||||
Goods, Postcard, Reading, DArrowRight, CollectionTag, Star, Edit, Bell,
|
||||
Search, User, Rank
|
||||
};
|
||||
|
||||
import RowConfigPanel from './RowConfigPanel.vue';
|
||||
|
||||
import ComponentConfigPanel from './ComponentConfigPanel.vue';
|
||||
@@ -367,19 +381,11 @@ const currentSelectedTitle = computed(() => {
|
||||
|
||||
|
||||
const currentSelectedIcon = computed(() => {
|
||||
|
||||
if (props.selectedComponent) {
|
||||
|
||||
if (props.selectedComponent.icon) return props.selectedComponent.icon;
|
||||
|
||||
const def = COMPONENT_DEFINITIONS.find(d => d.type === props.selectedComponent.type);
|
||||
|
||||
return def?.icon || 'Grid';
|
||||
|
||||
const iconName = props.selectedComponent.icon || COMPONENT_DEFINITIONS.find(d => d.type === props.selectedComponent.type)?.icon;
|
||||
return iconMap[iconName] || Grid;
|
||||
}
|
||||
|
||||
return 'Rank';
|
||||
|
||||
return Rank;
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -93,6 +93,7 @@ export const COMPONENT_DEFINITIONS = [
|
||||
{
|
||||
|
||||
type: 'topBar',
|
||||
defaultRowHeight: 40,
|
||||
|
||||
name: '快捷栏',
|
||||
|
||||
@@ -130,7 +131,7 @@ export const COMPONENT_DEFINITIONS = [
|
||||
|
||||
radius: [0, 0, 0, 0],
|
||||
|
||||
padding: [0, 16, 0, 16]
|
||||
padding: [0, 0, 0, 0]
|
||||
|
||||
}
|
||||
|
||||
@@ -139,6 +140,7 @@ export const COMPONENT_DEFINITIONS = [
|
||||
{
|
||||
|
||||
type: 'searchHeader',
|
||||
defaultRowHeight: 120,
|
||||
|
||||
name: '搜索头部',
|
||||
|
||||
@@ -152,7 +154,10 @@ export const COMPONENT_DEFINITIONS = [
|
||||
|
||||
logoUrl: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/logo/logo.png',
|
||||
|
||||
placeholder: '搜索品牌或商品',
|
||||
placeholder: '请输入商品名称',
|
||||
showSearchTypes: true,
|
||||
defaultSearchType: 'goods',
|
||||
storePlaceholder: '请输入店铺名称',
|
||||
|
||||
hotWords: ['手机', '数码家电', '秋季上新', '电脑整机', '智能手表'],
|
||||
|
||||
@@ -180,7 +185,7 @@ export const COMPONENT_DEFINITIONS = [
|
||||
|
||||
radius: [0, 0, 0, 0],
|
||||
|
||||
padding: [16, 20, 16, 20]
|
||||
padding: [16, 0, 16, 0]
|
||||
|
||||
}
|
||||
|
||||
@@ -189,6 +194,7 @@ export const COMPONENT_DEFINITIONS = [
|
||||
{
|
||||
|
||||
type: 'footerNav',
|
||||
defaultRowHeight: 450,
|
||||
|
||||
name: '页脚导航',
|
||||
|
||||
@@ -257,6 +263,7 @@ export const COMPONENT_DEFINITIONS = [
|
||||
{
|
||||
|
||||
type: 'categoryNav',
|
||||
defaultRowHeight: 48,
|
||||
name: '类目导航',
|
||||
group: 'navigation',
|
||||
icon: 'List',
|
||||
@@ -292,6 +299,7 @@ export const COMPONENT_DEFINITIONS = [
|
||||
{
|
||||
|
||||
type: 'channelGrid',
|
||||
defaultRowHeight: 120,
|
||||
name: '频道宫格',
|
||||
group: 'navigation',
|
||||
icon: 'Grid',
|
||||
@@ -318,15 +326,17 @@ export const COMPONENT_DEFINITIONS = [
|
||||
{
|
||||
|
||||
type: 'categoryMegaMenu',
|
||||
name: '类目弹层',
|
||||
defaultRowHeight: 320,
|
||||
name: '商品分类',
|
||||
group: 'navigation',
|
||||
icon: 'Fold',
|
||||
description: '垂直分类菜单与多级展开弹层',
|
||||
defaultOptions: {
|
||||
title: '分类',
|
||||
title: '商品分类',
|
||||
source: 'system',
|
||||
textColor: '#111927',
|
||||
fontSize: 14,
|
||||
activeColor: '#ea580c',
|
||||
activeColor: '#409EFF',
|
||||
categories: [
|
||||
{
|
||||
name: '精选频道',
|
||||
@@ -360,8 +370,8 @@ export const COMPONENT_DEFINITIONS = [
|
||||
},
|
||||
defaultComponentStyle: {
|
||||
widthRatio: '16.67%',
|
||||
backgroundColor: 'transparent',
|
||||
radius: [0, 0, 0, 0],
|
||||
backgroundColor: '#ffffff',
|
||||
radius: [8, 8, 8, 8],
|
||||
padding: [0, 0, 0, 0]
|
||||
}
|
||||
},
|
||||
@@ -373,6 +383,7 @@ export const COMPONENT_DEFINITIONS = [
|
||||
{
|
||||
|
||||
type: 'carousel',
|
||||
defaultRowHeight: 320,
|
||||
name: '轮播图',
|
||||
group: 'media',
|
||||
icon: 'PictureRounded',
|
||||
@@ -385,7 +396,7 @@ export const COMPONENT_DEFINITIONS = [
|
||||
showIndicator: true,
|
||||
fitMode: 'contain',
|
||||
aspectRatio: '2.5:1',
|
||||
height: 380,
|
||||
height: 420,
|
||||
list: [
|
||||
{
|
||||
img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/carousel/banner_1.png',
|
||||
@@ -411,8 +422,8 @@ export const COMPONENT_DEFINITIONS = [
|
||||
},
|
||||
defaultComponentStyle: {
|
||||
widthRatio: '66.67%',
|
||||
backgroundColor: 'transparent',
|
||||
radius: [0, 0, 0, 0],
|
||||
backgroundColor: '#ffffff',
|
||||
radius: [8, 8, 8, 8],
|
||||
padding: [0, 0, 0, 0]
|
||||
}
|
||||
},
|
||||
@@ -420,6 +431,7 @@ export const COMPONENT_DEFINITIONS = [
|
||||
{
|
||||
|
||||
type: 'brandWall',
|
||||
defaultRowHeight: 220,
|
||||
name: '品牌图',
|
||||
group: 'media',
|
||||
icon: 'Medal',
|
||||
@@ -444,6 +456,7 @@ export const COMPONENT_DEFINITIONS = [
|
||||
|
||||
{
|
||||
type: 'hotZone',
|
||||
defaultRowHeight: 750,
|
||||
name: '热区图',
|
||||
group: 'media',
|
||||
icon: 'Aim',
|
||||
@@ -488,6 +501,7 @@ export const COMPONENT_DEFINITIONS = [
|
||||
{
|
||||
|
||||
type: 'freeLayout',
|
||||
defaultRowHeight: 450,
|
||||
|
||||
name: '自由布局',
|
||||
|
||||
@@ -518,6 +532,7 @@ export const COMPONENT_DEFINITIONS = [
|
||||
{
|
||||
|
||||
type: 'promoGroup',
|
||||
defaultRowHeight: 180,
|
||||
|
||||
name: '活动卡组',
|
||||
|
||||
@@ -552,6 +567,7 @@ export const COMPONENT_DEFINITIONS = [
|
||||
{
|
||||
|
||||
type: 'video',
|
||||
defaultRowHeight: 360,
|
||||
name: '视频',
|
||||
group: 'media',
|
||||
icon: 'VideoCamera',
|
||||
@@ -559,6 +575,7 @@ export const COMPONENT_DEFINITIONS = [
|
||||
defaultOptions: {
|
||||
videoUrl: '',
|
||||
coverUrl: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/video/video_cover.png',
|
||||
url: '',
|
||||
autoPlay: false,
|
||||
loop: false,
|
||||
controls: true,
|
||||
@@ -580,6 +597,7 @@ export const COMPONENT_DEFINITIONS = [
|
||||
{
|
||||
|
||||
type: 'goodsList',
|
||||
defaultRowHeight: 980,
|
||||
|
||||
name: '商品列表',
|
||||
|
||||
@@ -709,6 +727,7 @@ export const COMPONENT_DEFINITIONS = [
|
||||
{
|
||||
|
||||
type: 'gridFloor',
|
||||
defaultRowHeight: 560,
|
||||
|
||||
name: '宫格楼层',
|
||||
|
||||
@@ -824,6 +843,7 @@ export const COMPONENT_DEFINITIONS = [
|
||||
{
|
||||
|
||||
type: 'goodsGrid',
|
||||
defaultRowHeight: 420,
|
||||
name: '商品宫格',
|
||||
group: 'commerce',
|
||||
icon: 'Menu',
|
||||
@@ -864,6 +884,7 @@ export const COMPONENT_DEFINITIONS = [
|
||||
{
|
||||
|
||||
type: 'goodsCard',
|
||||
defaultRowHeight: 360,
|
||||
|
||||
name: '商品卡片',
|
||||
|
||||
@@ -874,6 +895,8 @@ export const COMPONENT_DEFINITIONS = [
|
||||
description: '精选单品爆款特写大卡片',
|
||||
|
||||
defaultOptions: {
|
||||
type: 'goodsCard',
|
||||
showSales: false,
|
||||
goodsId: '1',
|
||||
goodsInfo: { id: '1', goodsName: '旗舰 5G 智能手机 12GB+256GB 超光影影像系统', price: '3999.00', originalPrice: '4599.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_1.png' },
|
||||
customTitle: '年度年度旗舰 · 科技美学',
|
||||
@@ -899,6 +922,7 @@ export const COMPONENT_DEFINITIONS = [
|
||||
{
|
||||
|
||||
type: 'bannerFloor',
|
||||
defaultRowHeight: 560,
|
||||
|
||||
name: '横幅楼层',
|
||||
|
||||
@@ -940,6 +964,7 @@ export const COMPONENT_DEFINITIONS = [
|
||||
{
|
||||
|
||||
type: 'goodsBar',
|
||||
defaultRowHeight: 280,
|
||||
|
||||
name: '商品条',
|
||||
|
||||
@@ -976,6 +1001,7 @@ export const COMPONENT_DEFINITIONS = [
|
||||
},
|
||||
|
||||
{type: 'channelFloor',
|
||||
defaultRowHeight: 620,
|
||||
|
||||
name: '频道楼层',
|
||||
|
||||
@@ -1019,6 +1045,7 @@ export const COMPONENT_DEFINITIONS = [
|
||||
|
||||
{
|
||||
type: 'recommendFeed',
|
||||
defaultRowHeight: 1270,
|
||||
|
||||
name: '推荐商品',
|
||||
|
||||
@@ -1059,6 +1086,7 @@ export const COMPONENT_DEFINITIONS = [
|
||||
// 5. 营销 (2个)
|
||||
{
|
||||
type: 'userBenefits',
|
||||
defaultRowHeight: 320,
|
||||
name: '用户权益',
|
||||
group: 'marketing',
|
||||
icon: 'User',
|
||||
@@ -1092,6 +1120,7 @@ export const COMPONENT_DEFINITIONS = [
|
||||
},
|
||||
{
|
||||
type: 'couponList',
|
||||
defaultRowHeight: 160,
|
||||
|
||||
name: '优惠券',
|
||||
|
||||
@@ -1131,6 +1160,7 @@ export const COMPONENT_DEFINITIONS = [
|
||||
{
|
||||
|
||||
type: 'titleBar',
|
||||
defaultRowHeight: 48,
|
||||
|
||||
name: '标题栏',
|
||||
|
||||
@@ -1165,6 +1195,7 @@ export const COMPONENT_DEFINITIONS = [
|
||||
},
|
||||
|
||||
{type: 'articleContent',
|
||||
defaultRowHeight: '',
|
||||
name: '文章内容',
|
||||
group: 'content',
|
||||
icon: 'Document',
|
||||
@@ -1201,6 +1232,7 @@ export const COMPONENT_DEFINITIONS = [
|
||||
|
||||
{
|
||||
type: 'noticeBar',
|
||||
defaultRowHeight: 40,
|
||||
|
||||
name: '公告快讯',
|
||||
|
||||
@@ -1234,7 +1266,7 @@ export const COMPONENT_DEFINITIONS = [
|
||||
|
||||
export const DEFAULT_ROW_STYLE = {
|
||||
width: 1920,
|
||||
height: 40,
|
||||
height: '',
|
||||
gap: 0,
|
||||
margin: [0, 0, 0, 0],
|
||||
padding: [0, 0, 0, 0],
|
||||
@@ -1250,27 +1282,32 @@ export const DEFAULT_ROW_STYLE = {
|
||||
*/
|
||||
|
||||
export function createRow(component = null) {
|
||||
|
||||
const rowId = `row_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
|
||||
|
||||
const components = [];
|
||||
let rowHeight = '';
|
||||
|
||||
if (component) {
|
||||
|
||||
components.push(createComponent(component));
|
||||
|
||||
if (component.type && !component.componentStyle) {
|
||||
// 传入的是组件定义 def
|
||||
components.push(createComponent(component));
|
||||
if (component.defaultRowHeight !== undefined && component.defaultRowHeight !== null) {
|
||||
rowHeight = component.defaultRowHeight;
|
||||
}
|
||||
} else {
|
||||
// 传入的是已有组件实例
|
||||
components.push(component);
|
||||
const def = COMPONENT_DEFINITIONS.find(d => d.type === component.type);
|
||||
if (def?.defaultRowHeight !== undefined && def?.defaultRowHeight !== null) {
|
||||
rowHeight = def.defaultRowHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
id: rowId,
|
||||
|
||||
rowStyle: { ...DEFAULT_ROW_STYLE },
|
||||
|
||||
rowStyle: { ...DEFAULT_ROW_STYLE, height: rowHeight },
|
||||
components
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -143,7 +143,7 @@
|
||||
|
||||
<el-icon :size="20" class="comp-icon">
|
||||
|
||||
<component :is="iconMap[element.icon] || 'Grid'" />
|
||||
<component :is="iconMap[element.icon] || Grid" />
|
||||
|
||||
</el-icon>
|
||||
|
||||
@@ -329,17 +329,21 @@
|
||||
|
||||
v-if="resolveCanvasTemplate(comp.type)"
|
||||
|
||||
:component="comp"
|
||||
|
||||
:options="comp.options || {}"
|
||||
|
||||
:component-style="comp.componentStyle || {}"
|
||||
|
||||
:row-style="row.rowStyle || {}"
|
||||
|
||||
/>
|
||||
|
||||
<div v-else class="comp-mock-placeholder">
|
||||
|
||||
<el-icon :size="24" class="mock-icon">
|
||||
|
||||
<component :is="iconMap[getCompIcon(comp.type)] || 'Grid'" />
|
||||
<component :is="iconMap[getCompIcon(comp.type)] || Grid" />
|
||||
|
||||
</el-icon>
|
||||
|
||||
@@ -435,7 +439,7 @@
|
||||
@selectedGoodsData="handleGlobalGoodsSelected"
|
||||
/>
|
||||
|
||||
<!-- 全局图片选择器弹窗 -->
|
||||
<!-- 全局媒体选择器弹窗(图片/视频) -->
|
||||
<el-dialog
|
||||
v-model="picModelFlag"
|
||||
width="1160px"
|
||||
@@ -443,7 +447,7 @@
|
||||
class="pc-image-picker-dialog"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
title="选择图片"
|
||||
:title="currentPickerType === 'video' ? '选择视频' : '选择图片'"
|
||||
@close="handleImageDialogClose"
|
||||
>
|
||||
<ossManage
|
||||
@@ -451,8 +455,8 @@
|
||||
:is-component="true"
|
||||
:initialize="picModelFlag"
|
||||
:max-select="1"
|
||||
:only-image="true"
|
||||
media-type="IMAGE"
|
||||
:only-image="currentPickerType !== 'video'"
|
||||
:media-type="currentPickerType === 'video' ? 'VIDEO' : 'IMAGE'"
|
||||
ref="ossManageRef"
|
||||
/>
|
||||
</el-dialog>
|
||||
@@ -478,7 +482,7 @@ import {
|
||||
|
||||
List, Grid, Fold, PictureRounded, Medal, Aim, FullScreen, VideoCamera,
|
||||
|
||||
Goods, Postcard, Reading, DArrowRight, CollectionTag, Star, Edit, Bell
|
||||
Goods, Postcard, Reading, DArrowRight, CollectionTag, Star, Edit, Bell, User
|
||||
|
||||
} from '@element-plus/icons-vue';
|
||||
|
||||
@@ -516,7 +520,7 @@ const iconMap = {
|
||||
|
||||
List, Grid, Fold, PictureRounded, Medal, Aim, FullScreen, Files, VideoCamera,
|
||||
|
||||
Goods, Postcard, Reading, DArrowRight, CollectionTag, Star, Edit, Bell
|
||||
Goods, Postcard, Reading, DArrowRight, CollectionTag, Star, Edit, Bell, User, Search
|
||||
|
||||
};
|
||||
|
||||
@@ -795,55 +799,26 @@ function clearSelection() {
|
||||
// 规范化:确保每个组件都是独立的一行(一个组件是一行)
|
||||
|
||||
function normalizeRows(list) {
|
||||
|
||||
if (!Array.isArray(list)) return [];
|
||||
|
||||
const normalized = [];
|
||||
|
||||
list.forEach(r => {
|
||||
|
||||
if (r.components && r.components.length > 1) {
|
||||
|
||||
r.components.forEach((c, idx) => {
|
||||
|
||||
const singleRow = createRow();
|
||||
|
||||
singleRow.id = idx === 0 ? r.id : `row_${Date.now()}_${Math.random().toString(36).substring(2, 6)}_${idx}`;
|
||||
|
||||
singleRow.rowStyle = JSON.parse(JSON.stringify(r.rowStyle || DEFAULT_ROW_STYLE));
|
||||
|
||||
singleRow.components = [c];
|
||||
|
||||
normalized.push(singleRow);
|
||||
|
||||
});
|
||||
|
||||
} else {
|
||||
|
||||
normalized.push(r);
|
||||
|
||||
if (!r) return;
|
||||
if (r.rowStyle && (r.rowStyle.height === 40 || r.rowStyle.height === "40")) {
|
||||
r.rowStyle.height = "";
|
||||
}
|
||||
|
||||
normalized.push(r);
|
||||
});
|
||||
|
||||
return normalized;
|
||||
|
||||
}
|
||||
|
||||
// 拖拽克隆生成行(一个组件是一行)
|
||||
|
||||
function handleCloneAsRow(def) {
|
||||
|
||||
markDirty();
|
||||
|
||||
const newComp = createComponent(def);
|
||||
|
||||
const newRow = createRow();
|
||||
|
||||
const newRow = createRow(def);
|
||||
newRow.components = [newComp];
|
||||
|
||||
return newRow;
|
||||
|
||||
}
|
||||
|
||||
// 点击左侧「+」或组件卡片:追加新组件(支持同排自动合并)
|
||||
@@ -857,15 +832,13 @@ function handleAppendComponent(def) {
|
||||
// 1. 若当前选中的是无组件的空白行,放入该行
|
||||
|
||||
if (selectedRow.value && (!selectedRow.value.components || selectedRow.value.components.length === 0)) {
|
||||
|
||||
selectedRow.value.components = [newComp];
|
||||
|
||||
if (def.defaultRowHeight !== undefined && def.defaultRowHeight !== null && def.defaultRowHeight !== '' && (!selectedRow.value.rowStyle.height || selectedRow.value.rowStyle.height === '')) {
|
||||
selectedRow.value.rowStyle.height = def.defaultRowHeight;
|
||||
}
|
||||
selectedCompId.value = newComp.id;
|
||||
|
||||
configPanelRef.value?.selectComponentTab();
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
// 2. 若当前选中的行已有组件且宽度未满 100%(例如已有 50% 频道宫格,再点击则同行放置)
|
||||
@@ -894,9 +867,7 @@ function handleAppendComponent(def) {
|
||||
}
|
||||
|
||||
// 3. 否则新建独立行
|
||||
|
||||
const newRow = createRow();
|
||||
|
||||
const newRow = createRow(def);
|
||||
newRow.components = [newComp];
|
||||
|
||||
if (selectedRowId.value) {
|
||||
@@ -943,10 +914,14 @@ function openLinkPicker(callback) {
|
||||
}
|
||||
}
|
||||
|
||||
function openGoodsPicker(callback) {
|
||||
function openGoodsPicker(callback, initialSelected = []) {
|
||||
pendingGoodsCallback.value = callback;
|
||||
if (liliDialogRef.value) {
|
||||
liliDialogRef.value.clearGoodsSelected();
|
||||
if (initialSelected && Array.isArray(initialSelected) && initialSelected.length) {
|
||||
liliDialogRef.value.goodsData = initialSelected.map(item => ({ ...item }));
|
||||
} else {
|
||||
liliDialogRef.value.clearGoodsSelected();
|
||||
}
|
||||
liliDialogRef.value.goodsFlag = true;
|
||||
liliDialogRef.value.flag = true;
|
||||
}
|
||||
@@ -1021,8 +996,10 @@ provide('openGoodsPicker', openGoodsPicker);
|
||||
const picModelFlag = ref(false);
|
||||
const ossManageRef = ref(null);
|
||||
const pendingImageCallback = ref(null);
|
||||
const currentPickerType = ref('pic');
|
||||
|
||||
function openImagePicker(callback) {
|
||||
currentPickerType.value = 'pic';
|
||||
pendingImageCallback.value = callback;
|
||||
picModelFlag.value = true;
|
||||
nextTick(() => {
|
||||
@@ -1036,6 +1013,21 @@ function openImagePicker(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function openVideoPicker(callback) {
|
||||
currentPickerType.value = 'video';
|
||||
pendingImageCallback.value = callback;
|
||||
picModelFlag.value = true;
|
||||
nextTick(() => {
|
||||
if (ossManageRef.value) {
|
||||
ossManageRef.value.selectImage = true;
|
||||
ossManageRef.value.fileType = 'video';
|
||||
if (typeof ossManageRef.value.changeFileType === 'function') {
|
||||
ossManageRef.value.changeFileType();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleGlobalImageSelected(val) {
|
||||
picModelFlag.value = false;
|
||||
let url = '';
|
||||
@@ -1056,6 +1048,7 @@ function handleImageDialogClose() {
|
||||
}
|
||||
|
||||
provide('openImagePicker', openImagePicker);
|
||||
provide('openVideoPicker', openVideoPicker);
|
||||
function handleAddNewRow() {
|
||||
|
||||
markDirty();
|
||||
@@ -1090,6 +1083,19 @@ function moveRow(index, offset) {
|
||||
|
||||
}
|
||||
|
||||
function moveRowTo(fromIndex, toIndex) {
|
||||
if (fromIndex === undefined || fromIndex === null || fromIndex < 0) return;
|
||||
if (toIndex === undefined || toIndex === null || toIndex < 0) return;
|
||||
const list = pageDocument.value.list;
|
||||
if (!Array.isArray(list) || fromIndex >= list.length || toIndex >= list.length) return;
|
||||
if (fromIndex === toIndex) return;
|
||||
|
||||
markDirty();
|
||||
const [removed] = list.splice(fromIndex, 1);
|
||||
list.splice(toIndex, 0, removed);
|
||||
selectedRowId.value = removed.id;
|
||||
}
|
||||
|
||||
function moveRowToExtreme(index, direction) {
|
||||
|
||||
markDirty();
|
||||
@@ -1211,7 +1217,7 @@ function handleDuplicateComp(row, cIndex, comp) {
|
||||
selectedCompId.value = clone.id;
|
||||
Message.success('已复制到当前行(同行展示)');
|
||||
} else {
|
||||
const newRow = createRow();
|
||||
const newRow = createRow(clone);
|
||||
newRow.components = [clone];
|
||||
const rIdx = pageDocument.value.list.findIndex(r => r.id === row.id);
|
||||
if (rIdx !== -1) {
|
||||
@@ -1273,7 +1279,9 @@ function getRowWrapperStyle(style) {
|
||||
if (style.height !== undefined && style.height !== '') {
|
||||
const h = String(style.height).trim();
|
||||
if (h && Number(h) > 0) {
|
||||
s.minHeight = /^\d+$/.test(h) ? `${h}px` : h;
|
||||
const hVal = /^\d+$/.test(h) ? `${h}px` : h;
|
||||
s.height = hVal;
|
||||
s.minHeight = hVal;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1311,15 +1319,24 @@ function getRowWrapperStyle(style) {
|
||||
}
|
||||
|
||||
function getRowFlexStyle(style) {
|
||||
if (!style) return { display: 'flex', flexDirection: 'row', flexWrap: 'nowrap', width: '100%' };
|
||||
if (!style) return { display: 'flex', flexDirection: 'row', flexWrap: 'nowrap', width: '100%', height: '100%' };
|
||||
const s = {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'nowrap',
|
||||
alignItems: style.alignItems || 'stretch',
|
||||
justifyContent: style.justifyContent || 'flex-start',
|
||||
width: '100%'
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
};
|
||||
if (style.height !== undefined && style.height !== '') {
|
||||
const h = String(style.height).trim();
|
||||
if (h && Number(h) > 0) {
|
||||
const hVal = /^\d+$/.test(h) ? `${h}px` : h;
|
||||
s.height = hVal;
|
||||
s.minHeight = hVal;
|
||||
}
|
||||
}
|
||||
if (style.gap !== undefined && style.gap !== '') {
|
||||
const g = String(style.gap).trim();
|
||||
if (g && Number(g) > 0) {
|
||||
@@ -1336,6 +1353,15 @@ function getCompWrapperStyle(style, rowStyle = {}, totalComps = 1) {
|
||||
if (!style) return {};
|
||||
|
||||
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;
|
||||
|
||||
@@ -2035,6 +2061,10 @@ onUnmounted(() => {
|
||||
|
||||
box-sizing: border-box;
|
||||
|
||||
display: flex;
|
||||
|
||||
flex-direction: column;
|
||||
|
||||
border: 2px solid transparent;
|
||||
|
||||
transition: border-color 0.2s;
|
||||
@@ -2241,7 +2271,7 @@ onUnmounted(() => {
|
||||
|
||||
box-sizing: border-box;
|
||||
|
||||
overflow: hidden;
|
||||
overflow: visible;
|
||||
|
||||
flex: 1;
|
||||
|
||||
@@ -2251,6 +2281,13 @@ onUnmounted(() => {
|
||||
|
||||
height: 100%;
|
||||
|
||||
> * {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.comp-mock-placeholder {
|
||||
|
||||
display: flex;
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
|
||||
@@ -57,6 +57,10 @@ const props = defineProps({
|
||||
componentStyle: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
rowStyle: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
@@ -78,10 +82,10 @@ 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' }
|
||||
];
|
||||
});
|
||||
|
||||
@@ -89,41 +93,25 @@ const currentSlide = computed(() => {
|
||||
return slidesList.value[currentIdx.value] || slidesList.value[0];
|
||||
});
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
@@ -158,6 +146,7 @@ function stopTimer() {
|
||||
}
|
||||
|
||||
function resetTimer() {
|
||||
stopTimer();
|
||||
startTimer();
|
||||
}
|
||||
|
||||
@@ -180,6 +169,7 @@ onBeforeUnmount(() => {
|
||||
<style lang="scss" scoped>
|
||||
.tpl-carousel {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
@@ -188,10 +178,13 @@ onBeforeUnmount(() => {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.carousel-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -208,81 +201,68 @@ onBeforeUnmount(() => {
|
||||
.slide-img {
|
||||
width: 100%;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 切换动效
|
||||
.carousel-fade-enter-active,
|
||||
.carousel-fade-leave-active {
|
||||
transition: opacity 0.4s ease;
|
||||
}
|
||||
.carousel-fade-enter-from,
|
||||
.carousel-fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
// 左右箭头按钮
|
||||
.arrow-btn {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: rgba(15, 23, 42, 0.35);
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
color: #ffffff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
transition: all 0.2s ease;
|
||||
z-index: 10;
|
||||
backdrop-filter: blur(2px);
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: rgba(15, 23, 42, 0.65);
|
||||
background: rgba(0, 0, 0, 0.65);
|
||||
transform: translateY(-50%) scale(1.08);
|
||||
}
|
||||
|
||||
&.prev-btn {
|
||||
left: 16px;
|
||||
left: 12px;
|
||||
}
|
||||
|
||||
&.next-btn {
|
||||
right: 16px;
|
||||
right: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
// 底部指示点
|
||||
.carousel-indicators {
|
||||
position: absolute;
|
||||
bottom: 14px;
|
||||
bottom: 12px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
z-index: 10;
|
||||
padding: 3px 8px;
|
||||
background: rgba(0, 0, 0, 0.18);
|
||||
border-radius: 10px;
|
||||
backdrop-filter: blur(2px);
|
||||
|
||||
.indicator-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.65);
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
cursor: pointer;
|
||||
transition: all 0.25s ease;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
|
||||
transition: all 0.25s;
|
||||
|
||||
&.active {
|
||||
width: 22px;
|
||||
border-radius: 5px;
|
||||
background: #ea580c;
|
||||
width: 16px;
|
||||
border-radius: 3px;
|
||||
background: #409EFF;
|
||||
}
|
||||
|
||||
&:hover:not(.active) {
|
||||
@@ -292,42 +272,33 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
}
|
||||
|
||||
&.is-compact {
|
||||
&.is-mini {
|
||||
.arrow-btn {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
font-size: 12px;
|
||||
&.prev-btn { left: 8px; }
|
||||
&.next-btn { right: 8px; }
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.carousel-indicators {
|
||||
bottom: 8px;
|
||||
gap: 5px;
|
||||
gap: 4px;
|
||||
.indicator-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
&.active { width: 14px; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-mini {
|
||||
.arrow-btn {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
font-size: 10px;
|
||||
&.prev-btn { left: 4px; }
|
||||
&.next-btn { right: 4px; }
|
||||
}
|
||||
.carousel-indicators {
|
||||
bottom: 4px;
|
||||
gap: 3px;
|
||||
.indicator-dot {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
&.active { width: 10px; }
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
&.active {
|
||||
width: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.carousel-fade-enter-active,
|
||||
.carousel-fade-leave-active {
|
||||
transition: opacity 0.35s ease;
|
||||
}
|
||||
|
||||
.carousel-fade-enter-from,
|
||||
.carousel-fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,32 +1,83 @@
|
||||
<template>
|
||||
<div class="tpl-category-mega-menu" :style="customStyle">
|
||||
<!-- 顶部标题:分类(同万米设计,纯净大方) -->
|
||||
<div
|
||||
class="tpl-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">
|
||||
<div
|
||||
v-for="(cat, idx) in categoriesList"
|
||||
:key="idx"
|
||||
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)"
|
||||
>
|
||||
<span class="cat-name">{{ cat.name }}</span>
|
||||
<el-icon class="arrow-icon"><ArrowRight /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧悬浮超级大面板 (Mega Menu 仿真预览) -->
|
||||
<transition name="fade-flyout">
|
||||
<div
|
||||
v-if="panelVisible && activeChildren.length"
|
||||
class="mega-flyout-panel"
|
||||
@mouseenter="handlePanelEnter"
|
||||
@mouseleave="handleMouseLeave"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
{{ 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">
|
||||
<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"
|
||||
>
|
||||
{{ leaf.name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { ArrowRight } from '@element-plus/icons-vue';
|
||||
import { getAllCategoryList } from '@/api/goods.js';
|
||||
|
||||
const props = defineProps({
|
||||
options: {
|
||||
@@ -36,41 +87,150 @@ 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: '母婴玩具' }
|
||||
{
|
||||
id: '1',
|
||||
name: '电脑办公',
|
||||
children: [
|
||||
{ id: '1-1', name: '手机通讯', children: [{ name: '手机' }, { name: '游戏手机' }, { name: '老人机' }] },
|
||||
{ id: '1-2', name: '电脑整机', children: [{ name: '笔记本' }, { name: '台式机' }, { name: '平板电脑' }] }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: '家用电器',
|
||||
children: [
|
||||
{ id: '2-1', name: '大家电', children: [{ name: '电视' }, { name: '冰箱' }, { name: '洗衣机' }, { name: '空调' }] },
|
||||
{ id: '2-2', name: '厨房小电', children: [{ name: '电饭煲' }, { name: '微波炉' }, { name: '咖啡机' }] }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: '服装鞋包',
|
||||
children: [
|
||||
{ id: '3-1', name: '女装', children: [{ name: '连衣裙' }, { name: '衬衫' }, { name: '羽绒服' }] },
|
||||
{ id: '3-2', name: '男装', children: [{ name: 'T恤' }, { name: '夹克' }, { name: '牛仔裤' }] }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: '食品生鲜',
|
||||
children: [
|
||||
{ id: '4-1', name: '生鲜水果', children: [{ name: '苹果' }, { name: '车厘子' }, { name: '奇异果' }] },
|
||||
{ id: '4-2', name: '休闲零食', children: [{ name: '坚果炒货' }, { name: '饼干蛋糕' }, { name: '糖果果冻' }] }
|
||||
]
|
||||
},
|
||||
{ id: '5', name: '精品箱包' },
|
||||
{ id: '6', name: '美妆个护' },
|
||||
{ id: '7', name: '运动户外' },
|
||||
{ id: '8', 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(() => {
|
||||
if (props.options?.source === 'system') return false;
|
||||
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 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 defaultCategories;
|
||||
});
|
||||
|
||||
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,
|
||||
children: (s.items || []).map((it, iIdx) => ({
|
||||
id: `leaf_${idx}_${iIdx}`,
|
||||
name: typeof it === 'string' ? it : it.name
|
||||
}))
|
||||
}));
|
||||
}
|
||||
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 fetchCategories() {
|
||||
getAllCategoryList(0).then(res => {
|
||||
if (res.success && Array.isArray(res.result) && res.result.length) {
|
||||
systemCategories.value = res.result;
|
||||
}
|
||||
}).catch(() => {
|
||||
// 降级使用默认真实分类
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchCategories();
|
||||
});
|
||||
|
||||
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>
|
||||
@@ -79,7 +239,6 @@ const customStyle = computed(() => {
|
||||
.tpl-category-mega-menu {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 420px;
|
||||
box-sizing: border-box;
|
||||
background: #ffffff;
|
||||
border: 1px solid #e2e8f0;
|
||||
@@ -87,16 +246,23 @@ const customStyle = computed(() => {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
user-select: none;
|
||||
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,15 +273,19 @@ 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;
|
||||
user-select: none;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
.cat-name {
|
||||
@@ -128,14 +298,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);
|
||||
@@ -149,5 +320,124 @@ 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: default;
|
||||
|
||||
.pill-arrow {
|
||||
font-size: 10px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
.lvl3-tags-wrap {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px 14px;
|
||||
|
||||
.lvl3-tag {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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>
|
||||
|
||||
@@ -462,11 +462,11 @@ const customStyle = computed(() => {
|
||||
flex: 1;
|
||||
|
||||
.goods-name {
|
||||
font-size: 12px;
|
||||
font-size: 14px;
|
||||
color: #1e293b;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
height: 34px;
|
||||
height: 40px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
<div class="title-left">
|
||||
<span class="main-title">{{ options.title }}</span>
|
||||
<span class="sub-title" v-if="options.subtitle">{{ options.subtitle }}</span>
|
||||
<span class="category-tag-badge" v-if="options.dataSource === 'category' && (options.categoryName || options.categoryId)">
|
||||
🏷️ {{ options.categoryName || `分类: ${options.categoryId}` }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="more-link" v-if="options.showMoreLink !== false">
|
||||
{{ options.moreText || '查看更多' }} >
|
||||
@@ -32,11 +35,6 @@
|
||||
{{ g.goodsName }}
|
||||
</div>
|
||||
|
||||
<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>
|
||||
@@ -93,6 +91,10 @@ 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 isGoodsCard = computed(() => {
|
||||
return props.component?.type === 'goodsCard' || props.options?.type === 'goodsCard';
|
||||
});
|
||||
|
||||
const actualColumns = computed(() => {
|
||||
// 商品列表整行默认 6 列
|
||||
let baseCols = Number(props.options?.columns) || 6;
|
||||
@@ -126,7 +128,7 @@ const customStyle = computed(() => {
|
||||
});
|
||||
|
||||
const sampleGoods = computed(() => {
|
||||
if (Array.isArray(props.options.goods) && props.options.goods.length > 0) {
|
||||
if (props.options.dataSource === 'manual' && Array.isArray(props.options.goods) && props.options.goods.length > 0) {
|
||||
return props.options.goods;
|
||||
}
|
||||
const count = props.options.goodsCount || (actualColumns.value * 2) || 12;
|
||||
@@ -153,26 +155,30 @@ const sampleGoods = computed(() => {
|
||||
gap: 10px;
|
||||
|
||||
.main-title {
|
||||
font-size: 20px;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
font-size: 13px;
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.category-tag-badge {
|
||||
font-size: 11px;
|
||||
background: #eff6ff;
|
||||
border: 1px solid #bfdbfe;
|
||||
color: #2563eb;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.more-link {
|
||||
font-size: 13px;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #e11d48;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,22 +193,6 @@ const sampleGoods = computed(() => {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: all 0.25s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.08);
|
||||
border-color: #e2e8f0;
|
||||
|
||||
.thumb-img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.add-cart-circle {
|
||||
background: #e11d48;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
.card-thumb-wrap {
|
||||
position: relative;
|
||||
@@ -217,7 +207,6 @@ const sampleGoods = computed(() => {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.self-tag {
|
||||
@@ -239,10 +228,11 @@ const sampleGoods = computed(() => {
|
||||
flex: 1;
|
||||
|
||||
.goods-name {
|
||||
font-size: 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #1e293b;
|
||||
line-height: 1.4;
|
||||
height: 34px;
|
||||
height: 40px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
@@ -302,12 +292,10 @@ const sampleGoods = computed(() => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -249,11 +249,11 @@ const customStyle = computed(() => {
|
||||
flex: 1;
|
||||
|
||||
.cell-name {
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
color: #334155;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
height: 36px;
|
||||
height: 40px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
|
||||
@@ -122,7 +122,7 @@ const posterTags = computed(() => {
|
||||
});
|
||||
|
||||
const posterImg = computed(() => {
|
||||
return props.options?.posterImg || 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/banner-floor/floor_banner_1.png';
|
||||
return props.options?.bannerImg || props.options?.posterImg || 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/banner-floor/floor_banner_1.png';
|
||||
});
|
||||
|
||||
const showPoster = computed(() => {
|
||||
@@ -444,11 +444,11 @@ const customStyle = computed(() => {
|
||||
flex: 1;
|
||||
|
||||
.goods-name-line {
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
color: #1e293b;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
height: 36px;
|
||||
height: 40px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
|
||||
@@ -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>
|
||||
@@ -146,161 +152,146 @@ const customStyle = computed(() => {
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
user-select: none;
|
||||
background: #ffffff;
|
||||
border: 1px solid #eef2f6;
|
||||
gap: 12px;
|
||||
border-radius: 8px;
|
||||
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-title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 13px;
|
||||
color: #334155;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
min-width: 0;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.notice-item-time {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 右侧更多 */
|
||||
.notice-more-action {
|
||||
flex-shrink: 0;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
|
||||
&: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-title {
|
||||
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-title {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
.more-text {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,40 @@
|
||||
</div>
|
||||
<div class="big-card-info">
|
||||
<div class="big-title">{{ featuredGoods.goodsName }}</div>
|
||||
<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>
|
||||
@@ -86,8 +119,7 @@
|
||||
<div class="card-body">
|
||||
<div class="goods-title">{{ item.goodsName }}</div>
|
||||
<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">
|
||||
@@ -312,10 +344,92 @@ const customStyle = computed(() => {
|
||||
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;
|
||||
@@ -438,10 +552,10 @@ const customStyle = computed(() => {
|
||||
flex: 1;
|
||||
|
||||
.goods-title {
|
||||
font-size: 12px;
|
||||
font-size: 14px;
|
||||
color: #1e293b;
|
||||
line-height: 1.4;
|
||||
height: 34px;
|
||||
height: 40px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
|
||||
@@ -7,6 +7,12 @@
|
||||
]"
|
||||
>
|
||||
<div class="title-content">
|
||||
<img
|
||||
v-if="options.titleImg || options.icon"
|
||||
:src="options.titleImg || options.icon"
|
||||
class="title-custom-icon"
|
||||
alt="icon"
|
||||
/>
|
||||
<span class="main-t">{{ options.mainTitle || '楼层主标题' }}</span>
|
||||
<span class="sub-t" v-if="options.subtitle">{{ options.subtitle }}</span>
|
||||
</div>
|
||||
@@ -53,11 +59,18 @@ const isCompact = computed(() => props.componentStyle?.widthRatio === '33.33%');
|
||||
|
||||
.title-content {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px 10px;
|
||||
gap: 8px 10px;
|
||||
min-width: 0;
|
||||
|
||||
.title-custom-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
object-fit: contain;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.main-t {
|
||||
font-size: 22px;
|
||||
font-weight: 800;
|
||||
@@ -84,6 +97,10 @@ const isCompact = computed(() => props.componentStyle?.widthRatio === '33.33%');
|
||||
|
||||
&.is-compact {
|
||||
.title-content {
|
||||
.title-custom-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
.main-t {
|
||||
font-size: 18px;
|
||||
}
|
||||
@@ -99,37 +116,31 @@ const isCompact = computed(() => props.componentStyle?.widthRatio === '33.33%');
|
||||
&.is-mini {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
gap: 6px;
|
||||
|
||||
&.align-center {
|
||||
align-items: center;
|
||||
.more-link {
|
||||
position: static;
|
||||
margin-top: 2px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.title-content {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 2px;
|
||||
|
||||
.title-custom-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
.main-t {
|
||||
font-size: 16px;
|
||||
font-size: 15px;
|
||||
}
|
||||
.sub-t {
|
||||
font-size: 12px;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
&.align-center .title-content {
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.more-link {
|
||||
font-size: 12px;
|
||||
margin-top: 2px;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,13 +72,15 @@ const customStyle = computed(() => {
|
||||
}
|
||||
|
||||
.bar-inner {
|
||||
width: 100%;
|
||||
width: 1200px;
|
||||
max-width: 100%;
|
||||
padding: 0 24px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
|
||||
.bar-left {
|
||||
display: flex;
|
||||
@@ -98,7 +100,7 @@ const customStyle = computed(() => {
|
||||
white-space: nowrap;
|
||||
|
||||
.auth-btn {
|
||||
color: #ff6a00;
|
||||
color: #409EFF;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
@@ -120,7 +122,7 @@ const customStyle = computed(() => {
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
color: #ff6a00;
|
||||
color: #409EFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,65 +30,29 @@
|
||||
|
||||
<!-- 3. 指标数据卡 (购物车 / 优惠券 / 订单) -->
|
||||
<div class="metrics-cards-row">
|
||||
<div class="metric-card">
|
||||
<div class="metric-card" v-if="options.showCart !== false">
|
||||
<div class="metric-val">0</div>
|
||||
<div class="metric-label">{{ options.cartText || '购物车' }}</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-card" v-if="options.showCoupons !== false">
|
||||
<div class="metric-val">0</div>
|
||||
<div class="metric-label">{{ options.couponText || '优惠券' }}</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-card" v-if="options.showOrders !== false">
|
||||
<div class="metric-val">0</div>
|
||||
<div class="metric-label">{{ options.orderText || '订单' }}</div>
|
||||
</div>
|
||||
</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>
|
||||
<span class="news-more">查看全部 ></span>
|
||||
</div>
|
||||
<div class="news-list">
|
||||
<div class="news-item" title="精选优质好物 官方正品保障">
|
||||
<span class="dot">·</span>
|
||||
<span class="news-text">精选优质好物 官方正品保障</span>
|
||||
</div>
|
||||
<div class="news-item" title="双11狂欢 跨店每满300减50">
|
||||
<span class="dot">·</span>
|
||||
<span class="news-text">双11狂欢 跨店每满300减50</span>
|
||||
</div>
|
||||
<div class="news-item" title="新人专享好礼 注册立领大礼包">
|
||||
<span class="dot">·</span>
|
||||
<span class="news-text">新人专享好礼 注册立领大礼包</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 6. 快捷便民服务入口(底部图标排) -->
|
||||
<!-- 4. 快捷便民服务入口(底部图标排) -->
|
||||
<div class="quick-service-row">
|
||||
<div class="service-item">
|
||||
<el-icon class="service-icon"><Star /></el-icon>
|
||||
<span class="service-name">收藏</span>
|
||||
</div>
|
||||
<div class="service-item">
|
||||
<el-icon class="service-icon"><Clock /></el-icon>
|
||||
<span class="service-name">足迹</span>
|
||||
</div>
|
||||
<div class="service-item">
|
||||
<el-icon class="service-icon"><Present /></el-icon>
|
||||
<span class="service-name">红包</span>
|
||||
</div>
|
||||
<div class="service-item">
|
||||
<el-icon class="service-icon"><Service /></el-icon>
|
||||
<span class="service-name">客服</span>
|
||||
<div
|
||||
v-for="(srv, sIdx) in displayServices"
|
||||
:key="sIdx"
|
||||
class="service-item"
|
||||
>
|
||||
<el-icon class="service-icon"><component :is="getServiceIcon(srv.icon)" /></el-icon>
|
||||
<span class="service-name">{{ srv.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -106,16 +70,48 @@ const props = defineProps({
|
||||
componentStyle: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
rowStyle: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
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>
|
||||
@@ -124,18 +120,19 @@ const customStyle = computed(() => {
|
||||
.tpl-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;
|
||||
user-select: none;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
overflow: hidden;
|
||||
|
||||
.user-main-info {
|
||||
display: flex;
|
||||
@@ -143,8 +140,8 @@ const customStyle = computed(() => {
|
||||
gap: 8px;
|
||||
|
||||
.avatar-box {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
@@ -175,7 +172,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;
|
||||
@@ -208,38 +205,28 @@ 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;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&.login-btn {
|
||||
background: #409EFF;
|
||||
color: #ffffff;
|
||||
box-shadow: 0 2px 6px rgba(255, 96, 0, 0.25);
|
||||
|
||||
&:hover {
|
||||
background: #66b1ff;
|
||||
}
|
||||
box-shadow: 0 2px 6px rgba(64, 158, 255, 0.25);
|
||||
}
|
||||
|
||||
&.register-btn {
|
||||
background: #ffffff;
|
||||
color: #409EFF;
|
||||
border: 1px solid #409EFF;
|
||||
|
||||
&:hover {
|
||||
background: #fff7ed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -248,21 +235,21 @@ 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;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.metric-val {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
line-height: 1.1;
|
||||
color: #409EFF;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
@@ -270,128 +257,30 @@ 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;
|
||||
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;
|
||||
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;
|
||||
cursor: pointer;
|
||||
color: #64748b;
|
||||
transition: color 0.15s;
|
||||
|
||||
.service-icon {
|
||||
font-size: 14px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.service-name {
|
||||
font-size: 10px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #409EFF;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,28 @@
|
||||
<template>
|
||||
<div class="tpl-video" :style="customStyle">
|
||||
<div class="video-preview-box">
|
||||
<img :src="options.coverUrl || 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/video/video_cover.png'" alt="cover" class="cover-img" />
|
||||
<div class="play-btn">▶</div>
|
||||
<video
|
||||
v-if="options.videoUrl"
|
||||
:src="options.videoUrl"
|
||||
:poster="options.coverUrl"
|
||||
:controls="options.controls !== false"
|
||||
muted
|
||||
class="video-el"
|
||||
/>
|
||||
<template v-else>
|
||||
<img
|
||||
:src="options.coverUrl || 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/video/video_cover.png'"
|
||||
alt="cover"
|
||||
class="cover-img"
|
||||
/>
|
||||
<div class="play-btn">▶</div>
|
||||
</template>
|
||||
|
||||
<!-- 链接徽标(装修态提示) -->
|
||||
<div class="video-link-badge" v-if="options.url" :title="options.url">
|
||||
<span class="badge-icon">🔗</span>
|
||||
<span class="badge-text">{{ options.url }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -24,7 +44,7 @@ const props = defineProps({
|
||||
const customStyle = computed(() => {
|
||||
const s = props.componentStyle || {};
|
||||
return {
|
||||
height: `${props.options?.height || 360}px`,
|
||||
height: `${props.options?.height || 450}px`,
|
||||
borderRadius: Array.isArray(s.radius)
|
||||
? `${s.radius[0]}px ${s.radius[1]}px ${s.radius[2]}px ${s.radius[3]}px`
|
||||
: '8px'
|
||||
@@ -48,30 +68,69 @@ const customStyle = computed(() => {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.video-el {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.cover-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.play-btn {
|
||||
position: absolute;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
border-radius: 50%;
|
||||
background: rgba(225, 29, 72, 0.9);
|
||||
color: #ffffff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
font-size: 22px;
|
||||
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35);
|
||||
transition: transform 0.2s ease;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.08);
|
||||
}
|
||||
}
|
||||
|
||||
.video-link-badge {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
max-width: 60%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 4px 10px;
|
||||
background: rgba(15, 23, 42, 0.75);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 20px;
|
||||
backdrop-filter: blur(4px);
|
||||
font-size: 11px;
|
||||
color: #e2e8f0;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
|
||||
.badge-icon {
|
||||
font-size: 11px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.badge-text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user