refactor(decoration): 重构店铺装修组件配置并规范化环境变量

- 重构装修组件配置,新增 icon 与 group 分类字段
- 将组件"图片"重命名为"热区","图片轮播"重命名为"轮播图"
- 新增搜索、公告、视频组件配置项
- 统一为各图片项添加 model 字段标识跳转模式
- 将装修页面组件间 pageGap 间距由 10 调整为 0
- 移除 im 模块中 VUE_APP_COMMON 变量,统一使用 VUE_APP_BUYER
- 为 im 环境变量与配置文件添加详细注释说明
This commit is contained in:
pikachu1995@126.com
2026-10-21 15:17:05 +08:00
parent da95f17885
commit f21ce6e252
118 changed files with 12064 additions and 1275 deletions

View File

@@ -114,7 +114,7 @@ const DEFAULTS = {
paddingTop: 10,
paddingBottom: 10,
paddingX: 12,
pageGap: 10,
pageGap: 0,
contentGap: 0,
borderRadius: 0,
imgRadius: 0,

View File

@@ -98,7 +98,7 @@ const DEFAULTS = {
paddingBottom: 10,
contentGap: 10,
paddingX: 0,
pageGap: 10,
pageGap: 0,
borderRadius: 0,
};
const DEFAULT_ITEM = { title: "文章列表", categoryId: "", count: 6 };

View File

@@ -37,7 +37,7 @@ const DEFAULTS = {
bgColor2: "#f5f5f5",
height: 10,
paddingX: 0,
pageGap: 10,
pageGap: 0,
};
export default {

View File

@@ -89,7 +89,7 @@ const DEFAULTS = {
paddingBottom: 10,
contentGap: 5,
paddingX: 12,
pageGap: 10,
pageGap: 0,
borderRadius: 0,
};
const DEFAULT_ITEM = { title: "优惠券", count: 5 };

View File

@@ -108,7 +108,7 @@ const DEFAULTS = {
paddingTop: 10,
paddingBottom: 10,
paddingX: 12,
pageGap: 10,
pageGap: 0,
borderRadius: 0,
imgRadius: 0,
};

View File

@@ -339,7 +339,7 @@ const DEFAULTS = {
paddingBottom: 10,
paddingX: 12,
contentGap: 10,
pageGap: 10,
pageGap: 0,
borderRadius: 0,
contentRadius: 7,
};

View File

@@ -47,7 +47,7 @@ const DEFAULTS = {
lineStyle: "dashed",
lineHeight: 1,
paddingX: 0,
pageGap: 10,
pageGap: 0,
};
export default {

View File

@@ -179,7 +179,7 @@ const DEFAULTS = {
paddingBottom: 10,
paddingX: 12,
contentGap: 10,
pageGap: 10,
pageGap: 0,
borderRadius: 0,
imgRadius: 30,
};

View File

@@ -164,7 +164,7 @@ const DEFAULTS = {
paddingTop: 0,
paddingBottom: 0,
paddingX: 12,
pageGap: 10,
pageGap: 0,
borderRadius: 0,
itemColor: "#333333",
};

View File

@@ -231,7 +231,7 @@ const DEFAULTS = {
paddingBottom: 10,
contentGap: 10,
paddingX: 12,
pageGap: 10,
pageGap: 0,
borderRadius: 0,
imgRadius: 10,
showGoodsName: true,

View File

@@ -58,7 +58,7 @@ const DEFAULTS = {
bgColor1: "#FFFFFF",
bgColor2: "#FFFFFF",
paddingX: 12,
pageGap: 10,
pageGap: 0,
borderRadius: 0,
};

View File

@@ -228,7 +228,7 @@ const DEFAULTS = {
paddingTop: 10,
paddingBottom: 10,
paddingX: 0,
pageGap: 10,
pageGap: 0,
borderRadius: 0,
imgRadius: 7,
};

View File

@@ -157,7 +157,7 @@ const DEFAULTS = {
paddingTop: 10,
paddingBottom: 10,
paddingX: 12,
pageGap: 10,
pageGap: 0,
radiusTop: 0,
radiusBottom: 0,
color: "#282828",

View File

@@ -115,7 +115,7 @@ const DEFAULTS = {
paddingTop: 10,
paddingBottom: 10,
paddingX: 12,
pageGap: 10,
pageGap: 0,
borderRadius: 0,
imgRadius: 0,
};

View File

@@ -851,7 +851,7 @@ export const modelData = [
bgColor: "#f5f5f5",
height: 10,
paddingX: 0,
pageGap: 10
pageGap: 0
}
},
{
@@ -866,7 +866,7 @@ export const modelData = [
lineStyle: "dashed",
lineHeight: 1,
paddingX: 0,
pageGap: 10
pageGap: 0
}
},
{
@@ -879,7 +879,7 @@ export const modelData = [
content: "",
bgColor: "#FFFFFF",
paddingX: 12,
pageGap: 10,
pageGap: 0,
borderRadius: 0
}
},
@@ -988,7 +988,7 @@ export const modelData = [
list: [{ title: "文章列表", categoryId: "", count: 3 }],
bgColor: "#FFFFFF",
paddingX: 0,
pageGap: 10,
pageGap: 0,
borderRadius: 0
}
},
@@ -1002,7 +1002,7 @@ export const modelData = [
list: [{ title: "店铺列表", count: 4 }],
bgColor: "#FFFFFF",
paddingX: 0,
pageGap: 10,
pageGap: 0,
borderRadius: 0
}
},
@@ -1044,7 +1044,7 @@ export const modelData = [
paddingBottom: 10,
paddingX: 12,
contentGap: 10,
pageGap: 10,
pageGap: 0,
borderRadius: 0,
contentRadius: 7
}

View File

@@ -34,12 +34,13 @@
<el-button size="default" type="primary" @click="handleSpinShow">保存页面</el-button>
<el-dialog
title="保存中"
:title="progress ? '保存页面' : '保存中'"
v-model="saveDialog"
:show-close="true"
:close-on-click-modal="false"
append-to-body
destroy-on-close
@closed="handleDialogClosed"
>
<div v-if="progress">
<div class="model-item">
@@ -56,7 +57,7 @@
</div>
<div class="model-item">
将当前装修内容写入到本地缓存中,下次进入页面时可继续使用
<el-button type="small" @click="witeLocalStore">写入</el-button>
<el-button size="small" @click="witeLocalStore">写入</el-button>
</div>
<el-button type="primary" @click="save()">保存</el-button>
@@ -84,7 +85,7 @@ const appStore = useAppStore();
const hasCache = ref(false); // 是否有缓存
const progress = ref(true); // 展示进度
const num = ref(20); // 提交进度
const num = ref(0); // 提交进度
const saveDialog = ref(false); // 加载状态
const way = ref([
// 装修tab栏切换
@@ -119,10 +120,17 @@ function startEditName() {
const submitWay = reactive({
// 表单信息
pageShow: route.query.type || false,
pageShow: route.query.type === "true" || route.query.type === true || route.query.type === "OPEN",
pageClientType: "H5"
});
watch(
() => route.query.type,
(val) => {
submitWay.pageShow = val === "true" || val === true || val === "OPEN";
}
);
// 模板名称与「页面设置」面板共用 store 中的同一份数据,双向同步
const pageName = computed({
get() {
@@ -191,9 +199,16 @@ function clickBtn(val) {
* 加载,并保存模板
*/
function handleSpinShow() {
progress.value = true;
num.value = 0;
saveDialog.value = true;
}
function handleDialogClosed() {
progress.value = true;
num.value = 0;
}
// 填写是否发布,模板名称之后保存
function save() {
// 楼层装修页面(非开屏/弹窗广告)需校验页面设置的必填项
@@ -220,49 +235,57 @@ function save() {
return false;
}
submitWay?.pageShow === true
? (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";
// submitWay.pageType = props.pagetype;
const payload = {
...submitWay,
pageShow: submitWay.pageShow ? "OPEN" : "CLOSE",
name: pageName.value,
pageData: JSON.stringify(appStore.styleStore),
pageType: route.query.pagetype || "INDEX",
};
if (route.query.pagetype == 'ALERT' || route.query.pagetype == 'OPEN_SCREEN_ANIMATION') {
update(submitWay);
update(payload);
} else {
route.query.id ? updateHome() : submit(submitWay);
route.query.id ? updateHome(payload) : submit(payload);
}
// route.query.id ? update() : submit(submitWay);
}
// 更新
function updateHome() {
function updateHome(payload) {
progress.value = false;
API_Other.updateHome(route.query.id, {
num.value = 20;
const data = payload || {
pageData: JSON.stringify(appStore.styleStore),
name: pageName.value,
pageShow: submitWay.pageShow,
pageType: submitWay.pageType,
pageShow: submitWay.pageShow ? "OPEN" : "CLOSE",
pageType: submitWay.pageType || route.query.pagetype || "INDEX",
pageClientType: "H5",
})
};
API_Other.updateHome(route.query.id, data)
.then(res => {
num.value = 50;
num.value = 60;
if (res.success) {
num.value = 80;
num.value = 100;
/**制作保存成功动画¸ */
setTimeout(() => {
saveDialog.value = false;
progress.value = true;
num.value = 0;
Message.success("修改成功");
goBack();
}, 1000);
}, 800);
} else {
progress.value = true;
num.value = 0;
saveDialog.value = false;
Message.error("修改失败,请稍后重试");
}
})
.catch(() => {
progress.value = true;
num.value = 0;
saveDialog.value = false;
Message.error("修改失败,请稍后重试");
});
}
@@ -274,6 +297,7 @@ function update(submitWay) {
return false;
}
progress.value = false;
num.value = 20;
let id = route.query.id;
const pagetype = route.query.pagetype;
@@ -283,29 +307,40 @@ function update(submitWay) {
OPEN_SCREEN_ANIMATION: "OPEN_SCREEN_ANIMATION",
}[pagetype ? pagetype : "INDEX"];
const payload = { ...submitWay };
if (pagetype) {
submitWay.pageData = JSON.stringify(
payload.pageData = JSON.stringify(
appStore.openStyleStore[0]
);
submitWay.pageType = pageType;
payload.pageType = pageType;
id = appStore.openStoreId;
}
API_Other.updateHome(id, submitWay)
API_Other.updateHome(id, payload)
.then((res) => {
num.value = 50;
num.value = 60;
if (res.success) {
num.value = 80;
num.value = 100;
/**制作保存成功动画¸ */
setTimeout(() => {
saveDialog.value = false;
progress.value = true;
num.value = 0;
Message.success("修改成功");
goBack();
}, 1000);
}, 800);
} else {
progress.value = true;
num.value = 0;
saveDialog.value = false;
Message.error("修改失败,请稍后重试");
}
})
.catch(() => {
progress.value = true;
num.value = 0;
saveDialog.value = false;
Message.error("修改失败,请稍后重试");
});
}
@@ -313,28 +348,42 @@ function update(submitWay) {
// 返回查询数据页面
function goBack() {
router.go(-1);
if (window.history.state?.back) {
router.go(-1);
} else {
window.close();
}
}
// 保存
function submit(submitWay) {
progress.value = false;
num.value = 20;
API_Other.setHomeSetup(submitWay)
.then(res => {
num.value = 50;
num.value = 60;
if (res.success) {
num.value = 80;
num.value = 100;
/**制作保存成功动画¸ */
setTimeout(() => {
saveDialog.value = false;
progress.value = true;
num.value = 0;
Message.success("保存成功");
goBack();
}, 1000);
}, 800);
} else {
progress.value = true;
num.value = 0;
saveDialog.value = false;
Message.error("保存失败,请稍后重试");
}
})
.catch(() => {
progress.value = true;
num.value = 0;
saveDialog.value = false;
Message.error("保存失败,请稍后重试");
});
}
</script>

View File

@@ -6,7 +6,7 @@
}
.layout {
padding: 8px;
margin: 4px 0;
margin: 0;
background: #fff;
}
.layout,

View File

@@ -58,7 +58,7 @@ export default {
return {
background: this.res.bgColor || "#FFFFFF",
padding: `10px ${this.res.paddingX || 0}px`,
marginBottom: `${this.res.pageGap !== undefined ? this.res.pageGap : 10}px`,
marginBottom: `${this.res.pageGap !== undefined ? this.res.pageGap : 0}px`,
borderRadius: `${this.res.borderRadius || 0}px`,
overflow: "hidden",
};

View File

@@ -4,7 +4,7 @@
:style="{
background: blankBg,
height: px(res.height ?? 10),
margin: `0 ${px(res.paddingX ?? 0)} ${px(res.pageGap ?? 10)}`,
margin: `0 ${px(res.paddingX ?? 0)} ${px(res.pageGap ?? 0)}`,
}"
></div>
</template>

View File

@@ -145,7 +145,7 @@ export default {
return {
background: this.res.bgColor || "#FFFFFF",
padding: `10px ${this.res.paddingX || 0}px`,
marginBottom: `${this.res.pageGap !== undefined ? this.res.pageGap : 10}px`,
marginBottom: `${this.res.pageGap !== undefined ? this.res.pageGap : 0}px`,
borderRadius: `${this.res.borderRadius || 0}px`,
overflow: "hidden",
};

View File

@@ -9,11 +9,11 @@
/>
<div v-else class="integral-head-title" :style="headTitleStyle">{{ title }}</div>
<div class="integral-head-space"></div>
<div class="integral-head-more" :style="moreStyle">更多 &gt;</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="join-item" v-for="item in count" :key="item">
<div class="item-img-box">
<img
class="item-img"
@@ -71,6 +71,45 @@
</template>
<script setup>
import { computed } from "vue";
const INTEGRAL_DEMO = [
{
goodsName: "小米小爱音箱 Play 增强版 智能红外遥控",
points: 1200,
originalPrice: 149,
thumbnail: "/demo/xiaomi-phone.png",
},
{
goodsName: "Apple 20W USB-C 官方原装快充充电器",
points: 1500,
originalPrice: 149,
thumbnail: "/demo/apple-phone.png",
},
{
goodsName: "米家自动洗手机 套装 抑菌洗手液",
points: 800,
originalPrice: 79,
thumbnail: "/demo/xiaomi-phone.png",
},
{
goodsName: "多功能桌面风扇 超长续航三档风力",
points: 600,
originalPrice: 59,
thumbnail: "/demo/vivo-phone.png",
},
{
goodsName: "精品保温杯 316不锈钢 480ml",
points: 990,
originalPrice: 99,
thumbnail: "/demo/apple-phone.png",
},
{
goodsName: "无线双模鼠标 2.4G+蓝牙 轻音便携",
points: 750,
originalPrice: 69,
thumbnail: "/demo/vivo-phone.png",
},
];
const props = defineProps(["res"]);
@@ -79,9 +118,27 @@ const title = computed(
);
const firstItem = computed(() => (props.res.list && props.res.list[0]) || {});
const layoutStyle = computed(() => props.res.layoutStyle || "1");
const count = computed(() => {
const c = firstItem.value?.count ?? props.res?.count;
if (c !== undefined && c !== null && !isNaN(Number(c)) && Number(c) > 0) {
return Number(c);
}
return layoutStyle.value === "1" ? 4 : 6;
});
const displayData = computed(() => {
const data = (firstItem.value && firstItem.value.data) || [];
return layoutStyle.value === "1" ? data.slice(0, 4) : data.slice(0, 6);
const raw = (firstItem.value && firstItem.value.data) || [];
const data = raw.length ? raw : INTEGRAL_DEMO;
const targetCount = count.value;
if (data.length >= targetCount) {
return data.slice(0, targetCount);
}
// 如果设置的数量大于现有模拟数据长度,循环补齐,使预览数量与配置数量严格一致
const result = [...data];
while (result.length < targetCount) {
result.push({ ...data[result.length % data.length] });
}
return result.slice(0, targetCount);
});
// 面板保存过样式字段才启用新样式,老数据保持原样
@@ -94,7 +151,7 @@ const wrapperStyle = computed(() => {
return {
background: res.bgColor || "#FFFFFF",
padding: `0 ${res.paddingX || 0}px`,
marginBottom: `${res.pageGap !== undefined ? res.pageGap : 10}px`,
marginBottom: `${res.pageGap !== undefined ? res.pageGap : 0}px`,
borderRadius: `${res.borderRadius || 0}px`,
overflow: "hidden",
};

View File

@@ -4,7 +4,7 @@
:style="{
background: lineBg,
padding: `0 ${px(res.paddingX ?? 0)}`,
marginBottom: px(res.pageGap ?? 10),
marginBottom: px(res.pageGap ?? 0),
}"
>
<div

View File

@@ -23,7 +23,7 @@
<span class="pt-success">{{ firstItem.groupSuccess }}</span>
</template>
<span class="pt-space"></span>
<span class="pt-more" :style="moreStyle">更多 &gt;</span>
<span class="pt-more" :style="moreStyle">更多 ></span>
</div>
<div class="pt-body">
<!-- 样式一:纵向商品行 -->
@@ -154,7 +154,7 @@
<span v-else class="lv-head-title">{{ firstItem.title || "精彩直播" }}</span>
<span class="lv-head-sub" :style="titleStyle">{{ firstItem.title1 || "精彩抢先看" }}</span>
<span class="lv-space"></span>
<span class="lv-more" :style="moreStyle">更多 &gt;</span>
<span class="lv-more" :style="moreStyle">更多 ></span>
</div>
<div class="lv-head-en">Exciting live streaming</div>
<!-- 单行:横向滑动卡片 -->
@@ -201,7 +201,7 @@
<span class="sk-divider"></span>
<span class="sk-head-sub" :style="titleStyle">{{ firstItem.title1 || "邀请你的好友来参与" }}</span>
<span class="sk-space"></span>
<span class="sk-go">GO &gt;</span>
<span class="sk-go">GO ></span>
</div>
<!-- 样式一:横向滑动 -->
<div v-if="layoutStyle === '1'" class="sk-scroll" :style="gapStyle">
@@ -246,7 +246,7 @@
<span class="sk-divider"></span>
<span class="sk-head-sub" :style="titleStyle">{{ firstItem.title1 || "邀请好友来砍价" }}</span>
<span class="sk-space"></span>
<span class="sk-go">GO &gt;</span>
<span class="sk-go">GO ></span>
</div>
<!-- 样式一:横向滑动 -->
<div v-if="layoutStyle === '1'" class="sk-scroll" :style="gapStyle">
@@ -287,7 +287,7 @@
<div v-if="styled && (res.icon || res.moreUrl)" class="promo-head">
<img v-if="res.icon" class="promo-head-icon" :src="res.icon" alt="" />
<div class="promo-head-space"></div>
<div v-if="res.moreUrl" class="promo-more" :style="moreStyle">更多 &gt;</div>
<div v-if="res.moreUrl" class="promo-more" :style="moreStyle">更多 ></div>
</div>
<div class="join-list" :style="gapStyle">
<div
@@ -343,6 +343,155 @@
</template>
<script setup>
import { computed } from "vue";
const SECKILL_DEMO = [
{
goodsName: "iPhone 16 Pro 256GB 黑色钛金属",
price: 6999,
originalPrice: 8999,
thumbnail: "/demo/apple-phone.png",
},
{
goodsName: "Xiaomi 15 Pro 16GB+512GB 沉墨色",
price: 4699,
originalPrice: 5299,
thumbnail: "/demo/xiaomi-phone.png",
},
{
goodsName: "vivo X200 Pro 16GB+512GB 钛色",
price: 4899,
originalPrice: 5499,
thumbnail: "/demo/vivo-phone.png",
},
{
goodsName: "Apple Watch Series 10 46mm GPS款",
price: 2699,
originalPrice: 3199,
thumbnail: "/demo/apple-phone.png",
},
{
goodsName: "AirPods Pro (第二代) 配USB-C充电盒",
price: 1399,
originalPrice: 1899,
thumbnail: "/demo/apple-phone.png",
},
{
goodsName: "Xiaomi 平板 6S Pro 12.4英寸",
price: 2799,
originalPrice: 3299,
thumbnail: "/demo/xiaomi-phone.png",
},
];
const PINTUAN_DEMO = [
{
goodsName: "iPhone 16 Pro 5G智能手机 256GB 白色钛金属",
price: 7999,
originalPrice: 8999,
thumbnail: "/demo/apple-phone.png",
groupNum: 2,
joined: 128,
},
{
goodsName: "Xiaomi 15 5G智能手机 12GB+256GB 白色",
price: 3699,
originalPrice: 4299,
thumbnail: "/demo/xiaomi-phone.png",
groupNum: 2,
joined: 256,
},
{
goodsName: "vivo X200 5G智能手机 12GB+256GB 宝石蓝",
price: 3999,
originalPrice: 4599,
thumbnail: "/demo/vivo-phone.png",
groupNum: 3,
joined: 89,
},
{
goodsName: "Apple AirPods 4 主动降噪无线耳机",
price: 1099,
originalPrice: 1399,
thumbnail: "/demo/apple-phone.png",
groupNum: 2,
joined: 412,
},
{
goodsName: "小米手环 9 Pro 晨曦金 金属机身",
price: 349,
originalPrice: 399,
thumbnail: "/demo/xiaomi-phone.png",
groupNum: 2,
joined: 680,
},
{
goodsName: "vivo Pad3 Pro 13英寸 3.1K 超清大屏",
price: 2999,
originalPrice: 3399,
thumbnail: "/demo/vivo-phone.png",
groupNum: 2,
joined: 195,
},
];
const KANJIA_DEMO = [
{
goodsName: "米家智能空气炸锅 6.5L 嫩烤大容量",
purchasePrice: 0,
price: 0,
originalPrice: 299,
thumbnail: "/demo/xiaomi-phone.png",
},
{
goodsName: "电动声波牙刷 T300 高频震动洁齿",
purchasePrice: 0,
price: 0,
originalPrice: 129,
thumbnail: "/demo/vivo-phone.png",
},
{
goodsName: "小米随身手持小风扇 大风力轻便便携",
purchasePrice: 0,
price: 0,
originalPrice: 59,
thumbnail: "/demo/xiaomi-phone.png",
},
{
goodsName: "智能温湿度计 电子墨水屏高精度",
purchasePrice: 0,
price: 0,
originalPrice: 69,
thumbnail: "/demo/apple-phone.png",
},
];
const LIVE_DEMO = [
{
cover: "/demo/apple-phone.png",
title: "双11新品首发专场 抢先看",
anchorName: "官方品牌主播",
status: "LIVE",
tagText: "直播中",
tagClass: "lv-tag-live",
},
{
cover: "/demo/xiaomi-phone.png",
title: "米粉狂欢日 爆款好物直降",
anchorName: "好物推荐官",
status: "PREVIEW",
startTime: "10/22 19:00",
tagText: "预告",
tagClass: "lv-tag-preview",
},
{
cover: "/demo/vivo-phone.png",
title: "旗舰影像新机实测现场",
anchorName: "数码达人",
status: "REPLAY",
tagText: "回放",
tagClass: "lv-tag-replay",
},
];
const props = defineProps(["res"]);
@@ -354,7 +503,8 @@ const isLive = computed(() => firstItem.value.type === "LIVE");
const isSeckill = computed(() => firstItem.value.type === "SECKILL");
const isKanjia = computed(() => firstItem.value.type === "KANJIA");
const seckillData = computed(() => {
const data = (firstItem.value && firstItem.value.data) || [];
const raw = (firstItem.value && firstItem.value.data) || [];
const data = raw.length ? raw : (isKanjia.value ? KANJIA_DEMO : SECKILL_DEMO);
return layoutStyle.value === "2" ? data.slice(0, 4) : data.slice(0, 6);
});
// 老数据未保存过样式时,给个红→白渐变兜底背景
@@ -382,9 +532,12 @@ const fmtLiveTime = (t) => {
return s;
};
const liveData = computed(() => {
const data = (firstItem.value && firstItem.value.data) || [];
const raw = (firstItem.value && firstItem.value.data) || [];
if (!raw.length) {
return LIVE_DEMO;
}
const limit = layoutStyle.value === "2" ? 3 : 6;
return data.slice(0, limit).map((item) => {
return raw.slice(0, limit).map((item) => {
const tag = liveTagOf(item.status != null ? item.status : item.liveStatus);
return {
cover: item.cover || item.roomImg || item.shareImg || item.thumbnail || "",
@@ -398,11 +551,12 @@ const liveData = computed(() => {
});
});
const displayData = computed(() => {
const data = (firstItem.value && firstItem.value.data) || [];
const raw = (firstItem.value && firstItem.value.data) || [];
const data = raw.length ? raw : PINTUAN_DEMO;
if (layoutStyle.value === "3" || layoutStyle.value === "4") {
return data.slice(0, 6);
}
return data.slice(0, 2);
return data.slice(0, 4);
});
const headTitleImage = computed(() =>
props.res.headTitleType === "image" ? props.res.headTitleImage : ""

View File

@@ -4,7 +4,7 @@
:style="{
background: richBg,
padding: `0 ${px(res.paddingX ?? 12)}`,
marginBottom: px(res.pageGap ?? 10),
marginBottom: px(res.pageGap ?? 0),
borderRadius: px(res.borderRadius ?? 0),
overflow: 'hidden',
}"

View File

@@ -3,7 +3,7 @@
<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">更多 &gt;</div>
<div v-if="hasMore" class="shop-more" :style="moreStyle">更多 ></div>
</div>
<!-- 老数据:两列网格 -->
@@ -14,7 +14,7 @@
</div>
</div>
<!-- 样式一:店铺卡片三列 -->
<!-- 样式一:店铺卡片三列(全景大片橱窗) -->
<div v-else-if="shopStyle === 'one'" class="shop-grid">
<div class="shop-card" v-for="(shop, i) in previewShops" :key="i">
<img
@@ -25,6 +25,7 @@
alt=""
/>
<div v-else class="shop-card-bg shop-card-bg-empty" :style="logoStyle"></div>
<div class="shop-card-badge" v-if="isSelf(shop)">自营</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>
@@ -32,19 +33,21 @@
</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 class="shop-street-info">
<img v-if="showLogo" class="shop-street-logo" :src="shop.logo" alt="" />
<div class="shop-street-meta">
<div class="shop-street-name-row">
<div v-if="showName" class="shop-street-name" :style="nameStyle">{{ shop.name }}</div>
<span class="shop-street-tag" v-if="isSelf(shop)">自营</span>
</div>
<div class="shop-street-desc">热卖好物甄选中</div>
</div>
</div>
<div class="shop-street-enter">进店逛逛 &gt;</div>
</div>
<div class="shop-street-goods">
<div
@@ -52,9 +55,13 @@
v-for="(goods, goodsIndex) in shop.goods"
:key="goodsIndex"
>
<img class="street-goods-img" :src="goods.img" alt="" />
<div class="street-goods-cover">
<img class="street-goods-img" :src="goods.img" alt="" />
</div>
<div class="street-goods-name">{{ goods.name }}</div>
<div class="street-goods-price">¥{{ $filters.unitPrice(goods.price) }}</div>
<div class="street-goods-price">
<span class="street-goods-symbol">¥</span>{{ $filters.unitPrice(goods.price) }}
</div>
</div>
</div>
</div>
@@ -138,8 +145,9 @@ export default {
id: s.id,
name: s.storeName,
logo: s.storeLogo,
selfOperated: s.selfOperated,
goods,
goodsImage: goods[0]?.img || s.goodsImage || s.storeBanner || "",
goodsImage: goods[0]?.img || s.goodsImage || s.storeBanner || s.storeLogo || "",
};
});
}
@@ -174,7 +182,7 @@ export default {
return {
background: this.res.bgColor || "#FFFFFF",
padding: `10px ${this.res.paddingX || 0}px`,
marginBottom: `${this.res.pageGap !== undefined ? this.res.pageGap : 10}px`,
marginBottom: `${this.res.pageGap !== undefined ? this.res.pageGap : 0}px`,
borderRadius: `${this.res.borderRadius || 0}px`,
overflow: "hidden",
};
@@ -205,6 +213,17 @@ export default {
this.loadGoodsImages();
},
methods: {
isSelf(shop) {
if (!shop) return false;
return Boolean(
shop.selfOperated === true ||
shop.selfOperated === 'true' ||
shop.selfOperated === 1 ||
shop.selfOperated === '1' ||
(shop.name && shop.name.includes('自营')) ||
(shop.storeName && shop.storeName.includes('自营'))
);
},
loadGoodsImages() {
if (this.first.dataMode !== "custom") return;
(this.first.shopList || []).forEach((shop) => {
@@ -282,132 +301,213 @@ export default {
color: #333;
margin-left: 8px;
}
/* 样式一 */
/* 样式一:全景大片橱窗 */
.shop-grid {
display: flex;
gap: 8px;
padding: 0 12px;
}
.shop-card {
position: relative;
flex: 1;
min-width: 0;
border-radius: 6px;
height: 112px;
border-radius: 8px;
overflow: hidden;
background: #f7f7f7;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.shop-card-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 56px;
height: 100%;
display: block;
object-fit: cover;
background: #eee;
}
.shop-card-bg-empty {
background: linear-gradient(135deg, #f5f5f5, #e8e8e8);
}
.shop-card-badge {
position: absolute;
top: 5px;
right: 5px;
z-index: 2;
font-size: 10px;
color: #fff;
background: #e1251b;
padding: 1px 5px;
border-radius: 4px;
line-height: 1.3;
font-weight: 500;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}
.shop-card-body {
padding: 4px;
text-align: center;
position: absolute;
left: 0;
right: 0;
bottom: 0;
z-index: 2;
padding: 16px 6px 6px;
display: flex;
align-items: center;
gap: 4px;
background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.25) 35%, rgba(0, 0, 0, 0.72) 100%);
}
.shop-card-logo {
position: relative;
z-index: 1;
flex-shrink: 0;
display: block;
width: 60px;
height: 24px;
margin: -12px auto 0;
width: 20px;
height: 20px;
border-radius: 3px;
object-fit: contain;
background: #fff;
padding: 1px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
}
.shop-card-name {
font-size: 12px;
margin-top: 3px;
flex: 1;
min-width: 0;
font-size: 11px;
font-weight: 600;
color: #fff;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
}
/* 样式二:店铺卡片 + 商品列表 */
/* 样式二:大牌名店街 + 精品橱窗 */
.shop-street-list {
padding: 0 12px;
display: flex;
flex-direction: column;
gap: 8px;
gap: 10px;
}
.shop-street-card {
border-radius: 6px;
border-radius: 10px;
overflow: hidden;
background: #f7f7f7;
background: #fbfbfb;
border: 1px solid #f0f0f0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
}
.shop-street-head {
position: relative;
display: flex;
align-items: center;
height: 34px;
padding: 0 8px;
box-sizing: border-box;
background: $theme_color;
overflow: hidden;
justify-content: space-between;
padding: 8px 10px;
background: #fff;
border-bottom: 1px solid #f5f5f5;
}
.shop-street-info {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
flex: 1;
}
.shop-street-logo {
flex-shrink: 0;
width: 44px;
height: 16px;
padding: 1px 3px;
width: 32px;
height: 32px;
padding: 2px;
box-sizing: border-box;
object-fit: contain;
background: #fff;
border-radius: 2px;
border-radius: 50%;
border: 1px solid #eee;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
.shop-street-meta {
min-width: 0;
flex: 1;
}
.shop-street-name-row {
display: flex;
align-items: center;
gap: 5px;
}
.shop-street-name {
max-width: calc(100% - 110px);
margin-left: 6px;
color: #fff;
font-size: 13px;
font-weight: 600;
color: #333;
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-tag {
flex-shrink: 0;
font-size: 9px;
color: #fff;
background: #e1251b;
padding: 0 4px;
border-radius: 3px;
line-height: 14px;
font-weight: 500;
}
.shop-street-desc {
font-size: 10px;
color: #999;
margin-top: 2px;
}
.shop-street-enter {
flex-shrink: 0;
font-size: 10px;
font-weight: 500;
color: $theme_color;
background: #fff;
border: 1px solid rgba(255, 60, 42, 0.25);
padding: 3px 8px;
border-radius: 12px;
box-shadow: 0 1px 2px rgba(255, 60, 42, 0.05);
cursor: pointer;
margin-left: 8px;
}
.shop-street-goods {
display: flex;
gap: 6px;
padding: 6px;
gap: 8px;
padding: 8px;
box-sizing: border-box;
}
.street-goods-item {
flex: 1;
min-width: 0;
border-radius: 4px;
border-radius: 6px;
overflow: hidden;
background: #fff;
border: 1px solid #f2f2f2;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
}
.street-goods-cover {
width: 100%;
aspect-ratio: 1 / 1;
overflow: hidden;
background: #fafafa;
}
.street-goods-img {
width: 100%;
height: 58px;
height: 100%;
display: block;
object-fit: cover;
background: #eee;
}
.street-goods-name {
padding: 4px 4px 0;
color: #282828;
padding: 4px 6px 0;
color: #333;
font-size: 11px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.street-goods-price {
padding: 2px 4px 5px;
padding: 2px 6px 6px;
color: $theme_color;
font-size: 11px;
font-weight: 600;
font-size: 12px;
font-weight: 700;
}
.street-goods-symbol {
font-size: 9px;
font-weight: normal;
margin-right: 1px;
}
</style>

View File

@@ -7,7 +7,7 @@
<span class="title-sub" :style="subStyle" v-if="item.title1">{{ item.title1 }}</span>
</div>
<div v-if="item.showMore" class="title-more" :style="moreStyle">
{{ item.moreText || "更多" }} &gt;
{{ item.moreText || "更多" }} >
</div>
</div>
<!-- 老数据:保持原样 -->

View File

@@ -24,6 +24,11 @@
{{ formatTime(row.createTime) }}
</template>
</el-table-column>
<el-table-column label="修改时间" width="180">
<template #default="{ row }">
{{ formatTime(row.updateTime) }}
</template>
</el-table-column>
<el-table-column label="状态" width="120" align="center">
<template #default="{ row }">
<el-switch

View File

@@ -0,0 +1,129 @@
<template>
<div class="layout">
<el-form ref="formValidateRef" label-width="200px" label-position="right" :model="formValidate" :rules="ruleValidate">
<el-form-item label="数据脱敏级别" prop="sensitiveLevel">
<el-radio-group v-model="formValidate.sensitiveLevel">
<el-radio :value="0">不做脱敏</el-radio>
<el-radio :value="1">管理端脱敏</el-radio>
<el-radio :value="2">全端脱敏</el-radio>
</el-radio-group>
<span class="desc">控制手机号、收货地址、身份证等敏感信息脱敏显示策略</span>
</el-form-item>
<el-form-item label="演示站点保护" prop="isDemoSite">
<el-switch v-model="formValidate.isDemoSite" active-text="开启" inactive-text="关闭" />
<span class="desc">开启后拦截核心管理端修改与删除请求,防止演示数据被篡改</span>
</el-form-item>
<el-form-item label="买家端Token有效期" prop="buyerTokenExpireTime">
<el-input type="number" v-model="formValidate.buyerTokenExpireTime">
<template #append>分钟</template>
</el-input>
<span class="desc">买家免密登录会话有效时长</span>
</el-form-item>
<el-form-item label="管理/商家端Token有效期" prop="managerTokenExpireTime">
<el-input type="number" v-model="formValidate.managerTokenExpireTime">
<template #append>分钟</template>
</el-input>
<span class="desc">管理后台及店铺端操作人员会话有效时长</span>
</el-form-item>
<div class="label-btns" style="margin-left: 200px;">
<el-button type="primary" @click="submit('formValidateRef')">保存设置</el-button>
</div>
</el-form>
</div>
</template>
<script setup>
import { getCurrentInstance, reactive, ref } from "vue";
import { ElMessage } from "element-plus";
import { setSetting } from "@/api/index";
import { handleSubmit } from "./validate";
const props = defineProps(["res", "type"]);
const { proxy } = getCurrentInstance();
const formValidateRef = ref(null);
const formValidate = ref({
sensitiveLevel: 1,
isDemoSite: false,
buyerTokenExpireTime: 259200,
managerTokenExpireTime: 86400,
});
const ruleValidate = reactive({
buyerTokenExpireTime: [
{ required: true, message: "请输入买家端Token有效时间", trigger: "blur" }
],
managerTokenExpireTime: [
{ required: true, message: "请输入管理端Token有效时间", trigger: "blur" }
],
});
function submit(name) {
handleSubmit(proxy, name)
.then(() => {
setupSetting();
})
.catch(() => {});
}
function setupSetting() {
setSetting(props.type, formValidate.value).then((res) => {
if (res.success) {
ElMessage.success("保存成功!");
} else {
ElMessage.error(res.message || "保存失败!");
}
});
}
function init() {
if (props.res) {
try {
const parsed = typeof props.res === "string" ? JSON.parse(props.res) : props.res;
formValidate.value = {
sensitiveLevel: Number(parsed.sensitiveLevel ?? 1),
isDemoSite: Boolean(parsed.isDemoSite ?? false),
buyerTokenExpireTime: Number(parsed.buyerTokenExpireTime ?? 259200),
managerTokenExpireTime: Number(parsed.managerTokenExpireTime ?? 86400),
};
} catch (e) {
console.error("解析安全设置失败", e);
}
}
}
init();
</script>
<style lang="scss" scoped>
@import "./style.scss";
:deep(.el-form-item__label) {
white-space: nowrap !important;
}
:deep(.el-form-item__content) {
display: flex;
align-items: center;
flex-wrap: nowrap;
white-space: nowrap;
}
.el-radio-group {
display: inline-flex;
flex-direction: row;
align-items: center;
white-space: nowrap;
}
.desc {
margin-left: 12px;
color: #999;
font-size: 12px;
white-space: nowrap;
}
</style>

View File

@@ -0,0 +1,128 @@
<template>
<div class="layout">
<el-form ref="formValidateRef" label-width="160px" label-position="right" :model="formValidate" :rules="ruleValidate">
<el-form-item label="有效时间(秒)" prop="effectiveTime">
<el-input-number v-model="formValidate.effectiveTime" :min="30" :max="3600" />
<span class="desc">包含滑块生成及校验结果缓存的有效时间(秒)</span>
</el-form-item>
<el-form-item label="水印文字" prop="watermark">
<el-input class="label-appkey" v-model="formValidate.watermark" placeholder="例如: LILI-SHOP" />
<span class="desc">滑块背景图水印文本</span>
</el-form-item>
<el-form-item label="干扰块数量" prop="interfereNum">
<el-input-number v-model="formValidate.interfereNum" :min="0" :max="2" />
<span class="desc">滑块干扰块数量,最大为 2</span>
</el-form-item>
<el-form-item label="容错像素" prop="faultTolerant">
<el-input-number v-model="formValidate.faultTolerant" :min="0" :max="20" />
<span class="desc">滑块比对横坐标容错像素范围</span>
</el-form-item>
<el-form-item label="登录免验证码" prop="loginBypassEnabled">
<el-switch v-model="formValidate.loginBypassEnabled" active-text="开启" inactive-text="关闭" />
<span class="desc">开启后登录流程跳过滑块校验,仅建议本地或测试验收环境开启</span>
</el-form-item>
<div class="label-btns">
<el-button type="primary" @click="submit('formValidateRef')">保存</el-button>
</div>
</el-form>
</div>
</template>
<script setup>
import { getCurrentInstance, reactive, ref } from "vue";
import { ElMessage } from "element-plus";
import { setSetting } from "@/api/index";
import { handleSubmit } from "./validate";
const props = defineProps(["res", "type"]);
const { proxy } = getCurrentInstance();
const formValidateRef = ref(null);
const formValidate = ref({
effectiveTime: 300,
watermark: "LILI-SHOP",
interfereNum: 0,
faultTolerant: 3,
loginBypassEnabled: false,
});
const ruleValidate = reactive({
effectiveTime: [
{ required: true, message: "请输入有效时间", trigger: "blur" }
],
watermark: [
{ required: true, message: "请输入水印文字", trigger: "blur" }
],
interfereNum: [
{ required: true, message: "请输入干扰块数量", trigger: "blur" }
],
faultTolerant: [
{ required: true, message: "请输入容错像素", trigger: "blur" }
],
});
function submit(name) {
handleSubmit(proxy, name)
.then(() => {
setupSetting();
})
.catch(() => {});
}
function setupSetting() {
setSetting(props.type, formValidate.value).then((res) => {
if (res.success) {
ElMessage.success("保存成功!");
} else {
ElMessage.error(res.message || "保存失败!");
}
});
}
function init() {
if (props.res) {
try {
const parsed = typeof props.res === "string" ? JSON.parse(props.res) : props.res;
formValidate.value = {
effectiveTime: Number(parsed.effectiveTime ?? 300),
watermark: String(parsed.watermark ?? "LILI-SHOP"),
interfereNum: Number(parsed.interfereNum ?? 0),
faultTolerant: Number(parsed.faultTolerant ?? 3),
loginBypassEnabled: Boolean(parsed.loginBypassEnabled ?? false),
};
} catch (e) {
console.error("解析验证码设置失败", e);
}
}
}
init();
</script>
<style lang="scss" scoped>
@import "./style.scss";
:deep(.el-form-item__label) {
white-space: nowrap !important;
}
:deep(.el-form-item__content) {
display: flex;
align-items: center;
flex-wrap: nowrap;
white-space: nowrap;
padding-bottom: 5px;
}
.desc {
margin-left: 10px;
color: #999;
font-size: 12px;
white-space: nowrap;
}
</style>

View File

@@ -75,6 +75,14 @@ const setting = ref([
type: "LIVE_SETTING",
name: "直播设置",
},
{
type: "VERIFICATION_SETTING",
name: "验证码设置",
},
{
type: "SECURITY_SETTING",
name: "安全设置",
},
]);
const authLogin = ref([
// 登录设置

View File

@@ -9,6 +9,8 @@ import SMS_SETTING from "./setting/SMS_SETTING";
import IM_SETTING from "./setting/IM_SETTING";
import LIVE_SETTING from "./setting/LIVE_SETTING";
import WITHDRAWAL_SETTING from "./setting/WITHDRAWAL_SETTING";
import VERIFICATION_SETTING from "./setting/VERIFICATION_SETTING";
import SECURITY_SETTING from "./setting/SECURITY_SETTING";
import ALIPAY_PAYMENT from "./pay/ALIPAY_PAYMENT";
import WECHAT_PAYMENT from "./pay/WECHAT_PAYMENT";
import PAYMENT_SUPPORT from "./pay/PAYMENT_SUPPORT";
@@ -27,6 +29,8 @@ export default {
IM_SETTING: markRaw(IM_SETTING),
LIVE_SETTING: markRaw(LIVE_SETTING),
WITHDRAWAL_SETTING: markRaw(WITHDRAWAL_SETTING),
VERIFICATION_SETTING: markRaw(VERIFICATION_SETTING),
SECURITY_SETTING: markRaw(SECURITY_SETTING),
PAYMENT_SUPPORT: markRaw(PAYMENT_SUPPORT),
WECHAT_PAYMENT: markRaw(WECHAT_PAYMENT),
ALIPAY_PAYMENT: markRaw(ALIPAY_PAYMENT),