feat(buyer,manager): 楼层装修组件体系与平台端页面优化
- buyer:楼层装修组件(RowWrapper、组件库、预设、注册表)及首页/商品列表适配 - manager:PC端楼层装修页面、店铺设置、OSS素材管理等页面样式优化
40
buyer/pnpm-lock.yaml
generated
@@ -36,6 +36,9 @@ importers:
|
||||
nprogress:
|
||||
specifier: ^0.2.0
|
||||
version: 0.2.0
|
||||
pinia:
|
||||
specifier: ^4.0.3
|
||||
version: 4.1.0(@vue/devtools-api@6.6.4)(vue@3.5.43)
|
||||
psl:
|
||||
specifier: ^1.8.0
|
||||
version: 1.15.0
|
||||
@@ -60,9 +63,6 @@ importers:
|
||||
vue-router:
|
||||
specifier: ^4.6.4
|
||||
version: 4.6.4(vue@3.5.43)
|
||||
vuex:
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0(vue@3.5.43)
|
||||
devDependencies:
|
||||
'@vue/cli-plugin-babel':
|
||||
specifier: ^5.0.9
|
||||
@@ -91,6 +91,9 @@ importers:
|
||||
less-loader:
|
||||
specifier: ^11.1.4
|
||||
version: 11.1.4(less@4.9.1)(webpack@5.111.0(postcss@8.5.28))
|
||||
vue-eslint-parser:
|
||||
specifier: ^10.4.1
|
||||
version: 10.4.1(eslint@9.39.4)
|
||||
webpack:
|
||||
specifier: ^5.95.0
|
||||
version: 5.111.0(cssnano@5.1.15(postcss@8.5.28))(postcss@8.5.28)
|
||||
@@ -2968,6 +2971,9 @@ packages:
|
||||
normalize-wheel-es@1.2.0:
|
||||
resolution: {integrity: sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==}
|
||||
|
||||
nostics@1.3.0:
|
||||
resolution: {integrity: sha512-wzRsrJwNlFbqIRQt8aCeL2WE8USip/uuvGSOZV1D02CxX8ABTMbw8IywKjH7yWHETMXbWJI4OBpihg9rmz2Vyg==}
|
||||
|
||||
npm-run-path@2.0.2:
|
||||
resolution: {integrity: sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -3135,6 +3141,16 @@ packages:
|
||||
resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
pinia@4.1.0:
|
||||
resolution: {integrity: sha512-+5/HMuVjYQfKlCD5xQ7zfXn1xUzYFEa7pd0DQK5SaMfZCaFRTHlaH/c02N1FoGOdlzSiwf5EOPgJx//1QDw6gw==}
|
||||
peerDependencies:
|
||||
'@vue/devtools-api': ^8.2.1
|
||||
typescript: '>=5.6.0'
|
||||
vue: ^3.5.11
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
pkg-dir@4.2.0:
|
||||
resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -4006,11 +4022,6 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
vuex@4.1.0:
|
||||
resolution: {integrity: sha512-hmV6UerDrPcgbSy9ORAtNXDr9M4wlNP4pEFKye4ujJF8oqgFFuxDCdOLS3eNoRTtq5O3hoBDh9Doj1bQMYHRbQ==}
|
||||
peerDependencies:
|
||||
vue: ^3.2.0
|
||||
|
||||
watchpack@2.5.2:
|
||||
resolution: {integrity: sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
@@ -7374,6 +7385,8 @@ snapshots:
|
||||
|
||||
normalize-wheel-es@1.2.0: {}
|
||||
|
||||
nostics@1.3.0: {}
|
||||
|
||||
npm-run-path@2.0.2:
|
||||
dependencies:
|
||||
path-key: 2.0.1
|
||||
@@ -7533,6 +7546,12 @@ snapshots:
|
||||
picomatch@4.0.7:
|
||||
optional: true
|
||||
|
||||
pinia@4.1.0(@vue/devtools-api@6.6.4)(vue@3.5.43):
|
||||
dependencies:
|
||||
'@vue/devtools-api': 6.6.4
|
||||
nostics: 1.3.0
|
||||
vue: 3.5.43
|
||||
|
||||
pkg-dir@4.2.0:
|
||||
dependencies:
|
||||
find-up: 4.1.0
|
||||
@@ -8449,11 +8468,6 @@ snapshots:
|
||||
'@vue/server-renderer': 3.5.43
|
||||
'@vue/shared': 3.5.43
|
||||
|
||||
vuex@4.1.0(vue@3.5.43):
|
||||
dependencies:
|
||||
'@vue/devtools-api': 6.6.4
|
||||
vue: 3.5.43
|
||||
|
||||
watchpack@2.5.2:
|
||||
dependencies:
|
||||
graceful-fs: 4.2.11
|
||||
|
||||
BIN
buyer/public/static/banner-floor/floor_banner_1.png
Normal file
|
After Width: | Height: | Size: 452 KiB |
BIN
buyer/public/static/brand-wall/brand_1.png
Normal file
|
After Width: | Height: | Size: 358 KiB |
BIN
buyer/public/static/brand-wall/brand_2.png
Normal file
|
After Width: | Height: | Size: 356 KiB |
BIN
buyer/public/static/brand-wall/brand_3.png
Normal file
|
After Width: | Height: | Size: 366 KiB |
BIN
buyer/public/static/brand-wall/brand_4.png
Normal file
|
After Width: | Height: | Size: 364 KiB |
BIN
buyer/public/static/carousel/banner_1.png
Normal file
|
After Width: | Height: | Size: 523 KiB |
BIN
buyer/public/static/carousel/banner_2.png
Normal file
|
After Width: | Height: | Size: 578 KiB |
BIN
buyer/public/static/carousel/banner_3.png
Normal file
|
After Width: | Height: | Size: 985 KiB |
BIN
buyer/public/static/carousel/banner_4.png
Normal file
|
After Width: | Height: | Size: 798 KiB |
BIN
buyer/public/static/category-nav/cat_1.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
buyer/public/static/category-nav/cat_10.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
buyer/public/static/category-nav/cat_2.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
buyer/public/static/category-nav/cat_3.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
buyer/public/static/category-nav/cat_4.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
buyer/public/static/category-nav/cat_5.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
buyer/public/static/category-nav/cat_6.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
buyer/public/static/category-nav/cat_7.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
buyer/public/static/category-nav/cat_8.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
buyer/public/static/category-nav/cat_9.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
buyer/public/static/channel-grid/channel_1.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
buyer/public/static/channel-grid/channel_2.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
buyer/public/static/channel-grid/channel_3.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
buyer/public/static/channel-grid/channel_4.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
buyer/public/static/goods/goods_1.png
Normal file
|
After Width: | Height: | Size: 280 KiB |
BIN
buyer/public/static/goods/goods_2.png
Normal file
|
After Width: | Height: | Size: 662 KiB |
BIN
buyer/public/static/goods/goods_3.png
Normal file
|
After Width: | Height: | Size: 613 KiB |
BIN
buyer/public/static/goods/goods_4.png
Normal file
|
After Width: | Height: | Size: 714 KiB |
BIN
buyer/public/static/goods/goods_5.png
Normal file
|
After Width: | Height: | Size: 577 KiB |
BIN
buyer/public/static/goods/goods_6.png
Normal file
|
After Width: | Height: | Size: 362 KiB |
BIN
buyer/public/static/goods/goods_7.png
Normal file
|
After Width: | Height: | Size: 744 KiB |
BIN
buyer/public/static/goods/goods_8.png
Normal file
|
After Width: | Height: | Size: 720 KiB |
BIN
buyer/public/static/hot-zone/hotzone_default.png
Normal file
|
After Width: | Height: | Size: 592 KiB |
BIN
buyer/public/static/logo/logo.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
buyer/public/static/promo-group/promo_1.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
buyer/public/static/promo-group/promo_2.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
buyer/public/static/promo-group/promo_3.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
buyer/public/static/video/video_cover.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
@@ -1,6 +1,16 @@
|
||||
<template>
|
||||
<div class="model-form">
|
||||
<div class="model-content">
|
||||
<div class="model-form" :style="pageContainerStyle">
|
||||
<!-- Version 2: 新万米行模型渲染 -->
|
||||
<div v-if="isVersion2" class="v2-rows-container">
|
||||
<row-wrapper
|
||||
v-for="row in v2Rows"
|
||||
:key="row.id"
|
||||
:row="row"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 旧版降级渲染 -->
|
||||
<div v-else class="model-content">
|
||||
<model-form-item
|
||||
v-for="(element, index) in renderList"
|
||||
:key="element.key"
|
||||
@@ -11,14 +21,34 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import ModelFormItem from './ModelFormItem.vue';
|
||||
import RowWrapper from './RowWrapper.vue';
|
||||
|
||||
defineOptions({ name: 'modelForm' });
|
||||
|
||||
const props = defineProps(['data']);
|
||||
|
||||
// 是否为 v2 行模型数据
|
||||
const isVersion2 = computed(() => {
|
||||
return (props.data?.version === 2 || props.data?.version === '2.0') && Array.isArray(props.data?.list);
|
||||
});
|
||||
|
||||
const v2Rows = computed(() => {
|
||||
return props.data?.list || [];
|
||||
});
|
||||
|
||||
const pageContainerStyle = computed(() => {
|
||||
if (isVersion2.value && props.data?.pageStyle?.backgroundColor) {
|
||||
return {
|
||||
backgroundColor: props.data.pageStyle.backgroundColor
|
||||
};
|
||||
}
|
||||
return {};
|
||||
});
|
||||
|
||||
const renderList = computed(() => {
|
||||
const list = props.data?.list || [];
|
||||
return list.filter(
|
||||
@@ -27,21 +57,26 @@ const renderList = computed(() => {
|
||||
});
|
||||
|
||||
function isBuiltinModule(type) {
|
||||
// 顶部广告、快捷导航由 Index 页单独渲染
|
||||
return type === 'topAdvert' || type === 'navBar';
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.model-form {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.v2-rows-container {
|
||||
width: 100%;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.model-content {
|
||||
width: 100%;
|
||||
min-height: 1200px;
|
||||
box-sizing: border-box;
|
||||
/* 勿设 overflow-x: hidden,会裁切 discountAdvert 的 margin-left: -100px 左溢背景 */
|
||||
overflow-x: visible;
|
||||
}
|
||||
</style>
|
||||
|
||||
325
buyer/src/components/indexDecorate/RowWrapper.vue
Normal file
@@ -0,0 +1,325 @@
|
||||
<template>
|
||||
|
||||
<div class="row-wrapper" :style="rowWrapperStyle">
|
||||
|
||||
<div class="row-inner-flex" :style="rowInnerFlexStyle">
|
||||
|
||||
<div
|
||||
|
||||
v-for="comp in row.components"
|
||||
|
||||
:key="comp.id"
|
||||
|
||||
class="component-cell"
|
||||
|
||||
:style="getComponentCellStyle(comp.componentStyle, row.rowStyle, row.components ? row.components.length : 1)"
|
||||
|
||||
>
|
||||
|
||||
<component
|
||||
|
||||
:is="resolveComponent(comp.type)"
|
||||
|
||||
v-if="resolveComponent(comp.type)"
|
||||
:component="comp"
|
||||
|
||||
:options="comp.options || {}"
|
||||
|
||||
:component-style="comp.componentStyle || {}"
|
||||
|
||||
/>
|
||||
|
||||
<div v-else class="unknown-component-placeholder">
|
||||
|
||||
未知组件: {{ comp.type }}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<script setup>
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { resolveComponent } from './registry.js';
|
||||
|
||||
|
||||
|
||||
const props = defineProps({
|
||||
|
||||
row: {
|
||||
|
||||
type: Object,
|
||||
|
||||
required: true
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
const rowWrapperStyle = computed(() => {
|
||||
const s = props.row.rowStyle || {};
|
||||
const style = {};
|
||||
|
||||
// 宽度 (width)
|
||||
if (s.width !== undefined && s.width !== '') {
|
||||
const w = String(s.width).trim();
|
||||
if (w && w !== '100%') {
|
||||
const num = parseInt(w, 10);
|
||||
if (!isNaN(num) && num >= 1920 && !w.endsWith('%')) {
|
||||
style.width = '100%';
|
||||
} else {
|
||||
style.width = /^\d+$/.test(w) ? `${w}px` : w;
|
||||
style.maxWidth = '100%';
|
||||
}
|
||||
} else {
|
||||
style.width = '100%';
|
||||
}
|
||||
} else {
|
||||
style.width = '100%';
|
||||
}
|
||||
|
||||
// 高度 (height)
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 外边距 (margin)
|
||||
const isNarrow = style.width !== '100%';
|
||||
if (Array.isArray(s.margin)) {
|
||||
style.marginTop = `${s.margin[0] || 0}px`;
|
||||
style.marginRight = s.margin[1] ? `${s.margin[1]}px` : (isNarrow ? 'auto' : '0px');
|
||||
style.marginBottom = `${s.margin[2] || 0}px`;
|
||||
style.marginLeft = s.margin[3] ? `${s.margin[3]}px` : (isNarrow ? 'auto' : '0px');
|
||||
} else if (isNarrow) {
|
||||
style.marginLeft = 'auto';
|
||||
style.marginRight = 'auto';
|
||||
}
|
||||
|
||||
// 内边距 (padding)
|
||||
if (Array.isArray(s.padding)) {
|
||||
style.paddingTop = `${s.padding[0] || 0}px`;
|
||||
style.paddingRight = `${s.padding[1] || 0}px`;
|
||||
style.paddingBottom = `${s.padding[2] || 0}px`;
|
||||
style.paddingLeft = `${s.padding[3] || 0}px`;
|
||||
}
|
||||
|
||||
// 圆角 (radius)
|
||||
if (Array.isArray(s.radius)) {
|
||||
style.borderTopLeftRadius = `${s.radius[0] || 0}px`;
|
||||
style.borderTopRightRadius = `${s.radius[1] || 0}px`;
|
||||
style.borderBottomRightRadius = `${s.radius[2] || 0}px`;
|
||||
style.borderBottomLeftRadius = `${s.radius[3] || 0}px`;
|
||||
}
|
||||
|
||||
// 行背景由各组件配置统一管理
|
||||
|
||||
return style;
|
||||
});
|
||||
|
||||
|
||||
|
||||
const rowInnerFlexStyle = computed(() => {
|
||||
|
||||
const s = props.row.rowStyle || {};
|
||||
|
||||
return {
|
||||
|
||||
display: 'flex',
|
||||
|
||||
flexDirection: 'row',
|
||||
|
||||
flexWrap: 'nowrap',
|
||||
|
||||
alignItems: s.alignItems || 'stretch',
|
||||
|
||||
justifyContent: s.justifyContent || 'flex-start',
|
||||
|
||||
width: '100%',
|
||||
|
||||
gap: `${s.gap || 0}px`
|
||||
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
function getComponentCellStyle(style, rowStyle = {}, totalComps = 1) {
|
||||
|
||||
if (!style) return { flex: '1 1 0%' };
|
||||
|
||||
const s = {};
|
||||
const gap = Number(rowStyle?.gap) || 0;
|
||||
const totalGap = (totalComps > 1 && gap > 0) ? gap * (totalComps - 1) : 0;
|
||||
|
||||
if (style.widthRatio && style.widthRatio !== 'auto') {
|
||||
let w = style.widthRatio;
|
||||
if (w === '16.67%' || w === '16.6%' || w === '16.7%') {
|
||||
w = totalGap > 0 ? `calc((100% - ${totalGap}px) / 6)` : 'calc(100% / 6)';
|
||||
} else if (w === '66.67%' || w === '66.6%' || w === '66.7%') {
|
||||
w = totalGap > 0 ? `calc((100% - ${totalGap}px) * 2 / 3)` : 'calc(100% * 2 / 3)';
|
||||
} else if (w === '33.33%' || w === '33.3%') {
|
||||
w = totalGap > 0 ? `calc((100% - ${totalGap}px) / 3)` : 'calc(100% / 3)';
|
||||
} else if (w === '50%') {
|
||||
w = totalGap > 0 ? `calc((100% - ${totalGap}px) / 2)` : '50%';
|
||||
}
|
||||
s.flex = `0 0 ${w}`;
|
||||
|
||||
s.width = w;
|
||||
|
||||
s.maxWidth = w;
|
||||
|
||||
s.minWidth = '0';
|
||||
|
||||
} else {
|
||||
|
||||
s.flex = '1 1 0%';
|
||||
|
||||
s.minWidth = '0';
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (Array.isArray(style.padding)) {
|
||||
|
||||
s.paddingTop = `${style.padding[0] || 0}px`;
|
||||
|
||||
s.paddingRight = `${style.padding[1] || 0}px`;
|
||||
|
||||
s.paddingBottom = `${style.padding[2] || 0}px`;
|
||||
|
||||
s.paddingLeft = `${style.padding[3] || 0}px`;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (Array.isArray(style.radius)) {
|
||||
|
||||
s.borderTopLeftRadius = `${style.radius[0] || 0}px`;
|
||||
|
||||
s.borderTopRightRadius = `${style.radius[1] || 0}px`;
|
||||
|
||||
s.borderBottomRightRadius = `${style.radius[2] || 0}px`;
|
||||
|
||||
s.borderBottomLeftRadius = `${style.radius[3] || 0}px`;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (Array.isArray(style.margin)) {
|
||||
|
||||
s.marginTop = `${style.margin[0] || 0}px`;
|
||||
|
||||
s.marginRight = `${style.margin[1] || 0}px`;
|
||||
|
||||
s.marginBottom = `${style.margin[2] || 0}px`;
|
||||
|
||||
s.marginLeft = `${style.margin[3] || 0}px`;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (style.backgroundColor && style.backgroundColor !== 'transparent') {
|
||||
let bg = style.backgroundColor;
|
||||
if (/^rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*0\s*\)$/.test(bg.trim())) {
|
||||
bg = bg.trim().replace(/,\s*0\s*\)$/, ', 1)');
|
||||
}
|
||||
s.backgroundColor = bg;
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (style.backgroundImage) {
|
||||
|
||||
s.backgroundImage = `url(${style.backgroundImage})`;
|
||||
|
||||
s.backgroundSize = 'cover';
|
||||
|
||||
s.backgroundRepeat = 'no-repeat';
|
||||
|
||||
s.backgroundPosition = 'center';
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
return s;
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.row-wrapper {
|
||||
|
||||
box-sizing: border-box;
|
||||
|
||||
|
||||
|
||||
.row-inner-flex {
|
||||
|
||||
box-sizing: border-box;
|
||||
|
||||
|
||||
|
||||
.component-cell {
|
||||
|
||||
box-sizing: border-box;
|
||||
|
||||
min-width: 0;
|
||||
|
||||
display: flex;
|
||||
|
||||
flex-direction: column;
|
||||
|
||||
height: 100%;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.unknown-component-placeholder {
|
||||
|
||||
padding: 20px;
|
||||
|
||||
background: #fef2f2;
|
||||
|
||||
border: 1px dashed #f87171;
|
||||
|
||||
color: #dc2626;
|
||||
|
||||
text-align: center;
|
||||
|
||||
font-size: 13px;
|
||||
|
||||
border-radius: 6px;
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
202
buyer/src/components/indexDecorate/components/ArticleContent.vue
Normal file
@@ -0,0 +1,202 @@
|
||||
<template>
|
||||
<div class="decor-article-content" :style="customStyle">
|
||||
<!-- 文章顶部标题区 -->
|
||||
<div class="article-header" v-if="options.showTitle !== false && options.title">
|
||||
<h2 class="article-main-title">{{ options.title }}</h2>
|
||||
<p class="article-sub-title" v-if="options.subtitle">{{ options.subtitle }}</p>
|
||||
|
||||
<div class="article-meta-bar" v-if="options.showMeta !== false">
|
||||
<span class="meta-tag">{{ options.tag || '品牌专栏' }}</span>
|
||||
<span class="meta-item author" v-if="options.author">作者:{{ options.author }}</span>
|
||||
<span class="meta-item date" v-if="options.date">发布于 {{ options.date }}</span>
|
||||
<span class="meta-item views">阅读 2.4k+</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文章正文排版区 (Rich Text Article Body) -->
|
||||
<div
|
||||
class="article-body-typography"
|
||||
:class="[`align-${options.textAlign || 'left'}`]"
|
||||
:style="{ fontSize: `${options.fontSize || 15}px` }"
|
||||
v-html="renderedContent"
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
componentStyle: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const defaultArticleHtml = `
|
||||
<h3>关于我们 · 甄选美好生活</h3>
|
||||
<p>我们始终坚信,一件好的产品不仅能改善日常生活的品质,更传递着对美好生活的执着追求。自创立以来,我们严选全球顶尖供应链,深耕每一道生产工艺与质检环节,力求将兼具美学价值与极致实用性的商品送达每一位用户手中。</p>
|
||||
<blockquote>品质是我们的信仰,体验是我们的底线。从选材到交付,每一步都经得起时光检验。</blockquote>
|
||||
<h4>四大核心服务保障</h4>
|
||||
<ul>
|
||||
<li><strong>100% 官方正品</strong>:源头品牌直采,全链路防伪溯源,假一赔十保障。</li>
|
||||
<li><strong>极速顺丰冷链</strong>:全国多仓联动发货,核心城市支持当日达/次日达。</li>
|
||||
<li><strong>7天无理由退换</strong>:专业客服团队 7×24 小时贴心守护,售后无忧。</li>
|
||||
<li><strong>会员专享权益</strong>:尊享积分抵现、生日特权礼及专属品鉴沙龙。</li>
|
||||
</ul>
|
||||
<p>感谢一路相伴的每一位朋友,未来我们将持续探索更多优质生活场景,与您共同见证品质生活的美好绽放。</p>
|
||||
`.trim();
|
||||
|
||||
const renderedContent = computed(() => {
|
||||
return props.options?.content || defaultArticleHtml;
|
||||
});
|
||||
|
||||
const customStyle = computed(() => {
|
||||
const s = props.componentStyle || {};
|
||||
return {
|
||||
backgroundColor: s.backgroundColor || '#ffffff',
|
||||
padding: Array.isArray(s.padding)
|
||||
? `${s.padding[0]}px ${s.padding[1]}px ${s.padding[2]}px ${s.padding[3]}px`
|
||||
: '24px 28px',
|
||||
borderRadius: Array.isArray(s.radius)
|
||||
? `${s.radius[0]}px ${s.radius[1]}px ${s.radius[2]}px ${s.radius[3]}px`
|
||||
: '12px'
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.decor-article-content {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
.article-header {
|
||||
padding-bottom: 16px;
|
||||
margin-bottom: 18px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
|
||||
.article-main-title {
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
color: #0f172a;
|
||||
line-height: 1.35;
|
||||
margin: 0 0 8px 0;
|
||||
letter-spacing: -0.3px;
|
||||
}
|
||||
|
||||
.article-sub-title {
|
||||
font-size: 14px;
|
||||
color: #64748b;
|
||||
line-height: 1.5;
|
||||
margin: 0 0 12px 0;
|
||||
}
|
||||
|
||||
.article-meta-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
|
||||
.meta-tag {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
background: #eff6ff;
|
||||
color: #2563eb;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.article-body-typography {
|
||||
color: #334155;
|
||||
line-height: 1.8;
|
||||
letter-spacing: 0.2px;
|
||||
|
||||
&.align-center {
|
||||
text-align: center;
|
||||
}
|
||||
&.align-justify {
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
:deep(h2), :deep(h3), :deep(h4) {
|
||||
color: #0f172a;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
:deep(h2) {
|
||||
font-size: 20px;
|
||||
border-left: 4px solid #2563eb;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
:deep(h3) {
|
||||
font-size: 18px;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
:deep(h4) {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
:deep(p) {
|
||||
margin: 0 0 14px 0;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
:deep(blockquote) {
|
||||
margin: 16px 0;
|
||||
padding: 12px 18px;
|
||||
background: #f8fafc;
|
||||
border-left: 4px solid #3b82f6;
|
||||
border-radius: 0 8px 8px 0;
|
||||
color: #475569;
|
||||
font-style: italic;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
:deep(ul), :deep(ol) {
|
||||
margin: 12px 0 16px 20px;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
margin-bottom: 6px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(img) {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
margin: 16px auto;
|
||||
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
:deep(strong) {
|
||||
color: #0f172a;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
142
buyer/src/components/indexDecorate/components/BrandWall.vue
Normal file
@@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<div class="decor-brand-wall" :style="customStyle">
|
||||
<div
|
||||
class="brand-grid"
|
||||
:style="{
|
||||
gridTemplateColumns: `repeat(${actualColumns}, 1fr)`,
|
||||
gap: `${options.gap ?? 12}px`
|
||||
}"
|
||||
>
|
||||
<router-link
|
||||
v-for="(item, idx) in brandList"
|
||||
:key="idx"
|
||||
:to="item.url || '/goods'"
|
||||
class="brand-card"
|
||||
>
|
||||
<img
|
||||
v-if="item.img"
|
||||
:src="item.img"
|
||||
:alt="item.title || 'brand'"
|
||||
class="brand-img"
|
||||
/>
|
||||
<div v-else class="brand-placeholder">
|
||||
<span>{{ item.title || item.name || '品牌' }}</span>
|
||||
</div>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
componentStyle: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const brandList = computed(() => {
|
||||
if (Array.isArray(props.options.items) && props.options.items.length > 0) {
|
||||
return props.options.items;
|
||||
}
|
||||
return [
|
||||
{ title: '数码潮玩 · 爆款立减500', img: '/static/brand-wall/brand_1.png', url: '/goodsList?category=1' },
|
||||
{ title: '美妆奢宠 · 每满300减50', img: '/static/brand-wall/brand_2.png', url: '/goodsList?category=2' },
|
||||
{ title: '运动潮品 · 爆款低至5折', img: '/static/brand-wall/brand_3.png', url: '/goodsList?category=3' },
|
||||
{ title: '品质生活 · 限时第2件半价', img: '/static/brand-wall/brand_4.png', url: '/goodsList?category=4' }
|
||||
];
|
||||
});
|
||||
|
||||
const actualColumns = computed(() => {
|
||||
const baseCols = Number(props.options?.columns) || 4;
|
||||
const ratio = props.componentStyle?.widthRatio || '100%';
|
||||
if (ratio === '33.33%' || ratio === '33.3%') {
|
||||
return Math.min(baseCols, 2);
|
||||
}
|
||||
if (ratio === '50%') {
|
||||
return Math.min(baseCols, 2);
|
||||
}
|
||||
if (ratio === '66.67%' || ratio === '66.6%' || ratio === '66.7%') {
|
||||
return Math.min(baseCols, 3);
|
||||
}
|
||||
return baseCols;
|
||||
});
|
||||
|
||||
const customStyle = computed(() => {
|
||||
const s = props.componentStyle || {};
|
||||
return {
|
||||
backgroundColor: s.backgroundColor || 'transparent',
|
||||
padding: Array.isArray(s.padding)
|
||||
? `${s.padding[0]}px ${s.padding[1]}px ${s.padding[2]}px ${s.padding[3]}px`
|
||||
: '10px 0'
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.decor-brand-wall {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
.brand-grid {
|
||||
max-width: 1600px;
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
width: 100%;
|
||||
|
||||
.brand-card {
|
||||
background: #ffffff;
|
||||
border: 1px solid #f1f5f9;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
aspect-ratio: 2 / 1;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
min-height: 80px;
|
||||
max-height: 180px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-decoration: none;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
|
||||
transition: all 0.25s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.08);
|
||||
border-color: #e2e8f0;
|
||||
|
||||
.brand-img {
|
||||
transform: scale(1.03);
|
||||
}
|
||||
}
|
||||
|
||||
.brand-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
transition: transform 0.25s ease;
|
||||
}
|
||||
|
||||
.brand-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f8fafc;
|
||||
color: #64748b;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
193
buyer/src/components/indexDecorate/components/Carousel.vue
Normal file
@@ -0,0 +1,193 @@
|
||||
<template>
|
||||
<div
|
||||
class="decor-carousel"
|
||||
:class="[
|
||||
isMini ? 'is-mini' : (isCompact ? 'is-compact' : '')
|
||||
]"
|
||||
:style="customStyle"
|
||||
>
|
||||
<el-carousel
|
||||
:autoplay="options.autoPlay !== false"
|
||||
:interval="Number(options.interval) || 4000"
|
||||
:arrow="options.showArrow ? 'hover' : 'never'"
|
||||
:indicator-position="options.showIndicator !== false ? '' : 'none'"
|
||||
class="custom-carousel"
|
||||
>
|
||||
<el-carousel-item v-for="(slide, idx) in slidesList" :key="idx">
|
||||
<router-link :to="slide.url || '/'" class="slide-link">
|
||||
<img
|
||||
:src="slide.img"
|
||||
:alt="slide.title || 'banner'"
|
||||
class="slide-img"
|
||||
/>
|
||||
</router-link>
|
||||
</el-carousel-item>
|
||||
</el-carousel>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
componentStyle: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const isMini = computed(() => props.componentStyle?.widthRatio === '16.67%');
|
||||
const isCompact = computed(() => props.componentStyle?.widthRatio === '33.33%');
|
||||
|
||||
const slidesList = computed(() => {
|
||||
if (Array.isArray(props.options.list) && props.options.list.length > 0) {
|
||||
return props.options.list;
|
||||
}
|
||||
return [
|
||||
{ title: '出游囤好货', img: '/static/carousel/banner_1.png', url: '/goodsList?seckill=1' },
|
||||
{ title: '科技新品', img: '/static/carousel/banner_2.png', url: '/goodsList?category=1' },
|
||||
{ title: '原产地直采', img: '/static/carousel/banner_3.png', url: '/goodsList?category=2' },
|
||||
{ title: '大牌美妆', img: '/static/carousel/banner_4.png', url: '/goodsList?category=3' }
|
||||
];
|
||||
});
|
||||
|
||||
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 || 'cover';
|
||||
|
||||
const styleObj = {
|
||||
borderRadius: Array.isArray(s.radius)
|
||||
? `${s.radius[0]}px ${s.radius[1]}px ${s.radius[2]}px ${s.radius[3]}px`
|
||||
: '0px',
|
||||
'--carousel-fit': fit
|
||||
};
|
||||
|
||||
if (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';
|
||||
}
|
||||
}
|
||||
|
||||
return styleObj;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.decor-carousel {
|
||||
width: 100%;
|
||||
aspect-ratio: var(--carousel-ratio, 2.5 / 1);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: #f8fafc;
|
||||
box-sizing: border-box;
|
||||
|
||||
.custom-carousel {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
:deep(.el-carousel__container) {
|
||||
height: 100% !important;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:deep(.el-carousel__indicators--horizontal) {
|
||||
bottom: 14px;
|
||||
|
||||
.el-carousel__indicator {
|
||||
padding: 4px;
|
||||
|
||||
.el-carousel__button {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
opacity: 1;
|
||||
transition: all 0.25s;
|
||||
}
|
||||
|
||||
&.is-active .el-carousel__button {
|
||||
width: 22px;
|
||||
border-radius: 5px;
|
||||
background-color: #ea580c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.slide-link {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.slide-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: var(--carousel-fit, contain);
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-compact {
|
||||
:deep(.el-carousel__arrow) {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
font-size: 12px;
|
||||
}
|
||||
: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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,154 @@
|
||||
<template>
|
||||
<div class="decor-category-mega-menu" :style="customStyle">
|
||||
<!-- 顶部标题:分类(同万米设计,纯净大方) -->
|
||||
<div class="menu-header">
|
||||
<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 || ''}`"
|
||||
class="cat-item"
|
||||
:style="{
|
||||
'--text-color': options.textColor || '#334155',
|
||||
'--active-color': options.activeColor || '#ff6000',
|
||||
'--font-size': `${options.fontSize || 13}px`
|
||||
}"
|
||||
>
|
||||
<span class="cat-name">{{ cat.name }}</span>
|
||||
<el-icon class="arrow-icon"><ArrowRight /></el-icon>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { ArrowRight } from '@element-plus/icons-vue';
|
||||
|
||||
const props = defineProps({
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
componentStyle: {
|
||||
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 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
|
||||
};
|
||||
});
|
||||
}
|
||||
return defaultCategories;
|
||||
});
|
||||
|
||||
const customStyle = computed(() => {
|
||||
const s = props.componentStyle || {};
|
||||
return {
|
||||
backgroundColor: s.backgroundColor || '#ffffff',
|
||||
borderRadius: Array.isArray(s.radius)
|
||||
? `${s.radius[0]}px ${s.radius[1]}px ${s.radius[2]}px ${s.radius[3]}px`
|
||||
: '8px'
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.decor-category-mega-menu {
|
||||
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);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
.menu-header {
|
||||
padding: 14px 16px 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.header-title {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
}
|
||||
|
||||
.category-list {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: 2px 0 10px;
|
||||
|
||||
.cat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
.cat-name {
|
||||
font-size: var(--font-size, 13px);
|
||||
color: var(--text-color, #334155);
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.arrow-icon {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
transition: transform 0.15s ease, color 0.15s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #fff7ed;
|
||||
|
||||
.cat-name {
|
||||
color: var(--active-color, #ff6000);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.arrow-icon {
|
||||
color: var(--active-color, #ff6000);
|
||||
transform: translateX(2px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
199
buyer/src/components/indexDecorate/components/CategoryNav.vue
Normal file
@@ -0,0 +1,199 @@
|
||||
<template>
|
||||
<div class="decor-category-nav" :style="customStyle">
|
||||
<div class="nav-inner">
|
||||
<!-- 1. 全部分类入口 (当开启显示全部分类时) -->
|
||||
<div
|
||||
v-if="options.showAllCategory"
|
||||
class="all-category-btn"
|
||||
:style="{ backgroundColor: options.activeColor || '#e11d48' }"
|
||||
>
|
||||
<img
|
||||
v-if="options.allCategoryIcon"
|
||||
:src="options.allCategoryIcon"
|
||||
class="all-icon-img"
|
||||
alt="icon"
|
||||
/>
|
||||
<el-icon v-else class="all-icon-svg"><Menu /></el-icon>
|
||||
<span class="all-text">{{ options.allCategoryText || '全部分类' }}</span>
|
||||
<el-icon class="all-arrow"><ArrowDown /></el-icon>
|
||||
</div>
|
||||
|
||||
<!-- 2. 分类导航项水平列表 -->
|
||||
<ul class="nav-items-ul">
|
||||
<li
|
||||
v-for="(item, idx) in options.navItems"
|
||||
:key="idx"
|
||||
class="nav-li"
|
||||
>
|
||||
<router-link
|
||||
:to="item.url || '/'"
|
||||
class="nav-link"
|
||||
:style="{
|
||||
'--text-color': options.textColor || '#334155',
|
||||
'--active-color': options.activeColor || '#e11d48'
|
||||
}"
|
||||
active-class="is-active"
|
||||
>
|
||||
<img v-if="item.img" :src="item.img" class="cat-icon" alt="cat-icon" />
|
||||
<span class="cat-title">{{ item.title }}</span>
|
||||
<span
|
||||
class="badge"
|
||||
v-if="item.badge"
|
||||
:style="{ backgroundColor: options.activeColor || '#e11d48' }"
|
||||
>
|
||||
{{ item.badge }}
|
||||
</span>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { Menu, ArrowDown } from '@element-plus/icons-vue';
|
||||
|
||||
const props = defineProps({
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
componentStyle: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const customStyle = computed(() => {
|
||||
const s = props.componentStyle || {};
|
||||
return {
|
||||
backgroundColor: s.backgroundColor || '#ffffff',
|
||||
height: `${s.height || 42}px`
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.decor-category-nav {
|
||||
width: 100%;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.nav-inner {
|
||||
max-width: 1600px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 0 16px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
|
||||
// 全部分类按钮
|
||||
.all-category-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
height: 100%;
|
||||
padding: 0 18px;
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
margin-right: 20px;
|
||||
user-select: none;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
.all-icon-img {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.all-icon-svg {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.all-text {
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.all-arrow {
|
||||
font-size: 12px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-items-ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 28px;
|
||||
height: 100%;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-li {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.nav-link {
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--text-color, #334155);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
height: 100%;
|
||||
white-space: nowrap;
|
||||
transition: color 0.2s;
|
||||
|
||||
.cat-icon {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
object-fit: contain;
|
||||
border-radius: 4px;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover, &.is-active {
|
||||
color: var(--active-color, #e11d48) !important;
|
||||
|
||||
.cat-icon {
|
||||
transform: scale(1.12);
|
||||
}
|
||||
}
|
||||
|
||||
.badge {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: -12px;
|
||||
color: #ffffff;
|
||||
font-size: 9px;
|
||||
line-height: 1;
|
||||
padding: 2px 4px;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
559
buyer/src/components/indexDecorate/components/ChannelFloor.vue
Normal file
@@ -0,0 +1,559 @@
|
||||
<template>
|
||||
<div class="decor-channel-floor" :style="customStyle">
|
||||
<!-- 1. 标题区 -->
|
||||
<div class="channel-floor-header" v-if="options.showTitle !== false && options.title">
|
||||
<div class="header-main-title">
|
||||
<span class="title-pill"></span>
|
||||
<h3 class="title-text">{{ options.title }}</h3>
|
||||
<span class="subtitle-text" v-if="options.subtitle">{{ options.subtitle }}</span>
|
||||
</div>
|
||||
<router-link :to="options.heroHref || '/goods'" class="header-more">
|
||||
探索更多场景 >
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- 2. 场景频道组合布局:左侧主视觉+活动入口 + 右侧商品网格 -->
|
||||
<div class="channel-floor-body" :class="{ 'no-hero': !showHero }">
|
||||
<!-- 左侧主视觉卡片 (Hero Card) -->
|
||||
<div
|
||||
class="channel-hero-card"
|
||||
:class="[`style-${options.heroStyle || 'gradient'}`]"
|
||||
v-if="showHero"
|
||||
>
|
||||
<div class="hero-top-info">
|
||||
<div class="hero-badge">SCENE CHANNEL</div>
|
||||
<h3 class="hero-title">{{ options.heroTitle || '品质生活馆' }}</h3>
|
||||
<p class="hero-sub">{{ options.heroSubtitle || '精选场景好物,一站购齐' }}</p>
|
||||
</div>
|
||||
|
||||
<div class="hero-image-wrap">
|
||||
<img :src="heroImg" alt="channel hero" class="hero-img" />
|
||||
<router-link :to="options.heroHref || '/goods'" class="hero-cta-btn">
|
||||
<span>{{ options.heroButtonText || '进入频道' }}</span>
|
||||
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
|
||||
<path d="M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41z"/>
|
||||
</svg>
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- 专属活动入口 (Promos) -->
|
||||
<div class="hero-promos-wrap" v-if="promosList.length">
|
||||
<router-link
|
||||
v-for="(p, pIdx) in promosList"
|
||||
:key="pIdx"
|
||||
:to="p.url || '/goods'"
|
||||
class="hero-promo-item"
|
||||
>
|
||||
<span class="promo-tag" v-if="p.tag">{{ p.tag }}</span>
|
||||
<div class="promo-text-box">
|
||||
<span class="promo-title">{{ p.title }}</span>
|
||||
<span class="promo-desc">{{ p.desc }}</span>
|
||||
</div>
|
||||
<span class="promo-arrow">></span>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧商品网格 (Scene Goods Grid) -->
|
||||
<div
|
||||
class="channel-goods-grid"
|
||||
:class="`cols-${rightColumns}`"
|
||||
:style="{ gridTemplateColumns: `repeat(${rightColumns}, 1fr)` }"
|
||||
>
|
||||
<div
|
||||
v-for="(g, idx) in displayGoods"
|
||||
:key="g.id || idx"
|
||||
class="scene-goods-card"
|
||||
>
|
||||
<router-link :to="`/goods/${g.id || 1}`" class="card-thumb-wrap">
|
||||
<img :src="g.thumbnail || defaultThumb" :alt="g.goodsName" class="goods-img" />
|
||||
<span class="scene-tag">场景精选</span>
|
||||
</router-link>
|
||||
|
||||
<div class="card-info">
|
||||
<h4 class="goods-name">
|
||||
<router-link :to="`/goods/${g.id || 1}`">{{ g.goodsName }}</router-link>
|
||||
</h4>
|
||||
<div class="price-action-row">
|
||||
<div class="price-box">
|
||||
<span class="sym">¥</span>
|
||||
<span class="val">{{ g.price }}</span>
|
||||
<span class="orig" v-if="g.originalPrice">¥{{ g.originalPrice }}</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="cart-btn"
|
||||
title="立即加购"
|
||||
@click="addToCart(g)"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
componentStyle: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const defaultThumb = '/static/goods/goods_1.png';
|
||||
|
||||
const heroImg = computed(() => {
|
||||
return props.options?.heroImage || '/static/banner-floor/floor_banner_1.png';
|
||||
});
|
||||
|
||||
const showHero = computed(() => {
|
||||
const ratio = props.componentStyle?.widthRatio || '100%';
|
||||
if (ratio === '16.67%' || ratio === '16.6%' || ratio === '33.33%' || ratio === '33.3%') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const rightColumns = computed(() => {
|
||||
const ratio = props.componentStyle?.widthRatio || '100%';
|
||||
if (ratio === '16.67%' || ratio === '16.6%') return 1;
|
||||
if (ratio === '33.33%' || ratio === '33.3%') return 2;
|
||||
if (ratio === '50%') return showHero.value ? 2 : 3;
|
||||
if (ratio === '66.67%' || ratio === '66.6%') return showHero.value ? 3 : 4;
|
||||
return 3;
|
||||
});
|
||||
|
||||
const promosList = computed(() => {
|
||||
if (Array.isArray(props.options?.promos) && props.options.promos.length > 0) {
|
||||
return props.options.promos;
|
||||
}
|
||||
return [
|
||||
{ title: '大牌补贴', desc: '领券最高减100', tag: '限时', url: '/goods' },
|
||||
{ title: '新品驾到', desc: '官方正品首发', tag: '新品', url: '/goods' }
|
||||
];
|
||||
});
|
||||
|
||||
const defaultGoods = [
|
||||
{ id: '1', goodsName: '旗舰 5G 智能手机 12GB+256GB 超光影影像系统', price: '3999.00', originalPrice: '4599.00', thumbnail: '/static/goods/goods_1.png' },
|
||||
{ id: '2', goodsName: '无线头戴式深度主动降噪蓝牙耳机 空间音频Hi-Res', price: '1099.00', originalPrice: '1499.00', thumbnail: '/static/goods/goods_2.png' },
|
||||
{ id: '3', goodsName: '14英寸 全面屏轻薄商务笔记本电脑 酷睿i7 高刷防眩光', price: '5699.00', originalPrice: '6299.00', thumbnail: '/static/goods/goods_3.png' },
|
||||
{ id: '4', goodsName: '运动健康专业监测智能手表 心率血氧长续航50米防水', price: '899.00', originalPrice: '1199.00', thumbnail: '/static/goods/goods_4.png' },
|
||||
{ id: '5', goodsName: '意式半自动家用复古咖啡机 20Bar高压恒温萃取蒸汽奶泡', price: '899.00', originalPrice: '1280.00', thumbnail: '/static/goods/goods_6.png' },
|
||||
{ id: '6', goodsName: '奢宠酵母焕活修护精华露 50ml 紧致淡纹抗皱滋养', price: '680.00', originalPrice: '890.00', thumbnail: '/static/goods/goods_5.png' }
|
||||
];
|
||||
|
||||
const displayGoods = computed(() => {
|
||||
if (Array.isArray(props.options?.goods) && props.options.goods.length > 0) {
|
||||
return props.options.goods;
|
||||
}
|
||||
const count = props.options?.goodsCount || 6;
|
||||
return defaultGoods.slice(0, count);
|
||||
});
|
||||
|
||||
const customStyle = computed(() => {
|
||||
const s = props.componentStyle || {};
|
||||
return {
|
||||
backgroundColor: s.backgroundColor || '#ffffff',
|
||||
padding: Array.isArray(s.padding)
|
||||
? `${s.padding[0]}px ${s.padding[1]}px ${s.padding[2]}px ${s.padding[3]}px`
|
||||
: '16px 20px',
|
||||
borderRadius: Array.isArray(s.radius)
|
||||
? `${s.radius[0]}px ${s.radius[1]}px ${s.radius[2]}px ${s.radius[3]}px`
|
||||
: '12px'
|
||||
};
|
||||
});
|
||||
|
||||
function addToCart(item) {
|
||||
ElMessage.success(`已将 "${item.goodsName?.slice(0, 10)}..." 加入购物车`);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.decor-channel-floor {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
.channel-floor-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 12px;
|
||||
margin-bottom: 14px;
|
||||
border-bottom: 2px solid #f1f5f9;
|
||||
|
||||
.header-main-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
.title-pill {
|
||||
width: 4px;
|
||||
height: 18px;
|
||||
background: linear-gradient(180deg, #6366f1, #4f46e5);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.subtitle-text {
|
||||
font-size: 13px;
|
||||
color: #94a3b8;
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.header-more {
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #4f46e5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.channel-floor-body {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: stretch;
|
||||
|
||||
&.no-hero {
|
||||
.channel-goods-grid {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.channel-hero-card {
|
||||
width: 320px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px 18px 16px;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.style-gradient {
|
||||
background: linear-gradient(145deg, #1e1b4b, #312e81 60%, #4338ca);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
&.style-plain {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
color: #1e293b;
|
||||
|
||||
.hero-badge {
|
||||
background: #e0e7ff;
|
||||
color: #4338ca;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.hero-sub {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.hero-cta-btn {
|
||||
background: #4f46e5;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
.hero-top-info {
|
||||
margin-bottom: 12px;
|
||||
|
||||
.hero-badge {
|
||||
display: inline-block;
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 1px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
backdrop-filter: blur(4px);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 22px;
|
||||
font-weight: 800;
|
||||
margin: 0 0 4px 0;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.hero-sub {
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
|
||||
.hero-image-wrap {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 220px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 14px;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
|
||||
.hero-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.4s ease;
|
||||
}
|
||||
|
||||
&:hover .hero-img {
|
||||
transform: scale(1.04);
|
||||
}
|
||||
|
||||
.hero-cta-btn {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
background: #ffffff;
|
||||
color: #312e81;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
padding: 6px 14px;
|
||||
border-radius: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
text-decoration: none;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.25);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #4f46e5;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hero-promos-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-top: auto;
|
||||
|
||||
.hero-promo-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
backdrop-filter: blur(6px);
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.22);
|
||||
transform: translateX(3px);
|
||||
}
|
||||
|
||||
.promo-tag {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
background: #ef4444;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.promo-text-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
|
||||
.promo-title {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.promo-desc {
|
||||
font-size: 11px;
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
|
||||
.promo-arrow {
|
||||
font-size: 13px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.channel-goods-grid {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
|
||||
.scene-goods-card {
|
||||
background: #ffffff;
|
||||
border: 1px solid #f1f5f9;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: all 0.25s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: #cbd5e1;
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.06);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.card-thumb-wrap {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 1 / 1;
|
||||
background: #f8fafc;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.goods-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
&:hover .goods-img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.scene-tag {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
left: 6px;
|
||||
background: #4f46e5;
|
||||
color: #ffffff;
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
padding: 1px 5px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
|
||||
.goods-name {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
height: 34px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
margin: 0 0 6px 0;
|
||||
|
||||
a {
|
||||
color: #1e293b;
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
color: #4f46e5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.price-action-row {
|
||||
margin-top: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.price-box {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 1px;
|
||||
|
||||
.sym {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #e11d48;
|
||||
}
|
||||
|
||||
.val {
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
color: #e11d48;
|
||||
}
|
||||
|
||||
.orig {
|
||||
font-size: 10px;
|
||||
color: #94a3b8;
|
||||
text-decoration: line-through;
|
||||
margin-left: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.cart-btn {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: #f1f5f9;
|
||||
color: #475569;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #4f46e5;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
277
buyer/src/components/indexDecorate/components/ChannelGrid.vue
Normal file
@@ -0,0 +1,277 @@
|
||||
<template>
|
||||
<div class="decor-channel-grid" :style="customStyle">
|
||||
<div
|
||||
class="grid-container"
|
||||
:style="{
|
||||
gridTemplateColumns: `repeat(${gridColumns}, 1fr)`,
|
||||
gap: `${options.gap ?? 12}px`
|
||||
}"
|
||||
>
|
||||
<router-link
|
||||
v-for="(item, idx) in options.items"
|
||||
:key="idx"
|
||||
:to="item.url || '/goods'"
|
||||
class="channel-card"
|
||||
:class="[
|
||||
`is-${layoutMode}`,
|
||||
{ 'is-compact': isCompact }
|
||||
]"
|
||||
>
|
||||
<!-- 图标/图片 -->
|
||||
<div
|
||||
class="card-thumb"
|
||||
:style="{ borderRadius: `${options.imageRadius ?? 10}px` }"
|
||||
>
|
||||
<img
|
||||
v-if="item.img"
|
||||
:src="item.img"
|
||||
:alt="item.title"
|
||||
class="thumb-img"
|
||||
/>
|
||||
<div v-else class="thumb-placeholder">
|
||||
<span>{{ (item.title || '频').charAt(0) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主标题与副标题 -->
|
||||
<div class="card-info">
|
||||
<div class="card-title">{{ item.title || '频道名称' }}</div>
|
||||
<div class="card-sub">{{ item.subtitle || '副标题文案' }}</div>
|
||||
</div>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
componentStyle: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
// 排版宽度比例 (整行: 100%, 三分之二: 66.67%, 一半: 50%, 三分之一: 33.33%, 六分之一: 16.67%)
|
||||
const widthRatio = computed(() => {
|
||||
return props.componentStyle?.widthRatio || '100%';
|
||||
});
|
||||
|
||||
// 响应式列数:50%、33.33%、16.67% 为 2 列;100%、66.67% 为 4 列
|
||||
const gridColumns = computed(() => {
|
||||
const w = widthRatio.value;
|
||||
if (w === '50%' || w === '33.33%' || w === '33.3%' || w === '16.67%' || w === '16.6%') {
|
||||
return 2;
|
||||
}
|
||||
return 4;
|
||||
});
|
||||
|
||||
// 卡片内部排布方式:
|
||||
// 100% (整行)、50% (一半) -> 水平横向 (左图右文)
|
||||
// 66.67% (三分之二)、33.33% (三分之一)、16.67% (六分之一) -> 垂直纵向 (上图下文居中)
|
||||
const layoutMode = computed(() => {
|
||||
const w = widthRatio.value;
|
||||
if (w === '66.67%' || w === '66.6%' || w === '33.33%' || w === '33.3%' || w === '16.67%' || w === '16.6%') {
|
||||
return 'vertical';
|
||||
}
|
||||
return 'horizontal';
|
||||
});
|
||||
|
||||
// 六分之一紧凑模式
|
||||
const isCompact = computed(() => {
|
||||
const w = widthRatio.value;
|
||||
return w === '16.67%' || w === '16.6%';
|
||||
});
|
||||
|
||||
const customStyle = computed(() => {
|
||||
const s = props.componentStyle || {};
|
||||
return {
|
||||
backgroundColor: s.backgroundColor || 'transparent',
|
||||
padding: Array.isArray(s.padding)
|
||||
? `${s.padding[0]}px ${s.padding[1]}px ${s.padding[2]}px ${s.padding[3]}px`
|
||||
: '10px 0'
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.decor-channel-grid {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
.grid-container {
|
||||
max-width: 1600px;
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
.channel-card {
|
||||
background: #ffffff;
|
||||
border: 1px solid #f1f5f9;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
|
||||
box-sizing: border-box;
|
||||
transition: all 0.25s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.06);
|
||||
border-color: #e2e8f0;
|
||||
|
||||
.card-thumb .thumb-img {
|
||||
transform: scale(1.06);
|
||||
}
|
||||
|
||||
.card-info .card-title {
|
||||
color: #ea580c;
|
||||
}
|
||||
}
|
||||
|
||||
.card-thumb {
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f8fafc;
|
||||
|
||||
.thumb-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.thumb-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, #f97316, #ea580c);
|
||||
color: #ffffff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-info {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
.card-title {
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.card-sub {
|
||||
color: #94a3b8;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 水平横向排布 (整行 100%、一半 50%)
|
||||
&.is-horizontal {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
|
||||
.card-thumb {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.card-info {
|
||||
flex: 1;
|
||||
gap: 3px;
|
||||
text-align: left;
|
||||
|
||||
.card-title {
|
||||
font-size: 14px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.card-sub {
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 垂直纵向排布 (三分之二 66.67%、三分之一 33.33%、六分之一 16.67%)
|
||||
&.is-vertical {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
gap: 8px;
|
||||
padding: 14px 10px 12px 10px;
|
||||
|
||||
.card-thumb {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.card-info {
|
||||
width: 100%;
|
||||
gap: 2px;
|
||||
text-align: center;
|
||||
|
||||
.card-title {
|
||||
font-size: 13px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.card-sub {
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 六分之一紧凑模式
|
||||
&.is-compact {
|
||||
padding: 10px 6px 8px 6px;
|
||||
gap: 5px;
|
||||
|
||||
.card-thumb {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.card-info {
|
||||
gap: 1px;
|
||||
|
||||
.card-title {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.card-sub {
|
||||
font-size: 11px;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
184
buyer/src/components/indexDecorate/components/CouponList.vue
Normal file
@@ -0,0 +1,184 @@
|
||||
<template>
|
||||
<div class="decor-coupon-list" :class="`skin-${options.skin || 'red'}`" :style="customStyle">
|
||||
<div
|
||||
class="coupon-grid"
|
||||
:style="{ gridTemplateColumns: `repeat(${actualColumns}, 1fr)` }"
|
||||
>
|
||||
<div
|
||||
v-for="(cp, idx) in coupons"
|
||||
:key="idx"
|
||||
class="coupon-card"
|
||||
>
|
||||
<div class="card-left">
|
||||
<div class="amount-wrap">
|
||||
<span class="symbol">¥</span>
|
||||
<span class="amount">{{ cp.price }}</span>
|
||||
</div>
|
||||
<span class="threshold">{{ cp.consumeThreshold ? `满 ${cp.consumeThreshold} 可用` : '无门槛' }}</span>
|
||||
</div>
|
||||
<div class="card-right">
|
||||
<span class="scope">{{ cp.title }}</span>
|
||||
<button type="button" class="claim-btn" @click="handleClaim(cp)">
|
||||
立即领取
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
const props = defineProps({
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
componentStyle: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const defaultCoupons = [
|
||||
{ id: '1', price: 20, consumeThreshold: 199, title: '全品类通用券', limitText: '全场通用 无门槛限制' },
|
||||
{ id: '2', price: 50, consumeThreshold: 399, title: '数码家电专享券', limitText: '数码/手机/家电可用' },
|
||||
{ id: '3', price: 100, consumeThreshold: 699, title: '大牌美妆特惠券', limitText: '美妆奢护专区可用' },
|
||||
{ id: '4', price: 10, consumeThreshold: 99, title: '新客首单立减券', limitText: '首次下单专享福利' }
|
||||
];
|
||||
|
||||
const actualColumns = computed(() => {
|
||||
const baseCols = Number(props.options?.columns) || 4;
|
||||
const ratio = props.componentStyle?.widthRatio || '100%';
|
||||
if (ratio === '16.67%' || ratio === '16.6%' || ratio === '16.7%') {
|
||||
return 1;
|
||||
}
|
||||
if (ratio === '33.33%' || ratio === '33.3%') {
|
||||
return Math.min(baseCols, 2);
|
||||
}
|
||||
if (ratio === '50%') {
|
||||
return Math.min(baseCols, 2);
|
||||
}
|
||||
if (ratio === '66.67%' || ratio === '66.6%' || ratio === '66.7%') {
|
||||
return Math.min(baseCols, 3);
|
||||
}
|
||||
return baseCols;
|
||||
});
|
||||
|
||||
const customStyle = computed(() => {
|
||||
const s = props.componentStyle || {};
|
||||
return {
|
||||
backgroundColor: s.backgroundColor || 'transparent',
|
||||
padding: Array.isArray(s.padding)
|
||||
? `${s.padding[0]}px ${s.padding[1]}px ${s.padding[2]}px ${s.padding[3]}px`
|
||||
: '10px 0',
|
||||
borderRadius: Array.isArray(s.radius)
|
||||
? `${s.radius[0]}px ${s.radius[1]}px ${s.radius[2]}px ${s.radius[3]}px`
|
||||
: '0'
|
||||
};
|
||||
});
|
||||
|
||||
const coupons = computed(() => {
|
||||
if (Array.isArray(props.options?.coupons) && props.options.coupons.length > 0) {
|
||||
return props.options.coupons;
|
||||
}
|
||||
const count = props.options?.columns || 4;
|
||||
return defaultCoupons.slice(0, count);
|
||||
});
|
||||
|
||||
function handleClaim(cp) {
|
||||
ElMessage.success(`领取成功!¥${cp.price} 优惠券已放入您的卡券包`);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.decor-coupon-list {
|
||||
width: 100%;
|
||||
|
||||
.coupon-grid {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
|
||||
.coupon-card {
|
||||
display: flex;
|
||||
height: 90px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
transition: transform 0.2s;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.card-left {
|
||||
width: 120px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #ffffff;
|
||||
padding: 8px;
|
||||
|
||||
.amount-wrap {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
||||
.symbol { font-size: 14px; font-weight: 700; }
|
||||
.amount { font-size: 28px; font-weight: 800; }
|
||||
}
|
||||
|
||||
.threshold {
|
||||
font-size: 11px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
.card-right {
|
||||
flex: 1;
|
||||
background: #ffffff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: 12px 14px;
|
||||
|
||||
.scope {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.claim-btn {
|
||||
align-self: flex-start;
|
||||
padding: 4px 12px;
|
||||
border-radius: 14px;
|
||||
border: none;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:hover { opacity: 0.85; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.skin-red {
|
||||
.coupon-card .card-left { background: linear-gradient(135deg, #f43f5e, #e11d48); }
|
||||
.claim-btn { background: #ffe4e6; color: #e11d48; }
|
||||
}
|
||||
|
||||
&.skin-dark {
|
||||
.coupon-card .card-left { background: linear-gradient(135deg, #334155, #0f172a); }
|
||||
.claim-btn { background: #f1f5f9; color: #0f172a; }
|
||||
}
|
||||
|
||||
&.skin-gradient {
|
||||
.coupon-card .card-left { background: linear-gradient(135deg, #8b5cf6, #ec4899); }
|
||||
.claim-btn { background: #fdf2f8; color: #db2777; }
|
||||
}
|
||||
}
|
||||
</style>
|
||||
165
buyer/src/components/indexDecorate/components/FooterNav.vue
Normal file
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<div class="decor-footer-nav" :style="customStyle">
|
||||
<div class="footer-inner">
|
||||
<!-- 1. 服务保障条 -->
|
||||
<div class="assurance-row" v-if="options.showAssurance && options.assurances?.length">
|
||||
<div
|
||||
v-for="(item, idx) in options.assurances"
|
||||
:key="idx"
|
||||
class="assurance-col"
|
||||
>
|
||||
<div class="code-badge">{{ item.code }}</div>
|
||||
<div class="text-info">
|
||||
<span class="main-t">{{ item.title }}</span>
|
||||
<span class="sub-t">{{ item.desc }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. 服务指南与链接列 -->
|
||||
<div class="guides-row" v-if="options.guideGroups?.length">
|
||||
<div
|
||||
v-for="(group, gIdx) in options.guideGroups"
|
||||
:key="gIdx"
|
||||
class="guide-col"
|
||||
>
|
||||
<h4 class="col-title">{{ group.title }}</h4>
|
||||
<ul class="col-links">
|
||||
<li v-for="(link, lIdx) in group.links" :key="lIdx">
|
||||
<router-link :to="link.url || '/article'">{{ link.text }}</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3. 版权与备案 -->
|
||||
<div class="copyright-row">
|
||||
<p class="icp-text" v-if="options.icpText">{{ options.icpText }}</p>
|
||||
<p class="copy-text">{{ options.copyright || 'Copyright © 2026 Lilishop. All Rights Reserved.' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
componentStyle: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const customStyle = computed(() => {
|
||||
const s = props.componentStyle || {};
|
||||
return {
|
||||
backgroundColor: s.backgroundColor || '#f1f5f9'
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.decor-footer-nav {
|
||||
width: 100%;
|
||||
padding: 40px 0 30px;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
|
||||
.footer-inner {
|
||||
max-width: 1600px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
|
||||
.assurance-row {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
padding-bottom: 30px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
|
||||
.assurance-col {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
.code-badge {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #e11d48;
|
||||
color: #e11d48;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.text-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.main-t {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.sub-t {
|
||||
font-size: 13px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.guides-row {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
padding: 30px 0;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
|
||||
.guide-col {
|
||||
.col-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.col-links {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
li a {
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #e11d48;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.copyright-row {
|
||||
text-align: center;
|
||||
padding-top: 24px;
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
line-height: 1.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
60
buyer/src/components/indexDecorate/components/FreeLayout.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<div class="decor-free-layout" :style="customStyle">
|
||||
<div
|
||||
v-for="(el, idx) in options.elements"
|
||||
:key="idx"
|
||||
class="free-element"
|
||||
:style="{
|
||||
top: `${el.top || 0}px`,
|
||||
left: `${el.left || 0}px`,
|
||||
width: el.width ? `${el.width}px` : 'auto',
|
||||
height: el.height ? `${el.height}px` : 'auto'
|
||||
}"
|
||||
>
|
||||
<img v-if="el.type === 'image'" :src="el.src" alt="" style="width:100%;height:100%;object-fit:cover;" />
|
||||
<span v-else-if="el.type === 'text'">{{ el.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
componentStyle: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const customStyle = computed(() => {
|
||||
const s = props.componentStyle || {};
|
||||
return {
|
||||
height: `${props.options?.height || 320}px`,
|
||||
backgroundColor: s.backgroundColor || 'transparent',
|
||||
padding: Array.isArray(s.padding)
|
||||
? `${s.padding[0]}px ${s.padding[1]}px ${s.padding[2]}px ${s.padding[3]}px`
|
||||
: '0',
|
||||
borderRadius: Array.isArray(s.radius)
|
||||
? `${s.radius[0]}px ${s.radius[1]}px ${s.radius[2]}px ${s.radius[3]}px`
|
||||
: '0'
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.decor-free-layout {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
|
||||
.free-element {
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
342
buyer/src/components/indexDecorate/components/GoodsGrid.vue
Normal file
@@ -0,0 +1,342 @@
|
||||
<template>
|
||||
<div class="decor-goods-grid" :style="customStyle">
|
||||
<!-- 宫格顶部标题区 -->
|
||||
<div class="grid-header" v-if="options.title">
|
||||
<div class="grid-title-wrap">
|
||||
<span class="grid-title-decor"></span>
|
||||
<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">
|
||||
{{ options.moreText || '查看全部' }} >
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- 紧凑严谨的商品九宫格/矩阵网格 (Bordered Matrix Grid) -->
|
||||
<div
|
||||
class="matrix-grid-container"
|
||||
:class="[`cols-${actualColumns}`, { 'with-grid-border': options.showBorder !== false }]"
|
||||
:style="{ gridTemplateColumns: `repeat(${actualColumns}, 1fr)` }"
|
||||
>
|
||||
<div
|
||||
v-for="(item, idx) in displayGoods"
|
||||
:key="item.id || idx"
|
||||
class="matrix-grid-cell"
|
||||
>
|
||||
<div class="cell-rank-tag" v-if="idx < 5">
|
||||
<span class="rank-num">TOP {{ idx + 1 }}</span>
|
||||
</div>
|
||||
|
||||
<router-link :to="`/goods/${item.id || 1}`" class="cell-thumb-box">
|
||||
<img :src="item.thumbnail || defaultThumb" :alt="item.goodsName" class="cell-thumb" />
|
||||
</router-link>
|
||||
|
||||
<div class="cell-body">
|
||||
<h4 class="cell-name">
|
||||
<router-link :to="`/goods/${item.id || 1}`">{{ item.goodsName }}</router-link>
|
||||
</h4>
|
||||
<div class="cell-meta">
|
||||
<span class="badge-label">热卖好物</span>
|
||||
</div>
|
||||
<div class="cell-bottom-row">
|
||||
<div class="price-box">
|
||||
<span class="sym">¥</span>
|
||||
<span class="val">{{ item.price }}</span>
|
||||
<span class="del" v-if="item.originalPrice">¥{{ item.originalPrice }}</span>
|
||||
</div>
|
||||
<span class="action-add-pill" title="立即选购" @click="addToCart(item)">+</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
componentStyle: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const defaultThumb = '/static/goods/goods_1.png';
|
||||
|
||||
const actualColumns = computed(() => {
|
||||
const ratio = props.componentStyle?.widthRatio || '100%';
|
||||
const base = Number(props.options?.columns) || 5;
|
||||
if (ratio === '16.67%' || ratio === '16.6%') return 1;
|
||||
if (ratio === '33.33%' || ratio === '33.3%') return 2;
|
||||
if (ratio === '50%') return 3;
|
||||
if (ratio === '66.67%' || ratio === '66.6%') return 4;
|
||||
return base;
|
||||
});
|
||||
|
||||
const defaultGridGoods = [
|
||||
{ id: '1', goodsName: '旗舰 5G 智能手机 12GB+256GB 超光影影像系统', price: '3999.00', originalPrice: '4599.00', thumbnail: '/static/goods/goods_1.png' },
|
||||
{ id: '2', goodsName: '无线头戴式深度主动降噪蓝牙耳机 空间音频Hi-Res', price: '1099.00', originalPrice: '1499.00', thumbnail: '/static/goods/goods_2.png' },
|
||||
{ id: '3', goodsName: '14英寸 全面屏轻薄商务笔记本电脑 酷睿i7 高刷防眩光', price: '5699.00', originalPrice: '6299.00', thumbnail: '/static/goods/goods_3.png' },
|
||||
{ id: '4', goodsName: '运动健康专业监测智能手表 心率血氧长续航50米防水', price: '899.00', originalPrice: '1199.00', thumbnail: '/static/goods/goods_4.png' },
|
||||
{ id: '5', goodsName: '奢宠酵母焕活修护精华露 50ml 紧致淡纹抗皱滋养', price: '680.00', originalPrice: '890.00', thumbnail: '/static/goods/goods_5.png' },
|
||||
{ id: '6', goodsName: '意式半自动家用复古咖啡机 20Bar高压恒温萃取蒸汽奶泡', price: '899.00', originalPrice: '1280.00', thumbnail: '/static/goods/goods_6.png' },
|
||||
{ id: '7', goodsName: '碳板专业竞速轻量缓震跑步鞋 透气防滑男女同款潮鞋', price: '459.00', originalPrice: '699.00', thumbnail: '/static/goods/goods_7.png' },
|
||||
{ id: '8', goodsName: '极简静音无雾负离子香薰加湿器 大容量客厅卧室通用', price: '299.00', originalPrice: '399.00', thumbnail: '/static/goods/goods_8.png' },
|
||||
{ id: '9', goodsName: '智能降噪AI翻译蓝牙墨镜 超轻防紫外线偏光太阳镜', price: '599.00', originalPrice: '799.00', thumbnail: '/static/goods/goods_1.png' },
|
||||
{ id: '10', goodsName: '15.6英寸 4K超高清便携式显示器 Type-C一线直连高色域', price: '1299.00', originalPrice: '1699.00', thumbnail: '/static/goods/goods_3.png' }
|
||||
];
|
||||
|
||||
const displayGoods = computed(() => {
|
||||
if (Array.isArray(props.options?.goods) && props.options.goods.length > 0) {
|
||||
return props.options.goods;
|
||||
}
|
||||
const count = (actualColumns.value * 2) || 10;
|
||||
return defaultGridGoods.slice(0, count);
|
||||
});
|
||||
|
||||
const customStyle = computed(() => {
|
||||
const s = props.componentStyle || {};
|
||||
return {
|
||||
backgroundColor: s.backgroundColor || '#ffffff',
|
||||
padding: Array.isArray(s.padding)
|
||||
? `${s.padding[0]}px ${s.padding[1]}px ${s.padding[2]}px ${s.padding[3]}px`
|
||||
: '18px 20px',
|
||||
borderRadius: Array.isArray(s.radius)
|
||||
? `${s.radius[0]}px ${s.radius[1]}px ${s.radius[2]}px ${s.radius[3]}px`
|
||||
: '12px'
|
||||
};
|
||||
});
|
||||
|
||||
function addToCart(item) {
|
||||
ElMessage.success(`已将 "${item.goodsName?.slice(0, 10)}..." 加入购物车`);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.decor-goods-grid {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
.grid-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 14px;
|
||||
margin-bottom: 14px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
|
||||
.grid-title-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
.grid-title-decor {
|
||||
width: 4px;
|
||||
height: 18px;
|
||||
background: #f97316;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.grid-main-title {
|
||||
font-size: 19px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.grid-sub-title {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.grid-more {
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #f97316;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.matrix-grid-container {
|
||||
display: grid;
|
||||
background: #ffffff;
|
||||
box-sizing: border-box;
|
||||
|
||||
&.with-grid-border {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
|
||||
.matrix-grid-cell {
|
||||
border-right: 1px solid #f1f5f9;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
}
|
||||
|
||||
.matrix-grid-cell {
|
||||
position: relative;
|
||||
padding: 16px 14px 14px 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
background: #ffffff;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: #fafbfc;
|
||||
z-index: 1;
|
||||
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.05);
|
||||
|
||||
.cell-thumb {
|
||||
transform: scale(1.06);
|
||||
}
|
||||
|
||||
.action-add-pill {
|
||||
background: #f97316;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
.cell-rank-tag {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
z-index: 2;
|
||||
|
||||
.rank-num {
|
||||
display: inline-block;
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
color: #f97316;
|
||||
background: #fff7ed;
|
||||
border: 1px solid #fed7aa;
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.cell-thumb-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
aspect-ratio: 1 / 1;
|
||||
overflow: hidden;
|
||||
margin-bottom: 12px;
|
||||
text-decoration: none;
|
||||
|
||||
.cell-thumb {
|
||||
width: 85%;
|
||||
height: 85%;
|
||||
object-fit: contain;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.cell-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
|
||||
.cell-name {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
height: 36px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
margin: 0 0 6px 0;
|
||||
|
||||
a {
|
||||
color: #334155;
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
color: #f97316;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cell-meta {
|
||||
display: flex;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.badge-label {
|
||||
font-size: 10px;
|
||||
color: #d97706;
|
||||
background: #fef3c7;
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.cell-bottom-row {
|
||||
margin-top: auto;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
|
||||
.price-box {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 1px;
|
||||
|
||||
.sym {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.val {
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.del {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
text-decoration: line-through;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.action-add-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
background: #f1f5f9;
|
||||
color: #64748b;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
transition: all 0.2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
327
buyer/src/components/indexDecorate/components/GoodsList.vue
Normal file
@@ -0,0 +1,327 @@
|
||||
<template>
|
||||
<div class="decor-goods-list" :style="customStyle">
|
||||
<!-- 标题栏 -->
|
||||
<div class="floor-title-bar" v-if="options.showTitle !== false && options.title">
|
||||
<div class="title-left">
|
||||
<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">
|
||||
{{ options.moreText || '查看更多' }} >
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- 6 列商品列表网格 (整行展示 6 个商品) -->
|
||||
<div
|
||||
class="goods-flow-grid"
|
||||
:class="`cols-${actualColumns}`"
|
||||
:style="{ gridTemplateColumns: `repeat(${actualColumns}, 1fr)` }"
|
||||
>
|
||||
<div
|
||||
v-for="(g, idx) in sampleGoods"
|
||||
:key="g.id || idx"
|
||||
class="goods-flow-card"
|
||||
>
|
||||
<router-link :to="`/goods/${g.id || 1}`" 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>
|
||||
</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>
|
||||
<span class="price-val" v-if="options.showPrice !== false">{{ g.price }}</span>
|
||||
<span class="orig-price" v-if="options.showOriginalPrice && g.originalPrice">¥{{ g.originalPrice }}</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="add-cart-circle"
|
||||
v-if="options.showCartBtn !== false"
|
||||
title="加入购物车"
|
||||
@click="addToCart(g)"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
componentStyle: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const defaultThumb = '/static/goods/goods_1.png';
|
||||
|
||||
const defaultMockGoods = [
|
||||
{ id: '1', goodsName: '旗舰 5G 智能手机 12GB+256GB 超光影影像系统', price: '3999.00', originalPrice: '4599.00', thumbnail: '/static/goods/goods_1.png' },
|
||||
{ id: '2', goodsName: '无线头戴式深度主动降噪蓝牙耳机 空间音频Hi-Res', price: '1099.00', originalPrice: '1499.00', thumbnail: '/static/goods/goods_2.png' },
|
||||
{ id: '3', goodsName: '14英寸 全面屏轻薄商务笔记本电脑 酷睿i7 高刷防眩光', price: '5699.00', originalPrice: '6299.00', thumbnail: '/static/goods/goods_3.png' },
|
||||
{ id: '4', goodsName: '运动健康专业监测智能手表 心率血氧长续航50米防水', price: '899.00', originalPrice: '1199.00', thumbnail: '/static/goods/goods_4.png' },
|
||||
{ id: '5', goodsName: '奢宠酵母焕活修护精华露 50ml 紧致淡纹抗皱滋养', price: '680.00', originalPrice: '890.00', thumbnail: '/static/goods/goods_5.png' },
|
||||
{ id: '6', goodsName: '意式半自动家用复古咖啡机 20Bar高压恒温萃取蒸汽奶泡', price: '899.00', originalPrice: '1280.00', thumbnail: '/static/goods/goods_6.png' },
|
||||
{ id: '7', goodsName: '碳板专业竞速轻量缓震跑步鞋 透气防滑男女同款潮鞋', price: '459.00', originalPrice: '699.00', thumbnail: '/static/goods/goods_7.png' },
|
||||
{ id: '8', goodsName: '极简静音无雾负离子香薰加湿器 大容量客厅卧室通用', price: '299.00', originalPrice: '399.00', thumbnail: '/static/goods/goods_8.png' },
|
||||
{ id: '9', goodsName: '折叠屏旗舰手机 典藏陶瓷白 悬停双旋水滴铰链', price: '6999.00', originalPrice: '7999.00', thumbnail: '/static/goods/goods_1.png' },
|
||||
{ id: '10', goodsName: '真无线触控降噪蓝牙耳机 超低延迟游戏降噪', price: '599.00', originalPrice: '799.00', thumbnail: '/static/goods/goods_2.png' },
|
||||
{ id: '11', goodsName: '国AA级减蓝光润眼长臂台灯 智能感光多档色温无极调光', price: '249.00', originalPrice: '359.00', thumbnail: '/static/goods/goods_4.png' },
|
||||
{ id: '12', goodsName: '航空级铝镁合金静音万向轮拉杆登机箱 20英寸防刮耐磨', price: '499.00', originalPrice: '699.00', thumbnail: '/static/goods/goods_7.png' }
|
||||
];
|
||||
|
||||
const actualColumns = computed(() => {
|
||||
let baseCols = Number(props.options?.columns) || 6;
|
||||
const ratio = props.componentStyle?.widthRatio || '100%';
|
||||
if (ratio === '16.67%' || ratio === '16.6%' || ratio === '16.7%') {
|
||||
return 1;
|
||||
}
|
||||
if (ratio === '33.33%' || ratio === '33.3%') {
|
||||
return Math.min(baseCols, 2);
|
||||
}
|
||||
if (ratio === '50%') {
|
||||
return Math.min(baseCols, 3);
|
||||
}
|
||||
if (ratio === '66.67%' || ratio === '66.6%' || ratio === '66.7%') {
|
||||
return Math.min(baseCols, 4);
|
||||
}
|
||||
return baseCols; // 100% 默认 6 列
|
||||
});
|
||||
|
||||
const customStyle = computed(() => {
|
||||
const s = props.componentStyle || {};
|
||||
return {
|
||||
backgroundColor: s.backgroundColor || 'transparent',
|
||||
padding: Array.isArray(s.padding)
|
||||
? `${s.padding[0]}px ${s.padding[1]}px ${s.padding[2]}px ${s.padding[3]}px`
|
||||
: '14px 0',
|
||||
borderRadius: Array.isArray(s.radius)
|
||||
? `${s.radius[0]}px ${s.radius[1]}px ${s.radius[2]}px ${s.radius[3]}px`
|
||||
: '0'
|
||||
};
|
||||
});
|
||||
|
||||
const sampleGoods = computed(() => {
|
||||
if (Array.isArray(props.options.goods) && props.options.goods.length > 0) {
|
||||
return props.options.goods;
|
||||
}
|
||||
const count = props.options.goodsCount || (actualColumns.value * 2) || 12;
|
||||
return defaultMockGoods.slice(0, count);
|
||||
});
|
||||
|
||||
function addToCart(item) {
|
||||
ElMessage.success(`已将 "${item.goodsName?.slice(0, 10)}..." 加入购物车`);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.decor-goods-list {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
.floor-title-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 14px;
|
||||
padding: 0 4px;
|
||||
|
||||
.title-left {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 10px;
|
||||
|
||||
.main-title {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
font-size: 13px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
|
||||
.more-link {
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #e11d48;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.goods-flow-grid {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
|
||||
.goods-flow-card {
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #f1f5f9;
|
||||
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 {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 1 / 1;
|
||||
background: #f8fafc;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.thumb-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.self-tag {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
left: 6px;
|
||||
background: #e11d48;
|
||||
color: #ffffff;
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
padding: 1px 5px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
|
||||
.goods-name {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
height: 34px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
margin: 0 0 4px 0;
|
||||
|
||||
a {
|
||||
color: #1e293b;
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
color: #e11d48;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.meta-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 10px;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.price-row {
|
||||
margin-top: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.price-box {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
||||
.symbol {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #e11d48;
|
||||
}
|
||||
|
||||
.price-val {
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
color: #e11d48;
|
||||
}
|
||||
|
||||
.orig-price {
|
||||
font-size: 10px;
|
||||
color: #94a3b8;
|
||||
text-decoration: line-through;
|
||||
margin-left: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.add-cart-circle {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: #f1f5f9;
|
||||
color: #64748b;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
553
buyer/src/components/indexDecorate/components/GridFloor.vue
Normal file
@@ -0,0 +1,553 @@
|
||||
<template>
|
||||
<div class="decor-grid-floor" :style="customStyle">
|
||||
<!-- 楼层大标题栏 (Floor Title Bar) -->
|
||||
<div class="floor-title-header" v-if="options.showFloorTitle !== false">
|
||||
<div class="floor-main-heading">
|
||||
<span class="floor-badge">{{ options.floorNum || '1F' }}</span>
|
||||
<h3 class="floor-title-text">{{ options.title || '潮流数码' }}</h3>
|
||||
<span class="floor-subtitle-text" v-if="options.subtitle">{{ options.subtitle }}</span>
|
||||
</div>
|
||||
|
||||
<div class="floor-header-right">
|
||||
<!-- 子分类快捷标签 -->
|
||||
<div class="sub-category-tags" v-if="subCategories.length">
|
||||
<router-link
|
||||
v-for="(tag, idx) in subCategories"
|
||||
:key="idx"
|
||||
to="/goods"
|
||||
class="tag-item"
|
||||
:class="{ active: activeTagIdx === idx }"
|
||||
@click.prevent="activeTagIdx = idx"
|
||||
>
|
||||
{{ tag }}
|
||||
</router-link>
|
||||
</div>
|
||||
<router-link :to="options.moreUrl || '/goods'" class="more-link">
|
||||
查看更多 >
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 楼层主体:左侧竖向营销海报 + 右侧商品网格 -->
|
||||
<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">
|
||||
<img :src="posterImg" alt="floor poster" class="poster-img" />
|
||||
<div class="poster-overlay">
|
||||
<div class="poster-top-badge">{{ options.floorNum || '1F' }} 甄选</div>
|
||||
<h4 class="poster-headline">{{ options.posterTitle || '数码潮电 酷玩季' }}</h4>
|
||||
<p class="poster-subline">{{ options.posterSub || '领券最高立减 600 元' }}</p>
|
||||
<div class="poster-pill-tags" v-if="posterTags.length">
|
||||
<span v-for="(pt, pIdx) in posterTags" :key="pIdx" class="pill-tag">{{ pt }}</span>
|
||||
</div>
|
||||
<div class="poster-action-btn">
|
||||
<span>立即抢购 ></span>
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- 右侧商品网格 (2行 x N列) -->
|
||||
<div
|
||||
class="floor-right-goods"
|
||||
:class="`cols-${rightColumns}`"
|
||||
:style="{ gridTemplateColumns: `repeat(${rightColumns}, 1fr)` }"
|
||||
>
|
||||
<div
|
||||
v-for="(g, idx) in displayGoods"
|
||||
:key="g.id || idx"
|
||||
class="floor-goods-card"
|
||||
>
|
||||
<router-link :to="`/goods/${g.id || 1}`" class="card-thumb-wrap">
|
||||
<img :src="g.thumbnail || defaultThumb" :alt="g.goodsName" class="goods-img" />
|
||||
<span class="corner-badge" v-if="idx < 3">HOT</span>
|
||||
</router-link>
|
||||
|
||||
<div class="card-detail">
|
||||
<h4 class="goods-name-line">
|
||||
<router-link :to="`/goods/${g.id || 1}`">{{ g.goodsName }}</router-link>
|
||||
</h4>
|
||||
<div class="goods-tag-row">
|
||||
<span class="mini-tag">官方直营</span>
|
||||
<span class="mini-tag light">免息</span>
|
||||
</div>
|
||||
<div class="price-action-row">
|
||||
<div class="price-group">
|
||||
<span class="currency">¥</span>
|
||||
<span class="amount">{{ g.price }}</span>
|
||||
<span class="orig-amount" v-if="g.originalPrice">¥{{ g.originalPrice }}</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="buy-action-btn"
|
||||
title="加入购物车"
|
||||
@click="addToCart(g)"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
|
||||
<path d="M11 9h2V6h3V4h-3V1h-2v3H8v2h3v3zm-4 9c-1.1 0-1.99.9-1.99 2S5.9 22 7 22s2-.9 2-2-.9-2-2-2zm10 0c-1.1 0-1.99.9-1.99 2s.89 2 1.99 2 2-.9 2-2-.9-2-2-2zm-9.83-3.25l.03-.12.9-1.63h7.45c.75 0 1.41-.41 1.75-1.03l3.86-7.01L19.42 4h-.01l-1.1 2-2.76 5H8.53l-.13-.27L6.16 6l-.95-2-.94-2H1v2h2l3.6 7.59-1.35 2.45c-.16.28-.25.61-.25.96 0 1.1.9 2 2 2h12v-2H7.42c-.13 0-.25-.11-.25-.25z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
componentStyle: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const defaultThumb = '/static/goods/goods_1.png';
|
||||
const activeTagIdx = ref(0);
|
||||
|
||||
const subCategories = computed(() => {
|
||||
if (Array.isArray(props.options?.subCategories) && props.options.subCategories.length > 0) {
|
||||
return props.options.subCategories;
|
||||
}
|
||||
return ['旗舰手机', '轻薄笔电', '降噪耳机', '智能穿戴', '数码配件'];
|
||||
});
|
||||
|
||||
const posterTags = computed(() => {
|
||||
if (Array.isArray(props.options?.posterTags) && props.options.posterTags.length > 0) {
|
||||
return props.options.posterTags;
|
||||
}
|
||||
return ['爆款低至5折', '新品首发', '分期免息'];
|
||||
});
|
||||
|
||||
const posterImg = computed(() => {
|
||||
return props.options?.posterImg || '/static/banner-floor/floor_banner_1.png';
|
||||
});
|
||||
|
||||
const showPoster = computed(() => {
|
||||
const ratio = props.componentStyle?.widthRatio || '100%';
|
||||
if (ratio === '16.67%' || ratio === '16.6%' || ratio === '33.33%' || ratio === '33.3%') {
|
||||
return false;
|
||||
}
|
||||
return props.options?.showPoster !== false;
|
||||
});
|
||||
|
||||
const rightColumns = computed(() => {
|
||||
const ratio = props.componentStyle?.widthRatio || '100%';
|
||||
let baseCols = Number(props.options?.columns) || 4;
|
||||
if (ratio === '16.67%' || ratio === '16.6%') return 1;
|
||||
if (ratio === '33.33%' || ratio === '33.3%') return 2;
|
||||
if (ratio === '50%') return showPoster.value ? 2 : 3;
|
||||
if (ratio === '66.67%' || ratio === '66.6%') return showPoster.value ? 3 : 4;
|
||||
return baseCols;
|
||||
});
|
||||
|
||||
const defaultFloorGoods = [
|
||||
{ id: '101', goodsName: '旗舰 5G 智能手机 12GB+256GB 超光影影像系统', price: '3999.00', originalPrice: '4599.00', thumbnail: '/static/goods/goods_1.png' },
|
||||
{ id: '102', goodsName: '无线头戴式深度主动降噪蓝牙耳机 空间音频Hi-Res', price: '1099.00', originalPrice: '1499.00', thumbnail: '/static/goods/goods_2.png' },
|
||||
{ id: '103', goodsName: '14英寸 全面屏轻薄商务笔记本电脑 酷睿i7 高刷防眩光', price: '5699.00', originalPrice: '6299.00', thumbnail: '/static/goods/goods_3.png' },
|
||||
{ id: '104', goodsName: '运动健康专业监测智能手表 心率血氧长续航50米防水', price: '899.00', originalPrice: '1199.00', thumbnail: '/static/goods/goods_4.png' },
|
||||
{ id: '105', goodsName: '奢宠酵母焕活修护精华露 50ml 紧致淡纹抗皱滋养', price: '680.00', originalPrice: '890.00', thumbnail: '/static/goods/goods_5.png' },
|
||||
{ id: '106', goodsName: '意式半自动家用复古咖啡机 20Bar高压恒温萃取蒸汽奶泡', price: '899.00', originalPrice: '1280.00', thumbnail: '/static/goods/goods_6.png' },
|
||||
{ id: '107', goodsName: '碳板专业竞速轻量缓震跑步鞋 透气防滑男女同款潮鞋', price: '459.00', originalPrice: '699.00', thumbnail: '/static/goods/goods_7.png' },
|
||||
{ id: '108', goodsName: '极简静音无雾负离子香薰加湿器 大容量客厅卧室通用', price: '299.00', originalPrice: '399.00', thumbnail: '/static/goods/goods_8.png' }
|
||||
];
|
||||
|
||||
const displayGoods = computed(() => {
|
||||
if (Array.isArray(props.options?.goods) && props.options.goods.length > 0) {
|
||||
return props.options.goods;
|
||||
}
|
||||
const count = (rightColumns.value * 2) || 8;
|
||||
return defaultFloorGoods.slice(0, count);
|
||||
});
|
||||
|
||||
const customStyle = computed(() => {
|
||||
const s = props.componentStyle || {};
|
||||
return {
|
||||
backgroundColor: s.backgroundColor || '#ffffff',
|
||||
padding: Array.isArray(s.padding)
|
||||
? `${s.padding[0]}px ${s.padding[1]}px ${s.padding[2]}px ${s.padding[3]}px`
|
||||
: '16px 20px',
|
||||
borderRadius: Array.isArray(s.radius)
|
||||
? `${s.radius[0]}px ${s.radius[1]}px ${s.radius[2]}px ${s.radius[3]}px`
|
||||
: '12px'
|
||||
};
|
||||
});
|
||||
|
||||
function addToCart(item) {
|
||||
ElMessage.success(`已将 "${item.goodsName?.slice(0, 10)}..." 加入购物车`);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.decor-grid-floor {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
.floor-title-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 14px;
|
||||
border-bottom: 2px solid #f1f5f9;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.floor-main-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
.floor-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #2563eb, #1d4ed8);
|
||||
color: #ffffff;
|
||||
font-weight: 800;
|
||||
font-size: 13px;
|
||||
padding: 3px 10px;
|
||||
border-radius: 4px;
|
||||
letter-spacing: 0.5px;
|
||||
box-shadow: 0 2px 6px rgba(37, 99, 235, 0.25);
|
||||
}
|
||||
|
||||
.floor-title-text {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
letter-spacing: -0.2px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.floor-subtitle-text {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #94a3b8;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.floor-header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.sub-category-tags {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.tag-item {
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
padding: 4px 10px;
|
||||
border-radius: 14px;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover, &.active {
|
||||
color: #2563eb;
|
||||
background: #eff6ff;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.more-link {
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
transition: color 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #2563eb;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.floor-main-body {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: stretch;
|
||||
|
||||
&.no-poster {
|
||||
.floor-right-goods {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.floor-left-poster {
|
||||
width: 240px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: #1e293b;
|
||||
|
||||
.poster-link {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 520px;
|
||||
text-decoration: none;
|
||||
|
||||
.poster-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
filter: brightness(0.85);
|
||||
transition: transform 0.4s ease;
|
||||
}
|
||||
|
||||
&:hover .poster-img {
|
||||
transform: scale(1.03);
|
||||
}
|
||||
|
||||
.poster-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
padding: 24px 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: linear-gradient(180deg, rgba(15, 23, 42, 0.75) 0%, rgba(15, 23, 42, 0.2) 40%, rgba(15, 23, 42, 0.85) 100%);
|
||||
color: #ffffff;
|
||||
|
||||
.poster-top-badge {
|
||||
align-self: flex-start;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
backdrop-filter: blur(4px);
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.poster-headline {
|
||||
font-size: 22px;
|
||||
font-weight: 800;
|
||||
line-height: 1.3;
|
||||
margin: 0 0 6px 0;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.poster-subline {
|
||||
font-size: 13px;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
margin: 0 0 16px 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.poster-pill-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-bottom: auto;
|
||||
|
||||
.pill-tag {
|
||||
font-size: 11px;
|
||||
padding: 3px 8px;
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
border-radius: 10px;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
}
|
||||
|
||||
.poster-action-btn {
|
||||
align-self: flex-start;
|
||||
margin-top: auto;
|
||||
background: #ffffff;
|
||||
color: #0f172a;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #2563eb;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.floor-right-goods {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
|
||||
.floor-goods-card {
|
||||
background: #ffffff;
|
||||
border: 1px solid #f1f5f9;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: all 0.25s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: #cbd5e1;
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.06);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.card-thumb-wrap {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 1 / 1;
|
||||
background: #f8fafc;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.goods-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
&:hover .goods-img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.corner-badge {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
left: 6px;
|
||||
background: linear-gradient(135deg, #ef4444, #dc2626);
|
||||
color: #ffffff;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
|
||||
.goods-name-line {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
height: 36px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
margin: 0 0 6px 0;
|
||||
|
||||
a {
|
||||
color: #1e293b;
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
color: #2563eb;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.goods-tag-row {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.mini-tag {
|
||||
font-size: 10px;
|
||||
padding: 1px 5px;
|
||||
border-radius: 3px;
|
||||
background: #fee2e2;
|
||||
color: #dc2626;
|
||||
|
||||
&.light {
|
||||
background: #eff6ff;
|
||||
color: #2563eb;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.price-action-row {
|
||||
margin-top: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.price-group {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 2px;
|
||||
|
||||
.currency {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #e11d48;
|
||||
}
|
||||
|
||||
.amount {
|
||||
font-size: 17px;
|
||||
font-weight: 800;
|
||||
color: #e11d48;
|
||||
}
|
||||
|
||||
.orig-amount {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
text-decoration: line-through;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.buy-action-btn {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: #f1f5f9;
|
||||
color: #475569;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #2563eb;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
101
buyer/src/components/indexDecorate/components/HotZone.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<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
|
||||
v-for="(zone, idx) in displayZones"
|
||||
:key="idx"
|
||||
:to="zone.url || '/'"
|
||||
class="zone-link"
|
||||
:style="{
|
||||
top: `${zone.top || 0}%`,
|
||||
left: `${zone.left || 0}%`,
|
||||
width: `${zone.width || 10}%`,
|
||||
height: `${zone.height || 10}%`
|
||||
}"
|
||||
:title="zone.title"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
componentStyle: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const defaultZones = [
|
||||
{ title: '大促主会场 · 领券中心', url: '/couponCenter', left: 3.5, top: 12, width: 28.5, height: 76 },
|
||||
{ title: '数码潮电馆 · 爆款直降', url: '/goodsList?category=1', left: 35.5, top: 12, width: 28.5, height: 76 },
|
||||
{ title: '美妆奢品馆 · 5折特惠', url: '/goodsList?category=2', left: 67.5, top: 12, width: 29, height: 76 }
|
||||
];
|
||||
|
||||
const displayImageUrl = computed(() => {
|
||||
return props.options?.imageUrl || '/static/hot-zone/hotzone_default.png';
|
||||
});
|
||||
|
||||
const displayZones = computed(() => {
|
||||
if (Array.isArray(props.options?.zones) && props.options.zones.length > 0) {
|
||||
return props.options.zones;
|
||||
}
|
||||
return defaultZones;
|
||||
});
|
||||
|
||||
const customStyle = computed(() => {
|
||||
const s = props.componentStyle || {};
|
||||
return {
|
||||
backgroundColor: s.backgroundColor || 'transparent',
|
||||
padding: Array.isArray(s.padding)
|
||||
? `${s.padding[0]}px ${s.padding[1]}px ${s.padding[2]}px ${s.padding[3]}px`
|
||||
: '10px 0',
|
||||
borderRadius: Array.isArray(s.radius)
|
||||
? `${s.radius[0]}px ${s.radius[1]}px ${s.radius[2]}px ${s.radius[3]}px`
|
||||
: '8px',
|
||||
overflow: 'hidden'
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.decor-hot-zone {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
.hot-zone-wrap {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 1600px;
|
||||
margin: 0 auto;
|
||||
border-radius: inherit;
|
||||
overflow: hidden;
|
||||
|
||||
.base-image {
|
||||
width: 100%;
|
||||
display: block;
|
||||
border-radius: inherit;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.zone-link {
|
||||
position: absolute;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
transition: background 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
313
buyer/src/components/indexDecorate/components/NoticeBar.vue
Normal file
@@ -0,0 +1,313 @@
|
||||
<template>
|
||||
<div
|
||||
class="decor-notice-bar"
|
||||
:class="[
|
||||
isMini ? 'is-mini' : (isCompact ? 'is-compact' : '')
|
||||
]"
|
||||
:style="customStyle"
|
||||
@mouseenter="stopTimer"
|
||||
@mouseleave="startTimer"
|
||||
>
|
||||
<!-- 左侧快讯徽章 -->
|
||||
<div class="notice-badge-box">
|
||||
<div 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>
|
||||
</div>
|
||||
<span class="badge-title">{{ options.title || '公告快讯' }}</span>
|
||||
<span class="badge-divider"></span>
|
||||
</div>
|
||||
|
||||
<!-- 中间动态滚动快讯区 (Vertical Carousel) -->
|
||||
<div class="notice-content-viewport">
|
||||
<div
|
||||
class="notice-items-strip"
|
||||
:style="{ transform: `translateY(-${currentIndex * 40}px)` }"
|
||||
>
|
||||
<div
|
||||
v-for="(item, idx) in noticeList"
|
||||
:key="idx"
|
||||
class="notice-strip-item"
|
||||
>
|
||||
<span class="notice-item-tag" :class="getTagClass(item.tag)">
|
||||
{{ item.tag || '最新' }}
|
||||
</span>
|
||||
<router-link :to="item.url || item.href || '/article'" class="notice-item-link">
|
||||
{{ item.title || item.text }}
|
||||
</router-link>
|
||||
<span class="notice-item-time" v-if="item.time && !isMini && !isCompact">
|
||||
{{ item.time }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧更多链接 -->
|
||||
<div class="notice-more-action" v-if="options.showMore !== false && !isMini">
|
||||
<router-link :to="options.moreUrl || '/article'" class="more-link">
|
||||
{{ options.moreText || '更多' }} >
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
componentStyle: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const isMini = computed(() => {
|
||||
const r = props.componentStyle?.widthRatio;
|
||||
return r === '16.67%' || r === '16.6%' || r === '16.7%';
|
||||
});
|
||||
|
||||
const isCompact = computed(() => {
|
||||
const r = props.componentStyle?.widthRatio;
|
||||
return r === '33.33%' || r === '33.3%';
|
||||
});
|
||||
|
||||
const defaultItems = [
|
||||
{ tag: '最新', title: '商城秋季大促重磅开启,全场满300立减50元!', time: '10-28', url: '/article' },
|
||||
{ tag: '活动', title: '会员专享积分商城全新上线,精选好礼0元兑换', time: '10-27', url: '/article' },
|
||||
{ tag: '通知', title: '关于商城系统例行优化维护及物流发货保障通知', time: '10-26', url: '/article' },
|
||||
{ tag: '热议', title: '品质生活馆场景专区火热来袭,领券最高减100', time: '10-25', url: '/article' }
|
||||
];
|
||||
|
||||
const noticeList = computed(() => {
|
||||
const list = props.options?.items || props.options?.notices;
|
||||
if (Array.isArray(list) && list.length > 0) {
|
||||
return list;
|
||||
}
|
||||
return defaultItems;
|
||||
});
|
||||
|
||||
const currentIndex = ref(0);
|
||||
let timer = null;
|
||||
|
||||
function startTimer() {
|
||||
if (timer) clearInterval(timer);
|
||||
if (noticeList.value.length <= 1) return;
|
||||
timer = setInterval(() => {
|
||||
currentIndex.value = (currentIndex.value + 1) % noticeList.value.length;
|
||||
}, 3500);
|
||||
}
|
||||
|
||||
function stopTimer() {
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
startTimer();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
stopTimer();
|
||||
});
|
||||
|
||||
function getTagClass(tag) {
|
||||
if (tag === '活动') return 'tag-promo';
|
||||
if (tag === '通知') return 'tag-notice';
|
||||
if (tag === '热议') return 'tag-hot';
|
||||
return 'tag-new';
|
||||
}
|
||||
|
||||
const customStyle = computed(() => {
|
||||
const s = props.componentStyle || {};
|
||||
return {
|
||||
backgroundColor: s.backgroundColor || '#ffffff',
|
||||
padding: Array.isArray(s.padding)
|
||||
? `${s.padding[0]}px ${s.padding[1]}px ${s.padding[2]}px ${s.padding[3]}px`
|
||||
: '0 16px',
|
||||
borderRadius: Array.isArray(s.radius)
|
||||
? `${s.radius[0]}px ${s.radius[1]}px ${s.radius[2]}px ${s.radius[3]}px`
|
||||
: '8px'
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.decor-notice-bar {
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #eef2f6;
|
||||
gap: 12px;
|
||||
|
||||
.notice-badge-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.badge-icon {
|
||||
color: #ef4444;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.badge-title {
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
color: #0f172a;
|
||||
white-space: nowrap;
|
||||
letter-spacing: -0.2px;
|
||||
}
|
||||
|
||||
.badge-divider {
|
||||
width: 1px;
|
||||
height: 14px;
|
||||
background: #e2e8f0;
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.notice-content-viewport {
|
||||
flex: 1;
|
||||
height: 40px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
|
||||
.notice-items-strip {
|
||||
transition: transform 0.4s ease-in-out;
|
||||
|
||||
.notice-strip-item {
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
|
||||
.notice-item-tag {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.tag-new {
|
||||
background: #fee2e2;
|
||||
color: #dc2626;
|
||||
}
|
||||
&.tag-promo {
|
||||
background: #ffedd5;
|
||||
color: #ea580c;
|
||||
}
|
||||
&.tag-notice {
|
||||
background: #eff6ff;
|
||||
color: #2563eb;
|
||||
}
|
||||
&.tag-hot {
|
||||
background: #fdf2f8;
|
||||
color: #db2777;
|
||||
}
|
||||
}
|
||||
|
||||
.notice-item-link {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
color: #334155;
|
||||
text-decoration: none;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
min-width: 0;
|
||||
transition: color 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #ef4444;
|
||||
}
|
||||
}
|
||||
|
||||
.notice-item-time {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notice-more-action {
|
||||
flex-shrink: 0;
|
||||
|
||||
.more-link {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
text-decoration: none;
|
||||
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-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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
140
buyer/src/components/indexDecorate/components/PromoGroup.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<div class="decor-promo-group" :style="customStyle">
|
||||
<div
|
||||
class="promo-grid"
|
||||
:style="{ gridTemplateColumns: `repeat(${actualColumns}, 1fr)` }"
|
||||
>
|
||||
<router-link
|
||||
v-for="(card, idx) in cardsList"
|
||||
:key="idx"
|
||||
:to="card.url || '/goods'"
|
||||
class="promo-card"
|
||||
:style="card.bgImg ? { backgroundImage: `url(${card.bgImg})` } : {}"
|
||||
>
|
||||
<div class="card-inner">
|
||||
<h3 class="card-title">{{ card.title }}</h3>
|
||||
<p class="card-sub">{{ card.subtitle }}</p>
|
||||
<span class="action-btn">立即查看 ></span>
|
||||
</div>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
componentStyle: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const defaultCards = [
|
||||
{ title: '限时秒杀', subtitle: '抢券立减50元 · 爆款直降', bgImg: '/static/promo-group/promo_1.png', url: '/goodsList?seckill=1' },
|
||||
{ title: '品牌特惠', subtitle: '大牌买一送一 · 领百元神券', bgImg: '/static/promo-group/promo_2.png', url: '/goodsList?brand=1' },
|
||||
{ title: '新品首发', subtitle: '首单专享特惠 · 1元起尝鲜', bgImg: '/static/promo-group/promo_3.png', url: '/goodsList?new=1' }
|
||||
];
|
||||
|
||||
const cardsList = computed(() => {
|
||||
if (Array.isArray(props.options?.cards) && props.options.cards.length > 0) {
|
||||
return props.options.cards;
|
||||
}
|
||||
return defaultCards;
|
||||
});
|
||||
|
||||
const actualColumns = computed(() => {
|
||||
const baseCols = Number(props.options?.columns) || 3;
|
||||
const ratio = props.componentStyle?.widthRatio || '100%';
|
||||
if (ratio === '16.67%' || ratio === '16.6%' || ratio === '16.7%') {
|
||||
return 1;
|
||||
}
|
||||
if (ratio === '33.33%' || ratio === '33.3%') {
|
||||
return 1;
|
||||
}
|
||||
if (ratio === '50%') {
|
||||
return Math.min(baseCols, 2);
|
||||
}
|
||||
if (ratio === '66.67%' || ratio === '66.6%' || ratio === '66.7%') {
|
||||
return Math.min(baseCols, 2);
|
||||
}
|
||||
return baseCols;
|
||||
});
|
||||
|
||||
const customStyle = computed(() => {
|
||||
const s = props.componentStyle || {};
|
||||
return {
|
||||
backgroundColor: s.backgroundColor || 'transparent',
|
||||
padding: Array.isArray(s.padding)
|
||||
? `${s.padding[0]}px ${s.padding[1]}px ${s.padding[2]}px ${s.padding[3]}px`
|
||||
: '10px 0'
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.decor-promo-group {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
.promo-grid {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
|
||||
.promo-card {
|
||||
min-height: 160px;
|
||||
padding: 24px;
|
||||
background-color: #1e293b;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
border-radius: 12px;
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
color: #ffffff;
|
||||
transition: all 0.25s ease;
|
||||
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08);
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.card-inner {
|
||||
.card-title {
|
||||
font-size: 22px;
|
||||
font-weight: 800;
|
||||
color: #ffffff;
|
||||
margin: 0;
|
||||
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.card-sub {
|
||||
font-size: 13px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin: 6px 0 14px 0;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
backdrop-filter: blur(4px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.4);
|
||||
border-radius: 14px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
572
buyer/src/components/indexDecorate/components/RecommendFeed.vue
Normal file
@@ -0,0 +1,572 @@
|
||||
<template>
|
||||
<div class="decor-recommend-feed" :style="customStyle">
|
||||
<!-- 1. 推荐流居中标题栏 -->
|
||||
<div class="recommend-feed-header">
|
||||
<div class="header-decor-line left"></div>
|
||||
<div class="header-center-title">
|
||||
<svg class="heart-icon" viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
|
||||
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/>
|
||||
</svg>
|
||||
<h3 class="main-title">{{ options.title || '猜你喜欢' }}</h3>
|
||||
<span class="sub-title" v-if="options.subtitle">{{ options.subtitle }}</span>
|
||||
</div>
|
||||
<div class="header-decor-line right"></div>
|
||||
</div>
|
||||
|
||||
<!-- 2. 推荐流布局:精选大卡+网格 (featured) 或 标准网格 (grid) -->
|
||||
<!-- A. 精选大卡+网格 (featured 布局) -->
|
||||
<div
|
||||
v-if="(options.layoutStyle || 'featured') === 'featured'"
|
||||
class="recommend-featured-layout"
|
||||
>
|
||||
<!-- 左侧精选特大卡片 (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">
|
||||
<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>
|
||||
</h4>
|
||||
<div class="big-reason">99.8% 高好评 · 猜你对这款高人气科技数码感兴趣</div>
|
||||
<div class="big-price-row">
|
||||
<div class="big-price">
|
||||
<span class="sym">¥</span>
|
||||
<span class="val">{{ featuredGoods.price }}</span>
|
||||
<span class="del" v-if="featuredGoods.originalPrice">¥{{ featuredGoods.originalPrice }}</span>
|
||||
</div>
|
||||
<button type="button" class="big-buy-btn" @click="addToCart(featuredGoods)">立即入手 ></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧与后续标准卡片网格 -->
|
||||
<div class="featured-sub-grid">
|
||||
<div
|
||||
v-for="(item, idx) in regularGoods"
|
||||
:key="item.id || idx"
|
||||
class="recommend-card"
|
||||
>
|
||||
<router-link :to="`/goods/${item.id || 1}`" 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>
|
||||
</h4>
|
||||
<div class="recommend-meta">
|
||||
<span class="reason-tag">同品类热度榜</span>
|
||||
</div>
|
||||
<div class="price-row">
|
||||
<div class="price-box">
|
||||
<span class="sym">¥</span>
|
||||
<span class="val">{{ item.price }}</span>
|
||||
</div>
|
||||
<button type="button" class="cart-pill-btn" @click="addToCart(item)">+</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- B. 标准网格流 (grid 布局) -->
|
||||
<div
|
||||
v-else
|
||||
class="recommend-standard-grid"
|
||||
:class="`cols-${actualCols}`"
|
||||
:style="{ gridTemplateColumns: `repeat(${actualCols}, 1fr)` }"
|
||||
>
|
||||
<div
|
||||
v-for="(item, idx) in displayGoods"
|
||||
:key="item.id || idx"
|
||||
class="recommend-card"
|
||||
>
|
||||
<router-link :to="`/goods/${item.id || 1}`" 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>
|
||||
</h4>
|
||||
<div class="recommend-meta">
|
||||
<span class="sales-num">已售 {{ 200 + idx * 70 }}+</span>
|
||||
<span class="rate-num">98%好评</span>
|
||||
</div>
|
||||
<div class="price-row">
|
||||
<div class="price-box">
|
||||
<span class="sym">¥</span>
|
||||
<span class="val">{{ item.price }}</span>
|
||||
</div>
|
||||
<button type="button" class="cart-pill-btn" @click="addToCart(item)">+</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部换一批 / 探索更多推荐 -->
|
||||
<div class="recommend-footer-action">
|
||||
<button type="button" class="refresh-rec-btn" @click="handleRefresh">
|
||||
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
|
||||
<path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/>
|
||||
</svg>
|
||||
<span>换一批推荐好物</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
componentStyle: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const defaultThumb = '/static/goods/goods_1.png';
|
||||
|
||||
const defaultPool = [
|
||||
{ id: '1', goodsName: '旗舰 5G 智能手机 12GB+256GB 超光影影像系统', price: '3999.00', originalPrice: '4599.00', thumbnail: '/static/goods/goods_1.png' },
|
||||
{ id: '2', goodsName: '无线头戴式深度主动降噪蓝牙耳机 空间音频Hi-Res', price: '1099.00', originalPrice: '1499.00', thumbnail: '/static/goods/goods_2.png' },
|
||||
{ id: '3', goodsName: '14英寸 全面屏轻薄商务笔记本电脑 酷睿i7 高刷防眩光', price: '5699.00', originalPrice: '6299.00', thumbnail: '/static/goods/goods_3.png' },
|
||||
{ id: '4', goodsName: '运动健康专业监测智能手表 心率血氧长续航50米防水', price: '899.00', originalPrice: '1199.00', thumbnail: '/static/goods/goods_4.png' },
|
||||
{ id: '5', goodsName: '奢宠酵母焕活修护精华露 50ml 紧致淡纹抗皱滋养', price: '680.00', originalPrice: '890.00', thumbnail: '/static/goods/goods_5.png' },
|
||||
{ id: '6', goodsName: '意式半自动家用复古咖啡机 20Bar高压恒温萃取蒸汽奶泡', price: '899.00', originalPrice: '1280.00', thumbnail: '/static/goods/goods_6.png' },
|
||||
{ id: '7', goodsName: '碳板专业竞速轻量缓震跑步鞋 透气防滑男女同款潮鞋', price: '459.00', originalPrice: '699.00', thumbnail: '/static/goods/goods_7.png' },
|
||||
{ id: '8', goodsName: '极简静音无雾负离子香薰加湿器 大容量客厅卧室通用', price: '299.00', originalPrice: '399.00', thumbnail: '/static/goods/goods_8.png' },
|
||||
{ id: '9', goodsName: '智能降噪AI翻译蓝牙墨镜 超轻防紫外线偏光太阳镜', price: '599.00', originalPrice: '799.00', thumbnail: '/static/goods/goods_1.png' }
|
||||
];
|
||||
|
||||
const displayGoods = computed(() => {
|
||||
if (Array.isArray(props.options?.goods) && props.options.goods.length > 0) {
|
||||
return props.options.goods;
|
||||
}
|
||||
const count = props.options?.pageSize || 8;
|
||||
return defaultPool.slice(0, count);
|
||||
});
|
||||
|
||||
const featuredGoods = computed(() => {
|
||||
return displayGoods.value[0] || defaultPool[0];
|
||||
});
|
||||
|
||||
const regularGoods = computed(() => {
|
||||
return displayGoods.value.slice(1, 7);
|
||||
});
|
||||
|
||||
const actualCols = computed(() => {
|
||||
const ratio = props.componentStyle?.widthRatio || '100%';
|
||||
if (ratio === '16.67%' || ratio === '16.6%') return 1;
|
||||
if (ratio === '33.33%' || ratio === '33.3%') return 2;
|
||||
if (ratio === '50%') return 3;
|
||||
if (ratio === '66.67%' || ratio === '66.6%') return 4;
|
||||
return 5;
|
||||
});
|
||||
|
||||
const customStyle = computed(() => {
|
||||
const s = props.componentStyle || {};
|
||||
const bg = props.options?.backgroundColor || s.backgroundColor || '#f8fafc';
|
||||
return {
|
||||
backgroundColor: bg,
|
||||
padding: Array.isArray(s.padding)
|
||||
? `${s.padding[0]}px ${s.padding[1]}px ${s.padding[2]}px ${s.padding[3]}px`
|
||||
: '24px 20px',
|
||||
borderRadius: Array.isArray(s.radius)
|
||||
? `${s.radius[0]}px ${s.radius[1]}px ${s.radius[2]}px ${s.radius[3]}px`
|
||||
: '12px'
|
||||
};
|
||||
});
|
||||
|
||||
function addToCart(item) {
|
||||
ElMessage.success(`已将 "${item.goodsName?.slice(0, 10)}..." 加入购物车`);
|
||||
}
|
||||
|
||||
function handleRefresh() {
|
||||
ElMessage.info('已刷新为您推荐的专属商品');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.decor-recommend-feed {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
.recommend-feed-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
gap: 16px;
|
||||
|
||||
.header-decor-line {
|
||||
flex: 1;
|
||||
max-width: 120px;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent, #cbd5e1);
|
||||
|
||||
&.right {
|
||||
background: linear-gradient(90deg, #cbd5e1, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
.header-center-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.heart-icon {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
font-size: 22px;
|
||||
font-weight: 800;
|
||||
color: #0f172a;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.recommend-featured-layout {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
align-items: stretch;
|
||||
|
||||
.featured-big-card {
|
||||
width: 320px;
|
||||
flex-shrink: 0;
|
||||
background: #ffffff;
|
||||
border-radius: 10px;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #f1f5f9;
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.big-card-badge {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
left: 12px;
|
||||
background: linear-gradient(135deg, #ef4444, #f97316);
|
||||
color: #ffffff;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.big-card-thumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
aspect-ratio: 1 / 1;
|
||||
overflow: hidden;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 12px;
|
||||
background: #f8fafc;
|
||||
text-decoration: none;
|
||||
|
||||
img {
|
||||
width: 85%;
|
||||
height: 85%;
|
||||
object-fit: contain;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
&:hover img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
.big-card-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
|
||||
.big-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
margin: 0 0 6px 0;
|
||||
|
||||
a {
|
||||
color: #0f172a;
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
color: #ef4444;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.big-reason {
|
||||
font-size: 12px;
|
||||
color: #f97316;
|
||||
background: #fff7ed;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 12px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.big-price-row {
|
||||
margin-top: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.big-price {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
||||
.sym {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.val {
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.del {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
text-decoration: line-through;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.big-buy-btn {
|
||||
background: linear-gradient(135deg, #ef4444, #dc2626);
|
||||
color: #ffffff;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
padding: 6px 14px;
|
||||
border-radius: 16px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
transform: scale(1.03);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.featured-sub-grid {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.recommend-standard-grid {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.recommend-card {
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid #f1f5f9;
|
||||
transition: all 0.25s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.06);
|
||||
|
||||
.card-thumb img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.cart-pill-btn {
|
||||
background: #ef4444;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
.card-thumb {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 1 / 1;
|
||||
background: #f8fafc;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 8px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.rec-tag {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
left: 6px;
|
||||
background: #ef4444;
|
||||
color: #ffffff;
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
padding: 1px 5px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
|
||||
.goods-title {
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
height: 34px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
margin: 0 0 4px 0;
|
||||
|
||||
a {
|
||||
color: #1e293b;
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
color: #ef4444;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.recommend-meta {
|
||||
font-size: 10px;
|
||||
color: #f97316;
|
||||
margin-bottom: 6px;
|
||||
|
||||
.reason-tag {
|
||||
background: #fff7ed;
|
||||
padding: 1px 5px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.sales-num {
|
||||
color: #94a3b8;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.rate-num {
|
||||
color: #10b981;
|
||||
}
|
||||
}
|
||||
|
||||
.price-row {
|
||||
margin-top: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.price-box {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
||||
.sym {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.val {
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
color: #dc2626;
|
||||
}
|
||||
}
|
||||
|
||||
.cart-pill-btn {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: #f1f5f9;
|
||||
color: #64748b;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.recommend-footer-action {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
|
||||
.refresh-rec-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #e2e8f0;
|
||||
color: #475569;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
padding: 8px 24px;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04);
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #ef4444;
|
||||
color: #ef4444;
|
||||
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.12);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
217
buyer/src/components/indexDecorate/components/SearchHeader.vue
Normal file
@@ -0,0 +1,217 @@
|
||||
<template>
|
||||
<div class="decor-search-header" :style="customStyle">
|
||||
<div class="header-inner">
|
||||
<!-- 商城 Logo -->
|
||||
<div class="logo-area">
|
||||
<router-link to="/">
|
||||
<img
|
||||
v-if="options.logoUrl"
|
||||
:src="options.logoUrl"
|
||||
alt="Logo"
|
||||
class="site-logo"
|
||||
/>
|
||||
<span v-else class="text-logo">LiliShop</span>
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- 搜索主区域 -->
|
||||
<div class="search-main">
|
||||
<div class="search-input-box">
|
||||
<el-input
|
||||
v-model="keyword"
|
||||
size="large"
|
||||
:placeholder="options.placeholder || '搜索品牌或商品'"
|
||||
@keyup.enter="handleSearch"
|
||||
class="input-control"
|
||||
/>
|
||||
<button type="button" class="search-btn" @click="handleSearch">
|
||||
<el-icon :size="18"><Search /></el-icon>
|
||||
<span>搜索</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 热门推荐词 -->
|
||||
<div class="hot-words" v-if="options.showHotWords && options.hotWords?.length">
|
||||
<span
|
||||
v-for="(word, idx) in options.hotWords"
|
||||
:key="idx"
|
||||
class="hot-word"
|
||||
@click="quickSearch(word)"
|
||||
>
|
||||
{{ word }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 购物车入口 -->
|
||||
<div class="cart-entry" v-if="options.showCart">
|
||||
<router-link to="/cart" class="cart-btn">
|
||||
<el-icon :size="20" class="cart-icon"><ShoppingCart /></el-icon>
|
||||
<span>我的购物车</span>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { Search, ShoppingCart } from '@element-plus/icons-vue';
|
||||
|
||||
const props = defineProps({
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
componentStyle: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const keyword = ref('');
|
||||
|
||||
function handleSearch() {
|
||||
const kw = keyword.value.trim();
|
||||
if (kw) {
|
||||
router.push({ path: '/goodsList', query: { keyword: kw } });
|
||||
}
|
||||
}
|
||||
|
||||
function quickSearch(word) {
|
||||
keyword.value = word;
|
||||
handleSearch();
|
||||
}
|
||||
|
||||
const customStyle = computed(() => {
|
||||
const s = props.componentStyle || {};
|
||||
return {
|
||||
backgroundColor: s.backgroundColor || '#ffffff',
|
||||
minHeight: `${s.height || 90}px`
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.decor-search-header {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.header-inner {
|
||||
max-width: 1600px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 16px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 40px;
|
||||
|
||||
.logo-area {
|
||||
flex-shrink: 0;
|
||||
|
||||
.site-logo {
|
||||
max-height: 52px;
|
||||
max-width: 180px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.text-logo {
|
||||
font-size: 26px;
|
||||
font-weight: 800;
|
||||
color: #e11d48;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
}
|
||||
|
||||
.search-main {
|
||||
flex: 1;
|
||||
max-width: 600px;
|
||||
|
||||
.search-input-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
:deep(.el-input__wrapper) {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
border-color: #e11d48;
|
||||
box-shadow: 0 0 0 1px #e11d48 inset !important;
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
height: 40px;
|
||||
padding: 0 24px;
|
||||
background: #e11d48;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #be123c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hot-words {
|
||||
margin-top: 6px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
font-size: 12px;
|
||||
|
||||
.hot-word {
|
||||
color: #94a3b8;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #e11d48;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cart-entry {
|
||||
flex-shrink: 0;
|
||||
|
||||
.cart-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
height: 40px;
|
||||
padding: 0 18px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 4px;
|
||||
color: #334155;
|
||||
text-decoration: none;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
background: #f8fafc;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #e11d48;
|
||||
color: #e11d48;
|
||||
background: #fff1f2;
|
||||
}
|
||||
|
||||
.cart-icon {
|
||||
color: #e11d48;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
136
buyer/src/components/indexDecorate/components/TitleBar.vue
Normal file
@@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<div
|
||||
class="decor-title-bar"
|
||||
:class="[
|
||||
`align-${options.align || 'left'}`,
|
||||
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>
|
||||
<router-link :to="options.moreUrl || '/goods'" class="more-link" v-if="options.showMore">
|
||||
{{ options.moreText || '查看更多 >' }}
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
componentStyle: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const isMini = computed(() => props.componentStyle?.widthRatio === '16.67%');
|
||||
const isCompact = computed(() => props.componentStyle?.widthRatio === '33.33%');
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.decor-title-bar {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
box-sizing: border-box;
|
||||
gap: 12px;
|
||||
|
||||
&.align-center {
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
|
||||
.more-link {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.title-wrap {
|
||||
min-width: 0;
|
||||
|
||||
.main-title {
|
||||
font-size: 26px;
|
||||
font-weight: 800;
|
||||
color: #0f172a;
|
||||
margin: 0;
|
||||
line-height: 1.2;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 14px;
|
||||
color: #94a3b8;
|
||||
margin: 6px 0 0 0;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
.more-link {
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
color: #e11d48;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-compact {
|
||||
.title-wrap {
|
||||
.main-title {
|
||||
font-size: 20px;
|
||||
}
|
||||
.subtitle {
|
||||
font-size: 13px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
.more-link {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-mini {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
|
||||
&.align-center {
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
|
||||
.more-link {
|
||||
position: static;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.title-wrap {
|
||||
.main-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
.subtitle {
|
||||
font-size: 12px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.more-link {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
177
buyer/src/components/indexDecorate/components/TopBar.vue
Normal file
@@ -0,0 +1,177 @@
|
||||
<template>
|
||||
<div class="decor-top-bar" :style="customStyle">
|
||||
<div class="top-bar-inner">
|
||||
<div class="bar-left">
|
||||
<span class="welcome-txt">{{ options.welcomeText || '欢迎来到商城' }}</span>
|
||||
<span class="auth-links" v-if="!username">
|
||||
<router-link :to="options.loginUrl || '/login'" class="auth-item">
|
||||
[{{ options.loginText || '登录' }}]
|
||||
</router-link>
|
||||
<router-link :to="options.registerUrl || '/signUp'" class="auth-item">
|
||||
[{{ options.registerText || '免费注册' }}]
|
||||
</router-link>
|
||||
</span>
|
||||
<span class="auth-links user-logged" v-else>
|
||||
<router-link :to="options.userCenterUrl || '/home/MyOrder'" class="user-name">
|
||||
欢迎,{{ username }}
|
||||
</router-link>
|
||||
<span class="sep">|</span>
|
||||
<a class="auth-item" @click="handleLogout">{{ options.logoutText || '退出' }}</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="bar-right">
|
||||
<router-link
|
||||
v-for="(menu, idx) in (options.rightMenus || defaultMenus)"
|
||||
:key="idx"
|
||||
:to="menu.url || '/'"
|
||||
class="bar-link"
|
||||
>
|
||||
{{ menu.title }}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
const props = defineProps({
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
componentStyle: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const defaultMenus = [
|
||||
{ title: '基本信息', url: '/home/UserInfo' },
|
||||
{ title: '我的订单', url: '/home/MyOrder' },
|
||||
{ title: '账户安全', url: '/home/AccountSafe' },
|
||||
{ title: '消息', url: '/home/Msg' }
|
||||
];
|
||||
|
||||
const username = computed(() => {
|
||||
const userInfo = Cookies.get('userInfo');
|
||||
if (userInfo) {
|
||||
try {
|
||||
const u = JSON.parse(userInfo);
|
||||
return u.nickName || u.username || '';
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
function handleLogout() {
|
||||
Cookies.remove('userInfo');
|
||||
Cookies.remove('accessToken');
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
const customStyle = computed(() => {
|
||||
const s = props.componentStyle || {};
|
||||
const opt = props.options || {};
|
||||
const textColor = opt.textColor || s.textColor || '#4b5563';
|
||||
const fontSize = opt.fontSize ? `${opt.fontSize}px` : '12px';
|
||||
|
||||
return {
|
||||
backgroundColor: s.backgroundColor || 'transparent',
|
||||
color: textColor,
|
||||
fontSize: fontSize,
|
||||
height: `${s.height || 36}px`,
|
||||
lineHeight: `${s.height || 36}px`
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.decor-top-bar {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
font-size: 12px;
|
||||
overflow: hidden;
|
||||
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.top-bar-inner {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
padding: 0 24px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.bar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
white-space: nowrap;
|
||||
|
||||
.welcome-txt {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.auth-links {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
white-space: nowrap;
|
||||
|
||||
.auth-item {
|
||||
color: #ff6a00;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.sep {
|
||||
margin: 0 4px;
|
||||
color: #dcdfe6;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
color: #333333;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
color: #ff6a00;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
white-space: nowrap;
|
||||
|
||||
.bar-link {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
color: #ff6a00;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
407
buyer/src/components/indexDecorate/components/UserBenefits.vue
Normal file
@@ -0,0 +1,407 @@
|
||||
<template>
|
||||
<div class="decor-user-benefits" :style="customStyle">
|
||||
<!-- 1. 用户头像与信息区 -->
|
||||
<div class="user-main-info">
|
||||
<div class="avatar-box">
|
||||
<img v-if="options.avatar" :src="options.avatar" alt="avatar" />
|
||||
<div v-else class="avatar-placeholder">
|
||||
<svg viewBox="0 0 24 24" width="22" height="22" fill="#ffffff">
|
||||
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="user-greeting">
|
||||
<div class="badge-tag">{{ options.badgeText || '请登录' }}</div>
|
||||
<div class="welcome-text" :title="options.welcomeText || '欢迎来到商城'">
|
||||
{{ options.welcomeText || '欢迎来到商城' }}
|
||||
</div>
|
||||
<div class="sub-text" :title="options.subtitle || '登录后查看专属权益'">
|
||||
{{ options.subtitle || '登录后查看专属权益' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. 登录 / 注册按钮组 -->
|
||||
<div class="action-btn-row">
|
||||
<router-link :to="options.loginUrl || '/login'" class="action-btn login-btn">
|
||||
{{ options.loginText || '登录' }}
|
||||
</router-link>
|
||||
<router-link :to="options.registerUrl || '/signUp'" class="action-btn register-btn">
|
||||
{{ options.registerText || '注册' }}
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- 3. 指标数据卡 (购物车 / 优惠券 / 订单) -->
|
||||
<div class="metrics-cards-row">
|
||||
<router-link :to="options.cartUrl || '/cart'" class="metric-card">
|
||||
<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">
|
||||
<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">
|
||||
<div class="metric-val">0</div>
|
||||
<div class="metric-label">{{ options.orderText || '订单' }}</div>
|
||||
</router-link>
|
||||
</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. 快捷便民服务入口(底部图标排) -->
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { Star, Clock, Present, Service } from '@element-plus/icons-vue';
|
||||
|
||||
const props = defineProps({
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
componentStyle: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const customStyle = computed(() => {
|
||||
const s = props.componentStyle || {};
|
||||
return {
|
||||
backgroundColor: s.backgroundColor || '#ffffff',
|
||||
borderRadius: Array.isArray(s.radius)
|
||||
? `${s.radius[0]}px ${s.radius[1]}px ${s.radius[2]}px ${s.radius[3]}px`
|
||||
: '8px'
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.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;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
|
||||
.user-main-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.avatar-box {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.avatar-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #cbd5e1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.user-greeting {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
.badge-tag {
|
||||
align-self: flex-start;
|
||||
padding: 1px 6px;
|
||||
background: #fff2e8;
|
||||
color: #ff6000;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
border-radius: 8px;
|
||||
line-height: 1.2;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.welcome-text {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
line-height: 1.25;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.sub-text {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
line-height: 1.25;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-btn-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
border-radius: 14px;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
text-decoration: none;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&.login-btn {
|
||||
background: #ff6000;
|
||||
color: #ffffff;
|
||||
box-shadow: 0 2px 6px rgba(255, 96, 0, 0.25);
|
||||
|
||||
&:hover {
|
||||
background: #e55600;
|
||||
}
|
||||
}
|
||||
|
||||
&.register-btn {
|
||||
background: #ffffff;
|
||||
color: #ff6000;
|
||||
border: 1px solid #ff6000;
|
||||
|
||||
&:hover {
|
||||
background: #fff7ed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.metrics-cards-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
padding: 8px 0;
|
||||
margin-top: 10px;
|
||||
background: #f8fafc;
|
||||
border-radius: 6px;
|
||||
|
||||
.metric-card {
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
|
||||
.metric-val {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: 11px;
|
||||
color: #64748b;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.metric-val, .metric-label {
|
||||
color: #ff6000;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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: #ff6000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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: #ff6000;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.news-text {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #ff6000;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.quick-service-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
padding-top: 8px;
|
||||
border-top: 1px dashed #f1f5f9;
|
||||
margin-top: 4px;
|
||||
|
||||
.service-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
color: #64748b;
|
||||
transition: color 0.15s;
|
||||
|
||||
.service-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.service-name {
|
||||
font-size: 10px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #ff6000;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
114
buyer/src/components/indexDecorate/components/VideoPlayer.vue
Normal file
@@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<div class="decor-video-player" :style="customStyle">
|
||||
<video
|
||||
v-if="options.videoUrl"
|
||||
:src="options.videoUrl"
|
||||
:poster="options.coverUrl"
|
||||
:autoplay="options.autoPlay"
|
||||
:loop="options.loop"
|
||||
:controls="options.controls !== false"
|
||||
:muted="options.autoPlay"
|
||||
:style="{ height: `${options.height || 450}px` }"
|
||||
class="video-element"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="video-placeholder"
|
||||
:style="{
|
||||
height: `${options.height || 450}px`,
|
||||
backgroundImage: `url(${options.coverUrl || '/static/video/video_cover.png'})`
|
||||
}"
|
||||
>
|
||||
<div class="video-play-hint">
|
||||
<span class="play-icon">▶</span>
|
||||
<span class="hint-text">官方品牌宣传片</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
componentStyle: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const customStyle = computed(() => {
|
||||
const s = props.componentStyle || {};
|
||||
return {
|
||||
borderRadius: Array.isArray(s.radius)
|
||||
? `${s.radius[0]}px ${s.radius[1]}px ${s.radius[2]}px ${s.radius[3]}px`
|
||||
: '8px'
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.decor-video-player {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
|
||||
.video-element {
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.video-placeholder {
|
||||
width: 100%;
|
||||
background: #0f172a;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
border-radius: inherit;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #64748b;
|
||||
font-size: 14px;
|
||||
position: relative;
|
||||
|
||||
.video-play-hint {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 16px 24px;
|
||||
background: rgba(15, 23, 42, 0.7);
|
||||
border-radius: 12px;
|
||||
backdrop-filter: blur(4px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
|
||||
.play-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: #e11d48;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #ffffff;
|
||||
font-size: 20px;
|
||||
padding-left: 3px;
|
||||
box-shadow: 0 4px 14px rgba(225, 29, 72, 0.4);
|
||||
}
|
||||
|
||||
.hint-text {
|
||||
color: #ffffff;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1932
buyer/src/components/indexDecorate/presets/double11Preset.js
Normal file
73
buyer/src/components/indexDecorate/registry.js
Normal file
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* PC 前台装修楼层组件注册表
|
||||
* 映射 6 大分类、24 个组件
|
||||
*/
|
||||
import TopBar from './components/TopBar.vue';
|
||||
import SearchHeader from './components/SearchHeader.vue';
|
||||
import FooterNav from './components/FooterNav.vue';
|
||||
import CategoryNav from './components/CategoryNav.vue';
|
||||
import ChannelGrid from './components/ChannelGrid.vue';
|
||||
import CategoryMegaMenu from './components/CategoryMegaMenu.vue';
|
||||
import Carousel from './components/Carousel.vue';
|
||||
import BrandWall from './components/BrandWall.vue';
|
||||
import HotZone from './components/HotZone.vue';
|
||||
import FreeLayout from './components/FreeLayout.vue';
|
||||
import PromoGroup from './components/PromoGroup.vue';
|
||||
import VideoPlayer from './components/VideoPlayer.vue';
|
||||
import GoodsList from './components/GoodsList.vue';
|
||||
import GridFloor from './components/GridFloor.vue';
|
||||
import GoodsGrid from './components/GoodsGrid.vue';
|
||||
import ChannelFloor from './components/ChannelFloor.vue';
|
||||
import RecommendFeed from './components/RecommendFeed.vue';
|
||||
import CouponList from './components/CouponList.vue';
|
||||
import UserBenefits from './components/UserBenefits.vue';
|
||||
import TitleBar from './components/TitleBar.vue';
|
||||
import ArticleContent from './components/ArticleContent.vue';
|
||||
import NoticeBar from './components/NoticeBar.vue';
|
||||
|
||||
export const COMPONENT_MAP = {
|
||||
// 1. 结构 (3)
|
||||
topBar: TopBar,
|
||||
searchHeader: SearchHeader,
|
||||
footerNav: FooterNav,
|
||||
|
||||
// 2. 导航 (3)
|
||||
categoryNav: CategoryNav,
|
||||
channelGrid: ChannelGrid,
|
||||
categoryMegaMenu: CategoryMegaMenu,
|
||||
|
||||
// 3. 广告 (6)
|
||||
carousel: Carousel,
|
||||
brandWall: BrandWall,
|
||||
hotZone: HotZone,
|
||||
freeLayout: FreeLayout,
|
||||
promoGroup: PromoGroup,
|
||||
video: VideoPlayer,
|
||||
|
||||
// 4. 商品 (8)
|
||||
goodsList: GoodsList,
|
||||
gridFloor: GridFloor,
|
||||
goodsGrid: GoodsGrid,
|
||||
goodsCard: GoodsList,
|
||||
bannerFloor: GridFloor,
|
||||
goodsBar: GoodsList,
|
||||
channelFloor: ChannelFloor,
|
||||
recommendFeed: RecommendFeed,
|
||||
|
||||
// 5. 营销 (1)
|
||||
couponList: CouponList,
|
||||
userBenefits: UserBenefits,
|
||||
|
||||
// 6. 内容 (3)
|
||||
titleBar: TitleBar,
|
||||
articleContent: ArticleContent,
|
||||
noticeBar: NoticeBar
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据组件 type 解析 Vue 组件
|
||||
*/
|
||||
export function resolveComponent(type) {
|
||||
if (!type) return null;
|
||||
return COMPONENT_MAP[type] || null;
|
||||
}
|
||||
@@ -345,7 +345,6 @@ getGoodsList();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.goods-show-seller {
|
||||
// padding:3px 0;
|
||||
|
||||
@@ -24,31 +24,32 @@
|
||||
<!-- 固定头部 -->
|
||||
<hover-search class="hover-search" :class="{show: topSearchShow}"></hover-search>
|
||||
<!-- 顶部广告 -->
|
||||
<FixedTopPage :data="topAdvert"></FixedTopPage>
|
||||
<FixedTopPage :data="topAdvert" v-if="!isVersion2"></FixedTopPage>
|
||||
<!-- 头部 包括登录,我的订单等 -->
|
||||
<BaseHeader></BaseHeader>
|
||||
<BaseHeader v-if="!hasComponentInV2('topBar')"></BaseHeader>
|
||||
<!-- 搜索框、logo -->
|
||||
<Search></Search>
|
||||
<Search v-if="!hasComponentInV2('searchHeader')"></Search>
|
||||
<div class="index-main">
|
||||
<!-- 商品分类 + 楼层装修(同一宽度容器,保证与分类栏左缘对齐) -->
|
||||
<cateNav :showAlways="true" v-if="showNav" :large="carouselLarge" :opacity="carouselOpacity"></cateNav>
|
||||
<cateNav :showAlways="true" v-if="!isVersion2 && showNav" :large="carouselLarge" :opacity="carouselOpacity"></cateNav>
|
||||
<model-form :data="modelFormData"></model-form>
|
||||
</div>
|
||||
<!-- 底部栏 -->
|
||||
<BaseFooter></BaseFooter>
|
||||
<BaseFooter v-if="!hasComponentInV2('footerNav')"></BaseFooter>
|
||||
<!-- 侧边栏 -->
|
||||
<fixedBar class="fixed-bar" :class="{'show-fixed': topSearchShow}"></fixedBar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted, ref } from "vue";
|
||||
import { onMounted, onUnmounted, ref, computed } from "vue";
|
||||
import Search from '@/components/Search';
|
||||
import ModelForm from '@/components/indexDecorate/ModelForm';
|
||||
import HoverSearch from '@/components/header/hoverSearch';
|
||||
import fixedBar from '@/components/fixed/index';
|
||||
import storage from "@/plugins/storage";
|
||||
import { indexData, getAutoCoup } from '@/api/index.js';
|
||||
import { DOUBLE11_PAGE_DOCUMENT } from '@/components/indexDecorate/presets/double11Preset.js';
|
||||
import { seckillByDay } from '@/api/promotion';
|
||||
|
||||
defineOptions({ name: "Index" });
|
||||
@@ -69,6 +70,16 @@ const topSearchShow = ref(false); // 滚动后顶部搜索栏展示
|
||||
const carouselLarge = ref(false); // 不同轮播分类尺寸
|
||||
const carouselOpacity = ref(false); // 不同轮播分类样式
|
||||
|
||||
const isVersion2 = computed(() => {
|
||||
return modelFormData.value?.version === 2 || modelFormData.value?.version === '2.0';
|
||||
});
|
||||
|
||||
const hasComponentInV2 = (type) => {
|
||||
if (!isVersion2.value || !Array.isArray(modelFormData.value?.list)) return false;
|
||||
return modelFormData.value.list.some(r => r.components && r.components.some(c => c.type === type));
|
||||
};
|
||||
|
||||
|
||||
// 优惠券可用范围
|
||||
function useScope(type, storeName) {
|
||||
let shop = '平台';
|
||||
@@ -137,7 +148,7 @@ function handleReachBottom() {
|
||||
function getIndexData() {
|
||||
if (props.pageData) {
|
||||
parsePageData(JSON.stringify(props.pageData)).catch(() => {
|
||||
// 装修数据解析失败时保持默认空楼层
|
||||
modelFormData.value = DOUBLE11_PAGE_DOCUMENT;
|
||||
});
|
||||
} else {
|
||||
// 获取首页装修数据
|
||||
@@ -146,11 +157,15 @@ function getIndexData() {
|
||||
try {
|
||||
await parsePageData(res.result.pageData);
|
||||
} catch {
|
||||
// 装修数据解析失败时保持默认空楼层
|
||||
modelFormData.value = DOUBLE11_PAGE_DOCUMENT;
|
||||
}
|
||||
} else {
|
||||
// 无服务端数据时默认展示双11平台首页
|
||||
modelFormData.value = DOUBLE11_PAGE_DOCUMENT;
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.warn('获取首页装修数据失败', err);
|
||||
console.warn('获取首页装修数据失败,启用双11平台首页预设', err);
|
||||
modelFormData.value = DOUBLE11_PAGE_DOCUMENT;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -160,6 +175,10 @@ async function parsePageData(pageData) {
|
||||
if (!dataJson?.list?.length) {
|
||||
return;
|
||||
}
|
||||
if (dataJson.version === 2 || dataJson.version === '2.0') {
|
||||
modelFormData.value = dataJson;
|
||||
return;
|
||||
}
|
||||
// 秒杀活动不是装修的数据,需要调用接口判断是否有秒杀商品
|
||||
// 轮播图根据不同轮播,样式不同
|
||||
for (let i = 0; i < dataJson.list.length; i++) {
|
||||
|
||||
@@ -3,7 +3,7 @@ var BASE = {
|
||||
* @description api请求基础路径
|
||||
*/
|
||||
API_DEV: {
|
||||
manager: "https://admin-api.pickmall.cn",
|
||||
manager: "http://127.0.0.1:8887",
|
||||
},
|
||||
API_PROD: {
|
||||
manager: "https://admin-api.pickmall.cn"
|
||||
|
||||
BIN
manager/public/static/banner-floor/floor_banner_1.png
Normal file
|
After Width: | Height: | Size: 452 KiB |
BIN
manager/public/static/brand-wall/brand_1.png
Normal file
|
After Width: | Height: | Size: 358 KiB |
BIN
manager/public/static/brand-wall/brand_2.png
Normal file
|
After Width: | Height: | Size: 356 KiB |
BIN
manager/public/static/brand-wall/brand_3.png
Normal file
|
After Width: | Height: | Size: 366 KiB |
BIN
manager/public/static/brand-wall/brand_4.png
Normal file
|
After Width: | Height: | Size: 364 KiB |
BIN
manager/public/static/carousel/banner_1.png
Normal file
|
After Width: | Height: | Size: 523 KiB |
BIN
manager/public/static/carousel/banner_2.png
Normal file
|
After Width: | Height: | Size: 578 KiB |
BIN
manager/public/static/carousel/banner_3.png
Normal file
|
After Width: | Height: | Size: 985 KiB |
BIN
manager/public/static/carousel/banner_4.png
Normal file
|
After Width: | Height: | Size: 798 KiB |
BIN
manager/public/static/category-nav/cat_1.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
manager/public/static/category-nav/cat_10.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
manager/public/static/category-nav/cat_2.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
manager/public/static/category-nav/cat_3.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
manager/public/static/category-nav/cat_4.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
manager/public/static/category-nav/cat_5.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
manager/public/static/category-nav/cat_6.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
manager/public/static/category-nav/cat_7.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
manager/public/static/category-nav/cat_8.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
manager/public/static/category-nav/cat_9.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
manager/public/static/channel-grid/channel_1.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
manager/public/static/channel-grid/channel_2.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
manager/public/static/channel-grid/channel_3.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
manager/public/static/channel-grid/channel_4.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
manager/public/static/goods/goods_1.png
Normal file
|
After Width: | Height: | Size: 280 KiB |
BIN
manager/public/static/goods/goods_2.png
Normal file
|
After Width: | Height: | Size: 662 KiB |
BIN
manager/public/static/goods/goods_3.png
Normal file
|
After Width: | Height: | Size: 613 KiB |
BIN
manager/public/static/goods/goods_4.png
Normal file
|
After Width: | Height: | Size: 714 KiB |
BIN
manager/public/static/goods/goods_5.png
Normal file
|
After Width: | Height: | Size: 577 KiB |
BIN
manager/public/static/goods/goods_6.png
Normal file
|
After Width: | Height: | Size: 362 KiB |
BIN
manager/public/static/goods/goods_7.png
Normal file
|
After Width: | Height: | Size: 744 KiB |
BIN
manager/public/static/goods/goods_8.png
Normal file
|
After Width: | Height: | Size: 720 KiB |
BIN
manager/public/static/hot-zone/hotzone_default.png
Normal file
|
After Width: | Height: | Size: 592 KiB |
BIN
manager/public/static/logo/logo.png
Normal file
|
After Width: | Height: | Size: 12 KiB |