mirror of
https://gitee.com/beijing_hongye_huicheng/lilishop-ui.git
synced 2026-11-06 17:09:08 +08:00
feat(seller): align PC decoration editor and remove legacy components
This commit is contained in:
@@ -206,7 +206,11 @@ function getIndexData() {
|
||||
}
|
||||
|
||||
async function parsePageData(pageData) {
|
||||
const dataJson = JSON.parse(pageData);
|
||||
const dataJson = typeof pageData === 'string' ? JSON.parse(pageData) : pageData;
|
||||
if (dataJson.version === 2 || dataJson.version === '2.0') {
|
||||
modelForm.value = dataJson;
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < dataJson.list.length; i++) {
|
||||
const type = dataJson.list[i].type;
|
||||
if (type === "carousel2") {
|
||||
|
||||
@@ -124,8 +124,7 @@ function open(type, mutiple) {
|
||||
}
|
||||
// 外部通过 ref 访问的成员(grep seller/src 全量确认):
|
||||
// - open:所有调用方打开弹窗(goods/link)
|
||||
// - singleGoods:shop/modelList/forYour.vue、mixModel.vue 直接调用
|
||||
// - clearGoodsSelected:shop/wap/decorate.vue、modelList/goodsAndType.vue 调用
|
||||
// - clearGoodsSelected:shop/wap/decorate.vue 调用
|
||||
// - goodsData:促销/采购/装修多处回写已选商品
|
||||
// - goodsFlag:shop/wap/decorate.vue 直接写入
|
||||
// - flag(可写 computed):shop/wap/decorate.vue 写入触发弹层开关
|
||||
|
||||
@@ -1,879 +0,0 @@
|
||||
import nav1 from '@/assets/nav/1.jpg';
|
||||
import nav2 from '@/assets/nav/2.jpg';
|
||||
import nav3 from '@/assets/nav/3.jpg';
|
||||
import nav4 from '@/assets/nav/4.jpg';
|
||||
import nav5 from '@/assets/nav/5.jpg';
|
||||
import navDecorate from '@/assets/nav/decorate.png';
|
||||
import navDecorate1 from '@/assets/nav/decorate1.png';
|
||||
import navDecorate2 from '@/assets/nav/decorate2.jpeg';
|
||||
import navDecorate3 from '@/assets/nav/decorate3.jpeg';
|
||||
import navDecorate4 from '@/assets/nav/decorate4.jpeg';
|
||||
import navDecorate5 from '@/assets/nav/decorate5.jpeg';
|
||||
import navDecorate6 from '@/assets/nav/decorate6.jpeg';
|
||||
import navDecorate7 from '@/assets/nav/decorate7.jpeg';
|
||||
import navDecorate8 from '@/assets/nav/decorate8.png';
|
||||
import navDecorate9 from '@/assets/nav/decorate9.png';
|
||||
import navDecorate10 from '@/assets/nav/decorate10.jpeg';
|
||||
import navDecorate11 from '@/assets/nav/decorate11.jpeg';
|
||||
|
||||
export const modelData = [
|
||||
{
|
||||
type: 'hotAdvert',
|
||||
name: '热门广告',
|
||||
icon: 'md-image',
|
||||
showName: '',
|
||||
options: {
|
||||
list: [{
|
||||
img: navDecorate1,
|
||||
url: '',
|
||||
size: '1200*自定义'
|
||||
},
|
||||
{
|
||||
img: nav1,
|
||||
url: '',
|
||||
size: '230*190'
|
||||
},
|
||||
{
|
||||
img: nav1,
|
||||
url: '',
|
||||
size: '230*190'
|
||||
},
|
||||
{
|
||||
img: nav1,
|
||||
url: '',
|
||||
size: '230*190'
|
||||
},
|
||||
{
|
||||
img: nav1,
|
||||
url: '',
|
||||
size: '230*190'
|
||||
},
|
||||
{
|
||||
img: nav1,
|
||||
url: '',
|
||||
size: '230*190'
|
||||
}
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'seckill',
|
||||
name: '促销活动',
|
||||
icon: 'md-image',
|
||||
showName: '',
|
||||
options: {
|
||||
list: [{
|
||||
time: 6,
|
||||
goodsList: [{
|
||||
img: nav1,
|
||||
price: 20,
|
||||
originalPrice: 30,
|
||||
name: '阿迪达斯三叶草asdasdafads123213a',
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: nav2,
|
||||
price: 20,
|
||||
originalPrice: 30,
|
||||
name: '阿迪达斯三叶草asdasdafadsa',
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: nav3,
|
||||
price: 20,
|
||||
originalPrice: 30,
|
||||
name: '阿迪达斯三叶草asdasdafadsa',
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: nav4,
|
||||
price: 20,
|
||||
originalPrice: 30,
|
||||
name: '阿迪达斯三叶草asdasdafadsa',
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: nav5,
|
||||
price: 20,
|
||||
originalPrice: 30,
|
||||
name: '阿迪达斯三叶草asdasdafadsa',
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: nav1,
|
||||
price: 20,
|
||||
originalPrice: 30,
|
||||
name: '阿迪达斯三叶草asdasdafadsa',
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: nav2,
|
||||
price: 20,
|
||||
originalPrice: 30,
|
||||
name: '阿迪达斯三叶草asdasdafadsa',
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: nav3,
|
||||
price: 20,
|
||||
originalPrice: 30,
|
||||
name: '阿迪达斯三叶草asdasdafadsa',
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: nav4,
|
||||
price: 20,
|
||||
originalPrice: 30,
|
||||
name: '阿迪达斯三叶草asdasdafadsa',
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: nav5,
|
||||
price: 20,
|
||||
originalPrice: 30,
|
||||
name: '阿迪达斯三叶草asdasdafadsa',
|
||||
url: ''
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
time: 8,
|
||||
goodsList: [{
|
||||
img: nav1,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: nav2,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: nav3,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: nav4,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: nav5,
|
||||
url: ''
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
time: 10,
|
||||
goodsList: [{
|
||||
img: nav1,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: nav2,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: nav3,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: nav4,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: nav5,
|
||||
url: ''
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
time: 12,
|
||||
goodsList: [{
|
||||
img: nav1,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: nav2,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: nav3,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: nav4,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: nav5,
|
||||
url: ''
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
time: 14,
|
||||
goodsList: []
|
||||
},
|
||||
{
|
||||
time: 16,
|
||||
goodsList: []
|
||||
},
|
||||
{
|
||||
time: 18,
|
||||
goodsList: []
|
||||
}
|
||||
|
||||
]
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'discountAdvert',
|
||||
name: '折扣广告',
|
||||
icon: 'md-image',
|
||||
options: {
|
||||
bgImg: {
|
||||
img: navDecorate,
|
||||
url: '',
|
||||
size: "1300*585"
|
||||
},
|
||||
classification: [{
|
||||
img: navDecorate2,
|
||||
url: '',
|
||||
size: '190*210'
|
||||
}, {
|
||||
img: navDecorate2,
|
||||
url: '',
|
||||
size: '190*210'
|
||||
}, {
|
||||
img: navDecorate2,
|
||||
url: '',
|
||||
size: '190*210'
|
||||
}, {
|
||||
img: navDecorate2,
|
||||
url: '',
|
||||
size: '190*210'
|
||||
}, {
|
||||
img: navDecorate2,
|
||||
url: '',
|
||||
size: '190*210'
|
||||
}, {
|
||||
img: navDecorate2,
|
||||
url: '',
|
||||
size: '190*210'
|
||||
}, {
|
||||
img: navDecorate2,
|
||||
url: '',
|
||||
size: '190*210'
|
||||
}, {
|
||||
img: navDecorate2,
|
||||
url: '',
|
||||
size: '190*210'
|
||||
}, {
|
||||
img: navDecorate2,
|
||||
url: '',
|
||||
size: '190*210'
|
||||
}, {
|
||||
img: navDecorate2,
|
||||
url: '',
|
||||
size: '190*210'
|
||||
}, ],
|
||||
brandList: [{
|
||||
img: navDecorate11,
|
||||
url: '',
|
||||
size: '240*105'
|
||||
}, {
|
||||
img: navDecorate11,
|
||||
url: '',
|
||||
size: '240*105'
|
||||
}, {
|
||||
img: navDecorate11,
|
||||
url: '',
|
||||
size: '240*105'
|
||||
}, {
|
||||
img: navDecorate11,
|
||||
url: '',
|
||||
size: '240*105'
|
||||
}, ]
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'recommend',
|
||||
name: '好货推荐',
|
||||
icon: 'md-image',
|
||||
options: {
|
||||
contentLeft: {
|
||||
title: '发现好货',
|
||||
secondTitle: '更多好货',
|
||||
bgColor: '#449dae',
|
||||
url: '',
|
||||
list: [{
|
||||
img: navDecorate3,
|
||||
name: '阿迪达斯三叶草',
|
||||
describe: '也许是每一款经典系列都应该有一个独特的故事吧',
|
||||
url: '',
|
||||
size: '160*160'
|
||||
},
|
||||
{
|
||||
img: navDecorate4,
|
||||
name: '360行车记录',
|
||||
describe: '夜行 监控 电子狗 蓝牙',
|
||||
url: '',
|
||||
size: '80*80'
|
||||
},
|
||||
{
|
||||
img: navDecorate4,
|
||||
name: '360行车记录',
|
||||
describe: '夜行 监控 电子狗 蓝牙',
|
||||
url: '',
|
||||
size: '80*80'
|
||||
},
|
||||
{
|
||||
img: navDecorate4,
|
||||
name: '360行车记录',
|
||||
describe: '夜行 监控 电子狗 蓝牙',
|
||||
url: '',
|
||||
size: '80*80'
|
||||
},
|
||||
{
|
||||
img: navDecorate4,
|
||||
name: '360行车记录',
|
||||
describe: '夜行 监控 电子狗 蓝牙',
|
||||
url: '',
|
||||
size: '80*80'
|
||||
},
|
||||
{
|
||||
img: navDecorate4,
|
||||
name: '360行车记录',
|
||||
describe: '夜行 监控 电子狗 蓝牙',
|
||||
url: '',
|
||||
size: '80*80'
|
||||
},
|
||||
{
|
||||
img: navDecorate4,
|
||||
name: '360行车记录',
|
||||
describe: '夜行 监控 电子狗 蓝牙',
|
||||
url: '',
|
||||
size: '80*80'
|
||||
},
|
||||
]
|
||||
},
|
||||
contentRight: {
|
||||
title: '特色推荐',
|
||||
secondTitle: '更多特色推荐',
|
||||
bgColor: '#a25684',
|
||||
url: '',
|
||||
list: [{
|
||||
img: navDecorate5,
|
||||
name: '好心情喝出来',
|
||||
describe: '遇见懂你的饮品',
|
||||
url: '',
|
||||
size: '100*100'
|
||||
},
|
||||
{
|
||||
img: navDecorate5,
|
||||
name: '好心情喝出来',
|
||||
describe: '遇见懂你的饮品',
|
||||
url: '',
|
||||
size: '100*100'
|
||||
},
|
||||
{
|
||||
img: navDecorate5,
|
||||
name: '好心情喝出来',
|
||||
describe: '遇见懂你的饮品',
|
||||
url: '',
|
||||
size: '100*100'
|
||||
},
|
||||
{
|
||||
img: navDecorate5,
|
||||
name: '好心情喝出来',
|
||||
describe: '遇见懂你的饮品',
|
||||
url: '',
|
||||
size: '100*100'
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'newGoodsSort',
|
||||
name: '新品排行',
|
||||
icon: 'md-image',
|
||||
options: {
|
||||
left: {
|
||||
title: '特卖',
|
||||
secondTitle: "更多特卖",
|
||||
bgColor: '#c43d7e',
|
||||
url: '',
|
||||
list: [{
|
||||
name: '新年心愿单',
|
||||
describe: '满269减50,满999减100',
|
||||
img: navDecorate6,
|
||||
url: '',
|
||||
size: "160*160"
|
||||
},
|
||||
{
|
||||
name: 'Ms.Maggie 冬季时尚',
|
||||
describe: '满269减50',
|
||||
img: navDecorate6,
|
||||
url: '',
|
||||
size: "90*90"
|
||||
},
|
||||
{
|
||||
name: 'Ms.Maggie 冬季时尚',
|
||||
describe: '满269减50',
|
||||
img: navDecorate6,
|
||||
url: '',
|
||||
size: "90*90"
|
||||
},
|
||||
{
|
||||
name: 'Ms.Maggie 冬季时尚',
|
||||
describe: '满269减50',
|
||||
img: navDecorate6,
|
||||
url: '',
|
||||
size: "90*90"
|
||||
},
|
||||
{
|
||||
name: '阿迪达斯 领跑时尚',
|
||||
describe: '满269减50',
|
||||
img: navDecorate6,
|
||||
url: '',
|
||||
size: "90*90"
|
||||
},
|
||||
],
|
||||
},
|
||||
middle: {
|
||||
title: '新品',
|
||||
secondTitle: "更多新品",
|
||||
bgColor: '#e66a07',
|
||||
url: '',
|
||||
list: [{
|
||||
name: '阿迪达斯 领跑时尚',
|
||||
describe: '满269减50',
|
||||
img: navDecorate6,
|
||||
url: '',
|
||||
size: "90*90"
|
||||
},
|
||||
{
|
||||
name: '阿迪达斯 领跑时尚',
|
||||
describe: '满269减50',
|
||||
img: navDecorate6,
|
||||
url: '',
|
||||
size: "90*90"
|
||||
},
|
||||
{
|
||||
name: '阿迪达斯 领跑时尚',
|
||||
describe: '满269减50',
|
||||
img: navDecorate6,
|
||||
url: '',
|
||||
size: "90*90"
|
||||
},
|
||||
{
|
||||
name: '阿迪达斯 领跑时尚',
|
||||
describe: '满269减50',
|
||||
img: navDecorate6,
|
||||
url: '',
|
||||
size: "90*90"
|
||||
},
|
||||
{
|
||||
name: '阿迪达斯 领跑时尚',
|
||||
describe: '满269减50',
|
||||
img: navDecorate6,
|
||||
url: '',
|
||||
size: "90*90"
|
||||
},
|
||||
{
|
||||
name: '阿迪达斯 领跑时尚',
|
||||
describe: '满269减50',
|
||||
img: navDecorate6,
|
||||
url: '',
|
||||
size: "90*90"
|
||||
},
|
||||
]
|
||||
},
|
||||
right: {
|
||||
title: '排行榜',
|
||||
secondTitle: "精品风向标",
|
||||
bgColor: '#b62323',
|
||||
url: '',
|
||||
list: [{
|
||||
name: '小米红米3s手机壳保护套红米3高配版指纹男女款潮版磨砂硬壳防摔 收藏截图 送大礼包',
|
||||
price: 14.9,
|
||||
img: navDecorate7,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
name: '小米红米3s手机壳保护套红米3高配版指纹男女款潮版磨砂硬壳防摔 收藏截图 送大礼包',
|
||||
price: 14.9,
|
||||
img: navDecorate7,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
name: '小米红米3s手机壳保护套红米3高配版指纹男女款潮版磨砂硬壳防摔 收藏截图 送大礼包',
|
||||
price: 14.9,
|
||||
img: navDecorate7,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
name: '小米红米3s手机壳保护套红米3高配版指纹男女款潮版磨砂硬壳防摔 收藏截图 送大礼包',
|
||||
price: 14.9,
|
||||
img: navDecorate7,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
name: '小米红米3s手机壳保护套红米3高配版指纹男女款潮版磨砂硬壳防摔 收藏截图 送大礼包',
|
||||
price: 14.9,
|
||||
img: navDecorate7,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
name: '小米红米3s手机壳保护套红米3高配版指纹男女款潮版磨砂硬壳防摔 收藏截图 送大礼包',
|
||||
price: 14.9,
|
||||
img: navDecorate7,
|
||||
url: ''
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'firstAdvert',
|
||||
name: '首页广告',
|
||||
icon: 'md-image',
|
||||
options: {
|
||||
list: [{
|
||||
name: '生鲜',
|
||||
describe: "年货带回家 满199减60",
|
||||
img: navDecorate8,
|
||||
url: '',
|
||||
fromColor: '#e89621',
|
||||
toColor: "#f5c568",
|
||||
size: '170*170'
|
||||
},
|
||||
{
|
||||
name: '众筹',
|
||||
describe: "年货带回家",
|
||||
img: navDecorate9,
|
||||
url: '',
|
||||
fromColor: "#325bb4",
|
||||
toColor: '#4c9afe',
|
||||
size: '170*170'
|
||||
},
|
||||
{
|
||||
name: '生鲜',
|
||||
describe: "年货带回家 满199减60",
|
||||
img: navDecorate8,
|
||||
url: '',
|
||||
fromColor: "#1c9daf",
|
||||
toColor: '#40cda7',
|
||||
size: '170*170'
|
||||
},
|
||||
{
|
||||
name: '众筹',
|
||||
describe: "备孕有孕检测仪",
|
||||
img: navDecorate9,
|
||||
url: '',
|
||||
fromColor: "#d13837",
|
||||
toColor: '#df6d4f',
|
||||
size: '170*170'
|
||||
},
|
||||
{
|
||||
name: '生鲜',
|
||||
describe: "年货带回家 满199减60",
|
||||
img: navDecorate8,
|
||||
url: '',
|
||||
fromColor: "#ca4283",
|
||||
toColor: '#eb75cf',
|
||||
size: '170*170'
|
||||
},
|
||||
{
|
||||
name: '众筹',
|
||||
describe: "备孕有孕检测仪",
|
||||
img: navDecorate9,
|
||||
url: '',
|
||||
fromColor: "#5d40c1",
|
||||
toColor: '#8c5fdb',
|
||||
size: '170*170'
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'bannerAdvert',
|
||||
name: '横幅广告',
|
||||
icon: 'md-image',
|
||||
options: {
|
||||
img: '',
|
||||
url: '',
|
||||
size: '1200*自定义'
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'notEnough',
|
||||
name: '还没逛够',
|
||||
icon: 'md-image',
|
||||
options: {
|
||||
list: [
|
||||
[{
|
||||
img: navDecorate10,
|
||||
name: 'Apple/苹果 13 英寸:MacBook Pro Multi-Touch Bar 和 Touch ID 2.9GHz 处理器 512GB 存储容量',
|
||||
price: 6666,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: navDecorate10,
|
||||
name: 'Apple/苹果 13 英寸:MacBook Pro Multi-Touch Bar 和 Touch ID 2.9GHz 处理器 512GB 存储容量',
|
||||
price: 6666,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: navDecorate10,
|
||||
name: 'Apple/苹果 13 英寸:MacBook Pro Multi-Touch Bar 和 Touch ID 2.9GHz 处理器 512GB 存储容量',
|
||||
price: 6666,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: navDecorate10,
|
||||
name: 'Apple/苹果 13 英寸:MacBook Pro Multi-Touch Bar 和 Touch ID 2.9GHz 处理器 512GB 存储容量',
|
||||
price: 6666,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: navDecorate10,
|
||||
name: 'Apple/苹果 13 英寸:MacBook Pro Multi-Touch Bar 和 Touch ID 2.9GHz 处理器 512GB 存储容量',
|
||||
price: 6666,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: navDecorate10,
|
||||
name: 'Apple/苹果 13 英寸:MacBook Pro Multi-Touch Bar 和 Touch ID 2.9GHz 处理器 512GB 存储容量',
|
||||
price: 6666,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: navDecorate10,
|
||||
name: 'Apple/苹果 13 英寸:MacBook Pro Multi-Touch Bar 和 Touch ID 2.9GHz 处理器 512GB 存储容量',
|
||||
price: 6666,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: navDecorate10,
|
||||
name: 'Apple/苹果 13 英寸:MacBook Pro Multi-Touch Bar 和 Touch ID 2.9GHz 处理器 512GB 存储容量',
|
||||
price: 6666,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: navDecorate10,
|
||||
name: 'Apple/苹果 13 英寸:MacBook Pro Multi-Touch Bar 和 Touch ID 2.9GHz 处理器 512GB 存储容量',
|
||||
price: 6666,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: navDecorate10,
|
||||
name: 'Apple/苹果 13 英寸:MacBook Pro Multi-Touch Bar 和 Touch ID 2.9GHz 处理器 512GB 存储容量',
|
||||
price: 6666,
|
||||
url: ''
|
||||
},
|
||||
],
|
||||
[{
|
||||
img: navDecorate10,
|
||||
name: 'Apple/苹果 13 英寸:MacBook Pro Multi-Touch Bar 和 Touch ID 2.9GHz 处理器 512GB 存储容量',
|
||||
price: 6666,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: navDecorate10,
|
||||
name: 'Apple/苹果 13 英寸:MacBook Pro Multi-Touch Bar 和 Touch ID 2.9GHz 处理器 512GB 存储容量',
|
||||
price: 6666,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: navDecorate10,
|
||||
name: 'Apple/苹果 13 英寸:MacBook Pro Multi-Touch Bar 和 Touch ID 2.9GHz 处理器 512GB 存储容量',
|
||||
price: 6666,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: navDecorate10,
|
||||
name: 'Apple/苹果 13 英寸:MacBook Pro Multi-Touch Bar 和 Touch ID 2.9GHz 处理器 512GB 存储容量',
|
||||
price: 6666,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: navDecorate10,
|
||||
name: 'Apple/苹果 13 英寸:MacBook Pro Multi-Touch Bar 和 Touch ID 2.9GHz 处理器 512GB 存储容量',
|
||||
price: 6666,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: navDecorate10,
|
||||
name: 'Apple/苹果 13 英寸:MacBook Pro Multi-Touch Bar 和 Touch ID 2.9GHz 处理器 512GB 存储容量',
|
||||
price: 6666,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: navDecorate10,
|
||||
name: 'Apple/苹果 13 英寸:MacBook Pro Multi-Touch Bar 和 Touch ID 2.9GHz 处理器 512GB 存储容量',
|
||||
price: 6666,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: navDecorate10,
|
||||
name: 'Apple/苹果 13 英寸:MacBook Pro Multi-Touch Bar 和 Touch ID 2.9GHz 处理器 512GB 存储容量',
|
||||
price: 6666,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: navDecorate10,
|
||||
name: 'Apple/苹果 13 英寸:MacBook Pro Multi-Touch Bar 和 Touch ID 2.9GHz 处理器 512GB 存储容量',
|
||||
price: 6666,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: navDecorate10,
|
||||
name: 'Apple/苹果 13 英寸:MacBook Pro Multi-Touch Bar 和 Touch ID 2.9GHz 处理器 512GB 存储容量',
|
||||
price: 6666,
|
||||
url: ''
|
||||
},
|
||||
],
|
||||
[{
|
||||
img: navDecorate10,
|
||||
name: 'Apple/苹果 13 英寸:MacBook Pro Multi-Touch Bar 和 Touch ID 2.9GHz 处理器 512GB 存储容量',
|
||||
price: 6666,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: navDecorate10,
|
||||
name: 'Apple/苹果 13 英寸:MacBook Pro Multi-Touch Bar 和 Touch ID 2.9GHz 处理器 512GB 存储容量',
|
||||
price: 6666,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: navDecorate10,
|
||||
name: 'Apple/苹果 13 英寸:MacBook Pro Multi-Touch Bar 和 Touch ID 2.9GHz 处理器 512GB 存储容量',
|
||||
price: 6666,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: navDecorate10,
|
||||
name: 'Apple/苹果 13 英寸:MacBook Pro Multi-Touch Bar 和 Touch ID 2.9GHz 处理器 512GB 存储容量',
|
||||
price: 6666,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: navDecorate10,
|
||||
name: 'Apple/苹果 13 英寸:MacBook Pro Multi-Touch Bar 和 Touch ID 2.9GHz 处理器 512GB 存储容量',
|
||||
price: 6666,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: navDecorate10,
|
||||
name: 'Apple/苹果 13 英寸:MacBook Pro Multi-Touch Bar 和 Touch ID 2.9GHz 处理器 512GB 存储容量',
|
||||
price: 6666,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: navDecorate10,
|
||||
name: 'Apple/苹果 13 英寸:MacBook Pro Multi-Touch Bar 和 Touch ID 2.9GHz 处理器 512GB 存储容量',
|
||||
price: 6666,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: navDecorate10,
|
||||
name: 'Apple/苹果 13 英寸:MacBook Pro Multi-Touch Bar 和 Touch ID 2.9GHz 处理器 512GB 存储容量',
|
||||
price: 6666,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: navDecorate10,
|
||||
name: 'Apple/苹果 13 英寸:MacBook Pro Multi-Touch Bar 和 Touch ID 2.9GHz 处理器 512GB 存储容量',
|
||||
price: 6666,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: navDecorate10,
|
||||
name: 'Apple/苹果 13 英寸:MacBook Pro Multi-Touch Bar 和 Touch ID 2.9GHz 处理器 512GB 存储容量',
|
||||
price: 6666,
|
||||
url: ''
|
||||
},
|
||||
],
|
||||
[{
|
||||
img: navDecorate10,
|
||||
name: 'Apple/苹果 13 英寸:MacBook Pro Multi-Touch Bar 和 Touch ID 2.9GHz 处理器 512GB 存储容量',
|
||||
price: 6666,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: navDecorate10,
|
||||
name: 'Apple/苹果 13 英寸:MacBook Pro Multi-Touch Bar 和 Touch ID 2.9GHz 处理器 512GB 存储容量',
|
||||
price: 6666,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: navDecorate10,
|
||||
name: 'Apple/苹果 13 英寸:MacBook Pro Multi-Touch Bar 和 Touch ID 2.9GHz 处理器 512GB 存储容量',
|
||||
price: 6666,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: navDecorate10,
|
||||
name: 'Apple/苹果 13 英寸:MacBook Pro Multi-Touch Bar 和 Touch ID 2.9GHz 处理器 512GB 存储容量',
|
||||
price: 6666,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: navDecorate10,
|
||||
name: 'Apple/苹果 13 英寸:MacBook Pro Multi-Touch Bar 和 Touch ID 2.9GHz 处理器 512GB 存储容量',
|
||||
price: 6666,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: navDecorate10,
|
||||
name: 'Apple/苹果 13 英寸:MacBook Pro Multi-Touch Bar 和 Touch ID 2.9GHz 处理器 512GB 存储容量',
|
||||
price: 6666,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: navDecorate10,
|
||||
name: 'Apple/苹果 13 英寸:MacBook Pro Multi-Touch Bar 和 Touch ID 2.9GHz 处理器 512GB 存储容量',
|
||||
price: 6666,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: navDecorate10,
|
||||
name: 'Apple/苹果 13 英寸:MacBook Pro Multi-Touch Bar 和 Touch ID 2.9GHz 处理器 512GB 存储容量',
|
||||
price: 6666,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: navDecorate10,
|
||||
name: 'Apple/苹果 13 英寸:MacBook Pro Multi-Touch Bar 和 Touch ID 2.9GHz 处理器 512GB 存储容量',
|
||||
price: 6666,
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
img: navDecorate10,
|
||||
name: 'Apple/苹果 13 英寸:MacBook Pro Multi-Touch Bar 和 Touch ID 2.9GHz 处理器 512GB 存储容量',
|
||||
price: 6666,
|
||||
url: ''
|
||||
},
|
||||
],
|
||||
|
||||
],
|
||||
navList: [{
|
||||
title: '精选',
|
||||
desc: '猜你喜欢'
|
||||
},
|
||||
{
|
||||
title: '智能先锋',
|
||||
desc: '大电器城'
|
||||
},
|
||||
{
|
||||
title: '居家优品',
|
||||
desc: '品质生活'
|
||||
},
|
||||
{
|
||||
title: '超市百货',
|
||||
desc: '百货生鲜'
|
||||
},
|
||||
]
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -1,415 +0,0 @@
|
||||
<template>
|
||||
<div class="model-form">
|
||||
<div class="model-content">
|
||||
<div
|
||||
class="top-fixed-advert"
|
||||
:style="{ backgroundColor: topAdvert.bgColor }"
|
||||
>
|
||||
<img :src="topAdvert.img" width="1200" height="80" alt="" />
|
||||
<div class="setup-box">
|
||||
<el-button size="small" @click.stop="handleModel('topAdvert')">编辑</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-con">
|
||||
<div></div>
|
||||
<ul class="detail">
|
||||
<li>立即注册</li>
|
||||
<li>请登录</li>
|
||||
<li>我的订单</li>
|
||||
<li>我的足迹</li>
|
||||
<li>
|
||||
<el-icon :size="18"><ShoppingCart /></el-icon>
|
||||
购物车
|
||||
</li>
|
||||
<li>店铺入驻</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="search-con">
|
||||
<img :src="logoImg" class="logo" alt="" />
|
||||
<div class="search">
|
||||
<el-input size="large" placeholder="输入你想查找的商品">
|
||||
<template #append>
|
||||
<el-button>搜索</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-con" v-if="$route.query.pageType !== 'SPECIAL'">
|
||||
<div class="all-categories">全部商品分类</div>
|
||||
<ul class="nav-item">
|
||||
<li v-for="(item, index) in navList.list" :key="index">
|
||||
<a href="#">{{ item.name }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="setup-box">
|
||||
<el-button size="small" @click.stop="handleModel('quickNav')">编辑</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<draggable
|
||||
class="model-form-list"
|
||||
v-model="data.list"
|
||||
:item-key="getModelItemKey"
|
||||
group="model"
|
||||
ghost-class="ghost"
|
||||
@end="handleMoveEnd"
|
||||
@add="handleModelAdd"
|
||||
>
|
||||
<template #item="{ element, index }">
|
||||
<model-form-item
|
||||
v-if="element && element.key"
|
||||
:element="element"
|
||||
:index="index"
|
||||
:data="data"
|
||||
/>
|
||||
</template>
|
||||
</draggable>
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog
|
||||
v-model="showModal"
|
||||
title="顶部广告"
|
||||
width="800px"
|
||||
:close-on-click-modal="false"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
>
|
||||
<div class="modal-top-advert">
|
||||
<div>
|
||||
<img class="show-image" width="600" height="40" :src="topAdvert.img" alt />
|
||||
</div>
|
||||
<div class="tips">建议尺寸:<span>{{ topAdvert.size }}</span></div>
|
||||
<div>
|
||||
图片链接:<el-input
|
||||
class="outsideUrl"
|
||||
v-model="topAdvert.url"
|
||||
:disabled="!!topAdvert.type && topAdvert.type !== 'link'"
|
||||
placeholder="https://"
|
||||
/>
|
||||
<el-button size="small" type="primary" @click="handleSelectLink">选择链接</el-button>
|
||||
</div>
|
||||
<div>
|
||||
选择图片:<el-button size="small" type="primary" @click="handleSelectImg">选择图片</el-button>
|
||||
</div>
|
||||
<div>选择背景色:<el-color-picker v-model="topAdvert.bgColor" /></div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
<el-dialog
|
||||
v-model="showModalNav"
|
||||
title="快捷导航"
|
||||
width="800px"
|
||||
:close-on-click-modal="false"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
>
|
||||
<div class="modal-tab-bar">
|
||||
<el-button type="primary" size="small" @click="handleAddNav">添加分类</el-button>
|
||||
<table cellspacing="0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="250">分类名称</th>
|
||||
<th width="250">链接地址</th>
|
||||
<th width="250">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(item, index) in navList.list" :key="index">
|
||||
<td><el-input v-model="item.name" /></td>
|
||||
<td>
|
||||
<el-input
|
||||
v-model="item.url"
|
||||
:disabled="!item.title || item.title !== '外部链接'"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<el-button type="primary" size="small" @click="handleSelectLink(item, index)">选择链接</el-button>
|
||||
<el-button type="danger" size="small" @click="handleDelNav(index)">删除</el-button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</el-dialog>
|
||||
<liliDialog ref="liliDialogRef" @selectedLink="selectedLink"></liliDialog>
|
||||
<el-dialog v-model="picModelFlag" width="1200px" append-to-body destroy-on-close>
|
||||
<ossManage
|
||||
@callback="callbackSelected"
|
||||
:is-component="true"
|
||||
:initialize="picModelFlag"
|
||||
:max-select="1"
|
||||
ref="ossManageRef"
|
||||
/>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, nextTick, onMounted } from "vue";
|
||||
import { ShoppingCart } from "@element-plus/icons-vue";
|
||||
import logoImg from "@/assets/logo.png";
|
||||
import Draggable from "vuedraggable";
|
||||
import ModelFormItem from "./modelFormItem.vue";
|
||||
import ossManage from "@/views/sys/oss-manage/ossManage";
|
||||
import { formatLinkType } from "@/utils/filters";
|
||||
|
||||
defineOptions({ name: "modelForm" });
|
||||
|
||||
const props = defineProps(["data"]);
|
||||
|
||||
const picModelFlag = ref(false);
|
||||
const showModal = ref(false);
|
||||
const showModalNav = ref(false);
|
||||
const selectedNav = ref(null);
|
||||
const topAdvert = ref({
|
||||
type: "topAdvert",
|
||||
img: "",
|
||||
url: "",
|
||||
bgColor: "#de000d",
|
||||
size: "1200*80",
|
||||
});
|
||||
const currentIndex = ref(0);
|
||||
const navList = ref({
|
||||
type: "navBar",
|
||||
list: [
|
||||
{ name: "秒杀", url: "", title: "" },
|
||||
{ name: "闪购", url: "", title: "" },
|
||||
{ name: "优惠券", url: "", title: "" },
|
||||
{ name: "拍卖", url: "", title: "" },
|
||||
{ name: "服装城", url: "", title: "" },
|
||||
],
|
||||
});
|
||||
|
||||
const liliDialogRef = ref(null);
|
||||
const ossManageRef = ref(null);
|
||||
|
||||
// 父组件 renovation.vue 通过 $refs.modelForm 读取并整体赋值 navList / topAdvert
|
||||
defineExpose({ navList, topAdvert });
|
||||
|
||||
onMounted(() => {
|
||||
document.body.ondrop = function (event) {
|
||||
let isFirefox = navigator.userAgent.toLowerCase().indexOf("firefox") > -1;
|
||||
if (isFirefox) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
function getModelItemKey(element) {
|
||||
return element.model || element.key || element.type;
|
||||
}
|
||||
|
||||
function handleSelectLink(item, index) {
|
||||
if (item) selectedNav.value = item;
|
||||
liliDialogRef.value.open("link");
|
||||
currentIndex.value = index;
|
||||
}
|
||||
|
||||
function selectedLink(val) {
|
||||
if (showModalNav.value) {
|
||||
selectedNav.value.url = formatLinkType(val);
|
||||
selectedNav.value.type = val.___type === "other" && val.url === "" ? "link" : "other";
|
||||
} else {
|
||||
topAdvert.value.url = formatLinkType(val);
|
||||
topAdvert.value.type = val.___type === "other" && val.url === "" ? "link" : "other";
|
||||
}
|
||||
navList.value.list[currentIndex.value].title = val.title;
|
||||
}
|
||||
|
||||
function handleDelNav(index) {
|
||||
navList.value.list.splice(index, 1);
|
||||
}
|
||||
|
||||
function handleAddNav() {
|
||||
navList.value.list.push({ name: "", url: "", title: "" });
|
||||
nextTick(() => {
|
||||
selectedNav.value = navList.value.list[navList.value.list.length - 1];
|
||||
});
|
||||
}
|
||||
|
||||
function handleMoveEnd({ newIndex, oldIndex }) {}
|
||||
|
||||
function handleModel(type) {
|
||||
if (type == "topAdvert") {
|
||||
showModal.value = true;
|
||||
} else {
|
||||
showModalNav.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelectImg() {
|
||||
picModelFlag.value = true;
|
||||
nextTick(() => {
|
||||
if (ossManageRef.value) {
|
||||
ossManageRef.value.selectImage = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function callbackSelected(item) {
|
||||
picModelFlag.value = false;
|
||||
topAdvert.value.img = item.url;
|
||||
}
|
||||
|
||||
function handleModelAdd(evt) {
|
||||
const newIndex = evt.newIndex;
|
||||
const current = props.data.list[newIndex];
|
||||
if (!current || current.key) {
|
||||
return;
|
||||
}
|
||||
const key = Date.now() + "_" + Math.ceil(Math.random() * 99999);
|
||||
props.data.list[newIndex] = {
|
||||
...JSON.parse(JSON.stringify(current)),
|
||||
options: {
|
||||
...current.options,
|
||||
},
|
||||
key,
|
||||
model: current.type + "_" + key,
|
||||
};
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "./modelList/setup-box.scss";
|
||||
.model-form {
|
||||
width: 1500px;
|
||||
max-width: 100%;
|
||||
}
|
||||
.model-content {
|
||||
width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: #fff;
|
||||
min-height: 1200px;
|
||||
overflow-x: visible;
|
||||
}
|
||||
.model-form-list {
|
||||
min-height: 500px;
|
||||
box-sizing: border-box;
|
||||
overflow: visible;
|
||||
}
|
||||
.top-fixed-advert {
|
||||
display: flex;
|
||||
width: 1500px;
|
||||
margin-left: -150px;
|
||||
background: $theme_color;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.header-con {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
height: 35px;
|
||||
padding: 0 15px;
|
||||
line-height: 35px;
|
||||
color: #999;
|
||||
font-weight: bold;
|
||||
div,
|
||||
li {
|
||||
&:hover {
|
||||
color: $theme_color;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.detail {
|
||||
display: flex;
|
||||
> li {
|
||||
margin-left: 10px;
|
||||
&::after {
|
||||
content: "|";
|
||||
padding-left: 10px;
|
||||
}
|
||||
&:last-child::after {
|
||||
content: "";
|
||||
padding-left: 0;
|
||||
}
|
||||
&:hover::after {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.search-con {
|
||||
padding-top: 15px;
|
||||
margin: 0px auto;
|
||||
margin-bottom: 10px;
|
||||
width: 1200px;
|
||||
position: relative;
|
||||
.logo {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
width: 150px;
|
||||
height: 50px;
|
||||
}
|
||||
.search {
|
||||
width: 460px;
|
||||
margin: 0 auto;
|
||||
:deep(.el-input--large .el-input__wrapper) {
|
||||
border: 2px solid $theme_color;
|
||||
font-size: 12px;
|
||||
height: 34px;
|
||||
box-shadow: none;
|
||||
}
|
||||
:deep(.el-input-group__append) {
|
||||
border: 1px solid $theme_color;
|
||||
border-left: none;
|
||||
height: 30px;
|
||||
background-color: $theme_color;
|
||||
color: #ffffff;
|
||||
.el-button {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.nav-con {
|
||||
width: 1200px;
|
||||
height: 40px;
|
||||
background: #eee;
|
||||
display: flex;
|
||||
.all-categories {
|
||||
width: 200px;
|
||||
line-height: 40px;
|
||||
color: #fff;
|
||||
background-color: $theme_color;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
}
|
||||
.nav-item {
|
||||
width: 1000px;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
overflow: hidden;
|
||||
list-style: none;
|
||||
background-color: #eee;
|
||||
display: flex;
|
||||
li {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-left: 20px;
|
||||
a {
|
||||
color: rgb(89, 88, 88);
|
||||
font-size: 15px;
|
||||
&:hover {
|
||||
color: $theme_color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.top-fixed-advert,
|
||||
.nav-con {
|
||||
position: relative;
|
||||
&:hover {
|
||||
.setup-box {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
.modal-top-advert {
|
||||
align-items: start;
|
||||
padding: 0 30px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,668 +0,0 @@
|
||||
<template>
|
||||
<div class="model-item" v-if="element && element.key">
|
||||
<!-- 轮播图模块,包括个人信息,快捷导航模块 -->
|
||||
<template v-if="element.type == 'carousel'">
|
||||
<model-carousel :data="element"></model-carousel>
|
||||
</template>
|
||||
<!-- 轮播图模块,100%宽度,无个人信息栏 -->
|
||||
<template v-if="element.type == 'carousel1'">
|
||||
<model-carousel1 class="mb_20" :data="element"></model-carousel1>
|
||||
</template>
|
||||
<!-- 轮播图模块,包括个人信息,两个轮播模块 -->
|
||||
<template v-if="element.type == 'carousel2'">
|
||||
<model-carousel2 class="mb_20" :data="element"></model-carousel2>
|
||||
</template>
|
||||
<!-- 热门广告 -->
|
||||
<template v-if="element.type == 'hotAdvert'">
|
||||
<div class="hot-advert">
|
||||
<div class="hot-advert-banner setup-content">
|
||||
<img
|
||||
:src="element.options.list[0].img"
|
||||
@click="$router.push(element.options.list[0].url)"
|
||||
alt=""
|
||||
/>
|
||||
<div class="setup-box">
|
||||
<div>
|
||||
<el-button
|
||||
size="small"
|
||||
@click.stop="handleSelectModel(element.options.list[0])"
|
||||
>编辑</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="advert-list">
|
||||
<template v-for="(item, index) in element.options.list">
|
||||
<li
|
||||
v-if="index !== 0"
|
||||
@click="$router.push(item.url)"
|
||||
class="setup-content"
|
||||
:key="index"
|
||||
>
|
||||
<img :src="item.img" alt="" />
|
||||
<div class="setup-box">
|
||||
<div>
|
||||
<el-button size="small" @click.stop="handleSelectModel(item)"
|
||||
>编辑</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 限时秒杀 待完善 -->
|
||||
<template v-if="element.type == 'seckill'">
|
||||
<seckill :data="element"></seckill>
|
||||
</template>
|
||||
<!-- 限时秒杀 待完善 -->
|
||||
<template v-if="element.type == 'seckill-only-album'">
|
||||
<seckill-only-album :data="element"></seckill-only-album>
|
||||
</template>
|
||||
<!-- 折扣广告 -->
|
||||
<template v-if="element.type == 'discountAdvert'">
|
||||
<div class="discountAdvert-wrap">
|
||||
<div
|
||||
class="discountAdvert"
|
||||
:style="{
|
||||
'background-image':
|
||||
'url(' + decorateImg + ')',
|
||||
}"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
v-for="(item, index) in element.options.classification"
|
||||
:key="index"
|
||||
class="setup-content"
|
||||
>
|
||||
<img :src="item.img" width="190" height="210" alt="" />
|
||||
<div class="setup-box">
|
||||
<div>
|
||||
<el-button size="small" @click.stop="handleSelectModel(item)"
|
||||
>编辑</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
v-for="(item, index) in element.options.brandList"
|
||||
:key="index"
|
||||
class="setup-content"
|
||||
>
|
||||
<img :src="item.img" width="240" height="105" alt="" />
|
||||
<div class="setup-box">
|
||||
<div>
|
||||
<el-button size="small" @click.stop="handleSelectModel(item)"
|
||||
>编辑</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 好货推荐 -->
|
||||
<template v-if="element.type == 'recommend'">
|
||||
<recommend :data="element" class="mb_20 width_1200_auto"></recommend>
|
||||
</template>
|
||||
<!-- 新品排行 -->
|
||||
<template v-if="element.type == 'newGoodsSort'">
|
||||
<new-goods-sort :data="element" class="mb_20 width_1200_auto"></new-goods-sort>
|
||||
</template>
|
||||
<!-- 首页广告 -->
|
||||
<template v-if="element.type == 'firstAdvert'">
|
||||
<first-page-advert :data="element" class="mb_20 width_1200_auto"></first-page-advert>
|
||||
</template>
|
||||
<!-- 商品模块 -->
|
||||
<template v-if="element.type == 'goodsType'">
|
||||
<goodsType :data="element" class="mb_20 width_1200_auto"></goodsType>
|
||||
</template>
|
||||
<!-- 商品模块2 -->
|
||||
<template v-if="element.type == 'onlyGoodsModel'">
|
||||
<onlyGoodsModel :data="element" class="mb_20 width_1200_auto"></onlyGoodsModel>
|
||||
</template>
|
||||
<!-- 混合模块 -->
|
||||
<template v-if="element.type == 'mixModel'">
|
||||
<mixModel :data="element" class="mb_20 width_1200_auto"></mixModel>
|
||||
</template>
|
||||
<!-- 为你推荐 -->
|
||||
<template v-if="element.type == 'forYour'">
|
||||
<forYour :data="element" class="mb_20 width_1200_auto"></forYour>
|
||||
</template>
|
||||
<!-- 一行三列 -->
|
||||
<template v-if="element.type == 'oneRowThreeColumns'">
|
||||
<oneRowThreeColumns :data="element" class="mb_20 width_1200_auto"></oneRowThreeColumns>
|
||||
</template>
|
||||
<!-- 横幅广告 -->
|
||||
<template v-if="element.type == 'bannerAdvert'">
|
||||
<div class="horizontal-advert setup-content">
|
||||
<img
|
||||
v-if="element.options.img"
|
||||
class="banner-ad-img"
|
||||
:src="element.options.img"
|
||||
alt=""
|
||||
/>
|
||||
<div v-else class="default-con">
|
||||
<p>广告图片</p>
|
||||
<p>1200*自定义</p>
|
||||
</div>
|
||||
<div class="setup-box">
|
||||
<div>
|
||||
<el-button
|
||||
size="small"
|
||||
@click.stop="handleSelectModel(element.options)"
|
||||
>编辑</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="element.type == 'notEnough'">
|
||||
<not-enough :data="element" class="mb_20 width_1200_auto"></not-enough>
|
||||
</template>
|
||||
<div class="del-btn">
|
||||
<el-button size="small" type="danger" @click="handleModelDelete">删除</el-button>
|
||||
</div>
|
||||
<el-dialog
|
||||
v-model="showModal"
|
||||
title="装修"
|
||||
width="800px"
|
||||
:close-on-click-modal="false"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
>
|
||||
<div class="modal-top-advert">
|
||||
<div class="modal-form-item modal-form-preview">
|
||||
<!-- 热门广告两种图片尺寸 -->
|
||||
<img
|
||||
class="show-image"
|
||||
width="600"
|
||||
height="40"
|
||||
v-if="selected.size && selected.size.indexOf('1200') >= 0 && selected.img"
|
||||
:src="selected.img"
|
||||
alt
|
||||
/>
|
||||
<img
|
||||
class="show-image"
|
||||
width="230"
|
||||
height="190"
|
||||
v-if="selected.size && selected.size.indexOf('230*190') >= 0"
|
||||
:src="selected.img"
|
||||
alt
|
||||
/>
|
||||
<!-- 折扣广告三种图片尺寸 -->
|
||||
<img
|
||||
class="show-image"
|
||||
width="600"
|
||||
height="270"
|
||||
v-if="selected.size && selected.size.indexOf('1300') >= 0"
|
||||
:src="selected.img"
|
||||
alt
|
||||
/>
|
||||
<img
|
||||
class="show-image"
|
||||
width="190"
|
||||
height="210"
|
||||
v-if="selected.size && selected.size.indexOf('190*210') >= 0"
|
||||
:src="selected.img"
|
||||
alt
|
||||
/>
|
||||
<img
|
||||
class="show-image"
|
||||
width="240"
|
||||
height="105"
|
||||
v-if="selected.size && selected.size.indexOf('240*105') >= 0"
|
||||
:src="selected.img"
|
||||
alt
|
||||
/>
|
||||
</div>
|
||||
<div class="tips">建议尺寸:{{ selected.size }}</div>
|
||||
<div class="modal-form-item modal-form-link">
|
||||
<span class="modal-label">图片链接:</span>
|
||||
<div class="modal-link-field">
|
||||
<el-input
|
||||
class="outsideUrl"
|
||||
v-model="selected.url"
|
||||
:disabled="!!selected.type && selected.type !== 'link'"
|
||||
placeholder="https://"
|
||||
/>
|
||||
<el-button size="small" type="primary" @click="handleSelectLink"
|
||||
>选择链接</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-form-item">
|
||||
<span class="modal-label">选择图片:</span>
|
||||
<el-button size="small" type="primary" @click="handleSelectImg"
|
||||
>选择图片</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="handleCancelModal">取消</el-button>
|
||||
<el-button type="primary" @click="handleConfirmModal">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<!-- 选择商品。链接 -->
|
||||
<liliDialog ref="liliDialog" @selectedLink="selectedLink"></liliDialog>
|
||||
<!-- 选择图片 -->
|
||||
<el-dialog v-model="picModelFlag" width="1200px" append-to-body destroy-on-close>
|
||||
<ossManage
|
||||
@callback="callbackSelected"
|
||||
:is-component="true"
|
||||
:initialize="picModelFlag"
|
||||
:max-select="1"
|
||||
ref="ossManageRef"
|
||||
/>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { nextTick, ref } from "vue";
|
||||
import ModelCarousel from "./modelList/carousel.vue";
|
||||
import ModelCarousel1 from "./modelList/carousel1.vue";
|
||||
import ModelCarousel2 from "./modelList/carousel2.vue";
|
||||
import FirstPageAdvert from "./modelList/firstPageAdvert.vue";
|
||||
import NewGoodsSort from "./modelList/newGoodsSort.vue";
|
||||
import Recommend from "./modelList/recommend.vue";
|
||||
import NotEnough from "./modelList/notEnough.vue";
|
||||
import Seckill from "./modelList/seckill.vue";
|
||||
import goodsType from "./modelList/goodsAndType.vue";
|
||||
import ossManage from "@/views/sys/oss-manage/ossManage";
|
||||
|
||||
import mixModel from "./modelList/mixModel.vue";
|
||||
import forYour from "./modelList/forYour.vue";
|
||||
import onlyGoodsModel from "./modelList/onlyGoodsModel.vue";
|
||||
|
||||
import oneRowThreeColumns from "./modelList/oneRowThreeColumns.vue";
|
||||
import seckillOnlyAlbum from "./modelList/seckill-only-album.vue";
|
||||
import decorateImg from "@/assets/nav/decorate.png";
|
||||
import { Modal } from "@/utils/message";
|
||||
import { formatLinkType } from "@/utils/filters";
|
||||
|
||||
defineOptions({ name: "modelFormItem" });
|
||||
|
||||
const props = defineProps(["element", "select", "index", "data"]);
|
||||
|
||||
const showModal = ref(false); // modal显隐
|
||||
const selected = ref({}); // 弹窗编辑草稿
|
||||
const editTarget = ref(null); // 确定后写回的原数据
|
||||
const picModelFlag = ref(false); // 图片选择器
|
||||
|
||||
// ossManageRef:模板 ref 与组件 import 同名,改名避免互相覆盖
|
||||
const liliDialog = ref(null);
|
||||
const ossManageRef = ref(null);
|
||||
|
||||
function cloneItem(item) {
|
||||
return JSON.parse(JSON.stringify(item || {}));
|
||||
}
|
||||
// 编辑模块
|
||||
function handleSelectModel(item) {
|
||||
editTarget.value = item;
|
||||
selected.value = cloneItem(item);
|
||||
showModal.value = true;
|
||||
}
|
||||
// 删除模块
|
||||
function handleModelDelete() {
|
||||
Modal.confirm({
|
||||
title: "提示",
|
||||
content: "<p>确定删除当前模块吗?</p>",
|
||||
onOk: () => {
|
||||
nextTick(() => {
|
||||
props.data.list.splice(props.index, 1);
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
function handleSelectLink(item, index) {
|
||||
// 调起选择链接弹窗
|
||||
liliDialog.value.open("link");
|
||||
}
|
||||
// 确定选择链接
|
||||
function selectedLink(val) {
|
||||
selected.value.url = formatLinkType(val);
|
||||
selected.value.type =
|
||||
val.___type === "other" && val.url === "" ? "link" : "other";
|
||||
}
|
||||
|
||||
function handleSelectImg() {
|
||||
// 选择图片
|
||||
picModelFlag.value = true;
|
||||
nextTick(() => {
|
||||
if (ossManageRef.value) {
|
||||
ossManageRef.value.selectImage = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
// 回显图片
|
||||
function callbackSelected(val) {
|
||||
picModelFlag.value = false;
|
||||
if (!val?.url) return;
|
||||
selected.value.img = val.url;
|
||||
}
|
||||
function handleCancelModal() {
|
||||
editTarget.value = null;
|
||||
selected.value = {};
|
||||
showModal.value = false;
|
||||
}
|
||||
function handleConfirmModal() {
|
||||
if (editTarget.value) {
|
||||
Object.assign(editTarget.value, cloneItem(selected.value));
|
||||
}
|
||||
editTarget.value = null;
|
||||
selected.value = {};
|
||||
showModal.value = false;
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "./modelList/setup-box.scss";
|
||||
.model-item {
|
||||
position: relative;
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
overflow: visible;
|
||||
|
||||
&:hover {
|
||||
.del-btn {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.model-item :deep(.width_1200_auto) {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
margin-left: 0 !important;
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
.del-btn {
|
||||
position: absolute;
|
||||
left: 100%;
|
||||
right: auto;
|
||||
top: 0;
|
||||
width: auto;
|
||||
white-space: nowrap;
|
||||
display: none;
|
||||
z-index: 100;
|
||||
|
||||
&:hover {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
/** 横幅广告 */
|
||||
.horizontal-advert {
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
overflow: hidden;
|
||||
line-height: 0;
|
||||
|
||||
.banner-ad-img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-height: 166px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.default-con {
|
||||
height: 100px;
|
||||
padding-top: 30px;
|
||||
text-align: center;
|
||||
background: #ddd;
|
||||
line-height: normal;
|
||||
}
|
||||
}
|
||||
/** 热门广告 */
|
||||
.hot-advert {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
|
||||
.hot-advert-banner {
|
||||
line-height: 0;
|
||||
width: 100%;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
}
|
||||
.advert-list {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
list-style: none;
|
||||
box-sizing: border-box;
|
||||
background: $theme_color;
|
||||
height: 210px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
> li {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 190px;
|
||||
object-fit: cover;
|
||||
cursor: pointer;
|
||||
border-radius: 10px;
|
||||
transition: all 150ms ease-in-out;
|
||||
&:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: rgba(0, 0, 0, 0.4) 0px 5px 20px 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/** 限时秒杀 */
|
||||
.limit-img {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
img {
|
||||
width: 300px;
|
||||
height: 100px;
|
||||
}
|
||||
}
|
||||
/** 折扣广告 */
|
||||
.discountAdvert-wrap {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.discountAdvert {
|
||||
width: 1300px;
|
||||
height: 596px;
|
||||
margin-left: -100px;
|
||||
box-sizing: border-box;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 1300px 596px;
|
||||
position: relative;
|
||||
> div {
|
||||
padding-left: 295px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
box-sizing: border-box;
|
||||
&:nth-child(1) img {
|
||||
margin: 10px 10px 0 0;
|
||||
}
|
||||
&:nth-child(2) {
|
||||
margin-top: 10px;
|
||||
img {
|
||||
margin: 0 10px 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/** 首页品牌 */
|
||||
.brand {
|
||||
.brand-view {
|
||||
display: flex;
|
||||
margin-top: 10px;
|
||||
.brand-view-content {
|
||||
width: 470px;
|
||||
margin-left: 10px;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 316px;
|
||||
}
|
||||
.brand-view-title {
|
||||
height: 50px;
|
||||
padding: 0 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
.brand-view-content:first-child {
|
||||
width: 240px;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
.brand-list {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
li {
|
||||
width: 121px;
|
||||
height: 112px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border: 1px solid #f5f5f5;
|
||||
margin: -1px -1px 0 0;
|
||||
&:hover {
|
||||
.brand-mash {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
.brand-img {
|
||||
text-align: center;
|
||||
margin-top: 30px;
|
||||
img {
|
||||
width: 100px;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
.brand-mash {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
.el-icon {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
font-size: 15px;
|
||||
}
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
div:last-child {
|
||||
background-color: $theme_color;
|
||||
border-radius: 9px;
|
||||
padding: 0 10px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.refresh {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
.el-icon {
|
||||
font-size: 18px;
|
||||
transition: all 0.3s ease-out;
|
||||
}
|
||||
&:hover {
|
||||
background-color: $theme_color;
|
||||
color: #fff;
|
||||
.el-icon {
|
||||
transform: rotateZ(360deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 装修模态框 内部样式start */
|
||||
.modal-top-advert {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
padding: 0 20px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
.modal-form-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.modal-label {
|
||||
flex-shrink: 0;
|
||||
width: 100px;
|
||||
text-align: right;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
.el-input {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-form-preview {
|
||||
justify-content: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.modal-form-link {
|
||||
align-items: flex-start;
|
||||
|
||||
.modal-label {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.modal-link-field {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
|
||||
.outsideUrl {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tips {
|
||||
padding-left: 112px;
|
||||
margin-bottom: 16px;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,306 +0,0 @@
|
||||
<template>
|
||||
<div class="model-carousel">
|
||||
<div class="nav-body clearfix">
|
||||
<!-- 侧边导航 -->
|
||||
<div class="nav-side">分类占位区</div>
|
||||
<div class="nav-content setup-content">
|
||||
<!-- 轮播图 -->
|
||||
<el-carousel autoplay height="340px">
|
||||
<el-carousel-item v-for="(item, index) in data.options.list" :key="index">
|
||||
<div style="overflow: hidden">
|
||||
<img alt="轮播图" :src="item.img" width="790" height="340" />
|
||||
</div>
|
||||
</el-carousel-item>
|
||||
</el-carousel>
|
||||
<div class="setup-box">
|
||||
<div>
|
||||
<el-button size="small" @click.stop="handleSelectModel">编辑</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-right">
|
||||
<div class="person-msg">
|
||||
<img :src="userInfo.face" v-if="userInfo.face" alt />
|
||||
<el-avatar class="mb_10" v-else :size="80" />
|
||||
<div>
|
||||
Hi,{{ userInfo.nickName || "欢迎来到管理后台" }}
|
||||
</div>
|
||||
<div v-if="userInfo.id">
|
||||
<el-button type="danger" circle>会员中心</el-button>
|
||||
</div>
|
||||
<div v-else>
|
||||
<el-button type="danger" circle>请登录</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="shop-msg">
|
||||
<div>
|
||||
<span>常见问题</span>
|
||||
<ul class="article-list">
|
||||
<li
|
||||
class="ellipsis"
|
||||
:alt="article.title"
|
||||
v-for="(article, index) in articleList"
|
||||
:key="index"
|
||||
>
|
||||
{{ article.title }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog
|
||||
v-model="showModal"
|
||||
title="快捷导航"
|
||||
|
||||
width="800"
|
||||
|
||||
:close-on-click-modal="false"
|
||||
append-to-body destroy-on-close>
|
||||
<div class="modal-tab-bar">
|
||||
<el-button type="primary" size="small" @click="handleAdd">添加轮播</el-button>
|
||||
|
||||
<span class="ml_10">图片尺寸:{{ data.size }}</span>
|
||||
<div style="color: #999" class="fz_12">点击缩略图替换图片</div>
|
||||
<table cellspacing="0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="250">所选图片</th>
|
||||
<th width="250">链接地址</th>
|
||||
<!-- <th width="150">排序</th> -->
|
||||
<th width="250">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(item, index) in data.options.list" :key="index">
|
||||
<td>
|
||||
<img
|
||||
style="cursor: pointer"
|
||||
:src="item.img"
|
||||
@click="handleSelectImg(item)"
|
||||
width="200"
|
||||
height="100"
|
||||
alt=""
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<el-input
|
||||
class="outsideUrl"
|
||||
v-model="item.url"
|
||||
:disabled="!!item.type && item.type !== 'link'"
|
||||
/>
|
||||
</td>
|
||||
<!-- <td><el-input v-model="item.sort"/></td> -->
|
||||
<td>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="handleSelectImg(item)"
|
||||
>选择图片</el-button
|
||||
>
|
||||
<el-button type="info" size="small" @click="handleSelectLink(item)"
|
||||
>选择链接</el-button
|
||||
>
|
||||
<el-button
|
||||
type="danger"
|
||||
plain
|
||||
size="small"
|
||||
@click="handleDel(index)"
|
||||
>删除</el-button
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</el-dialog>
|
||||
<!-- 选择商品。链接 -->
|
||||
<liliDialog ref="liliDialog" @selectedLink="selectedLink"></liliDialog>
|
||||
<!-- 选择图片 -->
|
||||
<el-dialog v-model="picModelFlag" width="1200px" append-to-body destroy-on-close>
|
||||
<ossManage
|
||||
@callback="callbackSelected"
|
||||
:is-component="true"
|
||||
:initialize="picModelFlag"
|
||||
:max-select="1"
|
||||
ref="ossManageRef"
|
||||
/>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, nextTick, getCurrentInstance } from "vue";
|
||||
import ossManage from "@/views/sys/oss-manage/ossManage";
|
||||
import { formatLinkType } from "@/utils/filters";
|
||||
|
||||
defineOptions({ name: "modelCarousel" });
|
||||
|
||||
const props = defineProps(["data"]);
|
||||
|
||||
const { proxy } = getCurrentInstance();
|
||||
|
||||
const showModal = ref(false); // modal显隐
|
||||
const selected = ref(null); // 已选数据
|
||||
const picModelFlag = ref(false); // 选择图片modal
|
||||
const userInfo = reactive({});
|
||||
const articleList = ref([
|
||||
{ title: "促销计算规则" },
|
||||
{ title: "商家申请开店" },
|
||||
{ title: "商家账号注册" },
|
||||
{ title: "促销计算规则" },
|
||||
]);
|
||||
|
||||
const liliDialog = ref(null);
|
||||
// ossManageRef:模板 ref 与组件 import 同名,改名避免互相覆盖
|
||||
const ossManageRef = ref(null);
|
||||
|
||||
function handleSelectModel() {
|
||||
// 编辑模块
|
||||
showModal.value = true;
|
||||
}
|
||||
// 添加轮播图
|
||||
function handleAdd() {
|
||||
props.data.options.list.push({ img: "", url: "", type: "" });
|
||||
proxy.$forceUpdate();
|
||||
}
|
||||
function handleSelectLink(item) {
|
||||
// 选择链接
|
||||
liliDialog.value.open("link");
|
||||
selected.value = item;
|
||||
}
|
||||
function callbackSelected(item) {
|
||||
// 选择图片回调
|
||||
picModelFlag.value = false;
|
||||
selected.value.img = item.url;
|
||||
}
|
||||
function handleDel(index) {
|
||||
// 删除图片
|
||||
props.data.options.list.splice(index, 1);
|
||||
}
|
||||
function selectedLink(val) {
|
||||
// 选择链接回调
|
||||
selected.value.url = formatLinkType(val);
|
||||
selected.value.type =
|
||||
val.___type === "other" && val.url === "" ? "link" : "other";
|
||||
}
|
||||
// 选择图片
|
||||
function handleSelectImg(item) {
|
||||
selected.value = item;
|
||||
picModelFlag.value = true;
|
||||
nextTick(() => {
|
||||
if (ossManageRef.value) {
|
||||
ossManageRef.value.selectImage = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "./setup-box.scss";
|
||||
.model-carousel {
|
||||
width: 1200px;
|
||||
height: 340px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nav-item li {
|
||||
float: left;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-left: 30px;
|
||||
}
|
||||
.nav-item a {
|
||||
text-decoration: none;
|
||||
color: #555555;
|
||||
}
|
||||
.nav-item a:hover {
|
||||
color: $theme_color;
|
||||
}
|
||||
/*大的导航信息,包含导航,幻灯片等*/
|
||||
.nav-body {
|
||||
width: 1200px;
|
||||
height: 340px;
|
||||
margin: 0px auto;
|
||||
}
|
||||
.nav-side {
|
||||
height: 100%;
|
||||
width: 200px;
|
||||
padding: 0px;
|
||||
color: #fff;
|
||||
float: left;
|
||||
background-color: #6e6568;
|
||||
line-height: 340px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/*导航内容*/
|
||||
.nav-content {
|
||||
width: 800px;
|
||||
height: 340px;
|
||||
overflow: hidden;
|
||||
float: left;
|
||||
position: relative;
|
||||
}
|
||||
.nav-right {
|
||||
float: left;
|
||||
width: 200px;
|
||||
.person-msg {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
margin: 20px auto;
|
||||
|
||||
button {
|
||||
height: 25px !important;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.el-btn-default {
|
||||
color: $theme_color;
|
||||
border-color: $theme_color;
|
||||
}
|
||||
|
||||
img {
|
||||
margin-bottom: 10px;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
.shop-msg {
|
||||
div {
|
||||
width: 100%;
|
||||
margin: 10px 27px;
|
||||
span {
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
margin-left: 5px;
|
||||
}
|
||||
span:nth-child(1) {
|
||||
color: $theme_color;
|
||||
margin-left: 0;
|
||||
}
|
||||
span:nth-child(2) {
|
||||
font-weight: normal;
|
||||
}
|
||||
span:nth-child(3):hover {
|
||||
color: $theme_color;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0 30px;
|
||||
li {
|
||||
cursor: pointer;
|
||||
margin: 5px 0;
|
||||
color: #999395;
|
||||
&:hover {
|
||||
color: $theme_color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,195 +0,0 @@
|
||||
<template>
|
||||
<div class="model-carousel1" :style="{ background: bgColor }">
|
||||
<div class="nav-body clearfix">
|
||||
<!-- 侧边导航 -->
|
||||
<div class="nav-side">分类占位区</div>
|
||||
<div class="nav-content setup-content">
|
||||
<!-- 轮播图 -->
|
||||
<el-carousel autoplay @change="autoChange">
|
||||
<el-carousel-item v-for="(item, index) in data.options.list" :key="index">
|
||||
<div style="overflow: hidden">
|
||||
<img alt="轮播图" :src="item.img" width="1200" height="470" />
|
||||
</div>
|
||||
</el-carousel-item>
|
||||
</el-carousel>
|
||||
<div class="setup-box">
|
||||
<div>
|
||||
<el-button size="small" @click.stop="handleSelectModel">编辑</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog
|
||||
v-model="showModal"
|
||||
title="快捷导航"
|
||||
|
||||
width="800"
|
||||
|
||||
:close-on-click-modal="false"
|
||||
append-to-body destroy-on-close>
|
||||
<div class="modal-tab-bar">
|
||||
<el-button type="primary" size="small" @click="handleAdd">添加轮播</el-button>
|
||||
|
||||
<span class="ml_10">图片尺寸:{{ data.size }}</span>
|
||||
<span style="color: red" class="fz_12 ml_10"
|
||||
>点击缩略图替换图片、点击颜色选择器选择背景色</span
|
||||
>
|
||||
<table cellspacing="0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="250">所选图片</th>
|
||||
<th width="250">链接地址</th>
|
||||
<th width="250">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(item, index) in data.options.list" :key="index">
|
||||
<td>
|
||||
<img
|
||||
style="cursor: pointer"
|
||||
:src="item.img"
|
||||
@click="handleSelectImg(item)"
|
||||
width="200"
|
||||
height="100"
|
||||
alt=""
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<el-input
|
||||
class="outsideUrl"
|
||||
v-model="item.url"
|
||||
:disabled="!!item.type && item.type !== 'link'"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<el-button type="info" size="small" @click="handleSelectLink(item)"
|
||||
>选择链接</el-button
|
||||
>
|
||||
<el-color-picker size="small" v-model="item.bgColor" />
|
||||
|
||||
<el-button
|
||||
type="danger"
|
||||
plain
|
||||
size="small"
|
||||
@click="handleDel(index)"
|
||||
>删除</el-button
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</el-dialog>
|
||||
<!-- 选择商品。链接 -->
|
||||
<liliDialog ref="liliDialog" @selectedLink="selectedLink"></liliDialog>
|
||||
<!-- 选择图片 -->
|
||||
<el-dialog v-model="picModelFlag" width="1200px" append-to-body destroy-on-close>
|
||||
<ossManage @callback="callbackSelected" :is-component="true" :initialize="picModelFlag" :max-select="1" ref="ossManageRef" />
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { getCurrentInstance, nextTick, onMounted, ref } from "vue";
|
||||
import ossManage from "@/views/sys/oss-manage/ossManage";
|
||||
import { formatLinkType } from "@/utils/filters";
|
||||
|
||||
defineOptions({ name: "modelCarousel" });
|
||||
|
||||
const props = defineProps(["data"]);
|
||||
|
||||
const { proxy } = getCurrentInstance();
|
||||
|
||||
const showModal = ref(false); // modal显隐
|
||||
const selected = ref(null); // 已选数据
|
||||
const picModelFlag = ref(false); // 选择图片modal
|
||||
const bgColor = ref("#fff"); // 轮播背景色
|
||||
|
||||
const liliDialog = ref(null);
|
||||
const ossManageRef = ref(null);
|
||||
|
||||
onMounted(() => {
|
||||
bgColor.value = props.data.options.list[0].bgColor;
|
||||
});
|
||||
|
||||
// 编辑模块
|
||||
function handleSelectModel() {
|
||||
showModal.value = true;
|
||||
}
|
||||
// 自动切换时改变背景色
|
||||
function autoChange(oVal, val) {
|
||||
bgColor.value = props.data.options.list[val].bgColor;
|
||||
}
|
||||
// 添加轮播图片和链接
|
||||
function handleAdd() {
|
||||
props.data.options.list.push({ img: "", url: "", bgColor: "#fff" });
|
||||
proxy.$forceUpdate();
|
||||
}
|
||||
// 打开选择链接modal
|
||||
function handleSelectLink(item) {
|
||||
liliDialog.value.open("link");
|
||||
selected.value = item;
|
||||
}
|
||||
// 选择图片回调
|
||||
function callbackSelected(item) {
|
||||
picModelFlag.value = false;
|
||||
selected.value.img = item.url;
|
||||
}
|
||||
// 删除图片
|
||||
function handleDel(index) {
|
||||
props.data.options.list.splice(index, 1);
|
||||
}
|
||||
// 选择链接回调
|
||||
function selectedLink(val) {
|
||||
selected.value.url = formatLinkType(val);
|
||||
selected.value.type =
|
||||
val.___type === "other" && val.url === "" ? "link" : "other";
|
||||
}
|
||||
// 打开选择图片modal
|
||||
function handleSelectImg(item) {
|
||||
selected.value = item;
|
||||
picModelFlag.value = true;
|
||||
nextTick(() => {
|
||||
if (ossManageRef.value) {
|
||||
ossManageRef.value.selectImage = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "./setup-box.scss";
|
||||
.model-carousel1 {
|
||||
width: 1500px;
|
||||
height: 470px;
|
||||
margin-left: -150px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/*大的导航信息,包含导航,幻灯片等*/
|
||||
.nav-body {
|
||||
width: 1200px;
|
||||
height: 470px;
|
||||
margin: 0px auto;
|
||||
}
|
||||
.nav-side {
|
||||
height: 100%;
|
||||
width: 200px;
|
||||
padding: 0px;
|
||||
color: #fff;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
line-height: 470px;
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/*导航内容*/
|
||||
.nav-content {
|
||||
width: 1200px;
|
||||
height: 470px;
|
||||
overflow: hidden;
|
||||
float: left;
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
@@ -1,404 +0,0 @@
|
||||
<template>
|
||||
<div class="model-carousel2">
|
||||
<div class="nav-body clearfix">
|
||||
<!-- 侧边导航 -->
|
||||
<div class="nav-side">分类占位区</div>
|
||||
<div class="nav-content setup-content">
|
||||
<!-- 轮播图 -->
|
||||
<el-carousel autoplay height="340px">
|
||||
<el-carousel-item v-for="(item, index) in data.options.list" :key="index">
|
||||
<div style="overflow: hidden">
|
||||
<img alt="轮播图" :src="item.img" width="590" height="470" />
|
||||
</div>
|
||||
</el-carousel-item>
|
||||
</el-carousel>
|
||||
<div class="setup-box">
|
||||
<div>
|
||||
<el-button size="small" @click.stop="handleSelectModel">编辑</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-content1 setup-content">
|
||||
<!-- 轮播图 -->
|
||||
<el-carousel autoplay :autoplay-speed="5000">
|
||||
<el-carousel-item
|
||||
v-for="(item, index) in data.options.listRight"
|
||||
:key="index"
|
||||
>
|
||||
<div class="mb_10">
|
||||
<img alt="轮播图" :src="item[0].img" width="190" height="150" />
|
||||
</div>
|
||||
<div class="mb_10">
|
||||
<img alt="轮播图" :src="item[1].img" width="190" height="150" />
|
||||
</div>
|
||||
<div>
|
||||
<img alt="轮播图" :src="item[2].img" width="190" height="150" />
|
||||
</div>
|
||||
</el-carousel-item>
|
||||
</el-carousel>
|
||||
<div class="setup-box">
|
||||
<div>
|
||||
<el-button size="small" @click.stop="handleSelectModel">编辑</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-right">
|
||||
<div class="person-msg">
|
||||
<img :src="userInfo.face" v-if="userInfo.face" alt />
|
||||
<el-avatar class="mb_10" v-else :size="80" />
|
||||
<div>
|
||||
Hi,{{ userInfo.nickName || "欢迎来到管理后台" }}
|
||||
</div>
|
||||
<div v-if="userInfo.id">
|
||||
<el-button type="danger" circle>会员中心</el-button>
|
||||
</div>
|
||||
<div v-else>
|
||||
<el-button type="danger" circle>请登录</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="shop-msg">
|
||||
<div>
|
||||
<span>常见问题</span>
|
||||
<ul class="article-list">
|
||||
<li
|
||||
class="ellipsis"
|
||||
:alt="article.title"
|
||||
v-for="(article, index) in articleList"
|
||||
:key="index"
|
||||
>
|
||||
{{ article.title }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 左侧轮播装修 -->
|
||||
<el-dialog
|
||||
v-model="showModal"
|
||||
title="快捷导航"
|
||||
|
||||
width="800"
|
||||
|
||||
:close-on-click-modal="false"
|
||||
append-to-body destroy-on-close>
|
||||
<div class="modal-tab-bar">
|
||||
<el-button type="primary" size="small" @click="handleAdd">添加轮播</el-button>
|
||||
|
||||
<span class="ml_10">图片尺寸:{{ data.size }}</span>
|
||||
<span style="color: red" class="fz_12 ml_10">点击缩略图替换图片</span>
|
||||
<table cellspacing="0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="250">所选图片</th>
|
||||
<th width="250">链接地址</th>
|
||||
<th width="250">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(item, index) in data.options.list" :key="index">
|
||||
<td>
|
||||
<img
|
||||
style="cursor: pointer"
|
||||
:src="item.img"
|
||||
@click="handleSelectImg(item)"
|
||||
width="200"
|
||||
height="100"
|
||||
alt=""
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<el-input
|
||||
class="outsideUrl"
|
||||
v-model="item.url"
|
||||
:disabled="!!item.type && item.type !== 'link'"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<el-button type="info" size="small" @click="handleSelectLink(item)"
|
||||
>选择链接</el-button
|
||||
>
|
||||
<el-button
|
||||
type="danger"
|
||||
plain
|
||||
size="small"
|
||||
@click="handleDel(index)"
|
||||
>删除</el-button
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</el-dialog>
|
||||
<!-- 右侧轮播装修 -->
|
||||
<el-dialog
|
||||
v-model="showModal"
|
||||
title="右侧装修"
|
||||
|
||||
width="800"
|
||||
|
||||
:close-on-click-modal="false"
|
||||
append-to-body destroy-on-close>
|
||||
<div class="modal-tab-bar">
|
||||
<el-button type="primary" size="small" @click="handleAddGroup"
|
||||
>添加组</el-button
|
||||
>
|
||||
|
||||
<span class="ml_10">图片尺寸:{{ data.size }}</span>
|
||||
<span style="color: red" class="fz_12 ml_10">点击缩略图替换图片</span>
|
||||
<el-tabs
|
||||
type="card"
|
||||
closable
|
||||
@tab-remove="handleTabRemove"
|
||||
class="mt_10"
|
||||
>
|
||||
<el-tab-pane
|
||||
:label="'图片组' + (gIndex + 1)"
|
||||
v-for="(group, gIndex) in data.options.listRight"
|
||||
:key="gIndex"
|
||||
>
|
||||
<table cellspacing="0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="250">所选图片</th>
|
||||
<th width="250">链接地址</th>
|
||||
<th width="250">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(item, index) in group" :key="index">
|
||||
<td>
|
||||
<img
|
||||
style="cursor: pointer"
|
||||
:src="item.img"
|
||||
@click="handleSelectImg(item)"
|
||||
width="200"
|
||||
height="100"
|
||||
alt=""
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<el-input
|
||||
class="outsideUrl"
|
||||
v-model="item.url"
|
||||
:disabled="!!item.type && item.type !== 'link'"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<el-button
|
||||
type="info"
|
||||
size="small"
|
||||
@click="handleSelectLink(item)"
|
||||
>选择链接</el-button
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</el-dialog>
|
||||
<!-- 选择商品。链接 -->
|
||||
<liliDialog ref="liliDialog" @selectedLink="selectedLink"></liliDialog>
|
||||
<!-- 选择图片 -->
|
||||
<el-dialog v-model="picModelFlag" width="1200px" append-to-body destroy-on-close>
|
||||
<ossManage @callback="callbackSelected" :is-component="true" :initialize="picModelFlag" :max-select="1" ref="ossManageRef" />
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, nextTick, getCurrentInstance } from "vue";
|
||||
import ossManage from "@/views/sys/oss-manage/ossManage";
|
||||
import { formatLinkType } from "@/utils/filters";
|
||||
|
||||
defineOptions({ name: "modelCarousel" });
|
||||
|
||||
const props = defineProps(["data"]);
|
||||
|
||||
const { proxy } = getCurrentInstance();
|
||||
|
||||
const showModal = ref(false); // modal显隐
|
||||
const selected = ref(null); // 已选数据
|
||||
const picModelFlag = ref(false); // 选择图片modal
|
||||
const userInfo = reactive({});
|
||||
const articleList = ref([
|
||||
{ title: "促销计算规则" },
|
||||
{ title: "商家申请开店" },
|
||||
{ title: "商家账号注册" },
|
||||
{ title: "促销计算规则" },
|
||||
]);
|
||||
|
||||
const liliDialog = ref(null);
|
||||
// ossManageRef:模板 ref 与组件 import 同名,改名避免互相覆盖
|
||||
const ossManageRef = ref(null);
|
||||
|
||||
function handleSelectModel() {
|
||||
// 编辑模块
|
||||
showModal.value = true;
|
||||
}
|
||||
// 添加轮播
|
||||
function handleAdd() {
|
||||
props.data.options.list.push({ img: "", url: "" });
|
||||
proxy.$forceUpdate();
|
||||
}
|
||||
// 添加图片组
|
||||
function handleAddGroup() {
|
||||
props.data.options.listRight.push([
|
||||
{ img: "", url: "" },
|
||||
{ img: "", url: "" },
|
||||
{ img: "", url: "" },
|
||||
]);
|
||||
}
|
||||
// 删除图片组
|
||||
function handleTabRemove(index) {
|
||||
props.data.options.listRight.splice(index, 1);
|
||||
}
|
||||
// 打开图片链接
|
||||
function handleSelectLink(item) {
|
||||
liliDialog.value.open("link");
|
||||
selected.value = item;
|
||||
}
|
||||
function callbackSelected(item) {
|
||||
// 选择图片回调
|
||||
picModelFlag.value = false;
|
||||
selected.value.img = item.url;
|
||||
}
|
||||
// 删除图片
|
||||
function handleDel(index) {
|
||||
props.data.options.list.splice(index, 1);
|
||||
}
|
||||
function selectedLink(val) {
|
||||
// 选择链接回调
|
||||
selected.value.url = formatLinkType(val);
|
||||
selected.value.type =
|
||||
val.___type === "other" && val.url === "" ? "link" : "other";
|
||||
}
|
||||
// 打开选择图片modal
|
||||
function handleSelectImg(item) {
|
||||
selected.value = item;
|
||||
picModelFlag.value = true;
|
||||
nextTick(() => {
|
||||
if (ossManageRef.value) {
|
||||
ossManageRef.value.selectImage = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "./setup-box.scss";
|
||||
.model-carousel2 {
|
||||
width: 1200px;
|
||||
height: 470px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nav-item li {
|
||||
float: left;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-left: 30px;
|
||||
}
|
||||
.nav-item a {
|
||||
text-decoration: none;
|
||||
color: #555555;
|
||||
}
|
||||
.nav-item a:hover {
|
||||
color: $theme_color;
|
||||
}
|
||||
/*大的导航信息,包含导航,幻灯片等*/
|
||||
.nav-body {
|
||||
width: 1200px;
|
||||
height: 470px;
|
||||
margin: 0px auto;
|
||||
}
|
||||
.nav-side {
|
||||
height: 100%;
|
||||
width: 200px;
|
||||
padding: 0px;
|
||||
color: #fff;
|
||||
float: left;
|
||||
background-color: #6e6568;
|
||||
line-height: 470px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/*导航内容*/
|
||||
.nav-content,
|
||||
.nav-content1 {
|
||||
width: 590px;
|
||||
height: 470px;
|
||||
overflow: hidden;
|
||||
float: left;
|
||||
position: relative;
|
||||
margin-left: 10px;
|
||||
}
|
||||
.nav-content1 {
|
||||
width: 190px;
|
||||
}
|
||||
.nav-right {
|
||||
float: left;
|
||||
width: 190px;
|
||||
margin-left: 10px;
|
||||
.person-msg {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
margin: 20px auto;
|
||||
|
||||
button {
|
||||
height: 25px !important;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.el-btn-default {
|
||||
color: $theme_color;
|
||||
border-color: $theme_color;
|
||||
}
|
||||
|
||||
img {
|
||||
margin-bottom: 10px;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
.shop-msg {
|
||||
div {
|
||||
width: 100%;
|
||||
margin: 10px 27px;
|
||||
span {
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
margin-left: 5px;
|
||||
}
|
||||
span:nth-child(1) {
|
||||
color: $theme_color;
|
||||
margin-left: 0;
|
||||
}
|
||||
span:nth-child(2) {
|
||||
font-weight: normal;
|
||||
}
|
||||
span:nth-child(3):hover {
|
||||
color: $theme_color;
|
||||
}
|
||||
}
|
||||
ul {
|
||||
li {
|
||||
cursor: pointer;
|
||||
margin: 5px 0;
|
||||
color: #999395;
|
||||
width: 150px;
|
||||
font-size: 12px;
|
||||
&:hover {
|
||||
color: $theme_color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,324 +0,0 @@
|
||||
<template>
|
||||
<div class="first-page-advert">
|
||||
<div
|
||||
class="item setup-content"
|
||||
:style="{
|
||||
backgroundImage: `linear-gradient(to right, ${item.fromColor}, ${item.toColor})`,
|
||||
}"
|
||||
v-for="(item, index) in options.list"
|
||||
:key="index"
|
||||
>
|
||||
<div>
|
||||
<span class="line top-line"></span>
|
||||
<p>{{ item.name }}</p>
|
||||
<span class="line btm-line"></span>
|
||||
<p>{{ item.describe }}</p>
|
||||
</div>
|
||||
<img :src="item.img" width="170" height="170" alt="" />
|
||||
<div class="setup-box">
|
||||
<div>
|
||||
<el-button size="small" @click.stop="handleSelectModel(item)"
|
||||
>编辑</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog
|
||||
v-model="showModal"
|
||||
title="装修"
|
||||
width="800px"
|
||||
:close-on-click-modal="false"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
>
|
||||
<div class="modal-top-advert">
|
||||
<div class="modal-form-item modal-form-preview">
|
||||
<img
|
||||
class="show-image"
|
||||
width="170"
|
||||
height="170"
|
||||
:src="selected.img"
|
||||
alt
|
||||
/>
|
||||
</div>
|
||||
<div class="modal-form-item">
|
||||
<span class="modal-label">图片主标题:</span>
|
||||
<el-input v-model="selected.name" />
|
||||
</div>
|
||||
<div class="modal-form-item">
|
||||
<span class="modal-label">图片描述:</span>
|
||||
<el-input v-model="selected.describe" />
|
||||
</div>
|
||||
<div class="tips">建议尺寸:{{ selected.size }}</div>
|
||||
<div class="modal-form-item modal-form-link">
|
||||
<span class="modal-label">图片链接:</span>
|
||||
<div class="modal-link-field">
|
||||
<el-input
|
||||
class="outsideUrl"
|
||||
v-model="selected.url"
|
||||
:disabled="!!selected.type && selected.type !== 'link'"
|
||||
placeholder="https://"
|
||||
/>
|
||||
<el-button size="small" type="primary" @click="handleSelectLink"
|
||||
>选择链接</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-form-item">
|
||||
<span class="modal-label">渐变背景色:</span>
|
||||
<div class="modal-color-field">
|
||||
<el-input v-model="selected.fromColor" />
|
||||
<el-color-picker
|
||||
v-if="selected.fromColor"
|
||||
v-model="selected.fromColor"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-form-item">
|
||||
<span class="modal-label">渐变背景色:</span>
|
||||
<div class="modal-color-field">
|
||||
<el-input v-model="selected.toColor" />
|
||||
<el-color-picker
|
||||
v-if="selected.toColor"
|
||||
v-model="selected.toColor"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-form-item modal-form-exhibition">
|
||||
<div
|
||||
:style="{
|
||||
backgroundImage: `linear-gradient(to right, ${selected.fromColor}, ${selected.toColor})`,
|
||||
}"
|
||||
class="exhibition"
|
||||
></div>
|
||||
</div>
|
||||
<div class="modal-form-item">
|
||||
<span class="modal-label">选择图片:</span>
|
||||
<el-button size="small" type="primary" @click="handleSelectImg"
|
||||
>选择图片</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="handleCancelModal">取消</el-button>
|
||||
<el-button type="primary" @click="handleConfirmModal">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<!-- 选择商品。链接 -->
|
||||
<liliDialog ref="liliDialog" @selectedLink="selectedLink"></liliDialog>
|
||||
<!-- 选择图片 -->
|
||||
<el-dialog v-model="picModelFlag" width="1200px" append-to-body destroy-on-close>
|
||||
<ossManage
|
||||
@callback="callbackSelected"
|
||||
:is-component="true"
|
||||
:initialize="picModelFlag"
|
||||
:max-select="1"
|
||||
ref="ossManageRef"
|
||||
/>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, nextTick } from "vue";
|
||||
import ossManage from "@/views/sys/oss-manage/ossManage";
|
||||
import { formatLinkType } from "@/utils/filters";
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const options = ref(props.data.options); // 当前类型数据
|
||||
const showModal = ref(false); // modal显隐
|
||||
const selected = ref({}); // 弹窗编辑草稿
|
||||
const editTarget = ref(null); // 确定后写回的原数据
|
||||
const picModelFlag = ref(false); // 图片选择器
|
||||
|
||||
// ossManageRef:模板 ref 与组件 import 同名,改名避免互相覆盖
|
||||
const liliDialog = ref(null);
|
||||
const ossManageRef = ref(null);
|
||||
|
||||
function cloneItem(item) {
|
||||
return JSON.parse(JSON.stringify(item || {}));
|
||||
}
|
||||
// 打开装修modal
|
||||
function handleSelectModel(item, type) {
|
||||
editTarget.value = item;
|
||||
selected.value = cloneItem(item);
|
||||
showModal.value = true;
|
||||
}
|
||||
function handleSelectLink(item, index) {
|
||||
// 调起选择链接弹窗
|
||||
liliDialog.value.open("link");
|
||||
}
|
||||
// 选择链接回调
|
||||
function selectedLink(val) {
|
||||
selected.value.url = formatLinkType(val);
|
||||
selected.value.type =
|
||||
val.___type === "other" && val.url === "" ? "link" : "other";
|
||||
}
|
||||
function handleSelectImg() {
|
||||
// 选择图片
|
||||
picModelFlag.value = true;
|
||||
nextTick(() => {
|
||||
if (ossManageRef.value) {
|
||||
ossManageRef.value.selectImage = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
// 选择图片回调
|
||||
function callbackSelected(val) {
|
||||
picModelFlag.value = false;
|
||||
if (!val?.url) return;
|
||||
selected.value.img = val.url;
|
||||
}
|
||||
function handleCancelModal() {
|
||||
editTarget.value = null;
|
||||
selected.value = {};
|
||||
showModal.value = false;
|
||||
}
|
||||
function handleConfirmModal() {
|
||||
if (editTarget.value) {
|
||||
Object.assign(editTarget.value, cloneItem(selected.value));
|
||||
}
|
||||
editTarget.value = null;
|
||||
selected.value = {};
|
||||
showModal.value = false;
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "./setup-box.scss";
|
||||
.first-page-advert {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
.item {
|
||||
width: 393px;
|
||||
height: 170px;
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
img {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
&:nth-of-type(1),
|
||||
&:nth-of-type(2),
|
||||
&:nth-of-type(3) {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
p:nth-of-type(1) {
|
||||
margin: 3px 0;
|
||||
font-size: 18px;
|
||||
color: #fff;
|
||||
}
|
||||
p:nth-of-type(2) {
|
||||
margin-top: 3px;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
.line {
|
||||
position: relative;
|
||||
display: block;
|
||||
height: 2px;
|
||||
background: url(../../../assets/festival_icon.png);
|
||||
z-index: 1;
|
||||
}
|
||||
.top-line {
|
||||
width: 78px;
|
||||
background-position: -1px -3px;
|
||||
}
|
||||
.btm-line {
|
||||
background-position: 0 -11px;
|
||||
width: 154px;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-top-advert {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
padding: 0 20px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
.modal-form-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.modal-label {
|
||||
flex-shrink: 0;
|
||||
width: 100px;
|
||||
text-align: right;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
.el-input {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-form-preview {
|
||||
justify-content: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.modal-form-link {
|
||||
align-items: flex-start;
|
||||
|
||||
.modal-label {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.modal-link-field {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
|
||||
.outsideUrl {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-color-field {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
.el-input {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-form-exhibition {
|
||||
padding-left: 112px;
|
||||
box-sizing: border-box;
|
||||
|
||||
.exhibition {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.tips {
|
||||
padding-left: 112px;
|
||||
margin-bottom: 16px;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,438 +0,0 @@
|
||||
<template>
|
||||
<div class="model" v-if="data">
|
||||
<div class="for-your">{{ data.options.title }}</div>
|
||||
|
||||
<div class="flex card">
|
||||
<div class="left">
|
||||
<img :src="data.options.data.image.src" alt="" />
|
||||
</div>
|
||||
<div class="right flex">
|
||||
<!-- 商品区 -->
|
||||
<div class="goods-list">
|
||||
<div
|
||||
class="goods-item"
|
||||
:key="index"
|
||||
v-for="(item, index) in data.options.data.list"
|
||||
>
|
||||
<div>
|
||||
<div class="goods-name">{{ item.title }}</div>
|
||||
<div class="goods-desc">{{ item.desc }}</div>
|
||||
</div>
|
||||
<img class="goods-img" :src="item.img" alt="" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 热卖区 -->
|
||||
<div class="hot-list">
|
||||
<div class="hot-title">{{ data.options.data.hot.title }}</div>
|
||||
<div
|
||||
class="hot-item flex flex-a-c"
|
||||
v-for="(item, index) in data.options.data.hot.list"
|
||||
:key="index"
|
||||
>
|
||||
<img :src="item.img" alt="" />
|
||||
<div class="hot-goods">
|
||||
<div class="hot-goods-title">{{ item.title }}</div>
|
||||
<div>
|
||||
<div class="hot-price">¥{{ $filters.unitPrice(item.price) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setup-box">
|
||||
<div>
|
||||
<el-button size="small" @click.stop="handleSelectModel()">编辑 </el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
v-model="showModal"
|
||||
title="装修"
|
||||
|
||||
width="800"
|
||||
|
||||
:close-on-click-modal="false"
|
||||
append-to-body destroy-on-close>
|
||||
<div>
|
||||
<div class="modal-tab-bar">
|
||||
<div>
|
||||
主标题:
|
||||
<el-input style="width: 200px" v-model="data.options.title" />
|
||||
</div>
|
||||
<el-divider content-position="left">左侧商品部分</el-divider>
|
||||
<div class="flex">
|
||||
<div>图片 :</div>
|
||||
<div>
|
||||
<img class="image" :src="data.options.data.image.src" alt="" />
|
||||
<div>
|
||||
<div>推荐尺寸:{{ data.options.data.image.size }}</div>
|
||||
<div class="link-src">
|
||||
链接地址:
|
||||
<el-input
|
||||
style="width: 200px"
|
||||
v-model="data.options.data.image.url"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
<el-button
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="handleSelectImg('image')"
|
||||
>选择图片
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
class="ml_10"
|
||||
type="primary"
|
||||
@click="handleSelectLink('image')"
|
||||
>选择链接
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider content-position="left">中间商品部分</el-divider>
|
||||
<div class="flex column-list">
|
||||
<div
|
||||
v-for="(item, index) in data.options.data.list"
|
||||
:key="index"
|
||||
class="draggable"
|
||||
>
|
||||
<div class="flex">
|
||||
<img alt="楼层图片" :src="item.img" class="column-img" />
|
||||
<div class="column-goods-list">
|
||||
<div><el-input v-model="item.title" /></div>
|
||||
<div><el-input v-model="item.desc" /></div>
|
||||
<div><el-input disabled v-model="item.url" /></div>
|
||||
<div>{{ item.size }}</div>
|
||||
<el-button
|
||||
size="small"
|
||||
class="ml_10"
|
||||
type="primary"
|
||||
@click="handleSelectLink('list', index)"
|
||||
>选择链接
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
class="ml_10"
|
||||
type="primary"
|
||||
@click="handleSelectImg('list', index)"
|
||||
>选择图片
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-divider content-position="left">右侧商品部分</el-divider>
|
||||
<div class="flex">
|
||||
<div>右侧标题 :</div>
|
||||
<el-input
|
||||
style="width: 200px"
|
||||
v-model="data.options.data.hot.title"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex column-list">
|
||||
<div
|
||||
v-for="(item, index) in data.options.data.hot.list"
|
||||
:key="index"
|
||||
class="draggable"
|
||||
>
|
||||
<div class="flex">
|
||||
<img alt="楼层图片" :src="item.img" class="column-img" />
|
||||
<div class="column-goods-list">
|
||||
<el-button
|
||||
size="small"
|
||||
class="ml_10"
|
||||
type="primary"
|
||||
@click="handleSelectGoods('hot', index)"
|
||||
>选择商品
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 选择商品。链接 -->
|
||||
<liliDialog
|
||||
ref="liliDialogRef"
|
||||
@selectedLink="selectedLink"
|
||||
@selectedGoodsData="selectedGoodsData"
|
||||
></liliDialog>
|
||||
|
||||
<!-- 选择图片 -->
|
||||
<el-dialog v-model="picModelFlag" width="1200px" append-to-body destroy-on-close>
|
||||
<ossManage
|
||||
@callback="callbackSelected"
|
||||
:is-component="true"
|
||||
:initialize="picModelFlag"
|
||||
:max-select="1"
|
||||
ref="ossManageRef"
|
||||
/>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, nextTick } from "vue";
|
||||
import { formatLinkType } from "@/utils/filters";
|
||||
import ossManage from "@/views/sys/oss-manage/ossManage.vue";
|
||||
|
||||
defineOptions({ name: "forYour" });
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
const showModal = ref(false);
|
||||
const picModelFlag = ref(false);
|
||||
const selectedData = ref(""); //选中的数据
|
||||
const goodsIndex = ref(0); // 商品索引
|
||||
const defaultCallbackImageType = ref("");
|
||||
|
||||
const liliDialogRef = ref(null);
|
||||
const ossManageRef = ref(null);
|
||||
|
||||
function handleSelectImg(type, index) {
|
||||
defaultCallbackImageType.value = type;
|
||||
goodsIndex.value = index;
|
||||
// 选择图片
|
||||
picModelFlag.value = true;
|
||||
nextTick(() => {
|
||||
if (ossManageRef.value) {
|
||||
ossManageRef.value.selectImage = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleSelectGoods(val, index) {
|
||||
liliDialogRef.value.open("goods");
|
||||
liliDialogRef.value.singleGoods();
|
||||
selectedData.value = val;
|
||||
goodsIndex.value = index;
|
||||
}
|
||||
function handleSelectLink(val, index) {
|
||||
// 调起选择链接弹窗
|
||||
liliDialogRef.value.open("link");
|
||||
selectedData.value = val;
|
||||
|
||||
goodsIndex.value = index;
|
||||
}
|
||||
// 回显图片
|
||||
function callbackSelected(val) {
|
||||
picModelFlag.value = false;
|
||||
if (defaultCallbackImageType.value == "image") {
|
||||
props.data.options.data.image.src = val.url;
|
||||
} else {
|
||||
props.data.options.data[defaultCallbackImageType.value][
|
||||
goodsIndex.value
|
||||
].img = val.url;
|
||||
}
|
||||
}
|
||||
// 选择商品回调
|
||||
function selectedGoodsData(val) {
|
||||
props.data.options.data.hot.list[goodsIndex.value].img = val[0].thumbnail;
|
||||
props.data.options.data.hot.list[goodsIndex.value].title = val[0].goodsName;
|
||||
props.data.options.data.hot.list[goodsIndex.value].price = val[0].price;
|
||||
props.data.options.data.hot.list[goodsIndex.value].url =
|
||||
formatLinkType(val[0]);
|
||||
}
|
||||
// 选择链接回调
|
||||
function selectedLink(val) {
|
||||
if (selectedData.value == "image") {
|
||||
props.data.options.data.image.url =
|
||||
formatLinkType(val);
|
||||
} else {
|
||||
props.data.options.data.list[
|
||||
goodsIndex.value
|
||||
].url = formatLinkType(val);
|
||||
}
|
||||
}
|
||||
/*
|
||||
* 点击不同模块进行编辑
|
||||
* 判断也很简单,就是第一个第二个模块进行判断
|
||||
*
|
||||
* */
|
||||
function handleSelectModel() {
|
||||
showModal.value = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "./setup-box.scss";
|
||||
.goods-list {
|
||||
display: flex;
|
||||
width: 558px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.goods-item {
|
||||
width: 278px;
|
||||
height: 277px;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
border-right: 1.4px solid #e2e2e2;
|
||||
&:nth-of-type(3) {
|
||||
border-top: 1.4px solid #e2e2e2;
|
||||
}
|
||||
&:nth-of-type(4) {
|
||||
border-top: 1.4px solid #e2e2e2;
|
||||
}
|
||||
}
|
||||
.active {
|
||||
color: #f31947;
|
||||
}
|
||||
|
||||
.goods-name {
|
||||
margin-top: 23px;
|
||||
font-size: 25px;
|
||||
font-weight: normal;
|
||||
line-height: 30px;
|
||||
text-align: center;
|
||||
letter-spacing: 0px;
|
||||
|
||||
color: #333333;
|
||||
}
|
||||
.goods-img {
|
||||
width: 190px;
|
||||
height: 156px;
|
||||
border-radius: 10px;
|
||||
margin-top: 19px;
|
||||
}
|
||||
.goods-desc {
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
line-height: 17px;
|
||||
text-align: center;
|
||||
margin-top: 11px;
|
||||
letter-spacing: 0px;
|
||||
|
||||
color: #333333;
|
||||
}
|
||||
.for-your {
|
||||
font-size: 35px;
|
||||
font-weight: normal;
|
||||
line-height: 42px;
|
||||
text-align: center;
|
||||
letter-spacing: 0px;
|
||||
margin-bottom: 32px;
|
||||
|
||||
color: #333333;
|
||||
}
|
||||
.column-goods-list {
|
||||
> div {
|
||||
margin: 10px;
|
||||
}
|
||||
}
|
||||
.link-src {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.card,
|
||||
.right-card {
|
||||
opacity: 1;
|
||||
background: #ffffff;
|
||||
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
.setup-box {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.column-img {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.column-list {
|
||||
flex-wrap: wrap;
|
||||
|
||||
> div {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.image {
|
||||
max-width: 100px;
|
||||
}
|
||||
|
||||
.modal-tab-bar {
|
||||
> .flex {
|
||||
align-items: center;
|
||||
margin: 10px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.draggable {
|
||||
margin: 10px 0;
|
||||
> .flex {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
.left {
|
||||
width: 346px;
|
||||
height: 554px;
|
||||
border-radius: 9.8px 0px 0px 9.8px;
|
||||
> img {
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
.right {
|
||||
width: 839px;
|
||||
height: 554px;
|
||||
border-radius: 0px 9.8px 9.8px 0px;
|
||||
}
|
||||
.hot-list {
|
||||
width: 279px;
|
||||
}
|
||||
.hot-title {
|
||||
margin-left: 25px;
|
||||
line-height: 63px;
|
||||
font-size: 17px;
|
||||
font-weight: normal;
|
||||
|
||||
letter-spacing: 0px;
|
||||
color: #333333;
|
||||
height: 63px;
|
||||
}
|
||||
.hot-item {
|
||||
box-sizing: border-box;
|
||||
border-top: 1.4px solid #e2e2e2;
|
||||
height: 122px;
|
||||
padding-left: 14px;
|
||||
align-items: center;
|
||||
> img {
|
||||
width: 76.3px;
|
||||
height: 77.7px;
|
||||
}
|
||||
}
|
||||
.hot-goods {
|
||||
margin-left: 14px;
|
||||
}
|
||||
.hot-goods-title {
|
||||
font-size: 13px;
|
||||
font-weight: normal;
|
||||
line-height: 16px;
|
||||
letter-spacing: 0px;
|
||||
|
||||
color: #333333;
|
||||
}
|
||||
.hot-price {
|
||||
margin-top: 10.3px;
|
||||
font-size: 24px;
|
||||
font-weight: normal;
|
||||
line-height: 29px;
|
||||
letter-spacing: 0px;
|
||||
|
||||
color: #f31947;
|
||||
}
|
||||
</style>
|
||||
@@ -1,356 +0,0 @@
|
||||
<template>
|
||||
<div class="goods-type-wrapper">
|
||||
<div class="flex goods-type-line">
|
||||
<div class="goods-type-title">{{ data.options.title }}</div>
|
||||
<div class="flex goods-type-labels">
|
||||
<div
|
||||
@click="tabCurrentlyIndex = index"
|
||||
:class="{ active: tabCurrentlyIndex === index }"
|
||||
class="goods-type-item"
|
||||
v-for="(item, index) in data.options.labels"
|
||||
:key="index"
|
||||
>
|
||||
{{ item.label }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 商品部分 -->
|
||||
<div class="goods-list flex">
|
||||
<div
|
||||
class="goods-list-item"
|
||||
v-for="(item, index) in data.options.list.filter((subset) => {
|
||||
return subset.___index == tabCurrentlyIndex;
|
||||
})"
|
||||
:key="index"
|
||||
>
|
||||
<div class="goods-img">
|
||||
<img alt="楼层图片" :src="item.img" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="goods-name wes-2">{{ item.title }}</div>
|
||||
<div class="goods-desc">{{ item.desc }}</div>
|
||||
</div>
|
||||
<div class="goods-price">
|
||||
{{ $filters.unitPrice(item.price, "¥") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="setup-box">
|
||||
<div>
|
||||
<el-button
|
||||
size="small"
|
||||
@click.stop="handleSelectModel(data.options.list)"
|
||||
>编辑</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
v-model="showModal"
|
||||
title="装修"
|
||||
|
||||
width="800"
|
||||
|
||||
:close-on-click-modal="false"
|
||||
append-to-body destroy-on-close>
|
||||
<div class="modal-tab-bar">
|
||||
<div class="tab-bar">
|
||||
标题
|
||||
<el-input style="width: 300px" v-model="data.options.title" />
|
||||
</div>
|
||||
<div class="tab-bar" v-if="data.options.labels[tabCurrentlyIndex]">
|
||||
标签
|
||||
<el-input
|
||||
style="width: 300px"
|
||||
v-model="data.options.labels[tabCurrentlyIndex].label"
|
||||
/>
|
||||
</div>
|
||||
<el-button @click="handleTabsAdd" size="small" class="mb_10">增加</el-button>
|
||||
<el-tabs
|
||||
type="card"
|
||||
v-model="tabIndex"
|
||||
@tab-click="handleClickTab"
|
||||
>
|
||||
<el-tab-pane
|
||||
v-for="(tab,tabIndex) in data.options.labels"
|
||||
:key="tabIndex"
|
||||
:label="tab.label"
|
||||
:name="tab.___index + ''"
|
||||
>
|
||||
<div class="flex flex-a-c">
|
||||
<el-button class="del-btn" type="primary" @click="handleContextMenuDelete(tab,tabIndex)">删除当前标签</el-button>
|
||||
<el-button class="add-goods" @click="addCurrentGoods">
|
||||
添加商品
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="(item, index) in data.options.list.filter((subset) => {
|
||||
return subset.___index == tabCurrentlyIndex;
|
||||
})"
|
||||
:key="index"
|
||||
class="draggable"
|
||||
>
|
||||
<div class="flex">
|
||||
<img alt="楼层图片" :src="item.img" class="column-img" />
|
||||
<div class="flex column-goods-config">
|
||||
<div class="column-config">
|
||||
<div>
|
||||
标题:
|
||||
<el-input v-model="item.title" />
|
||||
</div>
|
||||
<div>
|
||||
描述:
|
||||
<el-input v-model="item.desc" />
|
||||
</div>
|
||||
</div>
|
||||
<el-button @click="delGoods(item)">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 选择商品。链接 -->
|
||||
<liliDialog
|
||||
ref="liliDialog"
|
||||
@selectedLink="selectedLink"
|
||||
@selectedGoodsData="selectedGoodsData"
|
||||
></liliDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from "vue";
|
||||
import { formatLinkType } from "@/utils/filters";
|
||||
|
||||
defineOptions({ name: "goods" });
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
const liliDialog = ref(null);
|
||||
|
||||
const tabIndex = ref(0);
|
||||
const current = ref(0);
|
||||
const showModal = ref(false);
|
||||
const selected = ref({}); // 已选数据
|
||||
const tabCurrentlyIndex = ref(0); // 选项卡索引
|
||||
|
||||
onMounted(() => {
|
||||
tabIndex.value = props.data.options.labels[0].___index;
|
||||
});
|
||||
|
||||
/**
|
||||
* 算出最大的index 然后索引叠加
|
||||
*/
|
||||
function handleTabsAdd() {
|
||||
let findAllIndex = props.data.options.labels.map((item) => {
|
||||
return item.___index;
|
||||
});
|
||||
let max = Math.max.apply(null, findAllIndex);
|
||||
|
||||
props.data.options.labels.push({
|
||||
label: "标签" + (max + 1),
|
||||
___index: max + 1,
|
||||
});
|
||||
}
|
||||
|
||||
// 删除标签
|
||||
function handleContextMenuDelete(item, index) {
|
||||
// 删除标签下关联的商品
|
||||
props.data.options.list.forEach((lab, currently) => {
|
||||
if (lab.___index == item.___index) {
|
||||
props.data.options.list.splice(currently, 1);
|
||||
}
|
||||
});
|
||||
// 删除当前标签
|
||||
props.data.options.labels.splice(index, 1);
|
||||
}
|
||||
|
||||
// 删除商品
|
||||
function delGoods(val) {
|
||||
props.data.options.list.forEach((item, i) => {
|
||||
if (item.title == val.title && item.___index == tabCurrentlyIndex.value) {
|
||||
props.data.options.list.splice(i, 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 切换选项卡
|
||||
function handleClickTab(name) {
|
||||
tabCurrentlyIndex.value = name;
|
||||
}
|
||||
|
||||
// 添加当前选项卡中的商品
|
||||
function addCurrentGoods() {
|
||||
liliDialog.value.clearGoodsSelected();
|
||||
liliDialog.value.open("goods");
|
||||
}
|
||||
|
||||
// 编辑模块
|
||||
function handleSelectModel(item) {
|
||||
selected.value = item;
|
||||
showModal.value = true;
|
||||
}
|
||||
|
||||
// 选择商品回调
|
||||
function selectedGoodsData(val) {
|
||||
if (val.length) {
|
||||
val.forEach((item) => {
|
||||
const pushGoodsDetail = {
|
||||
img: item.thumbnail,
|
||||
price: item.price,
|
||||
title: item.goodsName,
|
||||
desc: "",
|
||||
url: `/goodsDetail?skuId=${item.id}&goodsId=${item.goodsId}`,
|
||||
___index: tabCurrentlyIndex.value,
|
||||
};
|
||||
props.data.options.list.push(pushGoodsDetail);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 选择链接回调
|
||||
function selectedLink(val) {
|
||||
selected.value[current.value].url = formatLinkType(val);
|
||||
selected.value[current.value].type =
|
||||
val.___type === "other" && val.url === "" ? "link" : "other";
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "./setup-box.scss";
|
||||
.goods-type-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
.del-btn{
|
||||
margin-left:10px;
|
||||
}
|
||||
.tab-bar {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.draggable {
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #ededed;
|
||||
transition: 0.35s;
|
||||
|
||||
&:hover {
|
||||
background-color: #ededed;
|
||||
}
|
||||
}
|
||||
.column-config {
|
||||
margin-left: 10px;
|
||||
> * {
|
||||
margin: 4px;
|
||||
}
|
||||
}
|
||||
.column-img {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
.add-goods {
|
||||
margin-left: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.goods-list {
|
||||
position: relative;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
&:hover {
|
||||
> .setup-box {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
.column-goods-config {
|
||||
flex: 2;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.goods-list-item {
|
||||
margin-bottom: 14.3px;
|
||||
width: 287px;
|
||||
height: 343.7px;
|
||||
border-radius: 9.8px;
|
||||
opacity: 1;
|
||||
cursor: pointer;
|
||||
background: #ffffff;
|
||||
transition: 0.35s;
|
||||
box-shadow: 0px 1px 13px 0px #e5e5e5;
|
||||
&:hover {
|
||||
box-shadow: 0px 1px 14px 0px #c5c5c5;
|
||||
}
|
||||
}
|
||||
.goods-img {
|
||||
text-align: center;
|
||||
> img {
|
||||
width: auto;
|
||||
max-height: 183px;
|
||||
}
|
||||
}
|
||||
.goods-name {
|
||||
font-size: 18px;
|
||||
font-weight: normal;
|
||||
line-height: 22px;
|
||||
text-align: center;
|
||||
letter-spacing: 0px;
|
||||
width: 200px;
|
||||
margin: 0 auto 12.4px auto;
|
||||
color: #333333;
|
||||
-webkit-text-stroke: #979797 0.7px; /* 浏览器可能不支持 */
|
||||
}
|
||||
.goods-desc {
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
line-height: 17px;
|
||||
text-align: center;
|
||||
letter-spacing: 0px;
|
||||
margin-bottom: 12.4px;
|
||||
color: #666666;
|
||||
|
||||
-webkit-text-stroke: #979797 0.7px; /* 浏览器可能不支持 */
|
||||
}
|
||||
.goods-price {
|
||||
font-size: 25.2px;
|
||||
font-weight: normal;
|
||||
line-height: 30px;
|
||||
text-align: center;
|
||||
letter-spacing: 0px;
|
||||
|
||||
color: #f31947;
|
||||
-webkit-text-stroke: #979797 0.7px; /* 浏览器可能不支持 */
|
||||
}
|
||||
.goods-type-line {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
.goods-type-title {
|
||||
font-size: 31px;
|
||||
font-weight: normal;
|
||||
line-height: 37px;
|
||||
letter-spacing: 0px;
|
||||
|
||||
color: #333333;
|
||||
}
|
||||
.active {
|
||||
color: #f31947;
|
||||
}
|
||||
.goods-type-labels {
|
||||
font-size: 21px;
|
||||
font-weight: normal;
|
||||
line-height: 25px;
|
||||
letter-spacing: 0px;
|
||||
}
|
||||
.goods-type-item {
|
||||
margin-left: 28px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,322 +0,0 @@
|
||||
<template>
|
||||
<div class="mix-model">
|
||||
<!-- 左侧 -->
|
||||
<div class="left-card">
|
||||
<left :data="data" />
|
||||
<div class="setup-box">
|
||||
<div>
|
||||
<el-button size="small" @click.stop="handleSelectModel('left')"
|
||||
>编辑
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右侧 -->
|
||||
<div class="right-card">
|
||||
<right :data="data" />
|
||||
<div class="setup-box">
|
||||
<div>
|
||||
<el-button size="small" @click.stop="handleSelectModel('right')"
|
||||
>编辑
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog
|
||||
v-model="showModal"
|
||||
title="装修"
|
||||
|
||||
width="800"
|
||||
|
||||
:close-on-click-modal="false"
|
||||
append-to-body destroy-on-close>
|
||||
<div v-if="current">
|
||||
<div class="modal-tab-bar">
|
||||
<div>
|
||||
当前模块 :
|
||||
<el-button-group>
|
||||
<el-button :type="current.model == 'goods' ? 'primary' : 'default'"
|
||||
>商品</el-button
|
||||
>
|
||||
<el-button :type="current.model == 'brand' ? 'primary' : 'default'"
|
||||
>品牌</el-button
|
||||
>
|
||||
</el-button-group>
|
||||
</div>
|
||||
|
||||
<div class="flex">
|
||||
<div>图片 :</div>
|
||||
<div>
|
||||
<img class="image" :src="current.data.image.src" alt="" />
|
||||
<div>
|
||||
<div>推荐尺寸:{{ current.data.image.size }}</div>
|
||||
<div class="link-src">
|
||||
链接地址:
|
||||
<el-input
|
||||
style="width: 200px"
|
||||
v-model="current.data.image.url"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
<el-button
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="handleSelectImg('default')"
|
||||
>选择图片
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
class="ml_10"
|
||||
type="primary"
|
||||
@click="handleSelectLink('image')"
|
||||
>选择链接
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex" v-if="current.model == 'goods'">
|
||||
<div>标签 :</div>
|
||||
<el-input
|
||||
style="width: 200px"
|
||||
type="text"
|
||||
v-model="current.data.badge.label"
|
||||
/>
|
||||
<el-input
|
||||
style="width: 100px; margin-left: 10px"
|
||||
disabled
|
||||
type="text"
|
||||
v-model="current.data.badge.url"
|
||||
/>
|
||||
<el-button
|
||||
size="small"
|
||||
class="ml_10"
|
||||
type="primary"
|
||||
@click="handleSelectLink('badge')"
|
||||
>选择链接
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="flex column-list">
|
||||
<div
|
||||
v-for="(item, index) in current.data.list"
|
||||
:key="index"
|
||||
class="draggable"
|
||||
>
|
||||
<div class="flex">
|
||||
<img alt="楼层图片" :src="item.img" class="column-img" />
|
||||
<el-button
|
||||
size="small"
|
||||
class="ml_10"
|
||||
type="primary"
|
||||
v-if="current.model == 'goods'"
|
||||
@click="handleSelectGoods('list', index)"
|
||||
>选择商品
|
||||
</el-button>
|
||||
<div>
|
||||
<div v-if="current.model == 'brand'" style="margin-bottom: 10px">
|
||||
<el-input disabled v-model="item.url" />
|
||||
</div>
|
||||
<el-button
|
||||
size="small"
|
||||
class="ml_10"
|
||||
type="primary"
|
||||
v-if="current.model == 'brand'"
|
||||
@click="handleSelectLink('list', index)"
|
||||
>
|
||||
选择链接
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
class="ml_10"
|
||||
type="primary"
|
||||
v-if="current.model == 'brand'"
|
||||
@click="handleSelectImg('list', index)"
|
||||
>选择图片
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 选择商品。链接 -->
|
||||
<liliDialog
|
||||
ref="liliDialog"
|
||||
@selectedLink="selectedLink"
|
||||
@selectedGoodsData="selectedGoodsData"
|
||||
></liliDialog>
|
||||
|
||||
<!-- 选择图片 -->
|
||||
<el-dialog v-model="picModelFlag" width="1200px" append-to-body destroy-on-close>
|
||||
<ossManage
|
||||
@callback="callbackSelected"
|
||||
:is-component="true"
|
||||
:initialize="picModelFlag"
|
||||
:max-select="1"
|
||||
ref="ossManageRef"
|
||||
/>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { getCurrentInstance, nextTick, ref } from "vue";
|
||||
import left from "./mixs/mix-goods";
|
||||
import right from "./mixs/mix-brand";
|
||||
import ossManage from "@/views/sys/oss-manage/ossManage.vue";
|
||||
import { formatLinkType } from "@/utils/filters";
|
||||
|
||||
defineOptions({ name: "mixModel" });
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
const { proxy } = getCurrentInstance();
|
||||
|
||||
const showModal = ref(false);
|
||||
const current = ref(""); //当前选择的模块
|
||||
const picModelFlag = ref(false);
|
||||
const selectedData = ref(""); //选中的数据
|
||||
const goodsIndex = ref(0); // 商品索引
|
||||
const align = ref("");
|
||||
const defaultCallbackImageType = ref("");
|
||||
|
||||
const liliDialog = ref(null);
|
||||
// ossManageRef:模板 ref 与组件 import 同名,改名避免互相覆盖
|
||||
const ossManageRef = ref(null);
|
||||
|
||||
function handleSelectImg(type, index) {
|
||||
defaultCallbackImageType.value = type;
|
||||
goodsIndex.value = index;
|
||||
// 选择图片
|
||||
picModelFlag.value = true;
|
||||
nextTick(() => {
|
||||
if (ossManageRef.value) {
|
||||
ossManageRef.value.selectImage = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleSelectGoods(val, index) {
|
||||
liliDialog.value.open("goods");
|
||||
liliDialog.value.singleGoods();
|
||||
selectedData.value = val;
|
||||
goodsIndex.value = index;
|
||||
}
|
||||
function handleSelectLink(val, index) {
|
||||
// 调起选择链接弹窗
|
||||
liliDialog.value.open("link");
|
||||
selectedData.value = val;
|
||||
goodsIndex.value = index;
|
||||
}
|
||||
// 回显图片
|
||||
function callbackSelected(val) {
|
||||
picModelFlag.value = false;
|
||||
if (defaultCallbackImageType.value == "default") {
|
||||
current.value.data.image.src = val.url;
|
||||
} else {
|
||||
props.data.options[align.value].data.list[goodsIndex.value].img = val.url;
|
||||
|
||||
// props.data.options[align.value].data[selectedData.value][goodsIndex.value].url = val.img
|
||||
}
|
||||
}
|
||||
// 选择商品回调
|
||||
function selectedGoodsData(val) {
|
||||
nextTick(() => {
|
||||
props.data.options[align.value].data.list[goodsIndex.value].img =
|
||||
val[0].thumbnail;
|
||||
props.data.options[align.value].data.list[goodsIndex.value].title =
|
||||
val[0].goodsName;
|
||||
props.data.options[align.value].data.list[goodsIndex.value].price =
|
||||
val[0].price;
|
||||
props.data.options[align.value].data.list[goodsIndex.value].url =
|
||||
val[0].url;
|
||||
});
|
||||
proxy.$forceUpdate();
|
||||
}
|
||||
// 选择链接回调
|
||||
function selectedLink(val) {
|
||||
if (selectedData.value != "list") {
|
||||
props.data.options[align.value].data[selectedData.value].url = formatLinkType(val);
|
||||
}
|
||||
if (selectedData.value == "list") {
|
||||
props.data.options[align.value].data[selectedData.value][goodsIndex.value]
|
||||
.url = formatLinkType(val);
|
||||
}
|
||||
}
|
||||
/*
|
||||
* 点击不同模块进行编辑
|
||||
* 判断也很简单,就是第一个第二个模块进行判断
|
||||
*
|
||||
* */
|
||||
function handleSelectModel(alignVal) {
|
||||
align.value = alignVal;
|
||||
current.value = props.data.options[alignVal];
|
||||
showModal.value = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "./setup-box.scss";
|
||||
|
||||
.link-src {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.mix-model {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.left-card,
|
||||
.right-card {
|
||||
width: 584px;
|
||||
height: 344px;
|
||||
border-radius: 10px;
|
||||
opacity: 1;
|
||||
background: #ffffff;
|
||||
box-shadow: 0px 1px 13px 0px #e5e5e5;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
.setup-box {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.column-img {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.column-list {
|
||||
flex-wrap: wrap;
|
||||
|
||||
> div {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.image {
|
||||
max-width: 100px;
|
||||
}
|
||||
|
||||
.modal-tab-bar {
|
||||
> .flex {
|
||||
align-items: center;
|
||||
margin: 10px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.draggable {
|
||||
> .flex {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,124 +0,0 @@
|
||||
<template>
|
||||
<div class="flex" v-if="data.options.right.model == 'brand'">
|
||||
<div class="left-side" >
|
||||
<img :src="data.options.right.data.image.src" alt="">
|
||||
</div>
|
||||
<div>
|
||||
<div class="badge-box flex">
|
||||
<div class="round">
|
||||
<el-icon><ArrowRight /></el-icon>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="flex goods-list">
|
||||
<div class="goods-item flex" :key="index" v-for="(item,index) in data.options.right.data.list">
|
||||
<div class="goods-thumbnail">
|
||||
<img :src="item.img" alt="">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ArrowRight } from "@element-plus/icons-vue";
|
||||
|
||||
defineOptions({ name: "mix-goods" });
|
||||
|
||||
defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.goods-detail{
|
||||
|
||||
}
|
||||
.goods-detail-title{
|
||||
font-size: 13px;
|
||||
font-weight: normal;
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
letter-spacing: 0px;
|
||||
color: #333333;
|
||||
}
|
||||
.goods-detail-price{
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
line-height: 17px;
|
||||
letter-spacing: 0px;
|
||||
color: #F31947;
|
||||
margin-top: 8px;
|
||||
margin-bottom:10px;
|
||||
}
|
||||
.goods-thumbnail{
|
||||
margin-left: 7px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
>img{
|
||||
width:90px;
|
||||
height: 90px;
|
||||
}
|
||||
}
|
||||
.badge-box{
|
||||
margin-top: 33px;
|
||||
justify-content:right;
|
||||
>.round{
|
||||
width: 17.5px;
|
||||
height: 17.5px;
|
||||
opacity: 1;
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
line-height: 17.5px;
|
||||
border: 0.7px solid #333333;
|
||||
margin-right: 17.5px;
|
||||
}
|
||||
}
|
||||
.goods-list{
|
||||
flex-wrap: wrap;
|
||||
justify-content:space-between;
|
||||
padding: 0 16px;
|
||||
}
|
||||
.goods-item{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width:50%;
|
||||
height: 85px;
|
||||
border-radius: 9.8px;
|
||||
opacity: 1;
|
||||
transition: .35s;
|
||||
background: #FFFFFF;
|
||||
margin-bottom:9px;
|
||||
|
||||
|
||||
}
|
||||
.left-side {
|
||||
>img{
|
||||
width: 254px;
|
||||
height: 344px;
|
||||
border-radius: 9.8px 0px 0px 9.8px;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.badge {
|
||||
width: 50px;
|
||||
height: 27px;
|
||||
line-height:27px;
|
||||
border-radius: 13.3px 0px 0px 13.3px;
|
||||
opacity: 1;
|
||||
background: #F31947;
|
||||
font-size: 12.6px;
|
||||
font-weight: normal;
|
||||
text-align: center;
|
||||
letter-spacing: 0px;
|
||||
color: #FFFFFF;
|
||||
margin-top: 26px;
|
||||
margin-bottom:17px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,115 +0,0 @@
|
||||
<template>
|
||||
<div class="flex" v-if="data.options.left.model == 'goods'">
|
||||
<div class="left-side" >
|
||||
<img :src="data.options.left.data.image.src" alt="">
|
||||
</div>
|
||||
<div>
|
||||
<div class="badge-box flex">
|
||||
<div class="badge">
|
||||
{{data.options.left.data.badge.label}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex goods-list">
|
||||
<div class="goods-item flex" :key="index" v-for="(item,index) in data.options.left.data.list">
|
||||
<div class="goods-thumbnail">
|
||||
<img :src="item.img" alt="">
|
||||
</div>
|
||||
<div class="goods-detail">
|
||||
<div class="goods-detail-title">{{item.title}}</div>
|
||||
<div class="goods-detail-price">{{ $filters.unitPrice(item.price, '¥') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineOptions({ name: "mix-goods" });
|
||||
|
||||
defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.goods-detail{
|
||||
|
||||
}
|
||||
.goods-detail-title{
|
||||
font-size: 13px;
|
||||
font-weight: normal;
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
letter-spacing: 0px;
|
||||
color: #333333;
|
||||
}
|
||||
.goods-detail-price{
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
line-height: 17px;
|
||||
letter-spacing: 0px;
|
||||
color: #F31947;
|
||||
margin-top: 8px;
|
||||
margin-bottom:10px;
|
||||
}
|
||||
.goods-thumbnail{
|
||||
margin-left: 7px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
>img{
|
||||
width:90px;
|
||||
height: 90px;
|
||||
}
|
||||
}
|
||||
.badge-box{
|
||||
|
||||
justify-content:right;
|
||||
}
|
||||
.goods-list{
|
||||
flex-wrap: wrap;
|
||||
justify-content:space-between;
|
||||
padding: 0 16px;
|
||||
}
|
||||
.goods-item{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 173.6px;
|
||||
height: 119px;
|
||||
border-radius: 9.8px;
|
||||
opacity: 1;
|
||||
transition: .35s;
|
||||
background: #FFFFFF;
|
||||
margin-bottom:9px;
|
||||
&:hover{
|
||||
box-shadow: 0px 1px 6px 0px #E5E5E5;
|
||||
}
|
||||
|
||||
}
|
||||
.left-side {
|
||||
>img{
|
||||
border-radius: 10px 0px 0px 10px;
|
||||
display: block;
|
||||
width: 196.7px;
|
||||
height: 343.7px;
|
||||
}
|
||||
}
|
||||
.badge {
|
||||
width: 50px;
|
||||
height: 27px;
|
||||
line-height:27px;
|
||||
border-radius: 13.3px 0px 0px 13.3px;
|
||||
opacity: 1;
|
||||
background: #F31947;
|
||||
font-size: 12.6px;
|
||||
font-weight: normal;
|
||||
text-align: center;
|
||||
letter-spacing: 0px;
|
||||
color: #FFFFFF;
|
||||
margin-top: 26px;
|
||||
margin-bottom:17px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,727 +0,0 @@
|
||||
<template>
|
||||
<div class="new-goods">
|
||||
<div class="left">
|
||||
<div
|
||||
class="top-header setup-content"
|
||||
:style="{ background: options.left.bgColor }"
|
||||
>
|
||||
<span>{{ options.left.title }}</span>
|
||||
<span>{{ options.left.secondTitle }} ></span>
|
||||
<div class="setup-box">
|
||||
<div>
|
||||
<el-button
|
||||
size="small"
|
||||
@click.stop="handleSelectModel(options.left, true)"
|
||||
>编辑</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div
|
||||
class="con-item setup-content"
|
||||
v-for="(item, index) in options.left.list"
|
||||
:key="index"
|
||||
>
|
||||
<div>
|
||||
<p>{{ item.name }}</p>
|
||||
<p class="describe">{{ item.describe }}</p>
|
||||
</div>
|
||||
<img :src="item.img" alt="" />
|
||||
<div class="setup-box">
|
||||
<div>
|
||||
<el-button size="small" @click.stop="handleSelectModel(item)"
|
||||
>编辑</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="middle">
|
||||
<div
|
||||
class="top-header setup-content"
|
||||
:style="{ background: options.middle.bgColor }"
|
||||
>
|
||||
<span>{{ options.middle.title }}</span>
|
||||
<span>{{ options.middle.secondTitle }} ></span>
|
||||
<div class="setup-box">
|
||||
<div>
|
||||
<el-button
|
||||
size="small"
|
||||
@click.stop="handleSelectModel(options.middle, true)"
|
||||
>编辑</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div
|
||||
class="con-item setup-content"
|
||||
v-for="(item, index) in options.middle.list"
|
||||
:key="index"
|
||||
>
|
||||
<div>
|
||||
<p>{{ item.name }}</p>
|
||||
<p class="describe">{{ item.describe }}</p>
|
||||
</div>
|
||||
<img :src="item.img" alt="" />
|
||||
<div class="setup-box">
|
||||
<div>
|
||||
<el-button size="small" @click.stop="handleSelectModel(item)"
|
||||
>编辑</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="right">
|
||||
<div
|
||||
class="top-header setup-content"
|
||||
:style="{ background: options.right.bgColor }"
|
||||
>
|
||||
<span>{{ options.right.title }}</span>
|
||||
<span>{{ options.right.secondTitle }} ></span>
|
||||
<div class="setup-box">
|
||||
<div>
|
||||
<el-button
|
||||
size="small"
|
||||
@click.stop="handleSelectModel(options.right, true)"
|
||||
>编辑</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div
|
||||
class="setup-content"
|
||||
v-for="(item, index) in options.right.list"
|
||||
:key="index"
|
||||
>
|
||||
<div class="img-wrap">
|
||||
<img :src="item.img" alt="" />
|
||||
<div class="jiaobiao" :class="'jiaobiao' + (index + 1)">
|
||||
{{ index + 1 }}
|
||||
</div>
|
||||
</div>
|
||||
<p>{{ item.name }}</p>
|
||||
<p>{{ $filters.unitPrice(item.price, "¥") }}</p>
|
||||
<div class="setup-box">
|
||||
<div>
|
||||
<el-button size="small" @click.stop="handleSelectGoods(item)"
|
||||
>编辑</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 装修内容 -->
|
||||
<el-dialog
|
||||
v-model="showModal"
|
||||
title="装修"
|
||||
width="800px"
|
||||
:close-on-click-modal="false"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
>
|
||||
<div class="modal-top-advert">
|
||||
<div class="modal-form-item modal-form-preview">
|
||||
<img
|
||||
class="show-image"
|
||||
width="160"
|
||||
height="160"
|
||||
v-if="selected.size && selected.size.indexOf('160*160') >= 0"
|
||||
:src="selected.img"
|
||||
alt
|
||||
/>
|
||||
<img
|
||||
class="show-image"
|
||||
width="80"
|
||||
height="80"
|
||||
v-if="selected.size && selected.size.indexOf('90*90') >= 0"
|
||||
:src="selected.img"
|
||||
alt
|
||||
/>
|
||||
</div>
|
||||
<div class="modal-form-item">
|
||||
<span class="modal-label">图片主标题:</span>
|
||||
<el-input v-model="selected.name" />
|
||||
</div>
|
||||
<div class="modal-form-item">
|
||||
<span class="modal-label">图片描述:</span>
|
||||
<el-input v-model="selected.describe" />
|
||||
</div>
|
||||
<div class="tips">建议尺寸:{{ selected.size }}</div>
|
||||
<div class="modal-form-item modal-form-link">
|
||||
<span class="modal-label">图片链接:</span>
|
||||
<div class="modal-link-field">
|
||||
<el-input
|
||||
class="outsideUrl"
|
||||
v-model="selected.url"
|
||||
:disabled="!!selected.type && selected.type !== 'link'"
|
||||
placeholder="https://"
|
||||
/>
|
||||
<el-button size="small" type="primary" @click="handleSelectLink"
|
||||
>选择链接</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-form-item modal-form-actions">
|
||||
<el-button size="small" type="primary" @click="handleSelectImg"
|
||||
>选择图片</el-button
|
||||
>
|
||||
<el-button size="small" type="primary" @click="handleSelectGoods('')"
|
||||
>选择商品</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="handleCancelModal">取消</el-button>
|
||||
<el-button type="primary" @click="handleConfirmModal">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<!-- 装修标题 -->
|
||||
<el-dialog
|
||||
v-model="showModal1"
|
||||
title="装修"
|
||||
width="800px"
|
||||
:close-on-click-modal="false"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
>
|
||||
<div class="modal-top-advert">
|
||||
<div class="modal-form-item">
|
||||
<span class="modal-label">主标题:</span>
|
||||
<el-input v-model="selected.title" />
|
||||
</div>
|
||||
<div class="modal-form-item">
|
||||
<span class="modal-label">副标题:</span>
|
||||
<el-input v-model="selected.secondTitle" />
|
||||
</div>
|
||||
<div class="modal-form-item modal-form-link">
|
||||
<span class="modal-label">副标题链接:</span>
|
||||
<div class="modal-link-field">
|
||||
<el-input
|
||||
class="outsideUrl"
|
||||
v-model="selected.url"
|
||||
:disabled="!!selected.type && selected.type !== 'link'"
|
||||
placeholder="https://"
|
||||
/>
|
||||
<el-button size="small" type="primary" @click="handleSelectLink"
|
||||
>选择链接</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-form-item">
|
||||
<span class="modal-label">背景色:</span>
|
||||
<div class="modal-color-field">
|
||||
<el-input v-model="selected.bgColor" />
|
||||
<el-color-picker v-if="selected.bgColor" v-model="selected.bgColor" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="handleCancelModal1">取消</el-button>
|
||||
<el-button type="primary" @click="handleConfirmModal1">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<!-- 选择商品。链接 -->
|
||||
<liliDialog
|
||||
ref="liliDialog"
|
||||
@selectedLink="selectedLink"
|
||||
@selectedGoodsData="selectedGoodsData"
|
||||
></liliDialog>
|
||||
<!-- 选择图片 -->
|
||||
<el-dialog v-model="picModelFlag" width="1200px" append-to-body destroy-on-close>
|
||||
<ossManage
|
||||
@callback="callbackSelected"
|
||||
:is-component="true"
|
||||
:initialize="picModelFlag"
|
||||
:max-select="1"
|
||||
ref="ossManageRef"
|
||||
/>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { nextTick, ref } from "vue";
|
||||
import { formatLinkType } from "@/utils/filters";
|
||||
import ossManage from "@/views/sys/oss-manage/ossManage";
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const options = ref(props.data.options); // 当前数据
|
||||
const showModal = ref(false); // modal显隐
|
||||
const showModal1 = ref(false); // modal显隐
|
||||
const selected = ref({}); // 弹窗编辑草稿
|
||||
const editTarget = ref(null); // 确定后写回的原数据
|
||||
const picModelFlag = ref(false); // 选择图片modal
|
||||
|
||||
const liliDialog = ref(null);
|
||||
// ossManageRef:模板 ref 与组件 import 同名,改名避免互相覆盖
|
||||
const ossManageRef = ref(null);
|
||||
|
||||
function cloneItem(item) {
|
||||
return JSON.parse(JSON.stringify(item || {}));
|
||||
}
|
||||
// 装修modal
|
||||
function handleSelectModel(item, type) {
|
||||
editTarget.value = item;
|
||||
selected.value = cloneItem(item);
|
||||
if (type) {
|
||||
showModal1.value = true;
|
||||
} else {
|
||||
showModal.value = true;
|
||||
}
|
||||
}
|
||||
function handleSelectLink() {
|
||||
// 调起选择链接弹窗
|
||||
liliDialog.value.open("link");
|
||||
}
|
||||
function handleSelectGoods(item) {
|
||||
if (item) {
|
||||
editTarget.value = item;
|
||||
selected.value = cloneItem(item);
|
||||
}
|
||||
liliDialog.value.open("goods", "single");
|
||||
setTimeout(() => {
|
||||
liliDialog.value.goodsData = [selected.value];
|
||||
}, 500);
|
||||
}
|
||||
// 选择链接回调
|
||||
function selectedLink(val) {
|
||||
if (!selected.value) return;
|
||||
selected.value.url = formatLinkType(val);
|
||||
selected.value.type =
|
||||
val.___type === "other" && val.url === "" ? "link" : "other";
|
||||
}
|
||||
// 选择商品回调
|
||||
function selectedGoodsData(val) {
|
||||
if (!val?.length || !selected.value) return;
|
||||
const goods = val[0];
|
||||
selected.value.img = goods.thumbnail;
|
||||
selected.value.price = goods.price;
|
||||
selected.value.name = goods.goodsName;
|
||||
selected.value.url = `/goodsDetail?skuId=${goods.id}&goodsId=${goods.goodsId}`;
|
||||
if (editTarget.value && !showModal.value && !showModal1.value) {
|
||||
Object.assign(editTarget.value, cloneItem(selected.value));
|
||||
editTarget.value = null;
|
||||
selected.value = {};
|
||||
}
|
||||
}
|
||||
function handleSelectImg() {
|
||||
// 选择图片
|
||||
picModelFlag.value = true;
|
||||
nextTick(() => {
|
||||
if (ossManageRef.value) {
|
||||
ossManageRef.value.selectImage = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
// 选择图片回显
|
||||
function callbackSelected(val) {
|
||||
picModelFlag.value = false;
|
||||
if (!val?.url) return;
|
||||
selected.value.img = val.url;
|
||||
}
|
||||
function handleCancelModal() {
|
||||
editTarget.value = null;
|
||||
selected.value = {};
|
||||
showModal.value = false;
|
||||
}
|
||||
function handleConfirmModal() {
|
||||
if (editTarget.value) {
|
||||
Object.assign(editTarget.value, cloneItem(selected.value));
|
||||
}
|
||||
editTarget.value = null;
|
||||
selected.value = {};
|
||||
showModal.value = false;
|
||||
}
|
||||
function handleCancelModal1() {
|
||||
editTarget.value = null;
|
||||
selected.value = {};
|
||||
showModal1.value = false;
|
||||
}
|
||||
function handleConfirmModal1() {
|
||||
if (editTarget.value) {
|
||||
Object.assign(editTarget.value, cloneItem(selected.value));
|
||||
}
|
||||
editTarget.value = null;
|
||||
selected.value = {};
|
||||
showModal1.value = false;
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "./setup-box.scss";
|
||||
.new-goods {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: stretch;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
> div {
|
||||
width: 393px;
|
||||
height: 440px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.left > .content {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-rows: repeat(3, 1fr);
|
||||
flex-direction: unset;
|
||||
overflow: hidden;
|
||||
> div {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
padding: 6px 4px 4px 6px;
|
||||
border-style: solid;
|
||||
border-color: #eee;
|
||||
border-width: 0;
|
||||
border-bottom-width: 1px;
|
||||
border-right-width: 1px;
|
||||
img {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin-top: 2px;
|
||||
flex-shrink: 0;
|
||||
object-fit: cover;
|
||||
}
|
||||
> div {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
p {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 12px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.describe {
|
||||
margin-top: 4px;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
}
|
||||
> div:nth-child(1) {
|
||||
grid-row: span 2;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 8px 6px;
|
||||
img {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
> div {
|
||||
flex: unset;
|
||||
width: 100%;
|
||||
p {
|
||||
white-space: normal;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
.describe {
|
||||
margin-top: 6px;
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
> div:nth-child(2),
|
||||
> div:nth-child(3),
|
||||
> div:nth-child(5) {
|
||||
border-right-width: 0;
|
||||
}
|
||||
> div:nth-child(4),
|
||||
> div:nth-child(5) {
|
||||
border-bottom-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.middle > .content {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-rows: repeat(3, 1fr);
|
||||
flex-direction: unset;
|
||||
> div {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
border-style: solid;
|
||||
border-color: #eee;
|
||||
border-width: 0;
|
||||
border-bottom-width: 1px;
|
||||
border-right-width: 1px;
|
||||
}
|
||||
> div:nth-child(2n) {
|
||||
border-right-width: 0;
|
||||
}
|
||||
> div:nth-child(n + 5) {
|
||||
border-bottom-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.right > .content {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-rows: repeat(2, 1fr);
|
||||
flex-direction: unset;
|
||||
flex-wrap: unset;
|
||||
font-size: 12px;
|
||||
overflow: hidden;
|
||||
> div {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
box-sizing: border-box;
|
||||
padding: 4px 4px 2px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid #eee;
|
||||
border-right: 1px solid #eee;
|
||||
.img-wrap {
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
flex-shrink: 0;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
}
|
||||
.jiaobiao {
|
||||
position: absolute;
|
||||
width: 23px;
|
||||
height: 23px;
|
||||
top: 0;
|
||||
right: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
max-height: none;
|
||||
overflow: visible;
|
||||
display: block;
|
||||
background: url("../../../assets/festival_icon.png") no-repeat;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
line-height: 23px;
|
||||
font-size: 12px;
|
||||
-webkit-line-clamp: unset;
|
||||
-webkit-box-orient: unset;
|
||||
}
|
||||
.jiaobiao1,
|
||||
.jiaobiao4 {
|
||||
background-position: -2px -30px;
|
||||
}
|
||||
.jiaobiao2,
|
||||
.jiaobiao5 {
|
||||
background-position: -31px -30px;
|
||||
}
|
||||
.jiaobiao3,
|
||||
.jiaobiao6 {
|
||||
background-position: -60px -30px;
|
||||
}
|
||||
}
|
||||
> p:nth-child(2) {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-height: 32px;
|
||||
line-height: 16px;
|
||||
margin-top: 4px;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
text-align: center;
|
||||
}
|
||||
> p:nth-child(3) {
|
||||
color: $theme_color;
|
||||
margin-top: 2px;
|
||||
line-height: 1.2;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
> div:nth-child(3n) {
|
||||
border-right: none;
|
||||
}
|
||||
> div:nth-child(n + 4) {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.top-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 50px;
|
||||
padding: 0 10px;
|
||||
background: #c43d7e;
|
||||
color: #fff;
|
||||
span:nth-child(1) {
|
||||
font-size: 20px;
|
||||
}
|
||||
span:nth-child(2) {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
.content {
|
||||
padding: 10px 12px 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: column;
|
||||
height: 370px;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
.con-item {
|
||||
width: 185px;
|
||||
height: 120px;
|
||||
display: flex;
|
||||
padding-left: 10px;
|
||||
padding-top: 10px;
|
||||
img {
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
.middle > .content .con-item {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
padding: 6px 4px 4px 6px;
|
||||
img {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin-top: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
> div {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
p {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 12px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.describe {
|
||||
margin-top: 4px;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.left > .content .con-item {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.describe {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
.modal-top-advert {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
padding: 0 20px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
.modal-form-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.modal-label {
|
||||
flex-shrink: 0;
|
||||
width: 100px;
|
||||
text-align: right;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
.el-input {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-form-preview {
|
||||
justify-content: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.modal-form-link {
|
||||
align-items: flex-start;
|
||||
|
||||
.modal-label {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.modal-link-field {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
|
||||
.outsideUrl {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-form-actions {
|
||||
padding-left: 112px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.modal-color-field {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
.el-input {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.tips {
|
||||
padding-left: 112px;
|
||||
margin-bottom: 16px;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,321 +0,0 @@
|
||||
<template>
|
||||
<div class="not-enough">
|
||||
<div class="nav-bar setup-content">
|
||||
<ul class="nav-list">
|
||||
<li v-for="(item, index) in conData.options.navList" :class="currentIndex===index?'curr':''" @click="changeCurr(index)" :key="index">
|
||||
<p>{{item.title}}</p>
|
||||
<p>{{item.desc}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="setup-box nav-setup-box">
|
||||
<div>
|
||||
<el-button size="small" @click.stop="handleSelectModel">编辑</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content" v-if="showContent">
|
||||
<div v-for="(item, index) in conData.options.list[currentIndex]" :key="index" class="setup-content">
|
||||
<img class="goods-img" :src="item.img" :alt="item.name">
|
||||
<p>{{item.name}}</p>
|
||||
<p>
|
||||
<span>{{ $filters.unitPrice(item.price, '¥') }}</span>
|
||||
</p>
|
||||
<div class="setup-box">
|
||||
<div>
|
||||
<el-button size="small" @click.stop="handleSelectGoods(item)">编辑</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog
|
||||
v-model="showModal"
|
||||
title="装修"
|
||||
width="800"
|
||||
:close-on-click-modal="false"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
@close="handleCancelModal">
|
||||
<div class="modal-tab-bar" v-if="draftNavList.length">
|
||||
<el-button type="primary" size='small' @click="handleAddNav">添加分类</el-button>
|
||||
<table cellspacing="0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="250">主标题</th>
|
||||
<th width="250">描述</th>
|
||||
<th width="250">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(item, index) in draftNavList" :key="index">
|
||||
<td><el-input v-model="item.title" /></td>
|
||||
<td><el-input v-model="item.desc" /></td>
|
||||
<td v-if="index!=0">
|
||||
<el-button type="danger" size="small" @click="handleDelNav(index)">删除</el-button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="handleCancelModal">取消</el-button>
|
||||
<el-button type="primary" @click="handleConfirmModal">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<el-dialog
|
||||
v-model="showGoodsModal"
|
||||
title="装修"
|
||||
width="600"
|
||||
:close-on-click-modal="false"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
@close="handleCancelGoodsModal">
|
||||
<div class="modal-top-advert">
|
||||
<div class="modal-form-item modal-form-preview" v-if="selected.img">
|
||||
<img class="show-image goods-preview" :src="selected.img" alt />
|
||||
</div>
|
||||
<div class="modal-form-item" v-if="selected.name">
|
||||
<span class="modal-label">商品名称:</span>
|
||||
<span>{{ selected.name }}</span>
|
||||
</div>
|
||||
<div class="modal-form-item" v-if="selected.price">
|
||||
<span class="modal-label">商品价格:</span>
|
||||
<span>{{ $filters.unitPrice(selected.price, '¥') }}</span>
|
||||
</div>
|
||||
<div class="modal-form-item modal-form-actions">
|
||||
<el-button size="small" type="primary" @click="handleOpenGoodsPicker">选择商品</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="handleCancelGoodsModal">取消</el-button>
|
||||
<el-button type="primary" @click="handleConfirmGoodsModal">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<liliDialog
|
||||
ref="liliDialog"
|
||||
@selectedGoodsData="selectedGoodsData"
|
||||
></liliDialog>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { nextTick, ref, watch } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
data:{
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
});
|
||||
const emit = defineEmits(['content']);
|
||||
|
||||
const currentIndex = ref(0);
|
||||
const conData = ref(props.data);
|
||||
const selected = ref({});
|
||||
const editTarget = ref(null);
|
||||
const showModal = ref(false);
|
||||
const showGoodsModal = ref(false);
|
||||
const showContent = ref(true);
|
||||
const draftNavList = ref([]);
|
||||
const draftGoodsList = ref([]);
|
||||
|
||||
const liliDialog = ref(null);
|
||||
|
||||
watch(() => props.data, (val) => {
|
||||
conData.value = val;
|
||||
});
|
||||
watch(conData, (val) => {
|
||||
emit('content', val);
|
||||
});
|
||||
|
||||
function cloneList(list) {
|
||||
return JSON.parse(JSON.stringify(list || []));
|
||||
}
|
||||
function cloneItem(item) {
|
||||
return JSON.parse(JSON.stringify(item || {}));
|
||||
}
|
||||
function changeCurr(index){
|
||||
currentIndex.value = index;
|
||||
}
|
||||
function handleSelectModel () {
|
||||
draftNavList.value = cloneList(conData.value.options.navList);
|
||||
draftGoodsList.value = cloneList(conData.value.options.list);
|
||||
showModal.value = true;
|
||||
}
|
||||
function handleSelectGoods(item) {
|
||||
editTarget.value = item;
|
||||
selected.value = cloneItem(item);
|
||||
showGoodsModal.value = true;
|
||||
}
|
||||
function handleOpenGoodsPicker() {
|
||||
liliDialog.value.open('goods', 'single');
|
||||
setTimeout(() => {
|
||||
liliDialog.value.goodsData = [selected.value];
|
||||
}, 500);
|
||||
}
|
||||
function selectedGoodsData(val){
|
||||
if (!val?.length) return;
|
||||
const goods = val[0];
|
||||
selected.value.img = goods.thumbnail;
|
||||
selected.value.price = goods.price;
|
||||
selected.value.name = goods.goodsName;
|
||||
selected.value.url = `/goodsDetail?skuId=${goods.id}&goodsId=${goods.goodsId}`;
|
||||
}
|
||||
function handleDelNav(index){
|
||||
draftNavList.value.splice(index,1);
|
||||
draftGoodsList.value.splice(index,1);
|
||||
}
|
||||
function handleAddNav(){
|
||||
draftNavList.value.push({ title:'', desc:'' });
|
||||
draftGoodsList.value.push([
|
||||
{ img:'', name:'', price:0, url:'' },
|
||||
{ img:'', name:'', price:0, url:'' },
|
||||
{ img:'', name:'', price:0, url:'' },
|
||||
{ img:'', name:'', price:0, url:'' },
|
||||
{ img:'', name:'', price:0, url:'' },
|
||||
{ img:'', name:'', price:0, url:'' },
|
||||
{ img:'', name:'', price:0, url:'' },
|
||||
{ img:'', name:'', price:0, url:'' },
|
||||
{ img:'', name:'', price:0, url:'' },
|
||||
{ img:'', name:'', price:0, url:'' },
|
||||
]);
|
||||
}
|
||||
function handleCancelModal() {
|
||||
draftNavList.value = [];
|
||||
draftGoodsList.value = [];
|
||||
showModal.value = false;
|
||||
}
|
||||
function handleConfirmModal() {
|
||||
conData.value.options.navList = cloneList(draftNavList.value);
|
||||
conData.value.options.list = cloneList(draftGoodsList.value);
|
||||
draftNavList.value = [];
|
||||
draftGoodsList.value = [];
|
||||
showContent.value = false;
|
||||
nextTick(() => {
|
||||
showContent.value = true;
|
||||
});
|
||||
showModal.value = false;
|
||||
}
|
||||
function handleCancelGoodsModal() {
|
||||
editTarget.value = null;
|
||||
selected.value = {};
|
||||
showGoodsModal.value = false;
|
||||
}
|
||||
function handleConfirmGoodsModal() {
|
||||
if (editTarget.value) {
|
||||
Object.assign(editTarget.value, cloneItem(selected.value));
|
||||
}
|
||||
editTarget.value = null;
|
||||
selected.value = {};
|
||||
showGoodsModal.value = false;
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import './setup-box.scss';
|
||||
.not-enough {
|
||||
width: 100%;
|
||||
}
|
||||
.nav-bar{
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
background-color: rgb(218, 217, 217);
|
||||
height: 60px;
|
||||
position: relative;
|
||||
}
|
||||
.nav-setup-box {
|
||||
left: auto;
|
||||
right: 0;
|
||||
width: 100px;
|
||||
}
|
||||
.nav-list {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
li{
|
||||
padding: 0 30px;
|
||||
text-align: center;
|
||||
p:nth-child(1){
|
||||
font-size: 16px;
|
||||
border-radius: 50px;
|
||||
padding: 0 7px;
|
||||
margin: 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
p:nth-child(2){
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
margin: 2px 0 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
&:hover{
|
||||
p{
|
||||
color: $theme_color;
|
||||
}
|
||||
cursor: pointer;
|
||||
}
|
||||
border-right: 1px solid #eee;
|
||||
|
||||
}
|
||||
li:last-of-type{
|
||||
border: none;
|
||||
}
|
||||
.curr{
|
||||
p:nth-child(1){
|
||||
background-color: $theme_color;
|
||||
color: #fff;
|
||||
}
|
||||
p:nth-child(2){
|
||||
color: $theme_color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content{
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
>div{
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #eee;
|
||||
min-width: 0;
|
||||
.goods-img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
aspect-ratio: 1;
|
||||
object-fit: cover;
|
||||
}
|
||||
p:nth-of-type(1){
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
text-overflow:ellipsis;
|
||||
margin: 10px 0 5px 0;
|
||||
}
|
||||
p:nth-of-type(2){
|
||||
color: $theme_color;
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
span:nth-child(2){
|
||||
text-decoration: line-through;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.goods-preview {
|
||||
width: 210px;
|
||||
height: 210px;
|
||||
object-fit: cover;
|
||||
}
|
||||
</style>
|
||||
@@ -1,163 +0,0 @@
|
||||
<template>
|
||||
<div class="line flex flex-j-sb">
|
||||
<div class="column" v-for="(item,index) in data.options.list" :key="index">
|
||||
<div v-if="!item.img" class="placeholder">占位符</div>
|
||||
<img alt="楼层图片" v-else :src="item.img" class="three-column-img">
|
||||
</div>
|
||||
<div class="setup-box">
|
||||
<div>
|
||||
<el-button
|
||||
size="small"
|
||||
@click.stop="handleSelectModel(data.options.list)"
|
||||
>编辑</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog
|
||||
v-model="showModal"
|
||||
title="装修"
|
||||
|
||||
width="800"
|
||||
|
||||
:close-on-click-modal="false"
|
||||
append-to-body destroy-on-close>
|
||||
<div class="modal-tab-bar">
|
||||
<div v-for="(item,index) in data.options.list" :key="index">
|
||||
<img alt="楼层图片" :src="item.img" class="three-column-img">
|
||||
<div>推荐尺寸:{{item.size}}</div>
|
||||
<el-button
|
||||
size="small"
|
||||
class="ml_10"
|
||||
type="primary"
|
||||
@click="handleSelectImg(index)"
|
||||
>选择图片</el-button
|
||||
>
|
||||
<el-button
|
||||
size="small"
|
||||
class="ml_10"
|
||||
type="primary"
|
||||
@click="handleSelectLink(index)"
|
||||
>选择链接</el-button
|
||||
>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</el-dialog>
|
||||
<!-- 选择商品。链接 -->
|
||||
<liliDialog
|
||||
ref="liliDialogRef"
|
||||
@selectedLink="selectedLink"
|
||||
@selectedGoodsData="selectedGoodsData"
|
||||
></liliDialog>
|
||||
<!-- 选择图片 -->
|
||||
<el-dialog v-model="picModelFlag" width="1200px" append-to-body destroy-on-close>
|
||||
<ossManage
|
||||
@callback="callbackSelected"
|
||||
:is-component="true"
|
||||
:initialize="picModelFlag"
|
||||
:max-select="1"
|
||||
ref="ossManageRef"
|
||||
/>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { nextTick, ref } from "vue";
|
||||
import { formatLinkType } from "@/utils/filters";
|
||||
import ossManage from "@/views/sys/oss-manage/ossManage";
|
||||
|
||||
defineOptions({ name: "oneRowThreeColumns" });
|
||||
|
||||
defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const showModal = ref(false);
|
||||
const selected = ref({}); // 已选数据
|
||||
const picModelFlag = ref(false);
|
||||
const current = ref(0);
|
||||
|
||||
const liliDialogRef = ref(null);
|
||||
const ossManageRef = ref(null);
|
||||
|
||||
// 回显图片
|
||||
function callbackSelected(val) {
|
||||
picModelFlag.value = false;
|
||||
selected.value[current.value].img = val.url;
|
||||
}
|
||||
|
||||
// 编辑模块
|
||||
function handleSelectModel(item) {
|
||||
selected.value = item;
|
||||
showModal.value = true;
|
||||
|
||||
}
|
||||
|
||||
// 选择商品回调
|
||||
function selectedGoodsData(val) {
|
||||
let goods = val[0];
|
||||
selected.value.img = goods.thumbnail;
|
||||
selected.value.price = goods.price;
|
||||
selected.value.name = goods.goodsName;
|
||||
selected.value.url = `/goodsDetail?skuId=${goods.id}&goodsId=${goods.goodsId}`;
|
||||
}
|
||||
|
||||
// 选择链接回调
|
||||
function selectedLink(val) {
|
||||
selected.value.url = formatLinkType(val);
|
||||
selected.value.type =
|
||||
val.___type === "other" && val.url === "" ? "link" : "other";
|
||||
}
|
||||
|
||||
function handleSelectLink(index) {
|
||||
// 调起选择链接弹窗
|
||||
liliDialogRef.value.open("link");
|
||||
current.value = index;
|
||||
}
|
||||
|
||||
function handleSelectImg(index) {
|
||||
// 选择图片
|
||||
current.value = index;
|
||||
picModelFlag.value = true;
|
||||
nextTick(() => {
|
||||
if (ossManageRef.value) {
|
||||
ossManageRef.value.selectImage = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "./setup-box.scss";
|
||||
.three-column-img{
|
||||
width:385px;
|
||||
height: 165px
|
||||
}
|
||||
.line:hover{
|
||||
>.setup-box{
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.placeholder{
|
||||
background: #666;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.line{
|
||||
position: relative;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.column{
|
||||
width: 385px;
|
||||
height: 165px;
|
||||
|
||||
>img{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,264 +0,0 @@
|
||||
<template>
|
||||
<div class="goods-type-wrapper">
|
||||
<!-- 商品部分 -->
|
||||
<div class="goods-list flex">
|
||||
<div
|
||||
class="goods-list-item"
|
||||
v-for="(item, index) in data.options.list"
|
||||
:key="index"
|
||||
>
|
||||
<div>
|
||||
<div class="goods-name wes-2">{{ item.title }}</div>
|
||||
<div class="goods-desc">{{ item.desc }}</div>
|
||||
</div>
|
||||
<div class="goods-img">
|
||||
<img alt="楼层图片" :src="item.img" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="setup-box">
|
||||
<div>
|
||||
<el-button
|
||||
size="small"
|
||||
@click.stop="handleSelectModel(data.options.list)"
|
||||
>编辑</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
v-model="showModal"
|
||||
title="装修"
|
||||
|
||||
width="800"
|
||||
|
||||
:close-on-click-modal="false"
|
||||
append-to-body destroy-on-close>
|
||||
<div class="modal-tab-bar">
|
||||
<div class="flex flex-a-c">
|
||||
<el-button class="add-goods" @click="addCurrentGoods">
|
||||
添加商品
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="(item, index) in data.options.list"
|
||||
:key="index"
|
||||
class="draggable"
|
||||
>
|
||||
<div class="flex">
|
||||
<img alt="楼层图片" :src="item.img" class="column-img" />
|
||||
<div class="flex column-goods-config">
|
||||
<div class="column-config">
|
||||
<div>
|
||||
标题:
|
||||
<el-input v-model="item.title" />
|
||||
</div>
|
||||
<div>
|
||||
描述:
|
||||
<el-input v-model="item.desc" />
|
||||
</div>
|
||||
</div>
|
||||
<el-button @click="delGoods(item,index)">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 选择商品。链接 -->
|
||||
<liliDialog
|
||||
ref="liliDialog"
|
||||
@selectedLink="selectedLink"
|
||||
@selectedGoodsData="selectedGoodsData"
|
||||
></liliDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import { formatLinkType } from "@/utils/filters";
|
||||
|
||||
defineOptions({ name: "onlyGoodsList" });
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
const current = ref(0);
|
||||
const showModal = ref(false);
|
||||
const selected = ref({}); // 已选数据
|
||||
|
||||
const liliDialog = ref(null);
|
||||
|
||||
// 删除商品
|
||||
function delGoods(val, i) {
|
||||
props.data.options.list.splice(i, 1);
|
||||
}
|
||||
// 添加当前选项卡中的商品
|
||||
function addCurrentGoods() {
|
||||
liliDialog.value.open("goods");
|
||||
}
|
||||
// 编辑模块
|
||||
function handleSelectModel(item) {
|
||||
selected.value = item;
|
||||
showModal.value = true;
|
||||
}
|
||||
// 选择商品回调
|
||||
function selectedGoodsData(val) {
|
||||
if (val.length) {
|
||||
val.forEach((item) => {
|
||||
const pushGoodsDetail = {
|
||||
img: item.thumbnail,
|
||||
price: item.price,
|
||||
title: item.goodsName,
|
||||
desc: "",
|
||||
url: `/goodsDetail?skuId=${item.id}&goodsId=${item.goodsId}`,
|
||||
};
|
||||
props.data.options.list.push(pushGoodsDetail);
|
||||
});
|
||||
}
|
||||
}
|
||||
// 选择链接回调
|
||||
function selectedLink(val) {
|
||||
selected.value[current.value].url = formatLinkType(val);
|
||||
selected.value[current.value].type =
|
||||
val.___type === "other" && val.url === "" ? "link" : "other";
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "./setup-box.scss";
|
||||
.goods-type-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
.del-btn{
|
||||
margin-left:10px;
|
||||
}
|
||||
.tab-bar {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.draggable {
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #ededed;
|
||||
transition: 0.35s;
|
||||
|
||||
&:hover {
|
||||
background-color: #ededed;
|
||||
}
|
||||
}
|
||||
.column-config {
|
||||
margin-left: 10px;
|
||||
> * {
|
||||
margin: 4px;
|
||||
}
|
||||
}
|
||||
.column-img {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
.add-goods {
|
||||
margin-left: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.goods-list {
|
||||
position: relative;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
&:hover {
|
||||
> .setup-box {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
.column-goods-config {
|
||||
flex: 2;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.goods-list-item {
|
||||
padding-top: 34.8px;
|
||||
margin-bottom: 14.3px;
|
||||
width: 287px;
|
||||
height: 343.7px;
|
||||
border-radius: 9.8px;
|
||||
opacity: 1;
|
||||
cursor: pointer;
|
||||
background: #ffffff;
|
||||
transition: 0.35s;
|
||||
box-shadow: 0px 1px 13px 0px #e5e5e5;
|
||||
&:hover {
|
||||
box-shadow: 0px 1px 14px 0px #c5c5c5;
|
||||
}
|
||||
}
|
||||
.goods-img {
|
||||
text-align: center;
|
||||
> img {
|
||||
width: auto;
|
||||
max-height: 183px;
|
||||
}
|
||||
}
|
||||
.goods-name {
|
||||
margin-bottom: 11.9px;
|
||||
font-size: 25px;
|
||||
font-weight: normal;
|
||||
line-height: 30px;
|
||||
text-align: center;
|
||||
letter-spacing: 0px;
|
||||
|
||||
color: #333333;
|
||||
|
||||
-webkit-text-stroke: #979797 0.7px; /* 浏览器可能不支持 */
|
||||
}
|
||||
.goods-desc {
|
||||
margin-bottom: 30px;
|
||||
font-size: 16px;
|
||||
font-weight: normal;
|
||||
line-height: 19px;
|
||||
text-align: center;
|
||||
letter-spacing: 0px;
|
||||
|
||||
color: #666666;
|
||||
|
||||
-webkit-text-stroke: #979797 0.7px; /* 浏览器可能不支持 */
|
||||
}
|
||||
.goods-price {
|
||||
font-size: 25.2px;
|
||||
font-weight: normal;
|
||||
line-height: 30px;
|
||||
text-align: center;
|
||||
letter-spacing: 0px;
|
||||
|
||||
color: #f31947;
|
||||
-webkit-text-stroke: #979797 0.7px; /* 浏览器可能不支持 */
|
||||
}
|
||||
.goods-type-line {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
.goods-type-title {
|
||||
font-size: 31px;
|
||||
font-weight: normal;
|
||||
line-height: 37px;
|
||||
letter-spacing: 0px;
|
||||
|
||||
color: #333333;
|
||||
}
|
||||
.active {
|
||||
color: #f31947;
|
||||
}
|
||||
.goods-type-labels {
|
||||
font-size: 21px;
|
||||
font-weight: normal;
|
||||
line-height: 25px;
|
||||
letter-spacing: 0px;
|
||||
}
|
||||
.goods-type-item {
|
||||
margin-left: 28px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,581 +0,0 @@
|
||||
<template>
|
||||
<div class="recommend">
|
||||
<div class="recommend-left">
|
||||
<div
|
||||
class="head-recommend setup-content"
|
||||
:style="{ background: msgLeft.bgColor }"
|
||||
>
|
||||
<span>{{ msgLeft.title }}</span>
|
||||
<span>{{ msgLeft.secondTitle }}></span>
|
||||
<div class="setup-box">
|
||||
<div>
|
||||
<el-button size="small" @click.stop="handleSelectModel(msgLeft, true)"
|
||||
>编辑</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content-left">
|
||||
<div class="setup-content">
|
||||
<img :src="msgLeft.list[0].img" width="160" height="160" alt="" />
|
||||
<div class="margin-left">{{ msgLeft.list[0].name }}</div>
|
||||
<div class="margin-left">{{ msgLeft.list[0].describe }}</div>
|
||||
<el-button
|
||||
size="small"
|
||||
:style="{ background: msgLeft.bgColor }"
|
||||
class="fz_12 view-btn"
|
||||
>点击查看</el-button
|
||||
>
|
||||
<div class="setup-box">
|
||||
<div>
|
||||
<el-button
|
||||
size="small"
|
||||
@click.stop="handleSelectModel(msgLeft.list[0])"
|
||||
>编辑</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<template v-for="(item, index) in msgLeft.list">
|
||||
<div v-if="index != 0" :key="index" class="setup-content">
|
||||
<img :src="item.img" width="80" height="80" alt="" />
|
||||
<div>
|
||||
<div>{{ item.name }}</div>
|
||||
<div>{{ item.describe }}</div>
|
||||
</div>
|
||||
<div class="setup-box">
|
||||
<div>
|
||||
<el-button size="small" @click.stop="handleSelectModel(item)"
|
||||
>编辑</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="recommend-right">
|
||||
<div
|
||||
class="head-recommend setup-content"
|
||||
:style="{ background: msgRight.bgColor }"
|
||||
>
|
||||
<span>{{ msgRight.title }}</span>
|
||||
<span>{{ msgRight.secondTitle }}></span>
|
||||
<div class="setup-box">
|
||||
<div>
|
||||
<el-button size="small" @click.stop="handleSelectModel(msgRight, true)"
|
||||
>编辑</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content-right">
|
||||
<div
|
||||
v-for="(item, index) in msgRight.list"
|
||||
:key="index"
|
||||
class="setup-content"
|
||||
>
|
||||
<div class="right-item">
|
||||
<div>
|
||||
<span :style="{ background: msgRight.bgColor }">{{
|
||||
item.name
|
||||
}}</span>
|
||||
<span>{{ item.describe }}</span>
|
||||
</div>
|
||||
<div class="right-img">
|
||||
<img :src="item.img" alt="" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="setup-box">
|
||||
<div>
|
||||
<el-button size="small" @click.stop="handleSelectModel(item)"
|
||||
>编辑</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog
|
||||
v-model="showModal"
|
||||
title="装修"
|
||||
width="800px"
|
||||
:close-on-click-modal="false"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
>
|
||||
<div class="modal-top-advert">
|
||||
<div class="modal-form-item modal-form-preview">
|
||||
<img
|
||||
class="show-image"
|
||||
width="160"
|
||||
height="160"
|
||||
v-if="selected.size && selected.size.indexOf('160*160') >= 0"
|
||||
:src="selected.img"
|
||||
alt
|
||||
/>
|
||||
<img
|
||||
class="show-image"
|
||||
width="80"
|
||||
height="80"
|
||||
v-if="selected.size && selected.size.indexOf('80*80') >= 0"
|
||||
:src="selected.img"
|
||||
alt
|
||||
/>
|
||||
<img
|
||||
class="show-image"
|
||||
width="100"
|
||||
height="100"
|
||||
v-if="selected.size && selected.size.indexOf('100*100') >= 0"
|
||||
:src="selected.img"
|
||||
alt
|
||||
/>
|
||||
</div>
|
||||
<div class="modal-form-item">
|
||||
<span class="modal-label">图片主标题:</span>
|
||||
<el-input v-model="selected.name" />
|
||||
</div>
|
||||
<div class="modal-form-item">
|
||||
<span class="modal-label">图片描述:</span>
|
||||
<el-input v-model="selected.describe" />
|
||||
</div>
|
||||
<div class="tips">建议尺寸:{{ selected.size }}</div>
|
||||
<div class="modal-form-item modal-form-link">
|
||||
<span class="modal-label">图片链接:</span>
|
||||
<div class="modal-link-field">
|
||||
<el-input
|
||||
class="outsideUrl"
|
||||
v-model="selected.url"
|
||||
:disabled="!!selected.type && selected.type !== 'link'"
|
||||
placeholder="https://"
|
||||
/>
|
||||
<el-button size="small" type="primary" @click="handleSelectLink"
|
||||
>选择链接</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-form-item modal-form-actions">
|
||||
<el-button size="small" type="primary" @click="handleSelectImg"
|
||||
>选择图片</el-button
|
||||
>
|
||||
<el-button size="small" type="primary" @click="handleSelectGoods"
|
||||
>选择商品</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="handleCancelModal">取消</el-button>
|
||||
<el-button type="primary" @click="handleConfirmModal">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<el-dialog
|
||||
v-model="showModal1"
|
||||
title="装修"
|
||||
width="800px"
|
||||
:close-on-click-modal="false"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
>
|
||||
<div class="modal-top-advert">
|
||||
<div class="modal-form-item">
|
||||
<span class="modal-label">主标题:</span>
|
||||
<el-input v-model="selected.title" />
|
||||
</div>
|
||||
<div class="modal-form-item">
|
||||
<span class="modal-label">副标题:</span>
|
||||
<el-input v-model="selected.secondTitle" />
|
||||
</div>
|
||||
<div class="modal-form-item modal-form-link">
|
||||
<span class="modal-label">副标题链接:</span>
|
||||
<div class="modal-link-field">
|
||||
<el-input
|
||||
class="outsideUrl"
|
||||
v-model="selected.url"
|
||||
:disabled="!!selected.type && selected.type !== 'link'"
|
||||
placeholder="https://"
|
||||
/>
|
||||
<el-button size="small" type="primary" @click="handleSelectLink"
|
||||
>选择链接</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-form-item">
|
||||
<span class="modal-label">背景色:</span>
|
||||
<div class="modal-color-field">
|
||||
<el-input v-model="selected.bgColor" />
|
||||
<el-color-picker v-if="selected.bgColor" v-model="selected.bgColor" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="handleCancelModal1">取消</el-button>
|
||||
<el-button type="primary" @click="handleConfirmModal1">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<!-- 选择商品。链接 -->
|
||||
<liliDialog
|
||||
ref="liliDialogRef"
|
||||
@selectedLink="selectedLink"
|
||||
@selectedGoodsData="selectedGoodsData"
|
||||
></liliDialog>
|
||||
<!-- 选择图片 -->
|
||||
<el-dialog v-model="picModelFlag" width="1200px" append-to-body destroy-on-close>
|
||||
<ossManage
|
||||
@callback="callbackSelected"
|
||||
:is-component="true"
|
||||
:initialize="picModelFlag"
|
||||
:max-select="1"
|
||||
ref="ossManageRef"
|
||||
/>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { nextTick, reactive, ref } from "vue";
|
||||
import ossManage from "@/views/sys/oss-manage/ossManage";
|
||||
import { formatLinkType } from "@/utils/filters";
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
const msgLeft = reactive(props.data.options.contentLeft); // 左侧数据
|
||||
const msgRight = reactive(props.data.options.contentRight); // 右侧数据
|
||||
const showModal = ref(false); // modal显隐
|
||||
const showModal1 = ref(false); // modal显隐
|
||||
const selected = ref({}); // 弹窗编辑草稿
|
||||
const editTarget = ref(null); // 确定后写回的原数据
|
||||
const picModelFlag = ref(false); // 图片选择
|
||||
|
||||
const liliDialogRef = ref(null);
|
||||
const ossManageRef = ref(null);
|
||||
|
||||
function cloneItem(item) {
|
||||
return JSON.parse(JSON.stringify(item || {}));
|
||||
}
|
||||
// 编辑
|
||||
function handleSelectModel(item, type) {
|
||||
editTarget.value = item;
|
||||
selected.value = cloneItem(item);
|
||||
if (type) {
|
||||
showModal1.value = true;
|
||||
} else {
|
||||
showModal.value = true;
|
||||
}
|
||||
}
|
||||
function handleSelectLink(item, index) {
|
||||
// 调起选择链接弹窗
|
||||
liliDialogRef.value.open("link");
|
||||
}
|
||||
function handleSelectGoods(item) {
|
||||
// 调起选择商品
|
||||
liliDialogRef.value.open("goods", "single");
|
||||
}
|
||||
// 选择链接回调
|
||||
function selectedLink(val) {
|
||||
selected.value.url = formatLinkType(val);
|
||||
selected.value.type =
|
||||
val.___type === "other" && val.url === "" ? "link" : "other";
|
||||
}
|
||||
// 选择商品回调
|
||||
function selectedGoodsData(val) {
|
||||
let goods = val[0];
|
||||
selected.value.img = goods.thumbnail;
|
||||
selected.value.price = goods.price;
|
||||
selected.value.name = goods.goodsName;
|
||||
selected.value.url = `/goodsDetail?skuId=${goods.id}&goodsId=${goods.goodsId}`;
|
||||
}
|
||||
function handleSelectImg() {
|
||||
// 选择图片
|
||||
picModelFlag.value = true;
|
||||
nextTick(() => {
|
||||
if (ossManageRef.value) {
|
||||
ossManageRef.value.selectImage = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
// 选择图片回调
|
||||
function callbackSelected(val) {
|
||||
picModelFlag.value = false;
|
||||
if (!val?.url) return;
|
||||
selected.value.img = val.url;
|
||||
}
|
||||
function handleCancelModal() {
|
||||
editTarget.value = null;
|
||||
selected.value = {};
|
||||
showModal.value = false;
|
||||
}
|
||||
function handleConfirmModal() {
|
||||
if (editTarget.value) {
|
||||
Object.assign(editTarget.value, cloneItem(selected.value));
|
||||
}
|
||||
editTarget.value = null;
|
||||
selected.value = {};
|
||||
showModal.value = false;
|
||||
}
|
||||
function handleCancelModal1() {
|
||||
editTarget.value = null;
|
||||
selected.value = {};
|
||||
showModal1.value = false;
|
||||
}
|
||||
function handleConfirmModal1() {
|
||||
if (editTarget.value) {
|
||||
Object.assign(editTarget.value, cloneItem(selected.value));
|
||||
}
|
||||
editTarget.value = null;
|
||||
selected.value = {};
|
||||
showModal1.value = false;
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "./setup-box.scss";
|
||||
.recommend {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.recommend-left {
|
||||
width: 595px;
|
||||
.content-left {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
padding-top: 10px;
|
||||
font-size: 12px;
|
||||
box-sizing: content-box;
|
||||
> div:nth-child(1) {
|
||||
width: 189px;
|
||||
border-right: 1px solid #eee;
|
||||
height: 360px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
img {
|
||||
margin: 40px 0 0 15px;
|
||||
}
|
||||
.margin-left {
|
||||
margin-left: 15px;
|
||||
width: 145px;
|
||||
}
|
||||
div:nth-of-type(1) {
|
||||
font-weight: bold;
|
||||
border-top: 1px solid #eee;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
div:nth-of-type(2) {
|
||||
color: #999;
|
||||
}
|
||||
.view-btn {
|
||||
align-self: center;
|
||||
width: 145px;
|
||||
margin-top: auto;
|
||||
margin-bottom: 15px;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
> div:nth-child(2) {
|
||||
width: 405px;
|
||||
height: 360px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-content: flex-start;
|
||||
box-sizing: border-box;
|
||||
> div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 200px;
|
||||
height: 120px;
|
||||
img {
|
||||
margin: 0 10px;
|
||||
}
|
||||
> div:nth-child(2) {
|
||||
// margin: 0 10px;
|
||||
:nth-child(2) {
|
||||
color: #449dae;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.recommend-right {
|
||||
width: 595px;
|
||||
|
||||
.head-recommend {
|
||||
background: #a25684;
|
||||
}
|
||||
.content-right {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-rows: repeat(2, 1fr);
|
||||
height: 360px;
|
||||
padding-top: 10px;
|
||||
box-sizing: content-box;
|
||||
|
||||
> div {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
padding: 0;
|
||||
border-right: 1px solid #eee;
|
||||
border-bottom: 1px solid #eee;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:nth-child(2n) {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
&:nth-child(n + 3) {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.right-item {
|
||||
border-bottom: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 10px 15px;
|
||||
font-size: 12px;
|
||||
box-sizing: border-box;
|
||||
|
||||
> div:nth-child(1) {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-top: 0;
|
||||
text-align: left;
|
||||
|
||||
span:nth-child(1) {
|
||||
color: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 0 5px;
|
||||
background-color: #a25684;
|
||||
display: block;
|
||||
max-width: 120px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
|
||||
span:nth-child(2) {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.right-img {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
flex-shrink: 0;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
|
||||
img {
|
||||
max-height: 100px;
|
||||
max-width: 100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.head-recommend {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 50px;
|
||||
padding: 0 10px;
|
||||
background: #449dae;
|
||||
color: #fff;
|
||||
span:nth-child(1) {
|
||||
font-size: 20px;
|
||||
}
|
||||
span:nth-child(2) {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-top-advert {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
padding: 0 20px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
.modal-form-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.modal-label {
|
||||
flex-shrink: 0;
|
||||
width: 100px;
|
||||
text-align: right;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
.el-input {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-form-preview {
|
||||
justify-content: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.modal-form-link {
|
||||
align-items: flex-start;
|
||||
|
||||
.modal-label {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.modal-link-field {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
|
||||
.outsideUrl {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-form-actions {
|
||||
padding-left: 112px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.modal-color-field {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
.el-input {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.tips {
|
||||
padding-left: 112px;
|
||||
margin-bottom: 16px;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,294 +0,0 @@
|
||||
<template>
|
||||
<div class="seckill">
|
||||
<div class="desc">秒杀商品需要在促销活动中添加商品,有商品时才会在首页展示</div>
|
||||
<div class="aside">
|
||||
<div class="title">{{ actName }}</div>
|
||||
<div class="hour">
|
||||
<span>{{ currHour }}:00</span>点场 倒计时
|
||||
</div>
|
||||
<div class="count-down" v-if="actStatus === 1">
|
||||
<span>{{ hours }}</span
|
||||
><span>{{ minutes }}</span
|
||||
><span>{{ seconds }}</span>
|
||||
</div>
|
||||
<div class="act-status" v-else>
|
||||
{{ actStatus == 0 ? "未开始" : "已结束" }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div ref="swiperEl" class="swiper">
|
||||
<div class="swiper-wrapper">
|
||||
<div
|
||||
class="swiper-slide"
|
||||
v-for="(item, index) in options.list[0].goodsList"
|
||||
:key="index"
|
||||
>
|
||||
<div class="content">
|
||||
<img :src="item.img" width="140" height="140" :alt="item.name" />
|
||||
<div class="ellipsis">{{ item.name }}</div>
|
||||
<div>
|
||||
<span>{{ $filters.unitPrice(item.price, "¥") }}</span>
|
||||
<span>{{ $filters.unitPrice(item.originalPrice, "¥") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, watch, onMounted, onBeforeUnmount } from "vue";
|
||||
import Swiper from "swiper";
|
||||
import "swiper/swiper-bundle.css";
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const options = ref(props.data.options); // 当前数据
|
||||
const actStatus = ref(0); // 0 未开始 1 进行中 2 已结束
|
||||
const actName = ref("限时秒杀");
|
||||
const currHour = ref("00"); // 当前秒杀场
|
||||
const diffSeconds = ref(0); // 倒地时
|
||||
const hours = ref("00"); // 小时
|
||||
const minutes = ref("00"); // 分钟
|
||||
const seconds = ref("00"); // 秒
|
||||
const interval = ref(undefined); // 定时器
|
||||
const swiperInstance = ref(null);
|
||||
const swiperEl = ref(null);
|
||||
|
||||
watch(diffSeconds, (val) => {
|
||||
const h = Math.floor(val / 3600);
|
||||
// 当前秒数 / 60,向下取整
|
||||
// 获取到所有分钟数 3600 / 60 = 60分钟
|
||||
// 对60取模,超过小时数的分钟数
|
||||
const m = Math.floor(val / 60) % 60;
|
||||
// 当前的秒数 % 60,获取到 超过小时数、分钟数的秒数(秒数)
|
||||
const s = val % 60;
|
||||
hours.value = h < 10 ? "0" + h : h;
|
||||
minutes.value = m < 10 ? "0" + m : m;
|
||||
seconds.value = s < 10 ? "0" + s : s;
|
||||
|
||||
if (val === 0) {
|
||||
clearInterval(interval.value);
|
||||
hours.value = 0;
|
||||
minutes.value = 0;
|
||||
seconds.value = 0;
|
||||
countDown(options.value.list);
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
countDown(options.value.list);
|
||||
initSwiper();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearInterval(interval.value);
|
||||
if (swiperInstance.value) {
|
||||
swiperInstance.value.destroy(true, true);
|
||||
swiperInstance.value = null;
|
||||
}
|
||||
});
|
||||
|
||||
function initSwiper() {
|
||||
if (!swiperEl.value) {
|
||||
return;
|
||||
}
|
||||
swiperInstance.value = new Swiper(swiperEl.value, {
|
||||
slidesPerView: 5,
|
||||
autoplay: true,
|
||||
loop: true,
|
||||
});
|
||||
}
|
||||
// 倒计时
|
||||
function countDown(list) {
|
||||
/**
|
||||
* 默认倒计时两小时
|
||||
* 如果没有开始,则显示未开始
|
||||
* 进行中显示倒计时
|
||||
* 今天的秒杀结束则显示已结束
|
||||
*/
|
||||
let nowHour = new Date().getHours();
|
||||
if (nowHour < Number(list[0].time)) {
|
||||
// 活动未开始
|
||||
currHour.value = list[0].time;
|
||||
actStatus.value = 0;
|
||||
} else if (nowHour >= Number(list[list.length - 1].time + 2)) {
|
||||
// 活动已结束
|
||||
actStatus.value = 2;
|
||||
currHour.value = list[list.length - 1].time;
|
||||
} else {
|
||||
// 活动进行中
|
||||
actStatus.value = 1;
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
if (nowHour == Number(list[i].time)) {
|
||||
currHour.value = list[i].time;
|
||||
}
|
||||
if (
|
||||
nowHour > Number(list[i].time) &&
|
||||
nowHour < Number(list[i].time + 2)
|
||||
) {
|
||||
currHour.value = list[i].time;
|
||||
}
|
||||
}
|
||||
// 当前0点时间戳
|
||||
let zeroTime = new Date(new Date().toLocaleDateString()).getTime();
|
||||
// 活动倒计时
|
||||
diffSeconds.value = Math.floor(
|
||||
(zeroTime +
|
||||
3600 * 1000 * (currHour.value + 2) -
|
||||
new Date().getTime()) /
|
||||
1000
|
||||
);
|
||||
interval.value = setInterval(() => {
|
||||
diffSeconds.value--;
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.seckill {
|
||||
width: 100%;
|
||||
height: 260px;
|
||||
display: flex;
|
||||
position: relative;
|
||||
.desc{
|
||||
position: absolute;
|
||||
color: $theme_color;
|
||||
left: 200px;
|
||||
top: 5px;
|
||||
}
|
||||
.aside {
|
||||
overflow: hidden;
|
||||
width: 190px;
|
||||
height: 100%;
|
||||
color: #fff;
|
||||
background-image: url("../../../assets/seckillBg.png");
|
||||
|
||||
.title {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 28px;
|
||||
margin-top: 31px;
|
||||
}
|
||||
|
||||
.hour {
|
||||
margin-top: 90px;
|
||||
text-align: center;
|
||||
span {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.count-down {
|
||||
margin: 10px 0 0 30px;
|
||||
> span {
|
||||
position: relative;
|
||||
float: left;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
text-align: center;
|
||||
background-color: #2f3430;
|
||||
margin-right: 20px;
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
&::after {
|
||||
content: ":";
|
||||
display: block;
|
||||
position: absolute;
|
||||
right: -20px;
|
||||
font-weight: bolder;
|
||||
font-size: 18px;
|
||||
width: 20px;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
> span:last-child::after {
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
|
||||
.act-status {
|
||||
margin: 10px 0 0 65px;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.section {
|
||||
width: 1000px;
|
||||
// background: #efefef;
|
||||
.swiper-slide {
|
||||
height: 260px;
|
||||
.content {
|
||||
width: 200px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
&::after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 0;
|
||||
width: 1px;
|
||||
height: 200px;
|
||||
transform: translateY(-50%);
|
||||
background: linear-gradient(180deg, white, #eeeeee, white);
|
||||
}
|
||||
img {
|
||||
margin-top: 30px;
|
||||
}
|
||||
> div {
|
||||
width: 160px;
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
position: relative;
|
||||
}
|
||||
> div:nth-of-type(1):hover {
|
||||
color: $theme_color;
|
||||
cursor: pointer;
|
||||
}
|
||||
> div:nth-of-type(2) {
|
||||
border: 1px solid $theme_color;
|
||||
line-height: 24px;
|
||||
display: flex;
|
||||
text-align: center;
|
||||
|
||||
span:nth-child(1) {
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
width: 92px;
|
||||
background-color: $theme_color;
|
||||
position: relative;
|
||||
&::before {
|
||||
content: " ";
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-color: transparent white transparent transparent;
|
||||
border-style: solid;
|
||||
border-width: 24px 8px 0 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 84px;
|
||||
}
|
||||
}
|
||||
|
||||
span:nth-child(2) {
|
||||
color: #999;
|
||||
width: 66px;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,293 +0,0 @@
|
||||
<template>
|
||||
<div class="seckill-wrap">
|
||||
<div class="desc">秒杀商品需要在促销活动中添加商品,有商品时才会在首页展示</div>
|
||||
<div class="seckill">
|
||||
<div class="aside">
|
||||
<div class="title">{{ actName }}</div>
|
||||
<div class="time-block">
|
||||
<div class="hour">
|
||||
<span>{{ currHour }}:00</span>点场 倒计时
|
||||
</div>
|
||||
<div class="count-down" v-if="actStatus === 1">
|
||||
<span>{{ hours }}</span>
|
||||
<span>{{ minutes }}</span>
|
||||
<span>{{ seconds }}</span>
|
||||
</div>
|
||||
<div class="act-status" v-else>
|
||||
{{ actStatus == 0 ? "未开始" : "已结束" }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="seckill-list">
|
||||
<div
|
||||
class="seckill-item"
|
||||
v-for="(item, index) in options.list[0].goodsList"
|
||||
:key="index"
|
||||
>
|
||||
<div class="content">
|
||||
<img :src="item.img" width="140" height="140" :alt="item.name" />
|
||||
<div class="ellipsis">{{ item.name }}</div>
|
||||
<div class="price-row">
|
||||
<span>{{ $filters.unitPrice(item.price, "¥") }}</span>
|
||||
<span>{{ $filters.unitPrice(item.originalPrice, "¥") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const options = ref(props.data.options);
|
||||
const actStatus = ref(0);
|
||||
const actName = ref("限时秒杀");
|
||||
const currHour = ref("00");
|
||||
const diffSeconds = ref(0);
|
||||
const hours = ref("00");
|
||||
const minutes = ref("00");
|
||||
const seconds = ref("00");
|
||||
let interval = undefined;
|
||||
|
||||
watch(diffSeconds, (val) => {
|
||||
const h = Math.floor(val / 3600);
|
||||
const m = Math.floor(val / 60) % 60;
|
||||
const s = val % 60;
|
||||
hours.value = h < 10 ? "0" + h : h;
|
||||
minutes.value = m < 10 ? "0" + m : m;
|
||||
seconds.value = s < 10 ? "0" + s : s;
|
||||
|
||||
if (val === 0) {
|
||||
clearInterval(interval);
|
||||
hours.value = 0;
|
||||
minutes.value = 0;
|
||||
seconds.value = 0;
|
||||
countDown(options.value.list);
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
countDown(options.value.list);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearInterval(interval);
|
||||
});
|
||||
|
||||
function countDown(list) {
|
||||
const nowHour = new Date().getHours();
|
||||
if (nowHour < Number(list[0].time)) {
|
||||
currHour.value = list[0].time;
|
||||
actStatus.value = 0;
|
||||
} else if (nowHour >= Number(list[list.length - 1].time + 2)) {
|
||||
actStatus.value = 2;
|
||||
currHour.value = list[list.length - 1].time;
|
||||
} else {
|
||||
actStatus.value = 1;
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
if (nowHour == Number(list[i].time)) {
|
||||
currHour.value = list[i].time;
|
||||
}
|
||||
if (
|
||||
nowHour > Number(list[i].time) &&
|
||||
nowHour < Number(list[i].time + 2)
|
||||
) {
|
||||
currHour.value = list[i].time;
|
||||
}
|
||||
}
|
||||
const zeroTime = new Date(new Date().toLocaleDateString()).getTime();
|
||||
diffSeconds.value = Math.floor(
|
||||
(zeroTime +
|
||||
3600 * 1000 * (currHour.value + 2) -
|
||||
new Date().getTime()) /
|
||||
1000
|
||||
);
|
||||
interval = setInterval(() => {
|
||||
diffSeconds.value--;
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.seckill-wrap {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.desc {
|
||||
color: $theme_color;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
padding: 0 0 8px;
|
||||
}
|
||||
|
||||
.seckill {
|
||||
width: 100%;
|
||||
height: 260px;
|
||||
display: flex;
|
||||
background-color: #fff;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.aside {
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
width: 190px;
|
||||
height: 100%;
|
||||
color: #fff;
|
||||
background-image: url("../../../assets/seckillBg.png");
|
||||
background-repeat: no-repeat;
|
||||
background-position: center top;
|
||||
|
||||
.title {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 28px;
|
||||
margin-top: 31px;
|
||||
}
|
||||
|
||||
.time-block {
|
||||
margin-top: 78px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hour {
|
||||
text-align: center;
|
||||
|
||||
span {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.count-down {
|
||||
margin-top: 10px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
> span {
|
||||
position: relative;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
text-align: center;
|
||||
background-color: #2f3430;
|
||||
margin-right: 20px;
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
|
||||
&::after {
|
||||
content: ":";
|
||||
position: absolute;
|
||||
right: -20px;
|
||||
font-weight: bolder;
|
||||
font-size: 18px;
|
||||
width: 20px;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
> span:last-child {
|
||||
margin-right: 0;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.act-status {
|
||||
margin-top: 10px;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.section {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
background-color: #fff;
|
||||
|
||||
.seckill-list {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
min-width: min-content;
|
||||
}
|
||||
|
||||
.seckill-item {
|
||||
flex: 0 0 200px;
|
||||
height: 100%;
|
||||
|
||||
.content {
|
||||
width: 200px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 0;
|
||||
width: 1px;
|
||||
height: 200px;
|
||||
transform: translateY(-50%);
|
||||
background: linear-gradient(180deg, white, #eeeeee, white);
|
||||
}
|
||||
|
||||
img {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.ellipsis {
|
||||
width: 160px;
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.price-row {
|
||||
width: 160px;
|
||||
margin-top: 10px;
|
||||
border: 1px solid $theme_color;
|
||||
line-height: 24px;
|
||||
display: flex;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
|
||||
span:nth-child(1) {
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
background-color: $theme_color;
|
||||
}
|
||||
|
||||
span:nth-child(2) {
|
||||
color: #999;
|
||||
width: 66px;
|
||||
flex-shrink: 0;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,49 +0,0 @@
|
||||
.setup-content{
|
||||
position: relative;
|
||||
&:hover{
|
||||
.setup-box{
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.setup-box{
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: rgba(0, 0, 0, .2);
|
||||
z-index: 99;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.el-button{
|
||||
float: right;
|
||||
margin-right: 5px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-tab-bar{
|
||||
margin-left: 20px;
|
||||
table{
|
||||
margin-top: 10px;
|
||||
display:inline-block;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
max-height: 400px !important;
|
||||
overflow: hidden auto;
|
||||
}
|
||||
thead{
|
||||
background-color: #eee;
|
||||
th{
|
||||
padding: 5px 0;
|
||||
}
|
||||
}
|
||||
td{
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
tbody>tr:hover{
|
||||
background-color: aliceblue;
|
||||
}
|
||||
}
|
||||
454
seller/src/views/shop/pc/config-panel/ComponentConfigPanel.vue
Normal file
454
seller/src/views/shop/pc/config-panel/ComponentConfigPanel.vue
Normal file
@@ -0,0 +1,454 @@
|
||||
<template>
|
||||
<div class="component-config-panel" v-if="component">
|
||||
<!-- 1. 容器大小 -->
|
||||
<div class="setting-collapse-card">
|
||||
<div class="collapse-header" @click="expandSize = !expandSize">
|
||||
<div class="header-left">
|
||||
<el-icon class="arrow-icon" :class="{ 'is-expanded': expandSize }"><ArrowRight /></el-icon>
|
||||
<span class="header-title">容器大小</span>
|
||||
<el-tooltip content="设置当前组件的排版模式、内外边距及圆角样式" placement="top">
|
||||
<el-icon class="help-icon"><QuestionFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="collapse-body" v-show="expandSize">
|
||||
<!-- 排版方式 -->
|
||||
<div class="form-row">
|
||||
<span class="row-label">排版宽度</span>
|
||||
<el-select v-model="compStyle.widthRatio" size="small" class="full-select">
|
||||
<el-option
|
||||
v-for="opt in layoutOptions"
|
||||
:key="opt.value"
|
||||
:value="opt.value"
|
||||
:label="opt.label"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<!-- 可视化嵌套盒模型 (外边距 + 内边距 + 圆角) -->
|
||||
<div class="box-model-section">
|
||||
<nested-box-model
|
||||
:key="props.component?.id || props.component?.key || 'comp'"
|
||||
:margin="compStyle.margin"
|
||||
:padding="compStyle.padding"
|
||||
:radius="compStyle.radius"
|
||||
:show-radius="true"
|
||||
@update:margin="val => compStyle.margin = val"
|
||||
@update:padding="val => compStyle.padding = val"
|
||||
@update:radius="val => compStyle.radius = val"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. 背景设置 -->
|
||||
<div class="setting-collapse-card">
|
||||
<div class="collapse-header" @click="expandBg = !expandBg">
|
||||
<div class="header-left">
|
||||
<el-icon class="arrow-icon" :class="{ 'is-expanded': expandBg }"><ArrowRight /></el-icon>
|
||||
<span class="header-title">背景设置</span>
|
||||
<el-tooltip content="设置当前组件的背景颜色与背景图片" placement="top">
|
||||
<el-icon class="help-icon"><QuestionFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="collapse-body" v-show="expandBg">
|
||||
<!-- 背景色 -->
|
||||
<div class="form-row">
|
||||
<span class="row-label">背景色</span>
|
||||
<div class="color-picker-row">
|
||||
<el-color-picker
|
||||
v-model="compStyle.backgroundColor"
|
||||
show-alpha
|
||||
size="small"
|
||||
class="color-picker-swatch"
|
||||
:predefine="predefineColors"
|
||||
@change="handleColorChange"
|
||||
@active-change="handleColorActiveChange"
|
||||
/>
|
||||
<el-input
|
||||
v-model="compStyle.backgroundColor"
|
||||
size="small"
|
||||
class="color-input"
|
||||
placeholder="透明"
|
||||
clearable
|
||||
@blur="handleInputBlur"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 背景图片 -->
|
||||
<div class="form-row mt-10">
|
||||
<span class="row-label">
|
||||
背景图片
|
||||
<el-tooltip content="建议大小: 500KB 内; 建议格式: WebP/JPG/PNG" placement="top">
|
||||
<el-icon class="help-icon"><QuestionFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
<div class="bg-img-btns">
|
||||
<el-button size="small" @click="handleSelectImage" class="action-btn">
|
||||
选择图片
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
:disabled="!compStyle.backgroundImage"
|
||||
@click="handleClearImage"
|
||||
class="action-btn secondary"
|
||||
>
|
||||
{{ compStyle.backgroundImage ? '清除背景图' : '未选择背景图' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="compStyle.backgroundImage" class="bg-preview-row">
|
||||
<img :src="compStyle.backgroundImage" class="bg-preview-img" alt="背景预览" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-panel">
|
||||
请在画布中选中一个组件进行组件配置
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, inject } from 'vue';
|
||||
import { ArrowRight, QuestionFilled } from '@element-plus/icons-vue';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import NestedBoxModel from '../controls/NestedBoxModel.vue';
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
});
|
||||
|
||||
const expandSize = ref(true);
|
||||
const expandBg = ref(true);
|
||||
|
||||
const layoutOptions = computed(() => {
|
||||
if (['topBar', 'searchHeader', 'footerNav', 'categoryNav'].includes(props.component?.type)) {
|
||||
return [
|
||||
{ value: '100%', label: '整行' }
|
||||
];
|
||||
}
|
||||
if (['channelGrid', 'freeLayout', 'promoGroup', 'video', 'goodsList', 'gridFloor', 'goodsGrid', 'goodsCard', 'bannerFloor', 'goodsBar', 'channelFloor', 'recommendFeed', 'couponList', 'titleBar', 'noticeBar'].includes(props.component?.type)) {
|
||||
return [
|
||||
{ value: '100%', label: '整行' },
|
||||
{ value: '66.67%', label: '三分之二' },
|
||||
{ value: '50%', label: '一半' },
|
||||
{ value: '33.33%', label: '三分之一' },
|
||||
{ value: '16.67%', label: '六分之一' }
|
||||
];
|
||||
}
|
||||
if (props.component?.type === 'carousel') {
|
||||
return [
|
||||
{ value: '66.67%', label: '三分之二' },
|
||||
{ value: '100%', label: '整行' },
|
||||
{ value: '50%', label: '一半' },
|
||||
{ value: '33.33%', label: '三分之一' },
|
||||
{ value: '16.67%', label: '六分之一' }
|
||||
];
|
||||
}
|
||||
if (['categoryMegaMenu', 'userBenefits'].includes(props.component?.type)) {
|
||||
return [
|
||||
{ value: '16.67%', label: '六分之一' },
|
||||
{ value: '33.33%', label: '三分之一' },
|
||||
{ value: '50%', label: '一半' }
|
||||
];
|
||||
}
|
||||
if (['brandWall', 'hotZone', 'articleContent'].includes(props.component?.type)) {
|
||||
return [
|
||||
{ value: '100%', label: '整行' },
|
||||
{ value: '66.67%', label: '三分之二' },
|
||||
{ value: '50%', label: '一半' },
|
||||
{ value: '33.33%', label: '三分之一' }
|
||||
];
|
||||
}
|
||||
return [
|
||||
{ value: '100%', label: '整行' },
|
||||
{ value: 'auto', label: '自适应' },
|
||||
{ value: '50%', label: '半行 (50%)' },
|
||||
{ value: '33.3%', label: '三分之一 (33.3%)' },
|
||||
{ value: '25%', label: '四分之一 (25%)' }
|
||||
];
|
||||
});
|
||||
|
||||
const compStyle = computed(() => {
|
||||
if (!props.component) return {};
|
||||
if (!props.component.componentStyle) {
|
||||
props.component.componentStyle = {};
|
||||
}
|
||||
const s = props.component.componentStyle;
|
||||
if (!Array.isArray(s.margin)) s.margin = [0, 0, 0, 0];
|
||||
if (!Array.isArray(s.padding)) s.padding = [0, 0, 0, 0];
|
||||
if (!Array.isArray(s.radius)) s.radius = [0, 0, 0, 0];
|
||||
if (['topBar', 'searchHeader', 'footerNav', 'categoryNav'].includes(props.component?.type)) {
|
||||
s.widthRatio = '100%';
|
||||
} else if (!s.widthRatio) {
|
||||
s.widthRatio = props.component?.type === 'carousel' ? '66.67%' : (['categoryMegaMenu', 'userBenefits'].includes(props.component?.type) ? '16.67%' : '100%');
|
||||
}
|
||||
if (['channelGrid', 'freeLayout', 'promoGroup', 'video', 'goodsList', 'gridFloor', 'goodsGrid', 'goodsCard', 'bannerFloor', 'goodsBar', 'channelFloor', 'recommendFeed', 'couponList', 'titleBar', 'noticeBar'].includes(props.component?.type)) {
|
||||
if (s.widthRatio === '33.3%') s.widthRatio = '33.33%';
|
||||
else if (s.widthRatio === '66.6%' || s.widthRatio === '66.7%') s.widthRatio = '66.67%';
|
||||
else if (s.widthRatio === '16.6%' || s.widthRatio === '16.7%') s.widthRatio = '16.67%';
|
||||
else if (!['100%', '66.67%', '50%', '33.33%', '16.67%'].includes(s.widthRatio)) {
|
||||
s.widthRatio = '100%';
|
||||
}
|
||||
}
|
||||
if (props.component?.type === 'carousel') {
|
||||
if (s.widthRatio === '33.3%') s.widthRatio = '33.33%';
|
||||
else if (s.widthRatio === '66.6%' || s.widthRatio === '66.7%') s.widthRatio = '66.67%';
|
||||
else if (s.widthRatio === '16.6%' || s.widthRatio === '16.7%') s.widthRatio = '16.67%';
|
||||
else if (!['66.67%', '100%', '50%', '33.33%', '16.67%'].includes(s.widthRatio)) {
|
||||
s.widthRatio = '66.67%';
|
||||
}
|
||||
}
|
||||
if (['categoryMegaMenu', 'userBenefits'].includes(props.component?.type)) {
|
||||
if (s.widthRatio === '33.3%') s.widthRatio = '33.33%';
|
||||
else if (s.widthRatio === '16.6%' || s.widthRatio === '16.7%') s.widthRatio = '16.67%';
|
||||
else if (!['16.67%', '33.33%', '50%'].includes(s.widthRatio)) {
|
||||
s.widthRatio = '16.67%';
|
||||
}
|
||||
}
|
||||
if (['brandWall', 'hotZone', 'articleContent'].includes(props.component?.type)) {
|
||||
if (s.widthRatio === '33.3%') s.widthRatio = '33.33%';
|
||||
else if (s.widthRatio === '66.6%' || s.widthRatio === '66.7%') s.widthRatio = '66.67%';
|
||||
else if (!['100%', '66.67%', '50%', '33.33%'].includes(s.widthRatio)) {
|
||||
s.widthRatio = '100%';
|
||||
}
|
||||
}
|
||||
if (s.backgroundColor === undefined) {
|
||||
s.backgroundColor = '';
|
||||
} else if (s.backgroundColor && /^rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*0\s*\)$/.test(s.backgroundColor.trim())) {
|
||||
s.backgroundColor = s.backgroundColor.trim().replace(/,\s*0\s*\)$/, ', 1)');
|
||||
}
|
||||
return s;
|
||||
});
|
||||
|
||||
const predefineColors = [
|
||||
'#ffffff',
|
||||
'#f8fafc',
|
||||
'#f1f5f9',
|
||||
'#f3f4f6',
|
||||
'#fee2e2',
|
||||
'#ffedd5',
|
||||
'#fef3c7',
|
||||
'#e0e7ff',
|
||||
'#1e293b',
|
||||
'#0f172a'
|
||||
];
|
||||
|
||||
function handleColorChange(val) {
|
||||
if (!val) return;
|
||||
if (/^rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*0\s*\)$/.test(val)) {
|
||||
compStyle.value.backgroundColor = val.replace(/,\s*0\s*\)$/, ', 1)');
|
||||
}
|
||||
}
|
||||
|
||||
function handleColorActiveChange(val) {
|
||||
if (!val) return;
|
||||
if (/^rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*0\s*\)$/.test(val)) {
|
||||
compStyle.value.backgroundColor = val.replace(/,\s*0\s*\)$/, ', 1)');
|
||||
}
|
||||
}
|
||||
|
||||
function handleInputBlur() {
|
||||
const val = compStyle.value.backgroundColor;
|
||||
if (!val) return;
|
||||
if (/^rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*0\s*\)$/.test(val.trim())) {
|
||||
compStyle.value.backgroundColor = val.trim().replace(/,\s*0\s*\)$/, ', 1)');
|
||||
}
|
||||
}
|
||||
|
||||
const openImagePicker = inject('openImagePicker', null);
|
||||
|
||||
function handleSelectImage() {
|
||||
if (openImagePicker) {
|
||||
openImagePicker((url) => {
|
||||
if (url) {
|
||||
compStyle.value.backgroundImage = url;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入背景图片 URL 地址:', '选择背景图片', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: compStyle.value.backgroundImage || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '请输入有效的图片 URL 地址'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
compStyle.value.backgroundImage = value;
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
function handleClearImage() {
|
||||
compStyle.value.backgroundImage = '';
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.component-config-panel {
|
||||
padding: 12px;
|
||||
box-sizing: border-box;
|
||||
|
||||
.setting-collapse-card {
|
||||
background: #ffffff;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
overflow: hidden;
|
||||
|
||||
.collapse-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 14px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
background: #fafafa;
|
||||
border-bottom: 1px solid transparent;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.arrow-icon {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
transition: transform 0.2s;
|
||||
|
||||
&.is-expanded {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.help-icon {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.collapse-body {
|
||||
padding: 14px;
|
||||
background: #ffffff;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
&.mt-12 {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.row-label {
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
.help-icon {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.full-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.color-picker-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.color-input {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.bg-img-btns {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
height: 32px;
|
||||
|
||||
&.secondary {
|
||||
background: #fafafa;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.box-model-section {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.tip-spec-box {
|
||||
background: #2b2e3b;
|
||||
color: #f1f5f9;
|
||||
padding: 8px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 10px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.bg-preview-row {
|
||||
margin-top: 10px;
|
||||
padding: 6px;
|
||||
border: 1px dashed #dcdfe6;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
|
||||
.bg-preview-img {
|
||||
max-width: 100%;
|
||||
max-height: 80px;
|
||||
object-fit: contain;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-panel {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
459
seller/src/views/shop/pc/config-panel/PanelBrandWall.vue
Normal file
459
seller/src/views/shop/pc/config-panel/PanelBrandWall.vue
Normal file
@@ -0,0 +1,459 @@
|
||||
<template>
|
||||
<div class="panel-brand-wall">
|
||||
<div class="panel-section">
|
||||
<div class="section-title">
|
||||
<div class="title-with-tip">
|
||||
<span>内容设置</span>
|
||||
<el-tooltip
|
||||
content="设置品牌图片列表、跳转链接与展示排版"
|
||||
placement="top"
|
||||
>
|
||||
<el-icon class="help-icon"><QuestionFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sub-section-title">图片列表</div>
|
||||
|
||||
<!-- 图片列表卡片 -->
|
||||
<div class="brand-cards-list">
|
||||
<div
|
||||
v-for="(item, idx) in options.items"
|
||||
:key="idx"
|
||||
class="brand-item-card"
|
||||
>
|
||||
<!-- 第一行:重新选择 + 缩略图预览 -->
|
||||
<div class="card-row-top">
|
||||
<el-button
|
||||
size="small"
|
||||
class="reselect-btn"
|
||||
@click="handleSelectImage(item)"
|
||||
>
|
||||
{{ item.img ? '重新选择' : '选择图片' }}
|
||||
</el-button>
|
||||
|
||||
<!-- 右侧图片缩略图 -->
|
||||
<div
|
||||
class="thumb-preview-box"
|
||||
@click="handleSelectImage(item)"
|
||||
:title="item.img ? '点击更换图片' : '点击上传图片'"
|
||||
>
|
||||
<template v-if="item.img">
|
||||
<img :src="item.img" alt="brand" class="thumb-img" />
|
||||
<span class="thumb-del-badge" @click.stop="item.img = ''" title="移除图片">
|
||||
<el-icon><Close /></el-icon>
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-icon class="upload-icon"><Plus /></el-icon>
|
||||
<span class="upload-text">上传图片</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第二行:图片标题 + 工具按钮组 -->
|
||||
<div class="card-row-bottom">
|
||||
<el-input
|
||||
v-model="item.title"
|
||||
size="small"
|
||||
placeholder="图片标题,可选"
|
||||
class="title-input"
|
||||
/>
|
||||
<div class="action-btn-group">
|
||||
<el-button
|
||||
size="small"
|
||||
class="action-btn"
|
||||
:class="{ 'is-linked': !!item.url }"
|
||||
@click="handleSetLink(item)"
|
||||
@contextmenu.prevent="item.url = ''"
|
||||
:title="item.url ? (`已设链接: ${item.url} (点击修改,右键清除)`) : '设置跳转链接'"
|
||||
>
|
||||
<el-icon><Link /></el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
class="action-btn"
|
||||
:disabled="idx === 0"
|
||||
@click="moveItem(idx, -1)"
|
||||
title="上移"
|
||||
>
|
||||
<el-icon><Top /></el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
class="action-btn"
|
||||
:disabled="idx === (options.items || []).length - 1"
|
||||
@click="moveItem(idx, 1)"
|
||||
title="下移"
|
||||
>
|
||||
<el-icon><Bottom /></el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
class="action-btn del-btn"
|
||||
@click="options.items.splice(idx, 1)"
|
||||
title="删除图片"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加图片大按钮 -->
|
||||
<el-button
|
||||
class="add-image-full-btn"
|
||||
@click="handleAddImage"
|
||||
>
|
||||
<el-icon><Plus /></el-icon> 添加图片
|
||||
</el-button>
|
||||
|
||||
<!-- 排版间距与列数 -->
|
||||
<div class="form-row mt-16">
|
||||
<span class="row-label">单行列数</span>
|
||||
<el-radio-group v-model="options.columns" size="small">
|
||||
<el-radio-button :label="3">3 列</el-radio-button>
|
||||
<el-radio-button :label="4">4 列</el-radio-button>
|
||||
<el-radio-button :label="5">5 列</el-radio-button>
|
||||
<el-radio-button :label="6">6 列</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
|
||||
<div class="form-row mt-12">
|
||||
<span class="row-label">卡片间距(px)</span>
|
||||
<el-input-number
|
||||
v-model="options.gap"
|
||||
:min="0"
|
||||
:max="40"
|
||||
:step="2"
|
||||
size="small"
|
||||
class="num-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject } from 'vue';
|
||||
import { Plus, Delete, Close, Link, Top, Bottom, QuestionFilled } from '@element-plus/icons-vue';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化默认字段
|
||||
if (!props.options.columns) props.options.columns = 4;
|
||||
if (props.options.gap === undefined) props.options.gap = 12;
|
||||
if (!Array.isArray(props.options.items)) {
|
||||
props.options.items = [
|
||||
{ title: '双层收纳 9.9RMB', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/brand-wall/brand_1.png', url: '/goodsList?brand=1' },
|
||||
{ title: '双层收纳 29.9', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/brand-wall/brand_2.png', url: '/goodsList?brand=2' },
|
||||
{ title: '双层收纳 29.9', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/brand-wall/brand_3.png', url: '/goodsList?brand=3' },
|
||||
{ title: '双层收纳 29.9', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/brand-wall/brand_4.png', url: '/goodsList?brand=4' }
|
||||
];
|
||||
}
|
||||
|
||||
const openImagePicker = inject('openImagePicker', null);
|
||||
const openLinkPicker = inject('openLinkPicker', null);
|
||||
|
||||
function handleSelectImage(item) {
|
||||
if (openImagePicker) {
|
||||
openImagePicker((url) => {
|
||||
if (url) item.img = url;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入图片 URL 地址:', '上传品牌图', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: item.img || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '图片地址不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
item.img = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
function handleSetLink(item) {
|
||||
if (openLinkPicker) {
|
||||
openLinkPicker(({ url, title }) => {
|
||||
item.url = url;
|
||||
if (!item.title && title) item.title = title;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入跳转链接(如 /goodsList):', '设置跳转链接', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: item.url || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '链接不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
item.url = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
function moveItem(idx, dir) {
|
||||
const list = props.options.items;
|
||||
const target = idx + dir;
|
||||
if (target < 0 || target >= list.length) return;
|
||||
const temp = list[idx];
|
||||
list[idx] = list[target];
|
||||
list[target] = temp;
|
||||
}
|
||||
|
||||
function handleAddImage() {
|
||||
const nextNum = (props.options.items?.length || 0) + 1;
|
||||
const iconIdx = ((nextNum - 1) % 4) + 1;
|
||||
props.options.items.push({
|
||||
title: `品牌图 ${nextNum}`,
|
||||
img: `https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/brand-wall/brand_${iconIdx}.png`,
|
||||
url: '/goodsList'
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.panel-brand-wall {
|
||||
font-size: 13px;
|
||||
color: #334155;
|
||||
|
||||
.panel-section {
|
||||
padding-bottom: 20px;
|
||||
|
||||
.section-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.title-with-tip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.help-icon {
|
||||
font-size: 14px;
|
||||
color: #94a3b8;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #475569;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sub-section-title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #475569;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.brand-cards-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.brand-item-card {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
transition: border-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #cbd5e1;
|
||||
}
|
||||
|
||||
.card-row-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
.reselect-btn {
|
||||
flex: 1;
|
||||
height: 38px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #1e293b;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #f97316;
|
||||
color: #ea580c;
|
||||
}
|
||||
}
|
||||
|
||||
.thumb-preview-box {
|
||||
flex: 1;
|
||||
height: 38px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #f97316;
|
||||
}
|
||||
|
||||
.thumb-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.upload-icon {
|
||||
font-size: 13px;
|
||||
color: #94a3b8;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.thumb-del-badge {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: rgba(15, 23, 42, 0.7);
|
||||
color: #ffffff;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: #ef4444;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-row-bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.title-input {
|
||||
flex: 1;
|
||||
|
||||
:deep(.el-input__wrapper) {
|
||||
background: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
.action-btn-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
.action-btn {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e2e8f0;
|
||||
background: #ffffff;
|
||||
color: #64748b;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
border-color: #cbd5e1;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
&.is-linked {
|
||||
border-color: #fdba74;
|
||||
background: #fff7ed;
|
||||
color: #ea580c;
|
||||
}
|
||||
|
||||
&.del-btn:hover {
|
||||
border-color: #fecaca;
|
||||
background: #fef2f2;
|
||||
color: #ef4444;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add-image-full-btn {
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
background: #ffffff;
|
||||
color: #334155;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #f97316;
|
||||
color: #ea580c;
|
||||
background: #fffaf5;
|
||||
}
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
&.mt-16 {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
&.mt-12 {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.row-label {
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.num-input {
|
||||
width: 120px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
529
seller/src/views/shop/pc/config-panel/PanelCarousel.vue
Normal file
529
seller/src/views/shop/pc/config-panel/PanelCarousel.vue
Normal file
@@ -0,0 +1,529 @@
|
||||
<template>
|
||||
<div class="panel-carousel">
|
||||
<!-- 内容设置 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">
|
||||
<div class="title-with-tip">
|
||||
<span>内容设置</span>
|
||||
<el-tooltip
|
||||
content="设置轮播播放参数及切换动效"
|
||||
placement="top"
|
||||
>
|
||||
<el-icon class="help-icon"><QuestionFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 1. 播放轮播 -->
|
||||
<div class="form-row flex-between">
|
||||
<span class="row-label">播放轮播</span>
|
||||
<el-switch v-model="options.autoPlay" size="small" />
|
||||
</div>
|
||||
|
||||
<!-- 2. 轮播间隔(ms) -->
|
||||
<div class="form-row flex-col mt-12">
|
||||
<span class="row-label">轮播间隔(ms)</span>
|
||||
<el-input-number
|
||||
v-model="options.interval"
|
||||
:min="1000"
|
||||
:max="15000"
|
||||
:step="500"
|
||||
size="small"
|
||||
controls-position="right"
|
||||
class="full-num-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 3. 显示左右切换按钮 -->
|
||||
<div class="form-row flex-between mt-12">
|
||||
<span class="row-label">显示左右切换按钮</span>
|
||||
<el-switch v-model="options.showArrow" size="small" />
|
||||
</div>
|
||||
|
||||
<!-- 4. 切换动效 -->
|
||||
<div class="form-row flex-col mt-12">
|
||||
<span class="row-label">切换动效</span>
|
||||
<el-select v-model="options.effect" size="small" class="full-select">
|
||||
<el-option label="淡入淡出" value="fade" />
|
||||
<el-option label="平滑滚动" value="slide" />
|
||||
<el-option label="卡片式" value="card" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<!-- 5. 图片缩放模式 -->
|
||||
<div class="form-row flex-col mt-12">
|
||||
<span class="row-label">图片缩放模式</span>
|
||||
<el-select v-model="options.fitMode" size="small" class="full-select">
|
||||
<el-option label="等比完整展示 (全画面不裁切)" value="contain" />
|
||||
<el-option label="等比填满裁剪 (铺满容器)" value="cover" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<!-- 6. 高宽比例 -->
|
||||
<div class="form-row flex-col mt-12">
|
||||
<span class="row-label">高宽比例 (随排版宽度等比缩放)</span>
|
||||
<el-select v-model="options.aspectRatio" size="small" class="full-select">
|
||||
<el-option label="2.5 : 1 (标准电商海报 推荐)" value="2.5:1" />
|
||||
<el-option label="16 : 9 (影音宽屏海报)" value="16:9" />
|
||||
<el-option label="2 : 1 (大促通栏横幅)" value="2:1" />
|
||||
<el-option label="3 : 1 (超宽细长条海报)" value="3:1" />
|
||||
<el-option label="固定高度模式 (不随宽度等比缩放)" value="custom" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<!-- 固定高度输入 (仅在 custom 模式下显示) -->
|
||||
<div class="form-row flex-col mt-12" v-if="options.aspectRatio === 'custom'">
|
||||
<span class="row-label">固定高度 (px)</span>
|
||||
<el-input-number
|
||||
v-model="options.height"
|
||||
:min="80"
|
||||
:max="800"
|
||||
:step="10"
|
||||
size="small"
|
||||
controls-position="right"
|
||||
class="full-num-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 7. 显示底部指示点 -->
|
||||
<div class="form-row flex-between mt-12">
|
||||
<span class="row-label">显示底部指示点</span>
|
||||
<el-switch v-model="options.showIndicator" size="small" />
|
||||
</div>
|
||||
|
||||
<!-- 6. 轮播图列表卡片 -->
|
||||
<div class="sub-section-title mt-16">轮播图</div>
|
||||
|
||||
<div class="slides-cards-list">
|
||||
<div
|
||||
v-for="(slide, idx) in options.list"
|
||||
:key="idx"
|
||||
class="slide-item-card"
|
||||
>
|
||||
<!-- 第一行:左侧重新选择按钮,右侧图片预览 -->
|
||||
<div class="card-row-top">
|
||||
<el-button
|
||||
size="small"
|
||||
class="reselect-btn"
|
||||
@click="handleSelectImage(slide)"
|
||||
>
|
||||
{{ slide.img ? '重新选择' : '选择图片' }}
|
||||
</el-button>
|
||||
|
||||
<!-- 右侧图片缩略图 -->
|
||||
<div
|
||||
class="slide-thumb-box"
|
||||
@click="handleSelectImage(slide)"
|
||||
:title="slide.img ? '点击更换海报图片' : '点击上传海报图片'"
|
||||
>
|
||||
<template v-if="slide.img">
|
||||
<img :src="slide.img" alt="slide" class="thumb-img" />
|
||||
<span class="thumb-del-badge" @click.stop="slide.img = ''" title="移除图片">
|
||||
<el-icon><Close /></el-icon>
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-icon class="upload-icon"><Plus /></el-icon>
|
||||
<span class="upload-text">上传海报</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第二行:左侧标题输入,右侧操作工具栏 -->
|
||||
<div class="card-row-bottom">
|
||||
<el-input
|
||||
v-model="slide.title"
|
||||
size="small"
|
||||
placeholder="图片标题,可选"
|
||||
class="title-input"
|
||||
/>
|
||||
<div class="action-btn-group">
|
||||
<el-button
|
||||
size="small"
|
||||
class="action-btn"
|
||||
:class="{ 'is-linked': !!slide.url }"
|
||||
@click="handleSetLink(slide)"
|
||||
@contextmenu.prevent="slide.url = ''"
|
||||
:title="slide.url ? (`已设链接: ${slide.url} (点击修改,右键清除)`) : '设置跳转链接'"
|
||||
>
|
||||
<el-icon><Link /></el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
class="action-btn"
|
||||
:disabled="idx === 0"
|
||||
@click="moveSlide(idx, -1)"
|
||||
title="上移"
|
||||
>
|
||||
<el-icon><Top /></el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
class="action-btn"
|
||||
:disabled="idx === (options.list || []).length - 1"
|
||||
@click="moveSlide(idx, 1)"
|
||||
title="下移"
|
||||
>
|
||||
<el-icon><Bottom /></el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
class="action-btn del-btn"
|
||||
@click="options.list.splice(idx, 1)"
|
||||
title="删除海报"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加图片大按钮 -->
|
||||
<el-button
|
||||
class="add-slide-full-btn"
|
||||
@click="handleAddSlide"
|
||||
>
|
||||
<el-icon><Plus /></el-icon> 添加图片
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject } from 'vue';
|
||||
import { Plus, Delete, Close, Link, Top, Bottom, QuestionFilled } from '@element-plus/icons-vue';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化默认字段
|
||||
if (props.options.autoPlay === undefined) props.options.autoPlay = true;
|
||||
if (props.options.interval === undefined) props.options.interval = 4000;
|
||||
if (props.options.showArrow === undefined) props.options.showArrow = true;
|
||||
if (props.options.showIndicator === undefined) props.options.showIndicator = true;
|
||||
if (!props.options.effect) props.options.effect = 'fade';
|
||||
if (!props.options.fitMode) props.options.fitMode = 'contain';
|
||||
if (!props.options.aspectRatio) props.options.aspectRatio = '2.5:1';
|
||||
if (props.options.height === undefined) props.options.height = 420;
|
||||
if (!Array.isArray(props.options.list)) {
|
||||
props.options.list = [
|
||||
{ title: '出游囤好货', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/carousel/banner_1.png', url: '/goodsList?seckill=1' },
|
||||
{ title: '科技新品', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/carousel/banner_2.png', url: '/goodsList?category=1' },
|
||||
{ title: '原产地直采', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/carousel/banner_3.png', url: '/goodsList?category=2' },
|
||||
{ title: '大牌美妆', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/carousel/banner_4.png', url: '/goodsList?category=3' }
|
||||
];
|
||||
}
|
||||
|
||||
// 注入全局选择器
|
||||
const openImagePicker = inject('openImagePicker', null);
|
||||
const openLinkPicker = inject('openLinkPicker', null);
|
||||
|
||||
function handleSelectImage(slide) {
|
||||
if (openImagePicker) {
|
||||
openImagePicker((url) => {
|
||||
if (url) slide.img = url;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入海报图片 URL 地址:', '上传轮播图', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: slide.img || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '图片地址不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
slide.img = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
function handleSetLink(slide) {
|
||||
if (openLinkPicker) {
|
||||
openLinkPicker(({ url, title }) => {
|
||||
slide.url = url;
|
||||
if (!slide.title && title) slide.title = title;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入跳转链接(如 /goodsList):', '设置跳转链接', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: slide.url || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '链接不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
slide.url = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
function moveSlide(idx, dir) {
|
||||
const list = props.options.list;
|
||||
const target = idx + dir;
|
||||
if (target < 0 || target >= list.length) return;
|
||||
const temp = list[idx];
|
||||
list[idx] = list[target];
|
||||
list[target] = temp;
|
||||
}
|
||||
|
||||
function handleAddSlide() {
|
||||
const nextNum = (props.options.list?.length || 0) + 1;
|
||||
const iconIdx = ((nextNum - 1) % 4) + 1;
|
||||
props.options.list.push({
|
||||
title: `轮播图 ${nextNum}`,
|
||||
img: `https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/carousel/banner_${iconIdx}.png`,
|
||||
url: '/goodsList'
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.panel-carousel {
|
||||
font-size: 13px;
|
||||
color: #334155;
|
||||
|
||||
.panel-section {
|
||||
padding-bottom: 20px;
|
||||
|
||||
.section-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.title-with-tip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.help-icon {
|
||||
font-size: 14px;
|
||||
color: #94a3b8;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #475569;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sub-section-title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #475569;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
|
||||
&.flex-between {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&.flex-col {
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
&.mt-12 {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
&.mt-16 {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.row-label {
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.full-num-input, .full-select {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.slides-cards-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.slide-item-card {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
transition: border-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #cbd5e1;
|
||||
}
|
||||
|
||||
.card-row-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
.reselect-btn {
|
||||
flex: 1;
|
||||
height: 38px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #1e293b;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #f97316;
|
||||
color: #ea580c;
|
||||
}
|
||||
}
|
||||
|
||||
.slide-thumb-box {
|
||||
flex: 1;
|
||||
height: 38px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #f97316;
|
||||
}
|
||||
|
||||
.thumb-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.upload-icon {
|
||||
font-size: 13px;
|
||||
color: #94a3b8;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.thumb-del-badge {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: rgba(15, 23, 42, 0.7);
|
||||
color: #ffffff;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: #ef4444;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-row-bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.title-input {
|
||||
flex: 1;
|
||||
|
||||
:deep(.el-input__wrapper) {
|
||||
background: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
.action-btn-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
.action-btn {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e2e8f0;
|
||||
background: #ffffff;
|
||||
color: #64748b;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
border-color: #cbd5e1;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
&.is-linked {
|
||||
border-color: #fdba74;
|
||||
background: #fff7ed;
|
||||
color: #ea580c;
|
||||
}
|
||||
|
||||
&.del-btn:hover {
|
||||
border-color: #fecaca;
|
||||
background: #fef2f2;
|
||||
color: #ef4444;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add-slide-full-btn {
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
background: #ffffff;
|
||||
color: #334155;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #f97316;
|
||||
color: #ea580c;
|
||||
background: #fffaf5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
394
seller/src/views/shop/pc/config-panel/PanelCategoryMegaMenu.vue
Normal file
394
seller/src/views/shop/pc/config-panel/PanelCategoryMegaMenu.vue
Normal file
@@ -0,0 +1,394 @@
|
||||
<template>
|
||||
<div class="panel-category-mega-menu">
|
||||
<!-- 1. 内容设置 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">
|
||||
<div class="title-with-tip">
|
||||
<span>内容设置</span>
|
||||
<el-tooltip
|
||||
content="设置商品分类的文字颜色及显示字号"
|
||||
placement="top"
|
||||
>
|
||||
<el-icon class="help-icon"><QuestionFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文字颜色 -->
|
||||
<div class="form-row flex-col">
|
||||
<span class="row-label">文字颜色</span>
|
||||
<div class="color-picker-input-group">
|
||||
<el-color-picker
|
||||
v-model="options.textColor"
|
||||
size="small"
|
||||
class="color-picker"
|
||||
/>
|
||||
<el-input
|
||||
v-model="options.textColor"
|
||||
size="small"
|
||||
placeholder="#111927"
|
||||
class="color-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 字号 -->
|
||||
<div class="form-row flex-col mt-16">
|
||||
<span class="row-label">字号</span>
|
||||
<el-input-number
|
||||
v-model="options.fontSize"
|
||||
:min="12"
|
||||
:max="24"
|
||||
:step="1"
|
||||
size="small"
|
||||
controls-position="right"
|
||||
class="font-size-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 标题文案 -->
|
||||
<div class="form-row flex-col mt-16">
|
||||
<span class="row-label">标题文案</span>
|
||||
<el-input
|
||||
v-model="options.title"
|
||||
size="small"
|
||||
placeholder="分类"
|
||||
class="full-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 悬浮高亮色 -->
|
||||
<div class="form-row flex-col mt-16">
|
||||
<span class="row-label">悬浮高亮色</span>
|
||||
<div class="color-picker-input-group">
|
||||
<el-color-picker
|
||||
v-model="options.activeColor"
|
||||
size="small"
|
||||
class="color-picker"
|
||||
/>
|
||||
<el-input
|
||||
v-model="options.activeColor"
|
||||
size="small"
|
||||
placeholder="#409EFF"
|
||||
class="color-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 数据来源 -->
|
||||
<div class="form-row flex-col mt-16">
|
||||
<span class="row-label">数据来源</span>
|
||||
<el-radio-group v-model="options.source" size="small">
|
||||
<el-radio-button label="system">系统分类 (自动)</el-radio-button>
|
||||
<el-radio-button label="custom">手动自定义</el-radio-button>
|
||||
</el-radio-group>
|
||||
<span class="form-tip mt-6" v-if="options.source !== 'custom'">
|
||||
已启用系统商品分类,前后台自动拉取商城最新分类及二级、三级悬浮大菜单。
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 分类项列表 (仅自定义模式可见) -->
|
||||
<div class="categories-sub-section mt-20" v-if="options.source === 'custom'">
|
||||
<div class="sub-header flex-between">
|
||||
<span class="sub-title">分类条目</span>
|
||||
<span class="sub-count">共 {{ (options.categories || []).length }} 项</span>
|
||||
</div>
|
||||
|
||||
<div class="category-cards-list">
|
||||
<div
|
||||
v-for="(cat, idx) in options.categories"
|
||||
:key="idx"
|
||||
class="cat-item-card"
|
||||
>
|
||||
<div class="cat-card-main">
|
||||
<el-input
|
||||
v-model="cat.name"
|
||||
size="small"
|
||||
placeholder="分类名称"
|
||||
class="cat-name-input"
|
||||
/>
|
||||
<div class="cat-card-actions">
|
||||
<el-button
|
||||
size="small"
|
||||
class="action-btn"
|
||||
:disabled="idx === 0"
|
||||
@click="moveCat(idx, -1)"
|
||||
title="上移"
|
||||
>
|
||||
<el-icon><Top /></el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
class="action-btn"
|
||||
:disabled="idx === (options.categories || []).length - 1"
|
||||
@click="moveCat(idx, 1)"
|
||||
title="下移"
|
||||
>
|
||||
<el-icon><Bottom /></el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
class="action-btn del-btn"
|
||||
@click="options.categories.splice(idx, 1)"
|
||||
title="删除分类"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-button
|
||||
class="add-cat-btn"
|
||||
@click="handleAddCategory"
|
||||
>
|
||||
<el-icon><Plus /></el-icon> 添加分类条目
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { QuestionFilled, Plus, Delete, Top, Bottom } from '@element-plus/icons-vue';
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化默认字段
|
||||
if (!props.options.source) props.options.source = 'system';
|
||||
if (!props.options.title) props.options.title = '分类';
|
||||
if (!props.options.textColor) props.options.textColor = '#111927';
|
||||
if (!props.options.fontSize) props.options.fontSize = 14;
|
||||
if (!props.options.activeColor) props.options.activeColor = '#ea580c';
|
||||
if (!Array.isArray(props.options.categories)) {
|
||||
props.options.categories = [
|
||||
{
|
||||
name: '精选频道',
|
||||
subCategories: [
|
||||
{ name: '每日精选', items: ['新品尝鲜', '大牌特惠', '超级秒杀', '今日热卖'] },
|
||||
{ name: '特色专区', items: ['企业团购', '会员专享', '积分兑换', '清仓特卖'] }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: '新品上架',
|
||||
subCategories: [
|
||||
{ name: '本周新发', items: ['数码新品', '秋季服饰', '美妆尝新', '家居收纳'] },
|
||||
{ name: '潮流榜单', items: ['人气推荐', '编辑精选', '预售抢先'] }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: '企业采购',
|
||||
subCategories: [
|
||||
{ name: '办公采购', items: ['办公用纸', '文具收纳', '商用打印', '会议茶水'] },
|
||||
{ name: '员工福利', items: ['节日礼盒', '数码卡券', '健康体检', '员工关怀'] }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: '领券中心',
|
||||
subCategories: [
|
||||
{ name: '平台券', items: ['全场满减', '品类专享', '运费券', '会员特权'] },
|
||||
{ name: '大牌券', items: ['数码大额券', '生鲜补贴', '美妆立减', '母婴满减'] }
|
||||
]
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
function handleAddCategory() {
|
||||
const nextNum = props.options.categories.length + 1;
|
||||
props.options.categories.push({
|
||||
name: `新分类 ${nextNum}`,
|
||||
subCategories: [
|
||||
{ name: '热门分类', items: ['精选推荐', '热销爆款', '品质优选'] }
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
function moveCat(idx, dir) {
|
||||
const list = props.options.categories;
|
||||
const target = idx + dir;
|
||||
if (target < 0 || target >= list.length) return;
|
||||
const temp = list[idx];
|
||||
list[idx] = list[target];
|
||||
list[target] = temp;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.panel-category-mega-menu {
|
||||
font-size: 13px;
|
||||
color: #334155;
|
||||
|
||||
.panel-section {
|
||||
padding-bottom: 20px;
|
||||
|
||||
.section-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
margin-bottom: 14px;
|
||||
|
||||
.title-with-tip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.help-icon {
|
||||
font-size: 14px;
|
||||
color: #94a3b8;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #475569;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
|
||||
&.flex-col {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&.mt-16 {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
&.mt-20 {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.row-label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.color-picker-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.color-picker {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.color-input {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.font-size-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.full-input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.categories-sub-section {
|
||||
border-top: 1px dashed #e2e8f0;
|
||||
padding-top: 14px;
|
||||
|
||||
.sub-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.sub-title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.sub-count {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
|
||||
.category-cards-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.cat-item-card {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
padding: 6px 8px;
|
||||
|
||||
.cat-card-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.cat-name-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.cat-card-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
.action-btn {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
padding: 0;
|
||||
border: 1px solid #e2e8f0;
|
||||
background: #ffffff;
|
||||
color: #64748b;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
border-color: #cbd5e1;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
&.del-btn:hover {
|
||||
border-color: #fecaca;
|
||||
background: #fef2f2;
|
||||
color: #ef4444;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add-cat-btn {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
border: 1px dashed #cbd5e1;
|
||||
border-radius: 6px;
|
||||
background: #ffffff;
|
||||
color: #475569;
|
||||
font-size: 12px;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #ea580c;
|
||||
color: #ea580c;
|
||||
background: #fffaf5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
534
seller/src/views/shop/pc/config-panel/PanelCategoryNav.vue
Normal file
534
seller/src/views/shop/pc/config-panel/PanelCategoryNav.vue
Normal file
@@ -0,0 +1,534 @@
|
||||
<template>
|
||||
<div class="panel-category-nav">
|
||||
<!-- 1. 内容设置 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">内容设置</div>
|
||||
|
||||
<!-- 显示全部分类开关 -->
|
||||
<div class="form-row flex-between">
|
||||
<span class="row-label">显示全部分类</span>
|
||||
<el-switch v-model="options.showAllCategory" size="small" />
|
||||
</div>
|
||||
|
||||
<!-- 全部分类文案 -->
|
||||
<div class="form-row mt-12" v-if="options.showAllCategory !== false">
|
||||
<span class="row-label">全部分类文案</span>
|
||||
<el-input
|
||||
v-model="options.allCategoryText"
|
||||
size="small"
|
||||
placeholder="全部分类"
|
||||
class="flex-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 全部分类图标 -->
|
||||
<div class="form-row mt-12" v-if="options.showAllCategory !== false">
|
||||
<span class="row-label">全部分类图标</span>
|
||||
<div class="all-cat-icon-ctrl">
|
||||
<div class="icon-thumb-box" v-if="options.allCategoryIcon" @click="handleSelectAllCategoryIcon" title="更换图标">
|
||||
<img :src="options.allCategoryIcon" alt="全部分类图标" class="thumb-img" />
|
||||
<el-icon class="thumb-del" @click.stop="options.allCategoryIcon = ''" title="删除"><Close /></el-icon>
|
||||
</div>
|
||||
<el-button size="small" class="action-btn" @click="handleSelectAllCategoryIcon">
|
||||
{{ options.allCategoryIcon ? '修改图标' : '选择图标' }}
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
link
|
||||
type="danger"
|
||||
v-if="options.allCategoryIcon"
|
||||
@click="options.allCategoryIcon = ''"
|
||||
>
|
||||
清除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. 导航项列表 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title flex-between">
|
||||
<span>导航项</span>
|
||||
<span class="items-count">共 {{ (options.navItems || []).length }} 项</span>
|
||||
</div>
|
||||
|
||||
<div class="nav-cards-list">
|
||||
<div
|
||||
v-for="(item, idx) in options.navItems"
|
||||
:key="idx"
|
||||
class="nav-item-card"
|
||||
>
|
||||
<!-- 卡片上半部分:左侧选择/标题,右侧图标缩略图 -->
|
||||
<div class="card-body">
|
||||
<div class="left-col">
|
||||
<el-button
|
||||
size="small"
|
||||
class="reselect-btn"
|
||||
@click="handleSelectCategory(item)"
|
||||
>
|
||||
{{ item.title ? '重新选择' : '选择分类' }}
|
||||
</el-button>
|
||||
<el-input
|
||||
v-model="item.title"
|
||||
size="small"
|
||||
placeholder="分类名称"
|
||||
class="title-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 右侧图标上传框 -->
|
||||
<div
|
||||
class="right-thumb-col"
|
||||
@click="handleSelectIcon(item)"
|
||||
:title="item.img ? '点击更换分类图标' : '点击上传分类图标'"
|
||||
>
|
||||
<template v-if="item.img">
|
||||
<img :src="item.img" alt="分类图标" class="cat-thumb" />
|
||||
<span class="thumb-del-badge" @click.stop="item.img = ''" title="移除图标">
|
||||
<el-icon><Close /></el-icon>
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-icon class="upload-plus"><Plus /></el-icon>
|
||||
<span class="upload-tip">图标</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 卡片底栏:操作按钮组 (链接、上移、下移、删除) -->
|
||||
<div class="card-actions">
|
||||
<el-button
|
||||
size="small"
|
||||
class="action-btn"
|
||||
:type="item.url ? 'primary' : 'default'"
|
||||
:plain="!item.url"
|
||||
@click="handleSetLink(item)"
|
||||
@contextmenu.prevent="item.url = ''"
|
||||
:title="item.url ? (`已设链接: ${item.url} (点击修改,右键清除)`) : '设置跳转链接'"
|
||||
>
|
||||
<el-icon><Link /></el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
class="action-btn"
|
||||
:disabled="idx === 0"
|
||||
@click="moveItem(idx, -1)"
|
||||
title="上移"
|
||||
>
|
||||
<el-icon><Top /></el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
class="action-btn"
|
||||
:disabled="idx === options.navItems.length - 1"
|
||||
@click="moveItem(idx, 1)"
|
||||
title="下移"
|
||||
>
|
||||
<el-icon><Bottom /></el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
class="action-btn del-btn"
|
||||
@click="options.navItems.splice(idx, 1)"
|
||||
title="删除导航项"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加导航项大按钮 -->
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
class="add-item-full-btn"
|
||||
@click="handleAddItem"
|
||||
>
|
||||
<el-icon><Plus /></el-icon> 添加导航项
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 3. 文字与悬浮颜色设置 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">文字与悬浮颜色</div>
|
||||
<div class="form-row">
|
||||
<span class="row-label">常态文字颜色</span>
|
||||
<el-color-picker v-model="options.textColor" size="small" />
|
||||
</div>
|
||||
<div class="form-row mt-12">
|
||||
<span class="row-label">悬浮/选中高亮色</span>
|
||||
<el-color-picker v-model="options.activeColor" size="small" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject } from 'vue';
|
||||
import { Plus, Delete, Close, Link, Top, Bottom } from '@element-plus/icons-vue';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
// 注入全局选择器
|
||||
const openLinkPicker = inject('openLinkPicker', null);
|
||||
const openImagePicker = inject('openImagePicker', null);
|
||||
|
||||
// 1. 重新选择分类
|
||||
function handleSelectCategory(item) {
|
||||
if (openLinkPicker) {
|
||||
openLinkPicker(({ url, title, raw }) => {
|
||||
if (title) item.title = title;
|
||||
if (url) item.url = url;
|
||||
if (raw && (raw.image || raw.icon || raw.pic)) {
|
||||
item.img = raw.image || raw.icon || raw.pic;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入分类名称:', '设置分类', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: item.title || '',
|
||||
inputPattern: /\S+/,
|
||||
inputErrorMessage: '分类名称不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
item.title = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 选择分类图标图片
|
||||
function handleSelectIcon(item) {
|
||||
if (openImagePicker) {
|
||||
openImagePicker((url) => {
|
||||
if (url) item.img = url;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入图标图片 URL 地址:', '上传图标', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: item.img || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '图片地址不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
item.img = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 选择全部分类图标
|
||||
function handleSelectAllCategoryIcon() {
|
||||
if (openImagePicker) {
|
||||
openImagePicker((url) => {
|
||||
if (url) props.options.allCategoryIcon = url;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入图标图片 URL 地址:', '设置全部分类图标', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: props.options.allCategoryIcon || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '图片地址不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
props.options.allCategoryIcon = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 设置链接
|
||||
function handleSetLink(item) {
|
||||
if (openLinkPicker) {
|
||||
openLinkPicker(({ url, title }) => {
|
||||
item.url = url;
|
||||
if (!item.title && title) item.title = title;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入跳转链接路径(留空清除链接):', `设置 [${item.title || '导航项'}] 链接`, {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: item.url || '',
|
||||
inputPattern: /^.*$/
|
||||
})
|
||||
.then(({ value }) => {
|
||||
item.url = (value || '').trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 上移/下移
|
||||
function moveItem(idx, direction) {
|
||||
const target = idx + direction;
|
||||
if (!Array.isArray(props.options.navItems)) return;
|
||||
if (target < 0 || target >= props.options.navItems.length) return;
|
||||
const moved = props.options.navItems.splice(idx, 1)[0];
|
||||
props.options.navItems.splice(target, 0, moved);
|
||||
}
|
||||
|
||||
// 6. 添加导航项
|
||||
function handleAddItem() {
|
||||
if (!Array.isArray(props.options.navItems)) props.options.navItems = [];
|
||||
const count = props.options.navItems.length + 1;
|
||||
props.options.navItems.push({
|
||||
title: `新分类${count}`,
|
||||
img: '',
|
||||
url: `/goodsList?categoryId=${count}`,
|
||||
badge: ''
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.panel-category-nav {
|
||||
padding: 12px;
|
||||
box-sizing: border-box;
|
||||
|
||||
.panel-section {
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px dashed #e2e8f0;
|
||||
|
||||
.section-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
margin-bottom: 12px;
|
||||
|
||||
&.flex-between {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.items-count {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
|
||||
&.flex-between {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&.mt-12 {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.row-label {
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.flex-1 {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.all-cat-icon-ctrl {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.icon-thumb-box {
|
||||
position: relative;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e2e8f0;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
|
||||
.thumb-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.thumb-del {
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
right: 1px;
|
||||
font-size: 10px;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
color: #fff;
|
||||
border-radius: 50%;
|
||||
padding: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nav-cards-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.nav-item-card {
|
||||
background: #ffffff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
padding: 10px 12px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #cbd5e1;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.card-body {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.left-col {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
|
||||
.reselect-btn {
|
||||
align-self: flex-start;
|
||||
font-size: 12px;
|
||||
color: #334155;
|
||||
border-color: #cbd5e1;
|
||||
padding: 0 10px;
|
||||
height: 26px;
|
||||
|
||||
&:hover {
|
||||
color: #2563eb;
|
||||
border-color: #93c5fd;
|
||||
}
|
||||
}
|
||||
|
||||
.title-input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.right-thumb-col {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 6px;
|
||||
border: 1px dashed #cbd5e1;
|
||||
background: #f8fafc;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
transition: border-color 0.15s, background-color 0.15s;
|
||||
|
||||
&:hover {
|
||||
border-color: #93c5fd;
|
||||
background-color: #f0f7ff;
|
||||
}
|
||||
|
||||
.cat-thumb {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.thumb-del-badge {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
right: 3px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: rgba(15, 23, 42, 0.65);
|
||||
color: #ffffff;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
transition: background 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: #e11d48;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-plus {
|
||||
font-size: 16px;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.upload-tip {
|
||||
font-size: 10px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px dashed #f1f5f9;
|
||||
|
||||
.action-btn {
|
||||
padding: 0 8px;
|
||||
height: 26px;
|
||||
|
||||
&.del-btn {
|
||||
color: #ef4444;
|
||||
&:hover {
|
||||
background: #fee2e2;
|
||||
border-color: #fca5a5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add-item-full-btn {
|
||||
width: 100%;
|
||||
height: 34px;
|
||||
font-size: 12px;
|
||||
border-dashed: true;
|
||||
border: 1px dashed #93c5fd;
|
||||
background: #f8fafc;
|
||||
|
||||
&:hover {
|
||||
background: #eff6ff;
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
780
seller/src/views/shop/pc/config-panel/PanelChannelFloor.vue
Normal file
780
seller/src/views/shop/pc/config-panel/PanelChannelFloor.vue
Normal file
@@ -0,0 +1,780 @@
|
||||
<template>
|
||||
<div class="panel-channel-floor">
|
||||
<!-- 1. 标题区设置 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">标题区设置</div>
|
||||
<div class="form-item flex-row">
|
||||
<span class="item-label">展示标题区</span>
|
||||
<el-switch v-model="options.showTitle" size="small" />
|
||||
</div>
|
||||
<template v-if="options.showTitle !== false">
|
||||
<div class="form-item">
|
||||
<span class="item-label">主标题</span>
|
||||
<el-input v-model="options.title" size="small" placeholder="如:场景频道" clearable />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">副标题</span>
|
||||
<el-input v-model="options.subtitle" size="small" placeholder="如:主视觉 + 商品组合楼层" clearable />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">“更多”跳转链接 (选填)</span>
|
||||
<div class="link-picker-wrap">
|
||||
<el-input
|
||||
v-model="options.moreUrl"
|
||||
size="small"
|
||||
placeholder="默认跟随主视觉链接,如 /goods"
|
||||
clearable
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon class="link-input-icon"><Link /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
size="small"
|
||||
class="action-picker-btn link-btn"
|
||||
@click="handleSelectMoreLink"
|
||||
>
|
||||
<el-icon class="mr-4"><Link /></el-icon>
|
||||
<span>选择链接</span>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="link-current-tag" v-if="options.moreUrl">
|
||||
<span class="tag-label">当前链接:</span>
|
||||
<span class="tag-val" :title="options.moreUrl">{{ options.moreUrl }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 2. 场景主视觉 (Hero Section) -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">场景主视觉</div>
|
||||
|
||||
<!-- 主视觉图片 -->
|
||||
<div class="form-item">
|
||||
<span class="item-label">主视觉图片 (建议尺寸 520x560px)</span>
|
||||
<div class="picker-flex-wrap">
|
||||
<div
|
||||
class="thumb-preview-box"
|
||||
@click="handleSelectHeroImage"
|
||||
:title="options.heroImage ? '点击更换主视觉图片' : '点击选择图片'"
|
||||
>
|
||||
<template v-if="options.heroImage">
|
||||
<img :src="options.heroImage" alt="主视觉" class="preview-img" />
|
||||
<span class="thumb-clear-btn" @click.stop="options.heroImage = ''" title="清除图片">
|
||||
<el-icon><Close /></el-icon>
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-icon class="thumb-empty-icon"><Picture /></el-icon>
|
||||
<span class="thumb-empty-tip">选图</span>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="input-with-action flex-1">
|
||||
<el-input
|
||||
v-model="options.heroImage"
|
||||
size="small"
|
||||
placeholder="请输入或选择主视觉图片"
|
||||
clearable
|
||||
/>
|
||||
<el-button
|
||||
size="small"
|
||||
class="action-picker-btn"
|
||||
@click="handleSelectHeroImage"
|
||||
>
|
||||
<el-icon class="mr-4"><Picture /></el-icon>
|
||||
<span>选择图片</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-item mt-10">
|
||||
<span class="item-label">主视觉大标题</span>
|
||||
<el-input v-model="options.heroTitle" size="small" placeholder="如:品质生活馆" clearable />
|
||||
</div>
|
||||
|
||||
<div class="form-item">
|
||||
<span class="item-label">主视觉副标题</span>
|
||||
<el-input v-model="options.heroSubtitle" size="small" placeholder="如:精选场景好物,一站购齐" clearable />
|
||||
</div>
|
||||
|
||||
<div class="form-item">
|
||||
<span class="item-label">行动按钮文案</span>
|
||||
<el-input v-model="options.heroButtonText" size="small" placeholder="如:进入频道" clearable />
|
||||
</div>
|
||||
|
||||
<!-- 按钮跳转链接 -->
|
||||
<div class="form-item">
|
||||
<span class="item-label">按钮跳转链接</span>
|
||||
<div class="link-picker-wrap">
|
||||
<el-input
|
||||
v-model="options.heroHref"
|
||||
size="small"
|
||||
placeholder="如 /goods 或活动页面链接"
|
||||
clearable
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon class="link-input-icon"><Link /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
size="small"
|
||||
class="action-picker-btn link-btn"
|
||||
@click="handleSelectHeroLink"
|
||||
>
|
||||
<el-icon class="mr-4"><Link /></el-icon>
|
||||
<span>选择链接</span>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="link-current-tag" v-if="options.heroHref">
|
||||
<span class="tag-label">当前链接:</span>
|
||||
<span class="tag-val" :title="options.heroHref">{{ options.heroHref }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-item flex-row mt-10">
|
||||
<span class="item-label">主视觉视觉风格</span>
|
||||
<el-radio-group v-model="options.heroStyle" size="small">
|
||||
<el-radio-button label="gradient">主题渐变</el-radio-button>
|
||||
<el-radio-button label="plain">简洁浅色</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3. 活动入口 (Promos) -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title flex-between">
|
||||
<span>专属活动入口 ({{ options.promos?.length || 0 }} 个)</span>
|
||||
<el-button link type="primary" size="small" @click="handleAddPromo">
|
||||
<el-icon><Plus /></el-icon> 添加活动
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="promos-list-editor" v-if="Array.isArray(options.promos) && options.promos.length">
|
||||
<div v-for="(p, pIdx) in options.promos" :key="pIdx" class="promo-editor-card">
|
||||
<!-- 顶部标签与排序按钮 -->
|
||||
<div class="promo-card-header">
|
||||
<span class="promo-idx-badge">入口 {{ pIdx + 1 }}{{ p.title ? ` · ${p.title}` : '' }}</span>
|
||||
<div class="promo-actions">
|
||||
<el-button
|
||||
link
|
||||
size="small"
|
||||
:disabled="pIdx === 0"
|
||||
@click="movePromo(pIdx, -1)"
|
||||
title="上移"
|
||||
>
|
||||
<el-icon><Top /></el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
size="small"
|
||||
:disabled="pIdx === options.promos.length - 1"
|
||||
@click="movePromo(pIdx, 1)"
|
||||
title="下移"
|
||||
>
|
||||
<el-icon><Bottom /></el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="options.promos.splice(pIdx, 1)"
|
||||
title="删除入口"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 属性输入 -->
|
||||
<div class="promo-row-1">
|
||||
<div class="sub-col" style="width: 100px;">
|
||||
<span class="micro-label">角标标签</span>
|
||||
<el-input v-model="p.tag" size="small" placeholder="限时/新品" clearable />
|
||||
</div>
|
||||
<div class="sub-col flex-1">
|
||||
<span class="micro-label">活动标题</span>
|
||||
<el-input v-model="p.title" size="small" placeholder="如:大牌补贴" clearable />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="promo-row-2 mt-6">
|
||||
<span class="micro-label">活动描述</span>
|
||||
<el-input v-model="p.desc" size="small" placeholder="如:领券最高减100" clearable />
|
||||
</div>
|
||||
|
||||
<!-- 活动链接选择器 -->
|
||||
<div class="promo-row-3 mt-6">
|
||||
<span class="micro-label">跳转链接</span>
|
||||
<div class="link-picker-wrap">
|
||||
<el-input
|
||||
v-model="p.url"
|
||||
size="small"
|
||||
placeholder="如 /goods 或活动专题页"
|
||||
clearable
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon class="link-input-icon"><Link /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
size="small"
|
||||
class="action-picker-btn link-btn"
|
||||
@click="handleSelectPromoLink(p)"
|
||||
>
|
||||
<el-icon class="mr-4"><Link /></el-icon>
|
||||
<span>选择链接</span>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="link-current-tag" v-if="p.url">
|
||||
<span class="tag-label">当前链接:</span>
|
||||
<span class="tag-val" :title="p.url">{{ p.url }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="empty-tip" v-else>
|
||||
暂无活动入口,点击右上角“添加活动”
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 4. 场景商品选择 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title flex-between">
|
||||
<span>场景商品位 ({{ options.goods?.length || 0 }} 件)</span>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<el-button v-if="options.goods?.length" link type="danger" size="small" @click="options.goods = []">清空</el-button>
|
||||
<el-button type="primary" size="small" @click="handleOpenGoodsPicker">选择商品</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-item flex-row">
|
||||
<span class="item-label">展示商品数量</span>
|
||||
<el-input-number v-model="options.goodsCount" :min="1" :max="8" size="small" controls-position="right" />
|
||||
</div>
|
||||
|
||||
<div class="goods-list-preview" v-if="options.goods?.length">
|
||||
<div v-for="(g, idx) in options.goods" :key="g.id || idx" class="goods-item-row">
|
||||
<img :src="g.thumbnail || defaultThumb" class="g-thumb" alt="" />
|
||||
<div class="g-info">
|
||||
<span class="g-name">{{ g.goodsName }}</span>
|
||||
<span class="g-price">¥{{ g.price }}</span>
|
||||
</div>
|
||||
<el-button link type="danger" size="small" @click="options.goods.splice(idx, 1)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 商品选择弹窗 (兜底) -->
|
||||
<el-dialog
|
||||
v-if="!openGoodsPicker && showGoodsModal"
|
||||
v-model="showGoodsModal"
|
||||
title="从商品库选择商品"
|
||||
width="1160px"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
>
|
||||
<goods-dialog
|
||||
:selectedWay="tempSelectedGoods"
|
||||
@selected="(val) => { tempSelectedGoods = val; }"
|
||||
/>
|
||||
<template #footer>
|
||||
<el-button @click="showGoodsModal = false">取消</el-button>
|
||||
<el-button type="primary" @click="confirmGoodsSelected">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, inject } from 'vue';
|
||||
import GoodsDialog from '@/views/lili-dialog/goods-dialog.vue';
|
||||
import { Plus, Delete, Link, Picture, Close, Top, Bottom } from '@element-plus/icons-vue';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const defaultThumb = 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_1.png';
|
||||
const showGoodsModal = ref(false);
|
||||
const tempSelectedGoods = ref([]);
|
||||
|
||||
// 注入来自 pc/index.vue 的全局选择器
|
||||
const openImagePicker = inject('openImagePicker', null);
|
||||
const openLinkPicker = inject('openLinkPicker', null);
|
||||
const openGoodsPicker = inject('openGoodsPicker', null);
|
||||
|
||||
// 默认值兜底
|
||||
if (props.options.showTitle === undefined) props.options.showTitle = true;
|
||||
if (!props.options.title) props.options.title = '场景频道';
|
||||
if (!props.options.subtitle) props.options.subtitle = '主视觉 + 商品组合楼层';
|
||||
if (!props.options.heroImage) props.options.heroImage = 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/banner-floor/floor_banner_1.png';
|
||||
if (!props.options.heroTitle) props.options.heroTitle = '品质生活馆';
|
||||
if (!props.options.heroSubtitle) props.options.heroSubtitle = '精选场景好物,一站购齐';
|
||||
if (!props.options.heroButtonText) props.options.heroButtonText = '进入频道';
|
||||
if (!props.options.heroHref) props.options.heroHref = '/goods';
|
||||
if (!props.options.heroStyle) props.options.heroStyle = 'gradient';
|
||||
if (!props.options.promos) {
|
||||
props.options.promos = [
|
||||
{ title: '大牌补贴', desc: '领券最高减100', tag: '限时', url: '/goods' },
|
||||
{ title: '新品驾到', desc: '官方正品首发', tag: '新品', url: '/goods' }
|
||||
];
|
||||
}
|
||||
if (!props.options.goodsCount) props.options.goodsCount = 6;
|
||||
if (!props.options.goods) {
|
||||
props.options.goods = [
|
||||
{ id: '1', goodsName: '旗舰 5G 智能手机 12GB+256GB 超光影影像系统', price: '3999.00', originalPrice: '4599.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_1.png' },
|
||||
{ id: '2', goodsName: '无线头戴式深度主动降噪蓝牙耳机 空间音频Hi-Res', price: '1099.00', originalPrice: '1499.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_2.png' },
|
||||
{ id: '3', goodsName: '14英寸 全面屏轻薄商务笔记本电脑 酷睿i7 高刷防眩光', price: '5699.00', originalPrice: '6299.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_3.png' },
|
||||
{ id: '4', goodsName: '运动健康专业监测智能手表 心率血氧长续航50米防水', price: '899.00', originalPrice: '1199.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_4.png' },
|
||||
{ id: '5', goodsName: '意式半自动家用复古咖啡机 20Bar高压恒温萃取蒸汽奶泡', price: '899.00', originalPrice: '1280.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_6.png' },
|
||||
{ id: '6', goodsName: '奢宠酵母焕活修护精华露 50ml 紧致淡纹抗皱滋养', price: '680.00', originalPrice: '890.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_5.png' }
|
||||
];
|
||||
}
|
||||
|
||||
// 1. 选择主视觉图片
|
||||
function handleSelectHeroImage() {
|
||||
if (openImagePicker) {
|
||||
openImagePicker((url) => {
|
||||
if (url) props.options.heroImage = url;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入主视觉图片 URL 地址:', '选择主视觉图片', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: props.options.heroImage || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '图片地址不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
props.options.heroImage = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 选择主视觉按钮链接
|
||||
function handleSelectHeroLink() {
|
||||
if (openLinkPicker) {
|
||||
openLinkPicker(({ url }) => {
|
||||
if (url) props.options.heroHref = url;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入主视觉跳转链接(如 /goods 或活动网址):', '设置主视觉链接', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: props.options.heroHref || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '链接不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
props.options.heroHref = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 选择“探索更多”链接
|
||||
function handleSelectMoreLink() {
|
||||
if (openLinkPicker) {
|
||||
openLinkPicker(({ url }) => {
|
||||
if (url) props.options.moreUrl = url;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入“更多”跳转链接:', '设置更多跳转链接', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: props.options.moreUrl || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '链接不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
props.options.moreUrl = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 活动入口增删与排序
|
||||
function handleAddPromo() {
|
||||
if (!Array.isArray(props.options.promos)) props.options.promos = [];
|
||||
props.options.promos.push({
|
||||
title: '热销特惠',
|
||||
desc: '全品类甄选底价',
|
||||
tag: '热卖',
|
||||
url: '/goods'
|
||||
});
|
||||
}
|
||||
|
||||
function movePromo(index, step) {
|
||||
const target = index + step;
|
||||
if (target < 0 || target >= props.options.promos.length) return;
|
||||
const item = props.options.promos.splice(index, 1)[0];
|
||||
props.options.promos.splice(target, 0, item);
|
||||
}
|
||||
|
||||
// 5. 选择活动入口链接
|
||||
function handleSelectPromoLink(p) {
|
||||
if (openLinkPicker) {
|
||||
openLinkPicker(({ url, title }) => {
|
||||
if (url) p.url = url;
|
||||
if (!p.title && title) p.title = title;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入活动入口跳转链接:', '设置活动链接', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: p.url || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '链接不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
p.url = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 商品选择
|
||||
function handleOpenGoodsPicker() {
|
||||
if (typeof openGoodsPicker === 'function') {
|
||||
openGoodsPicker((selectedGoods) => {
|
||||
applySelectedGoods(selectedGoods);
|
||||
}, props.options.goods || []);
|
||||
} else {
|
||||
tempSelectedGoods.value = Array.isArray(props.options.goods) ? [...props.options.goods] : [];
|
||||
showGoodsModal.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
function confirmGoodsSelected() {
|
||||
applySelectedGoods(tempSelectedGoods.value);
|
||||
showGoodsModal.value = false;
|
||||
}
|
||||
|
||||
function applySelectedGoods(selectedGoods) {
|
||||
if (Array.isArray(selectedGoods)) {
|
||||
props.options.goods = selectedGoods.map(g => ({
|
||||
id: g.id || g.skuId || g.goodsId,
|
||||
goodsId: g.goodsId,
|
||||
goodsName: g.goodsName || g.name,
|
||||
price: g.price,
|
||||
originalPrice: g.originalPrice || g.price,
|
||||
thumbnail: g.thumbnail || g.pic || defaultThumb
|
||||
}));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.panel-channel-floor {
|
||||
padding: 8px 4px;
|
||||
|
||||
.panel-section {
|
||||
margin-bottom: 18px;
|
||||
padding-bottom: 14px;
|
||||
border-bottom: 1px dashed #e2e8f0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
margin-bottom: 10px;
|
||||
|
||||
&.flex-between {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.form-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
&.flex-row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.item-label {
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mt-6 {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.mt-10 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.mr-4 {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.flex-1 {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.micro-label {
|
||||
font-size: 11px;
|
||||
color: #64748b;
|
||||
margin-bottom: 2px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.picker-flex-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.thumb-preview-box {
|
||||
position: relative;
|
||||
width: 58px;
|
||||
height: 40px;
|
||||
border-radius: 4px;
|
||||
border: 1px dashed #cbd5e1;
|
||||
background: #ffffff;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
transition: border-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #409eff;
|
||||
}
|
||||
|
||||
.preview-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.thumb-empty-icon {
|
||||
font-size: 15px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.thumb-empty-tip {
|
||||
font-size: 10px;
|
||||
color: #94a3b8;
|
||||
line-height: 1;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.thumb-clear-btn {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
background: rgba(0, 0, 0, 0.65);
|
||||
color: #ffffff;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: #ef4444;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input-with-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.action-picker-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link-picker-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.link-input-icon {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.action-picker-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 0 10px;
|
||||
|
||||
&.link-btn {
|
||||
background: #ecf5ff;
|
||||
border-color: #b3d8ff;
|
||||
color: #409eff;
|
||||
|
||||
&:hover {
|
||||
background: #409eff;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link-current-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 11px;
|
||||
background: #eff6ff;
|
||||
border: 1px solid #dbeafe;
|
||||
border-radius: 4px;
|
||||
padding: 2px 8px;
|
||||
margin-top: 2px;
|
||||
overflow: hidden;
|
||||
|
||||
.tag-label {
|
||||
color: #3b82f6;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tag-val {
|
||||
color: #1e40af;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.promos-list-editor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
.promo-editor-card {
|
||||
background: #f8fafc;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e2e8f0;
|
||||
|
||||
.promo-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 1px solid #edf2f7;
|
||||
|
||||
.promo-idx-badge {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.promo-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.promo-row-1 {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
text-align: center;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.goods-list-preview {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
|
||||
.goods-item-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: #f8fafc;
|
||||
padding: 6px 8px;
|
||||
border-radius: 6px;
|
||||
|
||||
.g-thumb {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.g-info {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.g-name {
|
||||
font-size: 12px;
|
||||
color: #1e293b;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.g-price {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #e11d48;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
509
seller/src/views/shop/pc/config-panel/PanelChannelGrid.vue
Normal file
509
seller/src/views/shop/pc/config-panel/PanelChannelGrid.vue
Normal file
@@ -0,0 +1,509 @@
|
||||
<template>
|
||||
<div class="panel-channel-grid">
|
||||
<!-- 1. 内容设置 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">
|
||||
<div class="title-with-tip">
|
||||
<span>内容设置</span>
|
||||
<el-tooltip
|
||||
content="设置频道宫格入口图文、跳转链接及排版间距"
|
||||
placement="top"
|
||||
>
|
||||
<el-icon class="help-icon"><QuestionFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sub-section-title">频道入口</div>
|
||||
|
||||
<!-- 频道卡片列表 -->
|
||||
<div class="channel-cards-list">
|
||||
<div
|
||||
v-for="(item, idx) in options.items"
|
||||
:key="idx"
|
||||
class="channel-item-card"
|
||||
>
|
||||
<!-- 第一行:左侧【选择资源】,右侧【替换图片/缩略图】 -->
|
||||
<div class="card-row-top">
|
||||
<el-button
|
||||
size="small"
|
||||
class="select-resource-btn"
|
||||
@click="handleSelectResource(item)"
|
||||
>
|
||||
选择资源
|
||||
</el-button>
|
||||
|
||||
<!-- 右侧图片上传/预览框 -->
|
||||
<div
|
||||
class="thumb-picker-box"
|
||||
@click="handleSelectImage(item)"
|
||||
:title="item.img ? '点击更换图片' : '点击上传图片'"
|
||||
>
|
||||
<template v-if="item.img">
|
||||
<img :src="item.img" alt="频道图" class="thumb-img" />
|
||||
<span class="thumb-del-badge" @click.stop="item.img = ''" title="移除图片">
|
||||
<el-icon><Close /></el-icon>
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="upload-tip-text">替换图片</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第二行:主标题与副标题 -->
|
||||
<div class="card-row-inputs">
|
||||
<el-input
|
||||
v-model="item.title"
|
||||
size="small"
|
||||
placeholder="主标题(如:企业采购)"
|
||||
class="title-input"
|
||||
/>
|
||||
<el-input
|
||||
v-model="item.subtitle"
|
||||
size="small"
|
||||
placeholder="副标题(如:批量省心)"
|
||||
class="subtitle-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 第三行:底栏工具条 (链接、上移、下移、删除) -->
|
||||
<div class="card-row-actions">
|
||||
<el-button
|
||||
size="small"
|
||||
class="action-btn link-btn"
|
||||
:class="{ 'is-linked': !!item.url }"
|
||||
@click="handleSetLink(item)"
|
||||
@contextmenu.prevent="item.url = ''"
|
||||
:title="item.url ? (`已设链接: ${item.url} (点击修改,右键清除)`) : '设置跳转链接'"
|
||||
>
|
||||
<el-icon><Link /></el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
class="action-btn"
|
||||
:disabled="idx === 0"
|
||||
@click="moveItem(idx, -1)"
|
||||
title="上移"
|
||||
>
|
||||
<el-icon><Top /></el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
class="action-btn"
|
||||
:disabled="idx === (options.items || []).length - 1"
|
||||
@click="moveItem(idx, 1)"
|
||||
title="下移"
|
||||
>
|
||||
<el-icon><Bottom /></el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
class="action-btn del-btn"
|
||||
@click="options.items.splice(idx, 1)"
|
||||
title="删除频道"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加频道按钮 -->
|
||||
<el-button
|
||||
class="add-channel-full-btn"
|
||||
@click="handleAddChannel"
|
||||
>
|
||||
<el-icon><Plus /></el-icon> 添加频道
|
||||
</el-button>
|
||||
|
||||
<!-- 排版间距与圆角参数 -->
|
||||
<div class="form-row mt-16">
|
||||
<span class="row-label">图片圆角</span>
|
||||
<el-input-number
|
||||
v-model="options.imageRadius"
|
||||
:min="0"
|
||||
:max="50"
|
||||
:step="1"
|
||||
size="small"
|
||||
class="num-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-row mt-12">
|
||||
<span class="row-label">间距</span>
|
||||
<el-input-number
|
||||
v-model="options.gap"
|
||||
:min="0"
|
||||
:max="40"
|
||||
:step="1"
|
||||
size="small"
|
||||
class="num-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject } from 'vue';
|
||||
import { Plus, Delete, Close, Link, Top, Bottom, QuestionFilled } from '@element-plus/icons-vue';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
// 注入全局选择器
|
||||
const openLinkPicker = inject('openLinkPicker', null);
|
||||
const openImagePicker = inject('openImagePicker', null);
|
||||
|
||||
// 1. 选择资源 (打开链接/资源树选择器,回填名称与链接)
|
||||
function handleSelectResource(item) {
|
||||
if (openLinkPicker) {
|
||||
openLinkPicker(({ url, title, raw }) => {
|
||||
if (title && !item.title) item.title = title;
|
||||
if (url) item.url = url;
|
||||
if (raw && (raw.image || raw.icon || raw.pic) && !item.img) {
|
||||
item.img = raw.image || raw.icon || raw.pic;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入频道名称:', '选择资源', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: item.title || '',
|
||||
inputPattern: /\S+/,
|
||||
inputErrorMessage: '名称不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
item.title = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 选择图片
|
||||
function handleSelectImage(item) {
|
||||
if (openImagePicker) {
|
||||
openImagePicker((url) => {
|
||||
if (url) item.img = url;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入图片 URL 地址:', '上传频道图片', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: item.img || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '图片地址不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
item.img = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 设置链接
|
||||
function handleSetLink(item) {
|
||||
if (openLinkPicker) {
|
||||
openLinkPicker(({ url, title }) => {
|
||||
item.url = url;
|
||||
if (!item.title && title) item.title = title;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入跳转链接(如 /goodsList?channel=1):', '设置跳转链接', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: item.url || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '链接不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
item.url = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 上移 / 下移
|
||||
function moveItem(idx, dir) {
|
||||
const list = props.options.items;
|
||||
const target = idx + dir;
|
||||
if (target < 0 || target >= list.length) return;
|
||||
const temp = list[idx];
|
||||
list[idx] = list[target];
|
||||
list[target] = temp;
|
||||
}
|
||||
|
||||
// 5. 添加频道
|
||||
function handleAddChannel() {
|
||||
if (!Array.isArray(props.options.items)) {
|
||||
props.options.items = [];
|
||||
}
|
||||
const nextNum = props.options.items.length + 1;
|
||||
const iconIdx = ((nextNum - 1) % 4) + 1;
|
||||
props.options.items.push({
|
||||
title: `频道 ${nextNum}`,
|
||||
subtitle: '品质好物',
|
||||
img: `https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/channel-grid/channel_${iconIdx}.png`,
|
||||
url: `/goodsList?channel=${nextNum}`
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.panel-channel-grid {
|
||||
font-size: 13px;
|
||||
color: #334155;
|
||||
|
||||
.panel-section {
|
||||
padding-bottom: 20px;
|
||||
|
||||
.section-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.title-with-tip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.help-icon {
|
||||
font-size: 14px;
|
||||
color: #94a3b8;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #475569;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sub-section-title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #475569;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.channel-cards-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.channel-item-card {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
transition: border-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #cbd5e1;
|
||||
}
|
||||
|
||||
// 第一行:选择资源 + 替换图片
|
||||
.card-row-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
.select-resource-btn {
|
||||
flex: 1;
|
||||
height: 38px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #1e293b;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #f97316;
|
||||
color: #ea580c;
|
||||
}
|
||||
}
|
||||
|
||||
.thumb-picker-box {
|
||||
flex: 1;
|
||||
height: 38px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
border-color: #f97316;
|
||||
|
||||
.upload-tip-text {
|
||||
color: #ea580c;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-tip-text {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.thumb-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.thumb-del-badge {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: rgba(15, 23, 42, 0.7);
|
||||
color: #ffffff;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: #ef4444;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 第二行:主副标题输入
|
||||
.card-row-inputs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.title-input, .subtitle-input {
|
||||
flex: 1;
|
||||
|
||||
:deep(.el-input__wrapper) {
|
||||
background: #ffffff;
|
||||
box-shadow: 0 0 0 1px #e2e8f0 inset;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 0 1px #cbd5e1 inset;
|
||||
}
|
||||
|
||||
&.is-focus {
|
||||
box-shadow: 0 0 0 1px #f97316 inset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 第三行:工具条
|
||||
.card-row-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e2e8f0;
|
||||
background: #ffffff;
|
||||
color: #64748b;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
border-color: #cbd5e1;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
&.link-btn.is-linked {
|
||||
border-color: #fdba74;
|
||||
background: #fff7ed;
|
||||
color: #ea580c;
|
||||
}
|
||||
|
||||
&.del-btn {
|
||||
&:hover {
|
||||
border-color: #fecaca;
|
||||
background: #fef2f2;
|
||||
color: #ef4444;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加频道按钮
|
||||
.add-channel-full-btn {
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
background: #ffffff;
|
||||
color: #334155;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #f97316;
|
||||
color: #ea580c;
|
||||
background: #fffaf5;
|
||||
}
|
||||
}
|
||||
|
||||
// 参数设置行
|
||||
.form-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
&.mt-16 {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
&.mt-12 {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.row-label {
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.num-input {
|
||||
width: 120px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
60
seller/src/views/shop/pc/config-panel/PanelFreeLayout.vue
Normal file
60
seller/src/views/shop/pc/config-panel/PanelFreeLayout.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<div class="panel-free-layout">
|
||||
<div class="panel-section">
|
||||
<div class="section-title">自由画布容器设置</div>
|
||||
<div class="form-item flex-row">
|
||||
<span class="item-label">容器固定高度 (px)</span>
|
||||
<el-input-number v-model="options.height" :min="100" :max="1000" :step="20" size="small" controls-position="right" />
|
||||
</div>
|
||||
<div class="layout-hint">
|
||||
当前自由布局容器内已包含 {{ options.elements?.length || 0 }} 个绝对定位元素。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.panel-free-layout {
|
||||
.panel-section {
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 16px;
|
||||
|
||||
.section-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.item-label {
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
}
|
||||
}
|
||||
|
||||
.layout-hint {
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1230
seller/src/views/shop/pc/config-panel/PanelGoods.vue
Normal file
1230
seller/src/views/shop/pc/config-panel/PanelGoods.vue
Normal file
File diff suppressed because it is too large
Load Diff
639
seller/src/views/shop/pc/config-panel/PanelGoodsGrid.vue
Normal file
639
seller/src/views/shop/pc/config-panel/PanelGoodsGrid.vue
Normal file
@@ -0,0 +1,639 @@
|
||||
<template>
|
||||
<div class="panel-goods-grid">
|
||||
<!-- 1. 标题与更多设置 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">宫格标题设置</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">主标题</span>
|
||||
<el-input v-model="options.title" size="small" placeholder="如:爆款热卖榜" clearable />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">副标题</span>
|
||||
<el-input v-model="options.subtitle" size="small" placeholder="如:近30天爆卖好物精选" clearable />
|
||||
</div>
|
||||
<div class="form-item flex-row">
|
||||
<span class="item-label">展示查看更多</span>
|
||||
<el-switch v-model="options.showMoreLink" size="small" />
|
||||
</div>
|
||||
<template v-if="options.showMoreLink !== false">
|
||||
<div class="form-item">
|
||||
<span class="item-label">更多文案</span>
|
||||
<el-input v-model="options.moreText" size="small" placeholder="如:查看全部" clearable />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">更多跳转链接</span>
|
||||
<div class="link-picker-wrap">
|
||||
<el-input
|
||||
v-model="options.moreUrl"
|
||||
size="small"
|
||||
placeholder="如:/goodsList 或专题页面"
|
||||
clearable
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon class="link-input-icon"><Link /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
size="small"
|
||||
class="action-picker-btn link-btn"
|
||||
@click="handleSelectMoreLink"
|
||||
>
|
||||
<el-icon class="mr-4"><Link /></el-icon>
|
||||
<span>选择链接</span>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="link-current-tag" v-if="options.moreUrl">
|
||||
<span class="tag-label">当前链接:</span>
|
||||
<span class="tag-val" :title="options.moreUrl">{{ options.moreUrl }}</span>
|
||||
<el-icon class="url-clear-icon" title="清除链接" @click.stop="options.moreUrl = ''"><Close /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 2. 宫格排版设置 (网格与列数) -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">宫格排版与线条</div>
|
||||
<div class="form-item flex-row">
|
||||
<span class="item-label">一行展示几个 (列数)</span>
|
||||
<el-radio-group v-model="options.columns" size="small">
|
||||
<el-radio-button :label="4">4 列</el-radio-button>
|
||||
<el-radio-button :label="5">5 列 (推荐)</el-radio-button>
|
||||
<el-radio-button :label="6">6 列</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<div class="form-item flex-row">
|
||||
<span class="item-label">矩阵实线网格分割线</span>
|
||||
<el-switch v-model="options.showBorder" size="small" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3. 商品列表与管理 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title flex-between">
|
||||
<span>宫格商品数据 ({{ options.goods?.length || 0 }} 件)</span>
|
||||
<div class="btn-group">
|
||||
<el-button v-if="options.goods?.length" link type="danger" size="small" @click="options.goods = []">
|
||||
清空
|
||||
</el-button>
|
||||
<el-button type="primary" size="small" @click="handleOpenGoodsPicker">
|
||||
选择商品
|
||||
</el-button>
|
||||
<el-button size="small" @click="handleAddCustomGoods">
|
||||
<el-icon class="mr-4"><Plus /></el-icon> 自定义加品
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="goods-grid-preview" v-if="options.goods?.length">
|
||||
<div v-for="(g, idx) in options.goods" :key="g.id || idx" class="grid-item-row">
|
||||
<span class="idx-badge">{{ idx + 1 }}</span>
|
||||
<div class="thumb-wrap-mini" @click="handleSelectGoodsThumb(g)" title="点击更换商品图片">
|
||||
<img :src="g.thumbnail || defaultThumb" class="g-thumb" alt="" />
|
||||
<div class="thumb-tip">换图</div>
|
||||
</div>
|
||||
<div class="g-info">
|
||||
<el-input
|
||||
v-model="g.goodsName"
|
||||
size="small"
|
||||
placeholder="商品名称"
|
||||
class="g-name-input"
|
||||
/>
|
||||
<div class="g-meta-row">
|
||||
<div class="price-input-wrap">
|
||||
<span class="price-sym">¥</span>
|
||||
<el-input v-model="g.price" size="small" placeholder="价格" class="price-mini-input" />
|
||||
</div>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="handleSelectGoodsLink(g)"
|
||||
:title="g.url || '设置跳转链接'"
|
||||
>
|
||||
<el-icon><Link /></el-icon>
|
||||
<span style="font-size: 11px; margin-left: 2px;">{{ g.url ? '已配链接' : '选链接' }}</span>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="item-url-badge" v-if="g.url">
|
||||
<span class="url-label">🔗</span>
|
||||
<span class="url-text" :title="g.url">{{ g.url }}</span>
|
||||
<el-icon class="url-clear-icon" title="清除链接" @click.stop="g.url = ''"><Close /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-btn-col">
|
||||
<el-button
|
||||
link
|
||||
size="small"
|
||||
:disabled="idx === 0"
|
||||
@click="handleMoveGoods(idx, -1)"
|
||||
title="上移"
|
||||
>
|
||||
<el-icon><Top /></el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
size="small"
|
||||
:disabled="idx === options.goods.length - 1"
|
||||
@click="handleMoveGoods(idx, 1)"
|
||||
title="下移"
|
||||
>
|
||||
<el-icon><Bottom /></el-icon>
|
||||
</el-button>
|
||||
<el-button link type="danger" size="small" @click="options.goods.splice(idx, 1)" title="删除">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="goods-empty-tip" v-else>
|
||||
<span>暂未选择商品,前台将展示推荐占位</span>
|
||||
<div class="empty-btn-row">
|
||||
<el-button type="primary" link size="small" @click="handleOpenGoodsPicker">
|
||||
+ 从商品库选择
|
||||
</el-button>
|
||||
<el-button type="primary" link size="small" @click="handleAddCustomGoods">
|
||||
+ 自定义加品
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 兜底商品选择弹窗(当未注入全局 openGoodsPicker 时生效) -->
|
||||
<el-dialog
|
||||
v-if="!openGoodsPicker && fallbackModal"
|
||||
v-model="fallbackModal"
|
||||
title="从商品库选择商品"
|
||||
width="1160px"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
>
|
||||
<goods-dialog
|
||||
:selectedWay="tempSelectedGoods"
|
||||
@selected="(val) => { tempSelectedGoods = val; }"
|
||||
/>
|
||||
<template #footer>
|
||||
<el-button @click="fallbackModal = false">取消</el-button>
|
||||
<el-button type="primary" @click="confirmFallbackSelected">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, inject } from 'vue';
|
||||
import { Delete, Link, Picture, Plus, Top, Bottom, Close } from '@element-plus/icons-vue';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import GoodsDialog from '@/views/lili-dialog/goods-dialog.vue';
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const defaultThumb = 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_1.png';
|
||||
|
||||
// 注入来自 pc/index.vue 的全局选择器
|
||||
const openImagePicker = inject('openImagePicker', null);
|
||||
const openLinkPicker = inject('openLinkPicker', null);
|
||||
const openGoodsPicker = inject('openGoodsPicker', null);
|
||||
|
||||
const fallbackModal = ref(false);
|
||||
const tempSelectedGoods = ref([]);
|
||||
|
||||
// 1. 选择更多链接
|
||||
function handleSelectMoreLink() {
|
||||
if (openLinkPicker) {
|
||||
openLinkPicker(({ url }) => {
|
||||
if (url) props.options.moreUrl = url;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入“查看更多”跳转链接:', '设置更多链接', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: props.options.moreUrl || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '链接不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
props.options.moreUrl = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 更换商品图片
|
||||
function handleSelectGoodsThumb(g) {
|
||||
if (openImagePicker) {
|
||||
openImagePicker((url) => {
|
||||
if (url) g.thumbnail = url;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入商品图片 URL 地址:', '更换商品图片', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: g.thumbnail || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '图片地址不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
g.thumbnail = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 配置商品专属跳转链接
|
||||
function handleSelectGoodsLink(g) {
|
||||
if (openLinkPicker) {
|
||||
openLinkPicker(({ url, title }) => {
|
||||
if (url) g.url = url;
|
||||
if (!g.goodsName && title) g.goodsName = title;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入商品跳转链接(如 /goodsDetail?goodsId=xxx 或外部网址):', '设置商品链接', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: g.url || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '链接不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
g.url = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 自定义加品
|
||||
function handleAddCustomGoods() {
|
||||
if (!Array.isArray(props.options.goods)) {
|
||||
props.options.goods = [];
|
||||
}
|
||||
const nextIdx = props.options.goods.length + 1;
|
||||
const newItem = {
|
||||
id: `custom_${Date.now()}_${nextIdx}`,
|
||||
goodsName: `爆款精选好物 ${nextIdx}`,
|
||||
price: '99.00',
|
||||
originalPrice: '199.00',
|
||||
thumbnail: defaultThumb,
|
||||
url: ''
|
||||
};
|
||||
props.options.goods.push(newItem);
|
||||
if (openImagePicker) {
|
||||
openImagePicker((url) => {
|
||||
if (url) newItem.thumbnail = url;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 排序调整
|
||||
function handleMoveGoods(idx, direction) {
|
||||
const list = props.options.goods;
|
||||
const target = idx + direction;
|
||||
if (target < 0 || target >= list.length) return;
|
||||
const temp = list[idx];
|
||||
list[idx] = list[target];
|
||||
list[target] = temp;
|
||||
}
|
||||
|
||||
// 6. 从商品库选择
|
||||
function handleOpenGoodsPicker() {
|
||||
if (typeof openGoodsPicker === 'function') {
|
||||
openGoodsPicker((selectedGoods) => {
|
||||
applySelectedGoods(selectedGoods);
|
||||
}, props.options.goods || []);
|
||||
} else {
|
||||
tempSelectedGoods.value = Array.isArray(props.options.goods) ? [...props.options.goods] : [];
|
||||
fallbackModal.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
function confirmFallbackSelected() {
|
||||
applySelectedGoods(tempSelectedGoods.value);
|
||||
fallbackModal.value = false;
|
||||
}
|
||||
|
||||
function applySelectedGoods(selectedGoods) {
|
||||
if (Array.isArray(selectedGoods)) {
|
||||
props.options.goods = selectedGoods.map(g => ({
|
||||
id: g.id || g.skuId || g.goodsId,
|
||||
goodsId: g.goodsId,
|
||||
goodsName: g.goodsName || g.name,
|
||||
price: g.price,
|
||||
originalPrice: g.originalPrice || g.price,
|
||||
thumbnail: g.thumbnail || g.pic || defaultThumb,
|
||||
url: g.url || ''
|
||||
}));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.panel-goods-grid {
|
||||
padding: 8px 4px;
|
||||
|
||||
.panel-section {
|
||||
margin-bottom: 18px;
|
||||
padding-bottom: 14px;
|
||||
border-bottom: 1px dashed #e2e8f0;
|
||||
|
||||
.section-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
margin-bottom: 10px;
|
||||
|
||||
&.flex-between {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.form-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
&.flex-row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.item-label {
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mr-4 {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.link-picker-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.link-input-icon {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.action-picker-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 0 10px;
|
||||
|
||||
&.link-btn {
|
||||
background: #ecf5ff;
|
||||
border-color: #b3d8ff;
|
||||
color: #409eff;
|
||||
|
||||
&:hover {
|
||||
background: #409eff;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link-current-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 11px;
|
||||
background: #eff6ff;
|
||||
border: 1px solid #dbeafe;
|
||||
border-radius: 4px;
|
||||
padding: 3px 8px;
|
||||
margin-top: 4px;
|
||||
overflow: hidden;
|
||||
|
||||
.tag-label {
|
||||
color: #3b82f6;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tag-val {
|
||||
color: #1e40af;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.url-clear-icon {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
color: #ef4444;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.goods-grid-preview {
|
||||
max-height: 380px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.grid-item-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: #f8fafc;
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e2e8f0;
|
||||
transition: background 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
.idx-badge {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: #e2e8f0;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
color: #475569;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.thumb-wrap-mini {
|
||||
position: relative;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
border: 1px solid #cbd5e1;
|
||||
|
||||
.g-thumb {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.thumb-tip {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(15, 23, 42, 0.7);
|
||||
color: #ffffff;
|
||||
font-size: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
&:hover .thumb-tip {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.g-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
.g-name-input {
|
||||
:deep(.el-input__inner) {
|
||||
font-size: 12px;
|
||||
padding: 0 6px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.g-meta-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
|
||||
.price-input-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
font-size: 11px;
|
||||
color: #ef4444;
|
||||
font-weight: 600;
|
||||
|
||||
.price-mini-input {
|
||||
width: 70px;
|
||||
:deep(.el-input__inner) {
|
||||
font-size: 11px;
|
||||
padding: 0 4px;
|
||||
height: 22px;
|
||||
color: #ef4444;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-url-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 10px;
|
||||
background: #eff6ff;
|
||||
border: 1px solid #dbeafe;
|
||||
border-radius: 3px;
|
||||
padding: 2px 6px;
|
||||
color: #2563eb;
|
||||
|
||||
.url-label {
|
||||
font-size: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.url-text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.url-clear-icon {
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
|
||||
&:hover {
|
||||
color: #ef4444;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-btn-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.el-button {
|
||||
margin: 0;
|
||||
padding: 2px;
|
||||
height: auto;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.goods-empty-tip {
|
||||
padding: 18px 12px;
|
||||
border: 1px dashed #cbd5e1;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
background: #f8fafc;
|
||||
|
||||
.empty-btn-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
736
seller/src/views/shop/pc/config-panel/PanelGridFloor.vue
Normal file
736
seller/src/views/shop/pc/config-panel/PanelGridFloor.vue
Normal file
@@ -0,0 +1,736 @@
|
||||
<template>
|
||||
<div class="panel-grid-floor">
|
||||
<!-- 1. 楼层标题设置 (Floor Header) -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">楼层大标题设置</div>
|
||||
<div class="form-item flex-row">
|
||||
<span class="item-label">展示楼层标题</span>
|
||||
<el-switch v-model="options.showFloorTitle" size="small" />
|
||||
</div>
|
||||
<div class="form-row-2">
|
||||
<div class="form-item">
|
||||
<span class="item-label">楼层编号</span>
|
||||
<el-input v-model="options.floorNum" size="small" placeholder="如:1F" style="width: 90px;" clearable />
|
||||
</div>
|
||||
<div class="form-item flex-1">
|
||||
<span class="item-label">楼层名称</span>
|
||||
<el-input v-model="options.title" size="small" placeholder="如:潮流数码" clearable />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">英文/副标题</span>
|
||||
<el-input v-model="options.subtitle" size="small" placeholder="如:DIGITAL & TECH" clearable />
|
||||
</div>
|
||||
|
||||
<!-- 更多链接选择器 -->
|
||||
<div class="form-item">
|
||||
<span class="item-label">“查看更多”跳转链接</span>
|
||||
<div class="link-picker-wrap">
|
||||
<el-input
|
||||
v-model="options.moreUrl"
|
||||
size="small"
|
||||
placeholder="如:/goodsList 或分类活动页面"
|
||||
clearable
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon class="link-input-icon"><Link /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
size="small"
|
||||
class="action-picker-btn link-btn"
|
||||
@click="handleSelectMoreLink"
|
||||
>
|
||||
<el-icon class="mr-4"><Link /></el-icon>
|
||||
<span>选择链接</span>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="link-current-tag" v-if="options.moreUrl">
|
||||
<span class="tag-label">当前链接:</span>
|
||||
<span class="tag-val" :title="options.moreUrl">{{ options.moreUrl }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. 右侧子分类快捷标签 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title flex-between">
|
||||
<span>右侧分类标签</span>
|
||||
<el-button link type="primary" size="small" @click="handleAddTag">
|
||||
<el-icon><Plus /></el-icon> 添加标签
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="tags-editor-wrap">
|
||||
<div v-for="(tag, tIdx) in options.subCategories" :key="tIdx" class="tag-edit-row">
|
||||
<el-input v-model="options.subCategories[tIdx]" size="small" placeholder="分类名称" clearable />
|
||||
<el-button link type="danger" size="small" @click="options.subCategories.splice(tIdx, 1)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3. 左侧专属营销竖版海报 / 横幅 (横幅楼层 bannerFloor 与宫格楼层通用) -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">左侧营销海报 / 横幅设置</div>
|
||||
<div class="form-item flex-row">
|
||||
<span class="item-label">展示左侧海报横幅</span>
|
||||
<el-switch v-model="options.showPoster" size="small" />
|
||||
</div>
|
||||
<template v-if="options.showPoster !== false">
|
||||
<div class="form-item">
|
||||
<span class="item-label">海报横幅图片</span>
|
||||
<div class="picker-flex-wrap">
|
||||
<div
|
||||
class="thumb-preview-box"
|
||||
@click="handleSelectPosterImg"
|
||||
:title="currentPosterImg ? '点击更换海报' : '点击选择图片'"
|
||||
>
|
||||
<template v-if="currentPosterImg">
|
||||
<img :src="currentPosterImg" alt="海报" class="preview-img" />
|
||||
<span class="thumb-clear-btn" @click.stop="handleClearPosterImg" title="清除图片">
|
||||
<el-icon><Close /></el-icon>
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-icon class="thumb-empty-icon"><Picture /></el-icon>
|
||||
<span class="thumb-empty-tip">选图</span>
|
||||
</template>
|
||||
</div>
|
||||
<div class="input-with-action flex-1">
|
||||
<el-input
|
||||
v-model="currentPosterImg"
|
||||
size="small"
|
||||
placeholder="请输入或选择海报横幅图地址"
|
||||
clearable
|
||||
/>
|
||||
<el-button
|
||||
size="small"
|
||||
class="action-picker-btn"
|
||||
@click="handleSelectPosterImg"
|
||||
>
|
||||
<el-icon class="mr-4"><Picture /></el-icon>
|
||||
<span>选择图片</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-item mt-10">
|
||||
<span class="item-label">海报主标题</span>
|
||||
<el-input v-model="options.posterTitle" size="small" placeholder="如:数码潮电 酷玩季" clearable />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">海报副标题</span>
|
||||
<el-input v-model="options.posterSub" size="small" placeholder="如:领券最高立减 600 元" clearable />
|
||||
</div>
|
||||
|
||||
<div class="form-item">
|
||||
<span class="item-label">海报跳转链接</span>
|
||||
<div class="link-picker-wrap">
|
||||
<el-input
|
||||
v-model="currentPosterUrl"
|
||||
size="small"
|
||||
placeholder="如:/goods 或活动专题"
|
||||
clearable
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon class="link-input-icon"><Link /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
size="small"
|
||||
class="action-picker-btn link-btn"
|
||||
@click="handleSelectPosterLink"
|
||||
>
|
||||
<el-icon class="mr-4"><Link /></el-icon>
|
||||
<span>选择链接</span>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="link-current-tag" v-if="currentPosterUrl">
|
||||
<span class="tag-label">当前链接:</span>
|
||||
<span class="tag-val" :title="currentPosterUrl">{{ currentPosterUrl }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 4. 右侧商品列数与排版 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">右侧商品排版</div>
|
||||
<div class="form-item flex-row">
|
||||
<span class="item-label">单行展示列数</span>
|
||||
<el-radio-group v-model="options.columns" size="small">
|
||||
<el-radio-button :label="3">3 列</el-radio-button>
|
||||
<el-radio-button :label="4">4 列 (推荐)</el-radio-button>
|
||||
<el-radio-button :label="5">5 列</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 5. 楼层商品选择 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title flex-between">
|
||||
<span>楼层商品数据 ({{ options.goods?.length || 0 }} 件)</span>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<el-button v-if="options.goods?.length" link type="danger" size="small" @click="options.goods = []">清空</el-button>
|
||||
<el-button type="primary" size="small" @click="handleOpenGoodsPicker">选择商品</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="goods-list-preview" v-if="options.goods?.length">
|
||||
<div v-for="(g, idx) in options.goods" :key="g.id || idx" class="goods-item-row">
|
||||
<div class="thumb-wrap-mini" @click="handleSelectGoodsThumb(g)" title="点击更换商品图片">
|
||||
<img :src="g.thumbnail || defaultThumb" class="g-thumb" alt="" />
|
||||
<div class="thumb-tip">换图</div>
|
||||
</div>
|
||||
<div class="g-info">
|
||||
<span class="g-name" :title="g.goodsName">{{ g.goodsName }}</span>
|
||||
<div class="g-meta-row">
|
||||
<span class="g-price">¥{{ g.price }}</span>
|
||||
<el-button link type="primary" size="small" @click="handleSelectGoodsLink(g)" :title="g.url || '设置商品链接'">
|
||||
<el-icon><Link /></el-icon>
|
||||
<span style="font-size: 11px; margin-left: 2px;">{{ g.url ? '已配链接' : '选链接' }}</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-button link type="danger" size="small" @click="options.goods.splice(idx, 1)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 商品选择弹窗 (兜底) -->
|
||||
<el-dialog
|
||||
v-if="!openGoodsPicker && showGoodsModal"
|
||||
v-model="showGoodsModal"
|
||||
title="从商品库选择商品"
|
||||
width="1160px"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
>
|
||||
<goods-dialog
|
||||
:selectedWay="tempSelectedGoods"
|
||||
@selected="(val) => { tempSelectedGoods = val; }"
|
||||
/>
|
||||
<template #footer>
|
||||
<el-button @click="showGoodsModal = false">取消</el-button>
|
||||
<el-button type="primary" @click="confirmGoodsSelected">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, inject } from 'vue';
|
||||
import GoodsDialog from '@/views/lili-dialog/goods-dialog.vue';
|
||||
import { Plus, Delete, Link, Picture, Close } from '@element-plus/icons-vue';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const defaultThumb = 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_1.png';
|
||||
const showGoodsModal = ref(false);
|
||||
const tempSelectedGoods = ref([]);
|
||||
|
||||
// 注入来自 pc/index.vue 的全局选择器
|
||||
const openImagePicker = inject('openImagePicker', null);
|
||||
const openLinkPicker = inject('openLinkPicker', null);
|
||||
const openGoodsPicker = inject('openGoodsPicker', null);
|
||||
|
||||
// 初始化缺省 options
|
||||
if (props.options.showFloorTitle === undefined) props.options.showFloorTitle = true;
|
||||
if (!props.options.floorNum) props.options.floorNum = '1F';
|
||||
if (!props.options.title) props.options.title = '潮流数码';
|
||||
if (!props.options.subtitle) props.options.subtitle = 'DIGITAL & TECH';
|
||||
if (!props.options.subCategories) {
|
||||
props.options.subCategories = ['旗舰手机', '轻薄笔电', '降噪耳机', '智能穿戴', '数码配件'];
|
||||
}
|
||||
|
||||
// 兼容 bannerFloor 与 gridFloor 的字段名双向打通
|
||||
const currentPosterImg = computed({
|
||||
get() {
|
||||
return props.options.bannerImg || props.options.posterImg || 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/banner-floor/floor_banner_1.png';
|
||||
},
|
||||
set(val) {
|
||||
props.options.posterImg = val;
|
||||
props.options.bannerImg = val;
|
||||
}
|
||||
});
|
||||
|
||||
const currentPosterUrl = computed({
|
||||
get() {
|
||||
return props.options.bannerUrl || props.options.posterUrl || '';
|
||||
},
|
||||
set(val) {
|
||||
props.options.posterUrl = val;
|
||||
props.options.bannerUrl = val;
|
||||
}
|
||||
});
|
||||
|
||||
function handleClearPosterImg() {
|
||||
props.options.posterImg = '';
|
||||
props.options.bannerImg = '';
|
||||
}
|
||||
|
||||
if (!props.options.posterTitle) props.options.posterTitle = '数码潮电 酷玩季';
|
||||
if (!props.options.posterSub) props.options.posterSub = '领券最高立减 600 元';
|
||||
if (!props.options.columns) props.options.columns = 4;
|
||||
if (!props.options.goods) {
|
||||
props.options.goods = [
|
||||
{ id: '101', goodsName: '旗舰 5G 智能手机 12GB+256GB 超光影影像系统', price: '3999.00', originalPrice: '4599.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_1.png' },
|
||||
{ id: '102', goodsName: '无线头戴式深度主动降噪蓝牙耳机 空间音频Hi-Res', price: '1099.00', originalPrice: '1499.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_2.png' },
|
||||
{ id: '103', goodsName: '14英寸 全面屏轻薄商务笔记本电脑 酷睿i7 高刷防眩光', price: '5699.00', originalPrice: '6299.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_3.png' },
|
||||
{ id: '104', goodsName: '运动健康专业监测智能手表 心率血氧长续航50米防水', price: '899.00', originalPrice: '1199.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_4.png' },
|
||||
{ id: '105', goodsName: '奢宠酵母焕活修护精华露 50ml 紧致淡纹抗皱滋养', price: '680.00', originalPrice: '890.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_5.png' },
|
||||
{ id: '106', goodsName: '意式半自动家用复古咖啡机 20Bar高压恒温萃取蒸汽奶泡', price: '899.00', originalPrice: '1280.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_6.png' },
|
||||
{ id: '107', goodsName: '碳板专业竞速轻量缓震跑步鞋 透气防滑男女同款潮鞋', price: '459.00', originalPrice: '699.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_7.png' },
|
||||
{ id: '108', goodsName: '极简静音无雾负离子香薰加湿器 大容量客厅卧室通用', price: '299.00', originalPrice: '399.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_8.png' }
|
||||
];
|
||||
}
|
||||
|
||||
function handleAddTag() {
|
||||
if (!Array.isArray(props.options.subCategories)) {
|
||||
props.options.subCategories = [];
|
||||
}
|
||||
props.options.subCategories.push('新标签');
|
||||
}
|
||||
|
||||
// 1. 选择更多链接
|
||||
function handleSelectMoreLink() {
|
||||
if (openLinkPicker) {
|
||||
openLinkPicker(({ url }) => {
|
||||
if (url) props.options.moreUrl = url;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入“查看更多”跳转链接:', '设置更多链接', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: props.options.moreUrl || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '链接不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
props.options.moreUrl = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 选择海报横幅图片
|
||||
function handleSelectPosterImg() {
|
||||
if (openImagePicker) {
|
||||
openImagePicker((url) => {
|
||||
if (url) {
|
||||
currentPosterImg.value = url;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入海报图片 URL 地址:', '选择海报图片', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: currentPosterImg.value || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '图片地址不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
currentPosterImg.value = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 选择海报跳转链接
|
||||
function handleSelectPosterLink() {
|
||||
if (openLinkPicker) {
|
||||
openLinkPicker(({ url }) => {
|
||||
if (url) {
|
||||
currentPosterUrl.value = url;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入海报跳转链接:', '设置海报链接', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: currentPosterUrl.value || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '链接不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
currentPosterUrl.value = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 更换单个商品图片
|
||||
function handleSelectGoodsThumb(g) {
|
||||
if (openImagePicker) {
|
||||
openImagePicker((url) => {
|
||||
if (url) g.thumbnail = url;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入商品图片 URL 地址:', '更换商品图片', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: g.thumbnail || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '图片地址不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
g.thumbnail = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 配置商品专属链接
|
||||
function handleSelectGoodsLink(g) {
|
||||
if (openLinkPicker) {
|
||||
openLinkPicker(({ url, title }) => {
|
||||
if (url) g.url = url;
|
||||
if (!g.goodsName && title) g.goodsName = title;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入商品跳转链接(如 /goodsDetail?goodsId=xxx):', '设置商品链接', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: g.url || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '链接不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
g.url = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
function handleOpenGoodsPicker() {
|
||||
if (typeof openGoodsPicker === 'function') {
|
||||
openGoodsPicker((selectedGoods) => {
|
||||
applySelectedGoods(selectedGoods);
|
||||
}, props.options.goods || []);
|
||||
} else {
|
||||
tempSelectedGoods.value = Array.isArray(props.options.goods) ? [...props.options.goods] : [];
|
||||
showGoodsModal.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
function confirmGoodsSelected() {
|
||||
applySelectedGoods(tempSelectedGoods.value);
|
||||
showGoodsModal.value = false;
|
||||
}
|
||||
|
||||
function applySelectedGoods(selectedGoods) {
|
||||
if (Array.isArray(selectedGoods)) {
|
||||
props.options.goods = selectedGoods.map(g => ({
|
||||
id: g.id || g.skuId || g.goodsId,
|
||||
goodsId: g.goodsId,
|
||||
goodsName: g.goodsName || g.name,
|
||||
price: g.price,
|
||||
originalPrice: g.originalPrice || g.price,
|
||||
thumbnail: g.thumbnail || g.pic || defaultThumb,
|
||||
url: g.url || ''
|
||||
}));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.panel-grid-floor {
|
||||
padding: 8px 4px;
|
||||
|
||||
.panel-section {
|
||||
margin-bottom: 18px;
|
||||
padding-bottom: 14px;
|
||||
border-bottom: 1px dashed #e2e8f0;
|
||||
|
||||
.section-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
margin-bottom: 10px;
|
||||
|
||||
&.flex-between {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.form-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
&.flex-row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.item-label {
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.form-row-2 {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tags-editor-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
|
||||
.tag-edit-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mr-4 {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.mt-10 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.flex-1 {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.picker-flex-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.thumb-preview-box {
|
||||
position: relative;
|
||||
width: 58px;
|
||||
height: 40px;
|
||||
border-radius: 4px;
|
||||
border: 1px dashed #cbd5e1;
|
||||
background: #ffffff;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
transition: border-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #409eff;
|
||||
}
|
||||
|
||||
.preview-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.thumb-empty-icon {
|
||||
font-size: 15px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.thumb-empty-tip {
|
||||
font-size: 10px;
|
||||
color: #94a3b8;
|
||||
line-height: 1;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.thumb-clear-btn {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
background: rgba(0, 0, 0, 0.65);
|
||||
color: #ffffff;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: #ef4444;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input-with-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.action-picker-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link-picker-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.link-input-icon {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.action-picker-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 0 10px;
|
||||
|
||||
&.link-btn {
|
||||
background: #ecf5ff;
|
||||
border-color: #b3d8ff;
|
||||
color: #409eff;
|
||||
|
||||
&:hover {
|
||||
background: #409eff;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link-current-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 11px;
|
||||
background: #eff6ff;
|
||||
border: 1px solid #dbeafe;
|
||||
border-radius: 4px;
|
||||
padding: 3px 8px;
|
||||
margin-top: 4px;
|
||||
overflow: hidden;
|
||||
|
||||
.tag-label {
|
||||
color: #3b82f6;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tag-val {
|
||||
color: #1e40af;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.goods-list-preview {
|
||||
max-height: 260px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
|
||||
.goods-item-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: #f8fafc;
|
||||
padding: 6px 8px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e2e8f0;
|
||||
|
||||
.thumb-wrap-mini {
|
||||
position: relative;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
border: 1px solid #cbd5e1;
|
||||
|
||||
.g-thumb {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.thumb-tip {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(15, 23, 42, 0.7);
|
||||
color: #ffffff;
|
||||
font-size: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
&:hover .thumb-tip {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.g-info {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
|
||||
.g-name {
|
||||
font-size: 12px;
|
||||
color: #1e293b;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.g-meta-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.g-price {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #e11d48;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
627
seller/src/views/shop/pc/config-panel/PanelHotZone.vue
Normal file
627
seller/src/views/shop/pc/config-panel/PanelHotZone.vue
Normal file
@@ -0,0 +1,627 @@
|
||||
<template>
|
||||
<div class="panel-hot-zone">
|
||||
<!-- 1. 热区底图设置 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">热区底图</div>
|
||||
|
||||
<!-- URL 输入与选择图片按钮 -->
|
||||
<div class="form-item">
|
||||
<span class="item-label">大图 URL 地址</span>
|
||||
<div class="input-with-action">
|
||||
<el-input
|
||||
v-model="options.imageUrl"
|
||||
size="small"
|
||||
placeholder="请输入或选择热区大图 URL"
|
||||
clearable
|
||||
/>
|
||||
<el-button
|
||||
size="small"
|
||||
class="action-picker-btn"
|
||||
@click="handleSelectImage"
|
||||
>
|
||||
<el-icon class="mr-4"><Picture /></el-icon>
|
||||
<span>选择图片</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底图预览框 -->
|
||||
<div class="hotzone-preview-box" v-if="options.imageUrl">
|
||||
<img :src="options.imageUrl" alt="hotzone" class="img-preview" />
|
||||
<div class="preview-actions-bar">
|
||||
<el-button size="small" type="primary" @click="handleOpenHotzone">
|
||||
<el-icon class="mr-4"><Aim /></el-icon>
|
||||
绘制热区
|
||||
</el-button>
|
||||
<el-button size="small" type="primary" plain @click="handleSelectImage">
|
||||
<el-icon class="mr-4"><Picture /></el-icon>
|
||||
更换底图
|
||||
</el-button>
|
||||
<el-button size="small" type="danger" plain @click="options.imageUrl = ''">
|
||||
<el-icon class="mr-4"><Close /></el-icon>
|
||||
移除底图
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. 热区列表与链接选择 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title flex-between">
|
||||
<span>热区列表 ({{ options.zones?.length || 0 }})</span>
|
||||
<div class="header-action-group">
|
||||
<el-button type="primary" plain size="small" @click="handleOpenHotzone">
|
||||
<el-icon class="mr-4"><Aim /></el-icon> 绘制热区
|
||||
</el-button>
|
||||
<el-button link type="primary" size="small" @click="handleAddZone">
|
||||
<el-icon><Plus /></el-icon> 添加热区
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="zones-list" v-if="Array.isArray(options.zones) && options.zones.length">
|
||||
<div
|
||||
v-for="(zone, idx) in options.zones"
|
||||
:key="idx"
|
||||
class="zone-card-item"
|
||||
>
|
||||
<!-- 热区卡片头部 -->
|
||||
<div class="zone-header">
|
||||
<span class="zone-idx-title">
|
||||
<span class="zone-num-badge">{{ idx + 1 }}</span>
|
||||
<span>{{ zone.title || `热区 ${idx + 1}` }}</span>
|
||||
</span>
|
||||
<div class="zone-header-btns">
|
||||
<el-button
|
||||
link
|
||||
size="small"
|
||||
:disabled="idx === 0"
|
||||
@click="moveZone(idx, -1)"
|
||||
title="上移"
|
||||
>
|
||||
<el-icon><Top /></el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
size="small"
|
||||
:disabled="idx === options.zones.length - 1"
|
||||
@click="moveZone(idx, 1)"
|
||||
title="下移"
|
||||
>
|
||||
<el-icon><Bottom /></el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="options.zones.splice(idx, 1)"
|
||||
title="删除热区"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 热区标题与链接设置 -->
|
||||
<div class="zone-body-inputs">
|
||||
<!-- 热区标题 -->
|
||||
<div class="field-item">
|
||||
<span class="field-lbl">热区标题</span>
|
||||
<el-input
|
||||
v-model="zone.title"
|
||||
size="small"
|
||||
placeholder="热区名称(如:领券中心 / 潮电专场)"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 跳转链接 + 链接选择器 -->
|
||||
<div class="field-item">
|
||||
<span class="field-lbl">跳转链接</span>
|
||||
<div class="link-picker-row">
|
||||
<el-input
|
||||
v-model="zone.url"
|
||||
size="small"
|
||||
placeholder="跳转链接(如 /goodsList?category=1)"
|
||||
clearable
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon class="link-prefix-icon"><Link /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
size="small"
|
||||
class="action-picker-btn link-btn"
|
||||
@click="handleSelectLink(zone)"
|
||||
>
|
||||
<el-icon class="mr-4"><Link /></el-icon>
|
||||
<span>选择链接</span>
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 当前链接高亮标识 -->
|
||||
<div class="link-current-tag" v-if="zone.url">
|
||||
<span class="tag-label">当前链接:</span>
|
||||
<span class="tag-val" :title="zone.url">{{ zone.url }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 热区位置百分比 (X, Y, W, H) -->
|
||||
<div class="field-item">
|
||||
<span class="field-lbl">热区位置 (坐标与尺寸 %)</span>
|
||||
<div class="coord-grid">
|
||||
<div class="coord-cell">
|
||||
<span class="coord-lbl">X</span>
|
||||
<el-input-number
|
||||
v-model="zone.left"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:step="1"
|
||||
:precision="1"
|
||||
size="small"
|
||||
controls-position="right"
|
||||
class="coord-input"
|
||||
/>
|
||||
</div>
|
||||
<div class="coord-cell">
|
||||
<span class="coord-lbl">Y</span>
|
||||
<el-input-number
|
||||
v-model="zone.top"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:step="1"
|
||||
:precision="1"
|
||||
size="small"
|
||||
controls-position="right"
|
||||
class="coord-input"
|
||||
/>
|
||||
</div>
|
||||
<div class="coord-cell">
|
||||
<span class="coord-lbl">宽</span>
|
||||
<el-input-number
|
||||
v-model="zone.width"
|
||||
:min="1"
|
||||
:max="100"
|
||||
:step="1"
|
||||
:precision="1"
|
||||
size="small"
|
||||
controls-position="right"
|
||||
class="coord-input"
|
||||
/>
|
||||
</div>
|
||||
<div class="coord-cell">
|
||||
<span class="coord-lbl">高</span>
|
||||
<el-input-number
|
||||
v-model="zone.height"
|
||||
:min="1"
|
||||
:max="100"
|
||||
:step="1"
|
||||
:precision="1"
|
||||
size="small"
|
||||
controls-position="right"
|
||||
class="coord-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="empty-zones-box" v-else>
|
||||
<span>暂无热区,可点击“绘制热区”在底图上拖拽画热区</span>
|
||||
<div class="mt-8">
|
||||
<el-button type="primary" size="small" @click="handleOpenHotzone">
|
||||
<el-icon class="mr-4"><Aim /></el-icon> 立即绘制热区
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 绘制热区弹窗(复用移动端热区绘制器) -->
|
||||
<hotzone ref="hotzoneDialogRef" width="960px" @changeZone="handleChangeZone" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, inject } from 'vue';
|
||||
import { Plus, Delete, Link, Picture, Close, Top, Bottom, Aim } from '@element-plus/icons-vue';
|
||||
import { ElMessageBox, ElMessage } from 'element-plus';
|
||||
import hotzone from '@/views/shop/hotzone';
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
// 注入来自 pc/index.vue 的全局选择器
|
||||
const openImagePicker = inject('openImagePicker', null);
|
||||
const openLinkPicker = inject('openLinkPicker', null);
|
||||
|
||||
// 1. 选择大图
|
||||
function handleSelectImage() {
|
||||
if (openImagePicker) {
|
||||
openImagePicker((url) => {
|
||||
if (url) props.options.imageUrl = url;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入热区大图 URL 地址:', '选择热区底图', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: props.options.imageUrl || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '图片地址不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
props.options.imageUrl = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 选择热区跳转链接
|
||||
function handleSelectLink(zone) {
|
||||
if (openLinkPicker) {
|
||||
openLinkPicker(({ url, title, raw }) => {
|
||||
if (url) zone.url = url;
|
||||
if (!zone.title && title) {
|
||||
zone.title = title;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入跳转链接地址(如 /goodsList?category=1 或外部网址):', '设置跳转链接', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: zone.url || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '链接地址不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
zone.url = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 上移/下移热区
|
||||
function moveZone(index, direction) {
|
||||
const newIdx = index + direction;
|
||||
if (!Array.isArray(props.options.zones)) return;
|
||||
if (newIdx < 0 || newIdx >= props.options.zones.length) return;
|
||||
const temp = props.options.zones[index];
|
||||
props.options.zones[index] = props.options.zones[newIdx];
|
||||
props.options.zones[newIdx] = temp;
|
||||
}
|
||||
|
||||
// 4. 添加热区
|
||||
function handleAddZone() {
|
||||
if (!Array.isArray(props.options.zones)) props.options.zones = [];
|
||||
const count = props.options.zones.length;
|
||||
props.options.zones.push({
|
||||
title: `热区 ${count + 1}`,
|
||||
url: '/goodsList',
|
||||
top: Math.min(10 + count * 5, 60),
|
||||
left: Math.min(10 + (count % 3) * 30, 70),
|
||||
width: 25,
|
||||
height: 35
|
||||
});
|
||||
}
|
||||
|
||||
const hotzoneDialogRef = ref(null);
|
||||
|
||||
// 绘制热区弹窗打开
|
||||
function handleOpenHotzone() {
|
||||
const imgUrl = props.options.imageUrl || '';
|
||||
if (!imgUrl) {
|
||||
ElMessage.warning('请先输入或选择热区底图');
|
||||
return;
|
||||
}
|
||||
const zoneInfo = (props.options.zones || []).map((z, idx) => {
|
||||
const leftPer = z.leftPer !== undefined ? z.leftPer : (Number(z.left) || 0) / 100;
|
||||
const topPer = z.topPer !== undefined ? z.topPer : (Number(z.top) || 0) / 100;
|
||||
const widthPer = z.widthPer !== undefined ? z.widthPer : (Number(z.width) || 20) / 100;
|
||||
const heightPer = z.heightPer !== undefined ? z.heightPer : (Number(z.height) || 20) / 100;
|
||||
const link = z.url || z.link || '';
|
||||
|
||||
return {
|
||||
...z,
|
||||
leftPer,
|
||||
topPer,
|
||||
widthPer,
|
||||
heightPer,
|
||||
link,
|
||||
title: z.title || `热区 ${idx + 1}`,
|
||||
type: z.type || 'other'
|
||||
};
|
||||
});
|
||||
|
||||
if (hotzoneDialogRef.value) {
|
||||
hotzoneDialogRef.value.open({
|
||||
img: imgUrl,
|
||||
zoneInfo
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 热区绘制器变更回调
|
||||
function handleChangeZone(zoneList) {
|
||||
if (!Array.isArray(zoneList)) return;
|
||||
props.options.zones = zoneList.map((z, idx) => {
|
||||
const leftVal = z.leftPer !== undefined ? z.leftPer * 100 : (z.left !== undefined ? z.left : 0);
|
||||
const topVal = z.topPer !== undefined ? z.topPer * 100 : (z.top !== undefined ? z.top : 0);
|
||||
const widthVal = z.widthPer !== undefined ? z.widthPer * 100 : (z.width !== undefined ? z.width : 20);
|
||||
const heightVal = z.heightPer !== undefined ? z.heightPer * 100 : (z.height !== undefined ? z.height : 20);
|
||||
const linkVal = z.link || z.url || '';
|
||||
const titleVal = z.title || `热区 ${idx + 1}`;
|
||||
|
||||
return {
|
||||
...z,
|
||||
title: titleVal,
|
||||
url: linkVal,
|
||||
link: linkVal,
|
||||
left: Math.round(leftVal * 10) / 10,
|
||||
top: Math.round(topVal * 10) / 10,
|
||||
width: Math.round(widthVal * 10) / 10,
|
||||
height: Math.round(heightVal * 10) / 10,
|
||||
type: z.type || 'other'
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.panel-hot-zone {
|
||||
.panel-section {
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px dashed #e2e8f0;
|
||||
|
||||
.section-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
margin-bottom: 12px;
|
||||
|
||||
&.flex-between {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.form-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
|
||||
.item-label {
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
}
|
||||
}
|
||||
|
||||
.input-with-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.action-picker-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.hotzone-preview-box {
|
||||
margin-top: 10px;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
background: #f8fafc;
|
||||
|
||||
.img-preview {
|
||||
width: 100%;
|
||||
max-height: 140px;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.preview-actions-bar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 10px;
|
||||
background: #f1f5f9;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.zones-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
.zone-card-item {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #cbd5e1;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.zone-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #edf2f7;
|
||||
|
||||
.zone-idx-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #334155;
|
||||
|
||||
.zone-num-badge {
|
||||
background: #409EFF;
|
||||
color: #ffffff;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.zone-header-btns {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.zone-body-inputs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
.field-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
.field-lbl {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: #64748b;
|
||||
}
|
||||
}
|
||||
|
||||
.link-picker-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.link-prefix-icon {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.action-picker-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 0 10px;
|
||||
|
||||
&.link-btn {
|
||||
background: #ecf5ff;
|
||||
border-color: #b3d8ff;
|
||||
color: #409EFF;
|
||||
|
||||
&:hover {
|
||||
background: #409EFF;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link-current-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 11px;
|
||||
background: #eff6ff;
|
||||
border: 1px solid #dbeafe;
|
||||
border-radius: 4px;
|
||||
padding: 2px 8px;
|
||||
margin-top: 2px;
|
||||
overflow: hidden;
|
||||
|
||||
.tag-label {
|
||||
color: #3b82f6;
|
||||
flex-shrink: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tag-val {
|
||||
color: #1e40af;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-family: monospace;
|
||||
}
|
||||
}
|
||||
|
||||
.coord-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 6px;
|
||||
|
||||
.coord-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 4px;
|
||||
padding: 2px 6px;
|
||||
|
||||
.coord-lbl {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #64748b;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.coord-input {
|
||||
width: 100%;
|
||||
|
||||
:deep(.el-input__wrapper) {
|
||||
padding: 0 2px;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
:deep(.el-input__inner) {
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-zones-box {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
border: 1px dashed #cbd5e1;
|
||||
border-radius: 6px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.mr-4 {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
521
seller/src/views/shop/pc/config-panel/PanelNotice.vue
Normal file
521
seller/src/views/shop/pc/config-panel/PanelNotice.vue
Normal file
@@ -0,0 +1,521 @@
|
||||
<template>
|
||||
<div class="panel-notice-bar">
|
||||
<!-- 1. 基础配置与图标设置 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">快讯标语与图标</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">前缀标语</span>
|
||||
<el-input v-model="options.title" size="small" placeholder="如:公告快讯 / 今日热点" clearable />
|
||||
</div>
|
||||
|
||||
<!-- 徽标图标 / 图片选择器 -->
|
||||
<div class="form-item mt-10">
|
||||
<span class="item-label">前缀徽标图标 (可选自定义图片)</span>
|
||||
<div class="badge-icon-picker-row">
|
||||
<div class="icon-thumb-preview" @click="handleSelectBadgeImg" title="点击选择/更换徽标图片">
|
||||
<img v-if="options.badgeImg || options.icon" :src="options.badgeImg || options.icon" class="badge-img" alt="" />
|
||||
<div v-else class="icon-placeholder">
|
||||
<el-icon><Picture /></el-icon>
|
||||
<span>选图</span>
|
||||
</div>
|
||||
<div class="hover-overlay" v-if="options.badgeImg || options.icon">换图</div>
|
||||
</div>
|
||||
<div class="icon-input-wrap">
|
||||
<div class="input-btn-row">
|
||||
<el-input
|
||||
v-model="badgeImgVal"
|
||||
size="small"
|
||||
placeholder="留空展示默认喇叭图标"
|
||||
clearable
|
||||
/>
|
||||
<el-button size="small" class="action-picker-btn" @click="handleSelectBadgeImg">
|
||||
<el-icon class="mr-4"><Picture /></el-icon>
|
||||
<span>选择图片</span>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="tip-sub-text">建议尺寸 24x24 或 32x32px 正方形小图标</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-item flex-row mt-10">
|
||||
<span class="item-label">展示“更多”入口</span>
|
||||
<el-switch v-model="options.showMore" size="small" />
|
||||
</div>
|
||||
|
||||
<template v-if="options.showMore !== false">
|
||||
<div class="form-item mt-10">
|
||||
<span class="item-label">“更多”文案</span>
|
||||
<el-input v-model="options.moreText" size="small" placeholder="如:更多" clearable />
|
||||
</div>
|
||||
<div class="form-item mt-10">
|
||||
<span class="item-label">“更多”跳转链接</span>
|
||||
<div class="link-picker-wrap">
|
||||
<el-input v-model="options.moreUrl" size="small" placeholder="如:/article 或文章列表" clearable>
|
||||
<template #prefix>
|
||||
<el-icon class="link-input-icon"><Link /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
size="small"
|
||||
class="action-picker-btn link-btn"
|
||||
@click="handleSelectMoreLink"
|
||||
>
|
||||
<el-icon class="mr-4"><Link /></el-icon>
|
||||
<span>选择链接</span>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="link-current-tag" v-if="options.moreUrl">
|
||||
<span class="tag-label">当前链接:</span>
|
||||
<span class="tag-val" :title="options.moreUrl">{{ options.moreUrl }}</span>
|
||||
<el-icon class="url-clear-icon" title="清除链接" @click.stop="options.moreUrl = ''"><Close /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 2. 快讯条目列表 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title flex-between">
|
||||
<span>快讯条目 ({{ itemsList?.length || 0 }} 条)</span>
|
||||
<el-button link type="primary" size="small" @click="handleAddItem">
|
||||
<el-icon><Plus /></el-icon> 添加快讯
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="notice-items-container">
|
||||
<div
|
||||
v-for="(item, idx) in itemsList"
|
||||
:key="idx"
|
||||
class="notice-card-item"
|
||||
>
|
||||
<div class="item-top-row">
|
||||
<el-select v-model="item.tag" size="small" style="width: 86px" filterable allow-create placeholder="标签">
|
||||
<el-option label="最新" value="最新" />
|
||||
<el-option label="活动" value="活动" />
|
||||
<el-option label="通知" value="通知" />
|
||||
<el-option label="热议" value="热议" />
|
||||
</el-select>
|
||||
<el-input v-model="item.time" size="small" placeholder="时间(如 10-28)" style="width: 100px" />
|
||||
<div class="item-actions">
|
||||
<el-button
|
||||
link
|
||||
size="small"
|
||||
:disabled="idx === 0"
|
||||
@click="moveItem(idx, -1)"
|
||||
title="上移"
|
||||
>
|
||||
<el-icon><Top /></el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
size="small"
|
||||
:disabled="idx === itemsList.length - 1"
|
||||
@click="moveItem(idx, 1)"
|
||||
title="下移"
|
||||
>
|
||||
<el-icon><Bottom /></el-icon>
|
||||
</el-button>
|
||||
<el-button link type="danger" size="small" @click="itemsList.splice(idx, 1)" title="删除">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item-title-row mt-6">
|
||||
<el-input v-model="item.title" size="small" placeholder="快讯内容标题" clearable />
|
||||
</div>
|
||||
|
||||
<div class="item-link-row mt-6">
|
||||
<div class="link-picker-wrap">
|
||||
<el-input v-model="item.url" size="small" placeholder="跳转路径(如: /article/101 或文章链接)" clearable>
|
||||
<template #prefix>
|
||||
<el-icon class="link-input-icon"><Link /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
size="small"
|
||||
class="action-picker-btn link-btn"
|
||||
@click="handleSelectItemLink(item)"
|
||||
>
|
||||
<el-icon class="mr-4"><Link /></el-icon>
|
||||
<span>选链接</span>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="link-current-tag" v-if="item.url">
|
||||
<span class="tag-label">当前链接:</span>
|
||||
<span class="tag-val" :title="item.url">{{ item.url }}</span>
|
||||
<el-icon class="url-clear-icon" title="清除链接" @click.stop="item.url = ''"><Close /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, inject } from 'vue';
|
||||
import { Plus, Delete, Top, Bottom, Link, Picture, Close } from '@element-plus/icons-vue';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
// 全局选择器注入
|
||||
const openImagePicker = inject('openImagePicker', null);
|
||||
const openLinkPicker = inject('openLinkPicker', null);
|
||||
|
||||
if (!props.options.title) props.options.title = '公告快讯';
|
||||
if (props.options.showMore === undefined) props.options.showMore = true;
|
||||
if (!props.options.moreText) props.options.moreText = '更多';
|
||||
if (!props.options.moreUrl) props.options.moreUrl = '/article';
|
||||
|
||||
const badgeImgVal = computed({
|
||||
get() {
|
||||
return props.options.badgeImg || props.options.icon || '';
|
||||
},
|
||||
set(v) {
|
||||
props.options.badgeImg = v;
|
||||
props.options.icon = v;
|
||||
}
|
||||
});
|
||||
|
||||
// 统一 items 数据源
|
||||
if (!props.options.items && !props.options.notices) {
|
||||
props.options.items = [
|
||||
{ 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' }
|
||||
];
|
||||
} else if (!props.options.items && props.options.notices) {
|
||||
props.options.items = props.options.notices.map(n => ({
|
||||
tag: n.tag || '最新',
|
||||
title: n.title || n.text || '',
|
||||
time: n.time || '10-28',
|
||||
url: n.url || '/article'
|
||||
}));
|
||||
}
|
||||
|
||||
const itemsList = computed(() => {
|
||||
return props.options.items;
|
||||
});
|
||||
|
||||
// 1. 选择前缀徽标图片
|
||||
function handleSelectBadgeImg() {
|
||||
if (openImagePicker) {
|
||||
openImagePicker((url) => {
|
||||
if (url) {
|
||||
props.options.badgeImg = url;
|
||||
props.options.icon = url;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入快讯徽标图片 URL:', '选择快讯图标', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: props.options.badgeImg || props.options.icon || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '图片地址不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
props.options.badgeImg = value.trim();
|
||||
props.options.icon = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 选择“更多”链接
|
||||
function handleSelectMoreLink() {
|
||||
if (openLinkPicker) {
|
||||
openLinkPicker(({ url }) => {
|
||||
if (url) props.options.moreUrl = url;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入“更多”跳转链接:', '设置跳转链接', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: props.options.moreUrl || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '链接不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
props.options.moreUrl = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 选择单条快讯链接
|
||||
function handleSelectItemLink(item) {
|
||||
if (openLinkPicker) {
|
||||
openLinkPicker(({ url, title }) => {
|
||||
if (url) item.url = url;
|
||||
if (!item.title && title) item.title = title;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入快讯跳转链接:', '设置快讯链接', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: item.url || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '链接不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
item.url = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
function handleAddItem() {
|
||||
props.options.items.push({
|
||||
tag: '最新',
|
||||
title: '新发布商城系统通知公告',
|
||||
time: '刚刚',
|
||||
url: '/article'
|
||||
});
|
||||
}
|
||||
|
||||
function moveItem(index, step) {
|
||||
const target = index + step;
|
||||
if (target < 0 || target >= props.options.items.length) return;
|
||||
const temp = props.options.items[index];
|
||||
props.options.items[index] = props.options.items[target];
|
||||
props.options.items[target] = temp;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.panel-notice-bar {
|
||||
padding: 8px 4px;
|
||||
|
||||
.panel-section {
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px dashed #e2e8f0;
|
||||
|
||||
.section-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
margin-bottom: 10px;
|
||||
|
||||
&.flex-between {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.form-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
|
||||
&.flex-row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.item-label {
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mt-6 {
|
||||
margin-top: 6px;
|
||||
}
|
||||
.mt-10 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.mr-4 {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.badge-icon-picker-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
|
||||
.icon-thumb-preview {
|
||||
position: relative;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #cbd5e1;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
|
||||
.badge-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.icon-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #e2e8f0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
color: #64748b;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.hover-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(15, 23, 42, 0.65);
|
||||
color: #ffffff;
|
||||
font-size: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
&:hover .hover-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-input-wrap {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
.input-btn-row {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.tip-sub-text {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link-picker-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.link-input-icon {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.action-picker-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 0 10px;
|
||||
|
||||
&.link-btn {
|
||||
background: #ecf5ff;
|
||||
border-color: #b3d8ff;
|
||||
color: #409eff;
|
||||
|
||||
&:hover {
|
||||
background: #409eff;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link-current-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 11px;
|
||||
background: #eff6ff;
|
||||
border: 1px solid #dbeafe;
|
||||
border-radius: 4px;
|
||||
padding: 3px 8px;
|
||||
margin-top: 4px;
|
||||
overflow: hidden;
|
||||
|
||||
.tag-label {
|
||||
color: #3b82f6;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tag-val {
|
||||
color: #1e40af;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.url-clear-icon {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
color: #ef4444;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notice-items-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
max-height: 420px;
|
||||
overflow-y: auto;
|
||||
|
||||
.notice-card-item {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
|
||||
.item-top-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.item-actions {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
484
seller/src/views/shop/pc/config-panel/PanelPromoGroup.vue
Normal file
484
seller/src/views/shop/pc/config-panel/PanelPromoGroup.vue
Normal file
@@ -0,0 +1,484 @@
|
||||
<template>
|
||||
<div class="panel-promo-group">
|
||||
<!-- 1. 排版列数 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">排版列数</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">卡片列数</span>
|
||||
<el-radio-group v-model="options.columns" size="small">
|
||||
<el-radio-button :label="2">2 列</el-radio-button>
|
||||
<el-radio-button :label="3">3 列</el-radio-button>
|
||||
<el-radio-button :label="4">4 列</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. 活动卡片列表 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title flex-between">
|
||||
<span>活动卡片列表 ({{ options.cards?.length || 0 }})</span>
|
||||
<el-button link type="primary" size="small" @click="handleAddCard">
|
||||
<el-icon><Plus /></el-icon> 添加卡片
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="cards-list" v-if="Array.isArray(options.cards) && options.cards.length">
|
||||
<div
|
||||
v-for="(card, idx) in options.cards"
|
||||
:key="idx"
|
||||
class="promo-card-item"
|
||||
>
|
||||
<!-- 顶部序号与工具按钮 -->
|
||||
<div class="card-header">
|
||||
<span class="card-idx-badge">卡片 {{ idx + 1 }}{{ card.title ? ` · ${card.title}` : '' }}</span>
|
||||
<div class="card-actions">
|
||||
<el-button
|
||||
link
|
||||
size="small"
|
||||
:disabled="idx === 0"
|
||||
@click="moveCard(idx, -1)"
|
||||
title="上移"
|
||||
>
|
||||
<el-icon><Top /></el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
size="small"
|
||||
:disabled="idx === options.cards.length - 1"
|
||||
@click="moveCard(idx, 1)"
|
||||
title="下移"
|
||||
>
|
||||
<el-icon><Bottom /></el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="options.cards.splice(idx, 1)"
|
||||
title="删除卡片"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 卡片属性输入 -->
|
||||
<div class="card-inputs">
|
||||
<!-- 标题行 -->
|
||||
<div class="input-row">
|
||||
<span class="field-label">主标题</span>
|
||||
<el-input v-model="card.title" size="small" placeholder="如:限时秒杀" clearable />
|
||||
</div>
|
||||
|
||||
<!-- 副标题行 -->
|
||||
<div class="input-row">
|
||||
<span class="field-label">副标题</span>
|
||||
<el-input v-model="card.subtitle" size="small" placeholder="如:抢券立减50元 · 爆款直降" clearable />
|
||||
</div>
|
||||
|
||||
<!-- 卡片底图 (图片选择器) -->
|
||||
<div class="input-row">
|
||||
<span class="field-label">卡片底图</span>
|
||||
<div class="picker-flex-wrap">
|
||||
<!-- 缩略图预览与快捷替换 -->
|
||||
<div
|
||||
class="thumb-preview-box"
|
||||
@click="handleSelectImage(card)"
|
||||
:title="card.bgImg ? '点击更换底图' : '点击上传底图'"
|
||||
>
|
||||
<template v-if="card.bgImg">
|
||||
<img :src="card.bgImg" alt="底图" class="preview-img" />
|
||||
<span class="thumb-clear-btn" @click.stop="card.bgImg = ''" title="清除图片">
|
||||
<el-icon><Close /></el-icon>
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-icon class="thumb-empty-icon"><Picture /></el-icon>
|
||||
<span class="thumb-empty-tip">选图</span>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- URL 输入框与选择图片按钮 -->
|
||||
<div class="input-with-action">
|
||||
<el-input
|
||||
v-model="card.bgImg"
|
||||
size="small"
|
||||
placeholder="底图 URL,可直接粘贴或选择"
|
||||
clearable
|
||||
/>
|
||||
<el-button
|
||||
size="small"
|
||||
class="action-picker-btn"
|
||||
@click="handleSelectImage(card)"
|
||||
>
|
||||
<el-icon class="mr-4"><Picture /></el-icon>
|
||||
<span>选择图片</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 跳转链接 (链接选择器) -->
|
||||
<div class="input-row">
|
||||
<span class="field-label">跳转链接</span>
|
||||
<div class="link-picker-wrap">
|
||||
<el-input
|
||||
v-model="card.url"
|
||||
size="small"
|
||||
placeholder="跳转链接(如 /goodsList?seckill=1)"
|
||||
clearable
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon class="link-input-icon"><Link /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
size="small"
|
||||
class="action-picker-btn link-btn"
|
||||
@click="handleSelectLink(card)"
|
||||
>
|
||||
<el-icon class="mr-4"><Link /></el-icon>
|
||||
<span>选择链接</span>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="link-current-tag" v-if="card.url">
|
||||
<span class="tag-label">当前链接:</span>
|
||||
<span class="tag-val" :title="card.url">{{ card.url }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="empty-cards-tip" v-else>
|
||||
<span>暂无活动卡片,请点击上方“添加卡片”</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject } from 'vue';
|
||||
import { Plus, Delete, Link, Picture, Close, Top, Bottom } from '@element-plus/icons-vue';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
// 注入来自 pc/index.vue 的全局选择器
|
||||
const openImagePicker = inject('openImagePicker', null);
|
||||
const openLinkPicker = inject('openLinkPicker', null);
|
||||
|
||||
// 1. 选择卡片背景图片
|
||||
function handleSelectImage(card) {
|
||||
if (openImagePicker) {
|
||||
openImagePicker((url) => {
|
||||
if (url) card.bgImg = url;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入卡片背景图 URL 地址:', '选择卡片底图', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: card.bgImg || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '图片地址不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
card.bgImg = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 选择跳转链接
|
||||
function handleSelectLink(card) {
|
||||
if (openLinkPicker) {
|
||||
openLinkPicker(({ url, title, raw }) => {
|
||||
if (url) card.url = url;
|
||||
if (!card.title && title) {
|
||||
card.title = title;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入跳转链接(如 /goodsList?seckill=1 或完整网址):', '设置跳转链接', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: card.url || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '链接不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
card.url = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 上移/下移卡片
|
||||
function moveCard(index, direction) {
|
||||
const newIdx = index + direction;
|
||||
if (!Array.isArray(props.options.cards)) return;
|
||||
if (newIdx < 0 || newIdx >= props.options.cards.length) return;
|
||||
const temp = props.options.cards[index];
|
||||
props.options.cards[index] = props.options.cards[newIdx];
|
||||
props.options.cards[newIdx] = temp;
|
||||
}
|
||||
|
||||
// 4. 添加卡片
|
||||
function handleAddCard() {
|
||||
if (!Array.isArray(props.options.cards)) props.options.cards = [];
|
||||
props.options.cards.push({
|
||||
title: '大促专场',
|
||||
subtitle: '限量特惠抢购',
|
||||
bgImg: '',
|
||||
url: '/goodsList'
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.panel-promo-group {
|
||||
.panel-section {
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px dashed #e2e8f0;
|
||||
|
||||
.section-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
margin-bottom: 12px;
|
||||
|
||||
&.flex-between {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.form-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.item-label {
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cards-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
|
||||
.promo-card-item {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #cbd5e1;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #edf2f7;
|
||||
|
||||
.card-idx-badge {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-inputs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
.input-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
.field-label {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: #64748b;
|
||||
}
|
||||
}
|
||||
|
||||
.picker-flex-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.thumb-preview-box {
|
||||
position: relative;
|
||||
width: 58px;
|
||||
height: 40px;
|
||||
border-radius: 4px;
|
||||
border: 1px dashed #cbd5e1;
|
||||
background: #ffffff;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
transition: border-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #409EFF;
|
||||
}
|
||||
|
||||
.preview-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.thumb-empty-icon {
|
||||
font-size: 15px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.thumb-empty-tip {
|
||||
font-size: 10px;
|
||||
color: #94a3b8;
|
||||
line-height: 1;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.thumb-clear-btn {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
background: rgba(0, 0, 0, 0.65);
|
||||
color: #ffffff;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: #ef4444;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input-with-action {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.action-picker-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link-picker-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.link-input-icon {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.action-picker-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 0 10px;
|
||||
|
||||
&.link-btn {
|
||||
background: #ecf5ff;
|
||||
border-color: #b3d8ff;
|
||||
color: #409EFF;
|
||||
|
||||
&:hover {
|
||||
background: #409EFF;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link-current-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 11px;
|
||||
background: #eff6ff;
|
||||
border: 1px solid #dbeafe;
|
||||
border-radius: 4px;
|
||||
padding: 2px 8px;
|
||||
margin-top: 2px;
|
||||
overflow: hidden;
|
||||
|
||||
.tag-label {
|
||||
color: #3b82f6;
|
||||
flex-shrink: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tag-val {
|
||||
color: #1e40af;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-family: monospace;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-cards-tip {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
border: 1px dashed #cbd5e1;
|
||||
border-radius: 6px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.mr-4 {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
284
seller/src/views/shop/pc/config-panel/PanelRecommendFeed.vue
Normal file
284
seller/src/views/shop/pc/config-panel/PanelRecommendFeed.vue
Normal file
@@ -0,0 +1,284 @@
|
||||
<template>
|
||||
<div class="panel-recommend-feed">
|
||||
<!-- 1. 标题设置 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">标题设置</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">主标题</span>
|
||||
<el-input v-model="options.title" size="small" placeholder="如:猜你喜欢" />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">副标题</span>
|
||||
<el-input v-model="options.subtitle" size="small" placeholder="如:真实推荐商品流" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. 布局样式 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">布局样式</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">展现形态</span>
|
||||
<el-radio-group v-model="options.layoutStyle" size="small">
|
||||
<el-radio-button label="featured">精选大卡+网格 (推荐)</el-radio-button>
|
||||
<el-radio-button label="grid">标准推荐网格</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">背景色</span>
|
||||
<el-color-picker v-model="options.backgroundColor" size="small" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3. 加载与数量控制 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">推荐流加载设置</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">首屏/每页数量</span>
|
||||
<el-input-number v-model="options.pageSize" :min="4" :max="20" size="small" controls-position="right" />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">最多加载页数</span>
|
||||
<el-input-number v-model="options.maxPages" :min="1" :max="10" size="small" controls-position="right" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 4. 推荐商品池管理 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title flex-between">
|
||||
<span>推荐商品池 ({{ options.goods?.length || 0 }} 件)</span>
|
||||
<div class="btn-group">
|
||||
<el-button v-if="options.goods?.length" link type="danger" size="small" @click="options.goods = []">
|
||||
清空
|
||||
</el-button>
|
||||
<el-button type="primary" size="small" @click="handleOpenGoodsPicker">
|
||||
选择商品
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="goods-list-preview" v-if="options.goods?.length">
|
||||
<div v-for="(g, idx) in options.goods" :key="g.id || idx" class="goods-item-row">
|
||||
<span class="idx-tag">{{ idx === 0 ? '精选' : idx + 1 }}</span>
|
||||
<img :src="g.thumbnail || defaultThumb" class="g-thumb" alt="" />
|
||||
<div class="g-info">
|
||||
<span class="g-name" :title="g.goodsName">{{ g.goodsName }}</span>
|
||||
<span class="g-price">¥{{ g.price }}</span>
|
||||
</div>
|
||||
<el-button link type="danger" size="small" @click="options.goods.splice(idx, 1)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="goods-empty-tip" v-else>
|
||||
<span>暂未选择特定商品,前台将展示系统推荐商品</span>
|
||||
<el-button type="primary" link size="small" @click="handleOpenGoodsPicker">
|
||||
+ 点击添加商品
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 兜底商品选择弹窗(当未注入全局 openGoodsPicker 时生效) -->
|
||||
<el-dialog
|
||||
v-if="!openGoodsPicker && showModal"
|
||||
v-model="showModal"
|
||||
title="从商品库选择商品"
|
||||
width="1160px"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
>
|
||||
<goods-dialog
|
||||
:selectedWay="tempSelectedGoods"
|
||||
@selected="(val) => { tempSelectedGoods = val; }"
|
||||
/>
|
||||
<template #footer>
|
||||
<el-button @click="showModal = false">取消</el-button>
|
||||
<el-button type="primary" @click="confirmGoodsSelected">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, inject } from 'vue';
|
||||
import GoodsDialog from '@/views/lili-dialog/goods-dialog.vue';
|
||||
import { Delete } from '@element-plus/icons-vue';
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const defaultThumb = 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_1.png';
|
||||
const showModal = ref(false);
|
||||
const openGoodsPicker = inject('openGoodsPicker', null);
|
||||
const tempSelectedGoods = ref([]);
|
||||
|
||||
if (!props.options.title) props.options.title = '猜你喜欢';
|
||||
if (!props.options.subtitle) props.options.subtitle = '真实推荐商品流';
|
||||
if (!props.options.layoutStyle) props.options.layoutStyle = 'featured';
|
||||
if (!props.options.backgroundColor) props.options.backgroundColor = '#f8fafc';
|
||||
if (!props.options.pageSize) props.options.pageSize = 8;
|
||||
if (!props.options.maxPages) props.options.maxPages = 3;
|
||||
if (!props.options.goods) {
|
||||
props.options.goods = [
|
||||
{ id: '1', goodsName: '旗舰 5G 智能手机 12GB+256GB 超光影影像系统', price: '3999.00', originalPrice: '4599.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_1.png' },
|
||||
{ id: '2', goodsName: '无线头戴式深度主动降噪蓝牙耳机 空间音频Hi-Res', price: '1099.00', originalPrice: '1499.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_2.png' },
|
||||
{ id: '3', goodsName: '14英寸 全面屏轻薄商务笔记本电脑 酷睿i7 高刷防眩光', price: '5699.00', originalPrice: '6299.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_3.png' },
|
||||
{ id: '4', goodsName: '运动健康专业监测智能手表 心率血氧长续航50米防水', price: '899.00', originalPrice: '1199.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_4.png' },
|
||||
{ id: '5', goodsName: '奢宠酵母焕活修护精华露 50ml 紧致淡纹抗皱滋养', price: '680.00', originalPrice: '890.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_5.png' },
|
||||
{ id: '6', goodsName: '意式半自动家用复古咖啡机 20Bar高压恒温萃取蒸汽奶泡', price: '899.00', originalPrice: '1280.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_6.png' },
|
||||
{ id: '7', goodsName: '碳板专业竞速轻量缓震跑步鞋 透气防滑男女同款潮鞋', price: '459.00', originalPrice: '699.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_7.png' },
|
||||
{ id: '8', goodsName: '极简静音无雾负离子香薰加湿器 大容量客厅卧室通用', price: '299.00', originalPrice: '399.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_8.png' }
|
||||
];
|
||||
}
|
||||
|
||||
function handleOpenGoodsPicker() {
|
||||
if (typeof openGoodsPicker === 'function') {
|
||||
openGoodsPicker((selectedGoods) => {
|
||||
applySelectedGoods(selectedGoods);
|
||||
}, props.options.goods || []);
|
||||
} else {
|
||||
tempSelectedGoods.value = Array.isArray(props.options.goods) ? [...props.options.goods] : [];
|
||||
showModal.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
function confirmGoodsSelected() {
|
||||
applySelectedGoods(tempSelectedGoods.value);
|
||||
showModal.value = false;
|
||||
}
|
||||
|
||||
function applySelectedGoods(selectedGoods) {
|
||||
if (Array.isArray(selectedGoods)) {
|
||||
props.options.goods = selectedGoods.map(g => ({
|
||||
id: g.id || g.skuId || g.goodsId,
|
||||
goodsId: g.goodsId,
|
||||
goodsName: g.goodsName || g.name,
|
||||
price: g.price,
|
||||
originalPrice: g.originalPrice || g.price,
|
||||
thumbnail: g.thumbnail || g.pic || defaultThumb
|
||||
}));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.panel-recommend-feed {
|
||||
padding: 8px 4px;
|
||||
|
||||
.panel-section {
|
||||
margin-bottom: 18px;
|
||||
padding-bottom: 14px;
|
||||
border-bottom: 1px dashed #e2e8f0;
|
||||
|
||||
.section-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
margin-bottom: 10px;
|
||||
|
||||
&.flex-between {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.item-label {
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
margin-right: 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.goods-list-preview {
|
||||
max-height: 260px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
|
||||
.goods-item-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: #f8fafc;
|
||||
padding: 6px 8px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #f1f5f9;
|
||||
|
||||
.idx-tag {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #64748b;
|
||||
background: #e2e8f0;
|
||||
padding: 1px 5px;
|
||||
border-radius: 3px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.g-thumb {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.g-info {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.g-name {
|
||||
font-size: 12px;
|
||||
color: #1e293b;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.g-price {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #e11d48;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.goods-empty-tip {
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
background: #f8fafc;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
410
seller/src/views/shop/pc/config-panel/PanelTitle.vue
Normal file
410
seller/src/views/shop/pc/config-panel/PanelTitle.vue
Normal file
@@ -0,0 +1,410 @@
|
||||
<template>
|
||||
<div class="panel-title-bar">
|
||||
<div class="panel-section">
|
||||
<div class="section-title">标题内容设置</div>
|
||||
|
||||
<div class="form-item">
|
||||
<span class="item-label">主标题文案</span>
|
||||
<el-input v-model="options.mainTitle" size="small" placeholder="如:限时特卖" clearable />
|
||||
</div>
|
||||
|
||||
<!-- 主标题前缀图标 / 图片 -->
|
||||
<div class="form-item mt-10">
|
||||
<span class="item-label">标题图标 / 徽标图片 (可选)</span>
|
||||
<div class="title-icon-picker-row">
|
||||
<div class="icon-thumb-preview" @click="handleSelectTitleImg" title="点击选择/更换标题图标">
|
||||
<img v-if="titleImgVal" :src="titleImgVal" class="title-img" alt="" />
|
||||
<div v-else class="icon-placeholder">
|
||||
<el-icon><Picture /></el-icon>
|
||||
<span>选图</span>
|
||||
</div>
|
||||
<div class="hover-overlay" v-if="titleImgVal">换图</div>
|
||||
</div>
|
||||
<div class="icon-input-wrap">
|
||||
<div class="input-btn-row">
|
||||
<el-input
|
||||
v-model="titleImgVal"
|
||||
size="small"
|
||||
placeholder="请输入或选择标题前缀图标"
|
||||
clearable
|
||||
/>
|
||||
<el-button size="small" class="action-picker-btn" @click="handleSelectTitleImg">
|
||||
<el-icon class="mr-4"><Picture /></el-icon>
|
||||
<span>选择图片</span>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="tip-sub-text">建议尺寸 32x32 或 48x48px,展示在标题前缀</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主标题跳转链接 -->
|
||||
<div class="form-item mt-10">
|
||||
<span class="item-label">主标题点击跳转链接 (可选)</span>
|
||||
<div class="link-picker-wrap">
|
||||
<el-input
|
||||
v-model="options.titleUrl"
|
||||
size="small"
|
||||
placeholder="如 /goods 或活动专区,留空不可点击"
|
||||
clearable
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon class="link-input-icon"><Link /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
size="small"
|
||||
class="action-picker-btn link-btn"
|
||||
@click="handleSelectTitleLink"
|
||||
>
|
||||
<el-icon class="mr-4"><Link /></el-icon>
|
||||
<span>选择链接</span>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="link-current-tag" v-if="options.titleUrl">
|
||||
<span class="tag-label">当前链接:</span>
|
||||
<span class="tag-val" :title="options.titleUrl">{{ options.titleUrl }}</span>
|
||||
<el-icon class="url-clear-icon" title="清除链接" @click.stop="options.titleUrl = ''"><Close /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-item mt-10">
|
||||
<span class="item-label">副标题文字</span>
|
||||
<el-input v-model="options.subtitle" size="small" placeholder="如:精选爆款 每日早十点上新" clearable />
|
||||
</div>
|
||||
|
||||
<div class="form-item flex-row mt-10">
|
||||
<span class="item-label">对齐方式</span>
|
||||
<el-radio-group v-model="options.align" size="small">
|
||||
<el-radio-button label="left">靠左对齐</el-radio-button>
|
||||
<el-radio-button label="center">居中对齐</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
|
||||
<div class="form-item flex-row mt-10">
|
||||
<span class="item-label">显示右侧“查看更多”</span>
|
||||
<el-switch v-model="options.showMore" size="small" />
|
||||
</div>
|
||||
|
||||
<template v-if="options.showMore">
|
||||
<div class="form-item mt-10">
|
||||
<span class="item-label">更多链接文案</span>
|
||||
<el-input v-model="options.moreText" size="small" placeholder="查看更多 >" clearable />
|
||||
</div>
|
||||
|
||||
<div class="form-item mt-10">
|
||||
<span class="item-label">更多跳转地址</span>
|
||||
<div class="link-picker-wrap">
|
||||
<el-input
|
||||
v-model="options.moreUrl"
|
||||
size="small"
|
||||
placeholder="如 /goods 或活动专题"
|
||||
clearable
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon class="link-input-icon"><Link /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
size="small"
|
||||
class="action-picker-btn link-btn"
|
||||
@click="handleSelectMoreLink"
|
||||
>
|
||||
<el-icon class="mr-4"><Link /></el-icon>
|
||||
<span>选择链接</span>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="link-current-tag" v-if="options.moreUrl">
|
||||
<span class="tag-label">当前链接:</span>
|
||||
<span class="tag-val" :title="options.moreUrl">{{ options.moreUrl }}</span>
|
||||
<el-icon class="url-clear-icon" title="清除链接" @click.stop="options.moreUrl = ''"><Close /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, inject } from 'vue';
|
||||
import { Link, Picture, Close } from '@element-plus/icons-vue';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
// 注入来自 pc/index.vue 的全局选择器
|
||||
const openImagePicker = inject('openImagePicker', null);
|
||||
const openLinkPicker = inject('openLinkPicker', null);
|
||||
|
||||
const titleImgVal = computed({
|
||||
get() {
|
||||
return props.options.titleImg || props.options.icon || '';
|
||||
},
|
||||
set(v) {
|
||||
props.options.titleImg = v;
|
||||
props.options.icon = v;
|
||||
}
|
||||
});
|
||||
|
||||
// 1. 选择前缀图片
|
||||
function handleSelectTitleImg() {
|
||||
if (openImagePicker) {
|
||||
openImagePicker((url) => {
|
||||
if (url) {
|
||||
props.options.titleImg = url;
|
||||
props.options.icon = url;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入标题图标图片 URL:', '选择标题图标', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: props.options.titleImg || props.options.icon || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '图片地址不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
props.options.titleImg = value.trim();
|
||||
props.options.icon = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 选择主标题跳转链接
|
||||
function handleSelectTitleLink() {
|
||||
if (openLinkPicker) {
|
||||
openLinkPicker(({ url }) => {
|
||||
if (url) props.options.titleUrl = url;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入主标题跳转链接:', '设置标题链接', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: props.options.titleUrl || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '链接不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
props.options.titleUrl = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 选择“查看更多”链接
|
||||
function handleSelectMoreLink() {
|
||||
if (openLinkPicker) {
|
||||
openLinkPicker(({ url }) => {
|
||||
if (url) props.options.moreUrl = url;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入“查看更多”跳转链接:', '设置更多链接', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: props.options.moreUrl || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '链接不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
props.options.moreUrl = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.panel-title-bar {
|
||||
padding: 8px 4px;
|
||||
|
||||
.panel-section {
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 16px;
|
||||
|
||||
.section-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
|
||||
&.flex-row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.item-label {
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mt-10 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.mr-4 {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.title-icon-picker-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
|
||||
.icon-thumb-preview {
|
||||
position: relative;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #cbd5e1;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
|
||||
.title-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.icon-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #e2e8f0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
color: #64748b;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.hover-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(15, 23, 42, 0.65);
|
||||
color: #ffffff;
|
||||
font-size: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
&:hover .hover-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-input-wrap {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
.input-btn-row {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.tip-sub-text {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link-picker-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.link-input-icon {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.action-picker-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 0 10px;
|
||||
|
||||
&.link-btn {
|
||||
background: #ecf5ff;
|
||||
border-color: #b3d8ff;
|
||||
color: #409eff;
|
||||
|
||||
&:hover {
|
||||
background: #409eff;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link-current-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 11px;
|
||||
background: #eff6ff;
|
||||
border: 1px solid #dbeafe;
|
||||
border-radius: 4px;
|
||||
padding: 3px 8px;
|
||||
margin-top: 4px;
|
||||
overflow: hidden;
|
||||
|
||||
.tag-label {
|
||||
color: #3b82f6;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tag-val {
|
||||
color: #1e40af;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.url-clear-icon {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
color: #ef4444;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
473
seller/src/views/shop/pc/config-panel/PanelVideo.vue
Normal file
473
seller/src/views/shop/pc/config-panel/PanelVideo.vue
Normal file
@@ -0,0 +1,473 @@
|
||||
<template>
|
||||
<div class="panel-video">
|
||||
<!-- 1. 媒体资源设置 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">视频源与媒体文件</div>
|
||||
|
||||
<!-- 视频文件选择 -->
|
||||
<div class="form-item">
|
||||
<span class="item-label">视频文件 URL (.mp4)</span>
|
||||
<div class="input-with-action">
|
||||
<el-input
|
||||
v-model="options.videoUrl"
|
||||
size="small"
|
||||
placeholder="请输入在线视频地址或点击右侧选择"
|
||||
clearable
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon class="field-icon"><VideoCamera /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
class="action-picker-btn"
|
||||
@click="handleSelectVideo"
|
||||
>
|
||||
<el-icon class="mr-4"><VideoCamera /></el-icon>
|
||||
<span>选择视频</span>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="source-status-tag" v-if="options.videoUrl">
|
||||
<span class="status-dot"></span>
|
||||
<span class="status-text" :title="options.videoUrl">已绑定视频: {{ getMediaName(options.videoUrl) }}</span>
|
||||
<span class="status-clear-btn" @click="options.videoUrl = ''" title="清除视频">清空</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 视频封面图选择 -->
|
||||
<div class="form-item mt-14">
|
||||
<span class="item-label">视频封面图片 (海报/底图)</span>
|
||||
<div class="picker-flex-wrap">
|
||||
<!-- 缩略图预览与快捷替换 -->
|
||||
<div
|
||||
class="thumb-preview-box"
|
||||
@click="handleSelectCover"
|
||||
:title="options.coverUrl ? '点击更换封面图' : '点击选择封面图'"
|
||||
>
|
||||
<template v-if="options.coverUrl">
|
||||
<img :src="options.coverUrl" alt="封面" class="preview-img" />
|
||||
<span class="thumb-clear-btn" @click.stop="options.coverUrl = ''" title="清除封面">
|
||||
<el-icon><Close /></el-icon>
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-icon class="thumb-empty-icon"><Picture /></el-icon>
|
||||
<span class="thumb-empty-tip">选图</span>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- URL 输入框与选择图片按钮 -->
|
||||
<div class="input-with-action flex-1">
|
||||
<el-input
|
||||
v-model="options.coverUrl"
|
||||
size="small"
|
||||
placeholder="请输入封面图 URL 或点击选择"
|
||||
clearable
|
||||
/>
|
||||
<el-button
|
||||
size="small"
|
||||
class="action-picker-btn"
|
||||
@click="handleSelectCover"
|
||||
>
|
||||
<el-icon class="mr-4"><Picture /></el-icon>
|
||||
<span>选择图片</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. 点击跳转链接 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">跳转与行动点 (CTA)</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">跳转链接 (选填,点击视频或行动点跳转)</span>
|
||||
<div class="link-picker-wrap">
|
||||
<el-input
|
||||
v-model="options.url"
|
||||
size="small"
|
||||
placeholder="如 /goodsList?seckill=1 或完整网址"
|
||||
clearable
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon class="link-input-icon"><Link /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
size="small"
|
||||
class="action-picker-btn link-btn"
|
||||
@click="handleSelectLink"
|
||||
>
|
||||
<el-icon class="mr-4"><Link /></el-icon>
|
||||
<span>选择链接</span>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="link-current-tag" v-if="options.url">
|
||||
<span class="tag-label">当前链接:</span>
|
||||
<span class="tag-val" :title="options.url">{{ options.url }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3. 播放与展示控制 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">播放与展示控制</div>
|
||||
|
||||
<div class="form-item flex-row">
|
||||
<span class="item-label">播放器高度 (px)</span>
|
||||
<el-input-number
|
||||
v-model="options.height"
|
||||
:min="200"
|
||||
:max="800"
|
||||
:step="20"
|
||||
size="small"
|
||||
controls-position="right"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-item flex-row">
|
||||
<span class="item-label">显示播放控制条</span>
|
||||
<el-switch v-model="options.controls" size="small" />
|
||||
</div>
|
||||
|
||||
<div class="form-item flex-row">
|
||||
<span class="item-label">自动静音播放</span>
|
||||
<el-switch v-model="options.autoPlay" size="small" />
|
||||
</div>
|
||||
|
||||
<div class="form-item flex-row">
|
||||
<span class="item-label">循环播放</span>
|
||||
<el-switch v-model="options.loop" size="small" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject } from 'vue';
|
||||
import { VideoCamera, Picture, Link, Close } from '@element-plus/icons-vue';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
// 注入来自 pc/index.vue 的全局选择器
|
||||
const openImagePicker = inject('openImagePicker', null);
|
||||
const openVideoPicker = inject('openVideoPicker', null);
|
||||
const openLinkPicker = inject('openLinkPicker', null);
|
||||
|
||||
// 提取 URL 中的文件名
|
||||
function getMediaName(url) {
|
||||
if (!url) return '';
|
||||
try {
|
||||
const cleanUrl = url.split('?')[0];
|
||||
const parts = cleanUrl.split('/');
|
||||
return parts[parts.length - 1] || url;
|
||||
} catch (e) {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 选择视频文件 (.mp4)
|
||||
function handleSelectVideo() {
|
||||
if (openVideoPicker) {
|
||||
openVideoPicker((url) => {
|
||||
if (url) props.options.videoUrl = url;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入在线视频 (.mp4) URL 地址:', '选择视频文件', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: props.options.videoUrl || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '视频地址不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
props.options.videoUrl = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 选择封面图片
|
||||
function handleSelectCover() {
|
||||
if (openImagePicker) {
|
||||
openImagePicker((url) => {
|
||||
if (url) props.options.coverUrl = url;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入视频封面图 URL 地址:', '选择视频封面', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: props.options.coverUrl || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '封面图地址不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
props.options.coverUrl = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 选择跳转链接
|
||||
function handleSelectLink() {
|
||||
if (openLinkPicker) {
|
||||
openLinkPicker(({ url }) => {
|
||||
if (url) props.options.url = url;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入跳转链接(如 /goodsList?seckill=1 或完整网址):', '设置跳转链接', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: props.options.url || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '链接不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
props.options.url = value.trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.panel-video {
|
||||
.panel-section {
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px dashed #e2e8f0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.mt-14 {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
&.flex-row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.item-label {
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mr-4 {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.flex-1 {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.field-icon {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.input-with-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
|
||||
.action-picker-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.source-status-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 11px;
|
||||
background: #f0fdf4;
|
||||
border: 1px solid #bbf7d0;
|
||||
color: #166534;
|
||||
border-radius: 4px;
|
||||
padding: 3px 8px;
|
||||
margin-top: 2px;
|
||||
overflow: hidden;
|
||||
|
||||
.status-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: #22c55e;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status-clear-btn {
|
||||
color: #dc2626;
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
padding-left: 4px;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.picker-flex-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.thumb-preview-box {
|
||||
position: relative;
|
||||
width: 58px;
|
||||
height: 40px;
|
||||
border-radius: 4px;
|
||||
border: 1px dashed #cbd5e1;
|
||||
background: #ffffff;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
transition: border-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #409eff;
|
||||
}
|
||||
|
||||
.preview-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.thumb-empty-icon {
|
||||
font-size: 15px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.thumb-empty-tip {
|
||||
font-size: 10px;
|
||||
color: #94a3b8;
|
||||
line-height: 1;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.thumb-clear-btn {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
background: rgba(0, 0, 0, 0.65);
|
||||
color: #ffffff;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: #ef4444;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link-picker-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.link-input-icon {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.action-picker-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 0 10px;
|
||||
|
||||
&.link-btn {
|
||||
background: #ecf5ff;
|
||||
border-color: #b3d8ff;
|
||||
color: #409eff;
|
||||
|
||||
&:hover {
|
||||
background: #409eff;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link-current-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 11px;
|
||||
background: #eff6ff;
|
||||
border: 1px solid #dbeafe;
|
||||
border-radius: 4px;
|
||||
padding: 3px 8px;
|
||||
margin-top: 4px;
|
||||
overflow: hidden;
|
||||
|
||||
.tag-label {
|
||||
color: #3b82f6;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tag-val {
|
||||
color: #1e40af;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
369
seller/src/views/shop/pc/config-panel/RowConfigPanel.vue
Normal file
369
seller/src/views/shop/pc/config-panel/RowConfigPanel.vue
Normal file
@@ -0,0 +1,369 @@
|
||||
<template>
|
||||
<div class="row-config-panel" v-if="row">
|
||||
<!-- 1. 行位置 -->
|
||||
<div class="setting-collapse-card">
|
||||
<div class="collapse-header" @click="expandOrder = !expandOrder">
|
||||
<div class="header-left">
|
||||
<el-icon class="arrow-icon" :class="{ 'is-expanded': expandOrder }"><ArrowRight /></el-icon>
|
||||
<span class="header-title">行位置</span>
|
||||
<el-tooltip content="调整当前行在整个页面中的上下层级与排序" placement="top">
|
||||
<el-icon class="help-icon"><QuestionFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="collapse-body" v-show="expandOrder">
|
||||
<!-- 上行:第 X 行 / 共 Y 行 + 置顶、置底 -->
|
||||
<div class="row-order-top">
|
||||
<div class="order-text-wrap">
|
||||
<div class="main-row-idx">第 {{ rowIndex + 1 }} 行</div>
|
||||
<div class="total-row-idx">共 {{ totalRows }} 行</div>
|
||||
</div>
|
||||
<div class="order-btns">
|
||||
<el-button
|
||||
size="small"
|
||||
class="pos-btn"
|
||||
:disabled="isFirst"
|
||||
@click="emit('move-top')"
|
||||
>
|
||||
置顶
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
class="pos-btn"
|
||||
:disabled="isLast"
|
||||
@click="emit('move-bottom')"
|
||||
>
|
||||
置底
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 下行:行切换下拉选择框 -->
|
||||
<div class="row-select-wrap">
|
||||
<el-select
|
||||
:model-value="rowIndex"
|
||||
@update:model-value="val => emit('move-to-row', val)"
|
||||
size="small"
|
||||
class="full-select"
|
||||
>
|
||||
<el-option
|
||||
v-for="(_, i) in totalRows"
|
||||
:key="i"
|
||||
:value="i"
|
||||
:label="`第 ${i + 1} 行`"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. 盒模型 -->
|
||||
<div class="setting-collapse-card">
|
||||
<div class="collapse-header" @click="expandBox = !expandBox">
|
||||
<div class="header-left">
|
||||
<el-icon class="arrow-icon" :class="{ 'is-expanded': expandBox }"><ArrowRight /></el-icon>
|
||||
<span class="header-title">盒模型</span>
|
||||
<el-tooltip content="设置当前行的宽度、高度、间距、内外边距与圆角" placement="top">
|
||||
<el-icon class="help-icon"><QuestionFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="collapse-body" v-show="expandBox">
|
||||
<!-- 3 列尺寸输入:宽度、高度、行内间距 -->
|
||||
<div class="box-dimension-grid">
|
||||
<div class="dimension-item">
|
||||
<span class="dim-label">宽度</span>
|
||||
<input
|
||||
type="text"
|
||||
v-model="row.rowStyle.width"
|
||||
class="dim-input"
|
||||
placeholder="1920"
|
||||
/>
|
||||
</div>
|
||||
<div class="dimension-item">
|
||||
<span class="dim-label">高度</span>
|
||||
<input
|
||||
type="text"
|
||||
v-model="row.rowStyle.height"
|
||||
class="dim-input"
|
||||
placeholder="默认"
|
||||
/>
|
||||
</div>
|
||||
<div class="dimension-item">
|
||||
<span class="dim-label">行内间距</span>
|
||||
<input
|
||||
type="text"
|
||||
v-model="row.rowStyle.gap"
|
||||
class="dim-input"
|
||||
placeholder="0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 可视化嵌套盒模型 (外边距 + 内边距 + 圆角) -->
|
||||
<div class="box-model-section">
|
||||
<nested-box-model
|
||||
:key="props.row?.id || props.row?.key || 'row'"
|
||||
:margin="row.rowStyle.margin"
|
||||
:padding="row.rowStyle.padding"
|
||||
:radius="row.rowStyle.radius"
|
||||
:show-radius="true"
|
||||
@update:margin="val => row.rowStyle.margin = val"
|
||||
@update:padding="val => row.rowStyle.padding = val"
|
||||
@update:radius="val => row.rowStyle.radius = val"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-panel">
|
||||
请在画布中选中一行以进行行配置
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { ArrowRight, QuestionFilled } from '@element-plus/icons-vue';
|
||||
import NestedBoxModel from '../controls/NestedBoxModel.vue';
|
||||
|
||||
const props = defineProps({
|
||||
row: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
rowIndex: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
totalRows: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
isFirst: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isLast: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
'move-up',
|
||||
'move-down',
|
||||
'move-top',
|
||||
'move-bottom',
|
||||
'move-to-row',
|
||||
'delete-row'
|
||||
]);
|
||||
|
||||
const expandOrder = ref(true);
|
||||
const expandBox = ref(true);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.row-config-panel {
|
||||
padding: 12px;
|
||||
box-sizing: border-box;
|
||||
|
||||
.setting-collapse-card {
|
||||
background: #ffffff;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
overflow: hidden;
|
||||
|
||||
.collapse-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 14px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
background: #fafafa;
|
||||
border-bottom: 1px solid transparent;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.arrow-icon {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
transition: transform 0.2s;
|
||||
|
||||
&.is-expanded {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.help-icon {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.collapse-body {
|
||||
padding: 14px;
|
||||
background: #ffffff;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
|
||||
/* 1. 行位置 */
|
||||
.row-order-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.order-text-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.main-row-idx {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.total-row-idx {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.order-btns {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
.pos-btn {
|
||||
padding: 0 14px;
|
||||
height: 28px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
color: #4b5563;
|
||||
background: #ffffff;
|
||||
border: 1px solid #d1d5db;
|
||||
|
||||
&:hover {
|
||||
color: #ff6a00;
|
||||
border-color: #ff6a00;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: #9ca3af;
|
||||
border-color: #e5e7eb;
|
||||
background: #f9fafb;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.row-select-wrap {
|
||||
width: 100%;
|
||||
.full-select {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* 2. 盒模型 3 列尺寸 */
|
||||
.box-dimension-grid {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 14px;
|
||||
|
||||
.dimension-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
|
||||
.dim-label {
|
||||
font-size: 12px;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.dim-input {
|
||||
width: 100%;
|
||||
height: 28px;
|
||||
line-height: 26px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: #303133;
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:focus {
|
||||
border-color: #ff6a00;
|
||||
box-shadow: 0 0 0 2px rgba(255, 106, 0, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.box-model-section {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* 3. 背景设置 */
|
||||
.form-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
&.mt-12 {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.row-label {
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
.help-icon {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-panel {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
692
seller/src/views/shop/pc/config-panel/index.vue
Normal file
692
seller/src/views/shop/pc/config-panel/index.vue
Normal file
@@ -0,0 +1,692 @@
|
||||
<template>
|
||||
|
||||
<div class="pc-config-panel-container">
|
||||
|
||||
<!-- 顶部选中状态卡片 (匹配魔方 dark navy card) -->
|
||||
|
||||
<div class="selected-header-card">
|
||||
|
||||
<div class="header-info">
|
||||
|
||||
<div class="selected-tag">当前选中</div>
|
||||
|
||||
<div class="selected-name">{{ currentSelectedTitle }}</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="header-icon-box">
|
||||
|
||||
<el-icon :size="24">
|
||||
|
||||
<component :is="currentSelectedIcon" />
|
||||
|
||||
</el-icon>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- 顶栏三 Tab 切换 -->
|
||||
|
||||
<div class="panel-header-tabs">
|
||||
|
||||
<div
|
||||
|
||||
class="tab-btn"
|
||||
|
||||
:class="{ active: activeTab === 'row' }"
|
||||
|
||||
@click="setTab('row')"
|
||||
|
||||
>
|
||||
|
||||
行配置
|
||||
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
||||
class="tab-btn"
|
||||
|
||||
:class="{ active: activeTab === 'component' }"
|
||||
|
||||
@click="setTab('component')"
|
||||
|
||||
>
|
||||
|
||||
组件配置
|
||||
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
||||
class="tab-btn"
|
||||
|
||||
:class="{ active: activeTab === 'business' }"
|
||||
|
||||
@click="setTab('business')"
|
||||
|
||||
>
|
||||
|
||||
业务配置
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="panel-scroll-content">
|
||||
|
||||
<!-- 1. 行配置 -->
|
||||
|
||||
<div v-show="activeTab === 'row'">
|
||||
|
||||
<row-config-panel
|
||||
|
||||
:row="selectedRow"
|
||||
|
||||
:row-index="selectedRowIndex"
|
||||
|
||||
:total-rows="totalRows"
|
||||
|
||||
:is-first="selectedRowIndex === 0"
|
||||
|
||||
:is-last="selectedRowIndex === totalRows - 1"
|
||||
|
||||
@move-up="emit('move-row-up')"
|
||||
|
||||
@move-down="emit('move-row-down')"
|
||||
|
||||
@move-top="emit('move-row-top')"
|
||||
|
||||
@move-bottom="emit('move-row-bottom')"
|
||||
@move-to-row="idx => emit('move-to-row', idx)"
|
||||
|
||||
@delete-row="emit('delete-row')"
|
||||
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- 2. 组件配置 -->
|
||||
|
||||
<div v-show="activeTab === 'component'">
|
||||
|
||||
<component-config-panel
|
||||
|
||||
:component="selectedComponent"
|
||||
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- 3. 业务配置 -->
|
||||
|
||||
<div v-show="activeTab === 'business'">
|
||||
|
||||
<div v-if="!selectedComponent" class="empty-panel">
|
||||
|
||||
请在画布中选中一个组件进行业务内容配置
|
||||
|
||||
</div>
|
||||
|
||||
<div v-else class="business-wrapper">
|
||||
|
||||
<!-- 动态分发业务配置面板 -->
|
||||
|
||||
<component
|
||||
|
||||
:is="businessPanelComponent"
|
||||
|
||||
v-if="businessPanelComponent"
|
||||
|
||||
:component="selectedComponent"
|
||||
|
||||
:options="selectedComponent.options"
|
||||
|
||||
/>
|
||||
|
||||
<div v-else class="fallback-business">
|
||||
|
||||
<div class="panel-title">{{ selectedComponent.name || selectedComponent.type }} 内容设置</div>
|
||||
|
||||
<p class="panel-desc">当前组件的基础属性与参数设置:</p>
|
||||
|
||||
<div class="form-group" v-for="(val, key) in selectedComponent.options" :key="key">
|
||||
|
||||
<span class="prop-key">{{ key }}</span>
|
||||
|
||||
<el-input
|
||||
|
||||
v-if="typeof val === 'string'"
|
||||
|
||||
v-model="selectedComponent.options[key]"
|
||||
|
||||
size="small"
|
||||
|
||||
/>
|
||||
|
||||
<el-switch
|
||||
|
||||
v-else-if="typeof val === 'boolean'"
|
||||
|
||||
v-model="selectedComponent.options[key]"
|
||||
|
||||
/>
|
||||
|
||||
<el-input-number
|
||||
|
||||
v-else-if="typeof val === 'number'"
|
||||
|
||||
v-model="selectedComponent.options[key]"
|
||||
|
||||
size="small"
|
||||
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<script setup>
|
||||
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
import {
|
||||
House, Compass, Picture, ShoppingBag, Ticket, Document, Menu, Bottom as BottomIcon,
|
||||
List, Grid, Fold, PictureRounded, Medal, Aim, FullScreen, Files, VideoCamera,
|
||||
Goods, Postcard, Reading, DArrowRight, CollectionTag, Star, Edit, Bell,
|
||||
Search, User, Rank
|
||||
} from '@element-plus/icons-vue';
|
||||
|
||||
const iconMap = {
|
||||
House, Compass, Picture, ShoppingBag, Ticket, Document, Menu, Bottom: BottomIcon,
|
||||
List, Grid, Fold, PictureRounded, Medal, Aim, FullScreen, Files, VideoCamera,
|
||||
Goods, Postcard, Reading, DArrowRight, CollectionTag, Star, Edit, Bell,
|
||||
Search, User, Rank
|
||||
};
|
||||
|
||||
import RowConfigPanel from './RowConfigPanel.vue';
|
||||
|
||||
import ComponentConfigPanel from './ComponentConfigPanel.vue';
|
||||
|
||||
import { COMPONENT_DEFINITIONS } from '../config.js';
|
||||
|
||||
|
||||
|
||||
// 导入业务面板
|
||||
|
||||
|
||||
|
||||
|
||||
import PanelCategoryNav from './PanelCategoryNav.vue';
|
||||
|
||||
import PanelChannelGrid from './PanelChannelGrid.vue';
|
||||
|
||||
import PanelCategoryMegaMenu from './PanelCategoryMegaMenu.vue';
|
||||
|
||||
import PanelCarousel from './PanelCarousel.vue';
|
||||
|
||||
import PanelBrandWall from './PanelBrandWall.vue';
|
||||
|
||||
import PanelHotZone from './PanelHotZone.vue';
|
||||
|
||||
import PanelFreeLayout from './PanelFreeLayout.vue';
|
||||
|
||||
import PanelPromoGroup from './PanelPromoGroup.vue';
|
||||
|
||||
import PanelVideo from './PanelVideo.vue';
|
||||
|
||||
import PanelGoods from './PanelGoods.vue';
|
||||
import PanelGridFloor from './PanelGridFloor.vue';
|
||||
import PanelGoodsGrid from './PanelGoodsGrid.vue';
|
||||
import PanelChannelFloor from './PanelChannelFloor.vue';
|
||||
import PanelRecommendFeed from './PanelRecommendFeed.vue';
|
||||
|
||||
|
||||
import PanelTitle from './PanelTitle.vue';
|
||||
|
||||
|
||||
import PanelNotice from './PanelNotice.vue';
|
||||
|
||||
|
||||
|
||||
const props = defineProps({
|
||||
|
||||
selectedRow: {
|
||||
|
||||
type: Object,
|
||||
|
||||
default: null
|
||||
|
||||
},
|
||||
|
||||
selectedRowIndex: {
|
||||
|
||||
type: Number,
|
||||
|
||||
default: 0
|
||||
|
||||
},
|
||||
|
||||
totalRows: {
|
||||
|
||||
type: Number,
|
||||
|
||||
default: 0
|
||||
|
||||
},
|
||||
|
||||
selectedComponent: {
|
||||
|
||||
type: Object,
|
||||
|
||||
default: null
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
const emit = defineEmits([
|
||||
|
||||
'move-row-up',
|
||||
|
||||
'move-row-down',
|
||||
|
||||
'move-row-top',
|
||||
|
||||
'move-row-bottom',
|
||||
'move-to-row',
|
||||
|
||||
'delete-row',
|
||||
'tab-change'
|
||||
]);
|
||||
|
||||
|
||||
|
||||
// 默认组件配置
|
||||
|
||||
const activeTab = ref('component');
|
||||
|
||||
|
||||
|
||||
function setTab(tab) {
|
||||
activeTab.value = tab;
|
||||
emit('tab-change', tab);
|
||||
}
|
||||
|
||||
function selectRowTab() {
|
||||
activeTab.value = 'row';
|
||||
emit('tab-change', 'row');
|
||||
}
|
||||
|
||||
function selectComponentTab() {
|
||||
activeTab.value = 'component';
|
||||
emit('tab-change', 'component');
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
selectComponentTab,
|
||||
selectRowTab,
|
||||
activeTab
|
||||
});
|
||||
|
||||
|
||||
|
||||
const currentSelectedTitle = computed(() => {
|
||||
|
||||
if (props.selectedComponent) {
|
||||
|
||||
return props.selectedComponent.name || props.selectedComponent.type;
|
||||
|
||||
}
|
||||
|
||||
if (props.selectedRow) {
|
||||
|
||||
return `第 ${props.selectedRowIndex + 1} 行`;
|
||||
|
||||
}
|
||||
|
||||
return '未选择项目';
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
const currentSelectedIcon = computed(() => {
|
||||
if (props.selectedComponent) {
|
||||
const iconName = props.selectedComponent.icon || COMPONENT_DEFINITIONS.find(d => d.type === props.selectedComponent.type)?.icon;
|
||||
return iconMap[iconName] || Grid;
|
||||
}
|
||||
return Rank;
|
||||
});
|
||||
|
||||
|
||||
|
||||
const panelMap = {
|
||||
|
||||
// 结构 (3)
|
||||
|
||||
|
||||
|
||||
|
||||
// 导航 (3)
|
||||
|
||||
categoryNav: PanelCategoryNav,
|
||||
|
||||
channelGrid: PanelChannelGrid,
|
||||
|
||||
categoryMegaMenu: PanelCategoryMegaMenu,
|
||||
|
||||
// 广告 (6)
|
||||
|
||||
carousel: PanelCarousel,
|
||||
|
||||
brandWall: PanelBrandWall,
|
||||
|
||||
hotZone: PanelHotZone,
|
||||
|
||||
freeLayout: PanelFreeLayout,
|
||||
|
||||
promoGroup: PanelPromoGroup,
|
||||
|
||||
video: PanelVideo,
|
||||
|
||||
// 商品 (8)
|
||||
|
||||
goodsList: PanelGoods,
|
||||
|
||||
gridFloor: PanelGridFloor,
|
||||
|
||||
goodsGrid: PanelGoodsGrid,
|
||||
|
||||
goodsCard: PanelGoods,
|
||||
|
||||
bannerFloor: PanelGridFloor,
|
||||
|
||||
goodsBar: PanelGoods,
|
||||
|
||||
channelFloor: PanelChannelFloor,
|
||||
|
||||
recommendFeed: PanelRecommendFeed,
|
||||
|
||||
// 营销 (1)
|
||||
|
||||
|
||||
// 内容 (3)
|
||||
|
||||
titleBar: PanelTitle,
|
||||
|
||||
|
||||
noticeBar: PanelNotice
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
const businessPanelComponent = computed(() => {
|
||||
|
||||
if (!props.selectedComponent) return null;
|
||||
|
||||
return panelMap[props.selectedComponent.type] || null;
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.pc-config-panel-container {
|
||||
|
||||
display: flex;
|
||||
|
||||
flex-direction: column;
|
||||
|
||||
height: 100%;
|
||||
|
||||
background: #ffffff;
|
||||
|
||||
|
||||
|
||||
/* 顶部暗蓝选中卡片 */
|
||||
|
||||
.selected-header-card {
|
||||
|
||||
display: flex;
|
||||
|
||||
justify-content: space-between;
|
||||
|
||||
align-items: center;
|
||||
|
||||
background: #1f2d48;
|
||||
|
||||
margin: 12px 12px 4px 12px;
|
||||
|
||||
padding: 12px 16px;
|
||||
|
||||
border-radius: 8px;
|
||||
|
||||
box-sizing: border-box;
|
||||
|
||||
|
||||
|
||||
.header-info {
|
||||
|
||||
display: flex;
|
||||
|
||||
flex-direction: column;
|
||||
|
||||
|
||||
|
||||
.selected-tag {
|
||||
|
||||
font-size: 11px;
|
||||
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
|
||||
margin-bottom: 4px;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.selected-name {
|
||||
|
||||
font-size: 16px;
|
||||
|
||||
font-weight: 700;
|
||||
|
||||
color: #ffffff;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.header-icon-box {
|
||||
|
||||
width: 44px;
|
||||
|
||||
height: 44px;
|
||||
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
|
||||
border-radius: 8px;
|
||||
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
justify-content: center;
|
||||
|
||||
color: #ffffff;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.panel-header-tabs {
|
||||
|
||||
display: flex;
|
||||
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
background: #ffffff;
|
||||
|
||||
margin: 0 12px;
|
||||
|
||||
|
||||
|
||||
.tab-btn {
|
||||
|
||||
flex: 1;
|
||||
|
||||
text-align: center;
|
||||
|
||||
padding: 12px 0;
|
||||
|
||||
font-size: 13px;
|
||||
|
||||
font-weight: 500;
|
||||
|
||||
color: #595959;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
border-bottom: 2px solid transparent;
|
||||
|
||||
transition: all 0.2s;
|
||||
|
||||
|
||||
|
||||
&:hover {
|
||||
|
||||
color: #ff6a00;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&.active {
|
||||
|
||||
color: #ff6a00;
|
||||
|
||||
font-weight: 600;
|
||||
|
||||
border-bottom-color: #ff6a00;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.panel-scroll-content {
|
||||
|
||||
flex: 1;
|
||||
|
||||
overflow-y: auto;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.empty-panel {
|
||||
|
||||
text-align: center;
|
||||
|
||||
padding: 60px 20px;
|
||||
|
||||
color: #94a3b8;
|
||||
|
||||
font-size: 13px;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.business-wrapper {
|
||||
|
||||
padding: 12px;
|
||||
|
||||
|
||||
|
||||
.fallback-business {
|
||||
|
||||
.panel-title {
|
||||
|
||||
font-size: 14px;
|
||||
|
||||
font-weight: 600;
|
||||
|
||||
color: #0f172a;
|
||||
|
||||
margin-bottom: 4px;
|
||||
|
||||
}
|
||||
|
||||
.panel-desc {
|
||||
|
||||
font-size: 12px;
|
||||
|
||||
color: #64748b;
|
||||
|
||||
margin-bottom: 16px;
|
||||
|
||||
}
|
||||
|
||||
.form-group {
|
||||
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
justify-content: space-between;
|
||||
|
||||
margin-bottom: 12px;
|
||||
|
||||
.prop-key {
|
||||
|
||||
font-size: 12px;
|
||||
|
||||
color: #334155;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
1099
seller/src/views/shop/pc/config.js
Normal file
1099
seller/src/views/shop/pc/config.js
Normal file
File diff suppressed because it is too large
Load Diff
228
seller/src/views/shop/pc/controls/BoxModelField.vue
Normal file
228
seller/src/views/shop/pc/controls/BoxModelField.vue
Normal file
@@ -0,0 +1,228 @@
|
||||
<template>
|
||||
<div class="box-model-field">
|
||||
<div class="field-header">
|
||||
<span class="field-title">{{ label }}</span>
|
||||
<el-tooltip :content="linked ? '点击解除四向联动' : '点击开启四向联动'" placement="top">
|
||||
<button
|
||||
type="button"
|
||||
class="link-btn"
|
||||
:class="{ active: linked }"
|
||||
@click="toggleLinked"
|
||||
>
|
||||
<el-icon :size="14">
|
||||
<Link v-if="linked" />
|
||||
<Paperclip v-else />
|
||||
</el-icon>
|
||||
<span class="link-text">{{ linked ? '已联动' : '独立' }}</span>
|
||||
</button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
<!-- 4-directional inputs -->
|
||||
<div class="field-inputs">
|
||||
<div
|
||||
v-for="dir in directions"
|
||||
:key="dir.key"
|
||||
class="input-item"
|
||||
:class="dir.key"
|
||||
>
|
||||
<span class="dir-tag">{{ dir.label }}</span>
|
||||
<el-input-number
|
||||
:model-value="values[dir.index]"
|
||||
:min="min"
|
||||
:max="max"
|
||||
:step="step"
|
||||
size="small"
|
||||
controls-position="right"
|
||||
class="num-input"
|
||||
@update:model-value="val => onValueChange(dir.index, val)"
|
||||
/>
|
||||
<span class="unit">px</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { Link, Paperclip } from '@element-plus/icons-vue';
|
||||
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
default: '边距设置'
|
||||
},
|
||||
modelValue: {
|
||||
type: Array,
|
||||
default: () => [0, 0, 0, 0]
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'box' // 'box' (上下左右) or 'corner' (左上、右上、右下、左下)
|
||||
},
|
||||
min: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
default: 500
|
||||
},
|
||||
step: {
|
||||
type: Number,
|
||||
default: 1
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'change']);
|
||||
|
||||
// Check if initial 4 values are all equal
|
||||
const isAllEqual = arr => {
|
||||
if (!Array.isArray(arr) || arr.length < 4) return true;
|
||||
return arr[0] === arr[1] && arr[1] === arr[2] && arr[2] === arr[3];
|
||||
};
|
||||
|
||||
const linked = ref(false);
|
||||
|
||||
const values = computed(() => {
|
||||
const v = props.modelValue;
|
||||
if (!Array.isArray(v) || v.length < 4) {
|
||||
return [0, 0, 0, 0];
|
||||
}
|
||||
return v.map(n => (typeof n === 'number' && !isNaN(n) ? n : 0));
|
||||
});
|
||||
|
||||
const directions = computed(() => {
|
||||
if (props.mode === 'corner') {
|
||||
return [
|
||||
{ key: 'top-left', label: '左上', index: 0 },
|
||||
{ key: 'top-right', label: '右上', index: 1 },
|
||||
{ key: 'bottom-right', label: '右下', index: 2 },
|
||||
{ key: 'bottom-left', label: '左下', index: 3 }
|
||||
];
|
||||
}
|
||||
return [
|
||||
{ key: 'top', label: '上', index: 0 },
|
||||
{ key: 'right', label: '右', index: 1 },
|
||||
{ key: 'bottom', label: '下', index: 2 },
|
||||
{ key: 'left', label: '左', index: 3 }
|
||||
];
|
||||
});
|
||||
|
||||
function toggleLinked() {
|
||||
linked.value = !linked.value;
|
||||
if (linked.value) {
|
||||
// When linking, sync all to the first value
|
||||
const firstVal = values.value[0];
|
||||
const newArr = [firstVal, firstVal, firstVal, firstVal];
|
||||
emit('update:modelValue', newArr);
|
||||
emit('change', newArr);
|
||||
}
|
||||
}
|
||||
|
||||
function onValueChange(index, val) {
|
||||
const num = typeof val === 'number' && !isNaN(val) ? val : 0;
|
||||
let newArr;
|
||||
if (linked.value) {
|
||||
newArr = [num, num, num, num];
|
||||
} else {
|
||||
newArr = [...values.value];
|
||||
newArr[index] = num;
|
||||
}
|
||||
emit('update:modelValue', newArr);
|
||||
emit('change', newArr);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.box-model-field {
|
||||
margin-bottom: 16px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
|
||||
.field-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.field-title {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.link-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 8px;
|
||||
font-size: 12px;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 4px;
|
||||
background: #ffffff;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #3b82f6;
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: #eff6ff;
|
||||
border-color: #3b82f6;
|
||||
color: #2563eb;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.field-inputs {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 8px;
|
||||
|
||||
.input-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #ffffff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
padding: 3px 6px;
|
||||
gap: 4px;
|
||||
|
||||
.dir-tag {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
min-width: 22px;
|
||||
}
|
||||
|
||||
.num-input {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
|
||||
:deep(.el-input__wrapper) {
|
||||
box-shadow: none !important;
|
||||
padding: 0 4px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
:deep(.el-input__inner) {
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: #1e293b;
|
||||
}
|
||||
}
|
||||
|
||||
.unit {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
524
seller/src/views/shop/pc/controls/NestedBoxModel.vue
Normal file
524
seller/src/views/shop/pc/controls/NestedBoxModel.vue
Normal file
@@ -0,0 +1,524 @@
|
||||
<template>
|
||||
<div class="nested-box-model-control">
|
||||
<!-- 外层: 外边距 (Margin) -->
|
||||
<div class="outer-box-wrapper">
|
||||
<div class="box-header-row">
|
||||
<span class="box-tag tag-orange">外边距</span>
|
||||
<div class="link-toggle">
|
||||
<span class="link-label">联动</span>
|
||||
<el-switch v-model="marginLinked" size="small" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 外边距 4 向输入框 -->
|
||||
<input
|
||||
type="text"
|
||||
v-model="localMargin[0]"
|
||||
@input="onMarginChange(0, $event.target.value)"
|
||||
class="num-input pos-top"
|
||||
title="外边距-上"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
v-model="localMargin[1]"
|
||||
@input="onMarginChange(1, $event.target.value)"
|
||||
class="num-input pos-right"
|
||||
title="外边距-右"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
v-model="localMargin[2]"
|
||||
@input="onMarginChange(2, $event.target.value)"
|
||||
class="num-input pos-bottom"
|
||||
title="外边距-下"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
v-model="localMargin[3]"
|
||||
@input="onMarginChange(3, $event.target.value)"
|
||||
class="num-input pos-left"
|
||||
title="外边距-左"
|
||||
/>
|
||||
|
||||
<!-- 内层: 内边距 (Padding) -->
|
||||
<div class="inner-box-wrapper">
|
||||
<div class="box-header-row">
|
||||
<span class="box-tag tag-green">内边距</span>
|
||||
<div class="link-toggle">
|
||||
<span class="link-label">联动</span>
|
||||
<el-switch v-model="paddingLinked" size="small" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 内边距 4 向输入框 -->
|
||||
<input
|
||||
type="text"
|
||||
v-model="localPadding[0]"
|
||||
@input="onPaddingChange(0, $event.target.value)"
|
||||
class="num-input pos-top"
|
||||
title="内边距-上"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
v-model="localPadding[1]"
|
||||
@input="onPaddingChange(1, $event.target.value)"
|
||||
class="num-input pos-right"
|
||||
title="内边距-右"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
v-model="localPadding[2]"
|
||||
@input="onPaddingChange(2, $event.target.value)"
|
||||
class="num-input pos-bottom"
|
||||
title="内边距-下"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
v-model="localPadding[3]"
|
||||
@input="onPaddingChange(3, $event.target.value)"
|
||||
class="num-input pos-left"
|
||||
title="内边距-左"
|
||||
/>
|
||||
|
||||
<!-- 中心参考方块 -->
|
||||
<div class="box-center-core"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 圆角 border-radius -->
|
||||
<div class="radius-control-card" v-if="showRadius">
|
||||
<div class="radius-header">
|
||||
<span class="radius-title">圆角 border-radius</span>
|
||||
<div class="link-toggle">
|
||||
<span class="link-label">联动</span>
|
||||
<el-switch v-model="radiusLinked" size="small" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="radius-grid">
|
||||
<!-- 左列: 左上、左下 -->
|
||||
<div class="radius-col">
|
||||
<div class="radius-item">
|
||||
<span class="item-label">左上</span>
|
||||
<input
|
||||
type="text"
|
||||
v-model="localRadius[0]"
|
||||
@input="onRadiusChange(0, $event.target.value)"
|
||||
class="num-input"
|
||||
/>
|
||||
</div>
|
||||
<div class="radius-item">
|
||||
<span class="item-label">左下</span>
|
||||
<input
|
||||
type="text"
|
||||
v-model="localRadius[3]"
|
||||
@input="onRadiusChange(3, $event.target.value)"
|
||||
class="num-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 中间预览方块 -->
|
||||
<div class="radius-preview-wrap">
|
||||
<div class="radius-preview-box" :style="radiusPreviewStyle"></div>
|
||||
</div>
|
||||
|
||||
<!-- 右列: 右上、右下 -->
|
||||
<div class="radius-col">
|
||||
<div class="radius-item">
|
||||
<span class="item-label">右上</span>
|
||||
<input
|
||||
type="text"
|
||||
v-model="localRadius[1]"
|
||||
@input="onRadiusChange(1, $event.target.value)"
|
||||
class="num-input"
|
||||
/>
|
||||
</div>
|
||||
<div class="radius-item">
|
||||
<span class="item-label">右下</span>
|
||||
<input
|
||||
type="text"
|
||||
v-model="localRadius[2]"
|
||||
@input="onRadiusChange(2, $event.target.value)"
|
||||
class="num-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
margin: {
|
||||
type: Array,
|
||||
default: () => [0, 0, 0, 0]
|
||||
},
|
||||
padding: {
|
||||
type: Array,
|
||||
default: () => [0, 0, 0, 0]
|
||||
},
|
||||
radius: {
|
||||
type: Array,
|
||||
default: () => [0, 0, 0, 0]
|
||||
},
|
||||
showRadius: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:margin', 'update:padding', 'update:radius', 'change']);
|
||||
|
||||
// 联动开关:默认关闭
|
||||
const marginLinked = ref(false);
|
||||
const paddingLinked = ref(false);
|
||||
const radiusLinked = ref(false);
|
||||
|
||||
watch(marginLinked, val => {
|
||||
if (val) {
|
||||
const v = localMargin.value[0] || 0;
|
||||
localMargin.value = [v, v, v, v];
|
||||
emit('update:margin', [...localMargin.value]);
|
||||
emit('change', { type: 'margin', value: [...localMargin.value] });
|
||||
}
|
||||
});
|
||||
|
||||
watch(paddingLinked, val => {
|
||||
if (val) {
|
||||
const v = localPadding.value[0] || 0;
|
||||
localPadding.value = [v, v, v, v];
|
||||
emit('update:padding', [...localPadding.value]);
|
||||
emit('change', { type: 'padding', value: [...localPadding.value] });
|
||||
}
|
||||
});
|
||||
|
||||
watch(radiusLinked, val => {
|
||||
if (val) {
|
||||
const v = localRadius.value[0] || 0;
|
||||
localRadius.value = [v, v, v, v];
|
||||
emit('update:radius', [...localRadius.value]);
|
||||
emit('change', { type: 'radius', value: [...localRadius.value] });
|
||||
}
|
||||
});
|
||||
|
||||
const localMargin = ref([...(props.margin || [0, 0, 0, 0])]);
|
||||
const localPadding = ref([...(props.padding || [0, 0, 0, 0])]);
|
||||
const localRadius = ref([...(props.radius || [0, 0, 0, 0])]);
|
||||
|
||||
watch(
|
||||
() => props.margin,
|
||||
val => {
|
||||
if (Array.isArray(val) && val.length >= 4) {
|
||||
localMargin.value = [...val];
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.padding,
|
||||
val => {
|
||||
if (Array.isArray(val) && val.length >= 4) {
|
||||
localPadding.value = [...val];
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.radius,
|
||||
val => {
|
||||
if (Array.isArray(val) && val.length >= 4) {
|
||||
localRadius.value = [...val];
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
function parseNum(val) {
|
||||
const n = parseInt(val, 10);
|
||||
return isNaN(n) ? 0 : Math.max(0, n);
|
||||
}
|
||||
|
||||
function onMarginChange(index, rawVal) {
|
||||
const num = parseNum(rawVal);
|
||||
if (marginLinked.value) {
|
||||
localMargin.value = [num, num, num, num];
|
||||
} else {
|
||||
localMargin.value[index] = num;
|
||||
}
|
||||
emit('update:margin', [...localMargin.value]);
|
||||
emit('change', { type: 'margin', value: [...localMargin.value] });
|
||||
}
|
||||
|
||||
function onPaddingChange(index, rawVal) {
|
||||
const num = parseNum(rawVal);
|
||||
if (paddingLinked.value) {
|
||||
localPadding.value = [num, num, num, num];
|
||||
} else {
|
||||
localPadding.value[index] = num;
|
||||
}
|
||||
emit('update:padding', [...localPadding.value]);
|
||||
emit('change', { type: 'padding', value: [...localPadding.value] });
|
||||
}
|
||||
|
||||
function onRadiusChange(index, rawVal) {
|
||||
const num = parseNum(rawVal);
|
||||
if (radiusLinked.value) {
|
||||
localRadius.value = [num, num, num, num];
|
||||
} else {
|
||||
localRadius.value[index] = num;
|
||||
}
|
||||
emit('update:radius', [...localRadius.value]);
|
||||
emit('change', { type: 'radius', value: [...localRadius.value] });
|
||||
}
|
||||
|
||||
const radiusPreviewStyle = computed(() => {
|
||||
const r = localRadius.value;
|
||||
return {
|
||||
borderTopLeftRadius: `${r[0] || 0}px`,
|
||||
borderTopRightRadius: `${r[1] || 0}px`,
|
||||
borderBottomRightRadius: `${r[2] || 0}px`,
|
||||
borderBottomLeftRadius: `${r[3] || 0}px`
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.nested-box-model-control {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
.num-input {
|
||||
width: 42px;
|
||||
height: 24px;
|
||||
line-height: 22px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: #303133;
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:focus {
|
||||
border-color: #ff6a00;
|
||||
box-shadow: 0 0 0 2px rgba(255, 106, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.box-header-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin-bottom: 2px;
|
||||
z-index: 2;
|
||||
|
||||
.box-tag {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
|
||||
&.tag-orange {
|
||||
color: #ff6a00;
|
||||
}
|
||||
|
||||
&.tag-green {
|
||||
color: #52c41a;
|
||||
}
|
||||
}
|
||||
|
||||
.link-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.link-label {
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 外层容器: 外边距 */
|
||||
.outer-box-wrapper {
|
||||
position: relative;
|
||||
background: #fffaf5;
|
||||
border: 1px dashed #ffd8bf;
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px 10px;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-height: 190px;
|
||||
|
||||
.pos-top {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.pos-bottom {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.pos-left {
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.pos-right {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
z-index: 3;
|
||||
}
|
||||
}
|
||||
|
||||
/* 内层容器: 内边距 */
|
||||
.inner-box-wrapper {
|
||||
position: relative;
|
||||
background: #f6ffed;
|
||||
border: 1px dashed #b7eb8f;
|
||||
border-radius: 6px;
|
||||
margin: 28px 40px;
|
||||
padding: 6px 8px 8px;
|
||||
box-sizing: border-box;
|
||||
min-height: 110px;
|
||||
|
||||
.pos-top {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.pos-bottom {
|
||||
position: absolute;
|
||||
bottom: 6px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.pos-left {
|
||||
position: absolute;
|
||||
left: 6px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.pos-right {
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.box-center-core {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 1px dashed #91caff;
|
||||
background: #e6f4ff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 圆角卡片 */
|
||||
.radius-control-card {
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin-top: 12px;
|
||||
background: #ffffff;
|
||||
box-sizing: border-box;
|
||||
|
||||
.radius-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.radius-title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.link-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.link-label {
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.radius-grid {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.radius-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
.radius-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.item-label {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
width: 26px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.radius-preview-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 10px;
|
||||
|
||||
.radius-preview-box {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border: 2px solid #409eff;
|
||||
background: #fdfdfd;
|
||||
box-sizing: border-box;
|
||||
transition: border-radius 0.2s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
2491
seller/src/views/shop/pc/index.vue
Normal file
2491
seller/src/views/shop/pc/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
148
seller/src/views/shop/pc/template/TplBrandWall.vue
Normal file
148
seller/src/views/shop/pc/template/TplBrandWall.vue
Normal file
@@ -0,0 +1,148 @@
|
||||
<template>
|
||||
<div class="tpl-brand-wall" :style="customStyle">
|
||||
<div
|
||||
class="brand-grid"
|
||||
:style="{
|
||||
gridTemplateColumns: `repeat(${actualColumns}, 1fr)`,
|
||||
gap: `${options.gap ?? 12}px`
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-for="(item, idx) in brandList"
|
||||
:key="idx"
|
||||
class="brand-box"
|
||||
@click="handleClickItem(item)"
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
</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: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/brand-wall/brand_1.png', url: '/goodsList?category=1' },
|
||||
{ title: '美妆奢宠 · 每满300减50', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/brand-wall/brand_2.png', url: '/goodsList?category=2' },
|
||||
{ title: '运动潮品 · 爆款低至5折', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/brand-wall/brand_3.png', url: '/goodsList?category=3' },
|
||||
{ title: '品质生活 · 限时第2件半价', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/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'
|
||||
};
|
||||
});
|
||||
|
||||
function handleClickItem(item) {
|
||||
if (item && item.url) {
|
||||
console.log('Navigate to:', item.url);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tpl-brand-wall {
|
||||
width: 100%;
|
||||
user-select: none;
|
||||
box-sizing: border-box;
|
||||
|
||||
.brand-grid {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
|
||||
.brand-box {
|
||||
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;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
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>
|
||||
304
seller/src/views/shop/pc/template/TplCarousel.vue
Normal file
304
seller/src/views/shop/pc/template/TplCarousel.vue
Normal file
@@ -0,0 +1,304 @@
|
||||
<template>
|
||||
<div
|
||||
class="tpl-carousel"
|
||||
:class="[
|
||||
isMini ? 'is-mini' : (isCompact ? 'is-compact' : '')
|
||||
]"
|
||||
:style="customStyle"
|
||||
>
|
||||
<div class="carousel-container">
|
||||
<!-- 轮播图主屏 -->
|
||||
<transition name="carousel-fade" mode="out-in">
|
||||
<div :key="currentIdx" class="slide-view">
|
||||
<img
|
||||
:src="currentSlide.img"
|
||||
:alt="currentSlide.title || 'banner'"
|
||||
class="slide-img"
|
||||
/>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<!-- 左右切换箭头按钮 -->
|
||||
<template v-if="options.showArrow !== false && slidesList.length > 1">
|
||||
<div class="arrow-btn prev-btn" @click.stop="prevSlide">
|
||||
<el-icon><ArrowLeft /></el-icon>
|
||||
</div>
|
||||
<div class="arrow-btn next-btn" @click.stop="nextSlide">
|
||||
<el-icon><ArrowRight /></el-icon>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 底部指示点 -->
|
||||
<div
|
||||
v-if="options.showIndicator !== false && slidesList.length > 1"
|
||||
class="carousel-indicators"
|
||||
>
|
||||
<span
|
||||
v-for="(_, idx) in slidesList"
|
||||
:key="idx"
|
||||
class="indicator-dot"
|
||||
:class="{ active: currentIdx === idx }"
|
||||
@click.stop="currentIdx = idx"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue';
|
||||
import { ArrowLeft, ArrowRight } from '@element-plus/icons-vue';
|
||||
|
||||
const props = defineProps({
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
componentStyle: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
rowStyle: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const currentIdx = ref(0);
|
||||
let timer = null;
|
||||
|
||||
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;
|
||||
}
|
||||
if (Array.isArray(props.options.banners) && props.options.banners.length > 0) {
|
||||
return props.options.banners.map(b => ({
|
||||
title: b.title,
|
||||
img: b.image || b.img || b.url,
|
||||
url: b.url || b.link
|
||||
}));
|
||||
}
|
||||
return [
|
||||
{ title: '潮流数码', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/carousel/banner_1.png', url: '/goodsList?category=1' },
|
||||
{ title: '奢宠美妆', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/carousel/banner_2.png', url: '/goodsList?category=2' },
|
||||
{ title: '运动潮流', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/carousel/banner_3.png', url: '/goodsList?category=3' },
|
||||
{ title: '居家生活', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/carousel/banner_4.png', url: '/goodsList?category=4' }
|
||||
];
|
||||
});
|
||||
|
||||
const currentSlide = computed(() => {
|
||||
return slidesList.value[currentIdx.value] || slidesList.value[0];
|
||||
});
|
||||
|
||||
const customStyle = computed(() => {
|
||||
const s = props.componentStyle || {};
|
||||
const fit = props.options.fitMode || 'cover';
|
||||
const hasRowHeight = Boolean(props.rowStyle?.height && Number(props.rowStyle.height) > 0);
|
||||
|
||||
const styleObj = {
|
||||
borderRadius: Array.isArray(s.radius) && s.radius.some(r => r > 0)
|
||||
? `${s.radius[0]}px ${s.radius[1]}px ${s.radius[2]}px ${s.radius[3]}px`
|
||||
: '8px',
|
||||
backgroundColor: (s.backgroundColor && s.backgroundColor !== 'transparent') ? s.backgroundColor : '#ffffff',
|
||||
'--carousel-fit': fit,
|
||||
height: '100%',
|
||||
minHeight: hasRowHeight ? '0' : '420px'
|
||||
};
|
||||
|
||||
if (props.options.aspectRatio === 'custom' && props.options.height) {
|
||||
styleObj.height = `${props.options.height}px`;
|
||||
styleObj.minHeight = `${props.options.height}px`;
|
||||
}
|
||||
return styleObj;
|
||||
});
|
||||
|
||||
function prevSlide() {
|
||||
const len = slidesList.value.length;
|
||||
currentIdx.value = (currentIdx.value - 1 + len) % len;
|
||||
resetTimer();
|
||||
}
|
||||
|
||||
function nextSlide() {
|
||||
const len = slidesList.value.length;
|
||||
currentIdx.value = (currentIdx.value + 1) % len;
|
||||
resetTimer();
|
||||
}
|
||||
|
||||
function startTimer() {
|
||||
stopTimer();
|
||||
if (props.options.autoPlay !== false && slidesList.value.length > 1) {
|
||||
const interval = Number(props.options.interval) || 4000;
|
||||
timer = setInterval(() => {
|
||||
const len = slidesList.value.length;
|
||||
currentIdx.value = (currentIdx.value + 1) % len;
|
||||
}, interval);
|
||||
}
|
||||
}
|
||||
|
||||
function stopTimer() {
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function resetTimer() {
|
||||
stopTimer();
|
||||
startTimer();
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.options.autoPlay, props.options.interval, slidesList.value.length],
|
||||
() => {
|
||||
startTimer();
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
startTimer();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopTimer();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tpl-carousel {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
background: #ffffff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.carousel-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.slide-view {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #ffffff;
|
||||
|
||||
.slide-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: var(--carousel-fit, cover);
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.arrow-btn {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
color: #ffffff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
z-index: 10;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.65);
|
||||
transform: translateY(-50%) scale(1.08);
|
||||
}
|
||||
|
||||
&.prev-btn {
|
||||
left: 12px;
|
||||
}
|
||||
|
||||
&.next-btn {
|
||||
right: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.carousel-indicators {
|
||||
position: absolute;
|
||||
bottom: 12px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
z-index: 10;
|
||||
padding: 3px 8px;
|
||||
background: rgba(0, 0, 0, 0.18);
|
||||
border-radius: 10px;
|
||||
backdrop-filter: blur(2px);
|
||||
|
||||
.indicator-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
cursor: pointer;
|
||||
transition: all 0.25s;
|
||||
|
||||
&.active {
|
||||
width: 16px;
|
||||
border-radius: 3px;
|
||||
background: #409EFF;
|
||||
}
|
||||
|
||||
&:hover:not(.active) {
|
||||
background: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-mini {
|
||||
.arrow-btn {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.carousel-indicators {
|
||||
bottom: 8px;
|
||||
gap: 4px;
|
||||
.indicator-dot {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
&.active {
|
||||
width: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.carousel-fade-enter-active,
|
||||
.carousel-fade-leave-active {
|
||||
transition: opacity 0.35s ease;
|
||||
}
|
||||
|
||||
.carousel-fade-enter-from,
|
||||
.carousel-fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
443
seller/src/views/shop/pc/template/TplCategoryMegaMenu.vue
Normal file
443
seller/src/views/shop/pc/template/TplCategoryMegaMenu.vue
Normal file
@@ -0,0 +1,443 @@
|
||||
<template>
|
||||
<div
|
||||
class="tpl-category-mega-menu"
|
||||
:class="{ 'is-hovering': panelVisible && activeChildren.length }"
|
||||
:style="customStyle"
|
||||
@mouseleave="handleMouseLeave"
|
||||
>
|
||||
<!-- 顶部标题:分类 / 商品分类 -->
|
||||
<div class="menu-header">
|
||||
<span class="header-title">{{ options.title || '商品分类' }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 垂直一级类目列表 -->
|
||||
<div class="category-list">
|
||||
<div
|
||||
v-for="(cat, idx) in displayCategories"
|
||||
:key="cat.id || idx"
|
||||
class="cat-item"
|
||||
:class="{ 'is-active': activeIndex === idx && panelVisible }"
|
||||
:style="{
|
||||
'--text-color': options.textColor || '#334155',
|
||||
'--active-color': options.activeColor || '#409EFF',
|
||||
'--font-size': `${options.fontSize || 13}px`
|
||||
}"
|
||||
@mouseenter="handleMouseEnter(idx, cat)"
|
||||
>
|
||||
<span class="cat-name">{{ cat.name }}</span>
|
||||
<el-icon class="arrow-icon"><ArrowRight /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧悬浮超级大面板 (Mega Menu 仿真预览) -->
|
||||
<transition name="fade-flyout">
|
||||
<div
|
||||
v-if="panelVisible && activeChildren.length"
|
||||
class="mega-flyout-panel"
|
||||
@mouseenter="handlePanelEnter"
|
||||
@mouseleave="handleMouseLeave"
|
||||
>
|
||||
<div class="flyout-top-tags" v-if="activeChildren && activeChildren.length">
|
||||
<span
|
||||
v-for="(sub, sIdx) in activeChildren.slice(0, 8)"
|
||||
:key="sub.id || sIdx"
|
||||
class="top-tag-pill"
|
||||
>
|
||||
{{ sub.name }}
|
||||
<el-icon class="pill-arrow"><ArrowRight /></el-icon>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flyout-content-scroll">
|
||||
<div
|
||||
v-for="(sub, sIdx) in activeChildren"
|
||||
:key="sub.id || sIdx"
|
||||
class="cat-row"
|
||||
>
|
||||
<div class="lvl2-title">
|
||||
<span class="lvl2-name">{{ sub.name }}</span>
|
||||
<el-icon class="lvl2-arrow"><ArrowRight /></el-icon>
|
||||
</div>
|
||||
<div class="lvl3-tags-wrap">
|
||||
<span
|
||||
v-for="(leaf, lIdx) in (sub.children || [])"
|
||||
:key="leaf.id || lIdx"
|
||||
class="lvl3-tag"
|
||||
>
|
||||
{{ leaf.name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { ArrowRight } from '@element-plus/icons-vue';
|
||||
import { getGoodsCategoryAll } from '@/api/goods.js';
|
||||
|
||||
const props = defineProps({
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
componentStyle: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
rowStyle: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const defaultCategories = [
|
||||
{
|
||||
id: '1',
|
||||
name: '电脑办公',
|
||||
children: [
|
||||
{ id: '1-1', name: '手机通讯', children: [{ name: '手机' }, { name: '游戏手机' }, { name: '老人机' }] },
|
||||
{ id: '1-2', name: '电脑整机', children: [{ name: '笔记本' }, { name: '台式机' }, { name: '平板电脑' }] }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: '家用电器',
|
||||
children: [
|
||||
{ id: '2-1', name: '大家电', children: [{ name: '电视' }, { name: '冰箱' }, { name: '洗衣机' }, { name: '空调' }] },
|
||||
{ id: '2-2', name: '厨房小电', children: [{ name: '电饭煲' }, { name: '微波炉' }, { name: '咖啡机' }] }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: '服装鞋包',
|
||||
children: [
|
||||
{ id: '3-1', name: '女装', children: [{ name: '连衣裙' }, { name: '衬衫' }, { name: '羽绒服' }] },
|
||||
{ id: '3-2', name: '男装', children: [{ name: 'T恤' }, { name: '夹克' }, { name: '牛仔裤' }] }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: '食品生鲜',
|
||||
children: [
|
||||
{ id: '4-1', name: '生鲜水果', children: [{ name: '苹果' }, { name: '车厘子' }, { name: '奇异果' }] },
|
||||
{ id: '4-2', name: '休闲零食', children: [{ name: '坚果炒货' }, { name: '饼干蛋糕' }, { name: '糖果果冻' }] }
|
||||
]
|
||||
},
|
||||
{ id: '5', name: '精品箱包' },
|
||||
{ id: '6', name: '美妆个护' },
|
||||
{ id: '7', name: '运动户外' },
|
||||
{ id: '8', name: '家居家装' }
|
||||
];
|
||||
|
||||
const systemCategories = ref([]);
|
||||
const activeIndex = ref(-1);
|
||||
const activeCategory = ref(null);
|
||||
const panelVisible = ref(false);
|
||||
|
||||
const isCustomSource = computed(() => {
|
||||
if (props.options?.source === 'system') return false;
|
||||
if (props.options?.source === 'custom') {
|
||||
if (Array.isArray(props.options?.categories) && props.options.categories.length === 4) {
|
||||
const names = props.options.categories.map(c => c.name || c.title || '');
|
||||
const isPlaceholder = names.some(n => n.includes('精选')) &&
|
||||
names.some(n => n.includes('新品')) &&
|
||||
names.some(n => n.includes('企业')) &&
|
||||
names.some(n => n.includes('领券'));
|
||||
if (isPlaceholder) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
const displayCategories = computed(() => {
|
||||
if (isCustomSource.value && Array.isArray(props.options?.categories) && props.options.categories.length) {
|
||||
return props.options.categories.map(c => ({
|
||||
...c,
|
||||
name: c.name || c.title || ''
|
||||
}));
|
||||
}
|
||||
if (systemCategories.value && systemCategories.value.length) {
|
||||
return systemCategories.value;
|
||||
}
|
||||
return defaultCategories;
|
||||
});
|
||||
|
||||
const activeChildren = computed(() => {
|
||||
if (!activeCategory.value) return [];
|
||||
if (Array.isArray(activeCategory.value.children) && activeCategory.value.children.length) {
|
||||
return activeCategory.value.children;
|
||||
}
|
||||
if (Array.isArray(activeCategory.value.subCategories) && activeCategory.value.subCategories.length) {
|
||||
return activeCategory.value.subCategories.map((s, idx) => ({
|
||||
id: s.id || `sub_${idx}`,
|
||||
name: s.name,
|
||||
children: (s.items || []).map((it, iIdx) => ({
|
||||
id: `leaf_${idx}_${iIdx}`,
|
||||
name: typeof it === 'string' ? it : it.name
|
||||
}))
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
function handleMouseEnter(idx, cat) {
|
||||
activeIndex.value = idx;
|
||||
activeCategory.value = cat;
|
||||
if (cat.children && cat.children.length) {
|
||||
panelVisible.value = true;
|
||||
} else if (cat.subCategories && cat.subCategories.length) {
|
||||
panelVisible.value = true;
|
||||
} else {
|
||||
panelVisible.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handlePanelEnter() {
|
||||
panelVisible.value = true;
|
||||
}
|
||||
|
||||
function handleMouseLeave() {
|
||||
panelVisible.value = false;
|
||||
activeIndex.value = -1;
|
||||
activeCategory.value = null;
|
||||
}
|
||||
|
||||
function fetchCategories() {
|
||||
getGoodsCategoryAll(0).then(res => {
|
||||
if (res.success && Array.isArray(res.result) && res.result.length) {
|
||||
systemCategories.value = res.result;
|
||||
}
|
||||
}).catch(() => {
|
||||
// 降级使用默认真实分类
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchCategories();
|
||||
});
|
||||
|
||||
const customStyle = computed(() => {
|
||||
const s = props.componentStyle || {};
|
||||
const hasRowHeight = Boolean(props.rowStyle?.height && Number(props.rowStyle.height) > 0);
|
||||
return {
|
||||
backgroundColor: (s.backgroundColor && s.backgroundColor !== 'transparent') ? s.backgroundColor : '#ffffff',
|
||||
borderRadius: Array.isArray(s.radius) && s.radius.some(r => r > 0)
|
||||
? `${s.radius[0]}px ${s.radius[1]}px ${s.radius[2]}px ${s.radius[3]}px`
|
||||
: '8px',
|
||||
height: '100%',
|
||||
minHeight: hasRowHeight ? '0' : '420px'
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tpl-category-mega-menu {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
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;
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
|
||||
&.is-hovering {
|
||||
z-index: 1050;
|
||||
}
|
||||
|
||||
.menu-header {
|
||||
height: 42px;
|
||||
padding: 0 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
flex-shrink: 0;
|
||||
|
||||
.header-title {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
}
|
||||
|
||||
.category-list {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-evenly;
|
||||
padding: 4px 0 8px;
|
||||
box-sizing: border-box;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
|
||||
.cat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 16px;
|
||||
cursor: pointer;
|
||||
user-select: 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: 11px;
|
||||
color: #94a3b8;
|
||||
transition: transform 0.15s ease, color 0.15s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.is-active {
|
||||
background: #ecf5ff;
|
||||
|
||||
.cat-name {
|
||||
color: var(--active-color, #409EFF);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.arrow-icon {
|
||||
color: var(--active-color, #409EFF);
|
||||
transform: translateX(2px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mega-flyout-panel {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 100%;
|
||||
width: 780px;
|
||||
min-height: 100%;
|
||||
max-height: 520px;
|
||||
background: #ffffff;
|
||||
z-index: 1100;
|
||||
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.12), 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
border: 1px solid #e2e8f0;
|
||||
border-left: none;
|
||||
border-radius: 0 8px 8px 0;
|
||||
padding: 16px 20px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.flyout-top-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
margin-bottom: 12px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.top-tag-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 10px;
|
||||
background: #f8fafc;
|
||||
border-radius: 14px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #334155;
|
||||
cursor: default;
|
||||
|
||||
.pill-arrow {
|
||||
font-size: 10px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.flyout-content-scroll {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding-right: 6px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #cbd5e1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.cat-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px dashed #f1f5f9;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.lvl2-title {
|
||||
width: 96px;
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
color: #1e293b;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
padding-top: 1px;
|
||||
|
||||
.lvl2-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.lvl2-arrow {
|
||||
font-size: 10px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
|
||||
.lvl3-tags-wrap {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px 14px;
|
||||
|
||||
.lvl3-tag {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fade-flyout-enter-active,
|
||||
.fade-flyout-leave-active {
|
||||
transition: opacity 0.15s ease, transform 0.15s ease;
|
||||
}
|
||||
|
||||
.fade-flyout-enter-from,
|
||||
.fade-flyout-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-4px);
|
||||
}
|
||||
</style>
|
||||
205
seller/src/views/shop/pc/template/TplCategoryNav.vue
Normal file
205
seller/src/views/shop/pc/template/TplCategoryNav.vue
Normal file
@@ -0,0 +1,205 @@
|
||||
<template>
|
||||
<div class="tpl-category-nav" :style="customStyle">
|
||||
<div class="nav-container">
|
||||
<!-- 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. 分类导航项水平列表 -->
|
||||
<div class="nav-items-wrapper">
|
||||
<div
|
||||
v-for="(item, idx) in options.navItems"
|
||||
:key="idx"
|
||||
class="cat-item"
|
||||
:class="{ 'is-first': idx === 0 }"
|
||||
@click="handleClickItem(item)"
|
||||
>
|
||||
<img
|
||||
v-if="item.img"
|
||||
:src="item.img"
|
||||
class="cat-icon"
|
||||
alt="cat-icon"
|
||||
/>
|
||||
<span
|
||||
class="cat-title"
|
||||
:style="{
|
||||
'--text-color': options.textColor || '#334155',
|
||||
'--active-color': options.activeColor || '#e11d48'
|
||||
}"
|
||||
>
|
||||
{{ item.title }}
|
||||
</span>
|
||||
<span v-if="item.badge" class="cat-badge" :style="{ backgroundColor: options.activeColor || '#e11d48' }">
|
||||
{{ item.badge }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</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`
|
||||
};
|
||||
});
|
||||
|
||||
function handleClickItem(item) {
|
||||
if (item && item.url) {
|
||||
console.log('Navigate to:', item.url);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tpl-category-nav {
|
||||
width: 100%;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.nav-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
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;
|
||||
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-wrapper {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 28px;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none; // Firefox
|
||||
&::-webkit-scrollbar {
|
||||
display: none; // Chrome/Safari
|
||||
}
|
||||
|
||||
.cat-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.15s ease;
|
||||
|
||||
.cat-icon {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
object-fit: contain;
|
||||
border-radius: 4px;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.cat-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--text-color, #334155);
|
||||
transition: color 0.2s ease;
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cat-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);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.cat-icon {
|
||||
transform: scale(1.12);
|
||||
}
|
||||
|
||||
.cat-title {
|
||||
color: var(--active-color, #e11d48);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
535
seller/src/views/shop/pc/template/TplChannelFloor.vue
Normal file
535
seller/src/views/shop/pc/template/TplChannelFloor.vue
Normal file
@@ -0,0 +1,535 @@
|
||||
<template>
|
||||
<div class="tpl-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>
|
||||
<span class="title-text">{{ options.title }}</span>
|
||||
<span class="subtitle-text" v-if="options.subtitle">{{ options.subtitle }}</span>
|
||||
</div>
|
||||
<div class="header-more">
|
||||
<span>探索更多场景 ></span>
|
||||
</div>
|
||||
</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" />
|
||||
<div 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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 专属活动入口 (Promos) -->
|
||||
<div class="hero-promos-wrap" v-if="promosList.length">
|
||||
<div
|
||||
v-for="(p, pIdx) in promosList"
|
||||
:key="pIdx"
|
||||
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>
|
||||
</div>
|
||||
</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"
|
||||
>
|
||||
<div class="card-thumb-wrap">
|
||||
<img :src="g.thumbnail || defaultThumb" :alt="g.goodsName" class="goods-img" />
|
||||
<span class="scene-tag">场景精选</span>
|
||||
</div>
|
||||
|
||||
<div class="card-info">
|
||||
<div class="goods-name">{{ g.goodsName }}</div>
|
||||
<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="立即加购">+</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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 defaultThumb = 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_1.png';
|
||||
|
||||
const heroImg = computed(() => {
|
||||
return props.options?.heroImage || 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/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; // 100% 满宽时:左侧主视觉 (320px) + 右侧 3 列或 4 列双行
|
||||
});
|
||||
|
||||
const promosList = computed(() => {
|
||||
if (Array.isArray(props.options?.promos) && props.options.promos.length > 0) {
|
||||
return props.options.promos;
|
||||
}
|
||||
return [
|
||||
{ title: '大牌补贴', desc: '领券最高减100', tag: '限时' },
|
||||
{ title: '新品驾到', desc: '官方正品首发', tag: '新品' }
|
||||
];
|
||||
});
|
||||
|
||||
const defaultGoods = [
|
||||
{ id: '1', goodsName: '旗舰 5G 智能手机 12GB+256GB 超光影影像系统', price: '3999.00', originalPrice: '4599.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_1.png' },
|
||||
{ id: '2', goodsName: '无线头戴式深度主动降噪蓝牙耳机 空间音频Hi-Res', price: '1099.00', originalPrice: '1499.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_2.png' },
|
||||
{ id: '3', goodsName: '14英寸 全面屏轻薄商务笔记本电脑 酷睿i7 高刷防眩光', price: '5699.00', originalPrice: '6299.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_3.png' },
|
||||
{ id: '4', goodsName: '运动健康专业监测智能手表 心率血氧长续航50米防水', price: '899.00', originalPrice: '1199.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_4.png' },
|
||||
{ id: '5', goodsName: '意式半自动家用复古咖啡机 20Bar高压恒温萃取蒸汽奶泡', price: '899.00', originalPrice: '1280.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_6.png' },
|
||||
{ id: '6', goodsName: '奢宠酵母焕活修护精华露 50ml 紧致淡纹抗皱滋养', price: '680.00', originalPrice: '890.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/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'
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tpl-channel-floor {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
user-select: none;
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.subtitle-text {
|
||||
font-size: 13px;
|
||||
color: #94a3b8;
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.header-more {
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
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;
|
||||
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;
|
||||
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 {
|
||||
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: 14px;
|
||||
color: #1e293b;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
height: 40px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.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>
|
||||
281
seller/src/views/shop/pc/template/TplChannelGrid.vue
Normal file
281
seller/src/views/shop/pc/template/TplChannelGrid.vue
Normal file
@@ -0,0 +1,281 @@
|
||||
<template>
|
||||
<div class="tpl-channel-grid" :style="customStyle">
|
||||
<div
|
||||
class="grid-wrap"
|
||||
:style="{
|
||||
gridTemplateColumns: `repeat(${gridColumns}, 1fr)`,
|
||||
gap: `${options.gap ?? 12}px`
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-for="(item, idx) in options.items"
|
||||
:key="idx"
|
||||
class="channel-card"
|
||||
:class="[
|
||||
`is-${layoutMode}`,
|
||||
{ 'is-compact': isCompact }
|
||||
]"
|
||||
@click="handleClickItem(item)"
|
||||
>
|
||||
<!-- 图标/图片 -->
|
||||
<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-text">
|
||||
<div class="main-title">{{ item.title || '频道名称' }}</div>
|
||||
<div class="sub-title">{{ item.subtitle || '副标题文案' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</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'
|
||||
};
|
||||
});
|
||||
|
||||
function handleClickItem(item) {
|
||||
if (item && item.url) {
|
||||
console.log('Navigate to:', item.url);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tpl-channel-grid {
|
||||
width: 100%;
|
||||
user-select: none;
|
||||
box-sizing: border-box;
|
||||
|
||||
.grid-wrap {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
|
||||
.channel-card {
|
||||
background: #ffffff;
|
||||
border: 1px solid #f1f5f9;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
transition: all 0.2s 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-text .main-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-text {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
.main-title {
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
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-text {
|
||||
flex: 1;
|
||||
gap: 3px;
|
||||
text-align: left;
|
||||
|
||||
.main-title {
|
||||
font-size: 14px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
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-text {
|
||||
width: 100%;
|
||||
gap: 2px;
|
||||
text-align: center;
|
||||
|
||||
.main-title {
|
||||
font-size: 13px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 六分之一紧凑模式
|
||||
&.is-compact {
|
||||
padding: 10px 6px 8px 6px;
|
||||
gap: 5px;
|
||||
|
||||
.card-thumb {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.card-text {
|
||||
gap: 1px;
|
||||
|
||||
.main-title {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
font-size: 11px;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
50
seller/src/views/shop/pc/template/TplFreeLayout.vue
Normal file
50
seller/src/views/shop/pc/template/TplFreeLayout.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div class="tpl-free-layout" :style="customStyle">
|
||||
<div class="layout-hint">
|
||||
<span>自由布局画布区 (高度: {{ options.height || 260 }}px)</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 || 450}px`,
|
||||
backgroundColor: s.backgroundColor || '#f8fafc',
|
||||
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>
|
||||
.tpl-free-layout {
|
||||
width: 100%;
|
||||
border: 1px dashed #cbd5e1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
user-select: none;
|
||||
box-sizing: border-box;
|
||||
|
||||
.layout-hint {
|
||||
color: #94a3b8;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
301
seller/src/views/shop/pc/template/TplGoods.vue
Normal file
301
seller/src/views/shop/pc/template/TplGoods.vue
Normal file
@@ -0,0 +1,301 @@
|
||||
<template>
|
||||
<div class="tpl-goods-list" :style="customStyle">
|
||||
<!-- 标题栏 -->
|
||||
<div class="floor-title-bar" v-if="options.showTitle !== false && options.title">
|
||||
<div class="title-left">
|
||||
<span class="main-title">{{ options.title }}</span>
|
||||
<span class="sub-title" v-if="options.subtitle">{{ options.subtitle }}</span>
|
||||
<span class="category-tag-badge" v-if="options.dataSource === 'category' && (options.categoryName || options.categoryId)">
|
||||
🏷️ {{ options.categoryName || `分类: ${options.categoryId}` }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="more-link" v-if="options.showMoreLink !== false">
|
||||
{{ options.moreText || '查看更多' }} >
|
||||
</span>
|
||||
</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"
|
||||
>
|
||||
<div 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>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="goods-name" v-if="options.showTitle !== false">
|
||||
{{ g.goodsName }}
|
||||
</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="加入购物车"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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 defaultThumb = 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_1.png';
|
||||
|
||||
const defaultMockGoods = [
|
||||
{ id: '1', goodsName: '旗舰 5G 智能手机 12GB+256GB 超光影影像系统', price: '3999.00', originalPrice: '4599.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_1.png' },
|
||||
{ id: '2', goodsName: '无线头戴式深度主动降噪蓝牙耳机 空间音频Hi-Res', price: '1099.00', originalPrice: '1499.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_2.png' },
|
||||
{ id: '3', goodsName: '14英寸 全面屏轻薄商务笔记本电脑 酷睿i7 高刷防眩光', price: '5699.00', originalPrice: '6299.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_3.png' },
|
||||
{ id: '4', goodsName: '运动健康专业监测智能手表 心率血氧长续航50米防水', price: '899.00', originalPrice: '1199.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_4.png' },
|
||||
{ id: '5', goodsName: '奢宠酵母焕活修护精华露 50ml 紧致淡纹抗皱滋养', price: '680.00', originalPrice: '890.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_5.png' },
|
||||
{ id: '6', goodsName: '意式半自动家用复古咖啡机 20Bar高压恒温萃取蒸汽奶泡', price: '899.00', originalPrice: '1280.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_6.png' },
|
||||
{ id: '7', goodsName: '碳板专业竞速轻量缓震跑步鞋 透气防滑男女同款潮鞋', price: '459.00', originalPrice: '699.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_7.png' },
|
||||
{ id: '8', goodsName: '极简静音无雾负离子香薰加湿器 大容量客厅卧室通用', price: '299.00', originalPrice: '399.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_8.png' },
|
||||
{ id: '9', goodsName: '折叠屏旗舰手机 典藏陶瓷白 悬停双旋水滴铰链', price: '6999.00', originalPrice: '7999.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_1.png' },
|
||||
{ id: '10', goodsName: '真无线触控降噪蓝牙耳机 超低延迟游戏降噪', price: '599.00', originalPrice: '799.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_2.png' },
|
||||
{ id: '11', goodsName: '国AA级减蓝光润眼长臂台灯 智能感光多档色温无极调光', price: '249.00', originalPrice: '359.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_4.png' },
|
||||
{ id: '12', goodsName: '航空级铝镁合金静音万向轮拉杆登机箱 20英寸防刮耐磨', price: '499.00', originalPrice: '699.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_7.png' }
|
||||
];
|
||||
|
||||
const isGoodsCard = computed(() => {
|
||||
return props.component?.type === 'goodsCard' || props.options?.type === 'goodsCard';
|
||||
});
|
||||
|
||||
const actualColumns = computed(() => {
|
||||
// 商品列表整行默认 6 列
|
||||
let baseCols = Number(props.options?.columns) || 6;
|
||||
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 (props.options.dataSource === 'manual' && Array.isArray(props.options.goods) && props.options.goods.length > 0) {
|
||||
return props.options.goods;
|
||||
}
|
||||
const count = props.options.goodsCount || (actualColumns.value * 2) || 12;
|
||||
return defaultMockGoods.slice(0, count);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tpl-goods-list {
|
||||
width: 100%;
|
||||
user-select: none;
|
||||
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: 18px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.category-tag-badge {
|
||||
font-size: 11px;
|
||||
background: #eff6ff;
|
||||
border: 1px solid #bfdbfe;
|
||||
color: #2563eb;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.more-link {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
|
||||
.card-thumb-wrap {
|
||||
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;
|
||||
}
|
||||
|
||||
.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: 14px;
|
||||
font-weight: 500;
|
||||
color: #1e293b;
|
||||
line-height: 1.4;
|
||||
height: 40px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
329
seller/src/views/shop/pc/template/TplGoodsGrid.vue
Normal file
329
seller/src/views/shop/pc/template/TplGoodsGrid.vue
Normal file
@@ -0,0 +1,329 @@
|
||||
<template>
|
||||
<div class="tpl-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>
|
||||
<div class="grid-more" v-if="options.showMoreLink !== false">
|
||||
<span>{{ options.moreText || '查看全部' }} ></span>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<div class="cell-thumb-box">
|
||||
<img :src="item.thumbnail || defaultThumb" :alt="item.goodsName" class="cell-thumb" />
|
||||
</div>
|
||||
|
||||
<div class="cell-body">
|
||||
<div class="cell-name">{{ item.goodsName }}</div>
|
||||
<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="立即选购">+</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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 defaultThumb = 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/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; // 100% 默认 5 列宫格
|
||||
});
|
||||
|
||||
const defaultGridGoods = [
|
||||
{ id: '1', goodsName: '旗舰 5G 智能手机 12GB+256GB 超光影影像系统', price: '3999.00', originalPrice: '4599.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_1.png' },
|
||||
{ id: '2', goodsName: '无线头戴式深度主动降噪蓝牙耳机 空间音频Hi-Res', price: '1099.00', originalPrice: '1499.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_2.png' },
|
||||
{ id: '3', goodsName: '14英寸 全面屏轻薄商务笔记本电脑 酷睿i7 高刷防眩光', price: '5699.00', originalPrice: '6299.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_3.png' },
|
||||
{ id: '4', goodsName: '运动健康专业监测智能手表 心率血氧长续航50米防水', price: '899.00', originalPrice: '1199.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_4.png' },
|
||||
{ id: '5', goodsName: '奢宠酵母焕活修护精华露 50ml 紧致淡纹抗皱滋养', price: '680.00', originalPrice: '890.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_5.png' },
|
||||
{ id: '6', goodsName: '意式半自动家用复古咖啡机 20Bar高压恒温萃取蒸汽奶泡', price: '899.00', originalPrice: '1280.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_6.png' },
|
||||
{ id: '7', goodsName: '碳板专业竞速轻量缓震跑步鞋 透气防滑男女同款潮鞋', price: '459.00', originalPrice: '699.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_7.png' },
|
||||
{ id: '8', goodsName: '极简静音无雾负离子香薰加湿器 大容量客厅卧室通用', price: '299.00', originalPrice: '399.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_8.png' },
|
||||
{ id: '9', goodsName: '智能降噪AI翻译蓝牙墨镜 超轻防紫外线偏光太阳镜', price: '599.00', originalPrice: '799.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_1.png' },
|
||||
{ id: '10', goodsName: '15.6英寸 4K超高清便携式显示器 Type-C一线直连高色域', price: '1299.00', originalPrice: '1699.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_3.png' }
|
||||
];
|
||||
|
||||
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'
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tpl-goods-grid {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
user-select: none;
|
||||
|
||||
.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;
|
||||
cursor: pointer;
|
||||
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 {
|
||||
width: 100%;
|
||||
aspect-ratio: 1 / 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.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: 14px;
|
||||
color: #334155;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
height: 40px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.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>
|
||||
534
seller/src/views/shop/pc/template/TplGridFloor.vue
Normal file
534
seller/src/views/shop/pc/template/TplGridFloor.vue
Normal file
@@ -0,0 +1,534 @@
|
||||
<template>
|
||||
<div class="tpl-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>
|
||||
<span class="floor-title-text">{{ options.title || '潮流数码' }}</span>
|
||||
<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">
|
||||
<span
|
||||
v-for="(tag, idx) in subCategories"
|
||||
:key="idx"
|
||||
class="tag-item"
|
||||
:class="{ active: activeTagIdx === idx }"
|
||||
@click="activeTagIdx = idx"
|
||||
>
|
||||
{{ tag }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="more-link">查看更多 ></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 楼层主体:左侧竖向营销海报 + 右侧商品网格 -->
|
||||
<div class="floor-main-body" :class="{ 'no-poster': !showPoster }">
|
||||
<!-- 左侧营销大促海报 -->
|
||||
<div class="floor-left-poster" v-if="showPoster">
|
||||
<div class="poster-inner">
|
||||
<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>
|
||||
</div>
|
||||
</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"
|
||||
>
|
||||
<div 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>
|
||||
</div>
|
||||
|
||||
<div class="card-detail">
|
||||
<div class="goods-name-line">{{ g.goodsName }}</div>
|
||||
<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="查看商品">
|
||||
<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';
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
componentStyle: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const defaultThumb = 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/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?.bannerImg || props.options?.posterImg || 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/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; // 100% 默认 4 列
|
||||
});
|
||||
|
||||
const defaultFloorGoods = [
|
||||
{ id: '101', goodsName: '旗舰 5G 智能手机 12GB+256GB 超光影影像系统', price: '3999.00', originalPrice: '4599.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_1.png' },
|
||||
{ id: '102', goodsName: '无线头戴式深度主动降噪蓝牙耳机 空间音频Hi-Res', price: '1099.00', originalPrice: '1499.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_2.png' },
|
||||
{ id: '103', goodsName: '14英寸 全面屏轻薄商务笔记本电脑 酷睿i7 高刷防眩光', price: '5699.00', originalPrice: '6299.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_3.png' },
|
||||
{ id: '104', goodsName: '运动健康专业监测智能手表 心率血氧长续航50米防水', price: '899.00', originalPrice: '1199.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_4.png' },
|
||||
{ id: '105', goodsName: '奢宠酵母焕活修护精华露 50ml 紧致淡纹抗皱滋养', price: '680.00', originalPrice: '890.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_5.png' },
|
||||
{ id: '106', goodsName: '意式半自动家用复古咖啡机 20Bar高压恒温萃取蒸汽奶泡', price: '899.00', originalPrice: '1280.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_6.png' },
|
||||
{ id: '107', goodsName: '碳板专业竞速轻量缓震跑步鞋 透气防滑男女同款潮鞋', price: '459.00', originalPrice: '699.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_7.png' },
|
||||
{ id: '108', goodsName: '极简静音无雾负离子香薰加湿器 大容量客厅卧室通用', price: '299.00', originalPrice: '399.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/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'
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tpl-grid-floor {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
user-select: none;
|
||||
|
||||
/* 楼层大标题栏 */
|
||||
.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;
|
||||
}
|
||||
|
||||
.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;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover, &.active {
|
||||
color: #2563eb;
|
||||
background: #eff6ff;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.more-link {
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
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-inner {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 520px;
|
||||
|
||||
.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;
|
||||
cursor: pointer;
|
||||
|
||||
&: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 {
|
||||
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: 14px;
|
||||
color: #1e293b;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
height: 40px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.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>
|
||||
135
seller/src/views/shop/pc/template/TplHotZone.vue
Normal file
135
seller/src/views/shop/pc/template/TplHotZone.vue
Normal file
@@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<div class="tpl-hot-zone" :style="customStyle">
|
||||
<div class="zone-wrap" v-if="displayImageUrl">
|
||||
<img :src="displayImageUrl" alt="hotzone" />
|
||||
<div
|
||||
v-for="(zone, idx) in displayZones"
|
||||
:key="idx"
|
||||
class="zone-mask"
|
||||
:style="{
|
||||
top: `${zone.top !== undefined ? zone.top : ((zone.topPer || 0) * 100)}%`,
|
||||
left: `${zone.left !== undefined ? zone.left : ((zone.leftPer || 0) * 100)}%`,
|
||||
width: `${zone.width !== undefined ? zone.width : ((zone.widthPer || 0.1) * 100)}%`,
|
||||
height: `${zone.height !== undefined ? zone.height : ((zone.heightPer || 0.1) * 100)}%`
|
||||
}"
|
||||
>
|
||||
<span class="zone-label">{{ zone.title || `热区 ${idx + 1}` }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="hotzone-empty">
|
||||
<span>热区图待配置底图</span>
|
||||
</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 || 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/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>
|
||||
.tpl-hot-zone {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
box-sizing: border-box;
|
||||
|
||||
.zone-wrap {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
border-radius: inherit;
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
display: block;
|
||||
border-radius: inherit;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.zone-mask {
|
||||
position: absolute;
|
||||
background: rgba(37, 99, 235, 0.22);
|
||||
border: 1.5px dashed #2563eb;
|
||||
color: #ffffff;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
backdrop-filter: blur(1px);
|
||||
box-shadow: 0 0 10px rgba(37, 99, 235, 0.35);
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(37, 99, 235, 0.35);
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
.zone-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.8);
|
||||
padding: 3px 6px;
|
||||
background: rgba(15, 23, 42, 0.65);
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
max-width: 90%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hotzone-empty {
|
||||
padding: 40px;
|
||||
background: #f8fafc;
|
||||
border: 1px dashed #cbd5e1;
|
||||
text-align: center;
|
||||
color: #94a3b8;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
298
seller/src/views/shop/pc/template/TplNotice.vue
Normal file
298
seller/src/views/shop/pc/template/TplNotice.vue
Normal file
@@ -0,0 +1,298 @@
|
||||
<template>
|
||||
<div
|
||||
class="tpl-notice-bar"
|
||||
:class="[
|
||||
isMini ? 'is-mini' : (isCompact ? 'is-compact' : '')
|
||||
]"
|
||||
:style="customStyle"
|
||||
@mouseenter="stopTimer"
|
||||
@mouseleave="startTimer"
|
||||
>
|
||||
<!-- 左侧快讯徽章 -->
|
||||
<div class="notice-badge-box">
|
||||
<img
|
||||
v-if="options.badgeImg || options.icon"
|
||||
:src="options.badgeImg || options.icon"
|
||||
class="badge-custom-img"
|
||||
alt="icon"
|
||||
/>
|
||||
<div v-else class="badge-icon">
|
||||
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
|
||||
<path d="M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-7 12h-2v-2h2v2zm0-4h-2V6h2v4z"/>
|
||||
</svg>
|
||||
</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>
|
||||
<span class="notice-item-title">{{ item.title || item.text }}</span>
|
||||
<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">
|
||||
<span class="more-text">{{ options.moreText || '更多' }} ></span>
|
||||
</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>
|
||||
.tpl-notice-bar {
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
user-select: none;
|
||||
background: #ffffff;
|
||||
border: 1px solid #eef2f6;
|
||||
gap: 12px;
|
||||
border-radius: 8px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
overflow: hidden;
|
||||
|
||||
&.is-mini {
|
||||
gap: 6px;
|
||||
padding: 0 8px !important;
|
||||
|
||||
.notice-badge-box {
|
||||
.badge-title, .badge-divider {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-compact {
|
||||
gap: 8px;
|
||||
padding: 0 10px !important;
|
||||
|
||||
.notice-badge-box {
|
||||
.badge-divider {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notice-badge-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.badge-icon {
|
||||
color: #f97316;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.badge-custom-img {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
object-fit: contain;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.badge-title {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.badge-divider {
|
||||
width: 1px;
|
||||
height: 14px;
|
||||
background: #cbd5e1;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.notice-content-viewport {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 40px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
.notice-items-strip {
|
||||
transition: transform 0.5s cubic-bezier(0.25, 1, 0.5, 1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.notice-strip-item {
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
box-sizing: border-box;
|
||||
|
||||
.notice-item-tag {
|
||||
font-size: 11px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.tag-new {
|
||||
background: #eff6ff;
|
||||
color: #2563eb;
|
||||
border: 1px solid #bfdbfe;
|
||||
}
|
||||
|
||||
&.tag-promo {
|
||||
background: #fef2f2;
|
||||
color: #ef4444;
|
||||
border: 1px solid #fecaca;
|
||||
}
|
||||
|
||||
&.tag-notice {
|
||||
background: #fffbeb;
|
||||
color: #d97706;
|
||||
border: 1px solid #fde68a;
|
||||
}
|
||||
|
||||
&.tag-hot {
|
||||
background: #fdf2f8;
|
||||
color: #db2777;
|
||||
border: 1px solid #fbcfe8;
|
||||
}
|
||||
}
|
||||
|
||||
.notice-item-title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 13px;
|
||||
color: #334155;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.notice-item-time {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notice-more-action {
|
||||
flex-shrink: 0;
|
||||
|
||||
.more-text {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
129
seller/src/views/shop/pc/template/TplPromoGroup.vue
Normal file
129
seller/src/views/shop/pc/template/TplPromoGroup.vue
Normal file
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<div class="tpl-promo-group" :style="customStyle">
|
||||
<div
|
||||
class="promo-grid"
|
||||
:style="{ gridTemplateColumns: `repeat(${actualColumns}, 1fr)` }"
|
||||
>
|
||||
<div
|
||||
v-for="(card, idx) in cardsList"
|
||||
:key="idx"
|
||||
class="card-item"
|
||||
:style="card.bgImg ? { backgroundImage: `url(${card.bgImg})` } : {}"
|
||||
>
|
||||
<span class="m-t">{{ card.title }}</span>
|
||||
<span class="s-t">{{ card.subtitle }}</span>
|
||||
<span class="btn">立即查看 ></span>
|
||||
</div>
|
||||
</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: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/promo-group/promo_1.png', url: '/goodsList?seckill=1' },
|
||||
{ title: '品牌特惠', subtitle: '大牌买一送一 · 领百元神券', bgImg: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/promo-group/promo_2.png', url: '/goodsList?brand=1' },
|
||||
{ title: '新品首发', subtitle: '首单专享特惠 · 1元起尝鲜', bgImg: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/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>
|
||||
.tpl-promo-group {
|
||||
width: 100%;
|
||||
user-select: none;
|
||||
box-sizing: border-box;
|
||||
|
||||
.promo-grid {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
|
||||
.card-item {
|
||||
padding: 24px;
|
||||
min-height: 150px;
|
||||
background-color: #1e293b;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
color: #ffffff;
|
||||
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08);
|
||||
|
||||
.m-t {
|
||||
font-size: 22px;
|
||||
font-weight: 800;
|
||||
color: #ffffff;
|
||||
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.s-t {
|
||||
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);
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
align-self: flex-start;
|
||||
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>
|
||||
658
seller/src/views/shop/pc/template/TplRecommendFeed.vue
Normal file
658
seller/src/views/shop/pc/template/TplRecommendFeed.vue
Normal file
@@ -0,0 +1,658 @@
|
||||
<template>
|
||||
<div class="tpl-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>
|
||||
<div class="big-card-thumb">
|
||||
<img :src="featuredGoods.thumbnail || defaultThumb" :alt="featuredGoods.goodsName" />
|
||||
</div>
|
||||
<div class="big-card-info">
|
||||
<div class="big-title">{{ featuredGoods.goodsName }}</div>
|
||||
<div class="big-reason">官方甄选 · 猜你对这款高人气商品感兴趣</div>
|
||||
|
||||
<!-- 核心特色标签 -->
|
||||
<div class="big-tags-row">
|
||||
<span class="feat-tag highlight">🔥 镇店爆品</span>
|
||||
<span class="feat-tag">🛡️ 官方正品</span>
|
||||
<span class="feat-tag">⚡ 极速发货</span>
|
||||
<span class="feat-tag">🏷️ 售后无忧</span>
|
||||
</div>
|
||||
|
||||
<!-- 权威热度指标 -->
|
||||
<div class="big-stats-bar">
|
||||
<div class="stat-cell">
|
||||
<span class="stat-val">100%</span>
|
||||
<span class="stat-lbl">品质正品</span>
|
||||
</div>
|
||||
<div class="stat-line"></div>
|
||||
<div class="stat-cell">
|
||||
<span class="stat-val">极速达</span>
|
||||
<span class="stat-lbl">顺丰速运</span>
|
||||
</div>
|
||||
<div class="stat-line"></div>
|
||||
<div class="stat-cell">
|
||||
<span class="stat-val">TOP 1</span>
|
||||
<span class="stat-lbl">品类热榜</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 服务保障权益 -->
|
||||
<div class="big-guarantees">
|
||||
<div class="guar-item"><span class="guar-icon">✓</span> 7天无理由退换货</div>
|
||||
<div class="guar-item"><span class="guar-icon">✓</span> 赠运费险 · 破损包赔</div>
|
||||
</div>
|
||||
|
||||
<div class="big-price-row">
|
||||
<div class="big-price">
|
||||
<span class="sym">¥</span>
|
||||
<span class="val">{{ featuredGoods.price }}</span>
|
||||
<span class="del" v-if="featuredGoods.originalPrice">¥{{ featuredGoods.originalPrice }}</span>
|
||||
</div>
|
||||
<button type="button" class="big-buy-btn">立即入手 ></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧与后续标准卡片网格 -->
|
||||
<div class="featured-sub-grid">
|
||||
<div
|
||||
v-for="(item, idx) in regularGoods"
|
||||
:key="item.id || idx"
|
||||
class="recommend-card"
|
||||
>
|
||||
<div class="card-thumb">
|
||||
<img :src="item.thumbnail || defaultThumb" :alt="item.goodsName" />
|
||||
<span class="rec-tag">推荐</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="goods-title">{{ item.goodsName }}</div>
|
||||
<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">+</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"
|
||||
>
|
||||
<div class="card-thumb">
|
||||
<img :src="item.thumbnail || defaultThumb" :alt="item.goodsName" />
|
||||
<span class="rec-tag">为你推荐</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="goods-title">{{ item.goodsName }}</div>
|
||||
<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">+</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部换一批 / 探索更多推荐 -->
|
||||
<div class="recommend-footer-action">
|
||||
<button type="button" class="refresh-rec-btn">
|
||||
<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';
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
componentStyle: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const defaultThumb = 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_1.png';
|
||||
|
||||
const defaultPool = [
|
||||
{ id: '1', goodsName: '旗舰 5G 智能手机 12GB+256GB 超光影影像系统', price: '3999.00', originalPrice: '4599.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_1.png' },
|
||||
{ id: '2', goodsName: '无线头戴式深度主动降噪蓝牙耳机 空间音频Hi-Res', price: '1099.00', originalPrice: '1499.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_2.png' },
|
||||
{ id: '3', goodsName: '14英寸 全面屏轻薄商务笔记本电脑 酷睿i7 高刷防眩光', price: '5699.00', originalPrice: '6299.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_3.png' },
|
||||
{ id: '4', goodsName: '运动健康专业监测智能手表 心率血氧长续航50米防水', price: '899.00', originalPrice: '1199.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_4.png' },
|
||||
{ id: '5', goodsName: '奢宠酵母焕活修护精华露 50ml 紧致淡纹抗皱滋养', price: '680.00', originalPrice: '890.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_5.png' },
|
||||
{ id: '6', goodsName: '意式半自动家用复古咖啡机 20Bar高压恒温萃取蒸汽奶泡', price: '899.00', originalPrice: '1280.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_6.png' },
|
||||
{ id: '7', goodsName: '碳板专业竞速轻量缓震跑步鞋 透气防滑男女同款潮鞋', price: '459.00', originalPrice: '699.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_7.png' },
|
||||
{ id: '8', goodsName: '极简静音无雾负离子香薰加湿器 大容量客厅卧室通用', price: '299.00', originalPrice: '399.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_8.png' },
|
||||
{ id: '9', goodsName: '智能降噪AI翻译蓝牙墨镜 超轻防紫外线偏光太阳镜', price: '599.00', originalPrice: '799.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_1.png' }
|
||||
];
|
||||
|
||||
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'
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tpl-recommend-feed {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
user-select: none;
|
||||
|
||||
.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 {
|
||||
width: 100%;
|
||||
aspect-ratio: 1 / 1;
|
||||
overflow: hidden;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 12px;
|
||||
background: #f8fafc;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
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;
|
||||
color: #0f172a;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.big-reason {
|
||||
font-size: 12px;
|
||||
color: #f97316;
|
||||
background: #fff7ed;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.big-tags-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.feat-tag {
|
||||
font-size: 11px;
|
||||
color: #475569;
|
||||
background: #f1f5f9;
|
||||
padding: 2px 7px;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
|
||||
&.highlight {
|
||||
color: #dc2626;
|
||||
background: #fee2e2;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.big-stats-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
padding: 8px 6px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.stat-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
|
||||
.stat-val {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.stat-lbl {
|
||||
font-size: 10px;
|
||||
color: #64748b;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-line {
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
background: #cbd5e1;
|
||||
}
|
||||
}
|
||||
|
||||
.big-guarantees {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 6px 10px;
|
||||
background: #fbfbfb;
|
||||
border-left: 3px solid #ef4444;
|
||||
border-radius: 0 4px 4px 0;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.guar-item {
|
||||
font-size: 11px;
|
||||
color: #475569;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
.guar-icon {
|
||||
color: #16a34a;
|
||||
font-weight: 700;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.big-price-row {
|
||||
margin-top: auto;
|
||||
display: flex;
|
||||
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 {
|
||||
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: 14px;
|
||||
color: #1e293b;
|
||||
line-height: 1.4;
|
||||
height: 40px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.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>
|
||||
147
seller/src/views/shop/pc/template/TplTitle.vue
Normal file
147
seller/src/views/shop/pc/template/TplTitle.vue
Normal file
@@ -0,0 +1,147 @@
|
||||
<template>
|
||||
<div
|
||||
class="tpl-title-bar"
|
||||
:class="[
|
||||
`align-${options.align || 'left'}`,
|
||||
isMini ? 'is-mini' : (isCompact ? 'is-compact' : '')
|
||||
]"
|
||||
>
|
||||
<div class="title-content">
|
||||
<img
|
||||
v-if="options.titleImg || options.icon"
|
||||
:src="options.titleImg || options.icon"
|
||||
class="title-custom-icon"
|
||||
alt="icon"
|
||||
/>
|
||||
<span class="main-t">{{ options.mainTitle || '楼层主标题' }}</span>
|
||||
<span class="sub-t" v-if="options.subtitle">{{ options.subtitle }}</span>
|
||||
</div>
|
||||
<span class="more-link" v-if="options.showMore">{{ options.moreText || '查看更多 >' }}</span>
|
||||
</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>
|
||||
.tpl-title-bar {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
user-select: none;
|
||||
box-sizing: border-box;
|
||||
gap: 8px;
|
||||
|
||||
&.align-center {
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
.more-link {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.title-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 10px;
|
||||
min-width: 0;
|
||||
|
||||
.title-custom-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
object-fit: contain;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.main-t {
|
||||
font-size: 22px;
|
||||
font-weight: 800;
|
||||
color: #0f172a;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.sub-t {
|
||||
font-size: 13px;
|
||||
color: #94a3b8;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.more-link {
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&.is-compact {
|
||||
.title-content {
|
||||
.title-custom-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
.main-t {
|
||||
font-size: 18px;
|
||||
}
|
||||
.sub-t {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
.more-link {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-mini {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
|
||||
&.align-center {
|
||||
align-items: center;
|
||||
.more-link {
|
||||
position: static;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.title-content {
|
||||
.title-custom-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
.main-t {
|
||||
font-size: 15px;
|
||||
}
|
||||
.sub-t {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
.more-link {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
136
seller/src/views/shop/pc/template/TplVideo.vue
Normal file
136
seller/src/views/shop/pc/template/TplVideo.vue
Normal file
@@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<div class="tpl-video" :style="customStyle">
|
||||
<div class="video-preview-box">
|
||||
<video
|
||||
v-if="options.videoUrl"
|
||||
:src="options.videoUrl"
|
||||
:poster="options.coverUrl"
|
||||
:controls="options.controls !== false"
|
||||
muted
|
||||
class="video-el"
|
||||
/>
|
||||
<template v-else>
|
||||
<img
|
||||
:src="options.coverUrl || 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/video/video_cover.png'"
|
||||
alt="cover"
|
||||
class="cover-img"
|
||||
/>
|
||||
<div class="play-btn">▶</div>
|
||||
</template>
|
||||
|
||||
<!-- 链接徽标(装修态提示) -->
|
||||
<div class="video-link-badge" v-if="options.url" :title="options.url">
|
||||
<span class="badge-icon">🔗</span>
|
||||
<span class="badge-text">{{ options.url }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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 || 450}px`,
|
||||
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>
|
||||
.tpl-video {
|
||||
width: 100%;
|
||||
background: #0f172a;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
box-sizing: border-box;
|
||||
|
||||
.video-preview-box {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.video-el {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.cover-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.play-btn {
|
||||
position: absolute;
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
border-radius: 50%;
|
||||
background: rgba(225, 29, 72, 0.9);
|
||||
color: #ffffff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 22px;
|
||||
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35);
|
||||
transition: transform 0.2s ease;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.08);
|
||||
}
|
||||
}
|
||||
|
||||
.video-link-badge {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
max-width: 60%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 4px 10px;
|
||||
background: rgba(15, 23, 42, 0.75);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 20px;
|
||||
backdrop-filter: blur(4px);
|
||||
font-size: 11px;
|
||||
color: #e2e8f0;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
|
||||
.badge-icon {
|
||||
font-size: 11px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.badge-text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
53
seller/src/views/shop/pc/template/index.js
Normal file
53
seller/src/views/shop/pc/template/index.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import TplCategoryNav from './TplCategoryNav.vue';
|
||||
import TplChannelGrid from './TplChannelGrid.vue';
|
||||
import TplCategoryMegaMenu from './TplCategoryMegaMenu.vue';
|
||||
import TplCarousel from './TplCarousel.vue';
|
||||
import TplBrandWall from './TplBrandWall.vue';
|
||||
import TplHotZone from './TplHotZone.vue';
|
||||
import TplFreeLayout from './TplFreeLayout.vue';
|
||||
import TplPromoGroup from './TplPromoGroup.vue';
|
||||
import TplVideo from './TplVideo.vue';
|
||||
import TplGoods from './TplGoods.vue';
|
||||
import TplGridFloor from './TplGridFloor.vue';
|
||||
import TplGoodsGrid from './TplGoodsGrid.vue';
|
||||
import TplChannelFloor from './TplChannelFloor.vue';
|
||||
import TplRecommendFeed from './TplRecommendFeed.vue';
|
||||
import TplTitle from './TplTitle.vue';
|
||||
import TplNotice from './TplNotice.vue';
|
||||
|
||||
export const CANVAS_TEMPLATE_MAP = {
|
||||
// 结构 (3)
|
||||
|
||||
// 导航 (3)
|
||||
categoryNav: TplCategoryNav,
|
||||
channelGrid: TplChannelGrid,
|
||||
categoryMegaMenu: TplCategoryMegaMenu,
|
||||
|
||||
// 广告 (6)
|
||||
carousel: TplCarousel,
|
||||
brandWall: TplBrandWall,
|
||||
hotZone: TplHotZone,
|
||||
freeLayout: TplFreeLayout,
|
||||
promoGroup: TplPromoGroup,
|
||||
video: TplVideo,
|
||||
|
||||
// 商品 (8)
|
||||
goodsList: TplGoods,
|
||||
gridFloor: TplGridFloor,
|
||||
goodsGrid: TplGoodsGrid,
|
||||
goodsCard: TplGoods,
|
||||
bannerFloor: TplGridFloor,
|
||||
goodsBar: TplGoods,
|
||||
channelFloor: TplChannelFloor,
|
||||
recommendFeed: TplRecommendFeed,
|
||||
|
||||
// 营销 (1)
|
||||
|
||||
// 内容 (3)
|
||||
titleBar: TplTitle,
|
||||
noticeBar: TplNotice
|
||||
};
|
||||
|
||||
export function resolveCanvasTemplate(type) {
|
||||
return CANVAS_TEMPLATE_MAP[type] || null;
|
||||
}
|
||||
@@ -1,20 +1,9 @@
|
||||
### 楼层装修页面
|
||||
# 店铺楼层装修
|
||||
|
||||
#### floorList.vue 楼层列表
|
||||
- floorList.vue:PC 楼层页面列表。
|
||||
- renovation.vue:PC 装修入口,加载 pc/index.vue。
|
||||
- pc/:新版 PC 行布局装修器,包含组件配置、配置面板和画布模板。
|
||||
- wap/:移动端楼层装修。
|
||||
- hotzone/:装修图片热区编辑器。
|
||||
|
||||
#### renovation.vue 楼层装修主页面
|
||||
|
||||
#### singleConfig.vue 右侧配置项
|
||||
|
||||
#### modelForm.vue 中间展示所选模块
|
||||
|
||||
#### modelFormItem.vue 单个模块样式
|
||||
|
||||
#### modelConfig.js 模块数据
|
||||
|
||||
#### modelList文件夹为添加的模块
|
||||
|
||||
### wap文件夹为移动端楼层装修
|
||||
|
||||
|
||||
### draggable中文文档 (http://www.itxst.com/vue-draggable/tutorial.html)
|
||||
旧版 PC 装修器及 modelList 组件已移除。
|
||||
|
||||
@@ -1,256 +1,7 @@
|
||||
<template>
|
||||
<div class="renovation">
|
||||
<div class="btn-bar">
|
||||
<el-button type="primary" :loading="submitLoading" @click="saveTemplate">保存模板</el-button>
|
||||
<el-button class="ml_10" @click="resetTemplate">还原模板</el-button>
|
||||
<el-button class="ml_10" @click="witeLocalStore">将装修内容写入到本地</el-button>
|
||||
<el-button class="ml_10" v-if="hasCache" @click="clearCache">清空本地装修缓存</el-button>
|
||||
</div>
|
||||
<div class="renovation-main">
|
||||
<div class="model-list">
|
||||
<div class="classification-title">基础模块</div>
|
||||
<draggable
|
||||
tag="ul"
|
||||
:list="modelData"
|
||||
:item-key="getModelKey"
|
||||
:clone="cloneModel"
|
||||
:group="{ name: 'model', pull: 'clone', put: false }"
|
||||
:sort="false"
|
||||
ghost-class="ghost"
|
||||
handle=".model-item"
|
||||
>
|
||||
<template #item="{ element: model }">
|
||||
<li class="model-item">
|
||||
<el-icon><Picture /></el-icon>
|
||||
<span>{{ model.name }}</span>
|
||||
</li>
|
||||
</template>
|
||||
</draggable>
|
||||
</div>
|
||||
<div class="show-content">
|
||||
<model-form ref="modelFormRef" :data="modelForm"></model-form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PcEditor />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { Picture } from "@element-plus/icons-vue";
|
||||
import { modelData } from "./modelConfig";
|
||||
import Draggable from "vuedraggable";
|
||||
import ModelForm from "./modelForm.vue";
|
||||
import * as API_floor from "@/api/other.js";
|
||||
import { getStore, setStore } from "@/libs/storage";
|
||||
import { Message, Modal } from "@/utils/message";
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const hasCache = ref(false);
|
||||
const modelForm = ref({ list: [] });
|
||||
const submitLoading = ref(false);
|
||||
|
||||
const modelFormRef = ref(null);
|
||||
|
||||
onMounted(() => {
|
||||
const cache = getStore('managerPCPageCache')
|
||||
hasCache.value = !!cache;
|
||||
if(cache){
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '获取到本地有缓存数据,是否使用缓存数据?',
|
||||
okText: '使用',
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
applyPageData(cache);
|
||||
}
|
||||
});
|
||||
}
|
||||
getTemplateItem(route.query.id);
|
||||
});
|
||||
|
||||
function getModelKey(model) {
|
||||
return model.type || model.name;
|
||||
}
|
||||
function cloneModel(model) {
|
||||
const key = Date.now() + "_" + Math.ceil(Math.random() * 99999);
|
||||
const cloned = JSON.parse(JSON.stringify(model));
|
||||
return {
|
||||
...cloned,
|
||||
key,
|
||||
model: cloned.type + "_" + key,
|
||||
};
|
||||
}
|
||||
function clearCache(){
|
||||
setStore('managerPCPageCache', '')
|
||||
Message.success('清除成功')
|
||||
}
|
||||
function witeLocalStore(){
|
||||
const data ={...modelForm.value}
|
||||
data.list.unshift(modelFormRef.value.navList);
|
||||
data.list.unshift(modelFormRef.value.topAdvert);
|
||||
setStore('managerPCPageCache', data)
|
||||
Message.success('写入成功')
|
||||
}
|
||||
function saveTemplate() {
|
||||
submitTemplate(route.query.pageShow ? 'OPEN' : 'CLOSE')
|
||||
}
|
||||
function submitTemplate(pageShow) {
|
||||
submitLoading.value = true
|
||||
const modelFormData = JSON.parse(JSON.stringify(modelForm.value))
|
||||
modelFormData.list.unshift(modelFormRef.value.navList);
|
||||
modelFormData.list.unshift(modelFormRef.value.topAdvert);
|
||||
const data = {
|
||||
id: route.query.id,
|
||||
pageData: JSON.stringify(modelFormData),
|
||||
pageShow: route.query.pageType === 'SPECIAL' ? 'CLOSE' : pageShow,
|
||||
pageClientType: 'PC',
|
||||
};
|
||||
API_floor.updateHome(route.query.id, data).then((res) => {
|
||||
submitLoading.value = false
|
||||
if (res.success) {
|
||||
Message.success("保存模板成功");
|
||||
}
|
||||
});
|
||||
}
|
||||
function resetTemplate() {
|
||||
getTemplateItem(route.query.id);
|
||||
}
|
||||
function applyPageData(raw) {
|
||||
const empty = { list: [] };
|
||||
if (raw == null || raw === "") {
|
||||
modelForm.value = empty;
|
||||
return;
|
||||
}
|
||||
let pageData = raw;
|
||||
if (typeof raw === "string") {
|
||||
try {
|
||||
pageData = JSON.parse(raw);
|
||||
} catch {
|
||||
modelForm.value = empty;
|
||||
return;
|
||||
}
|
||||
}
|
||||
const list = Array.isArray(pageData)
|
||||
? pageData
|
||||
: Array.isArray(pageData?.list)
|
||||
? pageData.list
|
||||
: [];
|
||||
const form = Array.isArray(pageData) ? { list } : { ...pageData, list };
|
||||
if (list[0]?.type === "topAdvert") {
|
||||
modelFormRef.value.topAdvert = list[0];
|
||||
if (list[1]) {
|
||||
modelFormRef.value.navList = list[1];
|
||||
}
|
||||
modelForm.value = { ...form, list: list.slice(2) };
|
||||
return;
|
||||
}
|
||||
modelForm.value = { ...form, list };
|
||||
}
|
||||
function getTemplateItem(id) {
|
||||
API_floor.getHomeData(id).then((res) => {
|
||||
if (res.success) {
|
||||
applyPageData(res.result?.pageData);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
watch(modelForm, function (val) {}, { deep: true });
|
||||
import PcEditor from './pc/index.vue';
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.renovation {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100%;
|
||||
}
|
||||
.btn-bar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 99;
|
||||
background: #fff;
|
||||
height: 50px;
|
||||
padding: 10px;
|
||||
box-shadow: 1px 1px 10px #999;
|
||||
}
|
||||
.renovation-main {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 20px;
|
||||
flex: 1;
|
||||
padding-top: 10px;
|
||||
}
|
||||
.model-list {
|
||||
flex-shrink: 0;
|
||||
width: 130px;
|
||||
max-height: calc(100vh - 180px);
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
background: #fff;
|
||||
position: sticky;
|
||||
top: 60px;
|
||||
z-index: 10;
|
||||
box-shadow: 1px 1px 10px #999;
|
||||
.classification-title {
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
:deep(ul) {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.model-item {
|
||||
width: 110px;
|
||||
height: 30px;
|
||||
background: #eee;
|
||||
margin-top: 10px;
|
||||
line-height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
color: #999;
|
||||
transition: 0.15s;
|
||||
border-radius: 4px;
|
||||
&:hover {
|
||||
background: $theme_color;
|
||||
cursor: move;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
.ghost::after {
|
||||
border: none;
|
||||
height: 0;
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
.show-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.ghost {
|
||||
background: #fff;
|
||||
height: 30px;
|
||||
position: relative;
|
||||
&::after {
|
||||
content: "松开鼠标添加模块";
|
||||
position: absolute;
|
||||
background: #fff;
|
||||
border: 1px dashed #409eff;
|
||||
color: #409eff;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
text-align: center;
|
||||
line-height: 50px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user