mirror of
https://gitee.com/beijing_hongye_huicheng/lilishop-ui.git
synced 2026-11-07 01:18:54 +08:00
feat(buyer,manager): 楼层装修组件体系与平台端页面优化
- buyer:楼层装修组件(RowWrapper、组件库、预设、注册表)及首页/商品列表适配 - manager:PC端楼层装修页面、店铺设置、OSS素材管理等页面样式优化
This commit is contained in:
@@ -26,17 +26,16 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-drawer
|
||||
<el-dialog
|
||||
v-else
|
||||
v-model="flag"
|
||||
class="link-selector-drawer"
|
||||
direction="rtl"
|
||||
size="1160px"
|
||||
:with-header="false"
|
||||
width="1160px"
|
||||
top="80px"
|
||||
:z-index="10000"
|
||||
:close-on-click-modal="false"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
class="link-selector-dialog"
|
||||
@close="clickClose"
|
||||
>
|
||||
<linkDialog
|
||||
@@ -52,7 +51,7 @@
|
||||
<el-button @click="clickClose">取消</el-button>
|
||||
<el-button type="primary" @click="clickOK">确定</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
|
||||
@@ -51,6 +51,11 @@ import {
|
||||
Star,
|
||||
Document,
|
||||
User,
|
||||
UserFilled,
|
||||
Service,
|
||||
Ticket,
|
||||
Bell,
|
||||
OfficeBuilding,
|
||||
Promotion,
|
||||
PriceTag,
|
||||
Sunny,
|
||||
@@ -61,27 +66,33 @@ import {
|
||||
ArrowRight,
|
||||
Check,
|
||||
} from "@element-plus/icons-vue";
|
||||
import { ElMessageBox } from "element-plus";
|
||||
|
||||
const emit = defineEmits(["selected"]);
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const linkList = ref([
|
||||
{ title: "首页", icon: markRaw(House), ___type: "home" },
|
||||
{ title: "购物车", icon: markRaw(ShoppingCart), ___type: "cart" },
|
||||
{ title: "收藏商品", icon: markRaw(Star), ___type: "collection" },
|
||||
{ title: "我的订单", icon: markRaw(Document), ___type: "order" },
|
||||
{ title: "个人中心", icon: markRaw(User), ___type: "user" },
|
||||
{ title: "拼团频道", icon: markRaw(Promotion), ___type: "group" },
|
||||
{ title: "秒杀频道", icon: markRaw(Promotion), ___type: "seckill" },
|
||||
{ title: "领券中心", icon: markRaw(PriceTag), ___type: "coupon" },
|
||||
{ title: "签到", icon: markRaw(Sunny), ___type: "sign" },
|
||||
{ title: "小程序直播", icon: markRaw(VideoCamera), ___type: "live" },
|
||||
{ title: "砍价", icon: markRaw(Share), ___type: "kanjia" },
|
||||
{ title: "积分商城", icon: markRaw(ShoppingBag), ___type: "point" },
|
||||
{ title: "首页", icon: markRaw(House), url: "/", ___type: "home" },
|
||||
{ title: "购物车", icon: markRaw(ShoppingCart), url: "/cart", ___type: "cart" },
|
||||
{ title: "我的订单", icon: markRaw(Document), url: "/home/MyOrder", ___type: "order" },
|
||||
{ title: "个人中心", icon: markRaw(User), url: "/home", ___type: "user" },
|
||||
{ title: "基本信息", icon: markRaw(UserFilled), url: "/home/Profile", ___type: "userInfo" },
|
||||
{ title: "我的售后", icon: markRaw(Service), url: "/home/AfterSale", ___type: "afterSale" },
|
||||
{ title: "我的优惠券", icon: markRaw(Ticket), url: "/home/Coupons", ___type: "myCoupons" },
|
||||
{ title: "收藏商品", icon: markRaw(Star), url: "/home/Favorites", ___type: "collection" },
|
||||
{ title: "消息中心", icon: markRaw(Bell), url: "/home/MsgList", ___type: "msg" },
|
||||
{ title: "商家入驻", icon: markRaw(OfficeBuilding), url: "/shopEntry", ___type: "merchantEntry" },
|
||||
{ title: "领券中心", icon: markRaw(PriceTag), url: "/coupon", ___type: "coupon" },
|
||||
{ title: "秒杀频道", icon: markRaw(Promotion), url: "/seckill", ___type: "seckill" },
|
||||
{ title: "拼团频道", icon: markRaw(Promotion), url: "/group", ___type: "group" },
|
||||
{ title: "签到", icon: markRaw(Sunny), url: "/sign", ___type: "sign" },
|
||||
{ title: "小程序直播", icon: markRaw(VideoCamera), url: "/live", ___type: "live" },
|
||||
{ title: "砍价", icon: markRaw(Share), url: "/kanjia", ___type: "kanjia" },
|
||||
{ title: "积分商城", icon: markRaw(ShoppingBag), url: "/point", ___type: "point" },
|
||||
]);
|
||||
const linkItem = ref({
|
||||
title: "外部链接",
|
||||
title: "自定义链接",
|
||||
icon: markRaw(Link),
|
||||
___type: "link",
|
||||
url: "",
|
||||
@@ -110,13 +121,37 @@ function showLinkItem(item) {
|
||||
);
|
||||
}
|
||||
function handleLink(val, index) {
|
||||
val = { ...val, ___type: "other" };
|
||||
if (val.title === "自定义链接" || val.title === "外部链接") {
|
||||
ElMessageBox.prompt("请输入链接地址(支持站内相对路径如 /cart 或外部网址如 https://...):", "自定义链接", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
inputValue: val.url || "",
|
||||
inputPlaceholder: "例如 /cart 或 https://...",
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: "链接地址不能为空",
|
||||
})
|
||||
.then(({ value }) => {
|
||||
const itemData = {
|
||||
...val,
|
||||
title: "自定义链接",
|
||||
url: value,
|
||||
originalType: "link",
|
||||
___type: "other",
|
||||
};
|
||||
selectedIndex.value = index;
|
||||
emit("selected", [itemData]);
|
||||
})
|
||||
.catch(() => {});
|
||||
return;
|
||||
}
|
||||
const itemData = {
|
||||
...val,
|
||||
originalType: val.___type,
|
||||
___type: "other",
|
||||
};
|
||||
selectedIndex.value = index;
|
||||
emit("selected", [val]);
|
||||
emit("selected", [itemData]);
|
||||
}
|
||||
|
||||
// 无 defineExpose:grep manager/src 全量确认无外部 ref/$refs/$parent 访问本组件成员
|
||||
// (link-dialog.vue 的 $refs["lili-component"][0].type 写入指向首个 tab 的 goods-dialog,与本组件无关)
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "../style.scss";
|
||||
|
||||
@@ -54,7 +54,9 @@
|
||||
<el-dialog
|
||||
v-model="picModalFlag"
|
||||
title="选择图片"
|
||||
width="1200px"
|
||||
width="1160px"
|
||||
top="16px"
|
||||
class="pc-image-picker-dialog"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
@closed="resetPicModal"
|
||||
|
||||
@@ -21,6 +21,16 @@ export const floorDecorationRouter = {
|
||||
component: () => import("@/views/page-decoration/wap/main.vue")
|
||||
};
|
||||
|
||||
// PC端楼层装修编辑器:全屏独立页,不在 Main(左侧菜单)布局内
|
||||
export const pcFloorDecorationRouter = {
|
||||
path: "/floorList/renovation",
|
||||
name: "renovation",
|
||||
meta: {
|
||||
title: "PC楼层装修"
|
||||
},
|
||||
component: () => import("@/views/page-decoration/pc/index.vue")
|
||||
};
|
||||
|
||||
// 作为Main组件的子页面展示但是不在左侧菜单显示的路由写在otherRouter里
|
||||
export const otherRouter = {
|
||||
path: "/",
|
||||
@@ -359,12 +369,7 @@ export const otherRouter = {
|
||||
name: "seckill-goods",
|
||||
component: () => import("@/views/promotions/seckill/seckill-goods.vue")
|
||||
},
|
||||
{
|
||||
path: "/floorList/renovation",
|
||||
title: "编辑模板",
|
||||
name: "renovation",
|
||||
component: () => import("@/views/page-decoration/renovation.vue")
|
||||
},
|
||||
|
||||
{
|
||||
path: "add-sms-sign",
|
||||
title: "短信签名",
|
||||
@@ -549,4 +554,4 @@ export const externalLink = {
|
||||
};
|
||||
|
||||
// 所有上面定义的路由都要写在下面的routers里
|
||||
export const routers = [loginRouter, floorDecorationRouter, otherRouter, page500, page403, externalLink];
|
||||
export const routers = [loginRouter, floorDecorationRouter, pcFloorDecorationRouter, otherRouter, page500, page403, externalLink];
|
||||
|
||||
@@ -271,6 +271,10 @@ export function formatLinkType (item) {
|
||||
break;
|
||||
|
||||
case 'other':
|
||||
if (item.url) {
|
||||
url = item.url;
|
||||
break;
|
||||
}
|
||||
switch (item.title) {
|
||||
case '首页':
|
||||
url = '/';
|
||||
@@ -281,13 +285,29 @@ export function formatLinkType (item) {
|
||||
case '我的订单':
|
||||
url = '/home/MyOrder';
|
||||
break;
|
||||
case '收藏商品':
|
||||
url = '/home/Favorites';
|
||||
break;
|
||||
case '个人中心':
|
||||
url = '/home';
|
||||
break;
|
||||
case '基本信息':
|
||||
url = '/home/Profile';
|
||||
break;
|
||||
case '我的售后':
|
||||
url = '/home/AfterSale';
|
||||
break;
|
||||
case '我的优惠券':
|
||||
url = '/home/Coupons';
|
||||
break;
|
||||
case '收藏商品':
|
||||
url = '/home/Favorites';
|
||||
break;
|
||||
case '消息中心':
|
||||
url = '/home/MsgList';
|
||||
break;
|
||||
case '商家入驻':
|
||||
url = '/shopEntry';
|
||||
break;
|
||||
case '外部链接':
|
||||
case '自定义链接':
|
||||
url = item.url;
|
||||
break;
|
||||
case '秒杀频道':
|
||||
|
||||
@@ -17,10 +17,10 @@
|
||||
style="width: 240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="商品ID" prop="id">
|
||||
<el-form-item label="SPU编码" prop="id">
|
||||
<el-input
|
||||
v-model="searchForm.id"
|
||||
placeholder="请输入商品ID"
|
||||
placeholder="请输入SPU编码"
|
||||
clearable
|
||||
style="width: 240px"
|
||||
/>
|
||||
@@ -126,7 +126,7 @@
|
||||
@selection-change="onSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="100" align="center" />
|
||||
<el-table-column prop="id" label="商品ID" width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="id" label="SPU编码" width="200" show-overflow-tooltip />
|
||||
<el-table-column label="商品图片" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<img alt="商品图片"
|
||||
|
||||
@@ -105,7 +105,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="goods-section-bd">
|
||||
<el-form-item label="商品ID">
|
||||
<el-form-item label="SPU编码">
|
||||
<span class="detail-value">{{ goods.id || "—" }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="店铺名称">
|
||||
|
||||
@@ -17,10 +17,10 @@
|
||||
style="width: 240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="商品ID" prop="goodsId">
|
||||
<el-form-item label="SPU编码" prop="goodsId">
|
||||
<el-input
|
||||
v-model="searchForm.goodsId"
|
||||
placeholder="请输入商品ID"
|
||||
placeholder="请输入SPU编码"
|
||||
clearable
|
||||
style="width: 240px"
|
||||
/>
|
||||
@@ -78,7 +78,7 @@
|
||||
>
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column prop="id" label="SKU ID" width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="goodsId" label="商品ID" width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="goodsId" label="SPU编码" width="180" show-overflow-tooltip />
|
||||
<el-table-column label="商品信息" min-width="360">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row" class="goods-info">
|
||||
|
||||
@@ -645,7 +645,7 @@ function loadOnlineUsers() {
|
||||
});
|
||||
}
|
||||
function buildShareUrl() {
|
||||
const base = (wapUrl.value || BASE?.WAP_URL || config.website || "").replace(/\/?$/, "/");
|
||||
const base = (wapUrl.value || BASE?.WAP_URL || localStorage.getItem("website") || config.website || "").replace(/\/?$/, "/");
|
||||
return `${base}pages/promotion/live/room?id=${liveId.value}`;
|
||||
}
|
||||
function handleShare() {
|
||||
|
||||
@@ -242,7 +242,7 @@ function handleControl(row) {
|
||||
}
|
||||
function buildH5ShareUrl(liveId) {
|
||||
// 历史遗留:BASE 未定义,wapUrl 为空时会抛 ReferenceError,保持原样
|
||||
const base = (wapUrl.value || BASE?.WAP_URL || config.website || "").replace(/\/?$/, "/");
|
||||
const base = (wapUrl.value || BASE?.WAP_URL || localStorage.getItem("website") || config.website || "").replace(/\/?$/, "/");
|
||||
return `${base}pages/promotion/live/room?id=${liveId}`;
|
||||
}
|
||||
function handleShare(row) {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<div class="foot">
|
||||
<el-row justify="space-around" class="help">
|
||||
<a class="item" :href="config.website" target="_blank">帮助</a>
|
||||
<a class="item" :href="config.website" target="_blank">隐私</a>
|
||||
<a class="item" :href="config.website" target="_blank">条款</a>
|
||||
<a class="item" :href="website" target="_blank">帮助</a>
|
||||
<a class="item" :href="website" target="_blank">隐私</a>
|
||||
<a class="item" :href="website" target="_blank">条款</a>
|
||||
</el-row>
|
||||
<el-row justify="center" class="copyright">
|
||||
Copyright © {{ year }} - Present
|
||||
<a :href="config.website" class="href" target="_blank" style="margin: 0 5px">{{ config.title }}</a>
|
||||
<a :href="website" class="href" target="_blank" style="margin: 0 5px">{{ config.title }}</a>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
@@ -17,6 +17,8 @@ import { ref } from "vue";
|
||||
import config from "@/config/index";
|
||||
|
||||
const year = ref(new Date().getFullYear());
|
||||
// 官网地址优先取接口下发缓存,兜底用本地配置
|
||||
const website = ref(localStorage.getItem("website") || config.website);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -26,7 +26,7 @@ function applyFavicon(href) {
|
||||
function getSite() {
|
||||
getBaseSite().then((res) => {
|
||||
if (res.success && res.result && res.result.settingValue) {
|
||||
const { domainLogo: logo, domainIcon, siteName } = JSON.parse(
|
||||
const { domainLogo: logo, domainIcon, siteName, website } = JSON.parse(
|
||||
res.result.settingValue
|
||||
);
|
||||
domainLogo.value = logo || "";
|
||||
@@ -35,6 +35,7 @@ function getSite() {
|
||||
localStorage.setItem("icon", logo || "");
|
||||
localStorage.setItem("domainIcon", domainIcon || "");
|
||||
localStorage.setItem("title", siteName || "");
|
||||
localStorage.setItem("website", website || "");
|
||||
applyFavicon(domainIcon || logo);
|
||||
if (siteName) {
|
||||
window.document.title = siteName + " - 运营后台";
|
||||
|
||||
@@ -114,7 +114,7 @@
|
||||
<a class="link-text" @click="linkTo(row.goodsId, row.skuId)">{{ row.goodsName }}</a>
|
||||
</div>
|
||||
<div style="color: #999; font-size: 12px; margin-top: 5px">
|
||||
商品ID: {{ row.goodsId }}
|
||||
SPU编码: {{ row.goodsId }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
/>
|
||||
</el-tabs>
|
||||
<div class="operation mb_10">
|
||||
<el-button type="primary" @click="createTemp">添加页面</el-button>
|
||||
<el-button type="primary" tag="a" :href="addHref" target="_blank" class="add-page-btn">添加页面</el-button>
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="list" style="width: 100%">
|
||||
<el-table-column prop="name" label="页面名称" min-width="200" show-overflow-tooltip>
|
||||
@@ -18,6 +18,16 @@
|
||||
<span v-if="row">{{ row.name || "暂无模板昵称" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatTime(row?.createTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="updateTime" label="修改时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatTime(row?.updateTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="searchForm.pageType !== 'SPECIAL'"
|
||||
label="状态"
|
||||
@@ -35,12 +45,10 @@
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" align="center" fixed="right">
|
||||
<el-table-column label="操作" width="160" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row">
|
||||
<a class="link-text" @click="Template(row)">编辑</a>
|
||||
<span class="op-split">|</span>
|
||||
<a class="link-text" @click="decorate(row)">装修</a>
|
||||
<a class="link-text" :href="decorateHref(row)" target="_blank">装修</a>
|
||||
<span class="op-split">|</span>
|
||||
<a class="link-text" @click="confirmDel(row.id)">删除</a>
|
||||
</template>
|
||||
@@ -61,43 +69,21 @@
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
<el-dialog
|
||||
v-model="showModal"
|
||||
title="模板设置"
|
||||
width="600px"
|
||||
:close-on-click-modal="false"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
>
|
||||
<el-form ref="form" :model="formData" label-width="80px">
|
||||
<el-form-item label="模板名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入模板名称" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showModal = false">取消</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="newTemplate">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from "vue";
|
||||
import { ref, reactive, computed, onMounted, onBeforeUnmount } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import * as API_floor from "@/api/other.js";
|
||||
import { Message, Modal } from "@/utils/message";
|
||||
import { unixToDate } from "@/utils/filters";
|
||||
|
||||
defineOptions({ name: "floorList" });
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const showModal = ref(false);
|
||||
const total = ref(0);
|
||||
const formData = ref({
|
||||
status: false,
|
||||
name: "",
|
||||
});
|
||||
const searchForm = reactive({
|
||||
pageNumber: 1,
|
||||
pageSize: 20,
|
||||
@@ -113,37 +99,48 @@ const pageTypes = ref([
|
||||
]);
|
||||
const list = ref([]);
|
||||
|
||||
const form = ref(null);
|
||||
const addHref = computed(() => {
|
||||
return router.resolve({
|
||||
path: "/floorList/renovation",
|
||||
query: {
|
||||
pageType: searchForm.pageType,
|
||||
mode: "add",
|
||||
},
|
||||
}).href;
|
||||
});
|
||||
|
||||
function decorateHref(row) {
|
||||
return router.resolve({
|
||||
path: "/floorList/renovation",
|
||||
query: {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
pageType: searchForm.pageType,
|
||||
},
|
||||
}).href;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getTemplateList();
|
||||
window.addEventListener("focus", onWindowFocus);
|
||||
});
|
||||
|
||||
function newTemplate() {
|
||||
form.value.validate((valid) => {
|
||||
if (valid) {
|
||||
const data = formData.value;
|
||||
data.status ? (data.pageShow = "OPEN") : (data.pageShow = "CLOSE");
|
||||
delete data.status;
|
||||
(data.pageType = searchForm.pageType),
|
||||
(data.pageClientType = "PC");
|
||||
if (data.id) {
|
||||
API_floor.updateHome(data.id, data).then((res) => {
|
||||
Message.success("编辑模板成功");
|
||||
showModal.value = false;
|
||||
getTemplateList();
|
||||
});
|
||||
} else {
|
||||
API_floor.setHomeSetup(data).then((res) => {
|
||||
Message.success("新建模板成功");
|
||||
showModal.value = false;
|
||||
getTemplateList();
|
||||
});
|
||||
}
|
||||
} else {
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("focus", onWindowFocus);
|
||||
});
|
||||
|
||||
function onWindowFocus() {
|
||||
getTemplateList();
|
||||
}
|
||||
|
||||
function formatTime(time) {
|
||||
if (!time) return "—";
|
||||
if (typeof time === "string" && time.includes("-")) {
|
||||
return time;
|
||||
}
|
||||
const ts = new Date(time).getTime();
|
||||
if (Number.isNaN(ts)) return time;
|
||||
return unixToDate(ts / 1000);
|
||||
}
|
||||
|
||||
function handleTabChange(type) {
|
||||
@@ -152,27 +149,6 @@ function handleTabChange(type) {
|
||||
getTemplateList();
|
||||
}
|
||||
|
||||
function createTemp() {
|
||||
form.value?.resetFields();
|
||||
formData.value = { status: false, name: "" };
|
||||
showModal.value = true;
|
||||
}
|
||||
|
||||
function Template(item) {
|
||||
item.status = item.pageShow;
|
||||
formData.value = { ...item };
|
||||
showModal.value = true;
|
||||
}
|
||||
|
||||
function decorate(val) {
|
||||
const data = { id: val.id, pageShow: val.pageShow, pageType: searchForm.pageType };
|
||||
|
||||
router.push({
|
||||
name: "renovation",
|
||||
query: data,
|
||||
});
|
||||
}
|
||||
|
||||
function changePageNum(val) {
|
||||
searchForm.pageNumber = val;
|
||||
getTemplateList();
|
||||
@@ -186,9 +162,9 @@ function getTemplateList() {
|
||||
loading.value = true;
|
||||
API_floor.getHomeList(searchForm).then((res) => {
|
||||
loading.value = false;
|
||||
if (res.success) {
|
||||
total.value = res.result.total;
|
||||
list.value = res.result.records;
|
||||
if (res.success && res.result) {
|
||||
total.value = res.result.total || 0;
|
||||
list.value = res.result.records || [];
|
||||
list.value.forEach((e) => {
|
||||
if (e.pageShow === "OPEN") {
|
||||
e.pageShow = true;
|
||||
@@ -197,6 +173,8 @@ function getTemplateList() {
|
||||
}
|
||||
});
|
||||
}
|
||||
}).catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -223,6 +201,7 @@ function beforeRelease(row) {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function confirmDel(id) {
|
||||
Modal.confirm({
|
||||
title: "提示",
|
||||
@@ -230,6 +209,7 @@ function confirmDel(id) {
|
||||
onOk: () => delTemplate(id),
|
||||
});
|
||||
}
|
||||
|
||||
function delTemplate(id) {
|
||||
API_floor.removePageHome(id).then((res) => {
|
||||
if (res.success) {
|
||||
@@ -244,4 +224,8 @@ function delTemplate(id) {
|
||||
.page-type-tabs {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.add-page-btn {
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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>
|
||||
@@ -0,0 +1,246 @@
|
||||
<template>
|
||||
<div class="panel-article-content">
|
||||
<!-- 1. 文章标题与元信息 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">文章标题与元信息</div>
|
||||
<div class="form-item">
|
||||
<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="如:品牌故事 · 匠心传承" />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">副标题</span>
|
||||
<el-input v-model="options.subtitle" size="small" placeholder="如:关于我们的品牌理念与品质坚持" />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">专栏标签</span>
|
||||
<el-input v-model="options.tag" size="small" placeholder="如:品牌专栏" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="form-item">
|
||||
<span class="item-label">展示作者与时间</span>
|
||||
<el-switch v-model="options.showMeta" size="small" />
|
||||
</div>
|
||||
<template v-if="options.showMeta !== false">
|
||||
<div class="form-item">
|
||||
<span class="item-label">作者/部门</span>
|
||||
<el-input v-model="options.author" size="small" placeholder="如:官方商城运营部" />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">发布日期</span>
|
||||
<el-input v-model="options.date" size="small" placeholder="如:2026-10-28" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 2. 文章富文本内容 (与万米一致) -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title flex-between">
|
||||
<span>文章内容</span>
|
||||
<el-button link type="primary" size="small" @click="editMode = (editMode === 'rich' ? 'raw' : 'rich')">
|
||||
{{ editMode === 'rich' ? '切换为HTML代码' : '切换为预览' }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 纯文本摘要预览 -->
|
||||
<div class="article-summary-box" v-if="editMode === 'rich'">
|
||||
<div class="summary-text">{{ cleanTextSummary || '暂无文章内容,请点击下方按钮编辑' }}</div>
|
||||
</div>
|
||||
|
||||
<div class="edit-btn-wrap" v-if="editMode === 'rich'">
|
||||
<el-button type="primary" size="small" style="width: 100%" @click="openEditorDialog = true">
|
||||
编辑文章富文本内容
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 源码编辑模式 -->
|
||||
<div class="raw-html-editor" v-if="editMode === 'raw'">
|
||||
<el-input
|
||||
v-model="options.content"
|
||||
type="textarea"
|
||||
:rows="10"
|
||||
placeholder="请输入或粘贴文章 HTML 内容..."
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3. 文章排版样式 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">文章阅读排版</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">正文字号</span>
|
||||
<el-radio-group v-model="options.fontSize" size="small">
|
||||
<el-radio-button :label="14">14px</el-radio-button>
|
||||
<el-radio-button :label="15">15px</el-radio-button>
|
||||
<el-radio-button :label="16">16px</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">正文对齐</span>
|
||||
<el-radio-group v-model="options.textAlign" size="small">
|
||||
<el-radio-button label="left">靠左</el-radio-button>
|
||||
<el-radio-button label="center">居中</el-radio-button>
|
||||
<el-radio-button label="justify">两端对齐</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 富文本编辑器弹窗 -->
|
||||
<el-dialog
|
||||
v-model="openEditorDialog"
|
||||
title="编辑文章富文本内容"
|
||||
width="900px"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
>
|
||||
<div class="dialog-editor-container">
|
||||
<tinymce-editor
|
||||
v-model="tempContent"
|
||||
height="450px"
|
||||
/>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="openEditorDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSaveEditor">确认保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import TinymceEditor from '@/components/editor/index.vue';
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const defaultArticleHtml = `
|
||||
<h3>关于我们 · 甄选美好生活</h3>
|
||||
<p>我们始终坚信,一件好的产品不仅能改善日常生活的品质,更传递着对美好生活的执着追求。自创立以来,我们严选全球顶尖供应链,深耕每一道生产工艺与质检环节,力求将兼具美学价值与极致实用性的商品送达每一位用户手中。</p>
|
||||
<blockquote>品质是我们的信仰,体验是我们的底线。从选材到交付,每一步都经得起时光检验。</blockquote>
|
||||
<h4>四大核心服务保障</h4>
|
||||
<ul>
|
||||
<li><strong>100% 官方正品</strong>:源头品牌直采,全链路防伪溯源,假一赔十保障。</li>
|
||||
<li><strong>极速顺丰冷链</strong>:全国多仓联动发货,核心城市支持当日达/次日达。</li>
|
||||
<li><strong>7天无理由退换</strong>:专业客服团队 7×24 小时贴心守护,售后无忧。</li>
|
||||
<li><strong>会员专享权益</strong>:尊享积分抵现、生日特权礼及专属品鉴沙龙。</li>
|
||||
</ul>
|
||||
<p>感谢一路相伴的每一位朋友,未来我们将持续探索更多优质生活场景,与您共同见证品质生活的美好绽放。</p>
|
||||
`.trim();
|
||||
|
||||
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.showMeta === undefined) props.options.showMeta = true;
|
||||
if (!props.options.author) props.options.author = '官方商城运营部';
|
||||
if (!props.options.date) props.options.date = '2026-10-28';
|
||||
if (!props.options.tag) props.options.tag = '品牌专栏';
|
||||
if (!props.options.fontSize) props.options.fontSize = 15;
|
||||
if (!props.options.textAlign) props.options.textAlign = 'left';
|
||||
if (!props.options.content) props.options.content = defaultArticleHtml;
|
||||
|
||||
const editMode = ref('rich');
|
||||
const openEditorDialog = ref(false);
|
||||
const tempContent = ref(props.options.content || defaultArticleHtml);
|
||||
|
||||
watch(openEditorDialog, (val) => {
|
||||
if (val) {
|
||||
tempContent.value = props.options.content || defaultArticleHtml;
|
||||
}
|
||||
});
|
||||
|
||||
const cleanTextSummary = computed(() => {
|
||||
const c = props.options?.content || '';
|
||||
const text = c.replace(/<[^>]*>/g, ' ').replace(/ /g, ' ').replace(/\s+/g, ' ').trim();
|
||||
return text.slice(0, 120) + (text.length > 120 ? '...' : '');
|
||||
});
|
||||
|
||||
function handleSaveEditor() {
|
||||
props.options.content = tempContent.value;
|
||||
openEditorDialog.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.panel-article-content {
|
||||
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;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.item-label {
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
margin-right: 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.article-summary-box {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
padding: 10px 12px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.summary-text {
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
color: #475569;
|
||||
max-height: 72px;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-btn-wrap {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.raw-html-editor {
|
||||
margin-top: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-editor-container {
|
||||
padding: 10px 0;
|
||||
}
|
||||
</style>
|
||||
@@ -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: '/static/brand-wall/brand_1.png', url: '/goodsList?brand=1' },
|
||||
{ title: '双层收纳 29.9', img: '/static/brand-wall/brand_2.png', url: '/goodsList?brand=2' },
|
||||
{ title: '双层收纳 29.9', img: '/static/brand-wall/brand_3.png', url: '/goodsList?brand=3' },
|
||||
{ title: '双层收纳 29.9', img: '/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: `/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>
|
||||
@@ -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 = 380;
|
||||
if (!Array.isArray(props.options.list)) {
|
||||
props.options.list = [
|
||||
{ title: '出游囤好货', img: '/static/carousel/banner_1.png', url: '/goodsList?seckill=1' },
|
||||
{ title: '科技新品', img: '/static/carousel/banner_2.png', url: '/goodsList?category=1' },
|
||||
{ title: '原产地直采', img: '/static/carousel/banner_3.png', url: '/goodsList?category=2' },
|
||||
{ title: '大牌美妆', img: '/static/carousel/banner_4.png', url: '/goodsList?category=3' }
|
||||
];
|
||||
}
|
||||
|
||||
// 注入全局选择器
|
||||
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: `/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>
|
||||
@@ -0,0 +1,381 @@
|
||||
<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="#ea580c"
|
||||
class="color-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分类项列表 -->
|
||||
<div class="categories-sub-section mt-20">
|
||||
<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.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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,368 @@
|
||||
<template>
|
||||
<div class="panel-channel-floor">
|
||||
<!-- 1. 标题区设置 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">标题区设置</div>
|
||||
<div class="form-item">
|
||||
<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="如:场景频道" />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">副标题</span>
|
||||
<el-input v-model="options.subtitle" size="small" placeholder="如:主视觉 + 商品组合楼层" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 2. 主视觉设置 (Hero Section) -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">场景主视觉</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">主视觉图片</span>
|
||||
<el-input v-model="options.heroImage" size="small" placeholder="建议尺寸: 520x560px" clearable />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">主视觉大标题</span>
|
||||
<el-input v-model="options.heroTitle" size="small" placeholder="如:品质生活馆" />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">主视觉副标题</span>
|
||||
<el-input v-model="options.heroSubtitle" size="small" placeholder="如:精选场景好物,一站购齐" />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">行动按钮文案</span>
|
||||
<el-input v-model="options.heroButtonText" size="small" placeholder="如:进入频道" />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">按钮跳转链接</span>
|
||||
<el-input v-model="options.heroHref" size="small" placeholder="如:/goods" />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<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">
|
||||
<div v-for="(p, pIdx) in options.promos" :key="pIdx" class="promo-editor-card">
|
||||
<div class="promo-row-1">
|
||||
<el-input v-model="p.tag" size="small" placeholder="标签(限时/新品)" style="width: 100px" />
|
||||
<el-input v-model="p.title" size="small" placeholder="活动主标题" style="flex: 1" />
|
||||
<el-button link type="danger" size="small" @click="options.promos.splice(pIdx, 1)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="promo-row-2">
|
||||
<el-input v-model="p.desc" size="small" placeholder="活动描述(如: 领券最高减100)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 4. 场景商品选择 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title flex-between">
|
||||
<span>场景商品位 ({{ options.goods?.length || 0 }} 件)</span>
|
||||
<el-button type="primary" size="small" @click="showGoodsModal = true">
|
||||
选择真实商品
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="form-item">
|
||||
<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-model="showGoodsModal" title="选择场景商品" width="600px" append-to-body>
|
||||
<div class="modal-goods-pool">
|
||||
<div
|
||||
v-for="item in mockPool"
|
||||
:key="item.id"
|
||||
class="pool-item"
|
||||
:class="{ selected: isSelected(item.id) }"
|
||||
@click="toggleSelect(item)"
|
||||
>
|
||||
<img :src="item.thumbnail" class="pool-thumb" alt="" />
|
||||
<div class="pool-meta">
|
||||
<div class="pool-name">{{ item.goodsName }}</div>
|
||||
<div class="pool-price">¥{{ item.price }}</div>
|
||||
</div>
|
||||
<el-icon v-if="isSelected(item.id)" class="check-icon"><Check /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="showGoodsModal = false">完成</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { Plus, Delete, Check } from '@element-plus/icons-vue';
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const defaultThumb = '/static/goods/goods_1.png';
|
||||
const showGoodsModal = ref(false);
|
||||
|
||||
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 = '/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: '/static/goods/goods_1.png' },
|
||||
{ id: '2', goodsName: '无线头戴式深度主动降噪蓝牙耳机 空间音频Hi-Res', price: '1099.00', originalPrice: '1499.00', thumbnail: '/static/goods/goods_2.png' },
|
||||
{ id: '3', goodsName: '14英寸 全面屏轻薄商务笔记本电脑 酷睿i7 高刷防眩光', price: '5699.00', originalPrice: '6299.00', thumbnail: '/static/goods/goods_3.png' },
|
||||
{ id: '4', goodsName: '运动健康专业监测智能手表 心率血氧长续航50米防水', price: '899.00', originalPrice: '1199.00', thumbnail: '/static/goods/goods_4.png' },
|
||||
{ id: '5', goodsName: '意式半自动家用复古咖啡机 20Bar高压恒温萃取蒸汽奶泡', price: '899.00', originalPrice: '1280.00', thumbnail: '/static/goods/goods_6.png' },
|
||||
{ id: '6', goodsName: '奢宠酵母焕活修护精华露 50ml 紧致淡纹抗皱滋养', price: '680.00', originalPrice: '890.00', thumbnail: '/static/goods/goods_5.png' }
|
||||
];
|
||||
}
|
||||
|
||||
const mockPool = [
|
||||
{ id: '1', goodsName: '旗舰 5G 智能手机 12GB+256GB 超光影影像系统', price: '3999.00', thumbnail: '/static/goods/goods_1.png' },
|
||||
{ id: '2', goodsName: '无线头戴式深度主动降噪蓝牙耳机 空间音频Hi-Res', price: '1099.00', thumbnail: '/static/goods/goods_2.png' },
|
||||
{ id: '3', goodsName: '14英寸 全面屏轻薄商务笔记本电脑 酷睿i7 高刷防眩光', price: '5699.00', thumbnail: '/static/goods/goods_3.png' },
|
||||
{ id: '4', goodsName: '运动健康专业监测智能手表 心率血氧长续航50米防水', price: '899.00', thumbnail: '/static/goods/goods_4.png' },
|
||||
{ id: '5', goodsName: '意式半自动家用复古咖啡机 20Bar高压恒温萃取蒸汽奶泡', price: '899.00', thumbnail: '/static/goods/goods_6.png' },
|
||||
{ id: '6', goodsName: '奢宠酵母焕活修护精华露 50ml 紧致淡纹抗皱滋养', price: '680.00', thumbnail: '/static/goods/goods_5.png' }
|
||||
];
|
||||
|
||||
function handleAddPromo() {
|
||||
if (!Array.isArray(props.options.promos)) props.options.promos = [];
|
||||
props.options.promos.push({
|
||||
title: '热销特惠',
|
||||
desc: '全品类甄选底价',
|
||||
tag: '热卖',
|
||||
url: '/goods'
|
||||
});
|
||||
}
|
||||
|
||||
function isSelected(id) {
|
||||
return props.options.goods?.some(g => String(g.id) === String(id));
|
||||
}
|
||||
|
||||
function toggleSelect(item) {
|
||||
if (!Array.isArray(props.options.goods)) props.options.goods = [];
|
||||
const idx = props.options.goods.findIndex(g => String(g.id) === String(item.id));
|
||||
if (idx > -1) {
|
||||
props.options.goods.splice(idx, 1);
|
||||
} else {
|
||||
props.options.goods.push({ ...item });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.panel-channel-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;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.item-label {
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
margin-right: 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.promos-list-editor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.promo-editor-card {
|
||||
background: #f8fafc;
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e2e8f0;
|
||||
|
||||
.promo-row-1 {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-goods-pool {
|
||||
max-height: 360px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.pool-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #4f46e5;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: #eef2ff;
|
||||
border-color: #4f46e5;
|
||||
}
|
||||
|
||||
.pool-thumb {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.pool-meta {
|
||||
flex: 1;
|
||||
.pool-name {
|
||||
font-size: 13px;
|
||||
color: #1e293b;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.pool-price {
|
||||
font-size: 12px;
|
||||
color: #e11d48;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
color: #4f46e5;
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -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: `/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>
|
||||
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<div class="panel-coupon">
|
||||
<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="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 class="form-item">
|
||||
<span class="item-label">卡券视觉皮肤</span>
|
||||
<el-radio-group v-model="options.skin" size="small">
|
||||
<el-radio-button label="red">经典喜庆红</el-radio-button>
|
||||
<el-radio-button label="dark">黑金尊享风</el-radio-button>
|
||||
<el-radio-button label="gradient">幻彩渐变</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
|
||||
<div class="form-item flex-row">
|
||||
<span class="item-label">展示使用门槛说明</span>
|
||||
<el-switch v-model="options.showLimitText" size="small" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-section">
|
||||
<div class="section-title flex-between">
|
||||
<span>所选优惠券活动</span>
|
||||
<el-button link type="primary" size="small" @click="handleSelectCouponDialog">
|
||||
<el-icon><Plus /></el-icon> 选择优惠券
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="coupon-summary">
|
||||
已选取 {{ options.couponIds?.length || 0 }} 张优惠券活动。买家端将实时查询当前优惠券可用库存并展示领取状态。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Plus } from '@element-plus/icons-vue';
|
||||
import { Message } from '@/utils/message';
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
function handleSelectCouponDialog() {
|
||||
Message.info('已开启优惠券关联。演示环境已预设可用券活动。');
|
||||
if (!Array.isArray(props.options.couponIds)) props.options.couponIds = [];
|
||||
props.options.couponIds = ['coupon_1', 'coupon_2', 'coupon_3', 'coupon_4'];
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.panel-coupon {
|
||||
.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;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.item-label {
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
}
|
||||
}
|
||||
|
||||
.coupon-summary {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
line-height: 1.6;
|
||||
background: #f8fafc;
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,363 @@
|
||||
<template>
|
||||
<div class="panel-footer-nav">
|
||||
<!-- 特色服务保障条 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title flex-between">
|
||||
<span>服务保障条</span>
|
||||
<el-switch v-model="options.showAssurance" size="small" />
|
||||
</div>
|
||||
|
||||
<div class="assurance-list" v-if="options.showAssurance">
|
||||
<div
|
||||
v-for="(item, idx) in options.assurances"
|
||||
:key="idx"
|
||||
class="assurance-item"
|
||||
>
|
||||
<el-input v-model="item.code" size="small" placeholder="字" class="code-input" />
|
||||
<el-input v-model="item.title" size="small" placeholder="标题" class="title-input" />
|
||||
<el-input v-model="item.desc" size="small" placeholder="副标题" class="desc-input" />
|
||||
<el-button
|
||||
size="small"
|
||||
class="action-btn link-btn"
|
||||
:type="item.url ? 'primary' : 'default'"
|
||||
:plain="!item.url"
|
||||
@click="handleSetAssuranceLink(item)"
|
||||
@contextmenu.prevent="handleClearAssuranceLink(item)"
|
||||
:title="item.url ? ('已设链接: ' + item.url + ' (点击修改,右键清除)') : '设置跳转链接'"
|
||||
>
|
||||
<el-icon><Link /></el-icon>
|
||||
</el-button>
|
||||
<el-button link type="danger" size="small" @click="options.assurances.splice(idx, 1)" title="删除">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
<el-button link type="primary" size="small" @click="handleAddAssurance" style="margin-top: 6px">
|
||||
<el-icon><Plus /></el-icon> 添加保障项
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 服务指南与文章分组 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title flex-between">
|
||||
<span>底部帮助与指南列</span>
|
||||
<el-button link type="primary" size="small" @click="handleAddGuideGroup">
|
||||
<el-icon><Plus /></el-icon> 添加分组
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="guide-groups-list">
|
||||
<div
|
||||
v-for="(group, gIdx) in options.guideGroups"
|
||||
:key="gIdx"
|
||||
class="guide-group-card"
|
||||
>
|
||||
<div class="group-header">
|
||||
<el-input v-model="group.title" size="small" placeholder="列标题(如:购物指南)" style="width: 160px" />
|
||||
<el-button link type="danger" size="small" @click="options.guideGroups.splice(gIdx, 1)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
<!-- 链接列表 -->
|
||||
<div class="group-links">
|
||||
<div v-for="(link, lIdx) in group.links" :key="lIdx" class="link-item">
|
||||
<el-input v-model="link.text" size="small" placeholder="名称" class="link-title-input" />
|
||||
<el-button
|
||||
size="small"
|
||||
class="action-btn link-btn"
|
||||
:type="link.url ? 'primary' : 'default'"
|
||||
:plain="!link.url"
|
||||
@click="handleSetGuideLink(link)"
|
||||
@contextmenu.prevent="handleClearGuideLink(link)"
|
||||
:title="link.url ? ('已设链接: ' + link.url + ' (点击修改,右键清除)') : '设置跳转链接'"
|
||||
>
|
||||
<el-icon><Link /></el-icon>
|
||||
</el-button>
|
||||
<el-button link type="danger" size="small" @click="group.links.splice(lIdx, 1)" title="删除">
|
||||
<el-icon><Close /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
<el-button link type="primary" size="small" @click="group.links.push({ text: '新链接', url: '/article' })">
|
||||
+ 添加子链接
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 版权与备案 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">版权与 ICP 备案</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">ICP 备案号</span>
|
||||
<div class="input-with-action">
|
||||
<el-input v-model="options.icpText" size="small" placeholder="浙ICP备XXXXXXXX号-1" class="flex-1" />
|
||||
<el-button
|
||||
size="small"
|
||||
class="action-btn link-btn"
|
||||
:type="options.icpLink ? 'primary' : 'default'"
|
||||
:plain="!options.icpLink"
|
||||
@click="handleSetIcpLink"
|
||||
@contextmenu.prevent="options.icpLink = ''"
|
||||
:title="options.icpLink ? ('已设链接: ' + options.icpLink + ' (点击修改,右键清除)') : '设置跳转链接'"
|
||||
>
|
||||
<el-icon><Link /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">版权声明文案</span>
|
||||
<div class="input-with-action">
|
||||
<el-input v-model="options.copyright" size="small" placeholder="Copyright © 2026 Lilishop." class="flex-1" />
|
||||
<el-button
|
||||
size="small"
|
||||
class="action-btn link-btn"
|
||||
:type="options.copyrightLink ? 'primary' : 'default'"
|
||||
:plain="!options.copyrightLink"
|
||||
@click="handleSetCopyrightLink"
|
||||
@contextmenu.prevent="options.copyrightLink = ''"
|
||||
:title="options.copyrightLink ? ('已设链接: ' + options.copyrightLink + ' (点击修改,右键清除)') : '设置跳转链接'"
|
||||
>
|
||||
<el-icon><Link /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject } from 'vue';
|
||||
import { Plus, Delete, Close, Link } 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);
|
||||
|
||||
function handleSetAssuranceLink(item) {
|
||||
if (openLinkPicker) {
|
||||
openLinkPicker(({ url }) => {
|
||||
item.url = url;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入跳转链接路径(留空则清除链接):', `设置 [${item.title || '服务保障'}] 链接`, {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: item.url || '',
|
||||
inputPattern: /^.*$/
|
||||
})
|
||||
.then(({ value }) => {
|
||||
item.url = (value || '').trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
function handleClearAssuranceLink(item) {
|
||||
item.url = '';
|
||||
}
|
||||
|
||||
function handleSetGuideLink(link) {
|
||||
if (openLinkPicker) {
|
||||
openLinkPicker(({ url, title }) => {
|
||||
link.url = url;
|
||||
if (!link.text && title) link.text = title;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入跳转链接路径(留空则清除链接):', `设置 [${link.text || '帮助链接'}] 路径`, {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: link.url || '',
|
||||
inputPattern: /^.*$/
|
||||
})
|
||||
.then(({ value }) => {
|
||||
link.url = (value || '').trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
function handleClearGuideLink(link) {
|
||||
link.url = '';
|
||||
}
|
||||
|
||||
function handleAddAssurance() {
|
||||
if (!Array.isArray(props.options.assurances)) props.options.assurances = [];
|
||||
props.options.assurances.push({ code: '优', title: '品质优选', desc: '放心购物', url: '/article' });
|
||||
}
|
||||
|
||||
function handleAddGuideGroup() {
|
||||
if (!Array.isArray(props.options.guideGroups)) props.options.guideGroups = [];
|
||||
props.options.guideGroups.push({ title: '新服务指南', links: [{ text: '常见问题', url: '/article' }] });
|
||||
}
|
||||
function handleSetIcpLink() {
|
||||
if (openLinkPicker) {
|
||||
openLinkPicker(({ url }) => {
|
||||
props.options.icpLink = url;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入 ICP 备案跳转链接(留空则清除链接):', '设置 ICP 备案链接', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: props.options.icpLink || 'https://beian.miit.gov.cn/',
|
||||
inputPattern: /^.*$/
|
||||
})
|
||||
.then(({ value }) => {
|
||||
props.options.icpLink = (value || '').trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
function handleSetCopyrightLink() {
|
||||
if (openLinkPicker) {
|
||||
openLinkPicker(({ url }) => {
|
||||
props.options.copyrightLink = url;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入版权跳转链接(留空则清除链接):', '设置版权链接', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: props.options.copyrightLink || '',
|
||||
inputPattern: /^.*$/
|
||||
})
|
||||
.then(({ value }) => {
|
||||
props.options.copyrightLink = (value || '').trim();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.panel-footer-nav {
|
||||
.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;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.item-label {
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.input-with-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.flex-1 {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.link-btn {
|
||||
padding: 0 7px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.assurance-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.assurance-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.code-input {
|
||||
width: 38px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.title-input {
|
||||
width: 82px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.desc-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.link-btn {
|
||||
padding: 0 7px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.guide-groups-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
.guide-group-card {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
|
||||
.group-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.group-links {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
|
||||
.link-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.link-title-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.link-btn {
|
||||
padding: 0 7px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -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>
|
||||
333
manager/src/views/page-decoration/pc/config-panel/PanelGoods.vue
Normal file
333
manager/src/views/page-decoration/pc/config-panel/PanelGoods.vue
Normal file
@@ -0,0 +1,333 @@
|
||||
<template>
|
||||
<div class="panel-goods-commerce">
|
||||
<!-- 1. 楼层标题设置 (部分包含标题的商品楼层可用) -->
|
||||
<div class="panel-section" v-if="hasTitleConfig">
|
||||
<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 class="form-item">
|
||||
<span class="item-label">查看更多链接</span>
|
||||
<el-input v-model="options.moreUrl" size="small" placeholder="如:/goods" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. 横幅楼层专属横幅海报 (bannerFloor) -->
|
||||
<div class="panel-section" v-if="component.type === 'bannerFloor'">
|
||||
<div class="section-title">左侧专属大促海报</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">海报图片 URL</span>
|
||||
<el-input v-model="options.bannerImg" size="small" placeholder="海报图地址" clearable />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">海报跳转地址</span>
|
||||
<el-input v-model="options.bannerUrl" size="small" placeholder="跳转路径" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3. 频道楼层专属多 Tab 设置 (channelFloor) -->
|
||||
<div class="panel-section" v-if="component.type === 'channelFloor'">
|
||||
<div class="section-title flex-between">
|
||||
<span>频道 Tabs 专区设置</span>
|
||||
<el-button link type="primary" size="small" @click="handleAddTab">
|
||||
<el-icon><Plus /></el-icon> 添加 Tab
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="tabs-list">
|
||||
<div v-for="(tab, tIdx) in options.tabs" :key="tIdx" class="tab-item-row">
|
||||
<el-input v-model="tab.title" size="small" placeholder="Tab 名称" style="width: 110px" />
|
||||
<el-input v-model="tab.categoryId" size="small" placeholder="绑定的分类 ID" style="flex: 1" />
|
||||
<el-button link type="danger" size="small" @click="options.tabs.splice(tIdx, 1)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 4. 排版与列数设置 -->
|
||||
<div class="panel-section" v-if="options.columns !== undefined">
|
||||
<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="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>
|
||||
|
||||
<!-- 5. 数据源设置 -->
|
||||
<div class="panel-section" v-if="component.type !== 'recommendFeed'">
|
||||
<div class="section-title">商品数据来源</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">数据源模式</span>
|
||||
<el-radio-group v-model="options.dataSource" size="small">
|
||||
<el-radio-button label="manual">手动选品</el-radio-button>
|
||||
<el-radio-button label="category">分类自动拉取</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
|
||||
<div class="form-item" v-if="options.dataSource === 'category'">
|
||||
<span class="item-label">拉取商品数量</span>
|
||||
<el-input-number v-model="options.goodsCount" :min="2" :max="20" size="small" controls-position="right" />
|
||||
</div>
|
||||
|
||||
<!-- 手动选品区 -->
|
||||
<div class="manual-pick-wrap" v-if="options.dataSource === 'manual'">
|
||||
<div class="pick-header">
|
||||
<span class="pick-info">已选商品 ({{ options.goods?.length || 0 }} 件)</span>
|
||||
<el-button type="primary" size="small" @click="showGoodsModal = true">
|
||||
选择商品
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 已选商品列表滚动区 -->
|
||||
<div class="selected-goods-list" v-if="options.goods?.length">
|
||||
<div
|
||||
v-for="(g, gIdx) in options.goods"
|
||||
:key="g.id || gIdx"
|
||||
class="selected-item"
|
||||
>
|
||||
<img :src="g.thumbnail || defaultThumb" alt="" class="item-thumb" />
|
||||
<div class="item-info">
|
||||
<span class="item-name">{{ g.goodsName }}</span>
|
||||
<span class="item-price">¥{{ g.price }}</span>
|
||||
</div>
|
||||
<el-button link type="danger" size="small" @click="removeSelectedGoods(gIdx)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 6. 商品卡片信息显隐开关 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">商品元素显隐控制</div>
|
||||
<div class="switch-grid">
|
||||
<div class="switch-item" v-if="options.showTitle !== undefined">
|
||||
<span>商品标题</span>
|
||||
<el-switch v-model="options.showTitle" size="small" />
|
||||
</div>
|
||||
<div class="switch-item" v-if="options.showPrice !== undefined">
|
||||
<span>销售价格</span>
|
||||
<el-switch v-model="options.showPrice" size="small" />
|
||||
</div>
|
||||
<div class="switch-item" v-if="options.showOriginalPrice !== undefined">
|
||||
<span>原价/划线价</span>
|
||||
<el-switch v-model="options.showOriginalPrice" size="small" />
|
||||
</div>
|
||||
<div class="switch-item" v-if="options.showCartBtn !== undefined">
|
||||
<span>加购按钮</span>
|
||||
<el-switch v-model="options.showCartBtn" size="small" />
|
||||
</div>
|
||||
<div class="switch-item" v-if="options.showTags !== undefined">
|
||||
<span>促销标签</span>
|
||||
<el-switch v-model="options.showTags" size="small" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 商品库选择弹窗 -->
|
||||
<el-dialog
|
||||
v-model="showGoodsModal"
|
||||
title="从商品库选择商品"
|
||||
width="900px"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
>
|
||||
<goods-dialog
|
||||
:selectedWay="options.goods || []"
|
||||
@selected="handleGoodsSelected"
|
||||
/>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { Plus, Delete } from '@element-plus/icons-vue';
|
||||
import GoodsDialog from '@/components/lili-dialog/goods-dialog.vue';
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const defaultThumb = '/static/goods/goods_1.png';
|
||||
const showGoodsModal = ref(false);
|
||||
|
||||
if (props.component?.type === 'goodsList' && (props.options.columns === undefined || props.options.columns === 4)) {
|
||||
props.options.columns = 6;
|
||||
}
|
||||
|
||||
const hasTitleConfig = computed(() => {
|
||||
return ['gridFloor', 'bannerFloor', 'goodsBar', 'channelFloor', 'recommendFeed'].includes(props.component.type);
|
||||
});
|
||||
|
||||
function handleAddTab() {
|
||||
if (!Array.isArray(props.options.tabs)) props.options.tabs = [];
|
||||
props.options.tabs.push({ title: '新频道专区', categoryId: '', goods: [] });
|
||||
}
|
||||
|
||||
function handleGoodsSelected(selectedGoods) {
|
||||
if (Array.isArray(selectedGoods)) {
|
||||
props.options.goods = selectedGoods.map(g => ({
|
||||
id: g.id,
|
||||
goodsName: g.goodsName,
|
||||
price: g.price,
|
||||
originalPrice: g.originalPrice,
|
||||
thumbnail: g.thumbnail
|
||||
}));
|
||||
}
|
||||
showGoodsModal.value = false;
|
||||
}
|
||||
|
||||
function removeSelectedGoods(index) {
|
||||
props.options.goods.splice(index, 1);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.panel-goods-commerce {
|
||||
.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;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.item-label {
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
}
|
||||
}
|
||||
|
||||
.tabs-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.tab-item-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.manual-pick-wrap {
|
||||
background: #f8fafc;
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e2e8f0;
|
||||
|
||||
.pick-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.pick-info {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #334155;
|
||||
}
|
||||
}
|
||||
|
||||
.selected-goods-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
|
||||
.selected-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 8px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 4px;
|
||||
|
||||
.item-thumb {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
object-fit: cover;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.item-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.item-name {
|
||||
font-size: 12px;
|
||||
color: #1e293b;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.item-price {
|
||||
font-size: 12px;
|
||||
color: #e11d48;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.switch-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
|
||||
.switch-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 10px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
color: #334155;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,301 @@
|
||||
<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="如:爆款热卖榜" />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">副标题</span>
|
||||
<el-input v-model="options.subtitle" size="small" placeholder="如:近30天爆卖好物精选" />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">展示查看更多</span>
|
||||
<el-switch v-model="options.showMoreLink" size="small" />
|
||||
</div>
|
||||
<div class="form-item" v-if="options.showMoreLink !== false">
|
||||
<span class="item-label">更多文案</span>
|
||||
<el-input v-model="options.moreText" size="small" placeholder="如:查看全部" />
|
||||
</div>
|
||||
</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.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">
|
||||
<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>
|
||||
<el-button type="primary" size="small" @click="showModal = true">
|
||||
选择真实商品
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="goods-grid-preview" v-if="options.goods?.length">
|
||||
<div v-for="(g, idx) in options.goods" :key="g.id || idx" class="grid-item-row">
|
||||
<span class="idx-badge">{{ idx + 1 }}</span>
|
||||
<img :src="g.thumbnail || defaultThumb" class="g-thumb" alt="" />
|
||||
<div class="g-info">
|
||||
<span class="g-name">{{ g.goodsName }}</span>
|
||||
<span class="g-price">¥{{ g.price }}</span>
|
||||
</div>
|
||||
<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-model="showModal" title="选择宫格商品" width="600px" append-to-body>
|
||||
<div class="modal-goods-pool">
|
||||
<div
|
||||
v-for="item in mockPool"
|
||||
:key="item.id"
|
||||
class="pool-item"
|
||||
:class="{ selected: isSelected(item.id) }"
|
||||
@click="toggleSelect(item)"
|
||||
>
|
||||
<img :src="item.thumbnail" class="pool-thumb" alt="" />
|
||||
<div class="pool-meta">
|
||||
<div class="pool-name">{{ item.goodsName }}</div>
|
||||
<div class="pool-price">¥{{ item.price }}</div>
|
||||
</div>
|
||||
<el-icon v-if="isSelected(item.id)" class="check-icon"><Check /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="showModal = false">完成</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { Delete, Check } from '@element-plus/icons-vue';
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const defaultThumb = '/static/goods/goods_1.png';
|
||||
const showModal = ref(false);
|
||||
|
||||
if (!props.options.title) props.options.title = '爆款热卖榜';
|
||||
if (!props.options.subtitle) props.options.subtitle = '近30天爆卖好物精选';
|
||||
if (props.options.showMoreLink === undefined) props.options.showMoreLink = true;
|
||||
if (!props.options.moreText) props.options.moreText = '查看全部';
|
||||
if (!props.options.columns) props.options.columns = 5;
|
||||
if (props.options.showBorder === undefined) props.options.showBorder = true;
|
||||
if (!props.options.goods) {
|
||||
props.options.goods = [
|
||||
{ id: '1', goodsName: '旗舰 5G 智能手机 12GB+256GB 超光影影像系统', price: '3999.00', originalPrice: '4599.00', thumbnail: '/static/goods/goods_1.png' },
|
||||
{ id: '2', goodsName: '无线头戴式深度主动降噪蓝牙耳机 空间音频Hi-Res', price: '1099.00', originalPrice: '1499.00', thumbnail: '/static/goods/goods_2.png' },
|
||||
{ id: '3', goodsName: '14英寸 全面屏轻薄商务笔记本电脑 酷睿i7 高刷防眩光', price: '5699.00', originalPrice: '6299.00', thumbnail: '/static/goods/goods_3.png' },
|
||||
{ id: '4', goodsName: '运动健康专业监测智能手表 心率血氧长续航50米防水', price: '899.00', originalPrice: '1199.00', thumbnail: '/static/goods/goods_4.png' },
|
||||
{ id: '5', goodsName: '奢宠酵母焕活修护精华露 50ml 紧致淡纹抗皱滋养', price: '680.00', originalPrice: '890.00', thumbnail: '/static/goods/goods_5.png' },
|
||||
{ id: '6', goodsName: '意式半自动家用复古咖啡机 20Bar高压恒温萃取蒸汽奶泡', price: '899.00', originalPrice: '1280.00', thumbnail: '/static/goods/goods_6.png' },
|
||||
{ id: '7', goodsName: '碳板专业竞速轻量缓震跑步鞋 透气防滑男女同款潮鞋', price: '459.00', originalPrice: '699.00', thumbnail: '/static/goods/goods_7.png' },
|
||||
{ id: '8', goodsName: '极简静音无雾负离子香薰加湿器 大容量客厅卧室通用', price: '299.00', originalPrice: '399.00', thumbnail: '/static/goods/goods_8.png' },
|
||||
{ id: '9', goodsName: '智能降噪AI翻译蓝牙墨镜 超轻防紫外线偏光太阳镜', price: '599.00', originalPrice: '799.00', thumbnail: '/static/goods/goods_1.png' },
|
||||
{ id: '10', goodsName: '15.6英寸 4K超高清便携式显示器 Type-C一线直连高色域', price: '1299.00', originalPrice: '1699.00', thumbnail: '/static/goods/goods_3.png' }
|
||||
];
|
||||
}
|
||||
|
||||
const mockPool = [
|
||||
{ id: '1', goodsName: '旗舰 5G 智能手机 12GB+256GB 超光影影像系统', price: '3999.00', thumbnail: '/static/goods/goods_1.png' },
|
||||
{ id: '2', goodsName: '无线头戴式深度主动降噪蓝牙耳机 空间音频Hi-Res', price: '1099.00', thumbnail: '/static/goods/goods_2.png' },
|
||||
{ id: '3', goodsName: '14英寸 全面屏轻薄商务笔记本电脑 酷睿i7 高刷防眩光', price: '5699.00', thumbnail: '/static/goods/goods_3.png' },
|
||||
{ id: '4', goodsName: '运动健康专业监测智能手表 心率血氧长续航50米防水', price: '899.00', thumbnail: '/static/goods/goods_4.png' },
|
||||
{ id: '5', goodsName: '奢宠酵母焕活修护精华露 50ml 紧致淡纹抗皱滋养', price: '680.00', thumbnail: '/static/goods/goods_5.png' },
|
||||
{ id: '6', goodsName: '意式半自动家用复古咖啡机 20Bar高压恒温萃取蒸汽奶泡', price: '899.00', thumbnail: '/static/goods/goods_6.png' },
|
||||
{ id: '7', goodsName: '碳板专业竞速轻量缓震跑步鞋 透气防滑男女同款潮鞋', price: '459.00', thumbnail: '/static/goods/goods_7.png' },
|
||||
{ id: '8', goodsName: '极简静音无雾负离子香薰加湿器 大容量客厅卧室通用', price: '299.00', thumbnail: '/static/goods/goods_8.png' }
|
||||
];
|
||||
|
||||
function isSelected(id) {
|
||||
return props.options.goods?.some(g => String(g.id) === String(id));
|
||||
}
|
||||
|
||||
function toggleSelect(item) {
|
||||
if (!Array.isArray(props.options.goods)) props.options.goods = [];
|
||||
const idx = props.options.goods.findIndex(g => String(g.id) === String(item.id));
|
||||
if (idx > -1) {
|
||||
props.options.goods.splice(idx, 1);
|
||||
} else {
|
||||
props.options.goods.push({ ...item });
|
||||
}
|
||||
}
|
||||
</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;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.item-label {
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
margin-right: 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.goods-grid-preview {
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
|
||||
.grid-item-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: #f8fafc;
|
||||
padding: 6px 8px;
|
||||
border-radius: 6px;
|
||||
|
||||
.idx-badge {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #94a3b8;
|
||||
width: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-goods-pool {
|
||||
max-height: 360px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.pool-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #f97316;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: #fff7ed;
|
||||
border-color: #f97316;
|
||||
}
|
||||
|
||||
.pool-thumb {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.pool-meta {
|
||||
flex: 1;
|
||||
.pool-name {
|
||||
font-size: 13px;
|
||||
color: #1e293b;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.pool-price {
|
||||
font-size: 12px;
|
||||
color: #e11d48;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
color: #f97316;
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,370 @@
|
||||
<template>
|
||||
<div class="panel-grid-floor">
|
||||
<!-- 1. 楼层标题设置 (Floor Header) -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">楼层大标题设置</div>
|
||||
<div class="form-item">
|
||||
<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;" />
|
||||
</div>
|
||||
<div class="form-item flex-1">
|
||||
<span class="item-label">楼层名称</span>
|
||||
<el-input v-model="options.title" size="small" placeholder="如:潮流数码" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">英文/副标题</span>
|
||||
<el-input v-model="options.subtitle" size="small" placeholder="如:DIGITAL & TECH" />
|
||||
</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="分类名称" />
|
||||
<el-button link type="danger" size="small" @click="options.subCategories.splice(tIdx, 1)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3. 左侧专属营销竖版海报 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">左侧专属大促海报</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">展示左侧海报</span>
|
||||
<el-switch v-model="options.showPoster" size="small" />
|
||||
</div>
|
||||
<template v-if="options.showPoster !== false">
|
||||
<div class="form-item">
|
||||
<span class="item-label">海报图片 URL</span>
|
||||
<el-input v-model="options.posterImg" size="small" placeholder="图片链接" clearable />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">海报主标题</span>
|
||||
<el-input v-model="options.posterTitle" size="small" placeholder="如:数码潮电 酷玩季" />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">海报副标题</span>
|
||||
<el-input v-model="options.posterSub" size="small" placeholder="如:领券最高立减 600 元" />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">跳转链接</span>
|
||||
<el-input v-model="options.posterUrl" size="small" placeholder="如:/goods" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 4. 右侧商品列数与排版 -->
|
||||
<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="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>
|
||||
<el-button type="primary" size="small" @click="showGoodsModal = true">
|
||||
选择真实商品
|
||||
</el-button>
|
||||
</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-model="showGoodsModal" title="选择楼层商品" width="600px" append-to-body>
|
||||
<div class="modal-goods-pool">
|
||||
<div
|
||||
v-for="item in mockPool"
|
||||
:key="item.id"
|
||||
class="pool-item"
|
||||
:class="{ selected: isSelected(item.id) }"
|
||||
@click="toggleSelect(item)"
|
||||
>
|
||||
<img :src="item.thumbnail" class="pool-thumb" alt="" />
|
||||
<div class="pool-meta">
|
||||
<div class="pool-name">{{ item.goodsName }}</div>
|
||||
<div class="pool-price">¥{{ item.price }}</div>
|
||||
</div>
|
||||
<el-icon v-if="isSelected(item.id)" class="check-icon"><Check /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="showGoodsModal = false">完成</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { Plus, Delete, Check } from '@element-plus/icons-vue';
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const defaultThumb = '/static/goods/goods_1.png';
|
||||
const showGoodsModal = ref(false);
|
||||
|
||||
// 初始化缺省 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 = ['旗舰手机', '轻薄笔电', '降噪耳机', '智能穿戴', '数码配件'];
|
||||
}
|
||||
if (!props.options.posterImg) {
|
||||
props.options.posterImg = '/static/banner-floor/floor_banner_1.png';
|
||||
}
|
||||
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: '/static/goods/goods_1.png' },
|
||||
{ id: '102', goodsName: '无线头戴式深度主动降噪蓝牙耳机 空间音频Hi-Res', price: '1099.00', originalPrice: '1499.00', thumbnail: '/static/goods/goods_2.png' },
|
||||
{ id: '103', goodsName: '14英寸 全面屏轻薄商务笔记本电脑 酷睿i7 高刷防眩光', price: '5699.00', originalPrice: '6299.00', thumbnail: '/static/goods/goods_3.png' },
|
||||
{ id: '104', goodsName: '运动健康专业监测智能手表 心率血氧长续航50米防水', price: '899.00', originalPrice: '1199.00', thumbnail: '/static/goods/goods_4.png' },
|
||||
{ id: '105', goodsName: '奢宠酵母焕活修护精华露 50ml 紧致淡纹抗皱滋养', price: '680.00', originalPrice: '890.00', thumbnail: '/static/goods/goods_5.png' },
|
||||
{ id: '106', goodsName: '意式半自动家用复古咖啡机 20Bar高压恒温萃取蒸汽奶泡', price: '899.00', originalPrice: '1280.00', thumbnail: '/static/goods/goods_6.png' },
|
||||
{ id: '107', goodsName: '碳板专业竞速轻量缓震跑步鞋 透气防滑男女同款潮鞋', price: '459.00', originalPrice: '699.00', thumbnail: '/static/goods/goods_7.png' },
|
||||
{ id: '108', goodsName: '极简静音无雾负离子香薰加湿器 大容量客厅卧室通用', price: '299.00', originalPrice: '399.00', thumbnail: '/static/goods/goods_8.png' }
|
||||
];
|
||||
}
|
||||
|
||||
const mockPool = [
|
||||
{ id: '101', goodsName: '旗舰 5G 智能手机 12GB+256GB 超光影影像系统', price: '3999.00', thumbnail: '/static/goods/goods_1.png' },
|
||||
{ id: '102', goodsName: '无线头戴式深度主动降噪蓝牙耳机 空间音频Hi-Res', price: '1099.00', thumbnail: '/static/goods/goods_2.png' },
|
||||
{ id: '103', goodsName: '14英寸 全面屏轻薄商务笔记本电脑 酷睿i7 高刷防眩光', price: '5699.00', thumbnail: '/static/goods/goods_3.png' },
|
||||
{ id: '104', goodsName: '运动健康专业监测智能手表 心率血氧长续航50米防水', price: '899.00', thumbnail: '/static/goods/goods_4.png' },
|
||||
{ id: '105', goodsName: '奢宠酵母焕活修护精华露 50ml 紧致淡纹抗皱滋养', price: '680.00', thumbnail: '/static/goods/goods_5.png' },
|
||||
{ id: '106', goodsName: '意式半自动家用复古咖啡机 20Bar高压恒温萃取蒸汽奶泡', price: '899.00', thumbnail: '/static/goods/goods_6.png' },
|
||||
{ id: '107', goodsName: '碳板专业竞速轻量缓震跑步鞋 透气防滑男女同款潮鞋', price: '459.00', thumbnail: '/static/goods/goods_7.png' },
|
||||
{ id: '108', goodsName: '极简静音无雾负离子香薰加湿器 大容量客厅卧室通用', price: '299.00', thumbnail: '/static/goods/goods_8.png' }
|
||||
];
|
||||
|
||||
function handleAddTag() {
|
||||
if (!Array.isArray(props.options.subCategories)) {
|
||||
props.options.subCategories = [];
|
||||
}
|
||||
props.options.subCategories.push('新分类');
|
||||
}
|
||||
|
||||
function isSelected(id) {
|
||||
return props.options.goods?.some(g => String(g.id) === String(id));
|
||||
}
|
||||
|
||||
function toggleSelect(item) {
|
||||
if (!Array.isArray(props.options.goods)) props.options.goods = [];
|
||||
const idx = props.options.goods.findIndex(g => String(g.id) === String(item.id));
|
||||
if (idx > -1) {
|
||||
props.options.goods.splice(idx, 1);
|
||||
} else {
|
||||
props.options.goods.push({ ...item });
|
||||
}
|
||||
}
|
||||
</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;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.item-label {
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
margin-right: 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.form-row-2 {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.flex-1 {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tags-editor-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
|
||||
.tag-edit-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.goods-list-preview {
|
||||
max-height: 240px;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-goods-pool {
|
||||
max-height: 360px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.pool-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: #eff6ff;
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
.pool-thumb {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.pool-meta {
|
||||
flex: 1;
|
||||
.pool-name {
|
||||
font-size: 13px;
|
||||
color: #1e293b;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.pool-price {
|
||||
font-size: 12px;
|
||||
color: #e11d48;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
color: #3b82f6;
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<div class="panel-hot-zone">
|
||||
<div class="panel-section">
|
||||
<div class="section-title">热区底图</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">大图 URL 地址</span>
|
||||
<el-input v-model="options.imageUrl" size="small" placeholder="请输入热区底图 URL" clearable />
|
||||
</div>
|
||||
<div class="hotzone-preview" v-if="options.imageUrl">
|
||||
<img :src="options.imageUrl" alt="hotzone" class="img-preview" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-section">
|
||||
<div class="section-title flex-between">
|
||||
<span>热区列表 ({{ options.zones?.length || 0 }})</span>
|
||||
<el-button link type="primary" size="small" @click="handleAddZone">
|
||||
<el-icon><Plus /></el-icon> 添加热区
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="zones-list">
|
||||
<div
|
||||
v-for="(zone, idx) in options.zones"
|
||||
:key="idx"
|
||||
class="zone-card"
|
||||
>
|
||||
<div class="zone-header">
|
||||
<span>热区 {{ idx + 1 }}</span>
|
||||
<el-button link type="danger" size="small" @click="options.zones.splice(idx, 1)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="zone-inputs">
|
||||
<el-input v-model="zone.title" size="small" placeholder="热区标题" />
|
||||
<el-input v-model="zone.url" size="small" placeholder="跳转路径,如 /goods/1" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Plus, Delete } from '@element-plus/icons-vue';
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
function handleAddZone() {
|
||||
if (!Array.isArray(props.options.zones)) props.options.zones = [];
|
||||
props.options.zones.push({
|
||||
title: `热区 ${props.options.zones.length + 1}`,
|
||||
url: '/',
|
||||
top: 10,
|
||||
left: 10,
|
||||
width: 20,
|
||||
height: 20
|
||||
});
|
||||
}
|
||||
</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;
|
||||
}
|
||||
}
|
||||
|
||||
.hotzone-preview {
|
||||
margin-top: 10px;
|
||||
max-height: 140px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 6px;
|
||||
|
||||
.img-preview {
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.zones-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.zone-card {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
|
||||
.zone-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.zone-inputs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,215 @@
|
||||
<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="如:公告快讯 / 今日热点" />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">展示更多入口</span>
|
||||
<el-switch v-model="options.showMore" size="small" />
|
||||
</div>
|
||||
<template v-if="options.showMore !== false">
|
||||
<div class="form-item">
|
||||
<span class="item-label">更多文案</span>
|
||||
<el-input v-model="options.moreText" size="small" placeholder="如:更多" />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">跳转链接</span>
|
||||
<el-input v-model="options.moreUrl" size="small" placeholder="如:/article" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 2. 快讯条目列表 (匹配万米 Mg 结构化列表) -->
|
||||
<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: 80px">
|
||||
<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">
|
||||
<el-input v-model="item.title" size="small" placeholder="快讯内容标题" />
|
||||
</div>
|
||||
|
||||
<div class="item-link-row">
|
||||
<el-input v-model="item.url" size="small" placeholder="跳转路径(如: /article/101)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { Plus, Delete, Top, Bottom } from '@element-plus/icons-vue';
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
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';
|
||||
|
||||
// 统一 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;
|
||||
});
|
||||
|
||||
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: 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;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.item-label {
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
margin-right: 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notice-items-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.notice-card-item {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
|
||||
.item-top-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.item-actions {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.item-title-row, .item-link-row {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<div class="panel-promo-group">
|
||||
<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>
|
||||
|
||||
<div class="panel-section">
|
||||
<div class="section-title flex-between">
|
||||
<span>活动卡片列表</span>
|
||||
<el-button link type="primary" size="small" @click="handleAddCard">
|
||||
<el-icon><Plus /></el-icon> 添加卡片
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="cards-list">
|
||||
<div
|
||||
v-for="(card, idx) in options.cards"
|
||||
:key="idx"
|
||||
class="promo-card-item"
|
||||
>
|
||||
<div class="card-header">
|
||||
<span>卡片 {{ idx + 1 }}</span>
|
||||
<el-button link type="danger" size="small" @click="options.cards.splice(idx, 1)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="card-inputs">
|
||||
<el-input v-model="card.title" size="small" placeholder="主标题(如:今日秒杀)" />
|
||||
<el-input v-model="card.subtitle" size="small" placeholder="副标题(如:抢神券立减50)" />
|
||||
<el-input v-model="card.bgImg" size="small" placeholder="卡片底图 URL" />
|
||||
<el-input v-model="card.url" size="small" placeholder="活动落地页链接" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Plus, Delete } from '@element-plus/icons-vue';
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
function handleAddCard() {
|
||||
if (!Array.isArray(props.options.cards)) props.options.cards = [];
|
||||
props.options.cards.push({
|
||||
title: '大促专场',
|
||||
subtitle: '限量特惠抢购',
|
||||
bgImg: '',
|
||||
url: '/goods'
|
||||
});
|
||||
}
|
||||
</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: 10px;
|
||||
|
||||
.promo-card-item {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.card-inputs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,302 @@
|
||||
<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>
|
||||
<el-button type="primary" size="small" @click="showModal = true">
|
||||
选择真实商品
|
||||
</el-button>
|
||||
</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">{{ 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-model="showModal" title="选择推荐商品" width="600px" append-to-body>
|
||||
<div class="modal-goods-pool">
|
||||
<div
|
||||
v-for="item in mockPool"
|
||||
:key="item.id"
|
||||
class="pool-item"
|
||||
:class="{ selected: isSelected(item.id) }"
|
||||
@click="toggleSelect(item)"
|
||||
>
|
||||
<img :src="item.thumbnail" class="pool-thumb" alt="" />
|
||||
<div class="pool-meta">
|
||||
<div class="pool-name">{{ item.goodsName }}</div>
|
||||
<div class="pool-price">¥{{ item.price }}</div>
|
||||
</div>
|
||||
<el-icon v-if="isSelected(item.id)" class="check-icon"><Check /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="showModal = false">完成</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { Delete, Check } from '@element-plus/icons-vue';
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const defaultThumb = '/static/goods/goods_1.png';
|
||||
const showModal = ref(false);
|
||||
|
||||
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: '/static/goods/goods_1.png' },
|
||||
{ id: '2', goodsName: '无线头戴式深度主动降噪蓝牙耳机 空间音频Hi-Res', price: '1099.00', originalPrice: '1499.00', thumbnail: '/static/goods/goods_2.png' },
|
||||
{ id: '3', goodsName: '14英寸 全面屏轻薄商务笔记本电脑 酷睿i7 高刷防眩光', price: '5699.00', originalPrice: '6299.00', thumbnail: '/static/goods/goods_3.png' },
|
||||
{ id: '4', goodsName: '运动健康专业监测智能手表 心率血氧长续航50米防水', price: '899.00', originalPrice: '1199.00', thumbnail: '/static/goods/goods_4.png' },
|
||||
{ id: '5', goodsName: '奢宠酵母焕活修护精华露 50ml 紧致淡纹抗皱滋养', price: '680.00', originalPrice: '890.00', thumbnail: '/static/goods/goods_5.png' },
|
||||
{ id: '6', goodsName: '意式半自动家用复古咖啡机 20Bar高压恒温萃取蒸汽奶泡', price: '899.00', originalPrice: '1280.00', thumbnail: '/static/goods/goods_6.png' },
|
||||
{ id: '7', goodsName: '碳板专业竞速轻量缓震跑步鞋 透气防滑男女同款潮鞋', price: '459.00', originalPrice: '699.00', thumbnail: '/static/goods/goods_7.png' },
|
||||
{ id: '8', goodsName: '极简静音无雾负离子香薰加湿器 大容量客厅卧室通用', price: '299.00', originalPrice: '399.00', thumbnail: '/static/goods/goods_8.png' }
|
||||
];
|
||||
}
|
||||
|
||||
const mockPool = [
|
||||
{ id: '1', goodsName: '旗舰 5G 智能手机 12GB+256GB 超光影影像系统', price: '3999.00', thumbnail: '/static/goods/goods_1.png' },
|
||||
{ id: '2', goodsName: '无线头戴式深度主动降噪蓝牙耳机 空间音频Hi-Res', price: '1099.00', thumbnail: '/static/goods/goods_2.png' },
|
||||
{ id: '3', goodsName: '14英寸 全面屏轻薄商务笔记本电脑 酷睿i7 高刷防眩光', price: '5699.00', thumbnail: '/static/goods/goods_3.png' },
|
||||
{ id: '4', goodsName: '运动健康专业监测智能手表 心率血氧长续航50米防水', price: '899.00', thumbnail: '/static/goods/goods_4.png' },
|
||||
{ id: '5', goodsName: '奢宠酵母焕活修护精华露 50ml 紧致淡纹抗皱滋养', price: '680.00', thumbnail: '/static/goods/goods_5.png' },
|
||||
{ id: '6', goodsName: '意式半自动家用复古咖啡机 20Bar高压恒温萃取蒸汽奶泡', price: '899.00', thumbnail: '/static/goods/goods_6.png' }
|
||||
];
|
||||
|
||||
function isSelected(id) {
|
||||
return props.options.goods?.some(g => String(g.id) === String(id));
|
||||
}
|
||||
|
||||
function toggleSelect(item) {
|
||||
if (!Array.isArray(props.options.goods)) props.options.goods = [];
|
||||
const idx = props.options.goods.findIndex(g => String(g.id) === String(item.id));
|
||||
if (idx > -1) {
|
||||
props.options.goods.splice(idx, 1);
|
||||
} else {
|
||||
props.options.goods.push({ ...item });
|
||||
}
|
||||
}
|
||||
</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;
|
||||
}
|
||||
}
|
||||
|
||||
.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: 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;
|
||||
|
||||
.idx-tag {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
background: #fee2e2;
|
||||
color: #dc2626;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-goods-pool {
|
||||
max-height: 360px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.pool-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #ef4444;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: #fef2f2;
|
||||
border-color: #ef4444;
|
||||
}
|
||||
|
||||
.pool-thumb {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.pool-meta {
|
||||
flex: 1;
|
||||
.pool-name {
|
||||
font-size: 13px;
|
||||
color: #1e293b;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.pool-price {
|
||||
font-size: 12px;
|
||||
color: #e11d48;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
color: #ef4444;
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,438 @@
|
||||
<template>
|
||||
<div class="panel-search-header">
|
||||
<!-- 1. Logo 设置 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">Logo</div>
|
||||
|
||||
<!-- 规格建议提示卡片 -->
|
||||
<div class="tip-spec-box">
|
||||
建议尺寸: 220 x 80px, 适配 Logo 可用 440 x 160px; 建议大小: 100KB 内; 建议格式: PNG/WebP
|
||||
</div>
|
||||
|
||||
<!-- 选择与预览区 -->
|
||||
<div class="logo-choose-row">
|
||||
<el-button size="small" class="reselect-btn" @click="handleSelectLogo">
|
||||
{{ options.logoUrl ? '重新选择' : '选择图片' }}
|
||||
</el-button>
|
||||
<div class="logo-thumb-wrapper" v-if="options.logoUrl">
|
||||
<img :src="options.logoUrl" alt="Logo preview" class="logo-preview-thumb" />
|
||||
<el-icon class="thumb-clear-btn" @click.stop="handleClearLogo" title="清除 Logo"><Close /></el-icon>
|
||||
</div>
|
||||
<div class="logo-empty-thumb" v-else>
|
||||
<span>未选择 Logo</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Logo 链接 -->
|
||||
<div class="form-row mt-12">
|
||||
<span class="row-label">Logo链接</span>
|
||||
<div class="link-input-group">
|
||||
<el-input
|
||||
v-model="options.logoLink"
|
||||
size="small"
|
||||
placeholder="/"
|
||||
class="link-input"
|
||||
/>
|
||||
<el-button size="small" class="link-btn" @click="handleSetLogoLink" title="选择跳转链接">
|
||||
<el-icon><Link /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. 搜索框与功能设置 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">搜索设置</div>
|
||||
<div class="form-row">
|
||||
<span class="row-label">搜索占位</span>
|
||||
<el-input v-model="options.placeholder" size="small" placeholder="请输入商品名称、品牌或型号" class="flex-1" />
|
||||
</div>
|
||||
|
||||
<div class="form-row mt-10">
|
||||
<span class="row-label">默认搜索类型</span>
|
||||
<el-select v-model="options.searchType" size="small" class="flex-1">
|
||||
<el-option label="商品搜索" value="goods" />
|
||||
<el-option label="店铺搜索" value="shop" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<div class="form-row mt-10 flex-between">
|
||||
<span class="row-label">重置搜索类型</span>
|
||||
<el-switch v-model="options.resetSearchType" size="small" />
|
||||
</div>
|
||||
|
||||
<div class="form-row mt-10 flex-between">
|
||||
<span class="row-label">显示右侧购物车</span>
|
||||
<el-switch v-model="options.showCart" size="small" />
|
||||
</div>
|
||||
|
||||
<div class="form-row mt-10 flex-between">
|
||||
<span class="row-label">显示热门推荐词</span>
|
||||
<el-switch v-model="options.showHotWords" size="small" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- 4. 购物车样式设置 -->
|
||||
<div class="panel-section" v-if="options.showCart !== false">
|
||||
<div class="section-title">购物车设置</div>
|
||||
<div class="form-row">
|
||||
<span class="row-label">购物车文案</span>
|
||||
<el-input v-model="options.cartText" size="small" placeholder="购物车" class="flex-1" />
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-row mt-10">
|
||||
<span class="row-label">购物车背景色</span>
|
||||
<div class="color-picker-row">
|
||||
<el-color-picker v-model="options.cartBgColor" size="small" />
|
||||
<el-input v-model="options.cartBgColor" size="small" placeholder="#ffffff" class="color-hex-input" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row mt-10">
|
||||
<span class="row-label">购物车边框色</span>
|
||||
<div class="color-picker-row">
|
||||
<el-color-picker v-model="options.cartBorderColor" size="small" />
|
||||
<el-input v-model="options.cartBorderColor" size="small" placeholder="#e2e8f0" class="color-hex-input" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row mt-10">
|
||||
<span class="row-label">购物车文字色</span>
|
||||
<div class="color-picker-row">
|
||||
<el-color-picker v-model="options.cartTextColor" size="small" />
|
||||
<el-input v-model="options.cartTextColor" size="small" placeholder="#111827" class="color-hex-input" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row mt-10">
|
||||
<span class="row-label">购物车数量色</span>
|
||||
<div class="color-picker-row">
|
||||
<el-color-picker v-model="options.cartBadgeColor" size="small" />
|
||||
<el-input v-model="options.cartBadgeColor" size="small" placeholder="#e05644" class="color-hex-input" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, inject } from 'vue';
|
||||
import { Plus, Close, Link, Top, Bottom, Delete } 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.logoLink === undefined) props.options.logoLink = '/';
|
||||
if (props.options.placeholder === undefined) props.options.placeholder = '请输入商品名称、品牌或型号';
|
||||
if (props.options.searchType === undefined) props.options.searchType = 'goods';
|
||||
if (props.options.resetSearchType === undefined) props.options.resetSearchType = false;
|
||||
if (props.options.showCart === undefined) props.options.showCart = true;
|
||||
if (props.options.showHotWords === undefined) props.options.showHotWords = true;
|
||||
if (props.options.cartText === undefined) props.options.cartText = '购物车';
|
||||
if (props.options.cartBgColor === undefined) props.options.cartBgColor = '#ffffff';
|
||||
if (props.options.cartBorderColor === undefined) props.options.cartBorderColor = '#e2e8f0';
|
||||
if (props.options.cartTextColor === undefined) props.options.cartTextColor = '#111827';
|
||||
if (props.options.cartBadgeColor === undefined) props.options.cartBadgeColor = '#e05644';
|
||||
if (!Array.isArray(props.options.hotWords)) {
|
||||
props.options.hotWords = ['工业品', '进口好物', '综合特惠', '企业采购'];
|
||||
}
|
||||
|
||||
const openImagePicker = inject('openImagePicker', null);
|
||||
const openLinkPicker = inject('openLinkPicker', null);
|
||||
|
||||
function handleSelectLogo() {
|
||||
if (openImagePicker) {
|
||||
openImagePicker((url) => {
|
||||
if (url) {
|
||||
props.options.logoUrl = url;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入 Logo 图片 URL 地址:', '选择 Logo 图片', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: props.options.logoUrl || '',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '图片地址不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
props.options.logoUrl = value;
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
function handleClearLogo() {
|
||||
props.options.logoUrl = '';
|
||||
}
|
||||
|
||||
function handleSetLogoLink() {
|
||||
if (openLinkPicker) {
|
||||
openLinkPicker(({ url }) => {
|
||||
props.options.logoLink = url;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入跳转链接路径:', '设置 Logo 链接', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: props.options.logoLink || '/',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '链接路径不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
props.options.logoLink = value;
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
function handleAddHotWord() {
|
||||
props.options.hotWords.push('新品推荐');
|
||||
}
|
||||
|
||||
function handleRemoveHotWord(idx) {
|
||||
props.options.hotWords.splice(idx, 1);
|
||||
}
|
||||
|
||||
function moveHotWord(idx, offset) {
|
||||
const target = idx + offset;
|
||||
if (target < 0 || target >= props.options.hotWords.length) return;
|
||||
const item = props.options.hotWords.splice(idx, 1)[0];
|
||||
props.options.hotWords.splice(target, 0, item);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.panel-search-header {
|
||||
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: 10px;
|
||||
|
||||
&.flex-between {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
|
||||
&.flex-between {
|
||||
justify-content: space-between;
|
||||
|
||||
.row-label {
|
||||
width: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.row-label {
|
||||
width: 90px;
|
||||
flex-shrink: 0;
|
||||
color: #475569;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.flex-1 {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.mt-10 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.mt-12 {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
/* 建议规格暗色卡片 */
|
||||
.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;
|
||||
}
|
||||
|
||||
/* Logo 选择行 */
|
||||
.logo-choose-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
.reselect-btn {
|
||||
width: 88px;
|
||||
flex-shrink: 0;
|
||||
background: #f1f5f9;
|
||||
color: #334155;
|
||||
border: 1px solid #cbd5e1;
|
||||
|
||||
&:hover {
|
||||
background: #e2e8f0;
|
||||
color: #0f172a;
|
||||
}
|
||||
}
|
||||
|
||||
.logo-thumb-wrapper {
|
||||
flex: 1;
|
||||
height: 48px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
padding: 4px 8px;
|
||||
box-sizing: border-box;
|
||||
|
||||
.logo-preview-thumb {
|
||||
max-height: 40px;
|
||||
max-width: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.thumb-clear-btn {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
font-size: 14px;
|
||||
color: #94a3b8;
|
||||
cursor: pointer;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border-radius: 50%;
|
||||
|
||||
&:hover {
|
||||
color: #ef4444;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logo-empty-thumb {
|
||||
flex: 1;
|
||||
height: 48px;
|
||||
background: #f8fafc;
|
||||
border: 1px dashed #cbd5e1;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Logo 链接输入区 */
|
||||
.link-input-group {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background: #ffffff;
|
||||
|
||||
.link-input {
|
||||
flex: 1;
|
||||
:deep(.el-input__wrapper) {
|
||||
box-shadow: none !important;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.link-btn {
|
||||
border: none;
|
||||
border-left: 1px solid #e2e8f0;
|
||||
border-radius: 0;
|
||||
background: #f8fafc;
|
||||
color: #64748b;
|
||||
padding: 0 10px;
|
||||
|
||||
&:hover {
|
||||
background: #f1f5f9;
|
||||
color: #ff6a00;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 搜索轮播列表 */
|
||||
.hot-words-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
|
||||
.hot-word-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: #f8fafc;
|
||||
padding: 4px 6px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e2e8f0;
|
||||
|
||||
.word-edit-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.word-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-tip-text {
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
padding: 8px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 颜色选择器行 */
|
||||
.color-picker-row {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.color-hex-input {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,87 @@
|
||||
<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="如:限时特卖" />
|
||||
</div>
|
||||
|
||||
<div class="form-item">
|
||||
<span class="item-label">副标题文字</span>
|
||||
<el-input v-model="options.subtitle" size="small" placeholder="如:精选爆款 每日早十点上新" />
|
||||
</div>
|
||||
|
||||
<div class="form-item flex-row">
|
||||
<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">
|
||||
<span class="item-label">显示右侧“查看更多”</span>
|
||||
<el-switch v-model="options.showMore" size="small" />
|
||||
</div>
|
||||
|
||||
<div class="form-item" v-if="options.showMore">
|
||||
<span class="item-label">更多链接文案</span>
|
||||
<el-input v-model="options.moreText" size="small" placeholder="查看更多 >" />
|
||||
</div>
|
||||
|
||||
<div class="form-item" v-if="options.showMore">
|
||||
<span class="item-label">更多跳转地址</span>
|
||||
<el-input v-model="options.moreUrl" size="small" placeholder="如 /goods" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.panel-title-bar {
|
||||
.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;
|
||||
margin-bottom: 12px;
|
||||
|
||||
&.flex-row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.item-label {
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,514 @@
|
||||
<template>
|
||||
<div class="panel-top-bar">
|
||||
<!-- 1. 顶部栏内容卡片 -->
|
||||
<div class="setting-collapse-card">
|
||||
<div class="collapse-header" @click="expandContent = !expandContent">
|
||||
<div class="header-left">
|
||||
<el-icon class="arrow-icon" :class="{ 'is-expanded': expandContent }"><ArrowRight /></el-icon>
|
||||
<span class="header-title">顶部栏内容</span>
|
||||
<el-tooltip content="设置顶部栏快捷入口和右侧快捷菜单" placement="top">
|
||||
<el-icon class="help-icon"><QuestionFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
<span class="header-subtitle">顶部栏快捷入口和右侧快捷菜单</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="collapse-body" v-show="expandContent">
|
||||
<!-- 左侧登录区 -->
|
||||
<div class="section-block">
|
||||
<div class="block-title">左侧登录区</div>
|
||||
|
||||
<div class="form-row">
|
||||
<span class="row-label">欢迎文案</span>
|
||||
<el-input
|
||||
v-model="localOptions.welcomeText"
|
||||
size="small"
|
||||
placeholder="欢迎来到商城"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<span class="row-label">登录文案</span>
|
||||
<div class="input-with-action">
|
||||
<el-input
|
||||
v-model="localOptions.loginText"
|
||||
size="small"
|
||||
placeholder="登录"
|
||||
/>
|
||||
<el-button
|
||||
size="small"
|
||||
class="action-btn"
|
||||
@click="handleSetLink('loginUrl', '设置登录跳转链接', localOptions.loginUrl || '/login')"
|
||||
title="设置登录链接"
|
||||
>
|
||||
<el-icon><Link /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<span class="row-label">注册文案</span>
|
||||
<div class="input-with-action">
|
||||
<el-input
|
||||
v-model="localOptions.registerText"
|
||||
size="small"
|
||||
placeholder="免费注册"
|
||||
/>
|
||||
<el-button
|
||||
size="small"
|
||||
class="action-btn"
|
||||
@click="handleSetLink('registerUrl', '设置注册跳转链接', localOptions.registerUrl || '/signUp')"
|
||||
title="设置注册链接"
|
||||
>
|
||||
<el-icon><Link /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<span class="row-label">用户中心链接</span>
|
||||
<div
|
||||
class="link-box-btn"
|
||||
@click="handleSetLink('userCenterUrl', '设置用户中心跳转链接', localOptions.userCenterUrl || '/order-list')"
|
||||
>
|
||||
<el-icon><Link /></el-icon>
|
||||
<span class="link-box-text">自定义链接 · {{ localOptions.userCenterUrl || '/order-list' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<span class="row-label">退出文案</span>
|
||||
<el-input
|
||||
v-model="localOptions.logoutText"
|
||||
size="small"
|
||||
placeholder="退出"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧快捷菜单 -->
|
||||
<div class="section-block mt-16">
|
||||
<div class="block-title">右侧快捷菜单</div>
|
||||
|
||||
<div class="menu-list">
|
||||
<div
|
||||
v-for="(item, idx) in localOptions.rightMenus"
|
||||
:key="idx"
|
||||
class="menu-row"
|
||||
>
|
||||
<el-input
|
||||
v-model="item.title"
|
||||
size="small"
|
||||
class="menu-input"
|
||||
placeholder="菜单名称"
|
||||
/>
|
||||
<el-button
|
||||
size="small"
|
||||
class="action-btn"
|
||||
@click="handleSetMenuItemLink(item)"
|
||||
title="设置链接"
|
||||
>
|
||||
<el-icon><Link /></el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
class="action-btn"
|
||||
:disabled="idx === 0"
|
||||
@click="moveMenuItem(idx, -1)"
|
||||
title="上移"
|
||||
>
|
||||
<el-icon><Top /></el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
class="action-btn"
|
||||
:disabled="idx === localOptions.rightMenus.length - 1"
|
||||
@click="moveMenuItem(idx, 1)"
|
||||
title="下移"
|
||||
>
|
||||
<el-icon><Bottom /></el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
class="action-btn del-btn"
|
||||
@click="deleteMenuItem(idx)"
|
||||
title="删除"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-button
|
||||
class="add-menu-btn"
|
||||
size="small"
|
||||
@click="addMenuItem"
|
||||
>
|
||||
+ 添加快捷入口
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. 样式卡片 -->
|
||||
<div class="setting-collapse-card">
|
||||
<div class="collapse-header" @click="expandStyle = !expandStyle">
|
||||
<div class="header-left">
|
||||
<el-icon class="arrow-icon" :class="{ 'is-expanded': expandStyle }"><ArrowRight /></el-icon>
|
||||
<span class="header-title">样式</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="collapse-body" v-show="expandStyle">
|
||||
<div class="form-row">
|
||||
<span class="row-label">文字颜色</span>
|
||||
<div class="color-picker-row">
|
||||
<el-color-picker
|
||||
v-model="localOptions.textColor"
|
||||
size="small"
|
||||
class="color-picker-swatch"
|
||||
/>
|
||||
<el-input
|
||||
v-model="localOptions.textColor"
|
||||
size="small"
|
||||
class="color-input"
|
||||
placeholder="#4b5563"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row mt-12">
|
||||
<span class="row-label">字号</span>
|
||||
<el-input
|
||||
v-model="localOptions.fontSize"
|
||||
size="small"
|
||||
placeholder="12"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, inject } from 'vue';
|
||||
import { ArrowRight, QuestionFilled, Link, Top, Bottom, Delete } from '@element-plus/icons-vue';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const expandContent = ref(true);
|
||||
const expandStyle = ref(true);
|
||||
|
||||
const localOptions = computed(() => {
|
||||
if (!props.options) return {};
|
||||
if (props.options.welcomeText === undefined) props.options.welcomeText = '欢迎来到商城';
|
||||
if (props.options.loginText === undefined) props.options.loginText = '登录';
|
||||
if (props.options.loginUrl === undefined) props.options.loginUrl = '/login';
|
||||
if (props.options.registerText === undefined) props.options.registerText = '免费注册';
|
||||
if (props.options.registerUrl === undefined) props.options.registerUrl = '/signUp';
|
||||
if (props.options.userCenterUrl === undefined) props.options.userCenterUrl = '/order-list';
|
||||
if (props.options.logoutText === undefined) props.options.logoutText = '退出';
|
||||
if (!Array.isArray(props.options.rightMenus)) {
|
||||
props.options.rightMenus = [
|
||||
{ title: '基本信息', url: '/home/UserInfo' },
|
||||
{ title: '我的订单', url: '/home/MyOrder' },
|
||||
{ title: '账户安全', url: '/home/AccountSafe' },
|
||||
{ title: '消息', url: '/home/Msg' }
|
||||
];
|
||||
}
|
||||
if (!props.options.textColor) props.options.textColor = '#4b5563';
|
||||
if (!props.options.fontSize) props.options.fontSize = 12;
|
||||
return props.options;
|
||||
});
|
||||
|
||||
const openLinkPicker = inject('openLinkPicker', null);
|
||||
|
||||
function handleSetLink(field, title, currentVal) {
|
||||
if (openLinkPicker) {
|
||||
openLinkPicker(({ url }) => {
|
||||
localOptions.value[field] = url;
|
||||
});
|
||||
} else {
|
||||
ElMessageBox.prompt('请输入跳转链接路径:', title, {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: currentVal || '/',
|
||||
inputPattern: /^\S+$/,
|
||||
inputErrorMessage: '链接路径不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
localOptions.value[field] = value;
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
function handleSetMenuItemLink(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: /^\S+$/,
|
||||
inputErrorMessage: '链接路径不能为空'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
item.url = value;
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
function moveMenuItem(idx, offset) {
|
||||
const target = idx + offset;
|
||||
const list = localOptions.value.rightMenus;
|
||||
if (target < 0 || target >= list.length) return;
|
||||
const temp = list[idx];
|
||||
list[idx] = list[target];
|
||||
list[target] = temp;
|
||||
}
|
||||
|
||||
function deleteMenuItem(idx) {
|
||||
localOptions.value.rightMenus.splice(idx, 1);
|
||||
}
|
||||
|
||||
function addMenuItem() {
|
||||
localOptions.value.rightMenus.push({
|
||||
title: '新快捷入口',
|
||||
url: '/'
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.panel-top-bar {
|
||||
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;
|
||||
flex-wrap: nowrap;
|
||||
|
||||
.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;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.help-icon {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.header-subtitle {
|
||||
font-size: 11px;
|
||||
color: #909399;
|
||||
margin-left: 6px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.collapse-body {
|
||||
padding: 14px;
|
||||
background: #ffffff;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
|
||||
.section-block {
|
||||
.block-title {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
&.mt-16 {
|
||||
margin-top: 16px;
|
||||
padding-top: 14px;
|
||||
border-top: 1px dashed #e5e7eb;
|
||||
}
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
&.mt-12 {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.row-label {
|
||||
font-size: 12px;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.input-with-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.action-btn {
|
||||
padding: 0 8px;
|
||||
height: 28px;
|
||||
color: #4b5563;
|
||||
border-color: #d1d5db;
|
||||
}
|
||||
}
|
||||
|
||||
.link-box-btn {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
color: #374151;
|
||||
font-size: 12px;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #ff6a00;
|
||||
color: #ff6a00;
|
||||
}
|
||||
|
||||
.link-box-text {
|
||||
max-width: 180px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.color-picker-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.color-input {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 菜单列表 */
|
||||
.menu-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.menu-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.menu-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-color: #e5e7eb;
|
||||
color: #6b7280;
|
||||
|
||||
&.del-btn {
|
||||
border-color: #fca5a5;
|
||||
color: #ef4444;
|
||||
|
||||
&:hover {
|
||||
background: #fef2f2;
|
||||
border-color: #ef4444;
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: #d1d5db;
|
||||
border-color: #f3f4f6;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add-menu-btn {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
border: 1px dashed #d1d5db;
|
||||
border-radius: 4px;
|
||||
background: #ffffff;
|
||||
color: #374151;
|
||||
font-size: 12px;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #ff6a00;
|
||||
border-color: #ff6a00;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,207 @@
|
||||
<template>
|
||||
<div class="panel-user-benefits">
|
||||
<!-- 用户问候与标语 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">问候信息</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">状态标签</span>
|
||||
<el-input v-model="options.badgeText" size="small" placeholder="如:请登录" clearable />
|
||||
</div>
|
||||
<div class="form-item mt-10">
|
||||
<span class="item-label">欢迎主标语</span>
|
||||
<el-input v-model="options.welcomeText" size="small" placeholder="如:欢迎来到商城" clearable />
|
||||
</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 mt-10">
|
||||
<span class="item-label">自定义头像 URL (留空展示默认头像)</span>
|
||||
<el-input v-model="options.avatar" size="small" placeholder="请输入头像图片地址" clearable />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 登录与注册按钮 -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">按钮配置</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">登录按钮文案</span>
|
||||
<el-input v-model="options.loginText" size="small" placeholder="登录" />
|
||||
</div>
|
||||
<div class="form-item mt-10">
|
||||
<span class="item-label">登录跳转路径</span>
|
||||
<el-input v-model="options.loginUrl" size="small" placeholder="/login" />
|
||||
</div>
|
||||
<div class="form-item mt-10">
|
||||
<span class="item-label">注册按钮文案</span>
|
||||
<el-input v-model="options.registerText" size="small" placeholder="注册" />
|
||||
</div>
|
||||
<div class="form-item mt-10">
|
||||
<span class="item-label">注册跳转路径</span>
|
||||
<el-input v-model="options.registerUrl" size="small" placeholder="/signUp" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3项指标节点 (购物车 / 优惠券 / 订单) -->
|
||||
<div class="panel-section">
|
||||
<div class="section-title">指标节点展示</div>
|
||||
|
||||
<!-- 购物车 -->
|
||||
<div class="metric-config-card">
|
||||
<div class="metric-config-head">
|
||||
<span class="metric-title">购物车</span>
|
||||
<el-switch v-model="options.showCart" size="small" />
|
||||
</div>
|
||||
<div class="metric-inputs" v-if="options.showCart !== false">
|
||||
<div class="sub-field">
|
||||
<span class="sub-label">展示文案</span>
|
||||
<el-input v-model="options.cartText" size="small" placeholder="购物车" />
|
||||
</div>
|
||||
<div class="sub-field mt-6">
|
||||
<span class="sub-label">跳转路径</span>
|
||||
<el-input v-model="options.cartUrl" size="small" placeholder="/cart" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 优惠券 -->
|
||||
<div class="metric-config-card mt-10">
|
||||
<div class="metric-config-head">
|
||||
<span class="metric-title">优惠券</span>
|
||||
<el-switch v-model="options.showCoupons" size="small" />
|
||||
</div>
|
||||
<div class="metric-inputs" v-if="options.showCoupons !== false">
|
||||
<div class="sub-field">
|
||||
<span class="sub-label">展示文案</span>
|
||||
<el-input v-model="options.couponText" size="small" placeholder="优惠券" />
|
||||
</div>
|
||||
<div class="sub-field mt-6">
|
||||
<span class="sub-label">跳转路径</span>
|
||||
<el-input v-model="options.couponUrl" size="small" placeholder="/member/coupon" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 订单 -->
|
||||
<div class="metric-config-card mt-10">
|
||||
<div class="metric-config-head">
|
||||
<span class="metric-title">订单</span>
|
||||
<el-switch v-model="options.showOrders" size="small" />
|
||||
</div>
|
||||
<div class="metric-inputs" v-if="options.showOrders !== false">
|
||||
<div class="sub-field">
|
||||
<span class="sub-label">展示文案</span>
|
||||
<el-input v-model="options.orderText" size="small" placeholder="订单" />
|
||||
</div>
|
||||
<div class="sub-field mt-6">
|
||||
<span class="sub-label">跳转路径</span>
|
||||
<el-input v-model="options.orderUrl" size="small" placeholder="/member/order" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化缺省配置
|
||||
if (props.options.badgeText === undefined) props.options.badgeText = '请登录';
|
||||
if (props.options.welcomeText === undefined) props.options.welcomeText = '欢迎来到商城';
|
||||
if (props.options.subtitle === undefined) props.options.subtitle = '登录后查看专属权益';
|
||||
if (props.options.loginText === undefined) props.options.loginText = '登录';
|
||||
if (props.options.registerText === undefined) props.options.registerText = '注册';
|
||||
if (props.options.loginUrl === undefined) props.options.loginUrl = '/login';
|
||||
if (props.options.registerUrl === undefined) props.options.registerUrl = '/signUp';
|
||||
|
||||
if (props.options.showCart === undefined) props.options.showCart = true;
|
||||
if (props.options.showCoupons === undefined) props.options.showCoupons = true;
|
||||
if (props.options.showOrders === undefined) props.options.showOrders = true;
|
||||
|
||||
if (props.options.cartText === undefined) props.options.cartText = '购物车';
|
||||
if (props.options.couponText === undefined) props.options.couponText = '优惠券';
|
||||
if (props.options.orderText === undefined) props.options.orderText = '订单';
|
||||
|
||||
if (props.options.cartUrl === undefined) props.options.cartUrl = '/cart';
|
||||
if (props.options.couponUrl === undefined) props.options.couponUrl = '/member/coupon';
|
||||
if (props.options.orderUrl === undefined) props.options.orderUrl = '/member/order';
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.panel-user-benefits {
|
||||
.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;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
|
||||
.item-label {
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
}
|
||||
}
|
||||
|
||||
.metric-config-card {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
padding: 10px 12px;
|
||||
|
||||
.metric-config-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.metric-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #334155;
|
||||
}
|
||||
}
|
||||
|
||||
.metric-inputs {
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px dashed #e2e8f0;
|
||||
|
||||
.sub-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.sub-label {
|
||||
width: 56px;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mt-6 { margin-top: 6px; }
|
||||
.mt-10 { margin-top: 10px; }
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<div class="panel-video">
|
||||
<div class="panel-section">
|
||||
<div class="section-title">视频播放设置</div>
|
||||
<div class="form-item">
|
||||
<span class="item-label">视频文件 URL (.mp4)</span>
|
||||
<el-input v-model="options.videoUrl" size="small" placeholder="请输入视频在线地址" clearable />
|
||||
</div>
|
||||
|
||||
<div class="form-item">
|
||||
<span class="item-label">视频封面图片 URL</span>
|
||||
<el-input v-model="options.coverUrl" size="small" placeholder="请输入视频封面图地址" clearable />
|
||||
</div>
|
||||
|
||||
<div class="form-item flex-row">
|
||||
<span class="item-label">播放高度 (px)</span>
|
||||
<el-input-number v-model="options.height" :min="200" :max="800" size="small" controls-position="right" />
|
||||
</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>
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.panel-video {
|
||||
.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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -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="40"
|
||||
/>
|
||||
</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>
|
||||
699
manager/src/views/page-decoration/pc/config-panel/index.vue
Normal file
699
manager/src/views/page-decoration/pc/config-panel/index.vue
Normal file
@@ -0,0 +1,699 @@
|
||||
<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 RowConfigPanel from './RowConfigPanel.vue';
|
||||
|
||||
import ComponentConfigPanel from './ComponentConfigPanel.vue';
|
||||
|
||||
import { COMPONENT_DEFINITIONS } from '../config.js';
|
||||
|
||||
|
||||
|
||||
// 导入业务面板
|
||||
|
||||
import PanelTopBar from './PanelTopBar.vue';
|
||||
|
||||
import PanelSearchHeader from './PanelSearchHeader.vue';
|
||||
|
||||
import PanelFooterNav from './PanelFooterNav.vue';
|
||||
|
||||
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 PanelCoupon from './PanelCoupon.vue';
|
||||
import PanelUserBenefits from './PanelUserBenefits.vue';
|
||||
|
||||
import PanelTitle from './PanelTitle.vue';
|
||||
|
||||
import PanelArticle from './PanelArticle.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) {
|
||||
|
||||
if (props.selectedComponent.icon) return props.selectedComponent.icon;
|
||||
|
||||
const def = COMPONENT_DEFINITIONS.find(d => d.type === props.selectedComponent.type);
|
||||
|
||||
return def?.icon || 'Grid';
|
||||
|
||||
}
|
||||
|
||||
return 'Rank';
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
const panelMap = {
|
||||
|
||||
// 结构 (3)
|
||||
|
||||
topBar: PanelTopBar,
|
||||
|
||||
searchHeader: PanelSearchHeader,
|
||||
|
||||
footerNav: PanelFooterNav,
|
||||
|
||||
// 导航 (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)
|
||||
|
||||
couponList: PanelCoupon,
|
||||
userBenefits: PanelUserBenefits,
|
||||
|
||||
// 内容 (3)
|
||||
|
||||
titleBar: PanelTitle,
|
||||
|
||||
articleContent: PanelArticle,
|
||||
|
||||
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>
|
||||
|
||||
1319
manager/src/views/page-decoration/pc/config.js
Normal file
1319
manager/src/views/page-decoration/pc/config.js
Normal file
File diff suppressed because it is too large
Load Diff
228
manager/src/views/page-decoration/pc/controls/BoxModelField.vue
Normal file
228
manager/src/views/page-decoration/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
manager/src/views/page-decoration/pc/controls/NestedBoxModel.vue
Normal file
524
manager/src/views/page-decoration/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>
|
||||
2445
manager/src/views/page-decoration/pc/index.vue
Normal file
2445
manager/src/views/page-decoration/pc/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
1932
manager/src/views/page-decoration/pc/presets/double11Preset.js
Normal file
1932
manager/src/views/page-decoration/pc/presets/double11Preset.js
Normal file
File diff suppressed because it is too large
Load Diff
204
manager/src/views/page-decoration/pc/template/TplArticle.vue
Normal file
204
manager/src/views/page-decoration/pc/template/TplArticle.vue
Normal file
@@ -0,0 +1,204 @@
|
||||
<template>
|
||||
<div class="tpl-article" :style="customStyle">
|
||||
<!-- 文章顶部标题区 -->
|
||||
<div class="article-header" v-if="options.showTitle !== false && options.title">
|
||||
<h2 class="article-main-title">{{ options.title }}</h2>
|
||||
<p class="article-sub-title" v-if="options.subtitle">{{ options.subtitle }}</p>
|
||||
|
||||
<div class="article-meta-bar" v-if="options.showMeta !== false">
|
||||
<span class="meta-tag">{{ options.tag || '品牌专栏' }}</span>
|
||||
<span class="meta-item author" v-if="options.author">作者:{{ options.author }}</span>
|
||||
<span class="meta-item date" v-if="options.date">发布于 {{ options.date }}</span>
|
||||
<span class="meta-item views">阅读 2.4k+</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文章正文排版区 (Rich Text Article Body) -->
|
||||
<div
|
||||
class="article-body-typography"
|
||||
:class="[`align-${options.textAlign || 'left'}`]"
|
||||
:style="{ fontSize: `${options.fontSize || 15}px` }"
|
||||
v-html="renderedContent"
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
componentStyle: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const defaultArticleHtml = `
|
||||
<h3>关于我们 · 甄选美好生活</h3>
|
||||
<p>我们始终坚信,一件好的产品不仅能改善日常生活的品质,更传递着对美好生活的执着追求。自创立以来,我们严选全球顶尖供应链,深耕每一道生产工艺与质检环节,力求将兼具美学价值与极致实用性的商品送达每一位用户手中。</p>
|
||||
<blockquote>品质是我们的信仰,体验是我们的底线。从选材到交付,每一步都经得起时光检验。</blockquote>
|
||||
<h4>四大核心服务保障</h4>
|
||||
<ul>
|
||||
<li><strong>100% 官方正品</strong>:源头品牌直采,全链路防伪溯源,假一赔十保障。</li>
|
||||
<li><strong>极速顺丰冷链</strong>:全国多仓联动发货,核心城市支持当日达/次日达。</li>
|
||||
<li><strong>7天无理由退换</strong>:专业客服团队 7×24 小时贴心守护,售后无忧。</li>
|
||||
<li><strong>会员专享权益</strong>:尊享积分抵现、生日特权礼及专属品鉴沙龙。</li>
|
||||
</ul>
|
||||
<p>感谢一路相伴的每一位朋友,未来我们将持续探索更多优质生活场景,与您共同见证品质生活的美好绽放。</p>
|
||||
`.trim();
|
||||
|
||||
const renderedContent = computed(() => {
|
||||
return props.options?.content || defaultArticleHtml;
|
||||
});
|
||||
|
||||
const customStyle = computed(() => {
|
||||
const s = props.componentStyle || {};
|
||||
return {
|
||||
backgroundColor: s.backgroundColor || '#ffffff',
|
||||
padding: Array.isArray(s.padding)
|
||||
? `${s.padding[0]}px ${s.padding[1]}px ${s.padding[2]}px ${s.padding[3]}px`
|
||||
: '24px 28px',
|
||||
borderRadius: Array.isArray(s.radius)
|
||||
? `${s.radius[0]}px ${s.radius[1]}px ${s.radius[2]}px ${s.radius[3]}px`
|
||||
: '12px'
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tpl-article {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
user-select: none;
|
||||
|
||||
.article-header {
|
||||
padding-bottom: 16px;
|
||||
margin-bottom: 18px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
|
||||
.article-main-title {
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
color: #0f172a;
|
||||
line-height: 1.35;
|
||||
margin: 0 0 8px 0;
|
||||
letter-spacing: -0.3px;
|
||||
}
|
||||
|
||||
.article-sub-title {
|
||||
font-size: 14px;
|
||||
color: #64748b;
|
||||
line-height: 1.5;
|
||||
margin: 0 0 12px 0;
|
||||
}
|
||||
|
||||
.article-meta-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
|
||||
.meta-tag {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
background: #eff6ff;
|
||||
color: #2563eb;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 文章排版样式系统 (Editorial Typography) */
|
||||
.article-body-typography {
|
||||
color: #334155;
|
||||
line-height: 1.8;
|
||||
letter-spacing: 0.2px;
|
||||
|
||||
&.align-center {
|
||||
text-align: center;
|
||||
}
|
||||
&.align-justify {
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
:deep(h2), :deep(h3), :deep(h4) {
|
||||
color: #0f172a;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
:deep(h2) {
|
||||
font-size: 20px;
|
||||
border-left: 4px solid #2563eb;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
:deep(h3) {
|
||||
font-size: 18px;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
:deep(h4) {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
:deep(p) {
|
||||
margin: 0 0 14px 0;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
:deep(blockquote) {
|
||||
margin: 16px 0;
|
||||
padding: 12px 18px;
|
||||
background: #f8fafc;
|
||||
border-left: 4px solid #3b82f6;
|
||||
border-radius: 0 8px 8px 0;
|
||||
color: #475569;
|
||||
font-style: italic;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
:deep(ul), :deep(ol) {
|
||||
margin: 12px 0 16px 20px;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
margin-bottom: 6px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(img) {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
margin: 16px auto;
|
||||
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
:deep(strong) {
|
||||
color: #0f172a;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
148
manager/src/views/page-decoration/pc/template/TplBrandWall.vue
Normal file
148
manager/src/views/page-decoration/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: '/static/brand-wall/brand_1.png', url: '/goodsList?category=1' },
|
||||
{ title: '美妆奢宠 · 每满300减50', img: '/static/brand-wall/brand_2.png', url: '/goodsList?category=2' },
|
||||
{ title: '运动潮品 · 爆款低至5折', img: '/static/brand-wall/brand_3.png', url: '/goodsList?category=3' },
|
||||
{ title: '品质生活 · 限时第2件半价', img: '/static/brand-wall/brand_4.png', url: '/goodsList?category=4' }
|
||||
];
|
||||
});
|
||||
|
||||
const actualColumns = computed(() => {
|
||||
const baseCols = Number(props.options?.columns) || 4;
|
||||
const ratio = props.componentStyle?.widthRatio || '100%';
|
||||
if (ratio === '33.33%' || ratio === '33.3%') {
|
||||
return Math.min(baseCols, 2);
|
||||
}
|
||||
if (ratio === '50%') {
|
||||
return Math.min(baseCols, 2);
|
||||
}
|
||||
if (ratio === '66.67%' || ratio === '66.6%' || ratio === '66.7%') {
|
||||
return Math.min(baseCols, 3);
|
||||
}
|
||||
return baseCols;
|
||||
});
|
||||
|
||||
const customStyle = computed(() => {
|
||||
const s = props.componentStyle || {};
|
||||
return {
|
||||
backgroundColor: s.backgroundColor || 'transparent',
|
||||
padding: Array.isArray(s.padding)
|
||||
? `${s.padding[0]}px ${s.padding[1]}px ${s.padding[2]}px ${s.padding[3]}px`
|
||||
: '10px 0'
|
||||
};
|
||||
});
|
||||
|
||||
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>
|
||||
312
manager/src/views/page-decoration/pc/template/TplCarousel.vue
Normal file
312
manager/src/views/page-decoration/pc/template/TplCarousel.vue
Normal file
@@ -0,0 +1,312 @@
|
||||
<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: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
return [
|
||||
{ title: '出游囤好货', img: '/static/carousel/banner_1.png', url: '/goodsList?seckill=1' },
|
||||
{ title: '科技新品', img: '/static/carousel/banner_2.png', url: '/goodsList?category=1' },
|
||||
{ title: '原产地直采', img: '/static/carousel/banner_3.png', url: '/goodsList?category=2' },
|
||||
{ title: '大牌美妆', img: '/static/carousel/banner_4.png', url: '/goodsList?category=3' }
|
||||
];
|
||||
});
|
||||
|
||||
const currentSlide = computed(() => {
|
||||
return slidesList.value[currentIdx.value] || slidesList.value[0];
|
||||
});
|
||||
|
||||
function getAspectRatioCss(ratio) {
|
||||
if (ratio === '16:9') return '16 / 9';
|
||||
if (ratio === '2:1') return '2 / 1';
|
||||
if (ratio === '3:1') return '3 / 1';
|
||||
if (ratio === 'custom') return null;
|
||||
return '2.5 / 1';
|
||||
}
|
||||
|
||||
const customStyle = computed(() => {
|
||||
const s = props.componentStyle || {};
|
||||
const fit = props.options.fitMode || 'cover';
|
||||
|
||||
const styleObj = {
|
||||
borderRadius: Array.isArray(s.radius)
|
||||
? `${s.radius[0]}px ${s.radius[1]}px ${s.radius[2]}px ${s.radius[3]}px`
|
||||
: '0px',
|
||||
'--carousel-fit': fit
|
||||
};
|
||||
|
||||
if (props.options.height) {
|
||||
styleObj.height = `${props.options.height}px`;
|
||||
styleObj.aspectRatio = 'auto';
|
||||
} else {
|
||||
const ratio = props.options.aspectRatio || '2.5:1';
|
||||
const ratioCss = getAspectRatioCss(ratio);
|
||||
if (ratioCss) {
|
||||
styleObj.aspectRatio = ratioCss;
|
||||
styleObj['--carousel-ratio'] = ratioCss;
|
||||
} else {
|
||||
styleObj.height = `${s.height || 420}px`;
|
||||
styleObj.aspectRatio = 'auto';
|
||||
}
|
||||
}
|
||||
|
||||
return styleObj;
|
||||
});
|
||||
|
||||
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() {
|
||||
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%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
background: #f8fafc;
|
||||
box-sizing: border-box;
|
||||
|
||||
.carousel-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
.slide-view {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.slide-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: var(--carousel-fit, contain);
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
// 切换动效
|
||||
.carousel-fade-enter-active,
|
||||
.carousel-fade-leave-active {
|
||||
transition: opacity 0.4s ease;
|
||||
}
|
||||
.carousel-fade-enter-from,
|
||||
.carousel-fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
// 左右箭头按钮
|
||||
.arrow-btn {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background: rgba(15, 23, 42, 0.35);
|
||||
color: #ffffff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
transition: all 0.2s ease;
|
||||
z-index: 10;
|
||||
backdrop-filter: blur(2px);
|
||||
|
||||
&:hover {
|
||||
background: rgba(15, 23, 42, 0.65);
|
||||
transform: translateY(-50%) scale(1.08);
|
||||
}
|
||||
|
||||
&.prev-btn {
|
||||
left: 16px;
|
||||
}
|
||||
|
||||
&.next-btn {
|
||||
right: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
// 底部指示点
|
||||
.carousel-indicators {
|
||||
position: absolute;
|
||||
bottom: 14px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
z-index: 10;
|
||||
|
||||
.indicator-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.65);
|
||||
cursor: pointer;
|
||||
transition: all 0.25s ease;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
|
||||
|
||||
&.active {
|
||||
width: 22px;
|
||||
border-radius: 5px;
|
||||
background: #ea580c;
|
||||
}
|
||||
|
||||
&:hover:not(.active) {
|
||||
background: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-compact {
|
||||
.arrow-btn {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
font-size: 12px;
|
||||
&.prev-btn { left: 8px; }
|
||||
&.next-btn { right: 8px; }
|
||||
}
|
||||
.carousel-indicators {
|
||||
bottom: 8px;
|
||||
gap: 5px;
|
||||
.indicator-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
&.active { width: 14px; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-mini {
|
||||
.arrow-btn {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
font-size: 10px;
|
||||
&.prev-btn { left: 4px; }
|
||||
&.next-btn { right: 4px; }
|
||||
}
|
||||
.carousel-indicators {
|
||||
bottom: 4px;
|
||||
gap: 3px;
|
||||
.indicator-dot {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
&.active { width: 10px; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<div class="tpl-category-mega-menu" :style="customStyle">
|
||||
<!-- 顶部标题:分类(同万米设计,纯净大方) -->
|
||||
<div class="menu-header">
|
||||
<span class="header-title">{{ options.title || '分类' }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 垂直一级类目列表(8项均匀铺满整个高度) -->
|
||||
<div class="category-list">
|
||||
<div
|
||||
v-for="(cat, idx) in categoriesList"
|
||||
:key="idx"
|
||||
class="cat-item"
|
||||
:style="{
|
||||
'--text-color': options.textColor || '#334155',
|
||||
'--active-color': options.activeColor || '#ff6000',
|
||||
'--font-size': `${options.fontSize || 13}px`
|
||||
}"
|
||||
>
|
||||
<span class="cat-name">{{ cat.name }}</span>
|
||||
<el-icon class="arrow-icon"><ArrowRight /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { ArrowRight } from '@element-plus/icons-vue';
|
||||
|
||||
const props = defineProps({
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
componentStyle: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const defaultCategories = [
|
||||
{ id: '1', name: '潮流数码' },
|
||||
{ id: '2', name: '奢宠美妆' },
|
||||
{ id: '3', name: '智能家电' },
|
||||
{ id: '4', name: '服饰鞋包' },
|
||||
{ id: '5', name: '运动潮流' },
|
||||
{ id: '6', name: '精品生鲜' },
|
||||
{ id: '7', name: '居家生活' },
|
||||
{ id: '8', name: '母婴玩具' }
|
||||
];
|
||||
|
||||
const categoriesList = computed(() => {
|
||||
if (Array.isArray(props.options?.categories) && props.options.categories.length) {
|
||||
return props.options.categories.map(c => {
|
||||
const rawName = c.name || c.title || '';
|
||||
const cleanName = rawName.includes('/') ? rawName.split('/')[0].trim() : rawName;
|
||||
return {
|
||||
...c,
|
||||
name: cleanName
|
||||
};
|
||||
});
|
||||
}
|
||||
return defaultCategories;
|
||||
});
|
||||
|
||||
const customStyle = computed(() => {
|
||||
const s = props.componentStyle || {};
|
||||
return {
|
||||
backgroundColor: s.backgroundColor || '#ffffff',
|
||||
borderRadius: Array.isArray(s.radius)
|
||||
? `${s.radius[0]}px ${s.radius[1]}px ${s.radius[2]}px ${s.radius[3]}px`
|
||||
: '8px'
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tpl-category-mega-menu {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 420px;
|
||||
box-sizing: border-box;
|
||||
background: #ffffff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
user-select: none;
|
||||
overflow: hidden;
|
||||
|
||||
.menu-header {
|
||||
padding: 14px 16px 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.header-title {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
}
|
||||
|
||||
.category-list {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: 2px 0 10px;
|
||||
|
||||
.cat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
.cat-name {
|
||||
font-size: var(--font-size, 13px);
|
||||
color: var(--text-color, #334155);
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.arrow-icon {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
transition: transform 0.15s ease, color 0.15s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #fff7ed;
|
||||
|
||||
.cat-name {
|
||||
color: var(--active-color, #ff6000);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.arrow-icon {
|
||||
color: var(--active-color, #ff6000);
|
||||
transform: translateX(2px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
205
manager/src/views/page-decoration/pc/template/TplCategoryNav.vue
Normal file
205
manager/src/views/page-decoration/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>
|
||||
@@ -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 = '/static/goods/goods_1.png';
|
||||
|
||||
const heroImg = computed(() => {
|
||||
return props.options?.heroImage || '/static/banner-floor/floor_banner_1.png';
|
||||
});
|
||||
|
||||
const showHero = computed(() => {
|
||||
const ratio = props.componentStyle?.widthRatio || '100%';
|
||||
if (ratio === '16.67%' || ratio === '16.6%' || ratio === '33.33%' || ratio === '33.3%') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const rightColumns = computed(() => {
|
||||
const ratio = props.componentStyle?.widthRatio || '100%';
|
||||
if (ratio === '16.67%' || ratio === '16.6%') return 1;
|
||||
if (ratio === '33.33%' || ratio === '33.3%') return 2;
|
||||
if (ratio === '50%') return showHero.value ? 2 : 3;
|
||||
if (ratio === '66.67%' || ratio === '66.6%') return showHero.value ? 3 : 4;
|
||||
return 3; // 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: '/static/goods/goods_1.png' },
|
||||
{ id: '2', goodsName: '无线头戴式深度主动降噪蓝牙耳机 空间音频Hi-Res', price: '1099.00', originalPrice: '1499.00', thumbnail: '/static/goods/goods_2.png' },
|
||||
{ id: '3', goodsName: '14英寸 全面屏轻薄商务笔记本电脑 酷睿i7 高刷防眩光', price: '5699.00', originalPrice: '6299.00', thumbnail: '/static/goods/goods_3.png' },
|
||||
{ id: '4', goodsName: '运动健康专业监测智能手表 心率血氧长续航50米防水', price: '899.00', originalPrice: '1199.00', thumbnail: '/static/goods/goods_4.png' },
|
||||
{ id: '5', goodsName: '意式半自动家用复古咖啡机 20Bar高压恒温萃取蒸汽奶泡', price: '899.00', originalPrice: '1280.00', thumbnail: '/static/goods/goods_6.png' },
|
||||
{ id: '6', goodsName: '奢宠酵母焕活修护精华露 50ml 紧致淡纹抗皱滋养', price: '680.00', originalPrice: '890.00', thumbnail: '/static/goods/goods_5.png' }
|
||||
];
|
||||
|
||||
const displayGoods = computed(() => {
|
||||
if (Array.isArray(props.options?.goods) && props.options.goods.length > 0) {
|
||||
return props.options.goods;
|
||||
}
|
||||
const count = props.options?.goodsCount || 6;
|
||||
return defaultGoods.slice(0, count);
|
||||
});
|
||||
|
||||
const customStyle = computed(() => {
|
||||
const s = props.componentStyle || {};
|
||||
return {
|
||||
backgroundColor: s.backgroundColor || '#ffffff',
|
||||
padding: Array.isArray(s.padding)
|
||||
? `${s.padding[0]}px ${s.padding[1]}px ${s.padding[2]}px ${s.padding[3]}px`
|
||||
: '16px 20px',
|
||||
borderRadius: Array.isArray(s.radius)
|
||||
? `${s.radius[0]}px ${s.radius[1]}px ${s.radius[2]}px ${s.radius[3]}px`
|
||||
: '12px'
|
||||
};
|
||||
});
|
||||
</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: 12px;
|
||||
color: #1e293b;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
height: 34px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
margin-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
manager/src/views/page-decoration/pc/template/TplChannelGrid.vue
Normal file
281
manager/src/views/page-decoration/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>
|
||||
162
manager/src/views/page-decoration/pc/template/TplCoupon.vue
Normal file
162
manager/src/views/page-decoration/pc/template/TplCoupon.vue
Normal file
@@ -0,0 +1,162 @@
|
||||
<template>
|
||||
<div class="tpl-coupon" :class="`skin-${options.skin || 'red'}`" :style="customStyle">
|
||||
<div
|
||||
class="coupon-grid"
|
||||
:style="{ gridTemplateColumns: `repeat(${actualColumns}, 1fr)` }"
|
||||
>
|
||||
<div v-for="(cp, idx) in couponList" :key="cp.id || idx" class="coupon-item">
|
||||
<div class="left-val">
|
||||
<span class="symbol">¥</span>
|
||||
<span class="val">{{ cp.price }}</span>
|
||||
</div>
|
||||
<div class="right-info">
|
||||
<span class="title">满{{ cp.consumeThreshold }}可用</span>
|
||||
<span class="scope" v-if="cp.title">{{ cp.title }}</span>
|
||||
<span class="btn">立即领取</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
componentStyle: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const defaultCoupons = [
|
||||
{ id: '1', price: 20, consumeThreshold: 199, title: '全品类通用券', limitText: '全场通用 无门槛限制' },
|
||||
{ id: '2', price: 50, consumeThreshold: 399, title: '数码家电专享券', limitText: '数码/手机/家电可用' },
|
||||
{ id: '3', price: 100, consumeThreshold: 699, title: '大牌美妆特惠券', limitText: '美妆奢护专区可用' },
|
||||
{ id: '4', price: 10, consumeThreshold: 99, title: '新客首单立减券', limitText: '首次下单专享福利' }
|
||||
];
|
||||
|
||||
const actualColumns = computed(() => {
|
||||
const baseCols = Number(props.options?.columns) || 4;
|
||||
const ratio = props.componentStyle?.widthRatio || '100%';
|
||||
if (ratio === '16.67%' || ratio === '16.6%' || ratio === '16.7%') {
|
||||
return 1;
|
||||
}
|
||||
if (ratio === '33.33%' || ratio === '33.3%') {
|
||||
return Math.min(baseCols, 2);
|
||||
}
|
||||
if (ratio === '50%') {
|
||||
return Math.min(baseCols, 2);
|
||||
}
|
||||
if (ratio === '66.67%' || ratio === '66.6%' || ratio === '66.7%') {
|
||||
return Math.min(baseCols, 3);
|
||||
}
|
||||
return baseCols;
|
||||
});
|
||||
|
||||
const customStyle = computed(() => {
|
||||
const s = props.componentStyle || {};
|
||||
return {
|
||||
backgroundColor: s.backgroundColor || 'transparent',
|
||||
padding: Array.isArray(s.padding)
|
||||
? `${s.padding[0]}px ${s.padding[1]}px ${s.padding[2]}px ${s.padding[3]}px`
|
||||
: '10px 0',
|
||||
borderRadius: Array.isArray(s.radius)
|
||||
? `${s.radius[0]}px ${s.radius[1]}px ${s.radius[2]}px ${s.radius[3]}px`
|
||||
: '0'
|
||||
};
|
||||
});
|
||||
|
||||
const couponList = computed(() => {
|
||||
if (Array.isArray(props.options?.coupons) && props.options.coupons.length > 0) {
|
||||
return props.options.coupons;
|
||||
}
|
||||
const count = props.options?.columns || 4;
|
||||
return defaultCoupons.slice(0, count);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tpl-coupon {
|
||||
width: 100%;
|
||||
user-select: none;
|
||||
box-sizing: border-box;
|
||||
|
||||
.coupon-grid {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
|
||||
.coupon-item {
|
||||
display: flex;
|
||||
height: 72px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
|
||||
.left-val {
|
||||
width: 80px;
|
||||
color: #ffffff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
|
||||
.symbol { font-size: 13px; }
|
||||
.val { font-size: 22px; }
|
||||
}
|
||||
|
||||
.right-info {
|
||||
flex: 1;
|
||||
background: #ffffff;
|
||||
padding: 6px 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
border: 1px solid #f1f5f9;
|
||||
border-left: none;
|
||||
|
||||
.title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.scope {
|
||||
font-size: 11px;
|
||||
color: #64748b;
|
||||
margin-top: 2px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.btn {
|
||||
font-size: 11px;
|
||||
color: #e11d48;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
margin-top: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.skin-red {
|
||||
.left-val { background: linear-gradient(135deg, #f43f5e 0%, #e11d48 100%); }
|
||||
.btn { color: #e11d48 !important; }
|
||||
}
|
||||
|
||||
&.skin-dark {
|
||||
.left-val { background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%); color: #f59e0b; }
|
||||
.btn { color: #d97706 !important; }
|
||||
}
|
||||
|
||||
&.skin-gradient {
|
||||
.left-val { background: linear-gradient(135deg, #8b5cf6 0%, #ec4899 100%); }
|
||||
.btn { color: #8b5cf6 !important; }
|
||||
}
|
||||
}
|
||||
</style>
|
||||
224
manager/src/views/page-decoration/pc/template/TplFooterNav.vue
Normal file
224
manager/src/views/page-decoration/pc/template/TplFooterNav.vue
Normal file
@@ -0,0 +1,224 @@
|
||||
<template>
|
||||
<div class="tpl-footer-nav" :style="customStyle">
|
||||
<div class="footer-wrap">
|
||||
<!-- 1. 服务保障条 -->
|
||||
<div class="assurances" v-if="options.showAssurance !== false && options.assurances?.length">
|
||||
<component
|
||||
:is="item.url ? 'a' : 'div'"
|
||||
v-for="(item, idx) in options.assurances"
|
||||
:key="idx"
|
||||
class="ass-item"
|
||||
:class="{ 'is-link': !!item.url }"
|
||||
:href="item.url || 'javascript:;'"
|
||||
@click.prevent
|
||||
>
|
||||
<span class="code">{{ item.code }}</span>
|
||||
<div class="ass-text">
|
||||
<span class="t">{{ item.title }}</span>
|
||||
<span class="desc" v-if="item.desc">{{ item.desc }}</span>
|
||||
</div>
|
||||
</component>
|
||||
</div>
|
||||
|
||||
<!-- 2. 帮助与指南分组列表 -->
|
||||
<div class="guide-groups" v-if="options.guideGroups && options.guideGroups.length">
|
||||
<div v-for="(group, gIdx) in options.guideGroups" :key="gIdx" class="guide-col">
|
||||
<div class="guide-title">{{ group.title }}</div>
|
||||
<div class="guide-links" v-if="group.links && group.links.length">
|
||||
<a
|
||||
v-for="(link, lIdx) in group.links"
|
||||
:key="lIdx"
|
||||
class="guide-link"
|
||||
:href="link.url || 'javascript:;'"
|
||||
@click.prevent
|
||||
>
|
||||
{{ link.text }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3. 版权与 ICP 备案 -->
|
||||
<div class="copyright">
|
||||
<template v-if="options.icpText">
|
||||
<a
|
||||
v-if="options.icpLink"
|
||||
class="footer-link"
|
||||
:href="options.icpLink"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@click.stop
|
||||
>{{ options.icpText }}</a>
|
||||
<span v-else>{{ options.icpText }}</span>
|
||||
<span class="divider"> | </span>
|
||||
</template>
|
||||
<a
|
||||
v-if="options.copyrightLink"
|
||||
class="footer-link"
|
||||
:href="options.copyrightLink"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@click.stop
|
||||
>{{ options.copyright || 'Copyright © 2026 Lilishop. All Rights Reserved.' }}</a>
|
||||
<span v-else>{{ options.copyright || 'Copyright © 2026 Lilishop. All Rights Reserved.' }}</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 || {};
|
||||
let bg = s.backgroundColor;
|
||||
if (bg && /^rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*0\s*\)$/.test(bg.trim())) {
|
||||
bg = bg.trim().replace(/,\s*0\s*\)$/, ', 1)');
|
||||
}
|
||||
return {
|
||||
backgroundColor: (bg && bg !== 'transparent') ? bg : 'transparent'
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tpl-footer-nav {
|
||||
width: 100%;
|
||||
padding: 30px 0 20px;
|
||||
user-select: none;
|
||||
|
||||
.footer-wrap {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
|
||||
.assurances {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
padding-bottom: 24px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
|
||||
.ass-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
text-decoration: none;
|
||||
|
||||
&.is-link {
|
||||
cursor: pointer;
|
||||
transition: transform 0.15s ease, opacity 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.85;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
|
||||
.code {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid #e11d48;
|
||||
color: #e11d48;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ass-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
|
||||
.t {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
line-height: 1.2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.guide-groups {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 28px 20px 24px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
|
||||
.guide-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.guide-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #334155;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.guide-links {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.guide-link {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
text-decoration: none;
|
||||
transition: color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
color: #e11d48;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.copyright {
|
||||
text-align: center;
|
||||
padding-top: 20px;
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
line-height: 1.6;
|
||||
|
||||
.footer-link {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
transition: color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
color: #e11d48;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
margin: 0 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -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 || 260}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>
|
||||
313
manager/src/views/page-decoration/pc/template/TplGoods.vue
Normal file
313
manager/src/views/page-decoration/pc/template/TplGoods.vue
Normal file
@@ -0,0 +1,313 @@
|
||||
<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>
|
||||
</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="meta-row" v-if="options.showSales !== false">
|
||||
<span class="sales-text">已售 {{ 120 + idx * 85 }}+ 件</span>
|
||||
<span class="rate-text">99%好评</span>
|
||||
</div>
|
||||
|
||||
<div class="price-row">
|
||||
<div class="price-box">
|
||||
<span class="symbol">¥</span>
|
||||
<span class="price-val" v-if="options.showPrice !== false">{{ g.price }}</span>
|
||||
<span class="orig-price" v-if="options.showOriginalPrice && g.originalPrice">¥{{ g.originalPrice }}</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="add-cart-circle"
|
||||
v-if="options.showCartBtn !== false"
|
||||
title="加入购物车"
|
||||
>
|
||||
+
|
||||
</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 = '/static/goods/goods_1.png';
|
||||
|
||||
const defaultMockGoods = [
|
||||
{ id: '1', goodsName: '旗舰 5G 智能手机 12GB+256GB 超光影影像系统', price: '3999.00', originalPrice: '4599.00', thumbnail: '/static/goods/goods_1.png' },
|
||||
{ id: '2', goodsName: '无线头戴式深度主动降噪蓝牙耳机 空间音频Hi-Res', price: '1099.00', originalPrice: '1499.00', thumbnail: '/static/goods/goods_2.png' },
|
||||
{ id: '3', goodsName: '14英寸 全面屏轻薄商务笔记本电脑 酷睿i7 高刷防眩光', price: '5699.00', originalPrice: '6299.00', thumbnail: '/static/goods/goods_3.png' },
|
||||
{ id: '4', goodsName: '运动健康专业监测智能手表 心率血氧长续航50米防水', price: '899.00', originalPrice: '1199.00', thumbnail: '/static/goods/goods_4.png' },
|
||||
{ id: '5', goodsName: '奢宠酵母焕活修护精华露 50ml 紧致淡纹抗皱滋养', price: '680.00', originalPrice: '890.00', thumbnail: '/static/goods/goods_5.png' },
|
||||
{ id: '6', goodsName: '意式半自动家用复古咖啡机 20Bar高压恒温萃取蒸汽奶泡', price: '899.00', originalPrice: '1280.00', thumbnail: '/static/goods/goods_6.png' },
|
||||
{ id: '7', goodsName: '碳板专业竞速轻量缓震跑步鞋 透气防滑男女同款潮鞋', price: '459.00', originalPrice: '699.00', thumbnail: '/static/goods/goods_7.png' },
|
||||
{ id: '8', goodsName: '极简静音无雾负离子香薰加湿器 大容量客厅卧室通用', price: '299.00', originalPrice: '399.00', thumbnail: '/static/goods/goods_8.png' },
|
||||
{ id: '9', goodsName: '折叠屏旗舰手机 典藏陶瓷白 悬停双旋水滴铰链', price: '6999.00', originalPrice: '7999.00', thumbnail: '/static/goods/goods_1.png' },
|
||||
{ id: '10', goodsName: '真无线触控降噪蓝牙耳机 超低延迟游戏降噪', price: '599.00', originalPrice: '799.00', thumbnail: '/static/goods/goods_2.png' },
|
||||
{ id: '11', goodsName: '国AA级减蓝光润眼长臂台灯 智能感光多档色温无极调光', price: '249.00', originalPrice: '359.00', thumbnail: '/static/goods/goods_4.png' },
|
||||
{ id: '12', goodsName: '航空级铝镁合金静音万向轮拉杆登机箱 20英寸防刮耐磨', price: '499.00', originalPrice: '699.00', thumbnail: '/static/goods/goods_7.png' }
|
||||
];
|
||||
|
||||
const actualColumns = computed(() => {
|
||||
// 商品列表整行默认 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 (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: 20px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
font-size: 13px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
|
||||
.more-link {
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #e11d48;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.goods-flow-grid {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
|
||||
.goods-flow-card {
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #f1f5f9;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: all 0.25s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.08);
|
||||
border-color: #e2e8f0;
|
||||
|
||||
.thumb-img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.add-cart-circle {
|
||||
background: #e11d48;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
.card-thumb-wrap {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 1 / 1;
|
||||
background: #f8fafc;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.thumb-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.self-tag {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
left: 6px;
|
||||
background: #e11d48;
|
||||
color: #ffffff;
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
padding: 1px 5px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
|
||||
.goods-name {
|
||||
font-size: 12px;
|
||||
color: #1e293b;
|
||||
line-height: 1.4;
|
||||
height: 34px;
|
||||
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;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
329
manager/src/views/page-decoration/pc/template/TplGoodsGrid.vue
Normal file
329
manager/src/views/page-decoration/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 = '/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: '/static/goods/goods_1.png' },
|
||||
{ id: '2', goodsName: '无线头戴式深度主动降噪蓝牙耳机 空间音频Hi-Res', price: '1099.00', originalPrice: '1499.00', thumbnail: '/static/goods/goods_2.png' },
|
||||
{ id: '3', goodsName: '14英寸 全面屏轻薄商务笔记本电脑 酷睿i7 高刷防眩光', price: '5699.00', originalPrice: '6299.00', thumbnail: '/static/goods/goods_3.png' },
|
||||
{ id: '4', goodsName: '运动健康专业监测智能手表 心率血氧长续航50米防水', price: '899.00', originalPrice: '1199.00', thumbnail: '/static/goods/goods_4.png' },
|
||||
{ id: '5', goodsName: '奢宠酵母焕活修护精华露 50ml 紧致淡纹抗皱滋养', price: '680.00', originalPrice: '890.00', thumbnail: '/static/goods/goods_5.png' },
|
||||
{ id: '6', goodsName: '意式半自动家用复古咖啡机 20Bar高压恒温萃取蒸汽奶泡', price: '899.00', originalPrice: '1280.00', thumbnail: '/static/goods/goods_6.png' },
|
||||
{ id: '7', goodsName: '碳板专业竞速轻量缓震跑步鞋 透气防滑男女同款潮鞋', price: '459.00', originalPrice: '699.00', thumbnail: '/static/goods/goods_7.png' },
|
||||
{ id: '8', goodsName: '极简静音无雾负离子香薰加湿器 大容量客厅卧室通用', price: '299.00', originalPrice: '399.00', thumbnail: '/static/goods/goods_8.png' },
|
||||
{ id: '9', goodsName: '智能降噪AI翻译蓝牙墨镜 超轻防紫外线偏光太阳镜', price: '599.00', originalPrice: '799.00', thumbnail: '/static/goods/goods_1.png' },
|
||||
{ id: '10', goodsName: '15.6英寸 4K超高清便携式显示器 Type-C一线直连高色域', price: '1299.00', originalPrice: '1699.00', thumbnail: '/static/goods/goods_3.png' }
|
||||
];
|
||||
|
||||
const displayGoods = computed(() => {
|
||||
if (Array.isArray(props.options?.goods) && props.options.goods.length > 0) {
|
||||
return props.options.goods;
|
||||
}
|
||||
const count = (actualColumns.value * 2) || 10;
|
||||
return defaultGridGoods.slice(0, count);
|
||||
});
|
||||
|
||||
const customStyle = computed(() => {
|
||||
const s = props.componentStyle || {};
|
||||
return {
|
||||
backgroundColor: s.backgroundColor || '#ffffff',
|
||||
padding: Array.isArray(s.padding)
|
||||
? `${s.padding[0]}px ${s.padding[1]}px ${s.padding[2]}px ${s.padding[3]}px`
|
||||
: '18px 20px',
|
||||
borderRadius: Array.isArray(s.radius)
|
||||
? `${s.radius[0]}px ${s.radius[1]}px ${s.radius[2]}px ${s.radius[3]}px`
|
||||
: '12px'
|
||||
};
|
||||
});
|
||||
</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: 13px;
|
||||
color: #334155;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
height: 36px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
margin-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
manager/src/views/page-decoration/pc/template/TplGridFloor.vue
Normal file
534
manager/src/views/page-decoration/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 = '/static/goods/goods_1.png';
|
||||
const activeTagIdx = ref(0);
|
||||
|
||||
const subCategories = computed(() => {
|
||||
if (Array.isArray(props.options?.subCategories) && props.options.subCategories.length > 0) {
|
||||
return props.options.subCategories;
|
||||
}
|
||||
return ['旗舰手机', '轻薄笔电', '降噪耳机', '智能穿戴', '数码配件'];
|
||||
});
|
||||
|
||||
const posterTags = computed(() => {
|
||||
if (Array.isArray(props.options?.posterTags) && props.options.posterTags.length > 0) {
|
||||
return props.options.posterTags;
|
||||
}
|
||||
return ['爆款低至5折', '新品首发', '分期免息'];
|
||||
});
|
||||
|
||||
const posterImg = computed(() => {
|
||||
return props.options?.posterImg || '/static/banner-floor/floor_banner_1.png';
|
||||
});
|
||||
|
||||
const showPoster = computed(() => {
|
||||
const ratio = props.componentStyle?.widthRatio || '100%';
|
||||
// 如果宽度太窄(如六分之一或三分之一),隐藏左侧海报避免挤压
|
||||
if (ratio === '16.67%' || ratio === '16.6%' || ratio === '33.33%' || ratio === '33.3%') {
|
||||
return false;
|
||||
}
|
||||
return props.options?.showPoster !== false;
|
||||
});
|
||||
|
||||
const rightColumns = computed(() => {
|
||||
const ratio = props.componentStyle?.widthRatio || '100%';
|
||||
let baseCols = Number(props.options?.columns) || 4;
|
||||
if (ratio === '16.67%' || ratio === '16.6%') return 1;
|
||||
if (ratio === '33.33%' || ratio === '33.3%') return 2;
|
||||
if (ratio === '50%') return showPoster.value ? 2 : 3;
|
||||
if (ratio === '66.67%' || ratio === '66.6%') return showPoster.value ? 3 : 4;
|
||||
return baseCols; // 100% 默认 4 列
|
||||
});
|
||||
|
||||
const defaultFloorGoods = [
|
||||
{ id: '101', goodsName: '旗舰 5G 智能手机 12GB+256GB 超光影影像系统', price: '3999.00', originalPrice: '4599.00', thumbnail: '/static/goods/goods_1.png' },
|
||||
{ id: '102', goodsName: '无线头戴式深度主动降噪蓝牙耳机 空间音频Hi-Res', price: '1099.00', originalPrice: '1499.00', thumbnail: '/static/goods/goods_2.png' },
|
||||
{ id: '103', goodsName: '14英寸 全面屏轻薄商务笔记本电脑 酷睿i7 高刷防眩光', price: '5699.00', originalPrice: '6299.00', thumbnail: '/static/goods/goods_3.png' },
|
||||
{ id: '104', goodsName: '运动健康专业监测智能手表 心率血氧长续航50米防水', price: '899.00', originalPrice: '1199.00', thumbnail: '/static/goods/goods_4.png' },
|
||||
{ id: '105', goodsName: '奢宠酵母焕活修护精华露 50ml 紧致淡纹抗皱滋养', price: '680.00', originalPrice: '890.00', thumbnail: '/static/goods/goods_5.png' },
|
||||
{ id: '106', goodsName: '意式半自动家用复古咖啡机 20Bar高压恒温萃取蒸汽奶泡', price: '899.00', originalPrice: '1280.00', thumbnail: '/static/goods/goods_6.png' },
|
||||
{ id: '107', goodsName: '碳板专业竞速轻量缓震跑步鞋 透气防滑男女同款潮鞋', price: '459.00', originalPrice: '699.00', thumbnail: '/static/goods/goods_7.png' },
|
||||
{ id: '108', goodsName: '极简静音无雾负离子香薰加湿器 大容量客厅卧室通用', price: '299.00', originalPrice: '399.00', thumbnail: '/static/goods/goods_8.png' }
|
||||
];
|
||||
|
||||
const displayGoods = computed(() => {
|
||||
if (Array.isArray(props.options?.goods) && props.options.goods.length > 0) {
|
||||
return props.options.goods;
|
||||
}
|
||||
const count = (rightColumns.value * 2) || 8;
|
||||
return defaultFloorGoods.slice(0, count);
|
||||
});
|
||||
|
||||
const customStyle = computed(() => {
|
||||
const s = props.componentStyle || {};
|
||||
return {
|
||||
backgroundColor: s.backgroundColor || '#ffffff',
|
||||
padding: Array.isArray(s.padding)
|
||||
? `${s.padding[0]}px ${s.padding[1]}px ${s.padding[2]}px ${s.padding[3]}px`
|
||||
: '16px 20px',
|
||||
borderRadius: Array.isArray(s.radius)
|
||||
? `${s.radius[0]}px ${s.radius[1]}px ${s.radius[2]}px ${s.radius[3]}px`
|
||||
: '12px'
|
||||
};
|
||||
});
|
||||
</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: 13px;
|
||||
color: #1e293b;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
height: 36px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
margin-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
manager/src/views/page-decoration/pc/template/TplHotZone.vue
Normal file
135
manager/src/views/page-decoration/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 || 0}%`,
|
||||
left: `${zone.left || 0}%`,
|
||||
width: `${zone.width || 10}%`,
|
||||
height: `${zone.height || 10}%`
|
||||
}"
|
||||
>
|
||||
<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 || '/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>
|
||||
307
manager/src/views/page-decoration/pc/template/TplNotice.vue
Normal file
307
manager/src/views/page-decoration/pc/template/TplNotice.vue
Normal file
@@ -0,0 +1,307 @@
|
||||
<template>
|
||||
<div
|
||||
class="tpl-notice-bar"
|
||||
:class="[
|
||||
isMini ? 'is-mini' : (isCompact ? 'is-compact' : '')
|
||||
]"
|
||||
:style="customStyle"
|
||||
@mouseenter="stopTimer"
|
||||
@mouseleave="startTimer"
|
||||
>
|
||||
<!-- 左侧快讯徽章 -->
|
||||
<div class="notice-badge-box">
|
||||
<div class="badge-icon">
|
||||
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
|
||||
<path d="M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-7 12h-2v-2h2v2zm0-4h-2V6h2v4z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="badge-title">{{ options.title || '公告快讯' }}</span>
|
||||
<span class="badge-divider"></span>
|
||||
</div>
|
||||
|
||||
<!-- 中间动态滚动快讯区 (Vertical Carousel) -->
|
||||
<div class="notice-content-viewport">
|
||||
<div
|
||||
class="notice-items-strip"
|
||||
:style="{ transform: `translateY(-${currentIndex * 40}px)` }"
|
||||
>
|
||||
<div
|
||||
v-for="(item, idx) in noticeList"
|
||||
:key="idx"
|
||||
class="notice-strip-item"
|
||||
>
|
||||
<span class="notice-item-tag" :class="getTagClass(item.tag)">
|
||||
{{ item.tag || '最新' }}
|
||||
</span>
|
||||
<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;
|
||||
border: 1px solid #eef2f6;
|
||||
gap: 12px;
|
||||
|
||||
/* 左侧徽章 */
|
||||
.notice-badge-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.badge-icon {
|
||||
color: #ef4444;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.badge-title {
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
color: #0f172a;
|
||||
white-space: nowrap;
|
||||
letter-spacing: -0.2px;
|
||||
}
|
||||
|
||||
.badge-divider {
|
||||
width: 1px;
|
||||
height: 14px;
|
||||
background: #e2e8f0;
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 中间快讯轮播视口 */
|
||||
.notice-content-viewport {
|
||||
flex: 1;
|
||||
height: 40px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
|
||||
.notice-items-strip {
|
||||
transition: transform 0.4s ease-in-out;
|
||||
|
||||
.notice-strip-item {
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
|
||||
.notice-item-tag {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.tag-new {
|
||||
background: #fee2e2;
|
||||
color: #dc2626;
|
||||
}
|
||||
&.tag-promo {
|
||||
background: #ffedd5;
|
||||
color: #ea580c;
|
||||
}
|
||||
&.tag-notice {
|
||||
background: #eff6ff;
|
||||
color: #2563eb;
|
||||
}
|
||||
&.tag-hot {
|
||||
background: #fdf2f8;
|
||||
color: #db2777;
|
||||
}
|
||||
}
|
||||
|
||||
.notice-item-title {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
color: #334155;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
min-width: 0;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.notice-item-time {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 右侧更多 */
|
||||
.notice-more-action {
|
||||
flex-shrink: 0;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #ef4444;
|
||||
}
|
||||
}
|
||||
|
||||
/* 紧凑形态 (三分之一) */
|
||||
&.is-compact {
|
||||
gap: 8px;
|
||||
|
||||
.notice-badge-box {
|
||||
.badge-title {
|
||||
font-size: 13px;
|
||||
}
|
||||
.badge-divider {
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.notice-content-viewport {
|
||||
.notice-strip-item {
|
||||
.notice-item-title {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 极简微缩形态 (六分之一) */
|
||||
&.is-mini {
|
||||
gap: 4px;
|
||||
padding: 0 8px !important;
|
||||
|
||||
.notice-badge-box {
|
||||
.badge-title {
|
||||
display: none;
|
||||
}
|
||||
.badge-divider {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.notice-content-viewport {
|
||||
.notice-strip-item {
|
||||
.notice-item-tag {
|
||||
display: none;
|
||||
}
|
||||
.notice-item-title {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
129
manager/src/views/page-decoration/pc/template/TplPromoGroup.vue
Normal file
129
manager/src/views/page-decoration/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: '/static/promo-group/promo_1.png', url: '/goodsList?seckill=1' },
|
||||
{ title: '品牌特惠', subtitle: '大牌买一送一 · 领百元神券', bgImg: '/static/promo-group/promo_2.png', url: '/goodsList?brand=1' },
|
||||
{ title: '新品首发', subtitle: '首单专享特惠 · 1元起尝鲜', bgImg: '/static/promo-group/promo_3.png', url: '/goodsList?new=1' }
|
||||
];
|
||||
|
||||
const cardsList = computed(() => {
|
||||
if (Array.isArray(props.options?.cards) && props.options.cards.length > 0) {
|
||||
return props.options.cards;
|
||||
}
|
||||
return defaultCards;
|
||||
});
|
||||
|
||||
const actualColumns = computed(() => {
|
||||
const baseCols = Number(props.options?.columns) || 3;
|
||||
const ratio = props.componentStyle?.widthRatio || '100%';
|
||||
if (ratio === '16.67%' || ratio === '16.6%' || ratio === '16.7%') {
|
||||
return 1;
|
||||
}
|
||||
if (ratio === '33.33%' || ratio === '33.3%') {
|
||||
return 1;
|
||||
}
|
||||
if (ratio === '50%') {
|
||||
return Math.min(baseCols, 2);
|
||||
}
|
||||
if (ratio === '66.67%' || ratio === '66.6%' || ratio === '66.7%') {
|
||||
return Math.min(baseCols, 2);
|
||||
}
|
||||
return baseCols;
|
||||
});
|
||||
|
||||
const customStyle = computed(() => {
|
||||
const s = props.componentStyle || {};
|
||||
return {
|
||||
backgroundColor: s.backgroundColor || 'transparent',
|
||||
padding: Array.isArray(s.padding)
|
||||
? `${s.padding[0]}px ${s.padding[1]}px ${s.padding[2]}px ${s.padding[3]}px`
|
||||
: '10px 0'
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.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>
|
||||
@@ -0,0 +1,544 @@
|
||||
<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">99.8% 高好评 · 猜你对这款高人气科技数码感兴趣</div>
|
||||
<div class="big-price-row">
|
||||
<div class="big-price">
|
||||
<span class="sym">¥</span>
|
||||
<span class="val">{{ featuredGoods.price }}</span>
|
||||
<span class="del" v-if="featuredGoods.originalPrice">¥{{ featuredGoods.originalPrice }}</span>
|
||||
</div>
|
||||
<button type="button" class="big-buy-btn">立即入手 ></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="sales-num">已售 {{ 200 + idx * 70 }}+</span>
|
||||
<span class="rate-num">98%好评</span>
|
||||
</div>
|
||||
<div class="price-row">
|
||||
<div class="price-box">
|
||||
<span class="sym">¥</span>
|
||||
<span class="val">{{ item.price }}</span>
|
||||
</div>
|
||||
<button type="button" class="cart-pill-btn">+</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 = '/static/goods/goods_1.png';
|
||||
|
||||
const defaultPool = [
|
||||
{ id: '1', goodsName: '旗舰 5G 智能手机 12GB+256GB 超光影影像系统', price: '3999.00', originalPrice: '4599.00', thumbnail: '/static/goods/goods_1.png' },
|
||||
{ id: '2', goodsName: '无线头戴式深度主动降噪蓝牙耳机 空间音频Hi-Res', price: '1099.00', originalPrice: '1499.00', thumbnail: '/static/goods/goods_2.png' },
|
||||
{ id: '3', goodsName: '14英寸 全面屏轻薄商务笔记本电脑 酷睿i7 高刷防眩光', price: '5699.00', originalPrice: '6299.00', thumbnail: '/static/goods/goods_3.png' },
|
||||
{ id: '4', goodsName: '运动健康专业监测智能手表 心率血氧长续航50米防水', price: '899.00', originalPrice: '1199.00', thumbnail: '/static/goods/goods_4.png' },
|
||||
{ id: '5', goodsName: '奢宠酵母焕活修护精华露 50ml 紧致淡纹抗皱滋养', price: '680.00', originalPrice: '890.00', thumbnail: '/static/goods/goods_5.png' },
|
||||
{ id: '6', goodsName: '意式半自动家用复古咖啡机 20Bar高压恒温萃取蒸汽奶泡', price: '899.00', originalPrice: '1280.00', thumbnail: '/static/goods/goods_6.png' },
|
||||
{ id: '7', goodsName: '碳板专业竞速轻量缓震跑步鞋 透气防滑男女同款潮鞋', price: '459.00', originalPrice: '699.00', thumbnail: '/static/goods/goods_7.png' },
|
||||
{ id: '8', goodsName: '极简静音无雾负离子香薰加湿器 大容量客厅卧室通用', price: '299.00', originalPrice: '399.00', thumbnail: '/static/goods/goods_8.png' },
|
||||
{ id: '9', goodsName: '智能降噪AI翻译蓝牙墨镜 超轻防紫外线偏光太阳镜', price: '599.00', originalPrice: '799.00', thumbnail: '/static/goods/goods_1.png' }
|
||||
];
|
||||
|
||||
const displayGoods = computed(() => {
|
||||
if (Array.isArray(props.options?.goods) && props.options.goods.length > 0) {
|
||||
return props.options.goods;
|
||||
}
|
||||
const count = props.options?.pageSize || 8;
|
||||
return defaultPool.slice(0, count);
|
||||
});
|
||||
|
||||
const featuredGoods = computed(() => {
|
||||
return displayGoods.value[0] || defaultPool[0];
|
||||
});
|
||||
|
||||
const regularGoods = computed(() => {
|
||||
return displayGoods.value.slice(1, 7);
|
||||
});
|
||||
|
||||
const actualCols = computed(() => {
|
||||
const ratio = props.componentStyle?.widthRatio || '100%';
|
||||
if (ratio === '16.67%' || ratio === '16.6%') return 1;
|
||||
if (ratio === '33.33%' || ratio === '33.3%') return 2;
|
||||
if (ratio === '50%') return 3;
|
||||
if (ratio === '66.67%' || ratio === '66.6%') return 4;
|
||||
return 5;
|
||||
});
|
||||
|
||||
const customStyle = computed(() => {
|
||||
const s = props.componentStyle || {};
|
||||
const bg = props.options?.backgroundColor || s.backgroundColor || '#f8fafc';
|
||||
return {
|
||||
backgroundColor: bg,
|
||||
padding: Array.isArray(s.padding)
|
||||
? `${s.padding[0]}px ${s.padding[1]}px ${s.padding[2]}px ${s.padding[3]}px`
|
||||
: '24px 20px',
|
||||
borderRadius: Array.isArray(s.radius)
|
||||
? `${s.radius[0]}px ${s.radius[1]}px ${s.radius[2]}px ${s.radius[3]}px`
|
||||
: '12px'
|
||||
};
|
||||
});
|
||||
</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: 12px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.big-price-row {
|
||||
margin-top: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.big-price {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
||||
.sym {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.val {
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.del {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
text-decoration: line-through;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.big-buy-btn {
|
||||
background: linear-gradient(135deg, #ef4444, #dc2626);
|
||||
color: #ffffff;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
padding: 6px 14px;
|
||||
border-radius: 16px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
transform: scale(1.03);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.featured-sub-grid {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 标准网格流 */
|
||||
.recommend-standard-grid {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.recommend-card {
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid #f1f5f9;
|
||||
transition: all 0.25s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.06);
|
||||
|
||||
.card-thumb img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.cart-pill-btn {
|
||||
background: #ef4444;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
.card-thumb {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 1 / 1;
|
||||
background: #f8fafc;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 8px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.rec-tag {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
left: 6px;
|
||||
background: #ef4444;
|
||||
color: #ffffff;
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
padding: 1px 5px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
|
||||
.goods-title {
|
||||
font-size: 12px;
|
||||
color: #1e293b;
|
||||
line-height: 1.4;
|
||||
height: 34px;
|
||||
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>
|
||||
@@ -0,0 +1,158 @@
|
||||
<template>
|
||||
<div class="tpl-search-header" :style="customStyle">
|
||||
<div class="header-inner">
|
||||
<div class="logo-area">
|
||||
<img v-if="options.logoUrl" :src="options.logoUrl" alt="logo" class="logo-img" />
|
||||
<span v-else class="text-logo">LiliShop</span>
|
||||
</div>
|
||||
|
||||
<div class="search-main">
|
||||
<div class="search-box">
|
||||
<input
|
||||
type="text"
|
||||
class="mock-input"
|
||||
:placeholder="options.placeholder || '搜索品牌或商品'"
|
||||
readonly
|
||||
/>
|
||||
<button type="button" class="search-btn">
|
||||
<el-icon><Search /></el-icon>
|
||||
<span>搜索</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="hot-words" v-if="options.showHotWords !== false && options.hotWords?.length">
|
||||
<span v-for="(w, idx) in options.hotWords" :key="idx" class="hot-w">{{ w }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="cart-btn"
|
||||
v-if="options.showCart !== false"
|
||||
:style="cartBtnStyle"
|
||||
>
|
||||
<el-icon class="cart-icon" :style="{ color: options.cartBadgeColor || '#e11d48' }">
|
||||
<ShoppingCart />
|
||||
</el-icon>
|
||||
<span>{{ options.cartText || '我的购物车' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { Search, ShoppingCart } 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',
|
||||
minHeight: `${s.height || 90}px`
|
||||
};
|
||||
});
|
||||
|
||||
const cartBtnStyle = computed(() => {
|
||||
const o = props.options || {};
|
||||
return {
|
||||
backgroundColor: o.cartBgColor || '#ffffff',
|
||||
borderColor: o.cartBorderColor || '#e2e8f0',
|
||||
color: o.cartTextColor || '#334155'
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tpl-search-header {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
|
||||
.header-inner {
|
||||
width: 100%;
|
||||
padding: 16px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 30px;
|
||||
|
||||
.logo-area {
|
||||
.logo-img {
|
||||
max-height: 48px;
|
||||
object-fit: contain;
|
||||
}
|
||||
.text-logo {
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
color: #e11d48;
|
||||
}
|
||||
}
|
||||
|
||||
.search-main {
|
||||
flex: 1;
|
||||
max-width: 550px;
|
||||
|
||||
.search-box {
|
||||
display: flex;
|
||||
border: 2px solid #e11d48;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
|
||||
.mock-input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
padding: 8px 12px;
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
background: #e11d48;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
padding: 0 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.hot-words {
|
||||
margin-top: 6px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
|
||||
.cart-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 16px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
color: #334155;
|
||||
background: #f8fafc;
|
||||
|
||||
.cart-icon {
|
||||
color: #e11d48;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
136
manager/src/views/page-decoration/pc/template/TplTitle.vue
Normal file
136
manager/src/views/page-decoration/pc/template/TplTitle.vue
Normal file
@@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<div
|
||||
class="tpl-title-bar"
|
||||
:class="[
|
||||
`align-${options.align || 'left'}`,
|
||||
isMini ? 'is-mini' : (isCompact ? 'is-compact' : '')
|
||||
]"
|
||||
>
|
||||
<div class="title-content">
|
||||
<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: baseline;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px 10px;
|
||||
min-width: 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 {
|
||||
.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: 4px;
|
||||
|
||||
&.align-center {
|
||||
align-items: center;
|
||||
.more-link {
|
||||
position: static;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.title-content {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 2px;
|
||||
|
||||
.main-t {
|
||||
font-size: 16px;
|
||||
}
|
||||
.sub-t {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
&.align-center .title-content {
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.more-link {
|
||||
font-size: 12px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
129
manager/src/views/page-decoration/pc/template/TplTopBar.vue
Normal file
129
manager/src/views/page-decoration/pc/template/TplTopBar.vue
Normal file
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<div class="tpl-top-bar" :style="customStyle">
|
||||
<div class="bar-inner">
|
||||
<div class="bar-left">
|
||||
<span class="welcome-txt">{{ options.welcomeText || '欢迎来到商城' }}</span>
|
||||
<span class="auth-links">
|
||||
<span class="auth-btn">[{{ options.loginText || '登录' }}]</span>
|
||||
<span class="auth-btn">[{{ options.registerText || '免费注册' }}]</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="bar-right">
|
||||
<span
|
||||
v-for="(menu, idx) in (options.rightMenus || defaultMenus)"
|
||||
:key="idx"
|
||||
class="bar-link"
|
||||
>
|
||||
{{ menu.title }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
componentStyle: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const defaultMenus = [
|
||||
{ title: '基本信息', url: '/home/UserInfo' },
|
||||
{ title: '我的订单', url: '/home/MyOrder' },
|
||||
{ title: '账户安全', url: '/home/AccountSafe' },
|
||||
{ title: '消息', url: '/home/Msg' }
|
||||
];
|
||||
|
||||
const customStyle = computed(() => {
|
||||
const s = props.componentStyle || {};
|
||||
const opt = props.options || {};
|
||||
const textColor = opt.textColor || s.textColor || '#4b5563';
|
||||
const fontSize = opt.fontSize ? `${opt.fontSize}px` : '12px';
|
||||
|
||||
return {
|
||||
backgroundColor: s.backgroundColor || 'transparent',
|
||||
color: textColor,
|
||||
fontSize: fontSize,
|
||||
height: `${s.height || 36}px`,
|
||||
lineHeight: `${s.height || 36}px`
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tpl-top-bar {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
font-size: 12px;
|
||||
user-select: none;
|
||||
overflow: hidden;
|
||||
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.bar-inner {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
padding: 0 24px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.bar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
|
||||
.welcome-txt {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.auth-links {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
white-space: nowrap;
|
||||
|
||||
.auth-btn {
|
||||
color: #ff6a00;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
|
||||
.bar-link {
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
color: #ff6a00;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,399 @@
|
||||
<template>
|
||||
<div class="tpl-user-benefits" :style="customStyle">
|
||||
<!-- 1. 用户头像与信息区 -->
|
||||
<div class="user-main-info">
|
||||
<div class="avatar-box">
|
||||
<img v-if="options.avatar" :src="options.avatar" alt="avatar" />
|
||||
<div v-else class="avatar-placeholder">
|
||||
<svg viewBox="0 0 24 24" width="22" height="22" fill="#ffffff">
|
||||
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="user-greeting">
|
||||
<div class="badge-tag">{{ options.badgeText || '请登录' }}</div>
|
||||
<div class="welcome-text" :title="options.welcomeText || '欢迎来到商城'">
|
||||
{{ options.welcomeText || '欢迎来到商城' }}
|
||||
</div>
|
||||
<div class="sub-text" :title="options.subtitle || '登录后查看专属权益'">
|
||||
{{ options.subtitle || '登录后查看专属权益' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. 登录 / 注册按钮组 -->
|
||||
<div class="action-btn-row">
|
||||
<div class="action-btn login-btn">{{ options.loginText || '登录' }}</div>
|
||||
<div class="action-btn register-btn">{{ options.registerText || '注册' }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 3. 指标数据卡 (购物车 / 优惠券 / 订单) -->
|
||||
<div class="metrics-cards-row">
|
||||
<div class="metric-card">
|
||||
<div class="metric-val">0</div>
|
||||
<div class="metric-label">{{ options.cartText || '购物车' }}</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-val">0</div>
|
||||
<div class="metric-label">{{ options.couponText || '优惠券' }}</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-val">0</div>
|
||||
<div class="metric-label">{{ options.orderText || '订单' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 4. 分隔线 -->
|
||||
<div class="section-divider"></div>
|
||||
|
||||
<!-- 5. 热点快报(万米魔方标准组件底部模块) -->
|
||||
<div class="news-section">
|
||||
<div class="news-header">
|
||||
<div class="news-title-wrap">
|
||||
<span class="news-title">热点快报</span>
|
||||
<span class="hot-badge">HOT</span>
|
||||
</div>
|
||||
<span class="news-more">查看全部 ></span>
|
||||
</div>
|
||||
<div class="news-list">
|
||||
<div class="news-item" title="精选优质好物 官方正品保障">
|
||||
<span class="dot">·</span>
|
||||
<span class="news-text">精选优质好物 官方正品保障</span>
|
||||
</div>
|
||||
<div class="news-item" title="双11狂欢 跨店每满300减50">
|
||||
<span class="dot">·</span>
|
||||
<span class="news-text">双11狂欢 跨店每满300减50</span>
|
||||
</div>
|
||||
<div class="news-item" title="新人专享好礼 注册立领大礼包">
|
||||
<span class="dot">·</span>
|
||||
<span class="news-text">新人专享好礼 注册立领大礼包</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 6. 快捷便民服务入口(底部图标排) -->
|
||||
<div class="quick-service-row">
|
||||
<div class="service-item">
|
||||
<el-icon class="service-icon"><Star /></el-icon>
|
||||
<span class="service-name">收藏</span>
|
||||
</div>
|
||||
<div class="service-item">
|
||||
<el-icon class="service-icon"><Clock /></el-icon>
|
||||
<span class="service-name">足迹</span>
|
||||
</div>
|
||||
<div class="service-item">
|
||||
<el-icon class="service-icon"><Present /></el-icon>
|
||||
<span class="service-name">红包</span>
|
||||
</div>
|
||||
<div class="service-item">
|
||||
<el-icon class="service-icon"><Service /></el-icon>
|
||||
<span class="service-name">客服</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { Star, Clock, Present, Service } from '@element-plus/icons-vue';
|
||||
|
||||
const props = defineProps({
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
componentStyle: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const customStyle = computed(() => {
|
||||
const s = props.componentStyle || {};
|
||||
return {
|
||||
backgroundColor: s.backgroundColor || '#ffffff',
|
||||
borderRadius: Array.isArray(s.radius)
|
||||
? `${s.radius[0]}px ${s.radius[1]}px ${s.radius[2]}px ${s.radius[3]}px`
|
||||
: '8px'
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tpl-user-benefits {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 420px;
|
||||
box-sizing: border-box;
|
||||
background: #ffffff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
|
||||
padding: 14px 12px 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
user-select: none;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
|
||||
.user-main-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.avatar-box {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.avatar-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #cbd5e1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.user-greeting {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
.badge-tag {
|
||||
align-self: flex-start;
|
||||
padding: 1px 6px;
|
||||
background: #fff2e8;
|
||||
color: #ff6000;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
border-radius: 8px;
|
||||
line-height: 1.2;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.welcome-text {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
line-height: 1.25;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.sub-text {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
line-height: 1.25;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-btn-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
border-radius: 14px;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&.login-btn {
|
||||
background: #ff6000;
|
||||
color: #ffffff;
|
||||
box-shadow: 0 2px 6px rgba(255, 96, 0, 0.25);
|
||||
|
||||
&:hover {
|
||||
background: #e55600;
|
||||
}
|
||||
}
|
||||
|
||||
&.register-btn {
|
||||
background: #ffffff;
|
||||
color: #ff6000;
|
||||
border: 1px solid #ff6000;
|
||||
|
||||
&:hover {
|
||||
background: #fff7ed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.metrics-cards-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
padding: 8px 0;
|
||||
margin-top: 10px;
|
||||
background: #f8fafc;
|
||||
border-radius: 6px;
|
||||
|
||||
.metric-card {
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
cursor: pointer;
|
||||
|
||||
.metric-val {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: 11px;
|
||||
color: #64748b;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.metric-val, .metric-label {
|
||||
color: #ff6000;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.section-divider {
|
||||
height: 1px;
|
||||
background: #f1f5f9;
|
||||
margin: 8px 0 6px;
|
||||
}
|
||||
|
||||
.news-section {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 0 2px;
|
||||
|
||||
.news-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 6px;
|
||||
|
||||
.news-title-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
.news-title {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.hot-badge {
|
||||
background: #ef4444;
|
||||
color: #ffffff;
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
padding: 0 3px;
|
||||
border-radius: 2px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
}
|
||||
|
||||
.news-more {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: #ff6000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.news-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
|
||||
.news-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 11px;
|
||||
color: #475569;
|
||||
cursor: pointer;
|
||||
line-height: 1.3;
|
||||
transition: color 0.15s;
|
||||
|
||||
.dot {
|
||||
color: #ff6000;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.news-text {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #ff6000;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.quick-service-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
padding-top: 8px;
|
||||
border-top: 1px dashed #f1f5f9;
|
||||
margin-top: 4px;
|
||||
|
||||
.service-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
cursor: pointer;
|
||||
color: #64748b;
|
||||
transition: color 0.15s;
|
||||
|
||||
.service-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.service-name {
|
||||
font-size: 10px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #ff6000;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
77
manager/src/views/page-decoration/pc/template/TplVideo.vue
Normal file
77
manager/src/views/page-decoration/pc/template/TplVideo.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<div class="tpl-video" :style="customStyle">
|
||||
<div class="video-preview-box">
|
||||
<img :src="options.coverUrl || '/static/video/video_cover.png'" alt="cover" class="cover-img" />
|
||||
<div class="play-btn">▶</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 || 360}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;
|
||||
|
||||
.cover-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.play-btn {
|
||||
position: absolute;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
background: rgba(225, 29, 72, 0.9);
|
||||
color: #ffffff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.08);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
65
manager/src/views/page-decoration/pc/template/index.js
Normal file
65
manager/src/views/page-decoration/pc/template/index.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import TplUserBenefits from './TplUserBenefits.vue';
|
||||
import TplTopBar from './TplTopBar.vue';
|
||||
import TplSearchHeader from './TplSearchHeader.vue';
|
||||
import TplFooterNav from './TplFooterNav.vue';
|
||||
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 TplCoupon from './TplCoupon.vue';
|
||||
import TplTitle from './TplTitle.vue';
|
||||
import TplArticle from './TplArticle.vue';
|
||||
import TplNotice from './TplNotice.vue';
|
||||
|
||||
export const CANVAS_TEMPLATE_MAP = {
|
||||
// 结构 (3)
|
||||
topBar: TplTopBar,
|
||||
searchHeader: TplSearchHeader,
|
||||
footerNav: TplFooterNav,
|
||||
|
||||
// 导航 (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)
|
||||
couponList: TplCoupon,
|
||||
userBenefits: TplUserBenefits,
|
||||
|
||||
// 内容 (3)
|
||||
titleBar: TplTitle,
|
||||
articleContent: TplArticle,
|
||||
noticeBar: TplNotice
|
||||
};
|
||||
|
||||
export function resolveCanvasTemplate(type) {
|
||||
return CANVAS_TEMPLATE_MAP[type] || null;
|
||||
}
|
||||
@@ -262,62 +262,83 @@
|
||||
<!-- 遮罩层 -->
|
||||
<div v-if="isRead" class="mask">只读不可修改</div>
|
||||
<el-form-item label="经营类目" prop="goodsManagementCategory">
|
||||
<div>
|
||||
<el-checkbox
|
||||
v-model="checkAll"
|
||||
:indeterminate="indeterminate"
|
||||
@change="handleCheckAll"
|
||||
>全选
|
||||
</el-checkbox>
|
||||
<div class="category-panel">
|
||||
<div class="category-toolbar">
|
||||
<el-checkbox
|
||||
v-model="checkAll"
|
||||
:indeterminate="indeterminate"
|
||||
@change="handleCheckAll"
|
||||
>全选
|
||||
</el-checkbox>
|
||||
<span class="category-count"
|
||||
>已选 {{ checkAllGroup.length }} / {{ categories.length }} 项</span
|
||||
>
|
||||
</div>
|
||||
<el-checkbox-group
|
||||
v-model="checkAllGroup"
|
||||
class="category-grid"
|
||||
@change="checkAllGroupChange"
|
||||
>
|
||||
<el-checkbox
|
||||
v-for="(item, i) in categories"
|
||||
:key="i + 1"
|
||||
:value="String(item.id)"
|
||||
>{{ item.name }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</div>
|
||||
<el-checkbox-group v-model="checkAllGroup" @change="checkAllGroupChange">
|
||||
<el-checkbox
|
||||
v-for="(item, i) in categories"
|
||||
:key="i + 1"
|
||||
:value="String(item.id)"
|
||||
>{{ item.name }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="结算信息" class="tab" name="settlement">
|
||||
<el-alert type="info" :closable="false" show-icon>
|
||||
<el-alert type="info" :closable="false" show-icon class="settlement-alert">
|
||||
已添加<span class="theme_color">{{ settlementCycle.length }}</span
|
||||
>个结算日,最多可添加5个结算日,当月不包含所设日期时,将会顺延到下一个结算日
|
||||
</el-alert>
|
||||
<el-form-item label="结算日期">
|
||||
<el-tag
|
||||
v-for="item in settlementCycle"
|
||||
:key="item"
|
||||
closable
|
||||
style="margin-left: 10px"
|
||||
@close="removesettlementCycle(item)"
|
||||
>{{ item }}
|
||||
</el-tag>
|
||||
<el-input-number
|
||||
size="small"
|
||||
:max="31"
|
||||
:min="1"
|
||||
v-model="day"
|
||||
v-show="settlementShow"
|
||||
/>
|
||||
<el-button
|
||||
type="default"
|
||||
@click="addsettlementCycle"
|
||||
size="small"
|
||||
v-if="addSettlementBtn && settlementCycle.length < 5"
|
||||
style="margin-left: 8px"
|
||||
>添加结算日期
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="addSettlementConfirmBtn"
|
||||
type="default"
|
||||
@click="addsettlementCycleConfirm"
|
||||
size="small"
|
||||
style="margin-left: 8px"
|
||||
>确认
|
||||
</el-button>
|
||||
<div class="settlement-panel">
|
||||
<div class="settlement-tags">
|
||||
<el-tag
|
||||
v-for="item in settlementCycle"
|
||||
:key="item"
|
||||
closable
|
||||
class="settlement-tag"
|
||||
@close="removesettlementCycle(item)"
|
||||
>每月 {{ item }} 日
|
||||
</el-tag>
|
||||
<span v-if="!settlementCycle.length" class="settlement-empty"
|
||||
>暂未添加结算日</span
|
||||
>
|
||||
</div>
|
||||
<div class="settlement-actions">
|
||||
<template v-if="settlementShow">
|
||||
<el-input-number
|
||||
size="small"
|
||||
:max="31"
|
||||
:min="1"
|
||||
v-model="day"
|
||||
/>
|
||||
<el-button
|
||||
v-if="addSettlementConfirmBtn"
|
||||
type="primary"
|
||||
@click="addsettlementCycleConfirm"
|
||||
size="small"
|
||||
>确认
|
||||
</el-button>
|
||||
<el-button type="default" @click="cancelSettlementCycle" size="small"
|
||||
>取消
|
||||
</el-button>
|
||||
</template>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
@click="addsettlementCycle"
|
||||
size="small"
|
||||
v-if="addSettlementBtn && settlementCycle.length < 5"
|
||||
>+ 添加结算日期
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
@@ -605,6 +626,13 @@ function addsettlementCycleConfirm() {
|
||||
settlementShow.value = false;
|
||||
day.value = 1;
|
||||
}
|
||||
//取消添加结算日
|
||||
function cancelSettlementCycle() {
|
||||
addSettlementConfirmBtn.value = false;
|
||||
addSettlementBtn.value = true;
|
||||
settlementShow.value = false;
|
||||
day.value = 1;
|
||||
}
|
||||
// 选择公司地址
|
||||
function selectedRegion(val) {
|
||||
shopForm.value.companyAddressIdPath = val[0].toString();
|
||||
@@ -846,9 +874,101 @@ init();
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.categories-checkbox {
|
||||
.category-panel {
|
||||
width: 100%;
|
||||
max-width: 960px;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.category-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 16px;
|
||||
background: #f8f9fb;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
|
||||
:deep(.el-checkbox) {
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.category-count {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.settlement-alert {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.settlement-panel {
|
||||
width: 100%;
|
||||
max-width: 960px;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.settlement-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 16px;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.settlement-tag {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.settlement-empty {
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.settlement-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 16px;
|
||||
border-top: 1px solid #ebeef5;
|
||||
background: #f8f9fb;
|
||||
}
|
||||
|
||||
.category-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
padding: 16px;
|
||||
|
||||
:deep(.el-checkbox) {
|
||||
height: auto;
|
||||
margin-right: 0;
|
||||
padding: 5px 14px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
transition: border-color 0.2s, background-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--el-color-primary, #409eff);
|
||||
}
|
||||
|
||||
&.is-checked {
|
||||
border-color: var(--el-color-primary, #409eff);
|
||||
background-color: var(--el-color-primary-light-9, #ecf5ff);
|
||||
}
|
||||
|
||||
.el-checkbox__label {
|
||||
padding-left: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.img {
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="goodsName" label="销售商品" min-width="160" />
|
||||
<el-table-column prop="goodsId" label="商品ID" width="140" />
|
||||
<el-table-column prop="goodsId" label="SPU编码" width="140" />
|
||||
<el-table-column prop="salesNum" label="商品销售数量" width="110" />
|
||||
<el-table-column prop="salesAmount" label="商品销售金额" width="120">
|
||||
<template #default="{ row }">{{ formatMoney(row.salesAmount) }}</template>
|
||||
|
||||
@@ -30,35 +30,48 @@
|
||||
}
|
||||
.img-box {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
align-content: flex-start;
|
||||
margin-top: 20px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
margin-top: 16px;
|
||||
box-sizing: border-box;
|
||||
|
||||
.img-item {
|
||||
width: 120px;
|
||||
height: 158px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
margin: 0 13px 8px;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.card {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
width: 100%;
|
||||
aspect-ratio: 1 / 1;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
border: 2px solid transparent;
|
||||
border: 2px solid #eef2f6;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
background: #f8fafc;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
img,
|
||||
.card-media {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.card-file {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -68,6 +81,7 @@
|
||||
background: #f5f7fa;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.card-checkbox {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
@@ -80,48 +94,48 @@
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.preview {
|
||||
width: 100%;
|
||||
height: 26px;
|
||||
background-color: #ffffff;
|
||||
color: #666666;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.12);
|
||||
> div {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.card:hover,
|
||||
|
||||
.card:hover {
|
||||
border-color: #ff6a00;
|
||||
}
|
||||
|
||||
.custom-checkbox-card-checked {
|
||||
border: 2px solid #ff5c58;
|
||||
border: 2px solid #ff6a00 !important;
|
||||
background: #fffaf5 !important;
|
||||
}
|
||||
|
||||
.oss-card-selectable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.text {
|
||||
width: 120px;
|
||||
height: 36px;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
color: #252931;
|
||||
font-size: 14px;
|
||||
line-height: 36px;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.sub-info {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: 18px;
|
||||
color: #94a3b8;
|
||||
font-size: 11px;
|
||||
line-height: 18px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -140,3 +154,100 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.pc-image-picker-dialog {
|
||||
margin-top: 16px !important;
|
||||
margin-bottom: 16px !important;
|
||||
max-height: calc(100vh - 32px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.el-dialog__header {
|
||||
padding: 12px 20px 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
padding: 6px 16px 12px;
|
||||
overflow: hidden !important;
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
flex: 1;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none !important; /* Chrome, Safari, Edge */
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-component-search {
|
||||
.is-component-card {
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
background: transparent !important;
|
||||
width: 100%;
|
||||
|
||||
.el-card__body {
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.oss-manage-box {
|
||||
.file-list {
|
||||
padding: 0 10px 10px;
|
||||
}
|
||||
.pic-list {
|
||||
padding: 0;
|
||||
}
|
||||
.oss-operation {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.img-box {
|
||||
margin-top: 6px;
|
||||
gap: 10px 12px;
|
||||
|
||||
.img-item {
|
||||
.card {
|
||||
max-width: 128px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.text {
|
||||
height: 18px;
|
||||
line-height: 18px;
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.sub-info {
|
||||
height: 16px;
|
||||
line-height: 16px;
|
||||
font-size: 11px;
|
||||
margin-top: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.page-box {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.oss-select-footer {
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.oss-group-tree {
|
||||
padding: 0;
|
||||
|
||||
.el-tree-node {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.el-tree-node__content {
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
|
||||
</style>
|
||||
<template>
|
||||
<div class="search">
|
||||
<div class="search" :class="{ 'is-component-search': isComponent }">
|
||||
<el-card v-if="!isComponent" class="tabs-card">
|
||||
<el-tabs v-model="activeRoleTab" @tab-change="handleRoleTabChange">
|
||||
<el-tab-pane label="管理员" name="MANAGER" />
|
||||
@@ -133,15 +133,15 @@
|
||||
<el-tab-pane label="客服" name="CUSTOMER" />
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
<el-card class="tabs-card category-tabs-card">
|
||||
<el-card v-if="!isComponent" class="tabs-card category-tabs-card">
|
||||
<el-tabs v-model="activeCategoryTab" @tab-change="handleCategoryTabChange">
|
||||
<el-tab-pane label="图片" name="IMAGE" />
|
||||
<el-tab-pane label="视频" name="VIDEO" />
|
||||
<el-tab-pane v-if="!isComponent" label="文件" name="FILE" />
|
||||
<el-tab-pane v-if="allowImage" label="图片" name="IMAGE" />
|
||||
<el-tab-pane v-if="allowVideo" label="视频" name="VIDEO" />
|
||||
<el-tab-pane v-if="!isComponent && !onlyImage" label="文件" name="FILE" />
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
<el-row>
|
||||
<el-card>
|
||||
<component :is="isComponent ? 'div' : 'el-card'" :class="{ 'is-component-card': isComponent }">
|
||||
<div>
|
||||
<el-row class="oss-manage-box">
|
||||
<el-col v-if="canShowGroups" :span="isComponent ? 5 : 4">
|
||||
@@ -217,8 +217,7 @@
|
||||
'custom-checkbox-card-checked': selectedOss.includes(item.id + ',' + item.url),
|
||||
'oss-card-selectable': isComponent && selectImage,
|
||||
}"
|
||||
@mouseenter="onMouseOver(item, index)"
|
||||
@mouseleave="onMouseOut(item, index)"
|
||||
|
||||
@click="handleCardClick(item, $event)"
|
||||
>
|
||||
<el-checkbox
|
||||
@@ -236,29 +235,12 @@
|
||||
<div v-else class="card-file">
|
||||
<el-icon :size="28"><Document /></el-icon>
|
||||
</div>
|
||||
<div v-if="item.isShowPreview" class="preview">
|
||||
<div @click.prevent="download(item)">
|
||||
<el-tooltip content="下载" placement="top">
|
||||
<el-icon :size="18"><Download /></el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div @click.prevent="remove(item)">
|
||||
<el-tooltip content="删除" placement="top">
|
||||
<el-icon :size="18"><Delete /></el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div
|
||||
@click.prevent="previewFile(item)"
|
||||
>
|
||||
<el-tooltip content="预览" placement="top">
|
||||
<el-icon :size="22"><View /></el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<el-tooltip :content="item.name" placement="bottom">
|
||||
<div class="text">{{ item.name }}</div>
|
||||
</el-tooltip>
|
||||
<div class="sub-info">{{ getFileSubInfo(item) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-checkbox-group>
|
||||
@@ -282,7 +264,7 @@
|
||||
</el-row>
|
||||
|
||||
</div>
|
||||
</el-card>
|
||||
</component>
|
||||
</el-row>
|
||||
<el-dialog
|
||||
v-model="modalVisible"
|
||||
@@ -427,6 +409,26 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
mediaType: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
onlyImage: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const allowVideo = computed(() => {
|
||||
if (props.onlyImage || props.mediaType === "IMAGE") return false;
|
||||
if (props.isComponent && fileType.value === "pic") return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
const allowImage = computed(() => {
|
||||
if (props.mediaType === "VIDEO") return false;
|
||||
if (props.isComponent && fileType.value === "video") return false;
|
||||
return true;
|
||||
});
|
||||
const emit = defineEmits(["callback", "selected"]);
|
||||
|
||||
@@ -834,7 +836,7 @@ const viewColumns = ref([
|
||||
]);
|
||||
const data = ref([]); // 表单数据
|
||||
const total = ref(0); // 表单数据总数
|
||||
const pageSizeOpts = ref([5, 10, 20]); // 页码展示项
|
||||
const pageSizeOpts = ref([20, 40, 60]); // 页码展示项
|
||||
const list = ref([]); // 列表
|
||||
//树结构
|
||||
const treeData = ref([]);
|
||||
@@ -921,6 +923,10 @@ watch(
|
||||
selectedOss.value = [];
|
||||
activeRoleTab.value = "MANAGER";
|
||||
searchForm.userEnums = "MANAGER";
|
||||
if (!allowVideo.value) {
|
||||
activeCategoryTab.value = "IMAGE";
|
||||
searchForm.fileCategory = "IMAGE";
|
||||
}
|
||||
delete searchForm.fileDirectoryId;
|
||||
selectedGroupData.value = "";
|
||||
}
|
||||
@@ -994,13 +1000,8 @@ function onTreeDropdownCommand(cmd, data) {
|
||||
handleContextMenuDelete();
|
||||
}
|
||||
}
|
||||
function onMouseOver(item, index) {
|
||||
data.value[index].isShowPreview = true;
|
||||
proxy.$forceUpdate();
|
||||
}
|
||||
function onMouseOut(item, index) {
|
||||
data.value[index].isShowPreview = false;
|
||||
}
|
||||
function onMouseOver() {}
|
||||
function onMouseOut() {}
|
||||
function handleRoleTabChange(name) {
|
||||
activeRoleTab.value = name;
|
||||
searchForm.pageNumber = 1;
|
||||
@@ -1021,7 +1022,7 @@ function handleCategoryTabChange(name) {
|
||||
if (showType.value === "list") {
|
||||
searchForm.pageSize = 20;
|
||||
} else {
|
||||
searchForm.pageSize = 12;
|
||||
searchForm.pageSize = 20;
|
||||
}
|
||||
getDataList();
|
||||
}
|
||||
@@ -1071,7 +1072,7 @@ function selectOssChange(e) {
|
||||
}
|
||||
function handleCardClick(item, event) {
|
||||
if (!(props.isComponent && selectImage.value)) return;
|
||||
if (event?.target?.closest?.(".preview") || event?.target?.closest?.(".card-checkbox")) {
|
||||
if (event?.target?.closest?.(".card-checkbox")) {
|
||||
return;
|
||||
}
|
||||
const key = `${item.id},${item.url}`;
|
||||
@@ -1370,22 +1371,26 @@ function changeShowType() {
|
||||
if (showType.value == "list") {
|
||||
searchForm.pageSize = 20;
|
||||
} else {
|
||||
searchForm.pageSize = 12;
|
||||
searchForm.pageSize = 20;
|
||||
}
|
||||
getDataList();
|
||||
}
|
||||
// 获取列表数据
|
||||
function getDataList(type = null) {
|
||||
if (showType.value == "list") {
|
||||
pageSizeOpts.value = [10, 20, 50];
|
||||
pageSizeOpts.value = [20, 40, 60];
|
||||
} else {
|
||||
pageSizeOpts.value = [12, 24, 48];
|
||||
pageSizeOpts.value = [20, 40, 60];
|
||||
}
|
||||
loading.value = true;
|
||||
getFileListData(searchForm)
|
||||
.then((res) => {
|
||||
if (res && res.success && res.result) {
|
||||
data.value = res.result.records || [];
|
||||
let records = res.result.records || [];
|
||||
if (!allowVideo.value) {
|
||||
records = records.filter(item => !isVideoFile(item));
|
||||
}
|
||||
data.value = records;
|
||||
total.value = res.result.total || 0;
|
||||
} else {
|
||||
data.value = [];
|
||||
@@ -1564,10 +1569,48 @@ onMounted(() => {
|
||||
// 是组件的话,初始化不调用接口
|
||||
init();
|
||||
} else {
|
||||
searchForm.pageSize = 18; // 页面大小
|
||||
searchForm.pageSize = 20; // 页面大小: 5列 x 4行 = 20
|
||||
}
|
||||
});
|
||||
|
||||
// 父组件通过 ref 设置 selectImage 开启选图模式;decorate.vue 还会写 fileType 并调用 changeFileType
|
||||
defineExpose({ selectImage, fileType, changeFileType });
|
||||
function formatFileSize(size) {
|
||||
if (!size || isNaN(size)) return '0 KB';
|
||||
const num = Number(size);
|
||||
if (num < 1024) return num + ' B';
|
||||
if (num < 1024 * 1024) return Math.round(num / 1024) + ' KB';
|
||||
return (num / (1024 * 1024)).toFixed(1) + ' MB';
|
||||
}
|
||||
|
||||
function getFileFormat(item) {
|
||||
if (item?.fileType) {
|
||||
const parts = item.fileType.split('/');
|
||||
if (parts.length > 1 && parts[1]) {
|
||||
return parts[1].toUpperCase();
|
||||
}
|
||||
}
|
||||
if (item?.name) {
|
||||
const ext = item.name.split('.').pop();
|
||||
if (ext && ext !== item.name) {
|
||||
return ext.toUpperCase();
|
||||
}
|
||||
}
|
||||
return 'IMG';
|
||||
}
|
||||
|
||||
function getFileDimension(item) {
|
||||
if (item?.width && item?.height) {
|
||||
return `${item.width}x${item.height}`;
|
||||
}
|
||||
return '未知尺寸';
|
||||
}
|
||||
|
||||
function getFileSubInfo(item) {
|
||||
const fmt = getFileFormat(item);
|
||||
const dim = getFileDimension(item);
|
||||
const size = formatFileSize(item.fileSize);
|
||||
return `${fmt} · ${dim} · ${size}`;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
@@ -64,6 +64,17 @@
|
||||
<el-form-item label="wap站点地址" prop="staticPageWapAddress">
|
||||
<el-input v-model="formValidate.staticPageWapAddress" />
|
||||
</el-form-item>
|
||||
<el-form-item label="公司名称" prop="companyName">
|
||||
<el-input style="width:350px;" v-model="formValidate.companyName" />
|
||||
<span class="desc">页脚展示的公司名称</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="公司链接" prop="companyUrl">
|
||||
<el-input style="width:350px;" v-model="formValidate.companyUrl" />
|
||||
<span class="desc">页脚公司名称的跳转链接</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="官网地址" prop="website">
|
||||
<el-input style="width:350px;" v-model="formValidate.website" />
|
||||
</el-form-item>
|
||||
<div class="label-btns">
|
||||
<el-button type="primary" @click="submit('formValidateRef')">保存</el-button>
|
||||
</div>
|
||||
@@ -102,12 +113,15 @@ const { proxy } = getCurrentInstance();
|
||||
const picModelFlag = ref(false);
|
||||
const formValidate = ref({
|
||||
buyerSideLogo: "",
|
||||
companyName: "",
|
||||
companyUrl: "",
|
||||
domainLogo: "",
|
||||
icp: "",
|
||||
storeSideLogo: "",
|
||||
siteName: "",
|
||||
staticPageAddress: "",
|
||||
staticPageWapAddress: "",
|
||||
website: "",
|
||||
});
|
||||
const selected = ref("");
|
||||
const ruleValidate = reactive({});
|
||||
|
||||
Reference in New Issue
Block a user