mirror of
https://gitee.com/beijing_hongye_huicheng/lilishop-ui.git
synced 2026-11-06 17:09:08 +08:00
refactor(page-decoration): 重构装修页面组件与交互方式
- 统一 liliDialog ref 命名为 liliDialogRef,避免与组件 import 冲突 - 商品选择弹窗改为 el-table 表格形式展示 - 链接选择器从 el-dialog 改为 el-drawer 抽屉形式 - 为所有 img 标签添加 alt 属性提升可访问性 - 新增拼团、直播、秒杀、砍价、积分商城、优惠券等营销组件默认配置 - 删除商品分类组件,新增商品列表组件配置 - i18n 配置从 legacy 模式切换为 composition 模式 - 注册 PanelSearch、PanelNotice、PanelVideo 等新面板组件 - 优化画布预览区域样式并添加页面设置入口
This commit is contained in:
BIN
manager/public/demo/apple-phone.png
Normal file
BIN
manager/public/demo/apple-phone.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 357 KiB |
BIN
manager/public/demo/apple.png
Normal file
BIN
manager/public/demo/apple.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.8 KiB |
BIN
manager/public/demo/vivo-phone.png
Normal file
BIN
manager/public/demo/vivo-phone.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 371 KiB |
BIN
manager/public/demo/vivo.png
Normal file
BIN
manager/public/demo/vivo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
BIN
manager/public/demo/xiaomi-phone.png
Normal file
BIN
manager/public/demo/xiaomi-phone.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 308 KiB |
BIN
manager/public/demo/xiaomi.png
Normal file
BIN
manager/public/demo/xiaomi.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
@@ -2,7 +2,7 @@
|
||||
<div class="hotzone-box">
|
||||
<div class="hotzone-item">
|
||||
<div ref="content" class="hz-m-wrap">
|
||||
<img class="hz-u-img" :src="image" />
|
||||
<img alt="热区背景图" class="hz-u-img" :src="image" />
|
||||
<ul class="hz-m-area" v-add-item>
|
||||
<zone
|
||||
class="hz-m-item"
|
||||
|
||||
@@ -36,10 +36,10 @@
|
||||
@mouseup.stop
|
||||
@mousemove.stop
|
||||
>
|
||||
<img width="17" height="17" src="../assets/styles/icons8-edit-64.png"></img>
|
||||
<img alt="编辑" width="17" height="17" src="../assets/styles/icons8-edit-64.png"></img>
|
||||
</li>
|
||||
<li class="hz-flex-img">
|
||||
<img class="hz-u-img" :src="zoneForm.img" />
|
||||
<img alt="热区图片" class="hz-u-img" :src="zoneForm.img" />
|
||||
</li>
|
||||
<li class="hz-u-square hz-u-square-tl" data-pointer="dealTL"></li>
|
||||
<li class="hz-u-square hz-u-square-tc" data-pointer="dealTC"></li>
|
||||
@@ -79,7 +79,7 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
<!-- 选择商品。链接 -->
|
||||
<liliDialog ref="liliDialog" @selectedLink="selectedLink"></liliDialog>
|
||||
<liliDialog ref="liliDialogRef" @selectedLink="selectedLink"></liliDialog>
|
||||
<!-- 选择图片 -->
|
||||
<el-dialog width="1200px" v-model="picModelFlag" append-to-body destroy-on-close>
|
||||
<ossManage
|
||||
@@ -124,7 +124,7 @@ const zoneForm = reactive({
|
||||
// 未在原 data 中声明、直接挂载在 this 上的遗留字段(仅赋值,未被读取)
|
||||
let selectedNav;
|
||||
|
||||
const liliDialog = ref(null);
|
||||
const liliDialogRef = ref(null);
|
||||
// 模板 ref 与 import 组件同名会互相覆盖,故改名 ossManageRef
|
||||
const ossManageRef = ref(null);
|
||||
|
||||
@@ -179,7 +179,7 @@ function callbackSelected(item) {
|
||||
// 调起选择链接弹窗
|
||||
function handleSelectLink(item, index) {
|
||||
if (item) selectedNav = item;
|
||||
liliDialog.value.open("link");
|
||||
liliDialogRef.value.open("link");
|
||||
}
|
||||
// 已选链接
|
||||
function selectedLink(val) {
|
||||
|
||||
@@ -28,49 +28,60 @@
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="wap-content-list">
|
||||
<div
|
||||
class="wap-content-item"
|
||||
:class="{ active: item.selected }"
|
||||
@click="checkedGoods(item, index)"
|
||||
v-for="(item, index) in goodsData"
|
||||
:key="index"
|
||||
>
|
||||
<div>
|
||||
<img :src="item.thumbnail" alt="" />
|
||||
</div>
|
||||
<div class="wap-content-desc">
|
||||
<div class="wap-content-desc-title">{{ item.goodsName }}</div>
|
||||
|
||||
<div class="wap-sku">
|
||||
{{ item.goodsUnit }}
|
||||
<el-tag
|
||||
v-if="item.goodsType"
|
||||
style="margin-left: 6px"
|
||||
size="small"
|
||||
:type="goodsTypeTagType(item.goodsType)"
|
||||
>
|
||||
{{ goodsTypeLabel(item.goodsType) }}
|
||||
</el-tag>
|
||||
<el-tag
|
||||
style="margin-left: 6px"
|
||||
:type="item.salesModel === 'RETAIL' ? 'info' : 'primary'"
|
||||
>
|
||||
{{ item.salesModel === "RETAIL" ? "零售型" : "批发型" }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="wap-content-desc-bottom">
|
||||
<div>¥{{ $filters.unitPrice(item.price) }}</div>
|
||||
<div v-if="item.goodsType === 'E_COUPON'" class="pool-stock-hint">
|
||||
库存 {{ item.quantity != null ? item.quantity : "—" }}(卡池)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="loading" v-loading="loading" class="loading-mask" />
|
||||
|
||||
<div v-if="empty" class="empty">暂无商品信息</div>
|
||||
</div>
|
||||
<el-table
|
||||
ref="tableRef"
|
||||
v-loading="loading"
|
||||
class="goods-select-table"
|
||||
:data="goodsData"
|
||||
row-key="id"
|
||||
height="340"
|
||||
empty-text="暂无商品信息"
|
||||
@selection-change="onSelectionChange"
|
||||
>
|
||||
<el-table-column
|
||||
type="selection"
|
||||
label="选择"
|
||||
width="60"
|
||||
align="center"
|
||||
reserve-selection
|
||||
/>
|
||||
<el-table-column label="商品图片" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<img class="goods-thumb" :src="row.thumbnail" alt="" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="goodsName"
|
||||
label="商品名称"
|
||||
min-width="200"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column label="商品规格" min-width="140" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ row.simpleSpecs || "默认规格" }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="价格" width="110">
|
||||
<template #default="{ row }">
|
||||
<span class="goods-price">¥{{ $filters.unitPrice(row.price) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="库存" width="90">
|
||||
<template #default="{ row }">
|
||||
{{ row.quantity != null ? row.quantity : "—" }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="销量" width="90">
|
||||
<template #default="{ row }">
|
||||
{{ row.buyCount != null ? row.buyCount : 0 }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="商品类型" width="110">
|
||||
<template #default="{ row }">
|
||||
{{ goodsTypeLabel(row.goodsType) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
v-model:current-page="goodsParams.pageNumber"
|
||||
class="pageration"
|
||||
@@ -85,9 +96,9 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref, watch } from "vue";
|
||||
import { nextTick, onMounted, reactive, ref, watch } from "vue";
|
||||
import * as API_Goods from "@/api/goods";
|
||||
import { goodsTypeLabel, goodsTypeTagType } from "@/constants/goodsType";
|
||||
import { goodsTypeLabel } from "@/constants/goodsType";
|
||||
|
||||
const props = defineProps({
|
||||
selectedWay: {
|
||||
@@ -114,8 +125,8 @@ const goodsParams = reactive({
|
||||
});
|
||||
const category = ref([]);
|
||||
const goodsData = ref([]);
|
||||
const empty = ref(false);
|
||||
const loading = ref(false);
|
||||
const tableRef = ref(null);
|
||||
|
||||
watch(category, (val) => {
|
||||
goodsParams.categoryPath = val && val.length ? val[2] : "";
|
||||
@@ -124,6 +135,7 @@ watch(
|
||||
() => props.selectedWay,
|
||||
(val) => {
|
||||
selectedList.value = Array.isArray(val) ? val.slice() : [];
|
||||
nextTick(syncTableSelection);
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
@@ -162,28 +174,46 @@ function getQueryGoodsList() {
|
||||
}
|
||||
function initGoods(res) {
|
||||
const records = res?.result?.records || [];
|
||||
if (records.length) {
|
||||
records.forEach((item) => {
|
||||
item.selected = false;
|
||||
item.___type = "goods";
|
||||
selectedList.value.forEach((e) => {
|
||||
if (e.id && e.id === item.id) {
|
||||
item.selected = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
total.value = res.result.total || 0;
|
||||
goodsData.value = records;
|
||||
empty.value = false;
|
||||
} else {
|
||||
goodsData.value = [];
|
||||
empty.value = true;
|
||||
records.forEach((item) => {
|
||||
item.___type = "goods";
|
||||
});
|
||||
total.value = res?.result?.total || 0;
|
||||
goodsData.value = records;
|
||||
nextTick(syncTableSelection);
|
||||
}
|
||||
function syncTableSelection() {
|
||||
const table = tableRef.value;
|
||||
if (!table) return;
|
||||
if (!selectedList.value.length) {
|
||||
table.clearSelection();
|
||||
return;
|
||||
}
|
||||
goodsData.value.forEach((item) => {
|
||||
const selected = selectedList.value.some((e) => e.id && e.id === item.id);
|
||||
table.toggleRowSelection(item, selected);
|
||||
});
|
||||
}
|
||||
function emitSelected(list) {
|
||||
selectedList.value = list;
|
||||
emit("selected", selectedList.value);
|
||||
}
|
||||
function onSelectionChange(rows) {
|
||||
// 选择链接时为单选:保留最后勾选的一项,多选框交互不变
|
||||
if (type.value !== "multiple") {
|
||||
if (!rows.length) {
|
||||
emitSelected([]);
|
||||
return;
|
||||
}
|
||||
const row = rows[rows.length - 1];
|
||||
if (rows.length > 1) {
|
||||
tableRef.value?.clearSelection();
|
||||
tableRef.value?.toggleRowSelection(row, true);
|
||||
}
|
||||
emitSelected([row]);
|
||||
return;
|
||||
}
|
||||
emitSelected(rows);
|
||||
}
|
||||
function init() {
|
||||
API_Goods.getGoodsSkuData(goodsParams).then((res) => {
|
||||
initGoods(res);
|
||||
@@ -221,24 +251,6 @@ function deepGroup(val) {
|
||||
});
|
||||
});
|
||||
}
|
||||
function checkedGoods(val) {
|
||||
if (type.value !== "multiple") {
|
||||
goodsData.value.forEach((item) => {
|
||||
item.selected = false;
|
||||
});
|
||||
val.selected = true;
|
||||
emitSelected([val]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!val.selected) {
|
||||
val.selected = true;
|
||||
emitSelected([...selectedList.value, val]);
|
||||
} else {
|
||||
val.selected = false;
|
||||
emitSelected(selectedList.value.filter((item) => item.id !== val.id));
|
||||
}
|
||||
}
|
||||
|
||||
// 父组件 lili-dialog/index.vue 通过 $refs.goodsDialog.type = "single" 切换单选;
|
||||
// expose 的 ref 外部读写等价于原 data 字段
|
||||
@@ -248,31 +260,19 @@ defineExpose({ type });
|
||||
@import "./style.scss";
|
||||
.wap-content {
|
||||
width: 100%;
|
||||
}
|
||||
.empty {
|
||||
text-align: center;
|
||||
padding: 8px 0;
|
||||
width: 100%;
|
||||
}
|
||||
.wap-content {
|
||||
flex: 1;
|
||||
padding: 0;
|
||||
}
|
||||
.wap-content-list {
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-wrap: wrap;
|
||||
height: 340px;
|
||||
.goods-thumb {
|
||||
display: block;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin: 0 auto;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
}
|
||||
.wap-content-item {
|
||||
width: 210px;
|
||||
margin: 10px 7px;
|
||||
padding: 6px 0;
|
||||
}
|
||||
.active {
|
||||
background: url("../../assets/selected.png") no-repeat;
|
||||
background-position: right;
|
||||
background-size: 10%;
|
||||
.goods-price {
|
||||
color: $theme_color;
|
||||
}
|
||||
.pageration {
|
||||
display: flex;
|
||||
@@ -283,10 +283,3 @@ defineExpose({ type });
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
<style lang="scss">
|
||||
.pool-stock-hint {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-if="goodsFlag"
|
||||
v-model="flag"
|
||||
width="1160px"
|
||||
top="120px"
|
||||
@@ -9,32 +10,49 @@
|
||||
destroy-on-close
|
||||
@close="clickClose"
|
||||
>
|
||||
<template v-if="flag">
|
||||
<goodsDialog
|
||||
@selected="
|
||||
(val) => {
|
||||
goodsData = val;
|
||||
}
|
||||
"
|
||||
v-if="goodsFlag"
|
||||
ref="goodsDialogRef"
|
||||
:selectedWay="goodsData"
|
||||
/>
|
||||
<linkDialog
|
||||
@selectedLink="
|
||||
(val) => {
|
||||
linkData = val;
|
||||
}
|
||||
"
|
||||
v-else
|
||||
class="linkDialog"
|
||||
/>
|
||||
</template>
|
||||
<goodsDialog
|
||||
v-if="flag"
|
||||
ref="goodsDialogRef"
|
||||
:selectedWay="goodsData"
|
||||
@selected="
|
||||
(val) => {
|
||||
goodsData = val;
|
||||
}
|
||||
"
|
||||
/>
|
||||
<template #footer>
|
||||
<el-button @click="clickClose">取消</el-button>
|
||||
<el-button type="primary" @click="clickOK">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-drawer
|
||||
v-else
|
||||
v-model="flag"
|
||||
class="link-selector-drawer"
|
||||
direction="rtl"
|
||||
size="1160px"
|
||||
:with-header="false"
|
||||
:z-index="10000"
|
||||
:close-on-click-modal="false"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
@close="clickClose"
|
||||
>
|
||||
<linkDialog
|
||||
v-if="flag"
|
||||
class="linkDialog"
|
||||
@selectedLink="
|
||||
(val) => {
|
||||
linkData = val;
|
||||
}
|
||||
"
|
||||
/>
|
||||
<template #footer>
|
||||
<el-button @click="clickClose">取消</el-button>
|
||||
<el-button type="primary" @click="clickOK">确定</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
@@ -120,3 +138,22 @@ defineExpose({
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
<style lang="scss">
|
||||
// 抽屉 append-to-body 后不在当前组件 DOM 内,不能使用 scoped 样式
|
||||
.link-selector-drawer {
|
||||
max-width: 96vw;
|
||||
.el-drawer__body {
|
||||
overflow: hidden;
|
||||
}
|
||||
.goods-select-table,
|
||||
.shops-select-table,
|
||||
.articles-select-table,
|
||||
.specials-select-table {
|
||||
height: calc(100vh - 240px) !important;
|
||||
}
|
||||
}
|
||||
// 商品分类下拉挂载到 body,需要高于抽屉的 10000 层级
|
||||
.goods-dialog-cascader-popper {
|
||||
z-index: 10001 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -34,8 +34,8 @@ const route = useRoute();
|
||||
const changed = ref(""); // 变更模板
|
||||
const selected = ref(0); // 已选数据
|
||||
const selectedLink = ref(""); //选中的链接
|
||||
// wap 为共享的模块级数组,保持原 data 引用同一数组的行为
|
||||
const wap = ref(wapList); // tab标签
|
||||
// 选择链接不再提供“活动”tab,wap.js 中的配置保留用于已有链接的名称展示
|
||||
const wap = ref(wapList.filter((item) => item.name !== "marketing")); // tab标签
|
||||
const activeTab = ref(wapList[0]?.title || "");
|
||||
// v-for 内模板 ref 收集为数组;原名 lili-component 含连字符,改名并同步 script
|
||||
const liliComponentRef = ref([]);
|
||||
@@ -75,11 +75,6 @@ onMounted(() => {
|
||||
});
|
||||
}
|
||||
} else {
|
||||
wap.value.push({
|
||||
title: "活动",
|
||||
url: "3",
|
||||
name: "marketing",
|
||||
});
|
||||
let obj = {};
|
||||
wap.value = wap.value.reduce((cur, next) => {
|
||||
//对象去重
|
||||
|
||||
@@ -1,36 +1,45 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-row :gutter="30">
|
||||
<template v-for="(item, index) in linkList" :key="index">
|
||||
<el-col
|
||||
v-if="showLinkItem(item)"
|
||||
:span="4"
|
||||
>
|
||||
<div
|
||||
class="card"
|
||||
:class="{ active: selectedIndex == index }"
|
||||
@click="handleLink(item, index)"
|
||||
>
|
||||
<el-icon :size="24">
|
||||
<component :is="item.icon" />
|
||||
</el-icon>
|
||||
<p>{{ item.title }}</p>
|
||||
</div>
|
||||
</el-col>
|
||||
</template>
|
||||
<el-col v-if="linkVisible" :span="4">
|
||||
<div
|
||||
class="card"
|
||||
:class="{ active: selectedIndex == linkList.length }"
|
||||
@click="handleLink(linkItem, linkList.length)"
|
||||
>
|
||||
<el-icon :size="24">
|
||||
<component :is="linkItem.icon" />
|
||||
<div class="link-list">
|
||||
<template v-for="(item, index) in linkList" :key="index">
|
||||
<div
|
||||
v-if="showLinkItem(item)"
|
||||
class="card"
|
||||
:class="{ active: selectedIndex == index }"
|
||||
@click="handleLink(item, index)"
|
||||
>
|
||||
<span class="link-icon">
|
||||
<el-icon :size="18">
|
||||
<component :is="item.icon" />
|
||||
</el-icon>
|
||||
<p>{{ linkItem.title }}</p>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</span>
|
||||
<p>{{ item.title }}</p>
|
||||
<el-icon v-if="selectedIndex == index" class="selected-icon" :size="18">
|
||||
<Check />
|
||||
</el-icon>
|
||||
<el-icon v-else class="arrow-icon" :size="16">
|
||||
<ArrowRight />
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
<div
|
||||
v-if="linkVisible"
|
||||
class="card"
|
||||
:class="{ active: selectedIndex == linkList.length }"
|
||||
@click="handleLink(linkItem, linkList.length)"
|
||||
>
|
||||
<span class="link-icon">
|
||||
<el-icon :size="18">
|
||||
<component :is="linkItem.icon" />
|
||||
</el-icon>
|
||||
</span>
|
||||
<p>{{ linkItem.title }}</p>
|
||||
<el-icon v-if="selectedIndex == linkList.length" class="selected-icon" :size="18">
|
||||
<Check />
|
||||
</el-icon>
|
||||
<el-icon v-else class="arrow-icon" :size="16">
|
||||
<ArrowRight />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
@@ -49,6 +58,8 @@ import {
|
||||
Share,
|
||||
ShoppingBag,
|
||||
Link,
|
||||
ArrowRight,
|
||||
Check,
|
||||
} from "@element-plus/icons-vue";
|
||||
|
||||
const emit = defineEmits(["selected"]);
|
||||
@@ -109,25 +120,74 @@ function handleLink(val, index) {
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "../style.scss";
|
||||
.link-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
max-height: calc(100vh - 170px);
|
||||
padding: 4px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 20px 0;
|
||||
margin: 10px 0;
|
||||
text-align: center;
|
||||
transition: 0.35s;
|
||||
flex-direction: column;
|
||||
min-height: 104px;
|
||||
padding: 16px 10px 14px;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
:deep(p) {
|
||||
margin: 10px 0;
|
||||
text-align: center;
|
||||
background: #fff;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s ease;
|
||||
p {
|
||||
margin: 10px 0 0;
|
||||
color: #303133;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
}
|
||||
border: 1px solid #ededed;
|
||||
}
|
||||
.card:hover {
|
||||
background: #ededed;
|
||||
border-color: #409eff;
|
||||
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.12);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.active {
|
||||
background: #ededed;
|
||||
.link-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
color: #409eff;
|
||||
background: #ecf5ff;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.arrow-icon {
|
||||
display: none;
|
||||
}
|
||||
.selected-icon {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
color: #409eff;
|
||||
}
|
||||
.active,
|
||||
.active:hover {
|
||||
color: #409eff;
|
||||
background: #ecf5ff;
|
||||
border-color: #409eff;
|
||||
box-shadow: none;
|
||||
p {
|
||||
color: #409eff;
|
||||
}
|
||||
.link-icon {
|
||||
color: #fff;
|
||||
background: #409eff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,72 +1,211 @@
|
||||
<template>
|
||||
<div class="wrapper">
|
||||
<!-- TODO 目前数据少暂且不用 -->
|
||||
<!-- <div class="list">
|
||||
<div class="list-item active">
|
||||
文章页
|
||||
</div>
|
||||
</div> -->
|
||||
<div class="content">
|
||||
<Article @callbacked="callbackArticle" :selected="true" />
|
||||
</div>
|
||||
<div class="query-wrapper">
|
||||
<div class="query-item">
|
||||
<div>文章标题</div>
|
||||
<el-input
|
||||
v-model="params.title"
|
||||
placeholder="请输入文章标题"
|
||||
clearable
|
||||
style="width: 180px"
|
||||
@clear="resetSearch"
|
||||
@keyup.enter="resetSearch"
|
||||
/>
|
||||
</div>
|
||||
<div class="query-item">
|
||||
<el-select
|
||||
v-model="params.categoryId"
|
||||
placeholder="全部分类"
|
||||
clearable
|
||||
filterable
|
||||
style="width: 220px"
|
||||
@change="resetSearch"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in categories"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="query-item">
|
||||
<el-button type="primary" @click="resetSearch">搜索</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<el-table
|
||||
ref="tableRef"
|
||||
v-loading="loading"
|
||||
class="articles-select-table"
|
||||
:data="data"
|
||||
row-key="id"
|
||||
height="340"
|
||||
empty-text="暂无文章信息"
|
||||
@selection-change="onSelectionChange"
|
||||
>
|
||||
<el-table-column
|
||||
type="selection"
|
||||
label="选择"
|
||||
width="60"
|
||||
align="center"
|
||||
reserve-selection
|
||||
/>
|
||||
<el-table-column
|
||||
prop="title"
|
||||
label="文章标题"
|
||||
min-width="240"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
prop="articleCategoryName"
|
||||
label="分类名称"
|
||||
width="140"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column label="是否显示" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.openStatus ? 'success' : 'info'" size="small">
|
||||
{{ row.openStatus ? "展示" : "隐藏" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sort" label="排序" width="80" align="center" />
|
||||
<el-table-column
|
||||
prop="id"
|
||||
label="文章ID"
|
||||
width="180"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
v-model:current-page="params.pageNumber"
|
||||
class="pageration"
|
||||
size="small"
|
||||
layout="total, prev, pager, next, jumper"
|
||||
:total="total"
|
||||
:page-size="params.pageSize"
|
||||
@current-change="changePage"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Article from "@/views/page/article-manage/articleList.vue";
|
||||
import { nextTick, onMounted, reactive, ref } from "vue";
|
||||
import { getArticle, getArticleCategory } from "@/api/pages";
|
||||
|
||||
const emit = defineEmits(["selected"]);
|
||||
|
||||
// 无 defineExpose:grep manager/src 全量确认无外部 ref/$refs/$parent 访问本组件成员
|
||||
// (link-dialog.vue 的 $refs["lili-component"][0].type 写入指向首个 tab 的 goods-dialog,与本组件无关)
|
||||
function callbackArticle(val) {
|
||||
val.___type = "pages";
|
||||
const loading = ref(false);
|
||||
const total = ref(0);
|
||||
const params = reactive({
|
||||
pageNumber: 1,
|
||||
pageSize: 20,
|
||||
sort: "createTime",
|
||||
order: "desc",
|
||||
categoryId: "",
|
||||
title: "",
|
||||
});
|
||||
const data = ref([]);
|
||||
const categories = ref([]);
|
||||
const tableRef = ref(null);
|
||||
const selectedArticle = ref(null);
|
||||
|
||||
val.___path = "/pages/passport/article";
|
||||
onMounted(() => {
|
||||
loadCategories();
|
||||
getDataList();
|
||||
});
|
||||
|
||||
emit("selected", [val]);
|
||||
function flattenCategories(tree, level = 0) {
|
||||
const result = [];
|
||||
(tree || []).forEach((node) => {
|
||||
result.push({
|
||||
id: node.id,
|
||||
name: `${" ".repeat(level)}${node.articleCategoryName}`,
|
||||
});
|
||||
if (node.children && node.children.length) {
|
||||
result.push(...flattenCategories(node.children, level + 1));
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function loadCategories() {
|
||||
getArticleCategory().then((res) => {
|
||||
if (res.success) {
|
||||
categories.value = flattenCategories(res.result);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function resetSearch() {
|
||||
params.pageNumber = 1;
|
||||
getDataList();
|
||||
}
|
||||
|
||||
function changePage(v) {
|
||||
params.pageNumber = v;
|
||||
getDataList();
|
||||
}
|
||||
|
||||
function getDataList() {
|
||||
loading.value = true;
|
||||
const query = { ...params };
|
||||
if (!query.categoryId) delete query.categoryId;
|
||||
if (!query.title) delete query.title;
|
||||
getArticle(query)
|
||||
.then((res) => {
|
||||
if (res.success) {
|
||||
total.value = res.result.total;
|
||||
data.value = res.result.records || [];
|
||||
nextTick(syncTableSelection);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
function syncTableSelection() {
|
||||
const table = tableRef.value;
|
||||
if (!table || !selectedArticle.value) return;
|
||||
data.value.forEach((row) => {
|
||||
table.toggleRowSelection(row, row.id === selectedArticle.value.id);
|
||||
});
|
||||
}
|
||||
|
||||
function onSelectionChange(rows) {
|
||||
// 选择链接只能保留一篇文章,勾选新文章时自动取消上一篇
|
||||
if (!rows.length) {
|
||||
selectedArticle.value = null;
|
||||
emit("selected", []);
|
||||
return;
|
||||
}
|
||||
const row = rows[rows.length - 1];
|
||||
if (rows.length > 1) {
|
||||
tableRef.value?.clearSelection();
|
||||
tableRef.value?.toggleRowSelection(row, true);
|
||||
}
|
||||
selectedArticle.value = row;
|
||||
emit("selected", [
|
||||
{ ...row, ___type: "pages", ___path: "/pages/passport/article" },
|
||||
]);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-card__body) {
|
||||
height: 414px;
|
||||
overflow: auto;
|
||||
}
|
||||
.list {
|
||||
margin: 0 1.5%;
|
||||
height: 400px;
|
||||
overflow: auto;
|
||||
> .list-item {
|
||||
padding: 10px;
|
||||
transition: 0.35s;
|
||||
cursor: pointer;
|
||||
}
|
||||
.list-item:hover {
|
||||
background: #ededed;
|
||||
}
|
||||
}
|
||||
.list {
|
||||
flex: 2;
|
||||
width: auto;
|
||||
}
|
||||
@import "../style.scss";
|
||||
.content {
|
||||
overflow: hidden;
|
||||
flex: 8;
|
||||
height: 431px;
|
||||
flex: 1;
|
||||
}
|
||||
.active {
|
||||
background: #ededed;
|
||||
}
|
||||
.wrapper {
|
||||
height: 416px;
|
||||
overflow: hidden;
|
||||
}
|
||||
:deep(.el-table) {
|
||||
height: 300px !important;
|
||||
overflow: auto;
|
||||
}
|
||||
:deep(.el-card__body) {
|
||||
padding: 0;
|
||||
height: auto;
|
||||
.pageration {
|
||||
margin-top: 12px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -18,31 +18,55 @@
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div v-loading="loading" class="wap-content-list">
|
||||
<div
|
||||
v-for="(item, index) in shopsData"
|
||||
:key="index"
|
||||
class="wap-content-item"
|
||||
:class="{ active: selected == index }"
|
||||
@click="clickShop(item, index)"
|
||||
>
|
||||
<div>
|
||||
<img class="shop-logo" :src="item.storeLogo" alt="" />
|
||||
</div>
|
||||
<div class="wap-content-desc">
|
||||
<div class="wap-content-desc-title">{{ item.storeName }}</div>
|
||||
<div class="self-operated" :class="{ theme_color: item.selfOperated }">
|
||||
{{ item.selfOperated ? "自营" : "非自营" }}
|
||||
</div>
|
||||
<div
|
||||
class="wap-sku"
|
||||
:class="{ theme_color: item.storeDisable === 'OPEN' }"
|
||||
>
|
||||
{{ item.storeDisable === "OPEN" ? "开启中" : "未开启" }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-table
|
||||
ref="tableRef"
|
||||
v-loading="loading"
|
||||
class="shops-select-table"
|
||||
:data="shopsData"
|
||||
row-key="id"
|
||||
height="340"
|
||||
empty-text="暂无店铺信息"
|
||||
@selection-change="onSelectionChange"
|
||||
>
|
||||
<el-table-column
|
||||
type="selection"
|
||||
label="选择"
|
||||
width="60"
|
||||
align="center"
|
||||
reserve-selection
|
||||
/>
|
||||
<el-table-column label="店铺图片" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<img class="shop-logo" :src="row.storeLogo" alt="" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="storeName"
|
||||
label="店铺名称"
|
||||
min-width="240"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
prop="createTime"
|
||||
label="创建时间"
|
||||
width="180"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column label="店铺类型" width="120">
|
||||
<template #default="{ row }">
|
||||
<span :class="{ theme_color: row.selfOperated }">
|
||||
{{ row.selfOperated ? "自营" : "非自营" }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="店铺状态" width="120">
|
||||
<template #default="{ row }">
|
||||
<span :class="{ theme_color: row.storeDisable === 'OPEN' }">
|
||||
{{ row.storeDisable === "OPEN" ? "开启中" : "未开启" }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
class="pageration"
|
||||
size="small"
|
||||
@@ -57,7 +81,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, reactive } from "vue";
|
||||
import { nextTick, reactive, ref } from "vue";
|
||||
import { getShopListData } from "@/api/shops.js";
|
||||
|
||||
const emit = defineEmits(["selected"]);
|
||||
@@ -71,7 +95,8 @@ const params = reactive({
|
||||
storeName: "",
|
||||
});
|
||||
const shopsData = ref([]);
|
||||
const selected = ref(9999999999);
|
||||
const tableRef = ref(null);
|
||||
const selectedShop = ref(null);
|
||||
|
||||
// 无 defineExpose:grep manager/src 全量确认无外部 ref/$refs/$parent 访问本组件成员
|
||||
// (link-dialog.vue 的 $refs["lili-component"][0].type 写入指向首个 tab 的 goods-dialog,与本组件无关)
|
||||
@@ -92,19 +117,41 @@ function changePageSize(v) {
|
||||
|
||||
function init() {
|
||||
loading.value = true;
|
||||
getShopListData(params).then((res) => {
|
||||
if (res.success) {
|
||||
total.value = res.result.total;
|
||||
shopsData.value = res.result.records;
|
||||
}
|
||||
loading.value = false;
|
||||
getShopListData(params)
|
||||
.then((res) => {
|
||||
if (res.success) {
|
||||
total.value = res.result.total;
|
||||
shopsData.value = res.result.records;
|
||||
nextTick(syncTableSelection);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
function syncTableSelection() {
|
||||
const table = tableRef.value;
|
||||
if (!table || !selectedShop.value) return;
|
||||
shopsData.value.forEach((row) => {
|
||||
table.toggleRowSelection(row, row.id === selectedShop.value.id);
|
||||
});
|
||||
}
|
||||
|
||||
function clickShop(val, i) {
|
||||
selected.value = i;
|
||||
val = { ...val, ___type: "shops" };
|
||||
emit("selected", [val]);
|
||||
function onSelectionChange(rows) {
|
||||
// 选择链接只能保留一个店铺,勾选新店铺时自动取消上一个
|
||||
if (!rows.length) {
|
||||
selectedShop.value = null;
|
||||
emit("selected", []);
|
||||
return;
|
||||
}
|
||||
const row = rows[rows.length - 1];
|
||||
if (rows.length > 1) {
|
||||
tableRef.value?.clearSelection();
|
||||
tableRef.value?.toggleRowSelection(row, true);
|
||||
}
|
||||
selectedShop.value = row;
|
||||
emit("selected", [{ ...row, ___type: "shops" }]);
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@@ -112,23 +159,13 @@ function clickShop(val, i) {
|
||||
.shop {
|
||||
display: flex;
|
||||
}
|
||||
.self-operated {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
.wap-content-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
height: 340px;
|
||||
min-height: 120px;
|
||||
}
|
||||
.shop-logo {
|
||||
object-fit: cover;
|
||||
}
|
||||
.active {
|
||||
background: url("../../../assets/selected.png") no-repeat;
|
||||
background-position: right;
|
||||
background-size: 10%;
|
||||
display: block;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin: 0 auto;
|
||||
border-radius: 4px;
|
||||
object-fit: contain;
|
||||
}
|
||||
.pageration {
|
||||
margin-top: 12px;
|
||||
|
||||
@@ -1,50 +1,67 @@
|
||||
<template>
|
||||
<div class="wrapper">
|
||||
<div class="content">
|
||||
<div>
|
||||
<div class="tables">
|
||||
<el-table v-loading="loading" border height="350" :data="data" style="width: 100%">
|
||||
<el-table-column prop="name" label="专题名称" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="100" fixed="right" align="center">
|
||||
<template #default="{ row, $index }">
|
||||
<el-button
|
||||
v-if="row"
|
||||
:type="index === $index ? 'primary' : 'default'"
|
||||
size="small"
|
||||
@click="selectRow(row, $index)"
|
||||
>
|
||||
{{ index === $index ? "已选" : "选择" }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-table
|
||||
ref="tableRef"
|
||||
v-loading="loading"
|
||||
class="specials-select-table"
|
||||
:data="data"
|
||||
row-key="id"
|
||||
height="340"
|
||||
empty-text="暂无专题信息"
|
||||
@selection-change="onSelectionChange"
|
||||
>
|
||||
<el-table-column
|
||||
type="selection"
|
||||
label="选择"
|
||||
width="60"
|
||||
align="center"
|
||||
reserve-selection
|
||||
/>
|
||||
<el-table-column
|
||||
prop="id"
|
||||
label="专题ID"
|
||||
width="240"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
prop="name"
|
||||
label="专题名称"
|
||||
min-width="260"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column label="状态" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.pageShow === 'OPEN' ? 'success' : 'info'" size="small">
|
||||
{{ row.pageShow === "OPEN" ? "开启" : "关闭" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
v-model:current-page="params.pageNumber"
|
||||
v-model:page-size="params.pageSize"
|
||||
class="mt_10"
|
||||
:total="Number(total)"
|
||||
layout="prev, pager, next, jumper"
|
||||
size="small"
|
||||
@current-change="changePageNum"
|
||||
@size-change="changePageSize"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<el-pagination
|
||||
v-model:current-page="params.pageNumber"
|
||||
v-model:page-size="params.pageSize"
|
||||
class="pageration"
|
||||
:total="Number(total)"
|
||||
layout="total, prev, pager, next, jumper"
|
||||
size="small"
|
||||
@current-change="changePageNum"
|
||||
@size-change="changePageSize"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref } from "vue";
|
||||
import { nextTick, onMounted, reactive, ref } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { getHomeList } from "@/api/other.js";
|
||||
|
||||
const emit = defineEmits(["selected"]);
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const loading = ref(true);
|
||||
const index = ref(999);
|
||||
const data = ref([]);
|
||||
const params = reactive({
|
||||
sort: "createTime",
|
||||
@@ -55,12 +72,9 @@ const params = reactive({
|
||||
pageType: "SPECIAL",
|
||||
});
|
||||
const total = ref(0);
|
||||
const tableRef = ref(null);
|
||||
const selectedSpecial = ref(null);
|
||||
|
||||
function selectRow(row, idx) {
|
||||
index.value = idx;
|
||||
const payload = { ...row, pageType: "special", ___type: "special" };
|
||||
emit("selected", [payload]);
|
||||
}
|
||||
function changePageNum(val) {
|
||||
params.pageNumber = val;
|
||||
init();
|
||||
@@ -71,43 +85,54 @@ function changePageSize(val) {
|
||||
init();
|
||||
}
|
||||
async function init() {
|
||||
loading.value = true;
|
||||
params.pageClientType = route.name === "renovation" ? "PC" : "H5";
|
||||
const res = await getHomeList(params);
|
||||
if (res.success) {
|
||||
loading.value = false;
|
||||
data.value = res.result.records;
|
||||
data.value = res.result.records || [];
|
||||
total.value = res.result.total;
|
||||
} else {
|
||||
loading.value = false;
|
||||
nextTick(syncTableSelection);
|
||||
}
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
function syncTableSelection() {
|
||||
const table = tableRef.value;
|
||||
if (!table || !selectedSpecial.value) return;
|
||||
data.value.forEach((row) => {
|
||||
table.toggleRowSelection(row, row.id === selectedSpecial.value.id);
|
||||
});
|
||||
}
|
||||
|
||||
function onSelectionChange(rows) {
|
||||
// 选择链接只能保留一个专题,勾选新专题时自动取消上一个
|
||||
if (!rows.length) {
|
||||
selectedSpecial.value = null;
|
||||
emit("selected", []);
|
||||
return;
|
||||
}
|
||||
const row = rows[rows.length - 1];
|
||||
if (rows.length > 1) {
|
||||
tableRef.value?.clearSelection();
|
||||
tableRef.value?.toggleRowSelection(row, true);
|
||||
}
|
||||
selectedSpecial.value = row;
|
||||
emit("selected", [{ ...row, pageType: "special", ___type: "special" }]);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
init();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
img {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
.search {
|
||||
width: 300px;
|
||||
}
|
||||
.page {
|
||||
margin-top: 2vh;
|
||||
text-align: right;
|
||||
}
|
||||
.tables {
|
||||
height: 400px;
|
||||
margin-top: 20px;
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
}
|
||||
.content {
|
||||
overflow: hidden;
|
||||
flex: 4;
|
||||
flex: 1;
|
||||
}
|
||||
.wrapper {
|
||||
overflow: hidden;
|
||||
.pageration {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -12,7 +12,8 @@ const messages = {
|
||||
};
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: true,
|
||||
// legacy:true 会禁用组合式 useI18n;关闭后配合 globalInjection 旧的 $t 写法依然可用
|
||||
legacy: false,
|
||||
globalInjection: true,
|
||||
locale: lang,
|
||||
fallbackLocale: "zh-CN",
|
||||
|
||||
@@ -25,9 +25,6 @@
|
||||
<el-dropdown-item v-if="res.store" command="shopAuth">
|
||||
<el-badge :value="res.store">待处理店铺入驻审核</el-badge>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="res.waitPayBill" command="accountStatementBill">
|
||||
<el-badge :value="res.waitPayBill">待与商家对账</el-badge>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="res.appointmentWorkOrder" command="manager-appointment-work-orders">
|
||||
<el-badge :value="res.appointmentWorkOrder">未完成预约工单</el-badge>
|
||||
</el-dropdown-item>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<el-carousel autoplay height="340px">
|
||||
<el-carousel-item v-for="(item, index) in data.options.list" :key="index">
|
||||
<div style="overflow: hidden">
|
||||
<img :src="item.img" width="790" height="340" />
|
||||
<img alt="轮播图" :src="item.img" width="790" height="340" />
|
||||
</div>
|
||||
</el-carousel-item>
|
||||
</el-carousel>
|
||||
@@ -117,7 +117,7 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
<!-- 选择商品。链接 -->
|
||||
<liliDialog ref="liliDialog" @selectedLink="selectedLink"></liliDialog>
|
||||
<liliDialog ref="liliDialogRef" @selectedLink="selectedLink"></liliDialog>
|
||||
<!-- 选择图片 -->
|
||||
<el-dialog v-model="picModelFlag" width="1200px" append-to-body destroy-on-close>
|
||||
<ossManage
|
||||
@@ -153,7 +153,7 @@ const articleList = ref([
|
||||
]);
|
||||
|
||||
// ossManageRef:模板 ref 与组件 import 同名,改名避免互相覆盖
|
||||
const liliDialog = ref(null);
|
||||
const liliDialogRef = ref(null);
|
||||
const ossManageRef = ref(null);
|
||||
|
||||
function cloneList(list) {
|
||||
@@ -182,7 +182,7 @@ function handleAdd() {
|
||||
}
|
||||
function handleSelectLink(item) {
|
||||
// 选择链接
|
||||
liliDialog.value.open("link");
|
||||
liliDialogRef.value.open("link");
|
||||
selected.value = item;
|
||||
}
|
||||
function callbackSelected(item) {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<el-carousel autoplay @change="autoChange">
|
||||
<el-carousel-item v-for="(item, index) in data.options.list" :key="index">
|
||||
<div style="overflow: hidden">
|
||||
<img :src="item.img" width="1200" height="470" />
|
||||
<img alt="轮播图" :src="item.img" width="1200" height="470" />
|
||||
</div>
|
||||
</el-carousel-item>
|
||||
</el-carousel>
|
||||
@@ -81,7 +81,7 @@
|
||||
</div>
|
||||
</el-dialog>
|
||||
<!-- 选择商品。链接 -->
|
||||
<liliDialog ref="liliDialog" @selectedLink="selectedLink"></liliDialog>
|
||||
<liliDialog ref="liliDialogRef" @selectedLink="selectedLink"></liliDialog>
|
||||
<!-- 选择图片 -->
|
||||
<el-dialog v-model="picModelFlag" width="1200px" append-to-body destroy-on-close>
|
||||
<ossManage @callback="callbackSelected" :is-component="true" :initialize="picModelFlag" :max-select="1" ref="ossManageRef" />
|
||||
@@ -107,7 +107,7 @@ const selected = ref(null); // 已选数据
|
||||
const picModelFlag = ref(false); // 选择图片modal
|
||||
const bgColor = ref("#fff"); // 轮播背景色
|
||||
|
||||
const liliDialog = ref(null);
|
||||
const liliDialogRef = ref(null);
|
||||
// ossManageRef:模板 ref 与组件 import 同名,改名避免互相覆盖
|
||||
const ossManageRef = ref(null);
|
||||
|
||||
@@ -130,7 +130,7 @@ function handleAdd() {
|
||||
}
|
||||
// 打开选择链接modal
|
||||
function handleSelectLink(item) {
|
||||
liliDialog.value.open("link");
|
||||
liliDialogRef.value.open("link");
|
||||
selected.value = item;
|
||||
}
|
||||
function callbackSelected(item) {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<el-carousel autoplay height="340px">
|
||||
<el-carousel-item v-for="(item, index) in data.options.list" :key="index">
|
||||
<div style="overflow: hidden">
|
||||
<img :src="item.img" width="590" height="470" />
|
||||
<img alt="轮播图" :src="item.img" width="590" height="470" />
|
||||
</div>
|
||||
</el-carousel-item>
|
||||
</el-carousel>
|
||||
@@ -26,13 +26,13 @@
|
||||
:key="index"
|
||||
>
|
||||
<div class="mb_10">
|
||||
<img :src="item[0].img" width="190" height="150" />
|
||||
<img alt="轮播图" :src="item[0].img" width="190" height="150" />
|
||||
</div>
|
||||
<div class="mb_10">
|
||||
<img :src="item[1].img" width="190" height="150" />
|
||||
<img alt="轮播图" :src="item[1].img" width="190" height="150" />
|
||||
</div>
|
||||
<div>
|
||||
<img :src="item[2].img" width="190" height="150" />
|
||||
<img alt="轮播图" :src="item[2].img" width="190" height="150" />
|
||||
</div>
|
||||
</el-carousel-item>
|
||||
</el-carousel>
|
||||
@@ -201,7 +201,7 @@
|
||||
</div>
|
||||
</el-dialog>
|
||||
<!-- 选择商品。链接 -->
|
||||
<liliDialog ref="liliDialog" @selectedLink="selectedLink"></liliDialog>
|
||||
<liliDialog ref="liliDialogRef" @selectedLink="selectedLink"></liliDialog>
|
||||
<!-- 选择图片 -->
|
||||
<el-dialog v-model="picModelFlag" width="1200px" append-to-body destroy-on-close>
|
||||
<ossManage @callback="callbackSelected" :is-component="true" :initialize="picModelFlag" :max-select="1" ref="ossManageRef" />
|
||||
@@ -231,7 +231,7 @@ const articleList = ref([
|
||||
{ title: "促销计算规则" },
|
||||
]);
|
||||
|
||||
const liliDialog = ref(null);
|
||||
const liliDialogRef = ref(null);
|
||||
// ossManageRef:模板 ref 与组件 import 同名,改名避免互相覆盖
|
||||
const ossManageRef = ref(null);
|
||||
|
||||
@@ -258,7 +258,7 @@ function handleTabRemove(index) {
|
||||
}
|
||||
// 打开图片链接
|
||||
function handleSelectLink(item) {
|
||||
liliDialog.value.open("link");
|
||||
liliDialogRef.value.open("link");
|
||||
selected.value = item;
|
||||
}
|
||||
function callbackSelected(item) {
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
class="draggable"
|
||||
>
|
||||
<div class="flex">
|
||||
<img :src="item.img" class="column-img" />
|
||||
<img alt="楼层图片" :src="item.img" class="column-img" />
|
||||
<div class="column-goods-list">
|
||||
<div><el-input v-model="item.title" /></div>
|
||||
<div><el-input v-model="item.desc" /></div>
|
||||
@@ -139,7 +139,7 @@
|
||||
class="draggable"
|
||||
>
|
||||
<div class="flex">
|
||||
<img :src="item.img" class="column-img" />
|
||||
<img alt="楼层图片" :src="item.img" class="column-img" />
|
||||
<div class="column-goods-list">
|
||||
<el-button
|
||||
size="small"
|
||||
@@ -162,7 +162,7 @@
|
||||
|
||||
<!-- 选择商品。链接 -->
|
||||
<liliDialog
|
||||
ref="liliDialog"
|
||||
ref="liliDialogRef"
|
||||
@selectedLink="selectedLink"
|
||||
@selectedGoodsData="selectedGoodsData"
|
||||
></liliDialog>
|
||||
@@ -203,7 +203,7 @@ const selectedData = ref(""); //选中的数据
|
||||
const goodsIndex = ref(0); // 商品索引
|
||||
const defaultCallbackImageType = ref("");
|
||||
|
||||
const liliDialog = ref(null);
|
||||
const liliDialogRef = ref(null);
|
||||
// ossManageRef:模板 ref 与组件 import 同名,改名避免互相覆盖
|
||||
const ossManageRef = ref(null);
|
||||
|
||||
@@ -223,14 +223,14 @@ function handleSelectImg(type, index) {
|
||||
}
|
||||
|
||||
function handleSelectGoods(val, index) {
|
||||
liliDialog.value.open("goods");
|
||||
liliDialog.value.singleGoods();
|
||||
liliDialogRef.value.open("goods");
|
||||
liliDialogRef.value.singleGoods();
|
||||
selectedData.value = val;
|
||||
goodsIndex.value = index;
|
||||
}
|
||||
function handleSelectLink(val, index) {
|
||||
// 调起选择链接弹窗
|
||||
liliDialog.value.open("link");
|
||||
liliDialogRef.value.open("link");
|
||||
selectedData.value = val;
|
||||
|
||||
goodsIndex.value = index;
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
:key="index"
|
||||
>
|
||||
<div class="goods-img">
|
||||
<img :src="item.img" />
|
||||
<img alt="楼层图片" :src="item.img" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="goods-name wes-2">{{ item.title }}</div>
|
||||
@@ -93,7 +93,7 @@
|
||||
class="draggable"
|
||||
>
|
||||
<div class="flex">
|
||||
<img :src="item.img" class="column-img" />
|
||||
<img alt="楼层图片" :src="item.img" class="column-img" />
|
||||
<div class="flex column-goods-config">
|
||||
<div class="column-config">
|
||||
<div>
|
||||
@@ -120,7 +120,7 @@
|
||||
|
||||
<!-- 选择商品。链接 -->
|
||||
<liliDialog
|
||||
ref="liliDialog"
|
||||
ref="liliDialogRef"
|
||||
@selectedLink="selectedLink"
|
||||
@selectedGoodsData="selectedGoodsData"
|
||||
></liliDialog>
|
||||
@@ -146,7 +146,7 @@ const showModal = ref(false);
|
||||
const draftOptions = ref(null);
|
||||
const tabCurrentlyIndex = ref(0); // 选项卡索引
|
||||
|
||||
const liliDialog = ref(null);
|
||||
const liliDialogRef = ref(null);
|
||||
|
||||
onMounted(() => {
|
||||
tabIndex.value = props.data.options.labels[0].___index;
|
||||
@@ -191,8 +191,8 @@ function handleClickTab(name) {
|
||||
}
|
||||
// 添加当前选项卡中的商品
|
||||
function addCurrentGoods() {
|
||||
liliDialog.value.clearGoodsSelected();
|
||||
liliDialog.value.open("goods");
|
||||
liliDialogRef.value.clearGoodsSelected();
|
||||
liliDialogRef.value.open("goods");
|
||||
}
|
||||
// 编辑模块
|
||||
function handleSelectModel() {
|
||||
@@ -228,7 +228,7 @@ function selectedLink(val) {
|
||||
item.type = val.___type === "other" && val.url === "" ? "link" : "other";
|
||||
}
|
||||
function handleSelectLink(index) {
|
||||
liliDialog.value.open("link");
|
||||
liliDialogRef.value.open("link");
|
||||
current.value = index;
|
||||
}
|
||||
function handleCancelModal() {
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
class="draggable"
|
||||
>
|
||||
<div class="flex column-item">
|
||||
<img :src="item.img" class="column-img" />
|
||||
<img alt="楼层图片" :src="item.img" class="column-img" />
|
||||
<el-button
|
||||
size="small"
|
||||
class="ml_10"
|
||||
|
||||
@@ -231,7 +231,7 @@
|
||||
</el-dialog>
|
||||
<!-- 选择商品。链接 -->
|
||||
<liliDialog
|
||||
ref="liliDialog"
|
||||
ref="liliDialogRef"
|
||||
@selectedLink="selectedLink"
|
||||
@selectedGoodsData="selectedGoodsData"
|
||||
></liliDialog>
|
||||
@@ -267,7 +267,7 @@ const editTarget = ref(null); // 确认后写回的原数据
|
||||
const picModelFlag = ref(false); // 选择图片modal
|
||||
|
||||
// ossManageRef:模板 ref 与组件 import 同名,改名避免互相覆盖
|
||||
const liliDialog = ref(null);
|
||||
const liliDialogRef = ref(null);
|
||||
const ossManageRef = ref(null);
|
||||
|
||||
function cloneItem(item) {
|
||||
@@ -285,16 +285,16 @@ function handleSelectModel(item, type) {
|
||||
}
|
||||
function handleSelectLink() {
|
||||
// 调起选择链接弹窗
|
||||
liliDialog.value.open("link");
|
||||
liliDialogRef.value.open("link");
|
||||
}
|
||||
function handleSelectGoods(item) {
|
||||
if (item) {
|
||||
editTarget.value = item;
|
||||
selected.value = cloneItem(item);
|
||||
}
|
||||
liliDialog.value.open("goods", "single");
|
||||
liliDialogRef.value.open("goods", "single");
|
||||
setTimeout(() => {
|
||||
liliDialog.value.goodsData = [selected.value];
|
||||
liliDialogRef.value.goodsData = [selected.value];
|
||||
}, 500);
|
||||
}
|
||||
// 选择链接回调
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
<liliDialog
|
||||
ref="liliDialog"
|
||||
ref="liliDialogRef"
|
||||
@selectedGoodsData="selectedGoodsData"
|
||||
></liliDialog>
|
||||
</div>
|
||||
@@ -117,7 +117,7 @@ const showContent = ref(true);
|
||||
const draftNavList = ref([]);
|
||||
const draftGoodsList = ref([]);
|
||||
|
||||
const liliDialog = ref(null);
|
||||
const liliDialogRef = ref(null);
|
||||
|
||||
watch(() => props.data, function(val){
|
||||
conData.value = val
|
||||
@@ -146,9 +146,9 @@ function handleSelectGoods(item) {
|
||||
showGoodsModal.value = true;
|
||||
}
|
||||
function handleOpenGoodsPicker() {
|
||||
liliDialog.value.open('goods', 'single');
|
||||
liliDialogRef.value.open('goods', 'single');
|
||||
setTimeout(() => {
|
||||
liliDialog.value.goodsData = [selected.value];
|
||||
liliDialogRef.value.goodsData = [selected.value];
|
||||
}, 500);
|
||||
}
|
||||
function selectedGoodsData(val){
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="line flex flex-j-sb">
|
||||
<div class="column" v-for="(item,index) in data.options.list" :key="index">
|
||||
<div v-if="!item.img" class="placeholder">占位符</div>
|
||||
<img v-else :src="item.img" class="three-column-img">
|
||||
<img alt="楼层图片" v-else :src="item.img" class="three-column-img">
|
||||
</div>
|
||||
<div class="setup-box">
|
||||
<div>
|
||||
@@ -23,7 +23,7 @@
|
||||
append-to-body destroy-on-close>
|
||||
<div class="modal-tab-bar">
|
||||
<div v-for="(item,index) in draftList" :key="index">
|
||||
<img :src="item.img" class="three-column-img">
|
||||
<img alt="楼层图片" :src="item.img" class="three-column-img">
|
||||
<div>推荐尺寸:{{item.size}}</div>
|
||||
<el-button
|
||||
size="small"
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<div class="goods-desc">{{ item.desc }}</div>
|
||||
</div>
|
||||
<div class="goods-img">
|
||||
<img :src="item.img" />
|
||||
<img alt="楼层图片" :src="item.img" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="setup-box">
|
||||
@@ -47,7 +47,7 @@
|
||||
class="draggable"
|
||||
>
|
||||
<div class="flex">
|
||||
<img :src="item.img" class="column-img" />
|
||||
<img alt="楼层图片" :src="item.img" class="column-img" />
|
||||
<div class="flex column-goods-config">
|
||||
<div class="column-config">
|
||||
<div>
|
||||
@@ -73,7 +73,7 @@
|
||||
|
||||
<!-- 选择商品。链接 -->
|
||||
<liliDialog
|
||||
ref="liliDialog"
|
||||
ref="liliDialogRef"
|
||||
@selectedLink="selectedLink"
|
||||
@selectedGoodsData="selectedGoodsData"
|
||||
></liliDialog>
|
||||
@@ -97,7 +97,7 @@ const current = ref(0);
|
||||
const showModal = ref(false);
|
||||
const draftList = ref([]);
|
||||
|
||||
const liliDialog = ref(null);
|
||||
const liliDialogRef = ref(null);
|
||||
|
||||
function cloneList(list) {
|
||||
return JSON.parse(JSON.stringify(list || []));
|
||||
@@ -110,7 +110,7 @@ function delGoods(val, i) {
|
||||
|
||||
// 添加当前选项卡中的商品
|
||||
function addCurrentGoods() {
|
||||
liliDialog.value.open("goods");
|
||||
liliDialogRef.value.open("goods");
|
||||
}
|
||||
|
||||
// 编辑模块
|
||||
@@ -156,7 +156,7 @@ function selectedLink(val) {
|
||||
|
||||
function handleSelectLink(index) {
|
||||
// 调起选择链接弹窗
|
||||
liliDialog.value.open("link");
|
||||
liliDialogRef.value.open("link");
|
||||
current.value = index;
|
||||
}
|
||||
|
||||
|
||||
@@ -216,7 +216,7 @@
|
||||
</el-dialog>
|
||||
<!-- 选择商品。链接 -->
|
||||
<liliDialog
|
||||
ref="liliDialog"
|
||||
ref="liliDialogRef"
|
||||
@selectedLink="selectedLink"
|
||||
@selectedGoodsData="selectedGoodsData"
|
||||
></liliDialog>
|
||||
@@ -253,7 +253,7 @@ const editTarget = ref(null); // 确认后写回的原数据
|
||||
const picModelFlag = ref(false); // 图片选择
|
||||
|
||||
// ossManageRef:模板 ref 与组件 import 同名,改名避免互相覆盖
|
||||
const liliDialog = ref(null);
|
||||
const liliDialogRef = ref(null);
|
||||
const ossManageRef = ref(null);
|
||||
|
||||
function cloneItem(item) {
|
||||
@@ -271,11 +271,11 @@ function handleSelectModel(item, type) {
|
||||
}
|
||||
function handleSelectLink(item, index) {
|
||||
// 调起选择链接弹窗
|
||||
liliDialog.value.open("link");
|
||||
liliDialogRef.value.open("link");
|
||||
}
|
||||
function handleSelectGoods(item) {
|
||||
// 调起选择商品
|
||||
liliDialog.value.open("goods", "single");
|
||||
liliDialogRef.value.open("goods", "single");
|
||||
}
|
||||
// 选择链接回调
|
||||
function selectedLink(val) {
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- <el-button type="primary" @click="addDecorate()" ghost>添加</el-button> -->
|
||||
<liliDialog ref="liliDialog" @selectedLink="selectedLink" :types="linkType"></liliDialog>
|
||||
<liliDialog ref="liliDialogRef" @selectedLink="selectedLink" :types="linkType"></liliDialog>
|
||||
</div>
|
||||
<el-dialog v-model="picModelFlag" width="1200px" append-to-body destroy-on-close>
|
||||
<ossManage @callback="callbackSelected" :is-component="true" :initialize="picModelFlag" ref="ossManageRef" />
|
||||
@@ -112,7 +112,7 @@ const advertising = ref([
|
||||
const selectedLinks = reactive({});
|
||||
const linkType = ref(""); // 选择类型
|
||||
|
||||
const liliDialog = ref(null);
|
||||
const liliDialogRef = ref(null);
|
||||
const ossManageRef = ref(null);
|
||||
|
||||
watch(
|
||||
@@ -145,7 +145,7 @@ function openPage() {
|
||||
}
|
||||
// 点击链接
|
||||
function clickLink(item) {
|
||||
liliDialog.value.open('link')
|
||||
liliDialogRef.value.open('link')
|
||||
}
|
||||
// 回调选择的链接
|
||||
function selectedLink(val) {
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<liliDialog
|
||||
ref="liliDialog"
|
||||
ref="liliDialogRef"
|
||||
@selectedLink="selectedLink"
|
||||
:types="linkType"
|
||||
></liliDialog>
|
||||
@@ -113,7 +113,7 @@ const advertising = ref([
|
||||
const linkType = ref(""); // 选择类型
|
||||
const selectedLinks = reactive({});
|
||||
|
||||
const liliDialog = ref(null);
|
||||
const liliDialogRef = ref(null);
|
||||
// files:v-for 文件输入框 ref,脚本内未直接读取(changeFile 通过 document.getElementById 取值)
|
||||
const files = ref([]);
|
||||
// ossManageRef:模板 ref 与组件 import 同名,改名避免互相覆盖
|
||||
@@ -174,7 +174,7 @@ function handleClickFile(item, index) {
|
||||
|
||||
// 点击链接
|
||||
function clickLink(item) {
|
||||
liliDialog.value.open("link");
|
||||
liliDialogRef.value.open("link");
|
||||
}
|
||||
|
||||
//点击图片解析成base64
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
</div>
|
||||
|
||||
<template v-if="activeTab === 'content'">
|
||||
<div class="panel-card">
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">图片设置</div>
|
||||
<div class="section-tip">当前风格:{{ res.name }},请按布局区域配置对应图片</div>
|
||||
<div class="img-item" v-for="(item, index) in res.options.list" :key="index">
|
||||
@@ -30,7 +30,7 @@
|
||||
<div class="img-placeholder" v-else>尺寸 {{ item.size || "不限" }}</div>
|
||||
<div class="field-row" v-if="item.title !== undefined">
|
||||
<div class="field-label">文字</div>
|
||||
<el-input v-model="item.title" size="small" maxlength="10" />
|
||||
<el-input v-model="item.title" maxlength="10" />
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
<el-button plain type="primary" @click="pickImage(item)">选择照片</el-button>
|
||||
@@ -62,32 +62,27 @@
|
||||
|
||||
<div class="panel-card">
|
||||
<div class="section-title">颜色设置</div>
|
||||
<color-field
|
||||
v-model="res.options.bgColor1"
|
||||
label="背景颜色"
|
||||
:default-value="defaults.bgColor1"
|
||||
<gradient-field
|
||||
v-model:start="res.options.bgColor1"
|
||||
v-model:end="res.options.bgColor2"
|
||||
:default-start="defaults.bgColor1"
|
||||
:default-end="defaults.bgColor2"
|
||||
/>
|
||||
<color-field
|
||||
v-model="res.options.bgColor2"
|
||||
label="渐变结束色"
|
||||
:default-value="defaults.bgColor2"
|
||||
/>
|
||||
<div class="section-tip">起始色与结束色一致时为纯色背景</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-card">
|
||||
<div class="section-title">边距设置</div>
|
||||
<slider-field v-model="res.options.paddingTop" label="上边距" :min="0" :max="100" />
|
||||
<slider-field v-model="res.options.paddingBottom" label="下边距" :min="0" :max="100" />
|
||||
<slider-field v-model="res.options.paddingX" label="左右边距" :min="0" :max="30" />
|
||||
<slider-field v-model="res.options.paddingX" label="左右边距" :min="0" :max="40" />
|
||||
<slider-field v-model="res.options.pageGap" label="页面间距" :min="0" :max="100" />
|
||||
<slider-field v-model="res.options.contentGap" label="内容间距" :min="0" :max="50" />
|
||||
<slider-field v-model="res.options.contentGap" label="内容间距" :min="0" :max="20" />
|
||||
</div>
|
||||
|
||||
<div class="panel-card">
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">圆角设置</div>
|
||||
<slider-field v-model="res.options.borderRadius" label="背景圆角" :min="0" :max="50" />
|
||||
<slider-field v-model="res.options.imgRadius" label="内容圆角" :min="0" :max="50" />
|
||||
<slider-field v-model="res.options.borderRadius" label="背景圆角" :min="0" :max="30" />
|
||||
<slider-field v-model="res.options.imgRadius" label="内容圆角" :min="0" :max="30" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -110,6 +105,7 @@ import ossManage from "@/views/sys/oss-manage/ossManage";
|
||||
import ways from "@/components/lili-dialog/wap.js";
|
||||
import { modelData } from "../config";
|
||||
import ColorField from "../controls/ColorField.vue";
|
||||
import GradientField from "../controls/GradientField.vue";
|
||||
import SliderField from "../controls/SliderField.vue";
|
||||
|
||||
const DEFAULTS = {
|
||||
@@ -127,7 +123,7 @@ const DEFAULTS = {
|
||||
export default {
|
||||
name: "PanelAdCube",
|
||||
hideDecorateTitle: true,
|
||||
components: { ossManage, ColorField, SliderField },
|
||||
components: { ossManage, ColorField, GradientField, SliderField },
|
||||
props: {
|
||||
res: { type: Object, required: true },
|
||||
},
|
||||
@@ -200,11 +196,12 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../controls/panel-common.scss";
|
||||
.panel-ad-cube {
|
||||
background: #f5f7fa;
|
||||
margin: 0 -20px -120px;
|
||||
padding: 0 12px 120px;
|
||||
min-height: calc(100vh - 70px);
|
||||
margin: 0 -20px;
|
||||
padding: 0 12px 12px;
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.panel-header {
|
||||
@@ -219,7 +216,7 @@ export default {
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
|
||||
box-sizing: border-box;
|
||||
.panel-name {
|
||||
font-size: 14px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
}
|
||||
@@ -252,13 +249,13 @@ export default {
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
.section-title {
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
.section-tip {
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
@@ -271,7 +268,7 @@ export default {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
margin-bottom: 8px;
|
||||
.img-size {
|
||||
@@ -307,7 +304,7 @@ export default {
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
.field-label {
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
margin-right: 10px;
|
||||
flex-shrink: 0;
|
||||
|
||||
@@ -1,73 +1,107 @@
|
||||
<template>
|
||||
<div class="panel-article">
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">标题</div>
|
||||
<div class="ctrl-body">
|
||||
<el-input v-model="firstItem.title" style="width: 200px" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">文章分类</div>
|
||||
<div class="ctrl-body">
|
||||
<el-select
|
||||
v-model="firstItem.categoryId"
|
||||
placeholder="全部分类"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
<div class="panel-header">
|
||||
<span class="panel-name">{{ res.name }}</span>
|
||||
<div class="panel-tabs">
|
||||
<div
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="panel-tab"
|
||||
:class="{ active: activeTab === tab.key }"
|
||||
@click="activeTab = tab.key"
|
||||
>
|
||||
<el-option
|
||||
v-for="cat in categories"
|
||||
:key="cat.id"
|
||||
:label="cat.name"
|
||||
:value="cat.id"
|
||||
/>
|
||||
</el-select>
|
||||
{{ tab.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<slider-field
|
||||
v-model="firstItem.count"
|
||||
label="显示数量"
|
||||
:min="1"
|
||||
:max="20"
|
||||
/>
|
||||
<color-field
|
||||
v-model="res.options.bgColor"
|
||||
label="背景颜色"
|
||||
:default-value="defaults.bgColor"
|
||||
/>
|
||||
<slider-field
|
||||
v-model="res.options.paddingX"
|
||||
label="左右边距"
|
||||
:min="0"
|
||||
:max="30"
|
||||
/>
|
||||
<slider-field
|
||||
v-model="res.options.pageGap"
|
||||
label="页面间距"
|
||||
:min="0"
|
||||
:max="100"
|
||||
/>
|
||||
<slider-field
|
||||
v-model="res.options.borderRadius"
|
||||
label="背景圆角"
|
||||
:min="0"
|
||||
:max="30"
|
||||
/>
|
||||
|
||||
<template v-if="activeTab === 'content'">
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">文章设置</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">标题</div>
|
||||
<div class="ctrl-body">
|
||||
<el-input v-model="firstItem.title" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">文章分类</div>
|
||||
<div class="ctrl-body">
|
||||
<el-select
|
||||
v-model="firstItem.categoryId"
|
||||
placeholder="全部分类"
|
||||
clearable
|
||||
|
||||
>
|
||||
<el-option
|
||||
v-for="cat in categories"
|
||||
:key="cat.id"
|
||||
:label="cat.name"
|
||||
:value="cat.id"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
<slider-field
|
||||
v-model="firstItem.count"
|
||||
label="文章数量"
|
||||
:min="1"
|
||||
:max="10"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="panel-card">
|
||||
<div class="section-title">颜色设置</div>
|
||||
<gradient-field
|
||||
v-model:start="res.options.bgColor1"
|
||||
v-model:end="res.options.bgColor2"
|
||||
:default-start="defaults.bgColor1"
|
||||
:default-end="defaults.bgColor2"
|
||||
/>
|
||||
<color-field
|
||||
v-model="res.options.titleColor"
|
||||
label="标题颜色"
|
||||
:default-value="defaults.titleColor"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="panel-card">
|
||||
<div class="section-title">边距设置</div>
|
||||
<slider-field v-model="res.options.paddingTop" label="上边距" :min="0" :max="100" />
|
||||
<slider-field v-model="res.options.paddingBottom" label="下边距" :min="0" :max="100" />
|
||||
<slider-field v-model="res.options.contentGap" label="文章间距" :min="0" :max="50" />
|
||||
<slider-field v-model="res.options.paddingX" label="左右边距" :min="0" :max="40" />
|
||||
<slider-field v-model="res.options.pageGap" label="页面间距" :min="0" :max="100" />
|
||||
</div>
|
||||
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">圆角设置</div>
|
||||
<slider-field v-model="res.options.borderRadius" label="背景圆角" :min="0" :max="30" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getArticleCategory } from "@/api/pages";
|
||||
import ColorField from "../controls/ColorField.vue";
|
||||
import GradientField from "../controls/GradientField.vue";
|
||||
import SliderField from "../controls/SliderField.vue";
|
||||
|
||||
const DEFAULTS = {
|
||||
bgColor: "#FFFFFF",
|
||||
bgColor1: "#FFFFFF",
|
||||
bgColor2: "#FFFFFF",
|
||||
titleColor: "#282828",
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
contentGap: 10,
|
||||
paddingX: 0,
|
||||
pageGap: 10,
|
||||
borderRadius: 0,
|
||||
};
|
||||
const DEFAULT_ITEM = { title: "文章列表", categoryId: "", count: 3 };
|
||||
const DEFAULT_ITEM = { title: "文章列表", categoryId: "", count: 6 };
|
||||
|
||||
function flattenCategories(tree, level = 0) {
|
||||
const result = [];
|
||||
@@ -82,12 +116,21 @@ function flattenCategories(tree, level = 0) {
|
||||
|
||||
export default {
|
||||
name: "PanelArticle",
|
||||
components: { ColorField, SliderField },
|
||||
hideDecorateTitle: true,
|
||||
components: { ColorField, GradientField, SliderField },
|
||||
props: {
|
||||
res: { type: Object, required: true },
|
||||
},
|
||||
data() {
|
||||
return { defaults: DEFAULTS, categories: [] };
|
||||
return {
|
||||
defaults: DEFAULTS,
|
||||
categories: [],
|
||||
activeTab: "content",
|
||||
tabs: [
|
||||
{ key: "content", title: "内容" },
|
||||
{ key: "style", title: "样式" },
|
||||
],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
firstItem() {
|
||||
@@ -110,4 +153,5 @@ export default {
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../controls/field-common.scss";
|
||||
@import "../controls/panel-common.scss";
|
||||
</style>
|
||||
|
||||
@@ -1,37 +1,40 @@
|
||||
<template>
|
||||
<div class="panel-blank">
|
||||
<color-field
|
||||
v-model="res.options.bgColor"
|
||||
label="背景颜色"
|
||||
:default-value="defaults.bgColor"
|
||||
/>
|
||||
<slider-field
|
||||
v-model="res.options.height"
|
||||
label="空白高度"
|
||||
:min="1"
|
||||
:max="100"
|
||||
/>
|
||||
<slider-field
|
||||
v-model="res.options.paddingX"
|
||||
label="左右边距"
|
||||
:min="0"
|
||||
:max="30"
|
||||
/>
|
||||
<slider-field
|
||||
v-model="res.options.pageGap"
|
||||
label="页面间距"
|
||||
:min="0"
|
||||
:max="100"
|
||||
/>
|
||||
<div class="panel-header">
|
||||
<span class="panel-name">{{ res.name }}</span>
|
||||
</div>
|
||||
|
||||
<div class="panel-card">
|
||||
<div class="section-title">颜色设置</div>
|
||||
<gradient-field
|
||||
v-model:start="res.options.bgColor1"
|
||||
v-model:end="res.options.bgColor2"
|
||||
:default-start="defaults.bgColor1"
|
||||
:default-end="defaults.bgColor2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="panel-card">
|
||||
<div class="section-title">高度设置</div>
|
||||
<slider-field v-model="res.options.height" label="空白高度" :min="1" :max="100" />
|
||||
</div>
|
||||
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">边距设置</div>
|
||||
<slider-field v-model="res.options.paddingX" label="左右边距" :min="0" :max="40" />
|
||||
<slider-field v-model="res.options.pageGap" label="页面间距" :min="0" :max="100" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ColorField from "../controls/ColorField.vue";
|
||||
import GradientField from "../controls/GradientField.vue";
|
||||
import SliderField from "../controls/SliderField.vue";
|
||||
|
||||
const DEFAULTS = {
|
||||
bgColor: "#f5f5f5",
|
||||
bgColor1: "#f5f5f5",
|
||||
bgColor2: "#f5f5f5",
|
||||
height: 10,
|
||||
paddingX: 0,
|
||||
pageGap: 10,
|
||||
@@ -39,7 +42,8 @@ const DEFAULTS = {
|
||||
|
||||
export default {
|
||||
name: "PanelBlank",
|
||||
components: { ColorField, SliderField },
|
||||
hideDecorateTitle: true,
|
||||
components: { GradientField, SliderField },
|
||||
props: {
|
||||
res: { type: Object, required: true },
|
||||
},
|
||||
@@ -51,3 +55,15 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../controls/panel-common.scss";
|
||||
|
||||
.panel-blank {
|
||||
background: #f5f7fa;
|
||||
margin: 0 -20px;
|
||||
padding: 0 12px 12px;
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -16,9 +16,8 @@
|
||||
</div>
|
||||
|
||||
<template v-if="activeTab === 'content'">
|
||||
<div class="panel-card">
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">图片设置</div>
|
||||
<div class="section-tip">最多添加 10 张,建议添加比例一致的图片</div>
|
||||
<div class="img-item" v-for="(item, index) in res.options.list" :key="index">
|
||||
<div class="img-item-head">
|
||||
<span>图片 {{ index + 1 }}</span>
|
||||
@@ -36,16 +35,21 @@
|
||||
<el-icon class="del" @click="removeItem(index)"><Delete /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="img-preview" v-if="item.img">
|
||||
<img :src="item.img" alt="" />
|
||||
</div>
|
||||
<div class="img-placeholder" v-else>建议尺寸 750*350</div>
|
||||
<div class="btn-row">
|
||||
<el-button plain type="primary" @click="pickImage(item)">选择照片</el-button>
|
||||
<el-button plain type="primary" @click="pickLink(item)">选择链接</el-button>
|
||||
</div>
|
||||
<div class="link-view" v-if="item.url && item.url.___type">
|
||||
已选链接:{{ linkText(item.url) }}
|
||||
<div class="img-item-body">
|
||||
<div class="img-thumb" v-if="item.img">
|
||||
<img :src="item.img" alt="" />
|
||||
</div>
|
||||
<div class="img-thumb placeholder" v-else>750*350</div>
|
||||
<div class="img-item-fields">
|
||||
<div class="btn-row">
|
||||
<el-button plain type="primary" @click="pickImage(item)">选择照片</el-button>
|
||||
<el-button plain type="primary" @click="pickLink(item)">选择链接</el-button>
|
||||
</div>
|
||||
<div class="link-view" v-if="item.url && item.url.___type">
|
||||
已选链接:{{ linkText(item.url) }}
|
||||
</div>
|
||||
<div class="link-view empty" v-else>未设置链接</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-button
|
||||
@@ -62,20 +66,19 @@
|
||||
<template v-else>
|
||||
<div class="panel-card">
|
||||
<div class="section-title">轮播风格</div>
|
||||
<div class="setting-row">
|
||||
<div class="setting-label">
|
||||
风格
|
||||
<span class="setting-value">{{ swiperStyleText }}</span>
|
||||
</div>
|
||||
<div class="seg">
|
||||
<div
|
||||
v-for="opt in swiperStyleOptions"
|
||||
:key="opt.value"
|
||||
class="seg-item"
|
||||
:class="{ active: res.options.swiperStyle === opt.value }"
|
||||
@click="res.options.swiperStyle = opt.value"
|
||||
>
|
||||
{{ opt.title }}
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">风格</div>
|
||||
<div class="ctrl-body">
|
||||
<div class="seg layout-seg">
|
||||
<div
|
||||
v-for="opt in swiperStyleOptions"
|
||||
:key="opt.value"
|
||||
class="seg-item"
|
||||
:class="{ active: res.options.swiperStyle === opt.value }"
|
||||
@click="res.options.swiperStyle = opt.value"
|
||||
>
|
||||
{{ opt.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -84,48 +87,48 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-card">
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">指示器设置</div>
|
||||
<div class="setting-row">
|
||||
<div class="setting-label">
|
||||
指示器样式
|
||||
<span class="setting-value">{{ indicatorStyleText }}</span>
|
||||
</div>
|
||||
<div class="seg">
|
||||
<div
|
||||
v-for="opt in indicatorStyleOptions"
|
||||
:key="opt.value"
|
||||
class="seg-item"
|
||||
:class="{ active: res.options.indicatorStyle === opt.value }"
|
||||
@click="res.options.indicatorStyle = opt.value"
|
||||
>
|
||||
{{ opt.title }}
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">指示器样式</div>
|
||||
<div class="ctrl-body">
|
||||
<div class="seg layout-seg-3">
|
||||
<div
|
||||
v-for="opt in indicatorStyleOptions"
|
||||
:key="opt.value"
|
||||
class="seg-item"
|
||||
:class="{ active: res.options.indicatorStyle === opt.value }"
|
||||
@click="res.options.indicatorStyle = opt.value"
|
||||
>
|
||||
{{ opt.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-row" v-if="res.options.indicatorStyle !== 'none'">
|
||||
<div class="setting-label">
|
||||
指示器位置
|
||||
<span class="setting-value">{{ indicatorPositionText }}</span>
|
||||
</div>
|
||||
<div class="seg">
|
||||
<div
|
||||
v-for="opt in indicatorPositionOptions"
|
||||
:key="opt.value"
|
||||
class="seg-item"
|
||||
:class="{ active: res.options.indicatorPosition === opt.value }"
|
||||
@click="res.options.indicatorPosition = opt.value"
|
||||
>
|
||||
{{ opt.title }}
|
||||
<div class="ctrl-row" v-if="res.options.indicatorStyle !== 'none'">
|
||||
<div class="ctrl-label">指示器位置</div>
|
||||
<div class="ctrl-body">
|
||||
<div class="seg layout-seg-3">
|
||||
<div
|
||||
v-for="opt in indicatorPositionOptions"
|
||||
:key="opt.value"
|
||||
class="seg-item"
|
||||
:class="{ active: res.options.indicatorPosition === opt.value }"
|
||||
@click="res.options.indicatorPosition = opt.value"
|
||||
>
|
||||
{{ opt.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-row" v-if="res.options.indicatorStyle !== 'none'">
|
||||
<div class="setting-label">色调</div>
|
||||
<el-radio-group v-model="res.options.indicatorColorMode">
|
||||
<el-radio value="theme">跟随主题风格</el-radio>
|
||||
<el-radio value="custom">自定义</el-radio>
|
||||
</el-radio-group>
|
||||
<div class="ctrl-row" v-if="res.options.indicatorStyle !== 'none'">
|
||||
<div class="ctrl-label">色调</div>
|
||||
<div class="ctrl-body">
|
||||
<el-radio-group v-model="res.options.indicatorColorMode">
|
||||
<el-radio value="theme">跟随主题风格</el-radio>
|
||||
<el-radio value="custom">自定义</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
<template
|
||||
v-if="
|
||||
@@ -143,31 +146,26 @@
|
||||
|
||||
<div class="panel-card">
|
||||
<div class="section-title">颜色设置</div>
|
||||
<color-field
|
||||
v-model="res.options.bgColor1"
|
||||
label="背景颜色"
|
||||
:default-value="defaults.bgColor1"
|
||||
<gradient-field
|
||||
v-model:start="res.options.bgColor1"
|
||||
v-model:end="res.options.bgColor2"
|
||||
:default-start="defaults.bgColor1"
|
||||
:default-end="defaults.bgColor2"
|
||||
/>
|
||||
<color-field
|
||||
v-model="res.options.bgColor2"
|
||||
label="渐变结束色"
|
||||
:default-value="defaults.bgColor2"
|
||||
/>
|
||||
<div class="section-tip">起始色与结束色一致时为纯色背景</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-card">
|
||||
<div class="section-title">边距设置</div>
|
||||
<slider-field v-model="res.options.paddingTop" label="上边距" :min="0" :max="100" />
|
||||
<slider-field v-model="res.options.paddingBottom" label="下边距" :min="0" :max="100" />
|
||||
<slider-field v-model="res.options.paddingX" label="左右边距" :min="0" :max="30" />
|
||||
<slider-field v-model="res.options.paddingX" label="左右边距" :min="0" :max="40" />
|
||||
<slider-field v-model="res.options.pageGap" label="页面间距" :min="0" :max="100" />
|
||||
</div>
|
||||
|
||||
<div class="panel-card">
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">圆角设置</div>
|
||||
<slider-field v-model="res.options.borderRadius" label="背景圆角" :min="0" :max="50" />
|
||||
<slider-field v-model="res.options.imgRadius" label="内容圆角" :min="0" :max="50" />
|
||||
<slider-field v-model="res.options.borderRadius" label="背景圆角" :min="0" :max="30" />
|
||||
<slider-field v-model="res.options.imgRadius" label="内容圆角" :min="0" :max="30" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -188,6 +186,7 @@
|
||||
<script>
|
||||
import { Top, Bottom, Delete } from "@element-plus/icons-vue";
|
||||
import ColorField from "../controls/ColorField.vue";
|
||||
import GradientField from "../controls/GradientField.vue";
|
||||
import SliderField from "../controls/SliderField.vue";
|
||||
import ossManage from "@/views/sys/oss-manage/ossManage";
|
||||
import ways from "@/components/lili-dialog/wap.js";
|
||||
@@ -211,7 +210,7 @@ const DEFAULTS = {
|
||||
export default {
|
||||
name: "PanelCarousel",
|
||||
hideDecorateTitle: true,
|
||||
components: { ColorField, SliderField, ossManage, Top, Bottom, Delete },
|
||||
components: { ColorField, GradientField, SliderField, ossManage, Top, Bottom, Delete },
|
||||
props: {
|
||||
res: { type: Object, required: true },
|
||||
},
|
||||
@@ -241,26 +240,6 @@ export default {
|
||||
currentItem: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
swiperStyleText() {
|
||||
const hit = this.swiperStyleOptions.find(
|
||||
(o) => o.value === this.res.options.swiperStyle
|
||||
);
|
||||
return hit ? hit.title : "";
|
||||
},
|
||||
indicatorStyleText() {
|
||||
const hit = this.indicatorStyleOptions.find(
|
||||
(o) => o.value === this.res.options.indicatorStyle
|
||||
);
|
||||
return hit ? hit.title : "";
|
||||
},
|
||||
indicatorPositionText() {
|
||||
const hit = this.indicatorPositionOptions.find(
|
||||
(o) => o.value === this.res.options.indicatorPosition
|
||||
);
|
||||
return hit ? hit.title : "";
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.res.options = Object.assign({}, DEFAULTS, this.res.options);
|
||||
if (!Array.isArray(this.res.options.list)) {
|
||||
@@ -329,11 +308,13 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../controls/field-common.scss";
|
||||
@import "../controls/panel-common.scss";
|
||||
.panel-carousel {
|
||||
background: #f5f7fa;
|
||||
margin: 0 -20px -120px;
|
||||
padding: 0 12px 120px;
|
||||
min-height: calc(100vh - 70px);
|
||||
margin: 0 -20px;
|
||||
padding: 0 12px 12px;
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.panel-header {
|
||||
@@ -348,7 +329,7 @@ export default {
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
|
||||
box-sizing: border-box;
|
||||
.panel-name {
|
||||
font-size: 14px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
}
|
||||
@@ -381,13 +362,13 @@ export default {
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
.section-title {
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
.section-tip {
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
@@ -400,7 +381,7 @@ export default {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
margin-bottom: 8px;
|
||||
.img-item-ops {
|
||||
@@ -424,27 +405,43 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
.img-preview {
|
||||
.img-item-body {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
.img-thumb {
|
||||
width: 132px;
|
||||
height: 62px;
|
||||
border: 1px dashed #dcdfe6;
|
||||
border-radius: 6px;
|
||||
padding: 6px;
|
||||
margin-bottom: 10px;
|
||||
padding: 3px;
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
}
|
||||
&.placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
.img-placeholder {
|
||||
height: 80px;
|
||||
.img-item-fields {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
border: 1px dashed #dcdfe6;
|
||||
border-radius: 6px;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
margin-bottom: 10px;
|
||||
gap: 6px;
|
||||
.link-view {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
.btn-row {
|
||||
display: flex;
|
||||
@@ -461,43 +458,4 @@ export default {
|
||||
.add-btn {
|
||||
width: 100%;
|
||||
}
|
||||
.setting-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 14px 0;
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.setting-label {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
.setting-value {
|
||||
margin-left: 8px;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
}
|
||||
.seg {
|
||||
display: inline-flex;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
.seg-item {
|
||||
padding: 6px 14px;
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
cursor: pointer;
|
||||
border-left: 1px solid #dcdfe6;
|
||||
transition: all 0.2s ease;
|
||||
&:first-child {
|
||||
border-left: none;
|
||||
}
|
||||
&.active {
|
||||
background: #ecf5ff;
|
||||
color: #409eff;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,63 +1,121 @@
|
||||
<template>
|
||||
<div class="panel-coupon">
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">标题</div>
|
||||
<div class="ctrl-body">
|
||||
<el-input v-model="firstItem.title" style="width: 200px" />
|
||||
<div class="panel-header">
|
||||
<span class="panel-name">{{ res.name }}</span>
|
||||
<div class="panel-tabs">
|
||||
<div
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="panel-tab"
|
||||
:class="{ active: activeTab === tab.key }"
|
||||
@click="activeTab = tab.key"
|
||||
>
|
||||
{{ tab.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<slider-field
|
||||
v-model="firstItem.count"
|
||||
label="显示数量"
|
||||
:min="1"
|
||||
:max="10"
|
||||
/>
|
||||
<color-field
|
||||
v-model="res.options.bgColor"
|
||||
label="背景颜色"
|
||||
:default-value="defaults.bgColor"
|
||||
/>
|
||||
<slider-field
|
||||
v-model="res.options.paddingX"
|
||||
label="左右边距"
|
||||
:min="0"
|
||||
:max="30"
|
||||
/>
|
||||
<slider-field
|
||||
v-model="res.options.pageGap"
|
||||
label="页面间距"
|
||||
:min="0"
|
||||
:max="100"
|
||||
/>
|
||||
<slider-field
|
||||
v-model="res.options.borderRadius"
|
||||
label="背景圆角"
|
||||
:min="0"
|
||||
:max="30"
|
||||
/>
|
||||
|
||||
<template v-if="activeTab === 'content'">
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">展示设置</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">标题</div>
|
||||
<div class="ctrl-body">
|
||||
<el-input v-model="firstItem.title" />
|
||||
</div>
|
||||
</div>
|
||||
<slider-field v-model="firstItem.count" label="展示数量" :min="1" :max="10" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="panel-card">
|
||||
<div class="section-title">布局设置</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">展示样式</div>
|
||||
<div class="ctrl-body">
|
||||
<div class="seg layout-seg">
|
||||
<div
|
||||
v-for="opt in layoutOptions"
|
||||
:key="opt.value"
|
||||
class="seg-item"
|
||||
:class="{ active: (res.options.layoutStyle || '1') === opt.value }"
|
||||
@click="res.options.layoutStyle = opt.value"
|
||||
>
|
||||
{{ opt.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-card">
|
||||
<div class="section-title">颜色设置</div>
|
||||
<gradient-field
|
||||
v-model:start="res.options.bgColor1"
|
||||
v-model:end="res.options.bgColor2"
|
||||
:default-start="defaults.bgColor1"
|
||||
:default-end="defaults.bgColor2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="panel-card">
|
||||
<div class="section-title">边距设置</div>
|
||||
<slider-field v-model="res.options.paddingTop" label="上边距" :min="0" :max="100" />
|
||||
<slider-field v-model="res.options.paddingBottom" label="下边距" :min="0" :max="100" />
|
||||
<slider-field v-model="res.options.contentGap" label="内容间距" :min="0" :max="20" />
|
||||
<slider-field v-model="res.options.paddingX" label="左右边距" :min="0" :max="40" />
|
||||
<slider-field v-model="res.options.pageGap" label="页面间距" :min="0" :max="100" />
|
||||
</div>
|
||||
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">圆角设置</div>
|
||||
<slider-field v-model="res.options.borderRadius" label="背景圆角" :min="0" :max="30" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ColorField from "../controls/ColorField.vue";
|
||||
import GradientField from "../controls/GradientField.vue";
|
||||
import SliderField from "../controls/SliderField.vue";
|
||||
|
||||
const DEFAULTS = {
|
||||
layoutStyle: "1",
|
||||
bgColor: "#FFFFFF",
|
||||
paddingX: 0,
|
||||
bgColor1: "#FFFFFF",
|
||||
bgColor2: "#FFFFFF",
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
contentGap: 5,
|
||||
paddingX: 12,
|
||||
pageGap: 10,
|
||||
borderRadius: 0,
|
||||
};
|
||||
const DEFAULT_ITEM = { title: "优惠券", count: 3 };
|
||||
const DEFAULT_ITEM = { title: "优惠券", count: 5 };
|
||||
|
||||
export default {
|
||||
name: "PanelCoupon",
|
||||
components: { ColorField, SliderField },
|
||||
hideDecorateTitle: true,
|
||||
components: { GradientField, SliderField },
|
||||
props: {
|
||||
res: { type: Object, required: true },
|
||||
},
|
||||
data() {
|
||||
return { defaults: DEFAULTS };
|
||||
return {
|
||||
defaults: DEFAULTS,
|
||||
activeTab: "content",
|
||||
tabs: [
|
||||
{ key: "content", title: "内容" },
|
||||
{ key: "style", title: "样式" },
|
||||
],
|
||||
layoutOptions: [
|
||||
{ value: "1", title: "样式一" },
|
||||
{ value: "2", title: "样式二" },
|
||||
{ value: "3", title: "样式三" },
|
||||
{ value: "4", title: "样式四" },
|
||||
],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
firstItem() {
|
||||
@@ -75,4 +133,13 @@ export default {
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../controls/field-common.scss";
|
||||
@import "../controls/panel-common.scss";
|
||||
|
||||
.panel-coupon {
|
||||
background: #f5f7fa;
|
||||
margin: 0 -20px;
|
||||
padding: 0 12px 12px;
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,646 @@
|
||||
<template>
|
||||
<div class="panel-goods">
|
||||
<div class="panel-header">
|
||||
<span class="panel-name">{{ res.name }}</span>
|
||||
<div class="panel-tabs">
|
||||
<div
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="panel-tab"
|
||||
:class="{ active: activeTab === tab.key }"
|
||||
@click="activeTab = tab.key"
|
||||
>
|
||||
{{ tab.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 旧版数据提示:保留原渲染,手动升级后才按新版配置 -->
|
||||
<div v-if="legacy" class="panel-card panel-card-fill">
|
||||
<div class="section-title">商品列表</div>
|
||||
<div class="section-tip">
|
||||
该楼层为旧版商品列表(分类标签 + 绑定商品),当前仍按旧版展示。
|
||||
升级后可使用展现形式、购物车按钮、商品筛选等新版功能,旧的分类与商品绑定将被清空。
|
||||
</div>
|
||||
<el-button type="primary" plain @click="upgradeLegacy">升级为新版</el-button>
|
||||
</div>
|
||||
|
||||
<template v-else-if="activeTab === 'content'">
|
||||
<div class="panel-card">
|
||||
<div class="section-title">展现形式</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">展现形式</div>
|
||||
<div class="ctrl-body">
|
||||
<div class="seg layout-seg">
|
||||
<div
|
||||
v-for="opt in layoutOptions"
|
||||
:key="opt.value"
|
||||
class="seg-item seg-icon"
|
||||
:class="{ active: firstItem.layout === opt.value }"
|
||||
@click="changeLayout(opt.value)"
|
||||
>
|
||||
<span class="ly-ico" :class="'ly-' + opt.value"><i></i><i></i><i></i></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-card">
|
||||
<div class="section-title">显示内容</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">展示信息</div>
|
||||
<div class="ctrl-body">
|
||||
<el-checkbox v-model="firstItem.showName">商品名称</el-checkbox>
|
||||
<el-checkbox v-model="firstItem.showPrice">商品价格</el-checkbox>
|
||||
<el-checkbox v-model="firstItem.showSales">已售数量</el-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-card">
|
||||
<div class="section-title">购物车按钮</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">是否显示</div>
|
||||
<div class="ctrl-body">
|
||||
<div class="seg layout-seg">
|
||||
<div
|
||||
class="seg-item"
|
||||
:class="{ active: firstItem.cartShow !== false }"
|
||||
@click="firstItem.cartShow = true"
|
||||
>
|
||||
显示
|
||||
</div>
|
||||
<div
|
||||
class="seg-item"
|
||||
:class="{ active: firstItem.cartShow === false }"
|
||||
@click="firstItem.cartShow = false"
|
||||
>
|
||||
隐藏
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ctrl-row" v-if="firstItem.cartShow !== false">
|
||||
<div class="ctrl-label">按钮样式</div>
|
||||
<div class="ctrl-body">
|
||||
<div class="seg layout-seg-3">
|
||||
<div
|
||||
v-for="opt in cartStyleOptions"
|
||||
:key="opt.value"
|
||||
class="seg-item seg-icon"
|
||||
:class="{ active: firstItem.cartStyle === opt.value }"
|
||||
@click="firstItem.cartStyle = opt.value"
|
||||
>
|
||||
<span v-if="opt.value === 'text'" class="cart-text-ico">加购</span>
|
||||
<span v-else-if="opt.value === 'plus'" class="cart-plus-ico">+</span>
|
||||
<span v-else class="cart-cart-ico">
|
||||
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
|
||||
<path
|
||||
d="M7 4h-2l-1 2h2l2.4 9.6a2 2 0 0 0 1.9 1.4h7.9a2 2 0 0 0 1.9-1.5l1.6-6.5h-12l-.8-3h11.5l.5-2h-13.4l-.0-.0zM10 20a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zm8 0a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ctrl-row" v-if="firstItem.cartShow !== false">
|
||||
<div class="ctrl-label">按钮效果</div>
|
||||
<div class="ctrl-body">
|
||||
<div class="seg layout-seg">
|
||||
<div
|
||||
class="seg-item"
|
||||
:class="{ active: firstItem.cartAction !== 'add' }"
|
||||
@click="firstItem.cartAction = 'detail'"
|
||||
>
|
||||
进入商品详情页
|
||||
</div>
|
||||
<div
|
||||
class="seg-item"
|
||||
:class="{ active: firstItem.cartAction === 'add' }"
|
||||
@click="firstItem.cartAction = 'add'"
|
||||
>
|
||||
商品加购
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">商品设置</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">选择方式</div>
|
||||
<div class="ctrl-body">
|
||||
<el-radio-group v-model="firstItem.dataMode">
|
||||
<el-radio value="category">商品分类</el-radio>
|
||||
<el-radio value="brand">商品品牌</el-radio>
|
||||
<el-radio value="store">指定商户</el-radio>
|
||||
<el-radio value="goods">指定商品</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="firstItem.dataMode === 'category'">
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">商品分类</div>
|
||||
<div class="ctrl-body">
|
||||
<el-cascader
|
||||
v-model="firstItem.categoryId"
|
||||
:options="categoryTree"
|
||||
:props="cascaderProps"
|
||||
placeholder="全部分类"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<slider-field v-model="firstItem.count" label="商品数量" :min="1" :max="50" />
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">商品排序</div>
|
||||
<div class="ctrl-body">
|
||||
<div class="seg layout-seg-3">
|
||||
<div
|
||||
v-for="opt in sortOptions"
|
||||
:key="opt.value"
|
||||
class="seg-item"
|
||||
:class="{ active: firstItem.sortType === opt.value }"
|
||||
@click="firstItem.sortType = opt.value"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="firstItem.dataMode === 'brand'">
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">商品品牌</div>
|
||||
<div class="ctrl-body">
|
||||
<el-select v-model="firstItem.brandId" placeholder="全部品牌" clearable filterable>
|
||||
<el-option v-for="b in brandList" :key="b.id" :label="b.name" :value="b.id" />
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
<slider-field v-model="firstItem.count" label="商品数量" :min="1" :max="50" />
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">商品排序</div>
|
||||
<div class="ctrl-body">
|
||||
<div class="seg layout-seg-3">
|
||||
<div
|
||||
v-for="opt in sortOptions"
|
||||
:key="opt.value"
|
||||
class="seg-item"
|
||||
:class="{ active: firstItem.sortType === opt.value }"
|
||||
@click="firstItem.sortType = opt.value"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="firstItem.dataMode === 'store'">
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">指定商户</div>
|
||||
<div class="ctrl-body">
|
||||
<el-select v-model="firstItem.storeId" placeholder="全部商户" clearable filterable>
|
||||
<el-option
|
||||
v-for="s in storeList"
|
||||
:key="s.id"
|
||||
:label="s.storeName"
|
||||
:value="s.id"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
<slider-field v-model="firstItem.count" label="商品数量" :min="1" :max="50" />
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">商品排序</div>
|
||||
<div class="ctrl-body">
|
||||
<div class="seg layout-seg-3">
|
||||
<div
|
||||
v-for="opt in sortOptions"
|
||||
:key="opt.value"
|
||||
class="seg-item"
|
||||
:class="{ active: firstItem.sortType === opt.value }"
|
||||
@click="firstItem.sortType = opt.value"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="goods-wall">
|
||||
<div class="goods-wall-item" v-for="(g, i) in firstItem.goodsList" :key="g.id">
|
||||
<img :src="g.thumbnail" alt="" />
|
||||
<div class="goods-wall-del" @click="firstItem.goodsList.splice(i, 1)">
|
||||
<el-icon><Close /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="goods-wall-add" @click="pickGoods">
|
||||
<el-icon><Plus /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-tip">最多选择 50 个商品,展示顺序与选择顺序一致</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="panel-card">
|
||||
<div class="section-title">颜色设置</div>
|
||||
<gradient-field
|
||||
v-model:start="res.options.bgColor1"
|
||||
v-model:end="res.options.bgColor2"
|
||||
:default-start="defaults.bgColor1"
|
||||
:default-end="defaults.bgColor2"
|
||||
/>
|
||||
<color-field
|
||||
v-model="res.options.titleColor"
|
||||
label="商品标题颜色"
|
||||
:default-value="defaults.titleColor"
|
||||
/>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">色调</div>
|
||||
<div class="ctrl-body">
|
||||
<div class="seg layout-seg">
|
||||
<div
|
||||
class="seg-item"
|
||||
:class="{ active: res.options.toneMode !== 'custom' }"
|
||||
@click="res.options.toneMode = 'theme'"
|
||||
>
|
||||
跟随主题风格
|
||||
</div>
|
||||
<div
|
||||
class="seg-item"
|
||||
:class="{ active: res.options.toneMode === 'custom' }"
|
||||
@click="res.options.toneMode = 'custom'"
|
||||
>
|
||||
自定义
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<color-field
|
||||
v-if="res.options.toneMode === 'custom'"
|
||||
v-model="res.options.priceColor"
|
||||
label="价格颜色"
|
||||
:default-value="defaults.priceColor"
|
||||
/>
|
||||
<color-field
|
||||
v-model="res.options.salesColor"
|
||||
label="已售数量颜色"
|
||||
:default-value="defaults.salesColor"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="panel-card">
|
||||
<div class="section-title">边距设置</div>
|
||||
<slider-field v-model="res.options.paddingTop" label="上边距" :min="0" :max="100" />
|
||||
<slider-field v-model="res.options.paddingBottom" label="下边距" :min="0" :max="100" />
|
||||
<slider-field v-model="res.options.paddingX" label="左右边距" :min="0" :max="40" />
|
||||
<slider-field v-model="res.options.contentGap" label="内容间距" :min="0" :max="50" />
|
||||
<slider-field v-model="res.options.pageGap" label="页面间距" :min="0" :max="100" />
|
||||
</div>
|
||||
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">圆角设置</div>
|
||||
<slider-field v-model="res.options.borderRadius" label="背景圆角" :min="0" :max="30" />
|
||||
<slider-field v-model="res.options.contentRadius" label="内容圆角" :min="0" :max="30" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<liliDialog ref="liliDialogRef" @selectedGoodsData="onGoodsSelected"></liliDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Close, Plus } from "@element-plus/icons-vue";
|
||||
import GradientField from "../controls/GradientField.vue";
|
||||
import ColorField from "../controls/ColorField.vue";
|
||||
import SliderField from "../controls/SliderField.vue";
|
||||
import { getCategoryTree, getBrandListData } from "@/api/goods";
|
||||
import { getShopListData } from "@/api/shops";
|
||||
|
||||
const DEFAULTS = {
|
||||
bgColor1: "#FFFFFF",
|
||||
bgColor2: "#FFFFFF",
|
||||
titleColor: "#282828",
|
||||
toneMode: "theme",
|
||||
priceColor: "#FF5C58",
|
||||
salesColor: "#666666",
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
paddingX: 12,
|
||||
contentGap: 10,
|
||||
pageGap: 10,
|
||||
borderRadius: 0,
|
||||
contentRadius: 7,
|
||||
};
|
||||
const DEFAULT_ITEM = {
|
||||
layout: "list",
|
||||
showName: true,
|
||||
showPrice: true,
|
||||
showSales: true,
|
||||
cartShow: true,
|
||||
cartStyle: "plus",
|
||||
cartAction: "detail",
|
||||
dataMode: "category",
|
||||
categoryId: "",
|
||||
brandId: "",
|
||||
storeId: "",
|
||||
goodsList: [],
|
||||
count: 6,
|
||||
sortType: "default",
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "PanelGoods",
|
||||
hideDecorateTitle: true,
|
||||
components: { GradientField, ColorField, SliderField, Close, Plus },
|
||||
props: {
|
||||
res: { type: Object, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
defaults: DEFAULTS,
|
||||
activeTab: "content",
|
||||
tabs: [
|
||||
{ key: "content", title: "内容" },
|
||||
{ key: "style", title: "样式" },
|
||||
],
|
||||
legacy: false,
|
||||
layoutOptions: [
|
||||
{ value: "card", label: "卡片" },
|
||||
{ value: "list", label: "列表" },
|
||||
{ value: "three", label: "三列" },
|
||||
{ value: "big", label: "大图" },
|
||||
],
|
||||
sortOptions: [
|
||||
{ value: "default", label: "综合" },
|
||||
{ value: "sales", label: "销量" },
|
||||
{ value: "price", label: "价格" },
|
||||
],
|
||||
cascaderProps: {
|
||||
label: "name",
|
||||
value: "id",
|
||||
children: "children",
|
||||
checkStrictly: true,
|
||||
emitPath: false,
|
||||
},
|
||||
categoryTree: [],
|
||||
brandList: [],
|
||||
storeList: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
firstItem() {
|
||||
return this.res.options.list[0];
|
||||
},
|
||||
// 卡片/大图支持文字按钮,两列/三列只有图标按钮
|
||||
cartStyleOptions() {
|
||||
const base = [
|
||||
{ value: "plus", label: "圆形加号" },
|
||||
{ value: "cart", label: "购物车" },
|
||||
];
|
||||
if (this.firstItem.layout === "card" || this.firstItem.layout === "big") {
|
||||
return [{ value: "text", label: "加购文字" }, ...base];
|
||||
}
|
||||
return base;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.res.options = Object.assign({}, DEFAULTS, this.res.options);
|
||||
if (!Array.isArray(this.res.options.list) || !this.res.options.list[0]) {
|
||||
this.res.options.list = [{ ...DEFAULT_ITEM }];
|
||||
}
|
||||
const item = this.firstItem;
|
||||
if (item.layout === undefined && Array.isArray(item.titleWay)) {
|
||||
this.legacy = true;
|
||||
return;
|
||||
}
|
||||
this.fillItemDefaults(item);
|
||||
getCategoryTree().then((res) => {
|
||||
if (res && res.success) this.categoryTree = res.result || [];
|
||||
});
|
||||
getBrandListData().then((res) => {
|
||||
this.brandList = Array.isArray(res) ? res : (res && res.result) || [];
|
||||
});
|
||||
getShopListData({ pageNumber: 1, pageSize: 100, status: "OPEN" }).then((res) => {
|
||||
if (res && res.success) this.storeList = (res.result && res.result.records) || [];
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
fillItemDefaults(item) {
|
||||
Object.keys(DEFAULT_ITEM).forEach((key) => {
|
||||
if (item[key] === undefined) {
|
||||
item[key] = Array.isArray(DEFAULT_ITEM[key])
|
||||
? [...DEFAULT_ITEM[key]]
|
||||
: DEFAULT_ITEM[key];
|
||||
}
|
||||
});
|
||||
},
|
||||
upgradeLegacy() {
|
||||
const item = this.firstItem;
|
||||
delete item.titleWay;
|
||||
delete item.listWay;
|
||||
this.fillItemDefaults(item);
|
||||
this.legacy = false;
|
||||
getCategoryTree().then((res) => {
|
||||
if (res && res.success) this.categoryTree = res.result || [];
|
||||
});
|
||||
getBrandListData().then((res) => {
|
||||
this.brandList = Array.isArray(res) ? res : (res && res.result) || [];
|
||||
});
|
||||
getShopListData({ pageNumber: 1, pageSize: 100, status: "OPEN" }).then((res) => {
|
||||
if (res && res.success) this.storeList = (res.result && res.result.records) || [];
|
||||
});
|
||||
},
|
||||
changeLayout(val) {
|
||||
this.firstItem.layout = val;
|
||||
// 两列/三列不支持文字按钮,切布局时兜底
|
||||
if (
|
||||
(val === "list" || val === "three") &&
|
||||
this.firstItem.cartStyle === "text"
|
||||
) {
|
||||
this.firstItem.cartStyle = "plus";
|
||||
}
|
||||
},
|
||||
pickGoods() {
|
||||
const dialog = this.$refs.liliDialogRef;
|
||||
dialog.clearGoodsSelected();
|
||||
dialog.goodsFlag = true;
|
||||
dialog.flag = true;
|
||||
},
|
||||
onGoodsSelected(val) {
|
||||
if (!Array.isArray(val) || !val.length) return;
|
||||
const list = val.map((item) => ({
|
||||
id: item.id,
|
||||
goodsName: item.goodsName,
|
||||
price: item.price,
|
||||
thumbnail: item.thumbnail,
|
||||
}));
|
||||
const exist = new Set(this.firstItem.goodsList.map((g) => g.id));
|
||||
const merged = this.firstItem.goodsList.concat(
|
||||
list.filter((g) => !exist.has(g.id))
|
||||
);
|
||||
this.firstItem.goodsList = merged.slice(0, 50);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../controls/field-common.scss";
|
||||
@import "../controls/panel-common.scss";
|
||||
|
||||
.seg-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 40px;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
// 展现形式小图标
|
||||
.ly-ico {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
height: 14px;
|
||||
i {
|
||||
display: block;
|
||||
background: currentColor;
|
||||
border-radius: 1px;
|
||||
}
|
||||
}
|
||||
.ly-card {
|
||||
i:first-child {
|
||||
width: 8px;
|
||||
height: 10px;
|
||||
}
|
||||
i:not(:first-child) {
|
||||
width: 12px;
|
||||
height: 2px;
|
||||
}
|
||||
flex-wrap: wrap;
|
||||
width: 24px;
|
||||
align-content: center;
|
||||
}
|
||||
.ly-list {
|
||||
i {
|
||||
width: 8px;
|
||||
height: 10px;
|
||||
&:nth-child(3) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
.ly-three {
|
||||
i {
|
||||
width: 6px;
|
||||
height: 10px;
|
||||
}
|
||||
}
|
||||
.ly-big {
|
||||
i:first-child {
|
||||
width: 22px;
|
||||
height: 10px;
|
||||
}
|
||||
i:not(:first-child) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// 购物车按钮样式小图标
|
||||
.cart-text-ico {
|
||||
display: inline-block;
|
||||
padding: 1px 8px;
|
||||
border-radius: 8px;
|
||||
background: #c0c4cc;
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
line-height: 14px;
|
||||
}
|
||||
.cart-plus-ico {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: #c0c4cc;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
}
|
||||
.seg-item.active .cart-text-ico,
|
||||
.seg-item.active .cart-plus-ico {
|
||||
background: #409eff;
|
||||
}
|
||||
.cart-cart-ico {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.goods-wall {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.goods-wall-item {
|
||||
position: relative;
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #ebeef5;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
.goods-wall-del {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
.goods-wall-add {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border: 1px dashed #dcdfe6;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #909399;
|
||||
font-size: 22px;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
border-color: #409eff;
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,63 +1,227 @@
|
||||
<template>
|
||||
<div class="panel-integral">
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">标题</div>
|
||||
<div class="ctrl-body">
|
||||
<el-input v-model="firstItem.title" style="width: 200px" />
|
||||
<div class="panel-header">
|
||||
<span class="panel-name">{{ res.name }}</span>
|
||||
<div class="panel-tabs">
|
||||
<div
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="panel-tab"
|
||||
:class="{ active: activeTab === tab.key }"
|
||||
@click="activeTab = tab.key"
|
||||
>
|
||||
{{ tab.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<slider-field
|
||||
v-model="firstItem.count"
|
||||
label="显示数量"
|
||||
:min="1"
|
||||
:max="20"
|
||||
/>
|
||||
<color-field
|
||||
v-model="res.options.bgColor"
|
||||
label="背景颜色"
|
||||
:default-value="defaults.bgColor"
|
||||
/>
|
||||
<slider-field
|
||||
v-model="res.options.paddingX"
|
||||
label="左右边距"
|
||||
:min="0"
|
||||
:max="30"
|
||||
/>
|
||||
<slider-field
|
||||
v-model="res.options.pageGap"
|
||||
label="页面间距"
|
||||
:min="0"
|
||||
:max="100"
|
||||
/>
|
||||
<slider-field
|
||||
v-model="res.options.borderRadius"
|
||||
label="背景圆角"
|
||||
:min="0"
|
||||
:max="30"
|
||||
/>
|
||||
|
||||
<template v-if="activeTab === 'content'">
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">头部设置</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">选择风格</div>
|
||||
<div class="ctrl-body">
|
||||
<el-radio-group v-model="res.options.headBgType">
|
||||
<el-radio value="image">背景图片</el-radio>
|
||||
<el-radio value="color">背景色</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
<gradient-field
|
||||
v-if="res.options.headBgType === 'color'"
|
||||
v-model:start="res.options.headBg1"
|
||||
v-model:end="res.options.headBg2"
|
||||
label="背景颜色"
|
||||
:default-start="defaults.headBg1"
|
||||
:default-end="defaults.headBg2"
|
||||
/>
|
||||
<div class="ctrl-row" v-else>
|
||||
<div class="ctrl-label">背景图片</div>
|
||||
<div class="ctrl-body">
|
||||
<div class="bg-preview" v-if="res.options.headBgImage">
|
||||
<img :src="res.options.headBgImage" alt="" />
|
||||
</div>
|
||||
<el-button plain type="primary" @click="pickFile('headBgImage')">上传背景图</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-tip" v-if="res.options.headBgType !== 'color'">建议 710px*96px</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">标题类型</div>
|
||||
<div class="ctrl-body">
|
||||
<el-radio-group v-model="res.options.titleType">
|
||||
<el-radio value="image">图片</el-radio>
|
||||
<el-radio value="text">文字</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ctrl-row" v-if="res.options.titleType === 'image'">
|
||||
<div class="ctrl-label">标题图片</div>
|
||||
<div class="ctrl-body">
|
||||
<div class="icon-preview" v-if="res.options.titleImage">
|
||||
<img :src="res.options.titleImage" alt="" />
|
||||
</div>
|
||||
<el-button plain type="primary" @click="pickFile('titleImage')">上传标题图</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ctrl-row" v-else>
|
||||
<div class="ctrl-label">标题文字</div>
|
||||
<div class="ctrl-body">
|
||||
<el-input v-model="firstItem.title" maxlength="10" placeholder="积分兑好礼" />
|
||||
</div>
|
||||
</div>
|
||||
<slider-field v-model="firstItem.count" label="展示数量" :min="1" :max="20" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="panel-card">
|
||||
<div class="section-title">布局设置</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">展示样式</div>
|
||||
<div class="ctrl-body">
|
||||
<div class="seg layout-seg-3">
|
||||
<div
|
||||
v-for="opt in layoutOptions"
|
||||
:key="opt.value"
|
||||
class="seg-item"
|
||||
:class="{ active: (res.options.layoutStyle || '1') === opt.value }"
|
||||
@click="res.options.layoutStyle = opt.value"
|
||||
>
|
||||
{{ opt.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-card">
|
||||
<div class="section-title">颜色设置</div>
|
||||
<gradient-field
|
||||
v-model:start="res.options.bgColor1"
|
||||
v-model:end="res.options.bgColor2"
|
||||
label="内容背景"
|
||||
:default-start="defaults.bgColor1"
|
||||
:default-end="defaults.bgColor2"
|
||||
/>
|
||||
<color-field
|
||||
v-model="res.options.pointColor"
|
||||
label="积分价格颜色"
|
||||
:default-value="defaults.pointColor"
|
||||
/>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">标题文字</div>
|
||||
<div class="ctrl-body">
|
||||
<div class="seg layout-seg-3">
|
||||
<div
|
||||
v-for="opt in titleStyleOptions"
|
||||
:key="opt.value"
|
||||
class="seg-item"
|
||||
:class="{ active: (res.options.titleStyle || 'bold') === opt.value }"
|
||||
@click="res.options.titleStyle = opt.value"
|
||||
>
|
||||
{{ opt.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<color-field
|
||||
v-model="res.options.titleColor"
|
||||
label="标题颜色"
|
||||
:default-value="defaults.titleColor"
|
||||
/>
|
||||
<color-field
|
||||
v-model="res.options.moreColor"
|
||||
label="更多按钮颜色"
|
||||
:default-value="defaults.moreColor"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="panel-card">
|
||||
<div class="section-title">边距设置</div>
|
||||
<slider-field v-model="res.options.paddingTop" label="上边距" :min="0" :max="100" />
|
||||
<slider-field v-model="res.options.paddingBottom" label="下边距" :min="0" :max="100" />
|
||||
<slider-field v-model="res.options.paddingX" label="左右边距" :min="0" :max="40" />
|
||||
<slider-field v-model="res.options.contentGap" label="内容间距" :min="0" :max="20" />
|
||||
<slider-field v-model="res.options.pageGap" label="页面间距" :min="0" :max="100" />
|
||||
</div>
|
||||
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">圆角设置</div>
|
||||
<slider-field v-model="res.options.borderRadius" label="背景圆角" :min="0" :max="30" />
|
||||
<slider-field v-model="res.options.imgRadius" label="图片圆角" :min="0" :max="30" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-dialog v-model="picModelFlag" width="1200px" append-to-body destroy-on-close>
|
||||
<ossManage
|
||||
@callback="callbackSelected"
|
||||
:is-component="true"
|
||||
:initialize="picModelFlag"
|
||||
:max-select="1"
|
||||
ref="ossManage"
|
||||
/>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import GradientField from "../controls/GradientField.vue";
|
||||
import ColorField from "../controls/ColorField.vue";
|
||||
import SliderField from "../controls/SliderField.vue";
|
||||
import ossManage from "@/views/sys/oss-manage/ossManage";
|
||||
|
||||
const DEFAULTS = {
|
||||
bgColor: "#FFFFFF",
|
||||
paddingX: 0,
|
||||
pageGap: 10,
|
||||
borderRadius: 0,
|
||||
layoutStyle: "1",
|
||||
pointColor: "#F5483A",
|
||||
headBgType: "color",
|
||||
headBg1: "#FF6A00",
|
||||
headBg2: "#FF2200",
|
||||
headBgImage: "",
|
||||
titleType: "text",
|
||||
titleImage: "",
|
||||
titleStyle: "bold",
|
||||
titleColor: "#FFFFFF",
|
||||
moreColor: "#FFFFFF",
|
||||
bgColor1: "#FFFFFF",
|
||||
bgColor2: "#FFFFFF",
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
paddingX: 12,
|
||||
contentGap: 12,
|
||||
pageGap: 0,
|
||||
borderRadius: 7,
|
||||
imgRadius: 5,
|
||||
};
|
||||
const DEFAULT_ITEM = { title: "积分商城", count: 3 };
|
||||
const DEFAULT_ITEM = { title: "积分兑好礼", count: 3 };
|
||||
|
||||
export default {
|
||||
name: "PanelIntegral",
|
||||
components: { ColorField, SliderField },
|
||||
hideDecorateTitle: true,
|
||||
components: { GradientField, ColorField, SliderField, ossManage },
|
||||
props: {
|
||||
res: { type: Object, required: true },
|
||||
},
|
||||
data() {
|
||||
return { defaults: DEFAULTS };
|
||||
return {
|
||||
defaults: DEFAULTS,
|
||||
activeTab: "content",
|
||||
tabs: [
|
||||
{ key: "content", title: "内容" },
|
||||
{ key: "style", title: "样式" },
|
||||
],
|
||||
titleStyleOptions: [
|
||||
{ value: "bold", title: "加粗" },
|
||||
{ value: "normal", title: "正常" },
|
||||
{ value: "italic", title: "倾斜" },
|
||||
],
|
||||
layoutOptions: [
|
||||
{ value: "1", title: "样式一" },
|
||||
{ value: "2", title: "样式二" },
|
||||
{ value: "3", title: "样式三" },
|
||||
],
|
||||
picModelFlag: false,
|
||||
currentField: "",
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
firstItem() {
|
||||
@@ -70,9 +234,70 @@ export default {
|
||||
this.res.options.list = [{ ...DEFAULT_ITEM }];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
pickFile(field) {
|
||||
this.currentField = field;
|
||||
this.picModelFlag = true;
|
||||
this.$nextTick(() => {
|
||||
const oss = this.$refs.ossManage;
|
||||
if (!oss) return;
|
||||
oss.selectImage = true;
|
||||
oss.fileType = "pic";
|
||||
oss.changeFileType();
|
||||
});
|
||||
},
|
||||
callbackSelected(val) {
|
||||
this.picModelFlag = false;
|
||||
if (val && val.url && this.currentField) {
|
||||
this.res.options[this.currentField] = val.url;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../controls/field-common.scss";
|
||||
@import "../controls/panel-common.scss";
|
||||
|
||||
.panel-integral {
|
||||
background: #f5f7fa;
|
||||
margin: 0 -20px;
|
||||
padding: 0 12px 12px;
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.bg-preview {
|
||||
width: 132px;
|
||||
height: 30px;
|
||||
border: 1px dashed #dcdfe6;
|
||||
border-radius: 6px;
|
||||
padding: 2px;
|
||||
box-sizing: border-box;
|
||||
margin-right: 10px;
|
||||
flex-shrink: 0;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
.icon-preview {
|
||||
width: 88px;
|
||||
height: 24px;
|
||||
border: 1px dashed #dcdfe6;
|
||||
border-radius: 6px;
|
||||
padding: 2px;
|
||||
box-sizing: border-box;
|
||||
margin-right: 10px;
|
||||
flex-shrink: 0;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,48 +1,48 @@
|
||||
<template>
|
||||
<div class="panel-line">
|
||||
<color-field
|
||||
v-model="res.options.bgColor"
|
||||
label="背景颜色"
|
||||
:default-value="defaults.bgColor"
|
||||
/>
|
||||
<color-field
|
||||
v-model="res.options.lineColor"
|
||||
label="线条颜色"
|
||||
:default-value="defaults.lineColor"
|
||||
/>
|
||||
<segment-field
|
||||
v-model="res.options.lineStyle"
|
||||
label="线条样式"
|
||||
:options="lineStyleOptions"
|
||||
/>
|
||||
<slider-field
|
||||
v-model="res.options.lineHeight"
|
||||
label="线条高度"
|
||||
:min="1"
|
||||
:max="100"
|
||||
/>
|
||||
<slider-field
|
||||
v-model="res.options.paddingX"
|
||||
label="左右边距"
|
||||
:min="0"
|
||||
:max="100"
|
||||
/>
|
||||
<slider-field
|
||||
v-model="res.options.pageGap"
|
||||
label="页面间距"
|
||||
:min="0"
|
||||
:max="100"
|
||||
/>
|
||||
<div class="panel-header">
|
||||
<span class="panel-name">{{ res.name }}</span>
|
||||
</div>
|
||||
|
||||
<div class="panel-card">
|
||||
<div class="section-title">颜色设置</div>
|
||||
<gradient-field
|
||||
v-model:start="res.options.bgColor1"
|
||||
v-model:end="res.options.bgColor2"
|
||||
:default-start="defaults.bgColor1"
|
||||
:default-end="defaults.bgColor2"
|
||||
/>
|
||||
<color-field
|
||||
v-model="res.options.lineColor"
|
||||
label="线条颜色"
|
||||
:default-value="defaults.lineColor"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">样式设置</div>
|
||||
<segment-field
|
||||
v-model="res.options.lineStyle"
|
||||
label="线条样式"
|
||||
:options="lineStyleOptions"
|
||||
/>
|
||||
<slider-field v-model="res.options.lineHeight" label="线条高度" :min="1" :max="100" />
|
||||
<slider-field v-model="res.options.paddingX" label="左右边距" :min="0" :max="40" />
|
||||
<slider-field v-model="res.options.pageGap" label="页面间距" :min="0" :max="100" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ColorField from "../controls/ColorField.vue";
|
||||
import GradientField from "../controls/GradientField.vue";
|
||||
import SliderField from "../controls/SliderField.vue";
|
||||
import SegmentField from "../controls/SegmentField.vue";
|
||||
|
||||
const DEFAULTS = {
|
||||
bgColor: "#FFFFFF",
|
||||
bgColor1: "#FFFFFF",
|
||||
bgColor2: "#FFFFFF",
|
||||
lineColor: "#979797",
|
||||
lineStyle: "dashed",
|
||||
lineHeight: 1,
|
||||
@@ -52,7 +52,8 @@ const DEFAULTS = {
|
||||
|
||||
export default {
|
||||
name: "PanelLine",
|
||||
components: { ColorField, SliderField, SegmentField },
|
||||
hideDecorateTitle: true,
|
||||
components: { ColorField, GradientField, SliderField, SegmentField },
|
||||
props: {
|
||||
res: { type: Object, required: true },
|
||||
},
|
||||
@@ -71,3 +72,15 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../controls/panel-common.scss";
|
||||
|
||||
.panel-line {
|
||||
background: #f5f7fa;
|
||||
margin: 0 -20px;
|
||||
padding: 0 12px 12px;
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
</div>
|
||||
|
||||
<template v-if="activeTab === 'content'">
|
||||
<div class="panel-card">
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">菜单设置</div>
|
||||
<div class="section-tip">每行展示 5 个,建议图标比例一致</div>
|
||||
<div class="section-tip">推荐尺寸:88*88px(1:1)</div>
|
||||
<div class="menu-item-card" v-for="(item, index) in res.options.list" :key="index">
|
||||
<div class="img-item-head">
|
||||
<span>菜单 {{ index + 1 }}</span>
|
||||
@@ -42,10 +42,10 @@
|
||||
</div>
|
||||
<div class="icon-placeholder" v-else>图标</div>
|
||||
<div class="menu-item-fields">
|
||||
<el-input v-model="item.title" size="small" maxlength="6" placeholder="菜单名称" />
|
||||
<el-input v-model="item.title" maxlength="6" placeholder="菜单名称" />
|
||||
<div class="btn-row">
|
||||
<el-button plain type="primary" size="small" @click="pickImage(item)">选择图标</el-button>
|
||||
<el-button plain type="primary" size="small" @click="pickLink(item)">选择链接</el-button>
|
||||
<el-button plain type="primary" @click="pickImage(item)">选择图标</el-button>
|
||||
<el-button plain type="primary" @click="pickLink(item)">选择链接</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -58,19 +58,71 @@
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="panel-card">
|
||||
<div class="section-title">展示设置</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">展示样式</div>
|
||||
<div class="ctrl-body">
|
||||
<div class="seg layout-seg">
|
||||
<div
|
||||
class="seg-item"
|
||||
:class="{ active: res.options.menuDisplay === 'single' }"
|
||||
@click="res.options.menuDisplay = 'single'"
|
||||
>
|
||||
单行
|
||||
</div>
|
||||
<div
|
||||
class="seg-item"
|
||||
:class="{ active: res.options.menuDisplay !== 'single' }"
|
||||
@click="res.options.menuDisplay = 'multi'"
|
||||
>
|
||||
多行
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ctrl-row" v-if="res.options.menuDisplay !== 'single'">
|
||||
<div class="ctrl-label">显示行数</div>
|
||||
<div class="ctrl-body">
|
||||
<div class="seg layout-seg-3">
|
||||
<div
|
||||
v-for="n in [2, 3, 4]"
|
||||
:key="n"
|
||||
class="seg-item"
|
||||
:class="{ active: res.options.menuRows === n }"
|
||||
@click="res.options.menuRows = n"
|
||||
>
|
||||
{{ n }}行
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">显示个数</div>
|
||||
<div class="ctrl-body">
|
||||
<div class="seg layout-seg-3">
|
||||
<div
|
||||
v-for="n in [3, 4, 5]"
|
||||
:key="n"
|
||||
class="seg-item"
|
||||
:class="{ active: res.options.menuCols === n }"
|
||||
@click="res.options.menuCols = n"
|
||||
>
|
||||
{{ n }}个
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-card">
|
||||
<div class="section-title">颜色设置</div>
|
||||
<color-field
|
||||
v-model="res.options.bgColor1"
|
||||
label="背景颜色"
|
||||
:default-value="defaults.bgColor1"
|
||||
<gradient-field
|
||||
v-model:start="res.options.bgColor1"
|
||||
v-model:end="res.options.bgColor2"
|
||||
:default-start="defaults.bgColor1"
|
||||
:default-end="defaults.bgColor2"
|
||||
/>
|
||||
<color-field
|
||||
v-model="res.options.bgColor2"
|
||||
label="渐变结束色"
|
||||
:default-value="defaults.bgColor2"
|
||||
/>
|
||||
<div class="section-tip">起始色与结束色一致时为纯色背景</div>
|
||||
<color-field
|
||||
v-model="res.options.textColor"
|
||||
label="文字颜色"
|
||||
@@ -82,15 +134,15 @@
|
||||
<div class="section-title">边距设置</div>
|
||||
<slider-field v-model="res.options.paddingTop" label="上边距" :min="0" :max="100" />
|
||||
<slider-field v-model="res.options.paddingBottom" label="下边距" :min="0" :max="100" />
|
||||
<slider-field v-model="res.options.paddingX" label="左右边距" :min="0" :max="30" />
|
||||
<slider-field v-model="res.options.contentGap" label="内容间距" :min="0" :max="50" />
|
||||
<slider-field v-model="res.options.paddingX" label="左右边距" :min="0" :max="40" />
|
||||
<slider-field v-model="res.options.contentGap" label="内容间距" :min="0" :max="20" />
|
||||
<slider-field v-model="res.options.pageGap" label="页面间距" :min="0" :max="100" />
|
||||
</div>
|
||||
|
||||
<div class="panel-card">
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">圆角设置</div>
|
||||
<slider-field v-model="res.options.borderRadius" label="背景圆角" :min="0" :max="50" />
|
||||
<slider-field v-model="res.options.imgRadius" label="内容圆角" :min="0" :max="50" />
|
||||
<slider-field v-model="res.options.borderRadius" label="背景圆角" :min="0" :max="30" />
|
||||
<slider-field v-model="res.options.imgRadius" label="内容圆角" :min="0" :max="30" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -111,11 +163,15 @@
|
||||
<script>
|
||||
import { Top, Bottom, Delete } from "@element-plus/icons-vue";
|
||||
import ColorField from "../controls/ColorField.vue";
|
||||
import GradientField from "../controls/GradientField.vue";
|
||||
import SliderField from "../controls/SliderField.vue";
|
||||
import ossManage from "@/views/sys/oss-manage/ossManage";
|
||||
import ways from "@/components/lili-dialog/wap.js";
|
||||
|
||||
const DEFAULTS = {
|
||||
menuDisplay: "single",
|
||||
menuRows: 3,
|
||||
menuCols: 4,
|
||||
bgColor1: "#FFFFFF",
|
||||
bgColor2: "#FFFFFF",
|
||||
textColor: "#282828",
|
||||
@@ -131,7 +187,7 @@ const DEFAULTS = {
|
||||
export default {
|
||||
name: "PanelMenu",
|
||||
hideDecorateTitle: true,
|
||||
components: { ColorField, SliderField, ossManage, Top, Bottom, Delete },
|
||||
components: { ColorField, GradientField, SliderField, ossManage, Top, Bottom, Delete },
|
||||
props: {
|
||||
res: { type: Object, required: true },
|
||||
},
|
||||
@@ -216,11 +272,13 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../controls/field-common.scss";
|
||||
@import "../controls/panel-common.scss";
|
||||
.panel-menu {
|
||||
background: #f5f7fa;
|
||||
margin: 0 -20px -120px;
|
||||
padding: 0 12px 120px;
|
||||
min-height: calc(100vh - 70px);
|
||||
margin: 0 -20px;
|
||||
padding: 0 12px 12px;
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.panel-header {
|
||||
@@ -235,7 +293,7 @@ export default {
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
|
||||
box-sizing: border-box;
|
||||
.panel-name {
|
||||
font-size: 14px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
}
|
||||
@@ -268,13 +326,13 @@ export default {
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
.section-title {
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
.section-tip {
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
@@ -288,7 +346,7 @@ export default {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
margin-bottom: 8px;
|
||||
.img-item-ops {
|
||||
@@ -313,14 +371,15 @@ export default {
|
||||
}
|
||||
.menu-item-body {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
gap: 12px;
|
||||
align-items: stretch;
|
||||
}
|
||||
.icon-preview {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border: 1px dashed #dcdfe6;
|
||||
border-radius: 8px;
|
||||
padding: 3px;
|
||||
padding: 4px;
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
img {
|
||||
@@ -331,15 +390,15 @@ export default {
|
||||
}
|
||||
}
|
||||
.icon-placeholder {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border: 1px dashed #dcdfe6;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.menu-item-fields {
|
||||
@@ -347,11 +406,16 @@ export default {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
.btn-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
.el-button {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
.link-view {
|
||||
margin-top: 8px;
|
||||
|
||||
@@ -0,0 +1,427 @@
|
||||
<template>
|
||||
<div class="panel-notice">
|
||||
<div class="panel-header">
|
||||
<span class="panel-name">{{ res.name }}</span>
|
||||
<div class="panel-tabs">
|
||||
<div
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="panel-tab"
|
||||
:class="{ active: activeTab === tab.key }"
|
||||
@click="activeTab = tab.key"
|
||||
>
|
||||
{{ tab.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="activeTab === 'content'">
|
||||
<div class="panel-card">
|
||||
<div class="section-title">图标设置</div>
|
||||
<div class="section-tip">支持上传 1 张图片,建议宽度 130*36px</div>
|
||||
<div class="icon-row">
|
||||
<div class="icon-preview" v-if="item.img">
|
||||
<img :src="item.img" alt="" />
|
||||
</div>
|
||||
<div class="icon-placeholder" v-else>图标</div>
|
||||
<el-button plain type="primary" @click="pickImage">上传图标</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">公告设置</div>
|
||||
<div class="section-tip">最多可添加 10 个版块</div>
|
||||
<div class="notice-item" v-for="(notice, index) in item.title" :key="index">
|
||||
<div class="notice-item-head">
|
||||
<span>版块 {{ index + 1 }}</span>
|
||||
<div class="img-item-ops">
|
||||
<el-icon :class="{ disabled: index === 0 }" @click="moveItem(index, -1)"><Top /></el-icon>
|
||||
<el-icon
|
||||
:class="{ disabled: index === item.title.length - 1 }"
|
||||
@click="moveItem(index, 1)"
|
||||
><Bottom
|
||||
/></el-icon>
|
||||
<el-icon class="del" @click="removeItem(index)"><Delete /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<div class="field-label">标题</div>
|
||||
<el-input v-model="notice.context" maxlength="50" placeholder="请输入公告内容" />
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<div class="field-label">链接</div>
|
||||
<div class="link-text" v-if="notice.url && notice.url.___type">{{ linkText(notice.url) }}</div>
|
||||
<el-button plain type="primary" @click="pickLink(notice)">选择链接</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-button
|
||||
v-if="item.title.length < 10"
|
||||
class="add-btn"
|
||||
plain
|
||||
type="primary"
|
||||
@click="addItem"
|
||||
>+ 添加模块</el-button
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="panel-card">
|
||||
<div class="section-title">文字设置</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">滚动方向</div>
|
||||
<div class="ctrl-body">
|
||||
<div class="seg layout-seg">
|
||||
<div
|
||||
class="seg-item"
|
||||
:class="{ active: item.direction !== 'horizontal' }"
|
||||
@click="item.direction = 'vertical'"
|
||||
>
|
||||
上下
|
||||
</div>
|
||||
<div
|
||||
class="seg-item"
|
||||
:class="{ active: item.direction === 'horizontal' }"
|
||||
@click="item.direction = 'horizontal'"
|
||||
>
|
||||
左右
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">文本位置</div>
|
||||
<div class="ctrl-body">
|
||||
<div class="seg layout-seg-3">
|
||||
<div
|
||||
v-for="opt in alignOptions"
|
||||
:key="opt.value"
|
||||
class="seg-item"
|
||||
:class="{ active: (item.textAlign || 'left') === opt.value }"
|
||||
@click="item.textAlign = opt.value"
|
||||
>
|
||||
{{ opt.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<color-field
|
||||
v-model="item.color"
|
||||
label="文字颜色"
|
||||
:default-value="defaults.itemColor"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="panel-card">
|
||||
<div class="section-title">颜色设置</div>
|
||||
<gradient-field
|
||||
v-model:start="res.options.bgColor1"
|
||||
v-model:end="res.options.bgColor2"
|
||||
:default-start="defaults.bgColor1"
|
||||
:default-end="defaults.bgColor2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="panel-card">
|
||||
<div class="section-title">边距设置</div>
|
||||
<slider-field v-model="res.options.paddingTop" label="上边距" :min="0" :max="100" />
|
||||
<slider-field v-model="res.options.paddingBottom" label="下边距" :min="0" :max="100" />
|
||||
<slider-field v-model="res.options.paddingX" label="左右边距" :min="0" :max="40" />
|
||||
<slider-field v-model="res.options.pageGap" label="页面间距" :min="0" :max="100" />
|
||||
</div>
|
||||
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">圆角设置</div>
|
||||
<slider-field v-model="res.options.borderRadius" label="背景圆角" :min="0" :max="30" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<liliDialog ref="liliDialog" @selectedLink="selectedLink"></liliDialog>
|
||||
|
||||
<el-dialog v-model="picModelFlag" width="1200px" append-to-body destroy-on-close>
|
||||
<ossManage
|
||||
@callback="callbackSelected"
|
||||
:is-component="true"
|
||||
:initialize="picModelFlag"
|
||||
:max-select="1"
|
||||
ref="ossManage"
|
||||
/>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Top, Bottom, Delete } from "@element-plus/icons-vue";
|
||||
import ColorField from "../controls/ColorField.vue";
|
||||
import GradientField from "../controls/GradientField.vue";
|
||||
import SliderField from "../controls/SliderField.vue";
|
||||
import ossManage from "@/views/sys/oss-manage/ossManage";
|
||||
import ways from "@/components/lili-dialog/wap.js";
|
||||
|
||||
const DEFAULTS = {
|
||||
bgColor1: "#FFFFFF",
|
||||
bgColor2: "#FFFFFF",
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
paddingX: 12,
|
||||
pageGap: 10,
|
||||
borderRadius: 0,
|
||||
itemColor: "#333333",
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "PanelNotice",
|
||||
hideDecorateTitle: true,
|
||||
components: { ColorField, GradientField, SliderField, ossManage, Top, Bottom, Delete },
|
||||
props: {
|
||||
res: { type: Object, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
defaults: DEFAULTS,
|
||||
activeTab: "content",
|
||||
tabs: [
|
||||
{ key: "content", title: "内容" },
|
||||
{ key: "style", title: "样式" },
|
||||
],
|
||||
alignOptions: [
|
||||
{ value: "left", title: "居左" },
|
||||
{ value: "center", title: "居中" },
|
||||
{ value: "right", title: "居右" },
|
||||
],
|
||||
picModelFlag: false,
|
||||
currentItem: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
item() {
|
||||
return this.res.options.list[0];
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.res.options = Object.assign({}, DEFAULTS, this.res.options);
|
||||
if (!Array.isArray(this.res.options.list) || !this.res.options.list.length) {
|
||||
this.res.options.list = [{ title: [], color: "#333333", direction: "vertical", img: "" }];
|
||||
}
|
||||
const item = this.item;
|
||||
if (!Array.isArray(item.title) || !item.title.length) {
|
||||
item.title = [{ context: "这是一条公告", url: "" }];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
linkText(url) {
|
||||
const way = ways.find((e) => e.name === url.___type);
|
||||
const typeTitle = way ? way.title : "发现";
|
||||
const name =
|
||||
url.name || url.goodsName || url.memberName || url.title || "";
|
||||
return name ? `${typeTitle} - ${name}` : typeTitle;
|
||||
},
|
||||
pickImage() {
|
||||
this.picModelFlag = true;
|
||||
this.$nextTick(() => {
|
||||
const oss = this.$refs.ossManage;
|
||||
if (!oss) return;
|
||||
oss.selectImage = true;
|
||||
oss.fileType = "pic";
|
||||
oss.changeFileType();
|
||||
});
|
||||
},
|
||||
callbackSelected(val) {
|
||||
this.picModelFlag = false;
|
||||
if (val && val.url) {
|
||||
this.item.img = val.url;
|
||||
}
|
||||
},
|
||||
pickLink(notice) {
|
||||
this.currentItem = notice;
|
||||
const dialog = this.$refs.liliDialog;
|
||||
dialog.clearGoodsSelected();
|
||||
dialog.goodsFlag = false;
|
||||
dialog.flag = true;
|
||||
},
|
||||
selectedLink(val) {
|
||||
if (!this.currentItem) return;
|
||||
delete val.selected;
|
||||
delete val.intro;
|
||||
delete val.mobileIntro;
|
||||
this.currentItem.url = val;
|
||||
},
|
||||
moveItem(index, dir) {
|
||||
const list = this.item.title;
|
||||
const target = index + dir;
|
||||
if (target < 0 || target >= list.length) return;
|
||||
list.splice(target, 1, ...list.splice(index, 1, list[target]));
|
||||
},
|
||||
removeItem(index) {
|
||||
this.item.title.splice(index, 1);
|
||||
},
|
||||
addItem() {
|
||||
this.item.title.push({ context: "", url: "" });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../controls/field-common.scss";
|
||||
@import "../controls/panel-common.scss";
|
||||
.panel-notice {
|
||||
background: #f5f7fa;
|
||||
margin: 0 -20px;
|
||||
padding: 0 12px 12px;
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 50px;
|
||||
margin-bottom: 12px;
|
||||
padding: 0 14px;
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
|
||||
box-sizing: border-box;
|
||||
.panel-name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
.panel-tabs {
|
||||
display: inline-flex;
|
||||
background: #f4f4f5;
|
||||
border-radius: 8px;
|
||||
padding: 3px;
|
||||
.panel-tab {
|
||||
padding: 5px 14px;
|
||||
font-size: 13px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
color: #606266;
|
||||
transition: all 0.2s ease;
|
||||
&.active {
|
||||
background: #ffffff;
|
||||
color: #409eff;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
}
|
||||
}
|
||||
.panel-card {
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
padding: 14px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
.section-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
.section-tip {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.icon-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.icon-preview {
|
||||
width: 88px;
|
||||
height: 32px;
|
||||
border: 1px dashed #dcdfe6;
|
||||
border-radius: 6px;
|
||||
padding: 3px;
|
||||
box-sizing: border-box;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
.icon-placeholder {
|
||||
width: 88px;
|
||||
height: 32px;
|
||||
border: 1px dashed #dcdfe6;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
}
|
||||
.notice-item {
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
.notice-item-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
.img-item-ops {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
.el-icon {
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
color: #606266;
|
||||
&:hover {
|
||||
color: #409eff;
|
||||
}
|
||||
&.del:hover {
|
||||
color: #e1251b;
|
||||
}
|
||||
&.disabled {
|
||||
color: #dcdfe6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
.field-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.field-label {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
margin-right: 10px;
|
||||
flex-shrink: 0;
|
||||
width: 32px;
|
||||
}
|
||||
.el-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.el-button {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
.link-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.add-btn {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,213 @@
|
||||
<template>
|
||||
<div class="panel-page-setting">
|
||||
<div class="panel-header">
|
||||
<span class="panel-name">页面设置</span>
|
||||
<div class="panel-tabs">
|
||||
<div
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="panel-tab"
|
||||
:class="{ active: activeTab === tab.key }"
|
||||
@click="activeTab = tab.key"
|
||||
>
|
||||
{{ tab.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="activeTab === 'content'">
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">基础设置</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label"><span class="required-star">*</span>模板名称</div>
|
||||
<div class="ctrl-body">
|
||||
<el-input v-model="res.name" maxlength="30" placeholder="请输入模板名称" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label"><span class="required-star">*</span>页面标题</div>
|
||||
<div class="ctrl-body">
|
||||
<el-input v-model="res.title" maxlength="30" placeholder="请输入页面标题" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">颜色设置</div>
|
||||
<segment-field v-model="res.bgType" label="背景设置" :options="bgTypeOptions" />
|
||||
<color-field
|
||||
v-if="res.bgType === 'color'"
|
||||
v-model="res.bgColor"
|
||||
label="背景颜色"
|
||||
:default-value="defaults.bgColor"
|
||||
/>
|
||||
<template v-else>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">背景图片</div>
|
||||
<div class="ctrl-body bg-image-body">
|
||||
<div class="bg-image-preview" v-if="res.bgImage">
|
||||
<img :src="res.bgImage" alt="" />
|
||||
<el-icon class="bg-image-clear" @click="clearImage"><Close /></el-icon>
|
||||
</div>
|
||||
<el-button plain type="primary" @click="pickImage">上传图片</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-tip upload-tip">上传图片 建议:宽度750px</div>
|
||||
<segment-field v-model="res.bgFill" label="填充方式" :options="bgFillOptions" />
|
||||
</template>
|
||||
<color-field
|
||||
v-model="res.topColor"
|
||||
label="顶部颜色"
|
||||
:default-value="defaults.topColor"
|
||||
/>
|
||||
<segment-field v-model="res.titleColor" label="标题颜色" :options="titleColorOptions" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-dialog v-model="picModelFlag" width="1200px" append-to-body destroy-on-close>
|
||||
<ossManage
|
||||
@callback="callbackSelected"
|
||||
:is-component="true"
|
||||
:initialize="picModelFlag"
|
||||
:max-select="1"
|
||||
ref="ossManage"
|
||||
/>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ColorField from "../controls/ColorField.vue";
|
||||
import SegmentField from "../controls/SegmentField.vue";
|
||||
import ossManage from "@/views/sys/oss-manage/ossManage";
|
||||
import { Close } from "@element-plus/icons-vue";
|
||||
|
||||
const DEFAULTS = {
|
||||
name: "",
|
||||
title: "",
|
||||
bgType: "color",
|
||||
bgColor: "#F5F5F5",
|
||||
bgImage: "",
|
||||
bgFill: "stretch",
|
||||
topColor: "#ffffff",
|
||||
titleColor: "black",
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "PanelPageSetting",
|
||||
components: { ColorField, SegmentField, ossManage, Close },
|
||||
props: {
|
||||
res: { type: Object, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
defaults: DEFAULTS,
|
||||
activeTab: "content",
|
||||
tabs: [
|
||||
{ key: "content", title: "内容" },
|
||||
{ key: "style", title: "样式" },
|
||||
],
|
||||
bgTypeOptions: [
|
||||
{ label: "背景色", value: "color" },
|
||||
{ label: "背景图", value: "image" },
|
||||
],
|
||||
bgFillOptions: [
|
||||
{ label: "拉伸", value: "stretch" },
|
||||
{ label: "缩放", value: "contain" },
|
||||
{ label: "填充", value: "cover" },
|
||||
],
|
||||
titleColorOptions: [
|
||||
{ label: "白色", value: "white" },
|
||||
{ label: "黑色", value: "black" },
|
||||
],
|
||||
picModelFlag: false,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
Object.keys(DEFAULTS).forEach((key) => {
|
||||
if (this.res[key] === undefined) {
|
||||
this.res[key] = DEFAULTS[key];
|
||||
}
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
pickImage() {
|
||||
this.picModelFlag = true;
|
||||
this.$nextTick(() => {
|
||||
const oss = this.$refs.ossManage;
|
||||
if (!oss) return;
|
||||
oss.selectImage = true;
|
||||
oss.fileType = "pic";
|
||||
oss.changeFileType();
|
||||
});
|
||||
},
|
||||
callbackSelected(val) {
|
||||
this.picModelFlag = false;
|
||||
if (val && val.url) {
|
||||
this.res.bgImage = val.url;
|
||||
}
|
||||
},
|
||||
clearImage() {
|
||||
this.res.bgImage = "";
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../controls/field-common.scss";
|
||||
@import "../controls/panel-common.scss";
|
||||
|
||||
.panel-page-setting {
|
||||
background: #f5f7fa;
|
||||
margin: 0 -20px;
|
||||
padding: 0 12px 12px;
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.required-star {
|
||||
color: #f56c6c;
|
||||
margin-right: 2px;
|
||||
}
|
||||
.bg-image-body {
|
||||
align-items: center;
|
||||
}
|
||||
.bg-image-preview {
|
||||
position: relative;
|
||||
width: 88px;
|
||||
height: 48px;
|
||||
border: 1px dashed #dcdfe6;
|
||||
border-radius: 6px;
|
||||
padding: 3px;
|
||||
box-sizing: border-box;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
}
|
||||
.bg-image-clear {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: #909399;
|
||||
color: #ffffff;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
&:hover {
|
||||
background: #f56c6c;
|
||||
}
|
||||
}
|
||||
}
|
||||
.upload-tip {
|
||||
margin: -8px 0 12px 102px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,446 @@
|
||||
<template>
|
||||
<div class="panel-promotion">
|
||||
<div class="panel-header">
|
||||
<span class="panel-name">{{ res.name }}</span>
|
||||
<div class="panel-tabs">
|
||||
<div
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="panel-tab"
|
||||
:class="{ active: activeTab === tab.key }"
|
||||
@click="activeTab = tab.key"
|
||||
>
|
||||
{{ tab.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="activeTab === 'content'">
|
||||
<div class="panel-card" v-if="!isPintuan">
|
||||
<div class="section-title">图标设置</div>
|
||||
<div class="section-tip">支持上传 1 张图片,建议 124*32px</div>
|
||||
<div class="icon-row">
|
||||
<div class="icon-preview" v-if="res.options.icon">
|
||||
<img :src="res.options.icon" alt="" />
|
||||
</div>
|
||||
<div class="icon-placeholder" v-else>图标</div>
|
||||
<el-button plain type="primary" @click="pickImage">上传图标</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-card" v-if="isPintuan">
|
||||
<div class="section-title">头部设置</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">选择风格</div>
|
||||
<div class="ctrl-body">
|
||||
<el-radio-group v-model="res.options.headBgType">
|
||||
<el-radio value="color">背景色</el-radio>
|
||||
<el-radio value="image">背景图片</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="res.options.headBgType === 'image'">
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">背景图片</div>
|
||||
<div class="ctrl-body">
|
||||
<div class="bg-preview" v-if="res.options.headBgImage">
|
||||
<img :src="res.options.headBgImage" alt="" />
|
||||
</div>
|
||||
<el-button plain type="primary" @click="pickImage('headBg')">上传背景图</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-tip">建议 710px*96px</div>
|
||||
</template>
|
||||
<gradient-field
|
||||
v-else
|
||||
v-model:start="res.options.headBgColor1"
|
||||
v-model:end="res.options.headBgColor2"
|
||||
:default-start="defaults.headBgColor1"
|
||||
:default-end="defaults.headBgColor2"
|
||||
label="头部背景"
|
||||
/>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">标题类型</div>
|
||||
<div class="ctrl-body">
|
||||
<el-radio-group v-model="res.options.headTitleType">
|
||||
<el-radio value="text">文字</el-radio>
|
||||
<el-radio value="image">图片</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ctrl-row" v-if="res.options.headTitleType !== 'image'">
|
||||
<div class="ctrl-label">标题文字</div>
|
||||
<div class="ctrl-body">
|
||||
<el-input v-model="firstItem.title" maxlength="10" placeholder="超值拼团" />
|
||||
</div>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">标题图片</div>
|
||||
<div class="ctrl-body">
|
||||
<div class="bg-preview" v-if="res.options.headTitleImage">
|
||||
<img :src="res.options.headTitleImage" alt="" />
|
||||
</div>
|
||||
<el-button plain type="primary" @click="pickImage('headTitle')">上传标题图</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-tip">建议 200px*40px</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="panel-card" v-if="!isPintuan">
|
||||
<div class="section-title">标题设置</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">标题内容</div>
|
||||
<div class="ctrl-body">
|
||||
<el-input
|
||||
v-model="firstItem.title1"
|
||||
maxlength="10"
|
||||
:placeholder="isSeckill ? '邀请你的好友来参与' : (isKanjia ? '邀请好友来砍价' : '精彩抢先看')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ctrl-row" v-if="!isLive && !isSeckill && !isKanjia">
|
||||
<div class="ctrl-label">更多链接</div>
|
||||
<div class="ctrl-body">
|
||||
<span class="link-text" v-if="res.options.moreUrl && res.options.moreUrl.___type">
|
||||
{{ linkText(res.options.moreUrl) }}
|
||||
</span>
|
||||
<el-button plain type="primary" @click="pickLink">选择链接</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-card panel-card-fill" v-if="!isLive">
|
||||
<div class="section-title">显示内容</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">展示信息</div>
|
||||
<div class="ctrl-body">
|
||||
<el-checkbox v-model="res.options.showGoodsName">商品名称</el-checkbox>
|
||||
<el-checkbox v-model="res.options.showGoodsPrice">商品价格</el-checkbox>
|
||||
<template v-if="isPintuan">
|
||||
<el-checkbox v-model="res.options.showGroupTag">活动标签</el-checkbox>
|
||||
<el-checkbox v-model="res.options.showOriginalPrice">商品原价</el-checkbox>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ctrl-row" v-if="isPintuan">
|
||||
<div class="ctrl-label">拼团按钮</div>
|
||||
<div class="ctrl-body">
|
||||
<el-radio-group v-model="res.options.showGroupBtn">
|
||||
<el-radio :value="true">显示</el-radio>
|
||||
<el-radio :value="false">隐藏</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="panel-card" v-if="isPintuan || isLive || isSeckill || isKanjia">
|
||||
<div class="section-title">布局设置</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">展示样式</div>
|
||||
<div class="ctrl-body">
|
||||
<div class="seg layout-seg" :class="{ 'layout-seg-3': isLive && !isPintuan }">
|
||||
<div
|
||||
v-for="opt in layoutOptions"
|
||||
:key="opt.value"
|
||||
class="seg-item"
|
||||
:class="{ active: (res.options.layoutStyle || '1') === opt.value }"
|
||||
@click="res.options.layoutStyle = opt.value"
|
||||
>
|
||||
{{ opt.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-card">
|
||||
<div class="section-title">颜色设置</div>
|
||||
<gradient-field
|
||||
v-model:start="res.options.bgColor1"
|
||||
v-model:end="res.options.bgColor2"
|
||||
:default-start="defaults.bgColor1"
|
||||
:default-end="defaults.bgColor2"
|
||||
/>
|
||||
<color-field
|
||||
v-model="res.options.titleColor"
|
||||
label="标题颜色"
|
||||
:default-value="defaults.titleColor"
|
||||
/>
|
||||
<color-field
|
||||
v-if="!isSeckill && !isKanjia"
|
||||
v-model="res.options.moreColor"
|
||||
label="更多按钮颜色"
|
||||
:default-value="defaults.moreColor"
|
||||
/>
|
||||
<color-field
|
||||
v-model="res.options.nameColor"
|
||||
label="商品名称颜色"
|
||||
:default-value="defaults.nameColor"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="panel-card">
|
||||
<div class="section-title">边距设置</div>
|
||||
<slider-field v-model="res.options.paddingTop" label="上边距" :min="0" :max="100" />
|
||||
<slider-field v-model="res.options.paddingBottom" label="下边距" :min="0" :max="100" />
|
||||
<slider-field v-model="res.options.contentGap" label="内容间距" :min="0" :max="20" />
|
||||
<slider-field v-model="res.options.paddingX" label="左右边距" :min="0" :max="40" />
|
||||
<slider-field v-model="res.options.pageGap" label="页面间距" :min="0" :max="100" />
|
||||
</div>
|
||||
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">圆角设置</div>
|
||||
<slider-field v-model="res.options.borderRadius" label="背景圆角" :min="0" :max="30" />
|
||||
<slider-field v-model="res.options.imgRadius" label="图片圆角" :min="0" :max="30" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<liliDialog ref="liliDialog" @selectedLink="selectedLink"></liliDialog>
|
||||
|
||||
<el-dialog v-model="picModelFlag" width="1200px" append-to-body destroy-on-close>
|
||||
<ossManage
|
||||
@callback="callbackSelected"
|
||||
:is-component="true"
|
||||
:initialize="picModelFlag"
|
||||
:max-select="1"
|
||||
ref="ossManage"
|
||||
/>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import GradientField from "../controls/GradientField.vue";
|
||||
import ColorField from "../controls/ColorField.vue";
|
||||
import SliderField from "../controls/SliderField.vue";
|
||||
import ossManage from "@/views/sys/oss-manage/ossManage";
|
||||
import ways from "@/components/lili-dialog/wap.js";
|
||||
|
||||
const DEFAULTS = {
|
||||
layoutStyle: "1",
|
||||
bgColor1: "#FFFFFF",
|
||||
bgColor2: "#FFFFFF",
|
||||
titleColor: "#282828",
|
||||
moreColor: "#282828",
|
||||
nameColor: "#282828",
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
contentGap: 10,
|
||||
paddingX: 12,
|
||||
pageGap: 10,
|
||||
borderRadius: 0,
|
||||
imgRadius: 10,
|
||||
showGoodsName: true,
|
||||
showGoodsPrice: true,
|
||||
showGroupTag: true,
|
||||
showOriginalPrice: true,
|
||||
showGroupBtn: true,
|
||||
headBgType: "color",
|
||||
headBgColor1: "#F5483A",
|
||||
headBgColor2: "#FF7E3D",
|
||||
headTitleType: "text",
|
||||
};
|
||||
|
||||
// 秒杀参考 CRMEB 默认:背景红→白渐变、标题白色、圆角 背景 7 / 内容 5
|
||||
const SECKILL_DEFAULTS = {
|
||||
...DEFAULTS,
|
||||
bgColor1: "#FF2200",
|
||||
bgColor2: "#FFFFFF",
|
||||
titleColor: "#FFFFFF",
|
||||
borderRadius: 7,
|
||||
imgRadius: 5,
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "PanelPromotion",
|
||||
hideDecorateTitle: true,
|
||||
components: { GradientField, ColorField, SliderField, ossManage },
|
||||
props: {
|
||||
res: { type: Object, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
defaults: DEFAULTS,
|
||||
activeTab: "content",
|
||||
tabs: [
|
||||
{ key: "content", title: "内容" },
|
||||
{ key: "style", title: "样式" },
|
||||
],
|
||||
picModelFlag: false,
|
||||
pickTarget: "icon",
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
firstItem() {
|
||||
return (this.res.options.list && this.res.options.list[0]) || {};
|
||||
},
|
||||
isPintuan() {
|
||||
return this.res.promotionsType === "PINTUAN" || this.firstItem.type === "PINTUAN";
|
||||
},
|
||||
isLive() {
|
||||
return this.res.promotionsType === "LIVE" || this.firstItem.type === "LIVE";
|
||||
},
|
||||
isSeckill() {
|
||||
return (
|
||||
this.res.promotionsType === "SECKILL" || this.firstItem.type === "SECKILL"
|
||||
);
|
||||
},
|
||||
isKanjia() {
|
||||
return (
|
||||
this.res.promotionsType === "KANJIA" || this.firstItem.type === "KANJIA"
|
||||
);
|
||||
},
|
||||
layoutOptions() {
|
||||
if (this.isLive && !this.isPintuan) {
|
||||
return [
|
||||
{ value: "1", title: "单行" },
|
||||
{ value: "2", title: "多行" },
|
||||
{ value: "3", title: "双排" },
|
||||
];
|
||||
}
|
||||
if ((this.isSeckill || this.isKanjia) && !this.isPintuan) {
|
||||
return [
|
||||
{ value: "1", title: "样式一" },
|
||||
{ value: "2", title: "样式二" },
|
||||
];
|
||||
}
|
||||
return [
|
||||
{ value: "1", title: "样式一" },
|
||||
{ value: "2", title: "样式二" },
|
||||
{ value: "3", title: "样式三" },
|
||||
{ value: "4", title: "样式四" },
|
||||
];
|
||||
},
|
||||
},
|
||||
created() {
|
||||
const defaults =
|
||||
["SECKILL", "KANJIA"].includes(this.res.promotionsType)
|
||||
? SECKILL_DEFAULTS
|
||||
: DEFAULTS;
|
||||
this.defaults = defaults;
|
||||
this.res.options = Object.assign({}, defaults, this.res.options);
|
||||
if (!Array.isArray(this.res.options.list)) {
|
||||
this.res.options.list = [];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
linkText(url) {
|
||||
const way = ways.find((e) => e.name === url.___type);
|
||||
const typeTitle = way ? way.title : "发现";
|
||||
const name =
|
||||
url.name || url.goodsName || url.memberName || url.title || "";
|
||||
return name ? `${typeTitle} - ${name}` : typeTitle;
|
||||
},
|
||||
pickImage(target = "icon") {
|
||||
this.pickTarget = target;
|
||||
this.picModelFlag = true;
|
||||
this.$nextTick(() => {
|
||||
const oss = this.$refs.ossManage;
|
||||
if (!oss) return;
|
||||
oss.selectImage = true;
|
||||
oss.fileType = "pic";
|
||||
oss.changeFileType();
|
||||
});
|
||||
},
|
||||
callbackSelected(val) {
|
||||
this.picModelFlag = false;
|
||||
if (val && val.url) {
|
||||
const target = this.pickTarget || "icon";
|
||||
if (target === "headBg") {
|
||||
this.res.options.headBgImage = val.url;
|
||||
} else if (target === "headTitle") {
|
||||
this.res.options.headTitleImage = val.url;
|
||||
} else {
|
||||
this.res.options.icon = val.url;
|
||||
}
|
||||
}
|
||||
},
|
||||
pickLink() {
|
||||
const dialog = this.$refs.liliDialog;
|
||||
dialog.clearGoodsSelected();
|
||||
dialog.goodsFlag = false;
|
||||
dialog.flag = true;
|
||||
},
|
||||
selectedLink(val) {
|
||||
delete val.selected;
|
||||
delete val.intro;
|
||||
delete val.mobileIntro;
|
||||
this.res.options.moreUrl = val;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../controls/field-common.scss";
|
||||
@import "../controls/panel-common.scss";
|
||||
|
||||
.panel-promotion {
|
||||
background: #f5f7fa;
|
||||
margin: 0 -20px;
|
||||
padding: 0 12px 12px;
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.icon-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.icon-preview {
|
||||
width: 88px;
|
||||
height: 32px;
|
||||
border: 1px dashed #dcdfe6;
|
||||
border-radius: 6px;
|
||||
padding: 3px;
|
||||
box-sizing: border-box;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
.icon-placeholder {
|
||||
width: 88px;
|
||||
height: 32px;
|
||||
border: 1px dashed #dcdfe6;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
}
|
||||
.link-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.bg-preview {
|
||||
width: 132px;
|
||||
height: 30px;
|
||||
border: 1px dashed #dcdfe6;
|
||||
border-radius: 6px;
|
||||
padding: 2px;
|
||||
box-sizing: border-box;
|
||||
margin-right: 10px;
|
||||
flex-shrink: 0;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,46 +1,62 @@
|
||||
<template>
|
||||
<div class="panel-rich-text">
|
||||
<el-tabs v-model="activeTab">
|
||||
<el-tab-pane label="内容" name="content">
|
||||
<div class="panel-header">
|
||||
<span class="panel-name">{{ res.name }}</span>
|
||||
<div class="panel-tabs">
|
||||
<div
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="panel-tab"
|
||||
:class="{ active: activeTab === tab.key }"
|
||||
@click="activeTab = tab.key"
|
||||
>
|
||||
{{ tab.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="activeTab === 'content'">
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">富文本内容</div>
|
||||
<editor v-model="res.options.content" height="300px" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="样式" name="style">
|
||||
<color-field
|
||||
v-model="res.options.bgColor"
|
||||
label="背景颜色"
|
||||
:default-value="defaults.bgColor"
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="panel-card">
|
||||
<div class="section-title">颜色设置</div>
|
||||
<gradient-field
|
||||
v-model:start="res.options.bgColor1"
|
||||
v-model:end="res.options.bgColor2"
|
||||
:default-start="defaults.bgColor1"
|
||||
:default-end="defaults.bgColor2"
|
||||
/>
|
||||
<slider-field
|
||||
v-model="res.options.paddingX"
|
||||
label="左右边距"
|
||||
:min="0"
|
||||
:max="100"
|
||||
/>
|
||||
<slider-field
|
||||
v-model="res.options.pageGap"
|
||||
label="页面间距"
|
||||
:min="0"
|
||||
:max="100"
|
||||
/>
|
||||
<slider-field
|
||||
v-model="res.options.borderRadius"
|
||||
label="背景圆角"
|
||||
:min="0"
|
||||
:max="30"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<div class="panel-card">
|
||||
<div class="section-title">边距设置</div>
|
||||
<slider-field v-model="res.options.paddingX" label="左右边距" :min="0" :max="40" />
|
||||
<slider-field v-model="res.options.pageGap" label="页面间距" :min="0" :max="100" />
|
||||
</div>
|
||||
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">圆角设置</div>
|
||||
<slider-field v-model="res.options.borderRadius" label="背景圆角" :min="0" :max="30" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import editor from "@/components/editor";
|
||||
import ColorField from "../controls/ColorField.vue";
|
||||
import GradientField from "../controls/GradientField.vue";
|
||||
import SliderField from "../controls/SliderField.vue";
|
||||
|
||||
const DEFAULTS = {
|
||||
content: "",
|
||||
bgColor: "#FFFFFF",
|
||||
bgColor1: "#FFFFFF",
|
||||
bgColor2: "#FFFFFF",
|
||||
paddingX: 12,
|
||||
pageGap: 10,
|
||||
borderRadius: 0,
|
||||
@@ -48,15 +64,35 @@ const DEFAULTS = {
|
||||
|
||||
export default {
|
||||
name: "PanelRichText",
|
||||
components: { editor, ColorField, SliderField },
|
||||
hideDecorateTitle: true,
|
||||
components: { editor, GradientField, SliderField },
|
||||
props: {
|
||||
res: { type: Object, required: true },
|
||||
},
|
||||
data() {
|
||||
return { defaults: DEFAULTS, activeTab: "content" };
|
||||
return {
|
||||
defaults: DEFAULTS,
|
||||
activeTab: "content",
|
||||
tabs: [
|
||||
{ key: "content", title: "内容" },
|
||||
{ key: "style", title: "样式" },
|
||||
],
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.res.options = Object.assign({}, DEFAULTS, this.res.options);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../controls/panel-common.scss";
|
||||
|
||||
.panel-rich-text {
|
||||
background: #f5f7fa;
|
||||
margin: 0 -20px;
|
||||
padding: 0 12px 12px;
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
<template>
|
||||
<div class="panel-search">
|
||||
<div class="panel-header">
|
||||
<span class="panel-name">{{ res.name }}</span>
|
||||
<div class="panel-tabs">
|
||||
<div
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="panel-tab"
|
||||
:class="{ active: activeTab === tab.key }"
|
||||
@click="activeTab = tab.key"
|
||||
>
|
||||
{{ tab.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="activeTab === 'content'">
|
||||
<div class="panel-card">
|
||||
<div class="section-title">提示文字设置</div>
|
||||
<div class="field-row">
|
||||
<div class="field-label">提示文字</div>
|
||||
<el-input
|
||||
v-model="res.options.list[0].title"
|
||||
size="default"
|
||||
maxlength="20"
|
||||
placeholder="搜索商品名称"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">搜索热词</div>
|
||||
<div class="hot-word" v-for="(word, index) in res.options.hotWords" :key="index">
|
||||
<el-input
|
||||
v-model="res.options.hotWords[index]"
|
||||
size="default"
|
||||
maxlength="10"
|
||||
placeholder="选填,不超过十个字"
|
||||
/>
|
||||
<el-icon class="del" @click="removeWord(index)"><CircleClose /></el-icon>
|
||||
</div>
|
||||
<el-button class="add-btn" plain type="primary" @click="addWord">
|
||||
+ 添加热词
|
||||
</el-button>
|
||||
<div class="field-row interval-row">
|
||||
<div class="field-label">切换时间</div>
|
||||
<el-input-number
|
||||
class="interval-input"
|
||||
v-model="res.options.hotInterval"
|
||||
:min="1"
|
||||
:max="60"
|
||||
size="default"
|
||||
controls-position="right"
|
||||
/>
|
||||
<span class="unit">秒</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="panel-card">
|
||||
<div class="section-title">文本位置</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">文本位置</div>
|
||||
<div class="ctrl-body">
|
||||
<div class="seg layout-seg">
|
||||
<div
|
||||
class="seg-item"
|
||||
:class="{ active: res.options.textAlign !== 'center' }"
|
||||
@click="res.options.textAlign = 'left'"
|
||||
>
|
||||
居左
|
||||
</div>
|
||||
<div
|
||||
class="seg-item"
|
||||
:class="{ active: res.options.textAlign === 'center' }"
|
||||
@click="res.options.textAlign = 'center'"
|
||||
>
|
||||
居中
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-card">
|
||||
<div class="section-title">颜色设置</div>
|
||||
<gradient-field
|
||||
v-model:start="res.options.bgColor1"
|
||||
v-model:end="res.options.bgColor2"
|
||||
:default-start="defaults.bgColor1"
|
||||
:default-end="defaults.bgColor2"
|
||||
/>
|
||||
<color-field
|
||||
v-model="res.options.boxColor"
|
||||
label="框体颜色"
|
||||
:default-value="defaults.boxColor"
|
||||
/>
|
||||
<color-field
|
||||
v-model="res.options.textColor"
|
||||
label="输入文字颜色"
|
||||
:default-value="defaults.textColor"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="panel-card">
|
||||
<div class="section-title">边距设置</div>
|
||||
<slider-field v-model="res.options.paddingX" label="左右边距" :min="0" :max="20" />
|
||||
</div>
|
||||
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">圆角设置</div>
|
||||
<slider-field v-model="res.options.borderRadius" label="背景圆角" :min="0" :max="30" />
|
||||
<slider-field v-model="res.options.boxRadius" label="内容圆角" :min="0" :max="30" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ColorField from "../controls/ColorField.vue";
|
||||
import GradientField from "../controls/GradientField.vue";
|
||||
import SliderField from "../controls/SliderField.vue";
|
||||
import { CircleClose } from "@element-plus/icons-vue";
|
||||
|
||||
const DEFAULTS = {
|
||||
textAlign: "left",
|
||||
bgColor1: "#FFFFFF",
|
||||
bgColor2: "#FFFFFF",
|
||||
boxColor: "#EDEDED",
|
||||
textColor: "#999999",
|
||||
paddingX: 12,
|
||||
borderRadius: 0,
|
||||
boxRadius: 15,
|
||||
hotWords: [],
|
||||
hotInterval: 3,
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "PanelSearch",
|
||||
hideDecorateTitle: true,
|
||||
components: { ColorField, GradientField, SliderField, CircleClose },
|
||||
props: {
|
||||
res: { type: Object, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
defaults: DEFAULTS,
|
||||
activeTab: "content",
|
||||
tabs: [
|
||||
{ key: "content", title: "内容" },
|
||||
{ key: "style", title: "样式" },
|
||||
],
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.res.options = Object.assign({}, DEFAULTS, this.res.options);
|
||||
if (!Array.isArray(this.res.options.list) || !this.res.options.list.length) {
|
||||
this.res.options.list = [{ title: "搜索" }];
|
||||
}
|
||||
if (!Array.isArray(this.res.options.hotWords)) {
|
||||
this.res.options.hotWords = [];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addWord() {
|
||||
this.res.options.hotWords.push("");
|
||||
},
|
||||
removeWord(index) {
|
||||
this.res.options.hotWords.splice(index, 1);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../controls/field-common.scss";
|
||||
@import "../controls/panel-common.scss";
|
||||
.panel-search {
|
||||
background: #f5f7fa;
|
||||
margin: 0 -20px;
|
||||
padding: 0 12px 12px;
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 50px;
|
||||
margin-bottom: 12px;
|
||||
padding: 0 14px;
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
|
||||
box-sizing: border-box;
|
||||
.panel-name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
.panel-tabs {
|
||||
display: inline-flex;
|
||||
background: #f4f4f5;
|
||||
border-radius: 8px;
|
||||
padding: 3px;
|
||||
.panel-tab {
|
||||
padding: 5px 14px;
|
||||
font-size: 13px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
color: #606266;
|
||||
transition: all 0.2s ease;
|
||||
&.active {
|
||||
background: #ffffff;
|
||||
color: #409eff;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
}
|
||||
}
|
||||
.panel-card {
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
padding: 14px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
.section-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
.field-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.field-label {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
margin-right: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
.hot-word {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
.del {
|
||||
cursor: pointer;
|
||||
color: #909399;
|
||||
font-size: 18px;
|
||||
flex-shrink: 0;
|
||||
&:hover {
|
||||
color: #e1251b;
|
||||
}
|
||||
}
|
||||
}
|
||||
.add-btn {
|
||||
width: 100%;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.interval-row {
|
||||
.interval-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
:deep(.el-input__inner) {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
.unit {
|
||||
margin-left: 8px;
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,63 +1,270 @@
|
||||
<template>
|
||||
<div class="panel-shop">
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">标题</div>
|
||||
<div class="ctrl-body">
|
||||
<el-input v-model="firstItem.title" style="width: 200px" />
|
||||
<div class="panel-header">
|
||||
<span class="panel-name">{{ res.name }}</span>
|
||||
<div class="panel-tabs">
|
||||
<div
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="panel-tab"
|
||||
:class="{ active: activeTab === tab.key }"
|
||||
@click="activeTab = tab.key"
|
||||
>
|
||||
{{ tab.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<slider-field
|
||||
v-model="firstItem.count"
|
||||
label="显示数量"
|
||||
:min="1"
|
||||
:max="20"
|
||||
/>
|
||||
<color-field
|
||||
v-model="res.options.bgColor"
|
||||
label="背景颜色"
|
||||
:default-value="defaults.bgColor"
|
||||
/>
|
||||
<slider-field
|
||||
v-model="res.options.paddingX"
|
||||
label="左右边距"
|
||||
:min="0"
|
||||
:max="30"
|
||||
/>
|
||||
<slider-field
|
||||
v-model="res.options.pageGap"
|
||||
label="页面间距"
|
||||
:min="0"
|
||||
:max="100"
|
||||
/>
|
||||
<slider-field
|
||||
v-model="res.options.borderRadius"
|
||||
label="背景圆角"
|
||||
:min="0"
|
||||
:max="30"
|
||||
/>
|
||||
|
||||
<template v-if="activeTab === 'content'">
|
||||
<div class="panel-card">
|
||||
<div class="section-title">图标设置</div>
|
||||
<div class="section-tip">支持上传 1 张图片,建议 124*32px</div>
|
||||
<div class="icon-row">
|
||||
<div class="icon-preview" v-if="firstItem.icon">
|
||||
<img :src="firstItem.icon" alt="" />
|
||||
</div>
|
||||
<div class="icon-placeholder" v-else>图标</div>
|
||||
<el-button plain type="primary" @click="pickImage">上传图标</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-card">
|
||||
<div class="section-title">标题设置</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">标题内容</div>
|
||||
<div class="ctrl-body">
|
||||
<el-input v-model="firstItem.title" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">更多链接</div>
|
||||
<div class="ctrl-body">
|
||||
<span class="link-text" v-if="firstItem.moreUrl && firstItem.moreUrl.___type">
|
||||
{{ linkText(firstItem.moreUrl) }}
|
||||
</span>
|
||||
<el-button plain type="primary" @click="pickLink">选择链接</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-card">
|
||||
<div class="section-title">显示内容</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">展示信息</div>
|
||||
<div class="ctrl-body">
|
||||
<el-checkbox v-model="firstItem.showLogo">店铺logo</el-checkbox>
|
||||
<el-checkbox v-model="firstItem.showName">店铺名称</el-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">店铺数据</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">店铺数据</div>
|
||||
<div class="ctrl-body">
|
||||
<el-radio-group v-model="firstItem.dataMode">
|
||||
<el-radio value="default">默认规则</el-radio>
|
||||
<el-radio value="custom">自定义</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
<slider-field
|
||||
v-if="firstItem.dataMode !== 'custom'"
|
||||
v-model="firstItem.count"
|
||||
label="展示数量"
|
||||
:min="1"
|
||||
:max="20"
|
||||
/>
|
||||
<template v-else>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">选择店铺</div>
|
||||
<div class="ctrl-body">
|
||||
<el-button plain type="primary" @click="shopDialogVisible = true">选择店铺</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="shop-chips" v-if="firstItem.shopList && firstItem.shopList.length">
|
||||
<el-tag
|
||||
v-for="(shop, i) in firstItem.shopList"
|
||||
:key="shop.id"
|
||||
closable
|
||||
class="shop-chip"
|
||||
@close="removeShop(i)"
|
||||
>
|
||||
{{ shop.storeName }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="panel-card">
|
||||
<div class="section-title">布局设置</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">展示样式</div>
|
||||
<div class="ctrl-body">
|
||||
<div class="seg layout-seg">
|
||||
<div
|
||||
class="seg-item"
|
||||
:class="{ active: (firstItem.shopStyle || 'one') === 'one' }"
|
||||
@click="firstItem.shopStyle = 'one'"
|
||||
>
|
||||
样式一
|
||||
</div>
|
||||
<div
|
||||
class="seg-item"
|
||||
:class="{ active: firstItem.shopStyle === 'two' }"
|
||||
@click="firstItem.shopStyle = 'two'"
|
||||
>
|
||||
样式二
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-card">
|
||||
<div class="section-title">颜色设置</div>
|
||||
<gradient-field
|
||||
v-model:start="res.options.bgColor1"
|
||||
v-model:end="res.options.bgColor2"
|
||||
:default-start="defaults.bgColor1"
|
||||
:default-end="defaults.bgColor2"
|
||||
/>
|
||||
<color-field
|
||||
v-model="res.options.titleColor"
|
||||
label="标题颜色"
|
||||
:default-value="defaults.titleColor"
|
||||
/>
|
||||
<color-field
|
||||
v-model="res.options.nameColor"
|
||||
label="店铺名称颜色"
|
||||
:default-value="defaults.nameColor"
|
||||
/>
|
||||
<color-field
|
||||
v-model="res.options.moreColor"
|
||||
label="更多按钮颜色"
|
||||
:default-value="defaults.moreColor"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="panel-card">
|
||||
<div class="section-title">边距设置</div>
|
||||
<slider-field v-model="res.options.paddingTop" label="上边距" :min="0" :max="100" />
|
||||
<slider-field v-model="res.options.paddingBottom" label="下边距" :min="0" :max="100" />
|
||||
<slider-field v-model="res.options.paddingX" label="左右边距" :min="0" :max="40" />
|
||||
<slider-field v-model="res.options.pageGap" label="页面间距" :min="0" :max="100" />
|
||||
</div>
|
||||
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">圆角设置</div>
|
||||
<slider-field v-model="res.options.borderRadius" label="背景圆角" :min="0" :max="30" />
|
||||
<slider-field v-model="res.options.imgRadius" label="内容圆角" :min="0" :max="30" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<liliDialog ref="liliDialog" @selectedLink="selectedLink"></liliDialog>
|
||||
|
||||
<el-dialog v-model="picModelFlag" width="1200px" append-to-body destroy-on-close>
|
||||
<ossManage
|
||||
@callback="callbackSelected"
|
||||
:is-component="true"
|
||||
:initialize="picModelFlag"
|
||||
:max-select="1"
|
||||
ref="ossManage"
|
||||
/>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="shopDialogVisible"
|
||||
title="选择店铺"
|
||||
width="720px"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
@open="loadShops"
|
||||
>
|
||||
<el-table
|
||||
:data="shopOptions"
|
||||
v-loading="shopLoading"
|
||||
max-height="400"
|
||||
@selection-change="onShopSelection"
|
||||
>
|
||||
<el-table-column type="selection" width="50" />
|
||||
<el-table-column label="店铺" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<div class="shop-cell">
|
||||
<img :src="row.storeLogo" class="shop-cell-logo" alt="" />
|
||||
<span>{{ row.storeName }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" width="100">
|
||||
<template #default="{ row }">{{ row.selfOperated ? "自营" : "旗舰店" }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<el-button @click="shopDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="confirmShops">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import GradientField from "../controls/GradientField.vue";
|
||||
import ColorField from "../controls/ColorField.vue";
|
||||
import SliderField from "../controls/SliderField.vue";
|
||||
import ossManage from "@/views/sys/oss-manage/ossManage";
|
||||
import ways from "@/components/lili-dialog/wap.js";
|
||||
import { getShopListData } from "@/api/shops";
|
||||
|
||||
const DEFAULTS = {
|
||||
bgColor: "#FFFFFF",
|
||||
bgColor1: "#FFFFFF",
|
||||
bgColor2: "#FFFFFF",
|
||||
titleColor: "#282828",
|
||||
nameColor: "#282828",
|
||||
moreColor: "#282828",
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
paddingX: 0,
|
||||
pageGap: 10,
|
||||
borderRadius: 0,
|
||||
imgRadius: 7,
|
||||
};
|
||||
const DEFAULT_ITEM = {
|
||||
title: "店铺列表",
|
||||
count: 4,
|
||||
icon: "",
|
||||
moreUrl: "",
|
||||
showLogo: true,
|
||||
showName: true,
|
||||
shopStyle: "one",
|
||||
dataMode: "default",
|
||||
shopList: [],
|
||||
};
|
||||
const DEFAULT_ITEM = { title: "店铺列表", count: 4 };
|
||||
|
||||
export default {
|
||||
name: "PanelShop",
|
||||
components: { ColorField, SliderField },
|
||||
hideDecorateTitle: true,
|
||||
components: { GradientField, ColorField, SliderField, ossManage },
|
||||
props: {
|
||||
res: { type: Object, required: true },
|
||||
},
|
||||
data() {
|
||||
return { defaults: DEFAULTS };
|
||||
return {
|
||||
defaults: DEFAULTS,
|
||||
activeTab: "content",
|
||||
tabs: [
|
||||
{ key: "content", title: "内容" },
|
||||
{ key: "style", title: "样式" },
|
||||
],
|
||||
picModelFlag: false,
|
||||
shopDialogVisible: false,
|
||||
shopLoading: false,
|
||||
shopOptions: [],
|
||||
selectedShops: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
firstItem() {
|
||||
@@ -69,10 +276,141 @@ export default {
|
||||
if (!Array.isArray(this.res.options.list) || !this.res.options.list[0]) {
|
||||
this.res.options.list = [{ ...DEFAULT_ITEM }];
|
||||
}
|
||||
const item = this.firstItem;
|
||||
if (item.showLogo === undefined) item.showLogo = true;
|
||||
if (item.showName === undefined) item.showName = true;
|
||||
if (!item.shopStyle) item.shopStyle = "one";
|
||||
if (!item.dataMode) item.dataMode = "default";
|
||||
if (!Array.isArray(item.shopList)) item.shopList = [];
|
||||
},
|
||||
methods: {
|
||||
// 加载店铺列表供选择
|
||||
loadShops() {
|
||||
this.shopLoading = true;
|
||||
this.selectedShops = [];
|
||||
getShopListData({ pageNumber: 1, pageSize: 100, status: "OPEN" })
|
||||
.then((res) => {
|
||||
this.shopOptions = (res && res.result && res.result.records) || [];
|
||||
})
|
||||
.finally(() => {
|
||||
this.shopLoading = false;
|
||||
});
|
||||
},
|
||||
onShopSelection(rows) {
|
||||
this.selectedShops = rows;
|
||||
},
|
||||
confirmShops() {
|
||||
this.firstItem.shopList = this.selectedShops.map((s) => ({
|
||||
id: s.id,
|
||||
storeName: s.storeName,
|
||||
storeLogo: s.storeLogo,
|
||||
selfOperated: s.selfOperated,
|
||||
storeBanner: s.storeBanner || "",
|
||||
}));
|
||||
this.shopDialogVisible = false;
|
||||
},
|
||||
removeShop(index) {
|
||||
this.firstItem.shopList.splice(index, 1);
|
||||
},
|
||||
linkText(url) {
|
||||
const way = ways.find((e) => e.name === url.___type);
|
||||
const typeTitle = way ? way.title : "发现";
|
||||
const name =
|
||||
url.name || url.goodsName || url.memberName || url.title || "";
|
||||
return name ? `${typeTitle} - ${name}` : typeTitle;
|
||||
},
|
||||
pickImage() {
|
||||
this.picModelFlag = true;
|
||||
this.$nextTick(() => {
|
||||
const oss = this.$refs.ossManage;
|
||||
if (!oss) return;
|
||||
oss.selectImage = true;
|
||||
oss.fileType = "pic";
|
||||
oss.changeFileType();
|
||||
});
|
||||
},
|
||||
callbackSelected(val) {
|
||||
this.picModelFlag = false;
|
||||
if (val && val.url) {
|
||||
this.firstItem.icon = val.url;
|
||||
}
|
||||
},
|
||||
pickLink() {
|
||||
const dialog = this.$refs.liliDialog;
|
||||
dialog.clearGoodsSelected();
|
||||
dialog.goodsFlag = false;
|
||||
dialog.flag = true;
|
||||
},
|
||||
selectedLink(val) {
|
||||
delete val.selected;
|
||||
delete val.intro;
|
||||
delete val.mobileIntro;
|
||||
this.firstItem.moreUrl = val;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../controls/field-common.scss";
|
||||
@import "../controls/panel-common.scss";
|
||||
|
||||
.icon-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.icon-preview {
|
||||
width: 88px;
|
||||
height: 32px;
|
||||
border: 1px dashed #dcdfe6;
|
||||
border-radius: 6px;
|
||||
padding: 3px;
|
||||
box-sizing: border-box;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
.icon-placeholder {
|
||||
width: 88px;
|
||||
height: 32px;
|
||||
border: 1px dashed #dcdfe6;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
}
|
||||
.shop-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.shop-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
.shop-cell-logo {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
.link-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-right: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
<template>
|
||||
<div class="panel-title">
|
||||
<div class="panel-header">
|
||||
<span class="panel-name">{{ res.name }}</span>
|
||||
<div class="panel-tabs">
|
||||
<div
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="panel-tab"
|
||||
:class="{ active: activeTab === tab.key }"
|
||||
@click="activeTab = tab.key"
|
||||
>
|
||||
{{ tab.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="activeTab === 'content'">
|
||||
<div class="panel-card">
|
||||
<div class="section-title">文本标题</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">标题名称</div>
|
||||
<div class="ctrl-body">
|
||||
<el-input v-model="item.title" maxlength="20" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">副标题</div>
|
||||
<div class="ctrl-body">
|
||||
<el-input v-model="item.title1" maxlength="20" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">右侧按钮</div>
|
||||
<div class="ctrl-body">
|
||||
<el-radio-group v-model="item.showMore">
|
||||
<el-radio :value="true">显示</el-radio>
|
||||
<el-radio :value="false">隐藏</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="item.showMore">
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">右侧文字</div>
|
||||
<div class="ctrl-body">
|
||||
<el-input v-model="item.moreText" maxlength="6" placeholder="更多" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">链接</div>
|
||||
<div class="ctrl-body">
|
||||
<span class="link-text" v-if="item.moreUrl && item.moreUrl.___type">
|
||||
{{ linkText(item.moreUrl) }}
|
||||
</span>
|
||||
<el-button plain type="primary" @click="pickLink">选择链接</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">选择风格</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">风格</div>
|
||||
<div class="ctrl-body">
|
||||
<el-radio-group v-model="item.bgType">
|
||||
<el-radio value="image">背景图片</el-radio>
|
||||
<el-radio value="color">背景色</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ctrl-row" v-if="item.bgType === 'color'">
|
||||
<div class="ctrl-label">背景色</div>
|
||||
<div class="ctrl-body">
|
||||
<el-color-picker v-model="item.bk_color" />
|
||||
<el-input v-model="item.bk_color" class="ctrl-color-input" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="ctrl-row" v-else>
|
||||
<div class="ctrl-label">上传背景图</div>
|
||||
<div class="ctrl-body">
|
||||
<div class="bg-preview" v-if="item.bgImage">
|
||||
<img :src="item.bgImage" alt="" />
|
||||
</div>
|
||||
<el-button plain type="primary" @click="pickImage">上传背景图</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-tip" v-if="item.bgType !== 'color'">建议 910px*86px</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="panel-card">
|
||||
<div class="section-title">文字设置</div>
|
||||
<color-field v-model="item.color" label="标题颜色" :default-value="defaults.color" />
|
||||
<slider-field v-model="item.fontSize" label="标题文字" :min="12" :max="40" />
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">文本样式</div>
|
||||
<div class="ctrl-body">
|
||||
<div class="seg layout-seg-3">
|
||||
<div
|
||||
v-for="opt in textStyleOptions"
|
||||
:key="opt.value"
|
||||
class="seg-item"
|
||||
:class="{ active: (item.textStyle || 'normal') === opt.value }"
|
||||
@click="item.textStyle = opt.value"
|
||||
>
|
||||
{{ opt.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<color-field v-model="item.color1" label="副标题颜色" :default-value="defaults.color1" />
|
||||
<slider-field v-model="item.subFontSize" label="副标题文字" :min="10" :max="30" />
|
||||
<template v-if="item.showMore">
|
||||
<color-field v-model="item.moreColor" label="按钮颜色" :default-value="defaults.moreColor" />
|
||||
<slider-field v-model="item.moreFontSize" label="按钮文字" :min="10" :max="24" />
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="panel-card">
|
||||
<div class="section-title">边距设置</div>
|
||||
<slider-field v-model="res.options.paddingTop" label="上边距" :min="0" :max="100" />
|
||||
<slider-field v-model="res.options.paddingBottom" label="下边距" :min="0" :max="100" />
|
||||
<slider-field v-model="res.options.paddingX" label="左右边距" :min="0" :max="40" />
|
||||
<slider-field v-model="res.options.pageGap" label="页面间距" :min="0" :max="100" />
|
||||
</div>
|
||||
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">圆角设置</div>
|
||||
<slider-field v-model="res.options.radiusTop" label="上圆角" :min="0" :max="30" />
|
||||
<slider-field v-model="res.options.radiusBottom" label="下圆角" :min="0" :max="30" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<liliDialog ref="liliDialog" @selectedLink="selectedLink"></liliDialog>
|
||||
|
||||
<el-dialog v-model="picModelFlag" width="1200px" append-to-body destroy-on-close>
|
||||
<ossManage
|
||||
@callback="callbackSelected"
|
||||
:is-component="true"
|
||||
:initialize="picModelFlag"
|
||||
:max-select="1"
|
||||
ref="ossManage"
|
||||
/>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ColorField from "../controls/ColorField.vue";
|
||||
import SliderField from "../controls/SliderField.vue";
|
||||
import ossManage from "@/views/sys/oss-manage/ossManage";
|
||||
import ways from "@/components/lili-dialog/wap.js";
|
||||
|
||||
const DEFAULTS = {
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
paddingX: 12,
|
||||
pageGap: 10,
|
||||
radiusTop: 0,
|
||||
radiusBottom: 0,
|
||||
color: "#282828",
|
||||
color1: "#282828",
|
||||
moreColor: "#282828",
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "PanelTitle",
|
||||
hideDecorateTitle: true,
|
||||
components: { ColorField, SliderField, ossManage },
|
||||
props: {
|
||||
res: { type: Object, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
defaults: DEFAULTS,
|
||||
activeTab: "content",
|
||||
tabs: [
|
||||
{ key: "content", title: "内容" },
|
||||
{ key: "style", title: "样式" },
|
||||
],
|
||||
textStyleOptions: [
|
||||
{ value: "normal", title: "正常" },
|
||||
{ value: "italic", title: "斜体" },
|
||||
{ value: "bold", title: "加粗" },
|
||||
],
|
||||
picModelFlag: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
item() {
|
||||
return this.res.options.list[0];
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.res.options = Object.assign({}, DEFAULTS, this.res.options);
|
||||
if (!Array.isArray(this.res.options.list) || !this.res.options.list.length) {
|
||||
this.res.options.list = [
|
||||
{
|
||||
title: "标题",
|
||||
title1: "副标题",
|
||||
color: "#282828",
|
||||
color1: "#282828",
|
||||
bk_color: "#FFFFFF",
|
||||
textAlign: "left",
|
||||
},
|
||||
];
|
||||
}
|
||||
const item = this.item;
|
||||
if (item.showMore === undefined) item.showMore = true;
|
||||
if (!item.moreText) item.moreText = "更多";
|
||||
if (!item.bgType) item.bgType = "color";
|
||||
if (!item.fontSize) item.fontSize = 20;
|
||||
if (!item.subFontSize) item.subFontSize = 14;
|
||||
if (!item.moreFontSize) item.moreFontSize = 12;
|
||||
if (!item.moreColor) item.moreColor = "#282828";
|
||||
},
|
||||
methods: {
|
||||
linkText(url) {
|
||||
const way = ways.find((e) => e.name === url.___type);
|
||||
const typeTitle = way ? way.title : "发现";
|
||||
const name =
|
||||
url.name || url.goodsName || url.memberName || url.title || "";
|
||||
return name ? `${typeTitle} - ${name}` : typeTitle;
|
||||
},
|
||||
pickImage() {
|
||||
this.picModelFlag = true;
|
||||
this.$nextTick(() => {
|
||||
const oss = this.$refs.ossManage;
|
||||
if (!oss) return;
|
||||
oss.selectImage = true;
|
||||
oss.fileType = "pic";
|
||||
oss.changeFileType();
|
||||
});
|
||||
},
|
||||
callbackSelected(val) {
|
||||
this.picModelFlag = false;
|
||||
if (val && val.url) {
|
||||
this.item.bgImage = val.url;
|
||||
}
|
||||
},
|
||||
pickLink() {
|
||||
const dialog = this.$refs.liliDialog;
|
||||
dialog.clearGoodsSelected();
|
||||
dialog.goodsFlag = false;
|
||||
dialog.flag = true;
|
||||
},
|
||||
selectedLink(val) {
|
||||
delete val.selected;
|
||||
delete val.intro;
|
||||
delete val.mobileIntro;
|
||||
this.item.moreUrl = val;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../controls/field-common.scss";
|
||||
@import "../controls/panel-common.scss";
|
||||
|
||||
.panel-title {
|
||||
background: #f5f7fa;
|
||||
margin: 0 -20px;
|
||||
padding: 0 12px 12px;
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.link-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.bg-preview {
|
||||
width: 132px;
|
||||
height: 30px;
|
||||
border: 1px dashed #dcdfe6;
|
||||
border-radius: 6px;
|
||||
padding: 2px;
|
||||
box-sizing: border-box;
|
||||
margin-right: 10px;
|
||||
flex-shrink: 0;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,300 @@
|
||||
<template>
|
||||
<div class="panel-video">
|
||||
<div class="panel-header">
|
||||
<span class="panel-name">{{ res.name }}</span>
|
||||
<div class="panel-tabs">
|
||||
<div
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="panel-tab"
|
||||
:class="{ active: activeTab === tab.key }"
|
||||
@click="activeTab = tab.key"
|
||||
>
|
||||
{{ tab.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="activeTab === 'content'">
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">视频内容设置</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">上传视频</div>
|
||||
<div class="ctrl-body">
|
||||
<div class="upload-state" :class="{ filled: item.videoUrl }">
|
||||
{{ item.videoUrl ? "已选择视频" : "未选择视频" }}
|
||||
</div>
|
||||
<el-button plain type="primary" @click="pickFile('videoUrl')">
|
||||
{{ item.videoUrl ? "重新选择" : "上传视频" }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">视频封面</div>
|
||||
<div class="ctrl-body">
|
||||
<div class="cover-preview" v-if="item.poster">
|
||||
<img :src="item.poster" alt="" />
|
||||
</div>
|
||||
<div class="cover-placeholder" v-else>封面</div>
|
||||
<el-button plain type="primary" @click="pickFile('poster')">
|
||||
{{ item.poster ? "重新选择" : "上传封面" }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">自动播放</div>
|
||||
<div class="ctrl-body">
|
||||
<el-switch v-model="item.autoplay" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">画面比例</div>
|
||||
<div class="ctrl-body">
|
||||
<div class="seg layout-seg-3">
|
||||
<div
|
||||
v-for="opt in aspectOptions"
|
||||
:key="opt"
|
||||
class="seg-item"
|
||||
:class="{ active: (item.aspectRatio || '16:9') === opt }"
|
||||
@click="item.aspectRatio = opt"
|
||||
>
|
||||
{{ opt }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="panel-card">
|
||||
<div class="section-title">颜色设置</div>
|
||||
<gradient-field
|
||||
v-model:start="res.options.bgColor1"
|
||||
v-model:end="res.options.bgColor2"
|
||||
:default-start="defaults.bgColor1"
|
||||
:default-end="defaults.bgColor2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="panel-card">
|
||||
<div class="section-title">边距设置</div>
|
||||
<slider-field v-model="res.options.paddingTop" label="上边距" :min="0" :max="100" />
|
||||
<slider-field v-model="res.options.paddingBottom" label="下边距" :min="0" :max="100" />
|
||||
<slider-field v-model="res.options.paddingX" label="左右边距" :min="0" :max="40" />
|
||||
<slider-field v-model="res.options.pageGap" label="页面间距" :min="0" :max="100" />
|
||||
</div>
|
||||
|
||||
<div class="panel-card panel-card-fill">
|
||||
<div class="section-title">圆角设置</div>
|
||||
<slider-field v-model="res.options.borderRadius" label="背景圆角" :min="0" :max="30" />
|
||||
<slider-field v-model="res.options.imgRadius" label="内容圆角" :min="0" :max="30" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-dialog v-model="picModelFlag" width="1200px" append-to-body destroy-on-close>
|
||||
<ossManage
|
||||
@callback="callbackSelected"
|
||||
:is-component="true"
|
||||
:initialize="picModelFlag"
|
||||
:max-select="1"
|
||||
ref="ossManage"
|
||||
/>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import GradientField from "../controls/GradientField.vue";
|
||||
import SliderField from "../controls/SliderField.vue";
|
||||
import ossManage from "@/views/sys/oss-manage/ossManage";
|
||||
|
||||
const DEFAULTS = {
|
||||
bgColor1: "#FFFFFF",
|
||||
bgColor2: "#FFFFFF",
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
paddingX: 12,
|
||||
pageGap: 10,
|
||||
borderRadius: 0,
|
||||
imgRadius: 0,
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "PanelVideo",
|
||||
hideDecorateTitle: true,
|
||||
components: { GradientField, SliderField, ossManage },
|
||||
props: {
|
||||
res: { type: Object, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
defaults: DEFAULTS,
|
||||
activeTab: "content",
|
||||
tabs: [
|
||||
{ key: "content", title: "内容" },
|
||||
{ key: "style", title: "样式" },
|
||||
],
|
||||
aspectOptions: ["16:9", "4:3", "1:1"],
|
||||
picModelFlag: false,
|
||||
currentField: "videoUrl",
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
item() {
|
||||
return this.res.options.list[0];
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.res.options = Object.assign({}, DEFAULTS, this.res.options);
|
||||
if (!Array.isArray(this.res.options.list) || !this.res.options.list.length) {
|
||||
this.res.options.list = [
|
||||
{ videoUrl: "", poster: "", autoplay: false, aspectRatio: "16:9" },
|
||||
];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
pickFile(field) {
|
||||
this.currentField = field;
|
||||
this.picModelFlag = true;
|
||||
this.$nextTick(() => {
|
||||
const oss = this.$refs.ossManage;
|
||||
if (!oss) return;
|
||||
oss.selectImage = true;
|
||||
oss.fileType = field === "videoUrl" ? "video" : "pic";
|
||||
oss.changeFileType();
|
||||
});
|
||||
},
|
||||
callbackSelected(val) {
|
||||
this.picModelFlag = false;
|
||||
if (val && val.url) {
|
||||
this.item[this.currentField] = val.url;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../controls/field-common.scss";
|
||||
@import "../controls/panel-common.scss";
|
||||
.panel-video {
|
||||
background: #f5f7fa;
|
||||
margin: 0 -20px;
|
||||
padding: 0 12px 12px;
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 50px;
|
||||
margin-bottom: 12px;
|
||||
padding: 0 14px;
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
|
||||
box-sizing: border-box;
|
||||
.panel-name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
.panel-tabs {
|
||||
display: inline-flex;
|
||||
background: #f4f4f5;
|
||||
border-radius: 8px;
|
||||
padding: 3px;
|
||||
.panel-tab {
|
||||
padding: 5px 14px;
|
||||
font-size: 13px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
color: #606266;
|
||||
transition: all 0.2s ease;
|
||||
&.active {
|
||||
background: #ffffff;
|
||||
color: #409eff;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
}
|
||||
}
|
||||
.panel-card {
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
padding: 14px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
.section-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
.upload-state {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
&.filled {
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
.cover-preview {
|
||||
width: 88px;
|
||||
height: 44px;
|
||||
border: 1px dashed #dcdfe6;
|
||||
border-radius: 6px;
|
||||
padding: 3px;
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
.cover-placeholder {
|
||||
width: 88px;
|
||||
height: 44px;
|
||||
border: 1px dashed #dcdfe6;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.seg {
|
||||
display: inline-flex;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
.seg-item {
|
||||
padding: 6px 14px;
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
cursor: pointer;
|
||||
border-left: 1px solid #dcdfe6;
|
||||
transition: all 0.2s ease;
|
||||
&:first-child {
|
||||
border-left: none;
|
||||
}
|
||||
&.active {
|
||||
background: #ecf5ff;
|
||||
color: #409eff;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -11,6 +11,12 @@ import PanelFlexOne from "./PanelFlexOne.vue";
|
||||
import PanelCarousel from "./PanelCarousel.vue";
|
||||
import PanelAdCube from "./PanelAdCube.vue";
|
||||
import PanelMenu from "./PanelMenu.vue";
|
||||
import PanelSearch from "./PanelSearch.vue";
|
||||
import PanelNotice from "./PanelNotice.vue";
|
||||
import PanelVideo from "./PanelVideo.vue";
|
||||
import PanelTitle from "./PanelTitle.vue";
|
||||
import PanelPromotion from "./PanelPromotion.vue";
|
||||
import PanelGoods from "./PanelGoods.vue";
|
||||
|
||||
// 图片魔方及其全部可选风格共用 PanelAdCube
|
||||
const adCubeTypes = [
|
||||
@@ -37,5 +43,11 @@ export default {
|
||||
flexOne: PanelFlexOne,
|
||||
carousel: PanelCarousel,
|
||||
menu: PanelMenu,
|
||||
search: PanelSearch,
|
||||
notice: PanelNotice,
|
||||
video: PanelVideo,
|
||||
title: PanelTitle,
|
||||
promotionDetail: PanelPromotion,
|
||||
goods: PanelGoods,
|
||||
...Object.fromEntries(adCubeTypes.map((t) => [t, PanelAdCube])),
|
||||
};
|
||||
|
||||
@@ -113,11 +113,11 @@ export const modelData = [
|
||||
list: [
|
||||
{
|
||||
title: "标题",
|
||||
color: "#fff",
|
||||
title1: '领取',
|
||||
color1: "#fff",
|
||||
bk_color: "#FF0000",
|
||||
textAlign: "center",
|
||||
color: "#282828",
|
||||
title1: '副标题',
|
||||
color1: "#999999",
|
||||
bk_color: "#FFFFFF",
|
||||
textAlign: "left",
|
||||
link: "",
|
||||
url: '',
|
||||
model: "link"
|
||||
@@ -473,14 +473,6 @@ export const modelData = [
|
||||
title: "标题",
|
||||
size: "88*88 (1:1)",
|
||||
model: "link"
|
||||
},
|
||||
{
|
||||
img: "https://i.loli.net/2020/12/05/eznDa3iNby5FkYL.png",
|
||||
url: "",
|
||||
link: "",
|
||||
title: "标题",
|
||||
size: "88*88 (1:1)",
|
||||
model: "link"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -502,74 +494,6 @@ export const modelData = [
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "goods",
|
||||
name: "商品分类",
|
||||
icon: "Goods",
|
||||
group: "base",
|
||||
img: "md-image",
|
||||
notAdd: true,
|
||||
notLink: true,
|
||||
notImg: true,
|
||||
options: {
|
||||
list: [
|
||||
{
|
||||
/**
|
||||
* 2021/12/9
|
||||
* 新增索引(‘index’)判断商品归属分类
|
||||
* 之前代码没有配置index也不会收到印象
|
||||
* 新建的楼层将采用索引判断分类
|
||||
*/
|
||||
titleWay: [
|
||||
{
|
||||
title: "精选",
|
||||
desc: "电子推荐",
|
||||
___index: 0,
|
||||
bindCategory:""
|
||||
},
|
||||
{
|
||||
title: "实惠",
|
||||
desc: "便宜好货",
|
||||
___index: 1,
|
||||
bindCategory:""
|
||||
},
|
||||
{
|
||||
title: "进口",
|
||||
desc: "国际自营",
|
||||
___index: 2,
|
||||
bindCategory:""
|
||||
},
|
||||
{
|
||||
title: "推荐",
|
||||
desc: "喂奶推荐",
|
||||
___index: 3,
|
||||
bindCategory:""
|
||||
}
|
||||
],
|
||||
listWay: [
|
||||
{
|
||||
img: "https://i.loli.net/2021/05/14/KTLSrOVJmEdX12A.png",
|
||||
price: "120",
|
||||
title:
|
||||
" 微软 (Microsoft) Xbox 无线控制器/手柄 湛蓝色 | 3.5mm耳机接口蓝牙连接 Xbox主机电脑平板通用",
|
||||
type: "精选",
|
||||
___index: 0
|
||||
},
|
||||
{
|
||||
img: "https://i.loli.net/2020/12/05/c9mptI5Pg8qJ6ny.png",
|
||||
title:
|
||||
"宏碁(acer) DP高清线1.2版 2K*4KDisplayPort公对公接线笔记本电脑显卡连接显示器视频线1.5米",
|
||||
price: "190",
|
||||
type: "精选",
|
||||
___index: 0
|
||||
},
|
||||
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
type: "notice",
|
||||
name: "公告",
|
||||
@@ -606,7 +530,66 @@ export const modelData = [
|
||||
subColor: "#fff",
|
||||
img: "md-image",
|
||||
options: {
|
||||
list: []
|
||||
list: [
|
||||
{
|
||||
type: "PINTUAN",
|
||||
title: "超值拼团",
|
||||
title1: "惊喜折扣",
|
||||
bk_color: "#e1212b",
|
||||
color1: "#fff",
|
||||
groupSuccess: "134人拼团成功",
|
||||
data: [
|
||||
{
|
||||
goodsName: "iPhone 16 Pro 5G智能手机 256GB 白色钛金属",
|
||||
price: 8999,
|
||||
originalPrice: 9999,
|
||||
thumbnail: "/demo/apple-phone.png",
|
||||
groupNum: 2,
|
||||
joined: 3
|
||||
},
|
||||
{
|
||||
goodsName: "Xiaomi 15 5G智能手机 12GB+256GB 白色",
|
||||
price: 3999,
|
||||
originalPrice: 4299,
|
||||
thumbnail: "/demo/xiaomi-phone.png",
|
||||
groupNum: 2,
|
||||
joined: 8
|
||||
},
|
||||
{
|
||||
goodsName: "vivo X200 5G智能手机 12GB+256GB 宝石蓝",
|
||||
price: 4199,
|
||||
originalPrice: 4599,
|
||||
thumbnail: "/demo/vivo-phone.png",
|
||||
groupNum: 2,
|
||||
joined: 5
|
||||
},
|
||||
{
|
||||
goodsName: "iPhone 16 5G智能手机 128GB 群青色",
|
||||
price: 5999,
|
||||
originalPrice: 6499,
|
||||
thumbnail: "/demo/apple-phone.png",
|
||||
groupNum: 2,
|
||||
joined: 12
|
||||
},
|
||||
{
|
||||
goodsName: "Redmi K80 5G智能手机 12GB+256GB 玄夜黑",
|
||||
price: 2499,
|
||||
originalPrice: 2799,
|
||||
thumbnail: "/demo/xiaomi-phone.png",
|
||||
groupNum: 2,
|
||||
joined: 21
|
||||
},
|
||||
{
|
||||
goodsName: "vivo S20 5G智能手机 8GB+256GB 凤羽金",
|
||||
price: 2299,
|
||||
originalPrice: 2599,
|
||||
thumbnail: "/demo/vivo-phone.png",
|
||||
groupNum: 2,
|
||||
joined: 16
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -624,7 +607,37 @@ export const modelData = [
|
||||
subColor: "#fff",
|
||||
img: "md-image",
|
||||
options: {
|
||||
list: []
|
||||
layoutStyle: "1",
|
||||
list: [
|
||||
{
|
||||
type: "LIVE",
|
||||
title: "精彩直播",
|
||||
title1: "精彩抢先看",
|
||||
bk_color: "#e1212b",
|
||||
color1: "#fff",
|
||||
data: [
|
||||
{
|
||||
title: "直播标题直播标题直播标题直播标题",
|
||||
anchorName: "樱桃小丸子",
|
||||
status: "LIVE",
|
||||
cover: "/demo/apple-phone.png"
|
||||
},
|
||||
{
|
||||
title: "直播标题直播标题直播标题直播标题",
|
||||
anchorName: "樱桃小丸子",
|
||||
status: "REPLAY",
|
||||
cover: "/demo/xiaomi-phone.png"
|
||||
},
|
||||
{
|
||||
title: "直播标题直播标题直播标题直播标题",
|
||||
anchorName: "樱桃小丸子",
|
||||
status: "PREVIEW",
|
||||
startTime: "7/29 10:00",
|
||||
cover: "/demo/vivo-phone.png"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -642,7 +655,38 @@ export const modelData = [
|
||||
subColor: "#fff",
|
||||
img: "md-image",
|
||||
options: {
|
||||
list: []
|
||||
layoutStyle: "1",
|
||||
list: [
|
||||
{
|
||||
type: "SECKILL",
|
||||
title: "限时秒杀",
|
||||
title1: "邀请你的好友来参与",
|
||||
bk_color: "#e1212b",
|
||||
color1: "#fff",
|
||||
data: [
|
||||
{
|
||||
goodsName: "一家很有品的店",
|
||||
price: 234,
|
||||
thumbnail: "/demo/apple-phone.png"
|
||||
},
|
||||
{
|
||||
goodsName: "一家很有品的店",
|
||||
price: 177,
|
||||
thumbnail: "/demo/xiaomi-phone.png"
|
||||
},
|
||||
{
|
||||
goodsName: "一家很有品的店",
|
||||
price: 177,
|
||||
thumbnail: "/demo/vivo-phone.png"
|
||||
},
|
||||
{
|
||||
goodsName: "一家很有品的店",
|
||||
price: 234,
|
||||
thumbnail: "/demo/apple-phone.png"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -660,7 +704,42 @@ export const modelData = [
|
||||
subColor: "#fff",
|
||||
img: "md-image",
|
||||
options: {
|
||||
list: []
|
||||
layoutStyle: "1",
|
||||
list: [
|
||||
{
|
||||
type: "KANJIA",
|
||||
title: "砍价免费拿",
|
||||
title1: "邀请好友来砍价",
|
||||
bk_color: "#e1212b",
|
||||
color1: "#fff",
|
||||
data: [
|
||||
{
|
||||
goodsName: "一家很有品的店",
|
||||
price: 234,
|
||||
purchasePrice: 99,
|
||||
thumbnail: "/demo/apple-phone.png"
|
||||
},
|
||||
{
|
||||
goodsName: "一家很有品的店",
|
||||
price: 177,
|
||||
purchasePrice: 77,
|
||||
thumbnail: "/demo/xiaomi-phone.png"
|
||||
},
|
||||
{
|
||||
goodsName: "一家很有品的店",
|
||||
price: 177,
|
||||
purchasePrice: 77,
|
||||
thumbnail: "/demo/vivo-phone.png"
|
||||
},
|
||||
{
|
||||
goodsName: "一家很有品的店",
|
||||
price: 234,
|
||||
purchasePrice: 99,
|
||||
thumbnail: "/demo/apple-phone.png"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -754,11 +833,39 @@ export const modelData = [
|
||||
group: "marketing",
|
||||
img: "md-image",
|
||||
options: {
|
||||
list: [{ title: "积分商城", count: 3 }],
|
||||
bgColor: "#FFFFFF",
|
||||
paddingX: 0,
|
||||
pageGap: 10,
|
||||
borderRadius: 0
|
||||
layoutStyle: "1",
|
||||
list: [
|
||||
{
|
||||
title: "积分兑好礼",
|
||||
count: 6,
|
||||
data: [
|
||||
{
|
||||
goodsName: "一家很有品的店",
|
||||
points: 200,
|
||||
originalPrice: 234,
|
||||
thumbnail: "/demo/apple-phone.png"
|
||||
},
|
||||
{
|
||||
goodsName: "一家很有品的店",
|
||||
points: 150,
|
||||
originalPrice: 177,
|
||||
thumbnail: "/demo/xiaomi-phone.png"
|
||||
},
|
||||
{
|
||||
goodsName: "一家很有品的店",
|
||||
points: 150,
|
||||
originalPrice: 177,
|
||||
thumbnail: "/demo/vivo-phone.png"
|
||||
},
|
||||
{
|
||||
goodsName: "一家很有品的店",
|
||||
points: 200,
|
||||
originalPrice: 234,
|
||||
thumbnail: "/demo/apple-phone.png"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -768,11 +875,50 @@ export const modelData = [
|
||||
group: "marketing",
|
||||
img: "md-image",
|
||||
options: {
|
||||
list: [{ title: "优惠券", count: 3 }],
|
||||
bgColor: "#FFFFFF",
|
||||
paddingX: 0,
|
||||
pageGap: 10,
|
||||
borderRadius: 0
|
||||
layoutStyle: "1",
|
||||
list: [
|
||||
{
|
||||
title: "优惠券",
|
||||
count: 5,
|
||||
data: [
|
||||
{
|
||||
id: 1,
|
||||
couponType: "PRICE",
|
||||
price: 70,
|
||||
consumeThreshold: 500,
|
||||
storeName: "platform"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
couponType: "PRICE",
|
||||
price: 50,
|
||||
consumeThreshold: 300,
|
||||
storeName: "platform"
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
couponType: "PRICE",
|
||||
price: 30,
|
||||
consumeThreshold: 200,
|
||||
storeName: "platform"
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
couponType: "PRICE",
|
||||
price: 20,
|
||||
consumeThreshold: 100,
|
||||
storeName: "platform"
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
couponType: "PRICE",
|
||||
price: 10,
|
||||
consumeThreshold: 0,
|
||||
storeName: "platform"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -802,5 +948,48 @@ export const modelData = [
|
||||
pageGap: 10,
|
||||
borderRadius: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "goods",
|
||||
name: "商品列表",
|
||||
icon: "Goods",
|
||||
group: "base",
|
||||
img: "md-image",
|
||||
notAdd: true,
|
||||
notLink: true,
|
||||
notImg: true,
|
||||
options: {
|
||||
list: [
|
||||
{
|
||||
layout: "list",
|
||||
showName: true,
|
||||
showPrice: true,
|
||||
showSales: true,
|
||||
cartShow: true,
|
||||
cartStyle: "plus",
|
||||
cartAction: "detail",
|
||||
dataMode: "category",
|
||||
categoryId: "",
|
||||
brandId: "",
|
||||
storeId: "",
|
||||
goodsList: [],
|
||||
count: 6,
|
||||
sortType: "default"
|
||||
}
|
||||
],
|
||||
bgColor1: "#FFFFFF",
|
||||
bgColor2: "#FFFFFF",
|
||||
titleColor: "#282828",
|
||||
toneMode: "theme",
|
||||
priceColor: "#FF5C58",
|
||||
salesColor: "#666666",
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
paddingX: 12,
|
||||
contentGap: 10,
|
||||
pageGap: 10,
|
||||
borderRadius: 0,
|
||||
contentRadius: 7
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<div class="ctrl-row">
|
||||
<div class="ctrl-label">{{ label }}</div>
|
||||
<div class="ctrl-body gradient-body">
|
||||
<div class="gradient-row">
|
||||
<el-color-picker
|
||||
:model-value="start"
|
||||
@update:model-value="$emit('update:start', $event)"
|
||||
/>
|
||||
<el-input
|
||||
:model-value="start"
|
||||
class="ctrl-color-input"
|
||||
placeholder="#FFFFFF"
|
||||
@update:model-value="$emit('update:start', $event)"
|
||||
/>
|
||||
<el-button link type="primary" @click="$emit('update:start', defaultStart)">重置</el-button>
|
||||
</div>
|
||||
<div class="gradient-row">
|
||||
<el-color-picker
|
||||
:model-value="end"
|
||||
@update:model-value="$emit('update:end', $event)"
|
||||
/>
|
||||
<el-input
|
||||
:model-value="end"
|
||||
class="ctrl-color-input"
|
||||
placeholder="#FFFFFF"
|
||||
@update:model-value="$emit('update:end', $event)"
|
||||
/>
|
||||
<el-button link type="primary" @click="$emit('update:end', defaultEnd)">重置</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "GradientField",
|
||||
props: {
|
||||
start: { type: String, default: "" },
|
||||
end: { type: String, default: "" },
|
||||
defaultStart: { type: String, default: "" },
|
||||
defaultEnd: { type: String, default: "" },
|
||||
label: { type: String, default: "背景颜色" },
|
||||
},
|
||||
emits: ["update:start", "update:end"],
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "./field-common.scss";
|
||||
|
||||
.gradient-body {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.gradient-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
position: relative;
|
||||
|
||||
// 两个色板之间的连接线(对齐色板中心)
|
||||
& + .gradient-row::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
top: -14px;
|
||||
width: 2px;
|
||||
height: 10px;
|
||||
background: #dcdfe6;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -6,11 +6,12 @@
|
||||
}
|
||||
|
||||
.ctrl-label {
|
||||
width: 72px;
|
||||
width: 90px;
|
||||
flex-shrink: 0;
|
||||
line-height: 32px;
|
||||
color: #606266;
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ctrl-body {
|
||||
|
||||
179
manager/src/views/page-decoration/wap/controls/panel-common.scss
Normal file
179
manager/src/views/page-decoration/wap/controls/panel-common.scss
Normal file
@@ -0,0 +1,179 @@
|
||||
// 新配置面板共用的骨架样式(灰底 + 白色卡片 + 头部 tabs)
|
||||
// 使用方式:面板根类名需与 .panel-root 一起用,或在面板 scoped 样式里 @import 后把根类加上
|
||||
.panel-search,
|
||||
.panel-promotion,
|
||||
.panel-coupon,
|
||||
.panel-title,
|
||||
.panel-shop,
|
||||
.panel-goods,
|
||||
.panel-notice,
|
||||
.panel-video,
|
||||
.panel-article,
|
||||
.panel-page-setting {
|
||||
background: #f5f7fa;
|
||||
margin: 0 -20px;
|
||||
padding: 0 12px 12px;
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 50px;
|
||||
margin-bottom: 12px;
|
||||
padding: 0 14px;
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
|
||||
box-sizing: border-box;
|
||||
.panel-name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
.panel-tabs {
|
||||
display: inline-flex;
|
||||
background: #f4f4f5;
|
||||
border-radius: 8px;
|
||||
padding: 3px;
|
||||
.panel-tab {
|
||||
padding: 5px 14px;
|
||||
font-size: 13px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
color: #606266;
|
||||
transition: all 0.2s ease;
|
||||
&.active {
|
||||
background: #ffffff;
|
||||
color: #409eff;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
}
|
||||
}
|
||||
.panel-card {
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
padding: 14px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
.panel-card-fill {
|
||||
flex: 1;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
// 面板根容器改为纵向 flex,让最后一张卡片可撑满到底
|
||||
.panel-flex-one,
|
||||
.panel-carousel,
|
||||
.panel-ad-cube,
|
||||
.panel-menu,
|
||||
.panel-search,
|
||||
.panel-notice,
|
||||
.panel-video,
|
||||
.panel-title,
|
||||
.panel-integral,
|
||||
.panel-coupon,
|
||||
.panel-article,
|
||||
.panel-shop,
|
||||
.panel-goods,
|
||||
.panel-blank,
|
||||
.panel-line,
|
||||
.panel-rich-text,
|
||||
.panel-promotion,
|
||||
.panel-page-setting {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.section-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
.section-tip {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.setting-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 14px 0;
|
||||
.setting-label {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
.setting-value {
|
||||
margin-left: 8px;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
}
|
||||
.seg {
|
||||
display: inline-flex;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
.seg-item {
|
||||
padding: 6px 14px;
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
cursor: pointer;
|
||||
border-left: 1px solid #dcdfe6;
|
||||
transition: all 0.2s ease;
|
||||
&:first-child {
|
||||
border-left: none;
|
||||
}
|
||||
&.active {
|
||||
background: #ecf5ff;
|
||||
color: #409eff;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 选项平铺选择器:一行两个,超出换行
|
||||
.layout-seg {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
.seg-item {
|
||||
flex: 1 0 40%;
|
||||
padding: 6px 0;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
box-sizing: border-box;
|
||||
&:nth-child(3) {
|
||||
border-left: none;
|
||||
}
|
||||
&:nth-child(n + 3) {
|
||||
border-top: 1px solid #dcdfe6;
|
||||
}
|
||||
}
|
||||
&.layout-seg-3 {
|
||||
flex-wrap: nowrap;
|
||||
.seg-item {
|
||||
flex: 1 1 0;
|
||||
&:nth-child(3) {
|
||||
border-left: 1px solid #dcdfe6;
|
||||
}
|
||||
&:nth-child(n + 3) {
|
||||
border-top: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 选项平铺选择器(独立用法):一行三个
|
||||
.layout-seg-3 {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
width: 100%;
|
||||
.seg-item {
|
||||
flex: 1 1 0;
|
||||
padding: 6px 0;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
@@ -75,7 +75,7 @@
|
||||
padding: 0 20px;
|
||||
height: calc(100vh - 96px);
|
||||
overflow-y: auto;
|
||||
padding-bottom: 120px;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.decorate-title {
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<!-- 老组件遗留配置分支 -->
|
||||
<template v-else>
|
||||
<el-alert
|
||||
v-if="res.name == '商品分类'"
|
||||
v-if="res.name == '商品列表'"
|
||||
type="warning"
|
||||
title="装修提示"
|
||||
:closable="false"
|
||||
@@ -497,7 +497,7 @@
|
||||
>
|
||||
|
||||
<liliDialog
|
||||
ref="liliDialog"
|
||||
ref="liliDialogRef"
|
||||
@selectedLink="selectedLink"
|
||||
@selectedGoodsData="selectedGoodsData"
|
||||
></liliDialog>
|
||||
@@ -580,7 +580,7 @@ const hidePanelTitle = computed(
|
||||
);
|
||||
|
||||
// hotzoneRef/ossManageRef:模板 ref 与组件 import 同名,改名避免互相覆盖
|
||||
const liliDialog = ref(null);
|
||||
const liliDialogRef = ref(null);
|
||||
const hotzoneRef = ref(null);
|
||||
const ossManageRef = ref(null);
|
||||
|
||||
@@ -748,9 +748,9 @@ function clickDrawer(item, index) {
|
||||
}
|
||||
// 打开图片选择器
|
||||
function liliDialogFlag(flag) {
|
||||
liliDialog.value.clearGoodsSelected();
|
||||
liliDialog.value.goodsFlag = flag;
|
||||
liliDialog.value.flag = true;
|
||||
liliDialogRef.value.clearGoodsSelected();
|
||||
liliDialogRef.value.goodsFlag = flag;
|
||||
liliDialogRef.value.flag = true;
|
||||
}
|
||||
|
||||
function changeTextAlign(val) {
|
||||
|
||||
@@ -32,8 +32,9 @@
|
||||
</div>
|
||||
|
||||
<div class="model-view-content">
|
||||
<div class="content">
|
||||
<div class="wap-title">首页</div>
|
||||
<div class="page-setting-entry" @click="showPageSetting">页面设置</div>
|
||||
<div class="content" :style="canvasStyle">
|
||||
<div class="wap-title" :style="titleBarStyle">{{ contentData.title || "页面标题" }}</div>
|
||||
<draggable
|
||||
class="draggable"
|
||||
group="model"
|
||||
@@ -46,14 +47,14 @@
|
||||
<template #item="{ element, index }">
|
||||
<div class="list">
|
||||
<div class="floor-name">{{ element.name || element.type }}</div>
|
||||
<component
|
||||
<div
|
||||
v-if="templates[element.type]"
|
||||
class="component"
|
||||
:class="{ active: selected == index }"
|
||||
@click="handleComponent(element, index)"
|
||||
:is="templates[element.type]"
|
||||
:res="element.options"
|
||||
></component>
|
||||
>
|
||||
<component :is="templates[element.type]" :res="element.options"></component>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="component unknown-component"
|
||||
@@ -92,6 +93,7 @@
|
||||
v-if="decorateData"
|
||||
:res="decorateData"
|
||||
></decorate>
|
||||
<PanelPageSetting v-else :res="contentData" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -132,6 +134,7 @@ import templates from "./template/index";
|
||||
import Draggable from "vuedraggable";
|
||||
import { modelData } from "./config";
|
||||
import decorate from "./decorate";
|
||||
import PanelPageSetting from "./config-panel/PanelPageSetting.vue";
|
||||
import * as API_Other from "@/api/other";
|
||||
import * as API_Promotions from "@/api/promotion";
|
||||
import { getStore } from "@/libs/storage";
|
||||
@@ -168,8 +171,28 @@ const iconMap = {
|
||||
|
||||
const qrcode = ref(""); // 二维码
|
||||
const selected = ref(0); // 已选下标
|
||||
// 页面设置默认值(老数据没有这些字段,加载时合并)
|
||||
const PAGE_SETTING_DEFAULTS = {
|
||||
name: "",
|
||||
title: "",
|
||||
bgType: "color", // color | image
|
||||
bgColor: "#F5F5F5",
|
||||
bgImage: "",
|
||||
bgFill: "stretch", // stretch 拉伸 | contain 缩放 | cover 填充
|
||||
topColor: "#ffffff",
|
||||
titleColor: "black", // black | white
|
||||
};
|
||||
function mergePageData(data) {
|
||||
const source = data && typeof data === "object" ? data : {};
|
||||
return {
|
||||
...PAGE_SETTING_DEFAULTS,
|
||||
...source,
|
||||
list: Array.isArray(source.list) ? source.list : [],
|
||||
};
|
||||
}
|
||||
const contentData = ref({
|
||||
// 总数据
|
||||
...PAGE_SETTING_DEFAULTS,
|
||||
list: [],
|
||||
});
|
||||
const decorateData = ref(""); // 装修数据
|
||||
@@ -180,9 +203,39 @@ watch(
|
||||
(val) => {
|
||||
appStore.styleStore = val;
|
||||
},
|
||||
{ deep: true }
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
|
||||
// 画布背景:背景色或背景图(拉伸/缩放/填充)
|
||||
const canvasStyle = computed(() => {
|
||||
const d = contentData.value;
|
||||
if (d.bgType === "image" && d.bgImage) {
|
||||
const size =
|
||||
{ stretch: "100% 100%", contain: "contain", cover: "cover" }[d.bgFill] ||
|
||||
"100% 100%";
|
||||
return {
|
||||
backgroundImage: `url(${d.bgImage})`,
|
||||
backgroundSize: size,
|
||||
backgroundRepeat: "no-repeat",
|
||||
backgroundColor: d.bgColor || "#F5F5F5",
|
||||
};
|
||||
}
|
||||
return { backgroundColor: d.bgColor || "#F5F5F5" };
|
||||
});
|
||||
// 画布顶部标题栏:背景用顶部颜色,文字色随标题颜色
|
||||
const titleBarStyle = computed(() => {
|
||||
const d = contentData.value;
|
||||
return {
|
||||
background: d.topColor || "#ffffff",
|
||||
color: d.titleColor === "black" ? "#000" : "#fff",
|
||||
};
|
||||
});
|
||||
// 清空楼层选中,右侧回到页面设置面板
|
||||
function showPageSetting() {
|
||||
selected.value = -1;
|
||||
decorateData.value = "";
|
||||
}
|
||||
|
||||
// 组件面板三类分组(drawer 标记的是图片魔方的可选风格,不进左侧组件列表)
|
||||
const modelGroups = computed(() => {
|
||||
const groups = [
|
||||
@@ -232,15 +285,25 @@ function init() {
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
const data = JSON.parse(cache);
|
||||
contentData.value = data;
|
||||
contentData.value = mergePageData(data);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
if (!route.query.id) return false;
|
||||
if (!route.query.id) {
|
||||
// 新建页面:模板名称取路由参数
|
||||
if (!contentData.value.name) {
|
||||
contentData.value.name = route.query.name || "";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
API_Other.getHomeData(route.query.id).then((res) => {
|
||||
contentData.value = JSON.parse(res.result.pageData);
|
||||
const parsed = JSON.parse(res.result.pageData);
|
||||
contentData.value = mergePageData(parsed);
|
||||
if (!contentData.value.name) {
|
||||
contentData.value.name = route.query.name || "";
|
||||
}
|
||||
const first = contentData.value.list[0];
|
||||
if (first && templates[first.type]) {
|
||||
handleComponent(first, 0);
|
||||
@@ -416,4 +479,32 @@ function handleMove() {
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
@import "./style.scss";
|
||||
|
||||
// 页面设置:画布容器支持悬浮入口、画布背景可透出到拖拽区
|
||||
.model-view-content {
|
||||
position: relative;
|
||||
}
|
||||
.page-setting-entry {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 16px;
|
||||
z-index: 200;
|
||||
padding: 6px 14px;
|
||||
font-size: 13px;
|
||||
color: #409eff;
|
||||
background: #ffffff;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
border-color: #409eff;
|
||||
}
|
||||
}
|
||||
.content > .draggable {
|
||||
background: transparent;
|
||||
}
|
||||
.wap-title {
|
||||
border-bottom: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,16 +5,17 @@
|
||||
<el-button size="default" @click="goBack">返回</el-button>
|
||||
<div class="page-name">
|
||||
<template v-if="!editingName">
|
||||
<span class="page-name-label">当前页面:{{ submitWay.name }}</span>
|
||||
<span class="page-name-label">当前页面:{{ pageName }}</span>
|
||||
<el-icon class="page-name-edit" @click="startEditName"><Edit /></el-icon>
|
||||
</template>
|
||||
<el-input
|
||||
v-else
|
||||
ref="nameInput"
|
||||
v-model="submitWay.name"
|
||||
v-model="pageName"
|
||||
size="small"
|
||||
style="width: 200px"
|
||||
maxlength="30"
|
||||
placeholder="请输入模板名称"
|
||||
@blur="editingName = false"
|
||||
@keyup.enter="editingName = false"
|
||||
/>
|
||||
@@ -43,7 +44,7 @@
|
||||
<div v-if="progress">
|
||||
<div class="model-item">
|
||||
模板名称
|
||||
<el-input style="width: 200px" v-model="submitWay.name" />
|
||||
<el-input style="width: 200px" v-model="pageName" placeholder="请输入模板名称" />
|
||||
</div>
|
||||
<div class="model-item">
|
||||
是否立即发布
|
||||
@@ -66,7 +67,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { reactive, ref, watch, onMounted, nextTick } from "vue";
|
||||
import { reactive, ref, watch, onMounted, nextTick, computed } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import * as API_Other from "@/api/other.js";
|
||||
import { Edit } from "@element-plus/icons-vue";
|
||||
@@ -119,10 +120,34 @@ function startEditName() {
|
||||
const submitWay = reactive({
|
||||
// 表单信息
|
||||
pageShow: route.query.type || false,
|
||||
name: route.query.name || "模板名称",
|
||||
pageClientType: "H5"
|
||||
});
|
||||
|
||||
// 模板名称与「页面设置」面板共用 store 中的同一份数据,双向同步
|
||||
const pageName = computed({
|
||||
get() {
|
||||
const store = appStore.styleStore;
|
||||
if (store && typeof store === "object") {
|
||||
return store.name || "";
|
||||
}
|
||||
return route.query.name || "";
|
||||
},
|
||||
set(val) {
|
||||
if (!appStore.styleStore || typeof appStore.styleStore !== "object") {
|
||||
appStore.styleStore = { ...appStore.styleStore, list: [] };
|
||||
}
|
||||
appStore.styleStore.name = val;
|
||||
},
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// 新建页面时把路由带来的模板名称写入 store(不覆盖已有名称)
|
||||
const store = appStore.styleStore;
|
||||
if (store && typeof store === "object" && !store.name && route.query.name) {
|
||||
store.name = route.query.name;
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.pagetype,
|
||||
(val) => {
|
||||
@@ -171,7 +196,23 @@ function handleSpinShow() {
|
||||
|
||||
// 填写是否发布,模板名称之后保存
|
||||
function save() {
|
||||
if (
|
||||
// 楼层装修页面(非开屏/弹窗广告)需校验页面设置的必填项
|
||||
const isFloorPage =
|
||||
route.query.pagetype != "ALERT" &&
|
||||
route.query.pagetype != "OPEN_SCREEN_ANIMATION";
|
||||
if (isFloorPage) {
|
||||
if (!pageName.value || !pageName.value.trim()) {
|
||||
Message.error("请填写模板名称");
|
||||
return false;
|
||||
}
|
||||
const title = appStore.styleStore && appStore.styleStore.title;
|
||||
if (!title || !String(title).trim()) {
|
||||
Message.error("请填写页面标题");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
appStore.styleStore == void 0 &&
|
||||
(route.query.pagetype && route.query.pagetype != 'ALERT' && route.query.pagetype != 'OPEN_SCREEN_ANIMATION')
|
||||
) {
|
||||
@@ -183,6 +224,7 @@ function save() {
|
||||
? (submitWay.pageShow = "OPEN")
|
||||
: (submitWay.pageShow = "CLOSE");
|
||||
|
||||
submitWay.name = pageName.value;
|
||||
submitWay.pageData = JSON.stringify(appStore.styleStore);
|
||||
submitWay.pageType = "INDEX";
|
||||
submitWay.pageType = route.query.pagetype || "INDEX";
|
||||
@@ -202,7 +244,7 @@ function updateHome() {
|
||||
progress.value = false;
|
||||
API_Other.updateHome(route.query.id, {
|
||||
pageData: JSON.stringify(appStore.styleStore),
|
||||
name: submitWay.name,
|
||||
name: pageName.value,
|
||||
pageShow: submitWay.pageShow,
|
||||
pageType: submitWay.pageType,
|
||||
pageClientType: "H5",
|
||||
|
||||
@@ -137,7 +137,7 @@
|
||||
}
|
||||
.floor-name {
|
||||
position: absolute;
|
||||
left: -78px;
|
||||
right: calc(100% + 8px);
|
||||
top: 8px;
|
||||
z-index: 60;
|
||||
background: #ffffff;
|
||||
@@ -217,9 +217,10 @@
|
||||
}
|
||||
.model-config {
|
||||
overflow-y: auto;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.active {
|
||||
box-sizing: border-box;
|
||||
border: 1px solid $theme_color;
|
||||
border: 2px solid $theme_color;
|
||||
}
|
||||
|
||||
@@ -1,36 +1,113 @@
|
||||
<template>
|
||||
<div class="tpl-article" :style="wrapperStyle">
|
||||
<div class="tpl-article" :class="{ styled: styled }" :style="wrapperStyle">
|
||||
<div class="article-title" v-if="title">{{ title }}</div>
|
||||
<div class="article-item" v-for="i in 3" :key="i">
|
||||
<div class="article-text">
|
||||
<div class="article-name">文章标题示例文章标题示例</div>
|
||||
<div class="article-time">2026-10-12</div>
|
||||
<template v-if="articleList.length">
|
||||
<div class="article-item" v-for="item in articleList" :key="item.id">
|
||||
<div class="article-text">
|
||||
<div class="article-name" :style="nameStyle">{{ item.title }}</div>
|
||||
<div class="article-time" v-if="item.createTime">{{ item.createTime }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="article-thumb"></div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="article-item" v-for="i in 3" :key="i">
|
||||
<div class="article-text">
|
||||
<div class="article-name" :style="nameStyle">文章标题示例文章标题示例</div>
|
||||
</div>
|
||||
<div class="article-thumb"></div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getArticle } from "@/api/pages";
|
||||
|
||||
export default {
|
||||
name: "tpl_article",
|
||||
props: {
|
||||
res: { type: Object, default: () => ({}) },
|
||||
},
|
||||
data() {
|
||||
return { articleList: [] };
|
||||
},
|
||||
computed: {
|
||||
first() {
|
||||
return (this.res.list && this.res.list[0]) || {};
|
||||
},
|
||||
title() {
|
||||
return (this.res.list && this.res.list[0] && this.res.list[0].title) || "";
|
||||
return this.first.title || "";
|
||||
},
|
||||
count() {
|
||||
return this.first.count || 3;
|
||||
},
|
||||
categoryId() {
|
||||
return this.first.categoryId || "";
|
||||
},
|
||||
// 面板保存过样式字段才启用自定义样式,老数据保持原样
|
||||
styled() {
|
||||
return this.res && this.res.paddingTop !== undefined;
|
||||
},
|
||||
nameStyle() {
|
||||
return this.styled && this.res.titleColor
|
||||
? { color: this.res.titleColor }
|
||||
: {};
|
||||
},
|
||||
wrapperStyle() {
|
||||
if (!this.styled) {
|
||||
return {
|
||||
background: this.res.bgColor || "#FFFFFF",
|
||||
padding: `10px ${this.res.paddingX || 0}px`,
|
||||
marginBottom: `${this.res.pageGap !== undefined ? this.res.pageGap : 10}px`,
|
||||
borderRadius: `${this.res.borderRadius || 0}px`,
|
||||
overflow: "hidden",
|
||||
};
|
||||
}
|
||||
const res = this.res;
|
||||
const bg =
|
||||
!res.bgColor2 || res.bgColor1 === res.bgColor2
|
||||
? res.bgColor1
|
||||
: `linear-gradient(180deg, ${res.bgColor1}, ${res.bgColor2})`;
|
||||
const px = (v) => `${(Number(v) || 0) / 2}px`;
|
||||
return {
|
||||
background: this.res.bgColor || "#FFFFFF",
|
||||
padding: `10px ${this.res.paddingX || 0}px`,
|
||||
marginBottom: `${this.res.pageGap !== undefined ? this.res.pageGap : 10}px`,
|
||||
borderRadius: `${this.res.borderRadius || 0}px`,
|
||||
overflow: "hidden",
|
||||
background: bg,
|
||||
padding: `${px(res.paddingTop)} ${px(res.paddingX)} ${px(res.paddingBottom)}`,
|
||||
marginBottom: px(res.pageGap),
|
||||
borderRadius: px(res.borderRadius),
|
||||
overflow: Number(res.borderRadius) > 0 ? "hidden" : "visible",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
rowGap: px(res.contentGap),
|
||||
};
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
categoryId() {
|
||||
this.loadArticles();
|
||||
},
|
||||
count() {
|
||||
this.loadArticles();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.loadArticles();
|
||||
},
|
||||
methods: {
|
||||
loadArticles() {
|
||||
const params = { pageNumber: 1, pageSize: this.count };
|
||||
if (this.categoryId) params.categoryId = this.categoryId;
|
||||
getArticle(params)
|
||||
.then((res) => {
|
||||
const records = res && res.result && res.result.records;
|
||||
this.articleList = Array.isArray(records)
|
||||
? records.slice(0, this.count)
|
||||
: [];
|
||||
})
|
||||
.catch(() => {
|
||||
this.articleList = [];
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -46,6 +123,10 @@ export default {
|
||||
padding: 8px 12px;
|
||||
border-top: 1px solid #f5f5f5;
|
||||
}
|
||||
.tpl-article.styled .article-item {
|
||||
border-top: none;
|
||||
padding: 0;
|
||||
}
|
||||
.article-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
@@ -3,21 +3,41 @@
|
||||
<div class="banner-box" :style="cardMode ? {} : boxStyle">
|
||||
<el-carousel
|
||||
class="carousel"
|
||||
:class="{ 'card-mode': cardMode }"
|
||||
v-if="res"
|
||||
v-if="res && !cardMode"
|
||||
autoplay
|
||||
:interval="5000"
|
||||
height="150px"
|
||||
:type="cardMode ? 'card' : ''"
|
||||
:indicator-position="customIndicator ? 'none' : ''"
|
||||
@change="onChange"
|
||||
>
|
||||
<el-carousel-item v-for="(item, index) in res.list" :key="index">
|
||||
<div class="slide-wrap">
|
||||
<img class="image-mode" :style="boxStyle" :src="item.img" />
|
||||
<img alt="轮播图" class="image-mode" :style="boxStyle" :src="item.img" />
|
||||
</div>
|
||||
</el-carousel-item>
|
||||
</el-carousel>
|
||||
<!-- 卡片风格:当前图 82% 居中放大,两侧 9% 露出并缩小,循环切换 -->
|
||||
<div
|
||||
v-if="res && cardMode"
|
||||
ref="cardPreview"
|
||||
class="card-preview"
|
||||
@mouseenter="stopAuto"
|
||||
@mouseleave="startAuto"
|
||||
>
|
||||
<div
|
||||
class="card-track"
|
||||
:style="{ transform: `translateX(${trackOffset}px)` }"
|
||||
>
|
||||
<div
|
||||
class="card-slide"
|
||||
:class="{ active: index === current }"
|
||||
v-for="(item, index) in res.list"
|
||||
:key="index"
|
||||
>
|
||||
<img alt="轮播图" class="card-img" :style="boxStyle" :src="item.img" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="customIndicator && res.indicatorStyle !== 'none' && res.list.length > 1"
|
||||
class="indicator"
|
||||
@@ -35,12 +55,15 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, computed } from "vue";
|
||||
import { ref, computed, watch, onBeforeUnmount } from "vue";
|
||||
|
||||
defineOptions({ title: "导航栏" });
|
||||
const props = defineProps(["res"]);
|
||||
|
||||
const current = ref(0);
|
||||
const previewWidth = ref(0);
|
||||
const cardPreview = ref(null);
|
||||
let timer = null;
|
||||
|
||||
// 配置过样式字段才启用自定义样式/指示器,老数据保持原样
|
||||
const customIndicator = computed(
|
||||
@@ -52,6 +75,10 @@ const indicatorColor = computed(() =>
|
||||
? props.res.indicatorColor
|
||||
: "#409eff"
|
||||
);
|
||||
// 卡片宽 82%,两侧各露出 9%
|
||||
const trackOffset = computed(
|
||||
() => previewWidth.value * 0.09 - current.value * previewWidth.value * 0.82
|
||||
);
|
||||
const wrapperStyle = computed(() => {
|
||||
const res = props.res || {};
|
||||
if (res.paddingTop === undefined) return {};
|
||||
@@ -77,6 +104,48 @@ const boxStyle = computed(() => {
|
||||
function onChange(index) {
|
||||
current.value = index;
|
||||
}
|
||||
|
||||
function measure() {
|
||||
if (cardPreview.value) {
|
||||
previewWidth.value = cardPreview.value.offsetWidth;
|
||||
}
|
||||
}
|
||||
|
||||
function startAuto() {
|
||||
stopAuto();
|
||||
if (!cardMode.value || !props.res.list || props.res.list.length < 2) return;
|
||||
timer = setInterval(() => {
|
||||
current.value = (current.value + 1) % props.res.list.length;
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function stopAuto() {
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
cardMode,
|
||||
(val) => {
|
||||
stopAuto();
|
||||
if (val) {
|
||||
measure();
|
||||
startAuto();
|
||||
}
|
||||
},
|
||||
{ immediate: true, flush: "post" }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.res.list && props.res.list.length,
|
||||
(len) => {
|
||||
if (current.value >= len) current.value = 0;
|
||||
}
|
||||
);
|
||||
|
||||
onBeforeUnmount(stopAuto);
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "./tpl.scss";
|
||||
@@ -98,16 +167,34 @@ function onChange(index) {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
}
|
||||
// 卡片模式:卡片铺满轮播项,去掉 el-carousel 默认卡片白底阴影
|
||||
.card-mode {
|
||||
:deep(.el-carousel__item--card) {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
:deep(.el-carousel__mask) {
|
||||
background: transparent;
|
||||
.card-preview {
|
||||
overflow: hidden;
|
||||
height: 150px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.card-track {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: transform 0.5s ease;
|
||||
width: 100%;
|
||||
}
|
||||
.card-slide {
|
||||
flex: 0 0 82%;
|
||||
padding: 0 3px;
|
||||
box-sizing: border-box;
|
||||
transform: scale(0.9);
|
||||
transition: transform 0.4s ease;
|
||||
&.active {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
.card-img {
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
}
|
||||
.indicator {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div
|
||||
class="tpl-blank"
|
||||
:style="{
|
||||
background: res.bgColor ?? '#f5f5f5',
|
||||
background: blankBg,
|
||||
height: (res.height ?? 10) + 'px',
|
||||
margin: `0 ${res.paddingX ?? 0}px ${res.pageGap ?? 10}px`,
|
||||
}"
|
||||
@@ -15,5 +15,16 @@ export default {
|
||||
props: {
|
||||
res: { type: Object, default: () => ({}) },
|
||||
},
|
||||
computed: {
|
||||
blankBg() {
|
||||
const res = this.res || {};
|
||||
if (res.bgColor1 !== undefined) {
|
||||
return !res.bgColor2 || res.bgColor1 === res.bgColor2
|
||||
? res.bgColor1
|
||||
: `linear-gradient(180deg, ${res.bgColor1}, ${res.bgColor2})`;
|
||||
}
|
||||
return res.bgColor || "#f5f5f5";
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<template>
|
||||
<div class="tpl-coupon" :style="wrapperStyle">
|
||||
<div class="tpl-coupon" :class="{ styled: styled }" :style="wrapperStyle">
|
||||
<div class="coupon-title" v-if="title">{{ title }}</div>
|
||||
<div class="coupon-list">
|
||||
|
||||
<!-- 老数据保持原样 -->
|
||||
<div v-if="!styled" class="coupon-list">
|
||||
<div class="coupon-item" v-for="i in 2" :key="i">
|
||||
<div class="coupon-left">
|
||||
<div class="coupon-amount">
|
||||
@@ -15,6 +17,98 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 样式一:红色大卡横滑,右侧竖排领取 -->
|
||||
<div v-else-if="layoutStyle === '1'" class="cp-scroll" :style="scrollGap">
|
||||
<div class="cp1-item" v-for="(item, i) in displayData" :key="i">
|
||||
<div class="cp1-left">
|
||||
<div class="cp1-amount">
|
||||
<template v-if="item.couponType == 'DISCOUNT'">
|
||||
<span class="cp1-value">{{ item.couponDiscount }}</span
|
||||
><span class="cp1-symbol">折</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="cp1-symbol">¥</span
|
||||
><span class="cp1-value">{{ item.price }}</span>
|
||||
</template>
|
||||
</div>
|
||||
<div class="cp1-threshold">{{ thresholdText(item) }}</div>
|
||||
</div>
|
||||
<div class="cp1-right">
|
||||
<span>领</span><span>取</span>
|
||||
</div>
|
||||
<i class="cp-notch cp-notch-t"></i>
|
||||
<i class="cp-notch cp-notch-b"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 样式二:白底小卡,底部去领取按钮 -->
|
||||
<div v-else-if="layoutStyle === '2'" class="cp-scroll" :style="scrollGap">
|
||||
<div class="cp2-item" v-for="(item, i) in displayData" :key="i">
|
||||
<div class="cp2-top">
|
||||
<div class="cp2-amount">
|
||||
<template v-if="item.couponType == 'DISCOUNT'">
|
||||
<span class="cp2-value">{{ item.couponDiscount }}</span
|
||||
><span class="cp2-symbol">折</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="cp2-symbol">¥</span
|
||||
><span class="cp2-value">{{ item.price }}</span>
|
||||
</template>
|
||||
</div>
|
||||
<div class="cp2-threshold">{{ thresholdText(item) }}</div>
|
||||
</div>
|
||||
<div class="cp2-btn">去领取</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 样式三:浅粉虚线卡,右侧竖排立即领取 -->
|
||||
<div v-else-if="layoutStyle === '3'" class="cp-scroll" :style="scrollGap">
|
||||
<div class="cp3-item" v-for="(item, i) in displayData" :key="i">
|
||||
<div class="cp3-left">
|
||||
<div class="cp3-amount">
|
||||
<template v-if="item.couponType == 'DISCOUNT'">
|
||||
<span class="cp3-value">{{ item.couponDiscount }}</span
|
||||
><span class="cp3-symbol">折</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="cp3-symbol">¥</span
|
||||
><span class="cp3-value">{{ item.price }}</span>
|
||||
</template>
|
||||
</div>
|
||||
<div class="cp3-threshold">{{ thresholdText(item) }}</div>
|
||||
</div>
|
||||
<div class="cp3-right">
|
||||
<span>立</span><span>即</span><span>领</span><span>取</span>
|
||||
</div>
|
||||
<i class="cp-notch cp-notch-t"></i>
|
||||
<i class="cp-notch cp-notch-b"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 样式四:浅粉渐变 + 右侧渐变竖条领取 -->
|
||||
<div v-else class="cp-scroll" :style="scrollGap">
|
||||
<div class="cp4-item" v-for="(item, i) in displayData" :key="i">
|
||||
<div class="cp4-left">
|
||||
<div class="cp4-amount">
|
||||
<template v-if="item.couponType == 'DISCOUNT'">
|
||||
<span class="cp4-value">{{ item.couponDiscount }}</span
|
||||
><span class="cp4-symbol">折</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="cp4-symbol">¥</span
|
||||
><span class="cp4-value">{{ item.price }}</span>
|
||||
</template>
|
||||
</div>
|
||||
<div class="cp4-threshold">{{ thresholdText(item) }}</div>
|
||||
</div>
|
||||
<div class="cp4-right">
|
||||
<span>领</span><span>取</span>
|
||||
</div>
|
||||
<i class="cp-notch cp-notch-t"></i>
|
||||
<i class="cp-notch cp-notch-b"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -25,19 +119,58 @@ export default {
|
||||
res: { type: Object, default: () => ({}) },
|
||||
},
|
||||
computed: {
|
||||
firstItem() {
|
||||
return (this.res.list && this.res.list[0]) || {};
|
||||
},
|
||||
title() {
|
||||
return (this.res.list && this.res.list[0] && this.res.list[0].title) || "";
|
||||
return this.firstItem.title || "";
|
||||
},
|
||||
layoutStyle() {
|
||||
return (this.res && this.res.layoutStyle) || "1";
|
||||
},
|
||||
displayData() {
|
||||
const data = this.firstItem.data || [];
|
||||
return data.slice(0, this.firstItem.count || 5);
|
||||
},
|
||||
// 面板保存过样式字段才启用自定义样式,老数据保持原样
|
||||
styled() {
|
||||
return this.res && this.res.paddingTop !== undefined;
|
||||
},
|
||||
scrollGap() {
|
||||
if (!this.styled) return {};
|
||||
return { columnGap: `${(Number(this.res.contentGap) || 0) / 2}px` };
|
||||
},
|
||||
wrapperStyle() {
|
||||
if (!this.styled) {
|
||||
return {
|
||||
background: this.res.bgColor || "#FFFFFF",
|
||||
padding: `10px ${this.res.paddingX || 0}px`,
|
||||
marginBottom: `${this.res.pageGap !== undefined ? this.res.pageGap : 10}px`,
|
||||
borderRadius: `${this.res.borderRadius || 0}px`,
|
||||
overflow: "hidden",
|
||||
};
|
||||
}
|
||||
const res = this.res;
|
||||
const bg =
|
||||
!res.bgColor2 || res.bgColor1 === res.bgColor2
|
||||
? res.bgColor1
|
||||
: `linear-gradient(180deg, ${res.bgColor1}, ${res.bgColor2})`;
|
||||
const px = (v) => `${(Number(v) || 0) / 2}px`;
|
||||
return {
|
||||
background: this.res.bgColor || "#FFFFFF",
|
||||
padding: `10px ${this.res.paddingX || 0}px`,
|
||||
marginBottom: `${this.res.pageGap !== undefined ? this.res.pageGap : 10}px`,
|
||||
borderRadius: `${this.res.borderRadius || 0}px`,
|
||||
background: bg,
|
||||
padding: `${px(res.paddingTop)} ${px(res.paddingX)} ${px(res.paddingBottom)}`,
|
||||
marginBottom: px(res.pageGap),
|
||||
borderRadius: px(res.borderRadius),
|
||||
overflow: "hidden",
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
thresholdText(item) {
|
||||
const t = Number(item.consumeThreshold);
|
||||
return t > 0 ? `满${item.consumeThreshold}元可用` : "无门槛使用";
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -91,4 +224,218 @@ export default {
|
||||
padding: 4px 10px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
.tpl-coupon.styled .coupon-item {
|
||||
margin: 0;
|
||||
}
|
||||
.tpl-coupon.styled .coupon-list {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
/* 参考 CRMEB 优惠券组件 */
|
||||
.tpl-coupon.styled .coupon-title {
|
||||
padding: 0 0 8px;
|
||||
}
|
||||
.cp-scroll {
|
||||
display: flex;
|
||||
column-gap: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.cp-notch {
|
||||
position: absolute;
|
||||
right: 30px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
}
|
||||
.cp-notch-t {
|
||||
top: -4px;
|
||||
}
|
||||
.cp-notch-b {
|
||||
bottom: -4px;
|
||||
}
|
||||
|
||||
/* 样式一:红色大卡 */
|
||||
.cp1-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
width: 132px;
|
||||
height: 66px;
|
||||
border-radius: 6px;
|
||||
background: linear-gradient(135deg, #ff5a36, #e1251b);
|
||||
color: #fff;
|
||||
}
|
||||
.cp1-left {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding-left: 12px;
|
||||
}
|
||||
.cp1-value {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.cp1-symbol {
|
||||
font-size: 11px;
|
||||
}
|
||||
.cp1-threshold {
|
||||
font-size: 10px;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
margin-top: 2px;
|
||||
}
|
||||
.cp1-right {
|
||||
width: 30px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
border-left: 1px dashed rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
.cp1-item .cp-notch {
|
||||
right: 26px;
|
||||
}
|
||||
|
||||
/* 样式二:白底小卡 */
|
||||
.cp2-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
width: 74px;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
border-left: 2px solid #f5483a;
|
||||
border-right: 2px solid #f5483a;
|
||||
overflow: hidden;
|
||||
}
|
||||
.cp2-top {
|
||||
padding: 8px 0 6px;
|
||||
text-align: center;
|
||||
}
|
||||
.cp2-value {
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
color: #e1251b;
|
||||
}
|
||||
.cp2-symbol {
|
||||
font-size: 10px;
|
||||
color: #e1251b;
|
||||
}
|
||||
.cp2-threshold {
|
||||
font-size: 9px;
|
||||
color: #999;
|
||||
margin-top: 1px;
|
||||
}
|
||||
.cp2-btn {
|
||||
background: linear-gradient(90deg, #ff6a3d, #e1251b);
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
text-align: center;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
/* 样式三:浅粉虚线卡 */
|
||||
.cp3-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
width: 138px;
|
||||
height: 60px;
|
||||
border-radius: 6px;
|
||||
background: #fdf1ec;
|
||||
border: 1px dashed #f5483a;
|
||||
}
|
||||
.cp3-left {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding-left: 12px;
|
||||
}
|
||||
.cp3-value {
|
||||
font-size: 19px;
|
||||
font-weight: 700;
|
||||
color: #e1251b;
|
||||
}
|
||||
.cp3-symbol {
|
||||
font-size: 10px;
|
||||
color: #e1251b;
|
||||
}
|
||||
.cp3-threshold {
|
||||
font-size: 10px;
|
||||
color: #e1251b;
|
||||
margin-top: 1px;
|
||||
}
|
||||
.cp3-right {
|
||||
width: 32px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 11px;
|
||||
color: #e1251b;
|
||||
border-left: 1px dashed #f5483a;
|
||||
}
|
||||
.cp3-item .cp-notch {
|
||||
right: 28px;
|
||||
}
|
||||
|
||||
/* 样式四:浅粉渐变 + 渐变竖条 */
|
||||
.cp4-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
width: 116px;
|
||||
height: 54px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.cp4-left {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding-left: 10px;
|
||||
background: linear-gradient(135deg, #fff3ec, #ffe0d3);
|
||||
}
|
||||
.cp4-value {
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
color: #e1251b;
|
||||
}
|
||||
.cp4-symbol {
|
||||
font-size: 10px;
|
||||
color: #e1251b;
|
||||
}
|
||||
.cp4-threshold {
|
||||
font-size: 9px;
|
||||
color: #e1251b;
|
||||
margin-top: 1px;
|
||||
}
|
||||
.cp4-right {
|
||||
width: 26px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 11px;
|
||||
color: #fff;
|
||||
background: linear-gradient(180deg, #ff6a3d, #e1251b);
|
||||
}
|
||||
.cp4-item .cp-notch {
|
||||
right: 22px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
.cp4-item .cp-notch-t {
|
||||
top: -3px;
|
||||
}
|
||||
.cp4-item .cp-notch-b {
|
||||
bottom: -3px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,97 +1,447 @@
|
||||
<template>
|
||||
<div v-if="goodsBlock" class="layout">
|
||||
<div class="goods-cell-title">
|
||||
<div
|
||||
class="goods-item-title"
|
||||
:class="{ selected: selected.index == index }"
|
||||
@click="handleClickTitle(title, index)"
|
||||
v-for="(title, index) in goodsBlock.titleWay"
|
||||
:key="index"
|
||||
>
|
||||
<h4>{{ title.title }}</h4>
|
||||
<div>{{ title.desc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="goods-list">
|
||||
<template v-for="(item, item_index) in goodsBlock.listWay" :key="item_index">
|
||||
<div v-if="isGoodsVisible(item)" class="goods-item">
|
||||
<div class="goods-img">
|
||||
<el-icon
|
||||
:size="20"
|
||||
color="#e1251b"
|
||||
@click="closeGoods(item, item_index)"
|
||||
class="goods-icon"
|
||||
>
|
||||
<CircleClose />
|
||||
</el-icon>
|
||||
<img :src="item.img" alt />
|
||||
<div class="tpl-goods" :class="{ 'legacy-mode': !isNew }" :style="wrapperStyle">
|
||||
<!-- 新版商品列表 -->
|
||||
<template v-if="isNew">
|
||||
<div v-if="displayGoods.length" class="g-list" :class="'mode-' + layout" :style="listStyle">
|
||||
<div class="g-item" v-for="(g, i) in displayGoods" :key="i" :style="itemStyle">
|
||||
<div class="g-imgbox">
|
||||
<img :src="g.img" alt="" />
|
||||
</div>
|
||||
<div class="goods-desc">
|
||||
<div class="goods-title">
|
||||
{{ item.title }}
|
||||
<div class="g-info">
|
||||
<div v-if="showName" class="g-name" :style="nameStyle">{{ g.name }}</div>
|
||||
<div v-if="showPrice" class="g-price" :style="priceStyle">
|
||||
¥{{ $filters.unitPrice(g.price) }}
|
||||
</div>
|
||||
<div class="goods-bottom">
|
||||
<div class="goods-price">¥{{ $filters.unitPrice(item.price) }}</div>
|
||||
<div class="g-bottom">
|
||||
<span
|
||||
v-if="showSales && g.sales != null"
|
||||
class="g-sales"
|
||||
:style="salesStyle"
|
||||
>已售 {{ g.sales }} 件</span
|
||||
>
|
||||
<span
|
||||
v-if="cartShow"
|
||||
class="g-cart"
|
||||
:class="'cart-' + cartStyle"
|
||||
:style="cartStyleObj"
|
||||
>
|
||||
<template v-if="cartStyle === 'text'">加购</template>
|
||||
<template v-else-if="cartStyle === 'plus'">+</template>
|
||||
<svg v-else viewBox="0 0 24 24" width="12" height="12" fill="currentColor">
|
||||
<path
|
||||
d="M7 4h-2l-1 2h2l2.4 9.6a2 2 0 0 0 1.9 1.4h7.9a2 2 0 0 0 1.9-1.5l1.6-6.5h-12l-.8-3h11.5l.5-2h-13.4zM10 20a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zm8 0a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="g-empty">暂无商品</div>
|
||||
</template>
|
||||
|
||||
<!-- 旧版:分类标签 + 绑定商品 -->
|
||||
<template v-else-if="goodsBlock">
|
||||
<div class="goods-cell-title">
|
||||
<div
|
||||
class="goods-item-title"
|
||||
:class="{ selected: selected.index == index }"
|
||||
@click="handleClickTitle(title, index)"
|
||||
v-for="(title, index) in goodsBlock.titleWay"
|
||||
:key="index"
|
||||
>
|
||||
<h4>{{ title.title }}</h4>
|
||||
<div>{{ title.desc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="goods-list">
|
||||
<template v-for="(item, item_index) in goodsBlock.listWay" :key="item_index">
|
||||
<div v-if="isGoodsVisible(item)" class="goods-item">
|
||||
<div class="goods-img">
|
||||
<img :src="item.img" alt />
|
||||
</div>
|
||||
<div class="goods-desc">
|
||||
<div class="goods-title">
|
||||
{{ item.title }}
|
||||
</div>
|
||||
<div class="goods-bottom">
|
||||
<div class="goods-price">¥{{ $filters.unitPrice(item.price) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { computed, reactive, watch } from "vue";
|
||||
import { CircleClose } from "@element-plus/icons-vue";
|
||||
|
||||
const props = defineProps(["res"]);
|
||||
<script>
|
||||
import { getGoodsListData } from "@/api/goods";
|
||||
|
||||
const selected = reactive({
|
||||
index: 0,
|
||||
val: "",
|
||||
});
|
||||
|
||||
const goodsBlock = computed(() => props.res?.list?.[0] || null);
|
||||
|
||||
watch(
|
||||
goodsBlock,
|
||||
(val) => {
|
||||
if (!val) {
|
||||
return;
|
||||
}
|
||||
const firstGoods = (val.listWay || []).find(Boolean);
|
||||
if (firstGoods) {
|
||||
selected.val = firstGoods.type;
|
||||
} else if (val.titleWay?.[0]?.title) {
|
||||
selected.val = val.titleWay[0].title;
|
||||
export default {
|
||||
name: "tpl_goods",
|
||||
props: {
|
||||
res: { type: Object, default: () => ({}) },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selected: { index: 0, val: "" },
|
||||
goodsData: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
first() {
|
||||
return (this.res.list && this.res.list[0]) || {};
|
||||
},
|
||||
// 新版标记:面板写入过 layout 字段
|
||||
isNew() {
|
||||
return this.first.layout !== undefined;
|
||||
},
|
||||
layout() {
|
||||
return this.first.layout || "list";
|
||||
},
|
||||
showName() {
|
||||
return this.first.showName !== false;
|
||||
},
|
||||
showPrice() {
|
||||
return this.first.showPrice !== false;
|
||||
},
|
||||
showSales() {
|
||||
return this.first.showSales !== false;
|
||||
},
|
||||
cartShow() {
|
||||
return this.first.cartShow !== false;
|
||||
},
|
||||
cartStyle() {
|
||||
return this.first.cartStyle || "plus";
|
||||
},
|
||||
themePrice() {
|
||||
return "#FF5C58";
|
||||
},
|
||||
priceColor() {
|
||||
return this.res.toneMode === "custom" && this.res.priceColor
|
||||
? this.res.priceColor
|
||||
: this.themePrice;
|
||||
},
|
||||
priceStyle() {
|
||||
return { color: this.priceColor };
|
||||
},
|
||||
nameStyle() {
|
||||
return this.res.titleColor ? { color: this.res.titleColor } : {};
|
||||
},
|
||||
salesStyle() {
|
||||
return this.res.salesColor ? { color: this.res.salesColor } : {};
|
||||
},
|
||||
cartStyleObj() {
|
||||
return { background: this.priceColor };
|
||||
},
|
||||
px() {
|
||||
return (v) => `${(Number(v) || 0) / 2}px`;
|
||||
},
|
||||
wrapperStyle() {
|
||||
const res = this.res;
|
||||
if (!this.isNew) {
|
||||
return {};
|
||||
}
|
||||
const bg =
|
||||
!res.bgColor2 || res.bgColor1 === res.bgColor2
|
||||
? res.bgColor1
|
||||
: `linear-gradient(180deg, ${res.bgColor1}, ${res.bgColor2})`;
|
||||
return {
|
||||
background: bg,
|
||||
padding: `${this.px(res.paddingTop)} ${this.px(res.paddingX)} ${this.px(res.paddingBottom)}`,
|
||||
marginBottom: this.px(res.pageGap),
|
||||
borderRadius: this.px(res.borderRadius),
|
||||
overflow: Number(res.borderRadius) > 0 ? "hidden" : "visible",
|
||||
};
|
||||
},
|
||||
listStyle() {
|
||||
return { gap: this.px(this.res.contentGap) };
|
||||
},
|
||||
itemStyle() {
|
||||
const r = this.px(this.res.contentRadius);
|
||||
return { borderRadius: r, overflow: Number(this.res.contentRadius) > 0 ? "hidden" : "visible" };
|
||||
},
|
||||
displayGoods() {
|
||||
if (this.first.dataMode === "goods") {
|
||||
return (this.first.goodsList || []).map((g) => ({
|
||||
name: g.goodsName,
|
||||
price: g.price,
|
||||
img: g.thumbnail,
|
||||
sales: g.buyCount != null ? g.buyCount : null,
|
||||
}));
|
||||
}
|
||||
return this.goodsData;
|
||||
},
|
||||
goodsBlock() {
|
||||
return this.res && this.res.list && this.res.list[0] ? this.res.list[0] : null;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
"first.dataMode": "loadGoods",
|
||||
"first.categoryId": "loadGoods",
|
||||
"first.brandId": "loadGoods",
|
||||
"first.storeId": "loadGoods",
|
||||
"first.count": "loadGoods",
|
||||
"first.sortType": "loadGoods",
|
||||
},
|
||||
created() {
|
||||
if (this.isNew) {
|
||||
this.loadGoods();
|
||||
} else {
|
||||
const block = this.goodsBlock;
|
||||
if (block) {
|
||||
const firstGoods = (block.listWay || []).find(Boolean);
|
||||
if (firstGoods) {
|
||||
this.selected.val = firstGoods.type;
|
||||
} else if (block.titleWay && block.titleWay[0]) {
|
||||
this.selected.val = block.titleWay[0].title;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
function isGoodsVisible(item) {
|
||||
if (!item) {
|
||||
return false;
|
||||
}
|
||||
if (item.___index != undefined) {
|
||||
return selected.index == item.___index;
|
||||
}
|
||||
return selected.val == item.type;
|
||||
}
|
||||
|
||||
function closeGoods(val, index) {
|
||||
goodsBlock.value.listWay.splice(index, 1);
|
||||
}
|
||||
|
||||
function handleClickTitle(val, index) {
|
||||
selected.index = index;
|
||||
selected.val = val.title;
|
||||
}
|
||||
methods: {
|
||||
loadGoods() {
|
||||
const item = this.first;
|
||||
if (!this.isNew || item.dataMode === "goods") {
|
||||
return;
|
||||
}
|
||||
const params = {
|
||||
pageNumber: 1,
|
||||
pageSize: item.count || 6,
|
||||
goodsStatus: "UPPER",
|
||||
};
|
||||
if (item.dataMode === "category" && item.categoryId) {
|
||||
// category_path 以逗号包裹,前后补逗号避免误匹配
|
||||
params.categoryPath = `,${item.categoryId},`;
|
||||
}
|
||||
if (item.dataMode === "brand" && item.brandId) {
|
||||
params.brandId = item.brandId;
|
||||
}
|
||||
if (item.dataMode === "store" && item.storeId) {
|
||||
params.storeId = item.storeId;
|
||||
}
|
||||
if (item.sortType === "sales") {
|
||||
params.sort = "buyCount";
|
||||
params.order = "desc";
|
||||
} else if (item.sortType === "price") {
|
||||
params.sort = "price";
|
||||
params.order = "asc";
|
||||
}
|
||||
getGoodsListData(params)
|
||||
.then((res) => {
|
||||
const records = (res && res.result && res.result.records) || [];
|
||||
this.goodsData = records.map((g) => ({
|
||||
name: g.goodsName,
|
||||
price: g.price,
|
||||
img: g.thumbnail || g.small || g.original,
|
||||
sales: g.buyCount != null ? g.buyCount : null,
|
||||
}));
|
||||
})
|
||||
.catch(() => {
|
||||
this.goodsData = [];
|
||||
});
|
||||
},
|
||||
isGoodsVisible(item) {
|
||||
if (!item) {
|
||||
return false;
|
||||
}
|
||||
if (item.___index != undefined) {
|
||||
return this.selected.index == item.___index;
|
||||
}
|
||||
return this.selected.val == item.type;
|
||||
},
|
||||
handleClickTitle(val, index) {
|
||||
this.selected.index = index;
|
||||
this.selected.val = val.title;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.layout {
|
||||
padding: 8px 0;
|
||||
background: #e8e8e8;
|
||||
.tpl-goods {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.legacy-mode {
|
||||
background: #e8e8e8;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.g-empty {
|
||||
padding: 30px 0;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.g-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.g-item {
|
||||
background: #fff;
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.g-imgbox {
|
||||
flex-shrink: 0;
|
||||
background: #f5f5f5;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.g-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.g-name {
|
||||
font-size: 12px;
|
||||
color: #282828;
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.g-bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.g-price {
|
||||
color: $theme_color;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.g-sales {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.g-cart {
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
&.cart-plus,
|
||||
&.cart-cart {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
}
|
||||
&.cart-text {
|
||||
border-radius: 9px;
|
||||
padding: 2px 8px;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 卡片:左图右文 */
|
||||
.mode-card {
|
||||
flex-direction: column;
|
||||
.g-item {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 6px;
|
||||
}
|
||||
.g-imgbox {
|
||||
width: 55px;
|
||||
height: 55px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.g-name {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 列表:两列网格 */
|
||||
.mode-list {
|
||||
.g-item {
|
||||
width: calc(50% - 3px);
|
||||
flex-direction: column;
|
||||
}
|
||||
.g-imgbox {
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
.g-info {
|
||||
padding: 6px;
|
||||
min-height: 52px;
|
||||
}
|
||||
.g-name {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 三列 */
|
||||
.mode-three {
|
||||
.g-item {
|
||||
width: calc(33.33% - 4px);
|
||||
flex-direction: column;
|
||||
}
|
||||
.g-imgbox {
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
.g-info {
|
||||
padding: 4px;
|
||||
min-height: 44px;
|
||||
}
|
||||
.g-name {
|
||||
-webkit-line-clamp: 1;
|
||||
font-size: 11px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.g-price {
|
||||
font-size: 12px;
|
||||
}
|
||||
.g-sales {
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 大图:一行一个 */
|
||||
.mode-big {
|
||||
flex-direction: column;
|
||||
.g-item {
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
}
|
||||
.g-imgbox {
|
||||
width: 100%;
|
||||
aspect-ratio: 3 / 2;
|
||||
}
|
||||
.g-info {
|
||||
padding: 6px;
|
||||
min-height: 44px;
|
||||
}
|
||||
.g-name {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 旧版样式 */
|
||||
.selected {
|
||||
> h4 {
|
||||
color: #000 !important;
|
||||
@@ -105,7 +455,6 @@ function handleClickTitle(val, index) {
|
||||
padding: 10px;
|
||||
transition: 0.35s;
|
||||
display: flex;
|
||||
// cursor: pointer;
|
||||
> .goods-item-title {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
@@ -156,9 +505,7 @@ function handleClickTitle(val, index) {
|
||||
display: -webkit-box;
|
||||
font-weight: 500;
|
||||
-webkit-box-orient: vertical;
|
||||
|
||||
-webkit-line-clamp: 2;
|
||||
|
||||
overflow: hidden;
|
||||
}
|
||||
> .goods-bottom {
|
||||
@@ -169,9 +516,4 @@ function handleClickTitle(val, index) {
|
||||
}
|
||||
}
|
||||
}
|
||||
.goods-icon {
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,27 +1,69 @@
|
||||
<template>
|
||||
<div class="layout" :style="wrapperStyle">
|
||||
<div class="join-list">
|
||||
<div class="join-title">
|
||||
<div>{{ title }}</div>
|
||||
|
||||
<div>更多</div>
|
||||
<div class="integral-head" :style="headStyle">
|
||||
<img
|
||||
v-if="styled && res.titleType === 'image' && res.titleImage"
|
||||
class="integral-head-img"
|
||||
:src="res.titleImage"
|
||||
alt=""
|
||||
/>
|
||||
<div v-else class="integral-head-title" :style="headTitleStyle">{{ title }}</div>
|
||||
<div class="integral-head-space"></div>
|
||||
<div class="integral-head-more" :style="moreStyle">更多 ></div>
|
||||
</div>
|
||||
<!-- 老数据:原始四列样式 -->
|
||||
<div v-if="!styled" class="join-box">
|
||||
<div class="join-item" v-for="item in 4" :key="item">
|
||||
<div class="item-img-box">
|
||||
<img
|
||||
class="item-img"
|
||||
src="https://picsum.photos/id/268/200/200"
|
||||
alt
|
||||
/>
|
||||
</div>
|
||||
<div class="item-price">
|
||||
<span>20积分</span>
|
||||
</div>
|
||||
<div class="item-line-through">
|
||||
<span>30积分</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="join-box">
|
||||
<div class="join-item" v-for="item in 4" :key="item">
|
||||
<div class="item-img-box">
|
||||
<img
|
||||
class="item-img"
|
||||
src="https://picsum.photos/id/268/200/200"
|
||||
alt
|
||||
/>
|
||||
|
||||
</div>
|
||||
<div class="item-price">
|
||||
<span>20积分</span>
|
||||
</div>
|
||||
<div class="item-line-through">
|
||||
<span>30积分</span>
|
||||
</div>
|
||||
<!-- 样式一:两列大卡片 -->
|
||||
<div v-else-if="layoutStyle === '1'" class="ig-grid2" :style="gapStyle">
|
||||
<div class="ig-g2-item" v-for="(g, i) in displayData" :key="i">
|
||||
<div class="ig-g2-img-box" :style="imgRadiusStyle">
|
||||
<img class="ig-img" :src="g.thumbnail ? g.thumbnail : g.goodsImage" alt />
|
||||
</div>
|
||||
<div class="ig-g2-name" :style="nameStyle">{{ g.goodsName ? g.goodsName : g.name }}</div>
|
||||
<div class="ig-price-row">
|
||||
<div class="ig-price" :style="pointStyle">
|
||||
<span class="ig-coin">¥</span>{{ g.points != null ? g.points : 0 }}积分
|
||||
</div>
|
||||
<s class="ig-original" v-if="g.originalPrice">¥{{ g.originalPrice }}</s>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 样式二:三列,积分价在名称上方 -->
|
||||
<div v-else-if="layoutStyle === '2'" class="ig-grid3" :style="gapStyle">
|
||||
<div class="ig-g3-item" v-for="(g, i) in displayData" :key="i">
|
||||
<div class="ig-g3-img-box" :style="imgRadiusStyle">
|
||||
<img class="ig-img" :src="g.thumbnail ? g.thumbnail : g.goodsImage" alt />
|
||||
</div>
|
||||
<div class="ig-price ig-price-sm" :style="pointStyle">
|
||||
<span class="ig-coin ig-coin-sm">¥</span>{{ g.points != null ? g.points : 0 }}积分
|
||||
</div>
|
||||
<div class="ig-g3-name" :style="nameStyle">{{ g.goodsName ? g.goodsName : g.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 样式三:三列,仅积分价 -->
|
||||
<div v-else class="ig-grid3" :style="gapStyle">
|
||||
<div class="ig-g3-item" v-for="(g, i) in displayData" :key="i">
|
||||
<div class="ig-g3-img-box" :style="imgRadiusStyle">
|
||||
<img class="ig-img" :src="g.thumbnail ? g.thumbnail : g.goodsImage" alt />
|
||||
</div>
|
||||
<div class="ig-price ig-price-sm" :style="pointStyle">
|
||||
<span class="ig-coin ig-coin-sm">¥</span>{{ g.points != null ? g.points : 0 }}积分
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -33,18 +75,117 @@ import { computed } from "vue";
|
||||
const props = defineProps(["res"]);
|
||||
|
||||
const title = computed(
|
||||
() => (props.res.list && props.res.list[0] && props.res.list[0].title) || "积分商城"
|
||||
() => (props.res.list && props.res.list[0] && props.res.list[0].title) || "积分兑好礼"
|
||||
);
|
||||
const firstItem = computed(() => (props.res.list && props.res.list[0]) || {});
|
||||
const layoutStyle = computed(() => props.res.layoutStyle || "1");
|
||||
const displayData = computed(() => {
|
||||
const data = (firstItem.value && firstItem.value.data) || [];
|
||||
return layoutStyle.value === "1" ? data.slice(0, 4) : data.slice(0, 6);
|
||||
});
|
||||
|
||||
// 面板保存过样式字段才启用新样式,老数据保持原样
|
||||
const styled = computed(() => props.res && props.res.paddingTop !== undefined);
|
||||
const px = (v) => `${(Number(v) || 0) / 2}px`;
|
||||
|
||||
const wrapperStyle = computed(() => {
|
||||
const res = props.res || {};
|
||||
if (!styled.value) {
|
||||
return {
|
||||
background: res.bgColor || "#FFFFFF",
|
||||
padding: `0 ${res.paddingX || 0}px`,
|
||||
marginBottom: `${res.pageGap !== undefined ? res.pageGap : 10}px`,
|
||||
borderRadius: `${res.borderRadius || 0}px`,
|
||||
overflow: "hidden",
|
||||
};
|
||||
}
|
||||
const bg =
|
||||
!res.bgColor2 || res.bgColor1 === res.bgColor2
|
||||
? res.bgColor1
|
||||
: `linear-gradient(180deg, ${res.bgColor1}, ${res.bgColor2})`;
|
||||
return {
|
||||
background: bg,
|
||||
padding: `${px(res.paddingTop)} ${px(res.paddingX)} ${px(res.paddingBottom)}`,
|
||||
marginBottom: px(res.pageGap),
|
||||
borderRadius: px(res.borderRadius),
|
||||
overflow: Number(res.borderRadius) > 0 ? "hidden" : "visible",
|
||||
};
|
||||
});
|
||||
|
||||
const headStyle = computed(() => {
|
||||
if (!styled.value) return {};
|
||||
const res = props.res;
|
||||
if (res.headBgType === "image" && res.headBgImage) {
|
||||
return {
|
||||
backgroundImage: `url(${res.headBgImage})`,
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
};
|
||||
}
|
||||
return {
|
||||
background:
|
||||
!res.headBg2 || res.headBg1 === res.headBg2
|
||||
? res.headBg1
|
||||
: `linear-gradient(90deg, ${res.headBg1}, ${res.headBg2})`,
|
||||
};
|
||||
});
|
||||
|
||||
const headTitleStyle = computed(() => {
|
||||
if (!styled.value) return {};
|
||||
const res = props.res;
|
||||
return {
|
||||
color: res.titleColor || "#FFFFFF",
|
||||
fontWeight: res.titleStyle === "bold" ? "bold" : "normal",
|
||||
fontStyle: res.titleStyle === "italic" ? "italic" : "normal",
|
||||
};
|
||||
});
|
||||
|
||||
const moreStyle = computed(() =>
|
||||
styled.value ? { color: props.res.moreColor || "#999999" } : {}
|
||||
);
|
||||
const pointStyle = computed(() =>
|
||||
styled.value && props.res.pointColor ? { color: props.res.pointColor } : {}
|
||||
);
|
||||
const nameStyle = computed(() =>
|
||||
styled.value && props.res.nameColor ? { color: props.res.nameColor } : {}
|
||||
);
|
||||
const gapStyle = computed(() =>
|
||||
styled.value ? { gap: px(props.res.contentGap) } : {}
|
||||
);
|
||||
const imgRadiusStyle = computed(() =>
|
||||
styled.value ? { borderRadius: px(props.res.imgRadius), overflow: "hidden" } : {}
|
||||
);
|
||||
const wrapperStyle = computed(() => ({
|
||||
background: props.res.bgColor || "#FFFFFF",
|
||||
padding: `0 ${props.res.paddingX || 0}px`,
|
||||
marginBottom: `${props.res.pageGap !== undefined ? props.res.pageGap : 10}px`,
|
||||
borderRadius: `${props.res.borderRadius || 0}px`,
|
||||
overflow: "hidden",
|
||||
}));
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "./tpl.scss";
|
||||
.integral-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-height: 48px;
|
||||
padding: 0 12px;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.integral-head-img {
|
||||
width: 77px;
|
||||
height: 16px;
|
||||
object-fit: contain;
|
||||
}
|
||||
.integral-head-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.integral-head-space {
|
||||
flex: 1;
|
||||
}
|
||||
.integral-head-more {
|
||||
flex-shrink: 0;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
.join-box {
|
||||
display: flex;
|
||||
}
|
||||
@@ -57,12 +198,7 @@ const wrapperStyle = computed(() => ({
|
||||
}
|
||||
.join-item {
|
||||
flex: 1;
|
||||
}
|
||||
.item-img {
|
||||
width: 75px;
|
||||
height: 75px;
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
.item-img-box {
|
||||
position: relative;
|
||||
@@ -75,26 +211,114 @@ const wrapperStyle = computed(() => ({
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
.item-position-tips {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
bottom: 0;
|
||||
.item-img {
|
||||
width: 75px;
|
||||
height: 75px;
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
}
|
||||
.join-title {
|
||||
|
||||
/* 积分商品卡片(参考 CRMEB 积分商城) */
|
||||
.ig-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
}
|
||||
.ig-grid2 {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
height: 50px;
|
||||
> div:nth-of-type(1) {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
> div:nth-of-type(2) {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
||||
.ig-g2-item {
|
||||
width: calc(50% - 5px);
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
padding-bottom: 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.ig-g2-img-box {
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.ig-g2-name {
|
||||
margin: 8px 8px 0;
|
||||
font-size: 13px;
|
||||
color: #282828;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.ig-price-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
margin: 5px 8px 0;
|
||||
min-width: 0;
|
||||
}
|
||||
.ig-price {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #f5483a;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.ig-price-sm {
|
||||
font-size: 12px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.ig-coin {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #ffe29a, #f5a623);
|
||||
color: #fff;
|
||||
font-size: 9px;
|
||||
font-weight: bold;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 3px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ig-coin-sm {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
font-size: 8px;
|
||||
}
|
||||
.ig-original {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
margin-left: 5px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.ig-grid3 {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.ig-g3-item {
|
||||
width: calc(33.33% - 7px);
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
padding-bottom: 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.ig-g3-item .ig-price {
|
||||
margin: 5px 8px 0;
|
||||
}
|
||||
.ig-g3-img-box {
|
||||
width: 100%;
|
||||
height: 92px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.ig-g3-name {
|
||||
margin: 4px 8px 0;
|
||||
font-size: 12px;
|
||||
color: #282828;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<template>
|
||||
<div class="layout" :style="[wrapperStyle, gapStyle]">
|
||||
<div class="view-height-150" :style="imgStyle">
|
||||
<img class="image-mode" :src="res.list[0].img" />
|
||||
<img alt="模板图片" class="image-mode" :src="res.list[0].img" />
|
||||
</div>
|
||||
<div class="view-height-150">
|
||||
<div class="view-height-75" :style="imgStyle">
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="view-height-150" :style="imgStyle">
|
||||
<img class="image-mode" style="height:150px;" :src="res.list[2].img" />
|
||||
<img alt="模板图片" class="image-mode" style="height:150px;" :src="res.list[2].img" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div
|
||||
class="tpl-line"
|
||||
:style="{
|
||||
background: res.bgColor ?? '#FFFFFF',
|
||||
background: lineBg,
|
||||
padding: `0 ${res.paddingX ?? 0}px`,
|
||||
marginBottom: (res.pageGap ?? 10) + 'px',
|
||||
}"
|
||||
@@ -21,5 +21,16 @@ export default {
|
||||
props: {
|
||||
res: { type: Object, default: () => ({}) },
|
||||
},
|
||||
computed: {
|
||||
lineBg() {
|
||||
const res = this.res || {};
|
||||
if (res.bgColor1 !== undefined) {
|
||||
return !res.bgColor2 || res.bgColor1 === res.bgColor2
|
||||
? res.bgColor1
|
||||
: `linear-gradient(180deg, ${res.bgColor1}, ${res.bgColor2})`;
|
||||
}
|
||||
return res.bgColor || "#FFFFFF";
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
<template>
|
||||
<div class="layout" :style="wrapperStyle">
|
||||
<div class="menu-list" :class="{ styled: styled }" :style="listStyle">
|
||||
<div class="menu-item" v-for="(item, index) in res.list" :key="index">
|
||||
<div
|
||||
class="menu-list"
|
||||
:class="{ styled: styled, single: isSingle }"
|
||||
:style="listStyle"
|
||||
>
|
||||
<div
|
||||
class="menu-item"
|
||||
:style="itemStyle"
|
||||
v-for="(item, index) in visibleList"
|
||||
:key="index"
|
||||
>
|
||||
<div class="menu-img-wrap" :style="imgStyle">
|
||||
<img class="menu-img" :src="item.img" alt="" />
|
||||
</div>
|
||||
@@ -26,6 +35,22 @@ const listStyle = computed(() =>
|
||||
const titleStyle = computed(() =>
|
||||
styled.value && props.res.textColor ? { color: props.res.textColor } : {}
|
||||
);
|
||||
|
||||
// 展示设置:单行/多行、行数、每行个数,超出部分不展示
|
||||
const isSingle = computed(
|
||||
() => styled.value && props.res.menuDisplay === "single"
|
||||
);
|
||||
const cols = computed(() => (styled.value ? Number(props.res.menuCols) || 4 : 4));
|
||||
const rows = computed(() => Number(props.res.menuRows) || 3);
|
||||
const itemStyle = computed(() =>
|
||||
styled.value ? { flex: `0 0 ${100 / cols.value}%` } : {}
|
||||
);
|
||||
const visibleList = computed(() => {
|
||||
const list = props.res.list || [];
|
||||
if (!styled.value) return list;
|
||||
const limit = isSingle.value ? cols.value : rows.value * cols.value;
|
||||
return list.slice(0, limit);
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "./tpl.scss";
|
||||
@@ -34,14 +59,21 @@ const titleStyle = computed(() =>
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
|
||||
&.single {
|
||||
flex-wrap: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
> .menu-item {
|
||||
text-align: center;
|
||||
width: 20%;
|
||||
width: 25%;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
&.styled > .menu-item {
|
||||
&.styled > .menu-item,
|
||||
&.single > .menu-item {
|
||||
margin: 0;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
.menu-img-wrap {
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
<template>
|
||||
<div class="layout">
|
||||
<div class="background" :style="{ backgroundColor: res.list[0].bk_color }">
|
||||
<img :src="res.list[0].img" alt="" style="float:left;width: 20px;height: 20px;margin-top: 10px;margin-left: 10px;margin-right: 10px">
|
||||
<div class="title" :style="{ color: res.list[0].color }">
|
||||
<div class="layout" :class="{ styled: styled }" :style="wrapperStyle" v-if="texts.length">
|
||||
<div class="background" :style="barStyle">
|
||||
<img
|
||||
v-if="styled && item.img"
|
||||
class="notice-icon"
|
||||
:src="item.img"
|
||||
alt=""
|
||||
/>
|
||||
<div class="title" :style="{ color: item.color, textAlign: alignValue }">
|
||||
{{ context }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -10,51 +15,109 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from "vue";
|
||||
import { ref, computed, watch, onBeforeUnmount } from "vue";
|
||||
|
||||
defineOptions({ title: "公告" });
|
||||
const props = defineProps(["res"]);
|
||||
defineOptions({ title: "公告" });
|
||||
const props = defineProps(["res"]);
|
||||
|
||||
const index = ref(0);
|
||||
const context = ref("");
|
||||
const item = computed(() => props.res?.list?.[0] || {});
|
||||
const texts = computed(() => {
|
||||
const titles = item.value?.title;
|
||||
if (!Array.isArray(titles)) return [];
|
||||
return titles.filter((t) => t && t.context != null && String(t.context).length > 0);
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
context.value = props.res.list[0].title[index.value].context;
|
||||
// 历史遗留:定时器未在卸载时清理,保持原行为
|
||||
setInterval(() => {
|
||||
context.value = props.res.list[0].title[index.value].context;
|
||||
if (index.value < props.res.list[0].title.length - 1) {
|
||||
index.value++;
|
||||
} else {
|
||||
index.value = 0;
|
||||
}
|
||||
}, 3000);
|
||||
});
|
||||
// 面板保存过样式字段才启用自定义样式,老数据保持原样
|
||||
const styled = computed(() => props.res && props.res.paddingX !== undefined);
|
||||
|
||||
const px = (v) => `${(Number(v) || 0) / 2}px`;
|
||||
const wrapperStyle = computed(() => {
|
||||
const res = props.res || {};
|
||||
if (!styled.value) return {};
|
||||
const bg =
|
||||
!res.bgColor2 || res.bgColor1 === res.bgColor2
|
||||
? res.bgColor1
|
||||
: `linear-gradient(180deg, ${res.bgColor1}, ${res.bgColor2})`;
|
||||
return {
|
||||
padding: `${px(res.paddingTop)} ${px(res.paddingX)} ${px(res.paddingBottom)}`,
|
||||
background: bg,
|
||||
borderRadius: px(res.borderRadius),
|
||||
marginBottom: px(res.pageGap),
|
||||
overflow: Number(res.borderRadius) > 0 ? "hidden" : "visible",
|
||||
};
|
||||
});
|
||||
const barStyle = computed(() =>
|
||||
styled.value ? {} : { backgroundColor: item.value.bk_color }
|
||||
);
|
||||
const alignValue = computed(() =>
|
||||
styled.value ? item.value.textAlign || "left" : "left"
|
||||
);
|
||||
|
||||
const index = ref(0);
|
||||
const context = ref("");
|
||||
let timer = null;
|
||||
|
||||
watch(
|
||||
texts,
|
||||
(list) => {
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
timer = null;
|
||||
}
|
||||
index.value = 0;
|
||||
context.value = list.length ? String(list[0].context) : "";
|
||||
if (list.length > 1) {
|
||||
timer = setInterval(() => {
|
||||
index.value = (index.value + 1) % list.length;
|
||||
context.value = String(list[index.value].context);
|
||||
}, 3000);
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (timer) clearInterval(timer);
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "./tpl.scss";
|
||||
.background {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
height: 42px;
|
||||
text-align: left;
|
||||
font-size: 10px;
|
||||
background-size: cover;
|
||||
}
|
||||
.layout {
|
||||
text-align: center;
|
||||
position: relative;
|
||||
height: 42px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
background: #ffffff;
|
||||
}
|
||||
.title {
|
||||
line-height: 42px;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
@import "./tpl.scss";
|
||||
.background {
|
||||
width: 100%;
|
||||
height: 42px;
|
||||
text-align: left;
|
||||
font-size: 10px;
|
||||
background-size: cover;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.layout {
|
||||
text-align: center;
|
||||
position: relative;
|
||||
height: 42px;
|
||||
}
|
||||
.layout.styled {
|
||||
height: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #ffffff;
|
||||
}
|
||||
.notice-icon {
|
||||
width: 65px;
|
||||
height: 18px;
|
||||
object-fit: contain;
|
||||
flex-shrink: 0;
|
||||
margin: 0 8px;
|
||||
}
|
||||
.title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
line-height: 42px;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
<div
|
||||
class="tpl-rich-text"
|
||||
:style="{
|
||||
background: res.bgColor ?? '#FFFFFF',
|
||||
background: richBg,
|
||||
padding: `0 ${res.paddingX ?? 12}px`,
|
||||
marginBottom: (res.pageGap ?? 10) + 'px',
|
||||
borderRadius: (res.borderRadius ?? 0) + 'px',
|
||||
@@ -19,6 +19,17 @@ export default {
|
||||
props: {
|
||||
res: { type: Object, default: () => ({}) },
|
||||
},
|
||||
computed: {
|
||||
richBg() {
|
||||
const res = this.res || {};
|
||||
if (res.bgColor1 !== undefined) {
|
||||
return !res.bgColor2 || res.bgColor1 === res.bgColor2
|
||||
? res.bgColor1
|
||||
: `linear-gradient(180deg, ${res.bgColor1}, ${res.bgColor2})`;
|
||||
}
|
||||
return res.bgColor || "#FFFFFF";
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,26 +1,144 @@
|
||||
<template>
|
||||
<div class="layout">
|
||||
<div class="search">
|
||||
<el-icon><Search /></el-icon>{{res.list[0].title}}
|
||||
</div>
|
||||
<div class="layout" :style="wrapperStyle">
|
||||
<div class="search" :style="boxStyle">
|
||||
<el-icon><Search /></el-icon>{{ displayText }}
|
||||
</div>
|
||||
<!-- 微信小程序右上角胶囊按钮 -->
|
||||
<div class="capsule">
|
||||
<span class="cap-icon">
|
||||
<svg width="18" height="5" viewBox="0 0 18 5" fill="currentColor">
|
||||
<circle cx="2.5" cy="2.5" r="2" />
|
||||
<circle cx="9" cy="2.5" r="2" />
|
||||
<circle cx="15.5" cy="2.5" r="2" />
|
||||
</svg>
|
||||
</span>
|
||||
<span class="cap-divider"></span>
|
||||
<span class="cap-icon">
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none">
|
||||
<circle cx="7.5" cy="7.5" r="6" stroke="currentColor" stroke-width="1.6" />
|
||||
<circle cx="7.5" cy="7.5" r="2.2" fill="currentColor" />
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { computed, ref, watch, onBeforeUnmount } from "vue";
|
||||
import { Search } from "@element-plus/icons-vue";
|
||||
|
||||
defineProps(['res']);
|
||||
const props = defineProps(["res"]);
|
||||
|
||||
// 搜索热词轮播:有热词时按切换时间轮换展示,否则展示提示文字
|
||||
const hotIndex = ref(0);
|
||||
let hotTimer = null;
|
||||
const validWords = computed(() =>
|
||||
(props.res.hotWords || []).filter((w) => w && w.trim())
|
||||
);
|
||||
const displayText = computed(() => {
|
||||
if (validWords.value.length) {
|
||||
return validWords.value[hotIndex.value % validWords.value.length];
|
||||
}
|
||||
return (props.res.list && props.res.list[0] && props.res.list[0].title) || "";
|
||||
});
|
||||
|
||||
function stopHot() {
|
||||
if (hotTimer) {
|
||||
clearInterval(hotTimer);
|
||||
hotTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [validWords.value.length, props.res.hotInterval],
|
||||
() => {
|
||||
stopHot();
|
||||
hotIndex.value = 0;
|
||||
if (validWords.value.length > 1) {
|
||||
hotTimer = setInterval(() => {
|
||||
hotIndex.value = (hotIndex.value + 1) % validWords.value.length;
|
||||
}, (Number(props.res.hotInterval) || 3) * 1000);
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
onBeforeUnmount(stopHot);
|
||||
|
||||
// 面板保存过样式字段才启用自定义样式,老数据保持原样
|
||||
const styled = computed(() => props.res && props.res.paddingX !== undefined);
|
||||
// 画布 375px ≈ 750rpx,预览按一半像素呈现
|
||||
const px = (v) => `${(Number(v) || 0) / 2}px`;
|
||||
const wrapperStyle = computed(() => {
|
||||
const res = props.res || {};
|
||||
if (!styled.value) return {};
|
||||
const bg =
|
||||
!res.bgColor2 || res.bgColor1 === res.bgColor2
|
||||
? res.bgColor1
|
||||
: `linear-gradient(180deg, ${res.bgColor1}, ${res.bgColor2})`;
|
||||
return {
|
||||
padding: `0 ${px(res.paddingX)}`,
|
||||
background: bg,
|
||||
borderRadius: px(res.borderRadius),
|
||||
overflow: Number(res.borderRadius) > 0 ? "hidden" : "visible",
|
||||
};
|
||||
});
|
||||
const boxStyle = computed(() => {
|
||||
const res = props.res || {};
|
||||
if (!styled.value) return {};
|
||||
return {
|
||||
background: res.boxColor || "#EDEDED",
|
||||
color: res.textColor || "#999999",
|
||||
borderRadius: px(res.boxRadius),
|
||||
justifyContent: res.textAlign === "center" ? "center" : "flex-start",
|
||||
};
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "./tpl.scss";
|
||||
.search{
|
||||
height: 32px;
|
||||
border-radius: 5px;
|
||||
.search {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 32px;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #ededed;
|
||||
gap: 4px;
|
||||
box-sizing: border-box;
|
||||
padding: 0 8px;
|
||||
}
|
||||
.layout {
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.capsule {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 30px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.55);
|
||||
box-sizing: border-box;
|
||||
color: #303133;
|
||||
.cap-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #ededed;
|
||||
svg {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.cap-divider {
|
||||
width: 1px;
|
||||
height: 16px;
|
||||
background: rgba(0, 0, 0, 0.12);
|
||||
margin: 0 10px;
|
||||
}
|
||||
}
|
||||
.layout{
|
||||
background: #fff;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,45 +1,265 @@
|
||||
<template>
|
||||
<div class="tpl-shop" :style="wrapperStyle">
|
||||
<div class="shop-title" v-if="title">{{ title }}</div>
|
||||
<div class="shop-grid">
|
||||
<div class="shop-head" v-if="headIcon || title || hasMore">
|
||||
<img v-if="headIcon" class="shop-head-icon" :src="headIcon" alt="" />
|
||||
<div class="shop-title" :style="titleStyle">{{ title }}</div>
|
||||
<div v-if="hasMore" class="shop-more" :style="moreStyle">更多 ></div>
|
||||
</div>
|
||||
|
||||
<!-- 老数据:两列网格 -->
|
||||
<div v-if="!styled" class="shop-grid-legacy">
|
||||
<div class="shop-item" v-for="i in 4" :key="i">
|
||||
<div class="shop-logo"></div>
|
||||
<div class="shop-name">店铺名称</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 样式一:店铺卡片三列 -->
|
||||
<div v-else-if="shopStyle === 'one'" class="shop-grid">
|
||||
<div class="shop-card" v-for="(shop, i) in previewShops" :key="i">
|
||||
<img
|
||||
v-if="shop.goodsImage"
|
||||
class="shop-card-bg"
|
||||
:style="logoStyle"
|
||||
:src="shop.goodsImage"
|
||||
alt=""
|
||||
/>
|
||||
<div v-else class="shop-card-bg shop-card-bg-empty" :style="logoStyle"></div>
|
||||
<div class="shop-card-body">
|
||||
<img v-if="showLogo" class="shop-card-logo" :src="shop.logo" alt="" />
|
||||
<div v-if="showName" class="shop-card-name" :style="nameStyle">{{ shop.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 样式二:店铺卡片 + 商品列表 -->
|
||||
<div v-else class="shop-street-list">
|
||||
<div class="shop-street-card" v-for="(shop, i) in previewShops" :key="i">
|
||||
<div class="shop-street-head">
|
||||
<img v-if="showLogo" class="shop-street-logo" :src="shop.logo" alt="" />
|
||||
<div v-if="showName" class="shop-street-name" :style="nameStyle">{{ shop.name }}</div>
|
||||
<img
|
||||
v-if="shop.goodsImage"
|
||||
class="shop-street-banner"
|
||||
:style="logoStyle"
|
||||
:src="shop.goodsImage"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<div class="shop-street-goods">
|
||||
<div
|
||||
class="street-goods-item"
|
||||
v-for="(goods, goodsIndex) in shop.goods"
|
||||
:key="goodsIndex"
|
||||
>
|
||||
<img class="street-goods-img" :src="goods.img" alt="" />
|
||||
<div class="street-goods-name">{{ goods.name }}</div>
|
||||
<div class="street-goods-price">¥{{ $filters.unitPrice(goods.price) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getGoodsListData } from "@/api/goods";
|
||||
|
||||
export default {
|
||||
name: "tpl_shop",
|
||||
props: {
|
||||
res: { type: Object, default: () => ({}) },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
goodsMap: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
first() {
|
||||
return (this.res.list && this.res.list[0]) || {};
|
||||
},
|
||||
title() {
|
||||
return (this.res.list && this.res.list[0] && this.res.list[0].title) || "";
|
||||
return this.first.title || "";
|
||||
},
|
||||
headIcon() {
|
||||
return this.first.icon || "";
|
||||
},
|
||||
hasMore() {
|
||||
return !!this.first.moreUrl;
|
||||
},
|
||||
showLogo() {
|
||||
return this.first.showLogo !== false;
|
||||
},
|
||||
showName() {
|
||||
return this.first.showName !== false;
|
||||
},
|
||||
shopStyle() {
|
||||
return this.first.shopStyle || "one";
|
||||
},
|
||||
// 自定义模式展示已选店铺,默认模式用示例店铺
|
||||
previewShops() {
|
||||
const DEMO = [
|
||||
{
|
||||
name: "Apple",
|
||||
logo: "/demo/apple.png",
|
||||
goodsImage: "/demo/apple-phone.png",
|
||||
goods: [
|
||||
{ name: "iPhone 16 Pro", price: 8999, img: "/demo/apple-phone.png" },
|
||||
{ name: "iPhone 16", price: 7999, img: "/demo/apple-phone.png" },
|
||||
{ name: "iPhone 15", price: 6999, img: "/demo/apple-phone.png" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "小米",
|
||||
logo: "/demo/xiaomi.png",
|
||||
goodsImage: "/demo/xiaomi-phone.png",
|
||||
goods: [
|
||||
{ name: "Xiaomi 15 Pro", price: 5299, img: "/demo/xiaomi-phone.png" },
|
||||
{ name: "Xiaomi 15", price: 3999, img: "/demo/xiaomi-phone.png" },
|
||||
{ name: "Xiaomi 14", price: 3299, img: "/demo/xiaomi-phone.png" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "VIVO",
|
||||
logo: "/demo/vivo.png",
|
||||
goodsImage: "/demo/vivo-phone.png",
|
||||
goods: [
|
||||
{ name: "vivo X200 Pro", price: 5299, img: "/demo/vivo-phone.png" },
|
||||
{ name: "vivo X200", price: 4299, img: "/demo/vivo-phone.png" },
|
||||
{ name: "vivo X100 Pro", price: 3999, img: "/demo/vivo-phone.png" },
|
||||
],
|
||||
},
|
||||
];
|
||||
if (this.first.dataMode === "custom" && this.first.shopList && this.first.shopList.length) {
|
||||
return this.first.shopList.map((s) => {
|
||||
const goods = this.goodsMap[s.id] || [];
|
||||
return {
|
||||
id: s.id,
|
||||
name: s.storeName,
|
||||
logo: s.storeLogo,
|
||||
goods,
|
||||
goodsImage: goods[0]?.img || s.goodsImage || s.storeBanner || "",
|
||||
};
|
||||
});
|
||||
}
|
||||
return DEMO;
|
||||
},
|
||||
// 面板保存过样式字段才启用自定义样式,老数据保持原样
|
||||
styled() {
|
||||
return this.res && this.res.paddingTop !== undefined;
|
||||
},
|
||||
titleStyle() {
|
||||
return this.styled && this.res.titleColor
|
||||
? { color: this.res.titleColor }
|
||||
: {};
|
||||
},
|
||||
nameStyle() {
|
||||
return this.styled && this.res.nameColor
|
||||
? { color: this.res.nameColor }
|
||||
: {};
|
||||
},
|
||||
moreStyle() {
|
||||
return this.styled && this.res.moreColor
|
||||
? { color: this.res.moreColor }
|
||||
: {};
|
||||
},
|
||||
logoStyle() {
|
||||
return this.styled
|
||||
? { borderRadius: `${(Number(this.res.imgRadius) || 0) / 2}px` }
|
||||
: {};
|
||||
},
|
||||
wrapperStyle() {
|
||||
if (!this.styled) {
|
||||
return {
|
||||
background: this.res.bgColor || "#FFFFFF",
|
||||
padding: `10px ${this.res.paddingX || 0}px`,
|
||||
marginBottom: `${this.res.pageGap !== undefined ? this.res.pageGap : 10}px`,
|
||||
borderRadius: `${this.res.borderRadius || 0}px`,
|
||||
overflow: "hidden",
|
||||
};
|
||||
}
|
||||
const res = this.res;
|
||||
const bg =
|
||||
!res.bgColor2 || res.bgColor1 === res.bgColor2
|
||||
? res.bgColor1
|
||||
: `linear-gradient(180deg, ${res.bgColor1}, ${res.bgColor2})`;
|
||||
const px = (v) => `${(Number(v) || 0) / 2}px`;
|
||||
return {
|
||||
background: this.res.bgColor || "#FFFFFF",
|
||||
padding: `10px ${this.res.paddingX || 0}px`,
|
||||
marginBottom: `${this.res.pageGap !== undefined ? this.res.pageGap : 10}px`,
|
||||
borderRadius: `${this.res.borderRadius || 0}px`,
|
||||
overflow: "hidden",
|
||||
background: bg,
|
||||
padding: `${px(res.paddingTop)} ${px(res.paddingX)} ${px(res.paddingBottom)}`,
|
||||
marginBottom: px(res.pageGap),
|
||||
borderRadius: px(res.borderRadius),
|
||||
overflow: Number(res.borderRadius) > 0 ? "hidden" : "visible",
|
||||
};
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
"first.dataMode": "loadGoodsImages",
|
||||
"first.shopList": {
|
||||
handler: "loadGoodsImages",
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.loadGoodsImages();
|
||||
},
|
||||
methods: {
|
||||
loadGoodsImages() {
|
||||
if (this.first.dataMode !== "custom") return;
|
||||
(this.first.shopList || []).forEach((shop) => {
|
||||
if (!shop.id || this.goodsMap[shop.id]) return;
|
||||
getGoodsListData({
|
||||
storeId: shop.id,
|
||||
pageNumber: 1,
|
||||
pageSize: 3,
|
||||
goodsStatus: "UPPER",
|
||||
})
|
||||
.then((res) => {
|
||||
const records = (res && res.result && res.result.records) || [];
|
||||
this.goodsMap[shop.id] = records
|
||||
.map((goods) => ({
|
||||
name: goods.goodsName,
|
||||
price: goods.price,
|
||||
img: goods.thumbnail || goods.small || goods.original,
|
||||
}))
|
||||
.filter((goods) => goods.img);
|
||||
})
|
||||
.catch(() => {});
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.shop-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
.shop-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4px 12px 10px;
|
||||
}
|
||||
.shop-grid {
|
||||
.shop-head-icon {
|
||||
width: 62px;
|
||||
height: 16px;
|
||||
object-fit: contain;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.shop-title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.shop-more {
|
||||
flex-shrink: 0;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
.shop-grid-legacy {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 0 12px;
|
||||
@@ -62,4 +282,132 @@ export default {
|
||||
color: #333;
|
||||
margin-left: 8px;
|
||||
}
|
||||
/* 样式一 */
|
||||
.shop-grid {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
.shop-card {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
background: #f7f7f7;
|
||||
}
|
||||
.shop-card-bg {
|
||||
width: 100%;
|
||||
height: 56px;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
background: #eee;
|
||||
}
|
||||
.shop-card-bg-empty {
|
||||
background: linear-gradient(135deg, #f5f5f5, #e8e8e8);
|
||||
}
|
||||
.shop-card-body {
|
||||
padding: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
.shop-card-logo {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: block;
|
||||
width: 60px;
|
||||
height: 24px;
|
||||
margin: -12px auto 0;
|
||||
object-fit: contain;
|
||||
background: #fff;
|
||||
}
|
||||
.shop-card-name {
|
||||
font-size: 12px;
|
||||
margin-top: 3px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
/* 样式二:店铺卡片 + 商品列表 */
|
||||
.shop-street-list {
|
||||
padding: 0 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.shop-street-card {
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
background: #f7f7f7;
|
||||
}
|
||||
.shop-street-head {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 34px;
|
||||
padding: 0 8px;
|
||||
box-sizing: border-box;
|
||||
background: $theme_color;
|
||||
overflow: hidden;
|
||||
}
|
||||
.shop-street-logo {
|
||||
flex-shrink: 0;
|
||||
width: 44px;
|
||||
height: 16px;
|
||||
padding: 1px 3px;
|
||||
box-sizing: border-box;
|
||||
object-fit: contain;
|
||||
background: #fff;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.shop-street-name {
|
||||
max-width: calc(100% - 110px);
|
||||
margin-left: 6px;
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.shop-street-banner {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 80px;
|
||||
height: 34px;
|
||||
object-fit: cover;
|
||||
}
|
||||
.shop-street-goods {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
padding: 6px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.street-goods-item {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
}
|
||||
.street-goods-img {
|
||||
width: 100%;
|
||||
height: 58px;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
background: #eee;
|
||||
}
|
||||
.street-goods-name {
|
||||
padding: 4px 4px 0;
|
||||
color: #282828;
|
||||
font-size: 11px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.street-goods-price {
|
||||
padding: 2px 4px 5px;
|
||||
color: $theme_color;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,56 +1,132 @@
|
||||
<template>
|
||||
<div class="layout" :style="{ textAlign: res.list[0].textAlign }">
|
||||
<div class="background" :style="{ backgroundColor: res.list[0].bk_color }">
|
||||
<div class="title" :style="{ color: res.list[0].color }">
|
||||
{{ res.list[0].title }}
|
||||
<div class="layout" :style="wrapperStyle">
|
||||
<!-- 新样式:左标题+副标题,右侧更多 -->
|
||||
<div v-if="styled" class="title-bar" :style="barStyle">
|
||||
<div class="title-left">
|
||||
<span class="title-main" :style="mainStyle">{{ item.title }}</span>
|
||||
<span class="title-sub" :style="subStyle" v-if="item.title1">{{ item.title1 }}</span>
|
||||
</div>
|
||||
<div
|
||||
style="
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 2px;
|
||||
color: #fff;
|
||||
line-height: 42px;
|
||||
font-size: 10px;
|
||||
"
|
||||
>
|
||||
<a
|
||||
:href="res.list[0].url"
|
||||
:style="{ color: res.list[0].color1 }"
|
||||
style="text-decoration: none"
|
||||
>{{ res.list[0].title1 }}</a
|
||||
>
|
||||
<div v-if="item.showMore" class="title-more" :style="moreStyle">
|
||||
{{ item.moreText || "更多" }} >
|
||||
</div>
|
||||
</div>
|
||||
<!-- 老数据:保持原样 -->
|
||||
<div
|
||||
v-else
|
||||
class="background"
|
||||
:style="{ backgroundColor: item.bk_color, textAlign: item.textAlign }"
|
||||
>
|
||||
<div class="title" :style="{ color: item.color }">{{ item.title }}</div>
|
||||
<div class="legacy-sub">
|
||||
<a :style="{ color: item.color1 }" style="text-decoration: none">{{ item.title1 }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineOptions({ title: "标题栏" });
|
||||
import { computed } from "vue";
|
||||
|
||||
defineProps(["res"]);
|
||||
defineOptions({ title: "标题栏" });
|
||||
const props = defineProps(["res"]);
|
||||
|
||||
const item = computed(() => props.res?.list?.[0] || {});
|
||||
|
||||
// 面板保存过样式字段才启用新样式,老数据保持原样
|
||||
const styled = computed(() => props.res && props.res.paddingTop !== undefined);
|
||||
|
||||
const px = (v) => `${(Number(v) || 0) / 2}px`;
|
||||
const wrapperStyle = computed(() => {
|
||||
const res = props.res || {};
|
||||
if (!styled.value) return {};
|
||||
return {
|
||||
padding: `${px(res.paddingTop)} ${px(res.paddingX)} ${px(res.paddingBottom)}`,
|
||||
marginBottom: px(res.pageGap),
|
||||
borderRadius: `${px(res.radiusTop)} ${px(res.radiusTop)} ${px(res.radiusBottom)} ${px(res.radiusBottom)}`,
|
||||
overflow:
|
||||
Number(res.radiusTop) > 0 || Number(res.radiusBottom) > 0 ? "hidden" : "visible",
|
||||
background: "#ffffff",
|
||||
boxSizing: "border-box",
|
||||
};
|
||||
});
|
||||
const barStyle = computed(() => {
|
||||
const it = item.value;
|
||||
if (it.bgType === "image" && it.bgImage) {
|
||||
return {
|
||||
backgroundImage: `url(${it.bgImage})`,
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
};
|
||||
}
|
||||
return { backgroundColor: it.bk_color || "#FFFFFF" };
|
||||
});
|
||||
const mainStyle = computed(() => ({
|
||||
color: item.value.color || "#282828",
|
||||
fontSize: px(item.value.fontSize || 20),
|
||||
fontWeight: item.value.textStyle === "bold" ? "bold" : "normal",
|
||||
fontStyle: item.value.textStyle === "italic" ? "italic" : "normal",
|
||||
}));
|
||||
const subStyle = computed(() => ({
|
||||
color: item.value.color1 || "#282828",
|
||||
fontSize: px(item.value.subFontSize || 14),
|
||||
}));
|
||||
const moreStyle = computed(() => ({
|
||||
color: item.value.moreColor || "#282828",
|
||||
fontSize: px(item.value.moreFontSize || 12),
|
||||
}));
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "./tpl.scss";
|
||||
.layout {
|
||||
background: #ffffff;
|
||||
}
|
||||
.title-bar {
|
||||
width: 100%;
|
||||
min-height: 42px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.title-left {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
overflow: hidden;
|
||||
}
|
||||
.title-main {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.title-sub {
|
||||
margin-left: 6px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.title-more {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.background {
|
||||
background-color: rgb(255, 87, 62);
|
||||
/*background: url("../../../../assets/title.png") no-repeat;*/
|
||||
position: absolute;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
|
||||
height: 42px;
|
||||
background-size: cover;
|
||||
}
|
||||
.layout {
|
||||
text-align: center;
|
||||
position: relative;
|
||||
height: 42px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
background: #ffffff;
|
||||
}
|
||||
.legacy-sub {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 2px;
|
||||
color: #fff;
|
||||
line-height: 42px;
|
||||
font-size: 10px;
|
||||
}
|
||||
.title {
|
||||
line-height: 42px;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<template>
|
||||
<div class="layout" :style="[wrapperStyle, gapStyle]">
|
||||
<div class="view-width-100" :style="imgStyle">
|
||||
<img class="image-mode" :src="res.list[0].img" />
|
||||
<img alt="模板图片" class="image-mode" :src="res.list[0].img" />
|
||||
</div>
|
||||
<div class="view-width-100" :style="gapStyle">
|
||||
<div class="view-height-85" :style="imgStyle">
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="view-width-100" :style="imgStyle">
|
||||
<img class="image-mode" :src="res.list[2].img" />
|
||||
<img alt="模板图片" class="image-mode" :src="res.list[2].img" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -44,8 +44,36 @@ const item = computed(() => {
|
||||
ensureVideoItem(data);
|
||||
return data;
|
||||
});
|
||||
const wrapperStyle = computed(() => getVideoWrapperStyle(item.value));
|
||||
const boxStyle = computed(() => getVideoBoxStyle(item.value));
|
||||
|
||||
// 面板保存过样式字段(options 级)才启用新样式,老数据走 item.style 体系
|
||||
const styled = computed(() => props.res && props.res.paddingTop !== undefined);
|
||||
// 画布 375px ≈ 750rpx,预览按一半像素呈现
|
||||
const px = (v) => `${(Number(v) || 0) / 2}px`;
|
||||
const wrapperStyle = computed(() => {
|
||||
const res = props.res || {};
|
||||
if (!styled.value) return getVideoWrapperStyle(item.value);
|
||||
const bg =
|
||||
!res.bgColor2 || res.bgColor1 === res.bgColor2
|
||||
? res.bgColor1
|
||||
: `linear-gradient(180deg, ${res.bgColor1}, ${res.bgColor2})`;
|
||||
return {
|
||||
padding: `${px(res.paddingTop)} ${px(res.paddingX)} ${px(res.paddingBottom)}`,
|
||||
background: bg,
|
||||
borderRadius: px(res.borderRadius),
|
||||
marginBottom: px(res.pageGap),
|
||||
overflow: Number(res.borderRadius) > 0 ? "hidden" : "visible",
|
||||
boxSizing: "border-box",
|
||||
};
|
||||
});
|
||||
const boxStyle = computed(() => {
|
||||
if (!styled.value) return getVideoBoxStyle(item.value);
|
||||
return {
|
||||
borderRadius: px(props.res.imgRadius),
|
||||
overflow: "hidden",
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
};
|
||||
});
|
||||
const aspectPadding = computed(() => getAspectPadding(item.value.aspectRatio));
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -10,14 +10,20 @@
|
||||
/>
|
||||
</el-tabs>
|
||||
<div class="operation mb_10">
|
||||
<el-button type="primary" @click="handleAdd()">添加页面</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>
|
||||
<div class="page-table-wrap">
|
||||
<el-table v-loading="loading" :data="list" style="width: 100%">
|
||||
<el-table-column prop="name" label="页面名称" min-width="240" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ row.name || "暂无模板昵称" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatTime(row.createTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-switch
|
||||
@@ -30,16 +36,17 @@
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="140" align="center" fixed="right">
|
||||
<el-table-column label="操作" width="160" align="center">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row">
|
||||
<a class="link-text" @click="handleEdit(row)">修改</a>
|
||||
<a class="link-text" :href="editHref(row)" target="_blank">修改</a>
|
||||
<span class="op-split">|</span>
|
||||
<a class="link-text" @click="confirmDel(row)">删除</a>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="mt_10" style="display: flex; justify-content: flex-end">
|
||||
<el-pagination
|
||||
v-model:current-page="params.pageNumber"
|
||||
@@ -57,10 +64,11 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref } from "vue";
|
||||
import { computed, onMounted, reactive, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import * as API_Other from "@/api/other.js";
|
||||
import { Message, Modal } from "@/utils/message";
|
||||
import { unixToDate } from "@/utils/filters";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
@@ -131,6 +139,12 @@ function beforeRelease(row) {
|
||||
});
|
||||
});
|
||||
}
|
||||
function formatTime(time) {
|
||||
if (!time) return "—";
|
||||
const ts = new Date(time).getTime();
|
||||
if (Number.isNaN(ts)) return time;
|
||||
return unixToDate(ts / 1000);
|
||||
}
|
||||
function init() {
|
||||
loading.value = true;
|
||||
API_Other.getHomeList(params).then((res) => {
|
||||
@@ -147,8 +161,8 @@ function init() {
|
||||
total.value = res.result.total;
|
||||
});
|
||||
}
|
||||
function handleEdit(val) {
|
||||
router.push({
|
||||
function editHref(val) {
|
||||
const { href } = router.resolve({
|
||||
path: "/floorList/main",
|
||||
query: {
|
||||
id: val.id,
|
||||
@@ -157,13 +171,15 @@ function handleEdit(val) {
|
||||
pagetype: params.pageType,
|
||||
},
|
||||
});
|
||||
return href;
|
||||
}
|
||||
function handleAdd() {
|
||||
router.push({
|
||||
path: "/floorList/main",
|
||||
query: { pagetype: params.pageType },
|
||||
});
|
||||
}
|
||||
const addHref = computed(
|
||||
() =>
|
||||
router.resolve({
|
||||
path: "/floorList/main",
|
||||
query: { pagetype: params.pageType },
|
||||
}).href
|
||||
);
|
||||
function changePageSize(v) {
|
||||
params.pageNumber = 1;
|
||||
params.pageSize = v;
|
||||
@@ -191,4 +207,11 @@ function handleDel(val) {
|
||||
.page-type-tabs {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.add-page-btn {
|
||||
text-decoration: none;
|
||||
}
|
||||
.page-table-wrap {
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -31,6 +31,9 @@ module.exports = {
|
||||
port: configs.port,
|
||||
client: {
|
||||
overlay: {
|
||||
// 编译警告(如上游分销模块未完成的导出)不再弹全屏浮层,真实错误仍然弹出
|
||||
warnings: false,
|
||||
errors: true,
|
||||
runtimeErrors: (error) => {
|
||||
const message = error?.message || "";
|
||||
if (message.includes("ResizeObserver loop")) {
|
||||
|
||||
Reference in New Issue
Block a user