chore:新增商品分类模板5

This commit is contained in:
2026-09-18 13:53:47 +08:00
parent 1b1704f7c6
commit a3d2ee562e
4 changed files with 322 additions and 31 deletions

View File

@@ -243,7 +243,7 @@ const localDevConfig = ({
// })['local-11']; // 选择要使用的环境配置
// })['2811']; // 本地
//})['5g-317']; // 电磁兼容
})['5g-312']; //
})['5g-351']; //
// })['local-2']; // 本地
// })['local-11']; // 本地
// })['1-test']; // test

View File

@@ -98,7 +98,9 @@
</block>
</block>
<!-- 模板1: 后台配置二级分类 + 有子分类时, 在一级分类标题下以双列网格显示所有商品(图 c2df0f3d29 样式) -->
<!-- 模板1: 后台配置二级分类 + 有子分类时, 在一级分类标题下以双列网格显示所有商品(图 c2df0f3d29 样式)
模板5 不走这里: 它渲染"一级分类标题 + 二级分类网格"(与 Branch_5cfee417 一致),
即下方 categoryLevel == 2 / 3 的分支 -->
<view v-else-if="value.template == 1">
<view :class="['category-title', 'category-title-with-lines']">{{ category.category_name }}</view>
<view class="template1-goods-wrap">
@@ -170,7 +172,7 @@
</block>
<!-- 模板1: 无子分类时(外层块不进入), 在一级分类标题下以双列网格显示所有商品(图 c2df0f3d29 样式) -->
<view v-else-if="value.template == 1 && type == 'category'">
<view v-else-if="(value.template == 1 || value.template == 5) && type == 'category'">
<view :class="['category-title', 'category-title-with-lines']">{{ category.category_name }}</view>
<view class="template1-goods-wrap">
<view class="goods-list show" :class="{ 'double-column': !isList, 'single-column': isList }" v-if="template1GoodsList.length">
@@ -617,6 +619,7 @@ export default {
* 三级分类 (value.level == 3) → true → 按二级分类分别显示各自商品
*/
isGroupedDisplay() {
// 仅模板1 走"按二级分类分组显示商品"; 模板5 渲染分类网格, 见下方 categoryLevel 分支
return this.value.template == 1 && this.value.level == 3;
},
/**
@@ -642,7 +645,7 @@ export default {
* 若 type 不跟随更新, 模板1 会误走 goods 分支, 导致"资讯类商品=否"的商品不显示立即咨询按钮
*/
type() {
return this.value.template == 1 || this.value.template == 99 ? 'category' : 'goods';
return this.value.template == 1 || this.value.template == 5 || this.value.template == 99 ? 'category' : 'goods';
},
},
created() {
@@ -664,7 +667,11 @@ export default {
// 模板1:根据后台"展示分类"配置 + 数据层级决定加载方式
// 三级分类模式 (isGroupedDisplay) + 有二级分类时,按二级分类分别加载各自商品
// 其他情况(二级分类模式 / 无二级分类),直接加载当前一级分类下的所有商品
if (this.value.template == 1 && this.category && this.category.category_id) {
// 模板5 渲染的是"分类网格"(与 Branch_5cfee417 一致), 有子分类时是静态网格, 无需加载商品;
// 只有一级分类下没有子分类时, 才回退到商品网格, 此时才需要加载商品
const template5NeedGoods = this.value.template == 5 &&
!(this.category && this.category.child_list && this.category.child_list.length);
if ((this.value.template == 1 || template5NeedGoods) && this.category && this.category.category_id) {
if (this.isGroupedDisplay && this.category.child_list && this.category.child_list.length) {
this.category.child_list.forEach(child => {
this.getTemplate1ChildGoods(child.category_id);
@@ -739,6 +746,8 @@ export default {
}
// 模板1:切换一级分类时重新加载该分类下的商品
// 三级分类模式 + 有二级分类时,按二级分类分别重新加载,否则直接加载当前分类
// 注意: 模板5 是"全部分类纵向平铺", 商品由 created 首次加载 + 父级链式懒加载提供,
// 滚动时 select 会随 listenScroll 变化, 若此处也重载会导致滚动中反复清空/重复请求, 故排除 5
if (this.value.template == 1 && this.index == this.select && this.category && this.category.category_id) {
this.template1GoodsList = [];
this.template1ChildGoodsList = {};
@@ -1489,42 +1498,42 @@ export default {
}
}
// 分类网格(模板5 / legacy 商品分类): 白底卡片 + 3 列 图片 + 名称
// 与 Branch_5cfee417 的商品分类保持一致
.category-list {
display: grid !important;
grid-template-columns: repeat(3, 1fr) !important;
background: transparent;
display: flex;
flex-wrap: wrap;
background: #fff;
margin: 0 20rpx;
border-radius: 0;
padding: 0;
gap: 20rpx 0;
border-radius: 10rpx;
padding: 10rpx 0;
.category-item {
width: 100% !important;
width: 33.33%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0;
padding: 16rpx;
box-sizing: border-box;
.img-box {
width: 70% !important;
width: 80%;
height: 0;
padding-bottom: 70%;
padding-top: 80%;
margin: 0 auto;
overflow: hidden;
margin-bottom: 16rpx;
margin-bottom: 20rpx;
position: relative;
background: transparent !important;
border-radius: 0 !important;
background: transparent;
image {
position: absolute;
width: 100% !important;
height: 100% !important;
left: 0 !important;
top: 0 !important;
transform: none !important;
width: 100%;
height: 100%;
left: 0;
top: 0;
transform: none;
}
}
@@ -1539,7 +1548,6 @@ export default {
font-size: var(--diy-category-item-name-size, 24rpx);
}
// 模板1选中状态
&.selected {
.img-box {
border: 2rpx solid var(--base-color, #FF6B35);

View File

@@ -122,7 +122,7 @@
{ select: select == index },
{ 'border-bottom': value.template == 4 && select + 1 === index },
{ 'border-top': value.template == 4 && select - 1 === index },
{ 'first-category-item': index == 0 && (value.template == 99 || value.template == 1) }
{ 'first-category-item': index == 0 && (value.template == 99 || value.template == 1 || value.template == 5) }
]" @tap.stop="switchOneCategory(index)">
<view class="">{{ item.category_name }}</view>
</view>
@@ -152,11 +152,30 @@
<diy-category-item :category="item" :value="value" ref="categoryItem" :index="index"
:select="select" :oneCategorySelect="oneCategorySelect" :isList="isList"
@tologin="toLogin" @selectsku="selectSku($event, index)" @addCart="addCartPoint"
@saveSecondCategory="saveSecondCategoryName"></diy-category-item>
@saveSecondCategory="saveSecondCategoryName"> </diy-category-item>
</view>
</scroll-view>
</scroll-view>
<scroll-view scroll-y="true" class="content-wrap" v-else-if="value.template == 99 || loadType == 'all'"
<!-- 模板5: 左侧分类树 + 右侧纵向平铺"全部分类"(每个一级分类: 标题 + 商品网格)
与模板1 的核心区别: 模板1 一次只渲染选中的那个分类(display 切换),
模板5 把全部分类按顺序纵向平铺; 左侧点击分类用 scroll-into-view 定位,
滚动时由 listenScroll 反推并高亮左侧分类 -->
<scroll-view scroll-y="true" class="content-wrap" v-else-if="value.template == 5"
ref="contentWrap" :scroll-into-view="categoryId" :scroll-with-animation="true"
:refresher-enabled="true" refresher-default-style="none"
:refresher-triggered="triggered" @refresherrefresh="onRefresh" @refresherrestore="onRestore"
@scroll="listenScroll" @touchstart="touchStart">
<view class="child-category" v-for="(item, index) in categoryTree" :key="index"
:id="'category-' + index">
<diy-category-item :category="item" :value="value" ref="categoryItem" :index="index"
:select="select" :oneCategorySelect="oneCategorySelect" :isList="isList"
@tologin="toLogin" @selectsku="selectSku($event, index)" @addCart="addCartPoint"
@loadfinish="getHeightArea" @saveSecondCategory="saveSecondCategoryName"></diy-category-item>
</view>
<view class="end-tips" ref="endTips" :style="{ opacity: endTips }">{{ $lang('components-diy.diy-category.reachedBottom') }}</view>
</scroll-view>
<scroll-view scroll-y="true" class="content-wrap" v-else-if="value.template == 99 || loadType == 'all'"
ref="contentWrap" :scroll-into-view="categoryId" :scroll-with-animation="true"
@scroll="listenScroll" @touchstart="touchStart" :refresher-enabled="true"
refresher-default-style="none" :refresher-triggered="triggered" @refresherrefresh="onRefresh"
@@ -308,6 +327,7 @@ export default {
})
.exec();
if (this.value.template == 1) this.getHeightArea(-1);
if (this.value.template == 5) this.getHeightArea(-1);
}, 500);
},
watch: {
@@ -317,6 +337,9 @@ export default {
if (nval === false && oval === true) {
this.$nextTick(() => {
if (!this.heightArea.length) this.getHeightArea(-1);
// 分类渲染完成后重新对齐到当前选中的一级分类顶部,
// 避免首屏第一个分类的标题被顶出可视区
if (this.categoryTree && this.categoryTree.length) this.alignToCategory(this.select);
});
}
}
@@ -536,8 +559,30 @@ export default {
switchOneCategory(index) {
if (index >= this.categoryTree.length) return;
this.select = index;
this.categoryId = 'category-' + index;
this.scrollLock = true;
this.alignToCategory(index);
},
/**
* 把右侧滚动区定位到指定一级分类的顶部
*
* uni-app 只在 scroll-into-view 的值"发生变化"时才重新滚动,
* 而 categoryId 的初值就是 'category-0', 且只有点击左侧分类才会被改写;
* 用户在右侧手动滚动后 categoryId 仍是 'category-0', 此时再点第一个分类(index 0)
* 值时没有变化, 右侧不会回滚到该分类, 该分类的标题就被顶出可视区
* (看起来像"标题丢了 / 卡片被切了一半"), 而点其它分类因为值变了所以都正常。
* 这里先置空、下一帧再设置目标 id, 保证每次点击都能重新定位到分类顶部。
* @param {Number} index 一级分类下标
*/
alignToCategory(index) {
const target = 'category-' + index;
if (this.categoryId === target) {
this.categoryId = '';
this.$nextTick(() => {
this.categoryId = target;
});
} else {
this.categoryId = target;
}
},
saveSecondCategoryName(name) {
this.lastSecondCategoryName = name;
@@ -559,7 +604,10 @@ export default {
break;
}
}
if (this.value.template != 1 && this.value.loadType == 'all' && this.heightArea[this.select][1] -
// 模板5 渲染的是分类网格(静态), 不需要懒加载商品; 这里只保留 loadType=all 的懒加载,
// 并补上 heightArea[this.select] 越界保护
if (this.value.template != 1 && this.value.loadType == 'all' &&
this.heightArea[this.select] && this.heightArea[this.select][1] -
scrollTop - contentWrapHeight < 300) {
if (Array.isArray(this.$refs.categoryItem) && this.$refs.categoryItem[this.select]) this.$refs.categoryItem[this.select].getGoodsList();
}
@@ -1674,6 +1722,134 @@ export default {
}
}
// ===================== 模板5 商品分类样式(新增: 基于模板1双栏布局) =====================
.category-template-5 {
.search-box {
padding: 20rpx 24rpx;
background: #fff;
.search-content {
background-color: #f5f5f5;
border-radius: 40rpx;
input {
background: #f5f5f5;
border-radius: 40rpx;
padding: 0 30rpx;
font-size: 26rpx;
color: #333;
}
.iconfont {
right: 16rpx;
color: #89899a;
font-size: 32rpx;
}
}
}
.content-box {
// 模板5 + 三级分类: 标题行所在整片右半边去掉白色, 与模板1一致
background: transparent;
.tree-wrap {
width: 170rpx;
background-color: #fff;
}
.right-flex-wrap {
background: #f5f5f5;
.content-wrap {
background: transparent;
}
// 模板5 纵向平铺: 每个一级分类独占一行(与模板99"全部分类"结构一致)
.child-category {
width: 100%;
box-sizing: border-box;
}
}
}
// 穿透到子组件 diy-category-item: 调整 banner 横向内边距 + 分类网格容器为透明
::v-deep .category-adv {
padding: 0 20rpx;
margin-top: 0;
}
// 模板5: .category-list 直接沿用子组件的 legacy 白底卡片样式(不再覆盖为透明)
// 穿透到子组件 diy-category-item: 模板5标题行定高, 与左侧第一个一级分类等高对齐
::v-deep .category-title-with-lines {
height: 100rpx;
box-sizing: border-box;
padding: 30rpx 0 20rpx;
}
// 模板5: 左侧第一个一级分类定高, 与右侧标题行垂直对齐
.tree-wrap .category-item.first-category-item {
height: 100rpx;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
white-space: nowrap;
overflow: hidden;
view {
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
// 模板5: 分类网格的 .img-box / .name 直接沿用子组件 legacy 尺寸(80% + 名称),
// 不再做 70%/透明 覆盖, 以与 Branch_5cfee417 的商品分类保持一致
// 左侧分类栏样式
.tree-wrap .category-item {
padding: 28rpx 16rpx;
background-color: var(--diy-category-tree-normal-bg-color, #f5f5f5);
border-radius: 0;
view {
color: var(--diy-category-tree-normal-text-color, #222222);
font-size: 26rpx;
line-height: 1.3;
}
&.border-top,
&.border-bottom {
border-radius: 0;
}
&.select {
background: var(--diy-category-tree-select-bg-color, #fff);
view {
color: var(--diy-category-tree-select-text-color, #333);
font-weight: bold;
font-size: 28rpx;
}
&::before {
content: ' ';
width: 6rpx;
height: 32rpx;
background: var(--base-color, #FF6B35);
display: block;
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
border-radius: 0 6rpx 6rpx 0;
}
}
}
}
// ===================== 模板99 商品分类样式 =====================
.category-template-99 {
.search-box {

View File

@@ -4,12 +4,13 @@
slide=滑动时显示(默认) / always=始终显示 / never=永不显示
小程序端: show-scrollbar 仅在 enhanced=true 时生效, 所以两者同步绑定 -->
<scroll-view :class="['image-nav', value.showStyle == 'fixed' ? 'fixed-layout' : value.showStyle, scrollBarClass]"
:style="imageSpacingStyle"
:scroll-x="value.showStyle == 'singleSlide'"
:show-scrollbar="scrollBarMode != 'never'"
:enhanced="scrollBarMode != 'never'">
<view class="uni-scroll-view-content">
<view v-for="(item, index) in value.list" :key="index" :class="['image-nav-item', value.mode]"
style="margin-right: 28rpx;">
:style="imageItemStyle">
<!-- 图片部分 -->
<view v-if="value.mode != 'text'" class="image-img" :style="{
'font-size': (value.imageSize * 2 + 'rpx') + ';',
@@ -199,6 +200,112 @@ export default {
const cls = ['scrollbar-' + this.scrollBarMode];
if (this.scrollBarMode == 'slide' && this.slideScrolling) cls.push('is-scrolling');
return cls;
},
// ========== PR-FRONTEND-DIY-IMAGE-NAV-SPACING (2026-09-18): 图片自定义间距 ==========
// 后台"滑动图片"组件新增 3 项间距配置:
// 1) 图片横向间距: 相邻图片项之间的横向间隔
// 2) 图片上间距 : 图片区域内侧的上内边距
// 3) 图片下间距 : 图片区域内侧的下内边距
// 单位: 按项目惯例(与商品 goodsMarginNum 一致)换算 rpx = 后台数值 × 2
// 字段名兼容: 后台各端命名未统一, 这里对常见命名做归一化匹配(忽略大小写/下划线/中划线),
// 并向下探测 value.imageSpacing / value.margin / value.padding 等嵌套容器,
// 保证后端用哪种命名都能生效。
// 兜底: 字段缺失/非法时返回 null, 由 imageSpacingStyle / imageItemStyle 回退历史视觉
// (横向 28rpx, 上/下沿用 CSS 的 16rpx), 老装修数据不受影响。
imageSpacingConfig() {
const v = this.value || {};
const RATIO = 2; // 后台数值 → rpx 换算比例
const norm = k => String(k).toLowerCase().replace(/[\s_-]/g, '');
// 非数字(如 'diy'/枚举)或空值 → null, 交给兜底
const toRpx = val => {
if (val === undefined || val === null || val === '') return null;
const n = Number(val);
if (isNaN(n)) return null;
return Math.max(0, n) * RATIO;
};
// 查找源: value 自身 + 常见嵌套容器(顶层优先)
const sources = [v];
['imageSpacing', 'imageSpace', 'spacing', 'space', 'margin', 'padding', 'style', 'config', 'setting']
.forEach(k => {
if (v[k] && typeof v[k] === 'object') sources.push(v[k]);
});
// 三级查找: ① 全源精确名 → ② 全源模糊包含名 → ③ 嵌套容器内的裸键
// (如 imageSpacing: { horizontal: 3, top: 0, bottom: 0 })
const pick = (exact, fuzzy, nested) => {
for (let s = 0; s < sources.length; s++) {
const src = sources[s];
for (const key in src) {
if (exact.indexOf(norm(key)) != -1) {
const rpx = toRpx(src[key]);
if (rpx !== null) return rpx;
}
}
}
for (let s = 0; s < sources.length; s++) {
const src = sources[s];
for (const key in src) {
const k = norm(key);
if (fuzzy.some(f => k.indexOf(f) != -1)) {
const rpx = toRpx(src[key]);
if (rpx !== null) return rpx;
}
}
}
// 裸键(top/bottom/both...)只在嵌套容器内解释, 避免误取顶层同名字段
for (let s = 1; s < sources.length; s++) {
const src = sources[s];
for (const key in src) {
if (nested.indexOf(norm(key)) != -1) {
const rpx = toRpx(src[key]);
if (rpx !== null) return rpx;
}
}
}
return null;
};
return {
// 图片横向间距
horizontal: pick(
['imagemargin', 'imagemarginnum', 'imagespace', 'imagespacenum', 'imagehorizontalspace',
'horizontalspace', 'horizontalspacenum', 'imagegap', 'space', 'spacenum', 'marginnum', 'gap'],
['horizontalspace', 'imagespace', 'imagemargin', 'spacenum', 'marginnum', 'imagegap', 'gap'],
['horizontal', 'horizontalnum', 'lr', 'leftright', 'both', 'bothnum', 'num', 'margin', 'space', 'gap']
),
// 图片上间距
paddingTop: pick(
['imagepaddingtop', 'imagespacingtop', 'imagespacetop', 'imagetopspace', 'paddingtop',
'spacingtop', 'topspace', 'margintop'],
['paddingtop', 'spacingtop', 'topspace', 'margintop'],
['top', 'topnum', 'paddingtop', 'margintop', 'spacetop']
),
// 图片下间距
paddingBottom: pick(
['imagepaddingbottom', 'imagespacingbottom', 'imagespacebottom', 'imagebottomspace', 'paddingbottom',
'spacingbottom', 'bottomspace', 'marginbottom'],
['paddingbottom', 'spacingbottom', 'bottomspace', 'marginbottom'],
['bottom', 'bottomnum', 'paddingbottom', 'marginbottom', 'spacebottom']
)
};
},
// 图片区域上/下内边距(作用在滚动容器上); 未配置时不输出内联样式, 沿用 CSS padding
imageSpacingStyle() {
const cfg = this.imageSpacingConfig;
let style = '';
if (cfg.paddingTop !== null) style += 'padding-top:' + cfg.paddingTop + 'rpx;';
if (cfg.paddingBottom !== null) style += 'padding-bottom:' + cfg.paddingBottom + 'rpx;';
return style;
},
// 图片项之间的横向间距; 未配置时回退原写死的 28rpx
imageItemStyle() {
const horizontal = this.imageSpacingConfig.horizontal;
return 'margin-right:' + (horizontal === null ? 28 : horizontal) + 'rpx;';
}
},
methods: {