chore: 同步搜索框与滑动图片相关5个文件到 dev/1.0

This commit is contained in:
2026-09-19 16:03:13 +08:00
parent ca772295ba
commit 34c712db32
5 changed files with 677 additions and 90 deletions

View File

@@ -68,7 +68,7 @@ const localDevConfig = ({
uniacid: 11,
domain: 'http://localhost:8050/',
},
'100': {
'100': {// 口译教学
uniacid: 100,
domain: 'https://5g.aigc-quickapp.com/',
},
@@ -76,15 +76,15 @@ const localDevConfig = ({
uniacid: 1,
domain: 'https://5g.aigc-quickapp.com/',
},
'115': {
'115': {// 低压断路器
uniacid: 115,
domain: 'https://5g.aigc-quickapp.com/',
},
'5g-11': {
'5g-11': {// 铝箔袋包装
uniacid: 11,
domain: 'https://5g.aigc-quickapp.com/',
},
'5g-206': {
'5g-206': {// 气体探测
uniacid: 206,
domain: 'https://5g.aigc-quickapp.com/',
},
@@ -92,19 +92,19 @@ const localDevConfig = ({
uniacid: 165,
domain: 'https://5g.aigc-quickapp.com/',
},
'47': { // 修改弹窗
'47': { // 免疫细胞干细胞外泌体 ,修改弹窗
uniacid: 47,
domain: 'https://5g.aigc-quickapp.com/',
},
'145': { // 表单链接转义问题
'145': { //昆山昆创净化科技有限公司 , 表单链接转义问题
uniacid: 145,
domain: 'https://5g.aigc-quickapp.com/',
},
'5g-213': { // 单图组点击链接无法跳转
'5g-213': { // 碱性康养水天然泡茶水 , 单图组点击链接无法跳转
uniacid: 213,
domain: 'https://5g.aigc-quickapp.com/',
},
'5g-4': { // 公告点击链接无法跳转
'5g-4': { // 高端硅钢制造装备 , 公告点击链接无法跳转
uniacid: 4,
domain: 'https://5g.aigc-quickapp.com/',
},
@@ -116,11 +116,11 @@ const localDevConfig = ({
uniacid: 50,
domain: 'https://5g.aigc-quickapp.com/',
},
'5g-237': { // 商品分类,没有立即咨询文字
'5g-237': { //高活性氧化镁 商品分类,没有立即咨询文字
uniacid: 237,
domain: 'https://5g.aigc-quickapp.com/',
},
'2847': { //
'2847': { // 家居装饰品
uniacid: 2847,
domain: 'https://xcx.aigc-quickapp.com/',
},
@@ -128,7 +128,7 @@ const localDevConfig = ({
uniacid: 2482,
domain: 'https://xcx6.aigc-quickapp.com/',
},
'2485': { // 验证电子表单前后端数据一致性
'2485': { // 智能物流仓储 , 验证电子表单前后端数据一致性
uniacid: 2485,
domain: 'https://xcx6.aigc-quickapp.com/',
},
@@ -152,11 +152,11 @@ const localDevConfig = ({
uniacid: 12,
domain: 'https://test.aigc-quickapp.com',
},
'248': { // 大宗商品贸易,支持自定义字体大小、颜色
'248': { // 工业称重系统垃圾吊电子秤,支持自定义字体大小、颜色
uniacid: 248,
domain: 'https://5g.aigc-quickapp.com/',
},
'228': { // 商品分类,没有立即咨询文字
'228': { // 反渗透阻垢剂RO装置 商品分类,没有立即咨询文字
uniacid: 228,
domain: 'https://5g.aigc-quickapp.com/',
},
@@ -164,7 +164,7 @@ const localDevConfig = ({
uniacid: 2797,
domain: 'https://xcx.aigc-quickapp.com/',
},
'5g-95': { // 商品分类,没有立即咨询文字
'5g-95': { // 加热器 ,商品分类,没有立即咨询文字
uniacid: 95,
domain: 'https://5g.aigc-quickapp.com/',
},
@@ -185,7 +185,7 @@ const localDevConfig = ({
domain: 'http://192.168.8.118:8050/',
// domain: 'http://localhost:8050/',
},
'5g-59': { // 合成孔径雷达,一级分类背景字体颜色
'5g-59': { // 脱模剂粘合剂乐瑞固Lorechem
uniacid: 59,
domain: 'https://5g.aigc-quickapp.com/',
},
@@ -210,6 +210,18 @@ const localDevConfig = ({
uniacid: 2505,
domain: 'https://xcx.aigc-quickapp.com/',
},
'3007': { // 钎焊材料
uniacid: 3007,
domain: 'https://xcx.aigc-quickapp.com/',
},
'5g-317': { // 电磁兼容,模板显示不全商品
uniacid: 317,
domain: 'https://5g.aigc-quickapp.com/',
},
'5g-312': { // 天然植物功效活性物
uniacid: 312,
domain: 'https://5g.aigc-quickapp.com/',
},
// })['5g-237']; // 商品分类,没有立即咨询文字
// })['5g-97']; // 大宗商品贸易,支持自定义字体大小、颜色
// })['5g-50']; // 燃气安全产品机器人,购物车无反应
@@ -222,10 +234,16 @@ const localDevConfig = ({
// })['2881']; // 鸡肉调理品, 优惠卷显示不全,及逻辑错误
// })['2724']; // 生物菌肥
// })['2811']; // 选择要使用的环境配置
// })['5g-351']; //洗车机
// })['5g-204']; //热固性塑料
// })['5g-59']; //合成孔径雷达
// })['2709']; //中央厨房
// })['3007']; //钎焊材料
// })['2505']; //煤矿钻机
// })['local-11']; // 选择要使用的环境配置
// })['2811']; // 本地
})['2821']; // 本地
//})['5g-317']; // 电磁兼容
})['5g-351']; //
// })['local-2']; // 本地
// })['local-11']; // 本地
// })['1-test']; // test

View File

@@ -98,7 +98,8 @@
</block>
</block>
<!-- 模板1: 后台配置二级分类 + 有子分类时, 在一级分类标题下以双列网格显示所有商品(图 c2df0f3d29 样式) -->
<!-- 模板1: 后台配置二级分类 + 有子分类时, 在一级分类标题下以双列网格显示所有商品(图 c2df0f3d29 样式)
模板5 不走这里: 它有专属分支 value.template == 5(后台"展示分类"决定渲染商品网格或二级分类网格) -->
<view v-else-if="value.template == 1">
<view :class="['category-title', 'category-title-with-lines']">{{ category.category_name }}</view>
<view class="template1-goods-wrap">
@@ -139,8 +140,51 @@
</view>
</view>
<!-- 非模板1, 2级分类: 显示二级分类网格,点击跳转到列表页 -->
<block v-else-if="categoryLevel == 2">
<!-- 模板5 专属: 后台"展示分类"决定右侧渲染内容(对应图2/图3)
二级分类(value.level == 2) → 一级分类标题 + 商品网格(图2)
三级分类(value.level == 3) → 一级分类标题 + 二级分类网格(图3) -->
<block v-else-if="value.template == 5">
<block v-if="value.level == 2">
<view :class="['category-title', '']">{{ 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">
<view class="goods-item" v-for="(item, index) in template1GoodsList" :key="index">
<view class="goods-img" @tap.stop="toDetail(item)">
<image :src="goodsImg(item.goods_image)" mode="widthFix" @error="template1ImgError(index)" show-menu-by-longpress />
<view class="color-base-bg goods-tag" v-if="item.label_name">{{ item.label_name }}</view>
</view>
<view class="info-wrap">
<view class="name-wrap" @tap.stop="toDetail(item)">
<view class="goods-name">{{ isEnEnv ? item.en_goods_name : item.goods_name }}</view>
</view>
<view class="price-wrap">
<view class="discount-price">
<block v-if="item.isinformation == 0">
<text class="unit price-style small">{{ $lang('common.currencySymbol') }}</text>
<text class="price price-style large">{{ parseFloat(showPrice(item)).toFixed(2).split('.')[0] }}</text>
<text class="unit price-style small">.{{ parseFloat(showPrice(item)).toFixed(2).split('.')[1] }}</text>
</block>
</view>
<view class="delete-price font-size-activity-tag color-tip price-font"
v-if="item.isinformation == 0 && showMarketPrice(item)">
<text class="unit">{{ $lang('common.currencySymbol') }}</text>
<text>{{ showMarketPrice(item) }}</text>
</view>
</view>
<view class="footer-wrap footer-wrap-template1">
<view class="detail-btn" @tap.stop="toDetail(item)">{{ $lang('components-diy.diy-category-item.consultNow') }}</view>
</view>
</view>
</view>
</view>
<view class="category-goods-empty" v-else-if="!template1Loading">
<image :src="$util.img('public/uniapp/category/empty.png')" mode="widthFix" show-menu-by-longpress></image>
<view class="tips">暂无商品</view>
</view>
</view>
</block>
<block v-else>
<!-- 三级分类(及更深层级): 一级分类标题 + 二级分类网格(图3) -->
<view :class="['category-title', '']">{{ category.category_name }}</view>
<view class="category-list">
<view class="category-item" v-for="(one, oneIndex) in category.child_list" :key="oneIndex"
@@ -152,25 +196,11 @@
</view>
</view>
</block>
<block v-else-if="categoryLevel == 3">
<block v-for="(one, oneIndex) in category.child_list" :key="oneIndex">
<view :class="['category-title', '']">{{ one.category_name }}</view>
<view class="category-list">
<view class="category-item" v-for="(two, twoIndex) in one.child_list" :key="twoIndex"
@tap.stop="$util.redirectTo('/pages_goods/list', { category_id: two.category_id })">
<view class="img-box">
<image :src="categoryImg(two.image)" mode="aspectFit" @error="categoryImgError(3, oneIndex, twoIndex)" show-menu-by-longpress />
</view>
<view class="name">{{ two.category_name }}</view>
</view>
</view>
</block>
</block>
</block>
</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,13 +647,13 @@ export default {
* 三级分类 (value.level == 3) → true → 按二级分类分别显示各自商品
*/
isGroupedDisplay() {
// 仅模板1 走"按二级分类分组显示商品"; 模板5 的层级由 value.level 直接决定(见下方 value.template == 5 分支)
return this.value.template == 1 && this.value.level == 3;
},
/**
* 模板1三级分类: 渲染所有二级分类(包括无商品的空分类)
* 不再按"是否有商品"过滤, 保证一级分类下的二级分类全部显示,
* 避免 10 个二级分类只显示 8 个(2 个被当成空分类过滤掉)的问题
* 加载中(template1ChildLoading[id] === true)的二级分类暂不渲染, 避免闪烁
* 模板1三级分类: 只渲染"有商品"的二级分类
* 无商品的二级分类整块(标题 + 网格)都不渲染; 加载中的二级分类也暂不渲染(避免闪烁)
* 判定时机: 该二级分类加载完成(template1ChildLoading[id] === false)且商品列表非空
*/
filteredGroupedChildren() {
if (!this.isGroupedDisplay) return [];
@@ -631,8 +661,9 @@ export default {
return this.category.child_list.filter(one => {
// 还在加载中: 暂不渲染(避免闪烁)
if (this.template1ChildLoading[one.category_id]) return false;
// 加载完成(无论是否有商品)都渲染, 无商品时显示空状态
return true;
// 加载完成: 只有该二级分类下有商品时才显示, 无商品则整块隐藏
const list = this.template1ChildGoodsList[one.category_id];
return !!(list && list.length);
});
},
/**
@@ -642,7 +673,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 +695,14 @@ export default {
// 模板1:根据后台"展示分类"配置 + 数据层级决定加载方式
// 三级分类模式 (isGroupedDisplay) + 有二级分类时,按二级分类分别加载各自商品
// 其他情况(二级分类模式 / 无二级分类),直接加载当前一级分类下的所有商品
if (this.value.template == 1 && this.category && this.category.category_id) {
// 模板5 渲染规则:
// 二级分类(value.level == 2) → 商品网格(图2), 需要加载该一级分类下所有商品(合并其二级分类)
// 三级分类(value.level == 3) → 二级分类网格(图3), 静态网格无需加载商品
// 若该一级分类下没有子分类(兜底), 仍需要加载商品
const template5NeedGoods = this.value.template == 5 &&
(this.value.level == 2 ||
!(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 +777,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 +1529,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 +1579,6 @@ export default {
font-size: var(--diy-category-item-name-size, 24rpx);
}
// 模板1选中状态
&.selected {
.img-box {
border: 2rpx solid var(--base-color, #FF6B35);
@@ -2490,18 +2529,28 @@ export default {
.screen-category-wrap {
display: flex;
align-items: center;
position: relative;
.icon-unfold {
font-size: 24rpx;
position: absolute;
right: 15rpx;
top: 0;
z-index: 2;
width: 70rpx;
height: 70rpx;
line-height: 62rpx;
text-align: center;
font-size: 36rpx;
color: #999;
padding: 0 0 0 20rpx;
padding: 0;
}
}
.screen-category {
flex: 1;
width: 0;
padding: 0 0 20rpx 0;
padding: 0 70rpx 20rpx 0;
white-space: nowrap;
height: 60rpx;

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,44 @@
<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 + 二级分类(value.level==2): 单面板切换 - 右侧只显示"当前选中"的一级分类,
点左侧分类切换, 其它分类隐藏(display:none); 每个一级分类: 标题 + 商品网格 -->
<scroll-view scroll-y="true" class="content-wrap"
v-else-if="value.template == 5 && value.level == 2"
:refresher-enabled="true" refresher-default-style="none"
:refresher-triggered="triggered" @refresherrefresh="onRefresh" @refresherrestore="onRestore">
<view class="child-category-wrap" v-for="(item, index) in categoryTree" :key="index"
:id="'category-' + index" :style="{ display: select == index ? 'block' : 'none' }">
<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>
</view>
</scroll-view>
<!-- 模板5 + 三级分类(value.level!=2): 左侧分类树 + 右侧纵向平铺"全部一级分类",
每个一级分类: 标题 + 二级分类网格; 左侧点击分类用 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 +341,7 @@ export default {
})
.exec();
if (this.value.template == 1) this.getHeightArea(-1);
if (this.value.template == 5) this.getHeightArea(-1);
}, 500);
},
watch: {
@@ -317,6 +351,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 +573,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 +618,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 +1736,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

@@ -377,14 +377,41 @@ export default {
: index + 1;
if (newItem.componentName !== 'Search' || newItem.positionWay !== 'fixed') {
newItem.pageStyle = `background-color:${newItem.pageBgColor};`;
// PR-DIY-SEARCH-MARGIN-COLOR (2026-09-17): 搜索组件的上下左右边距区域颜色
// 与搜索框背景色(componentBgColor, 后台自定义)保持一致, 使边距和搜索框融为一体;
// 兼容旧数据: 未配置 componentBgColor 时回退 pageBgColor。其余组件仍用 pageBgColor。
const wrapBgColor = (newItem.componentName === 'Search' && newItem.componentBgColor)
? newItem.componentBgColor
: newItem.pageBgColor;
newItem.pageStyle = `background-color:${wrapBgColor};`;
if (newItem.margin) {
newItem.pageStyle += `padding-top:${newItem.margin.top * 2}rpx;`;
newItem.pageStyle += `padding-bottom:${newItem.margin.bottom * 2}rpx;`;
newItem.pageStyle += `padding-right:${newItem.margin.both * 2}rpx;`;
newItem.pageStyle += `padding-left:${newItem.margin.both * 2}rpx;`;
}
// 边距归一化: 兼容后台两种数据结构(否则边距不生效)
// ① 标准对象形态 margin = { top, bottom, both } —— 大多数组件
// ② 扁平字段形态 marginTop / marginBottom / marginBoth
// —— 如"滑动图片"(ImageNav), 其 margin 是空数组 [], 左右边距存在 marginBoth 里
// both / marginBoth 表示"左右边距"; 数值 → rpx 统一 × 2 (与其它组件一致)
const rawMargin = newItem.margin;
const objMargin = (rawMargin && typeof rawMargin === 'object' && !Array.isArray(rawMargin)) ? rawMargin : null;
const toMarginNum = (v) => { const n = Number(v); return isNaN(n) ? 0 : n; };
const pickMargin = (objKey, flatKey) => {
if (objMargin && objMargin[objKey] !== undefined && objMargin[objKey] !== null && objMargin[objKey] !== '') {
return toMarginNum(objMargin[objKey]);
}
return toMarginNum(newItem[flatKey]);
};
const marginTopNum = pickMargin('top', 'marginTop');
const marginBottomNum = pickMargin('bottom', 'marginBottom');
const marginBothNum = pickMargin('both', 'marginBoth');
// 左右: 若有独立的 left/right 字段则优先, 否则用 both(左右一致)
const marginLeftNum = (objMargin && objMargin.left !== undefined) ? toMarginNum(objMargin.left)
: (newItem.marginLeft !== undefined ? toMarginNum(newItem.marginLeft) : marginBothNum);
const marginRightNum = (objMargin && objMargin.right !== undefined) ? toMarginNum(objMargin.right)
: (newItem.marginRight !== undefined ? toMarginNum(newItem.marginRight) : marginBothNum);
newItem.pageStyle += `padding-top:${marginTopNum * 2}rpx;`;
newItem.pageStyle += `padding-bottom:${marginBottomNum * 2}rpx;`;
newItem.pageStyle += `padding-left:${marginLeftNum * 2}rpx;`;
newItem.pageStyle += `padding-right:${marginRightNum * 2}rpx;`;
}
return newItem;

View File

@@ -1,10 +1,16 @@
<template>
<view :style="componentStyle" data-component-name="diy-image-nav">
<scroll-view :class="['image-nav', value.showStyle == 'fixed' ? 'fixed-layout' : value.showStyle]"
:scroll-x="value.showStyle == 'singleSlide'">
<!-- PR-FRONTEND-DIY-SCROLLBAR (2026-09-18): 水平滚动条显隐三态(后台配置)
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') + ';',
@@ -60,7 +66,10 @@ export default {
slideDidDrag: false, // 5px 阈值后置 true,用于判断 mouseup 后是否拦截 click
slideStartX: 0,
slideStartScrollLeft: 0,
_suppressNextClick: false // 拖拽结束后拦截一次 click,防止误触发跳转
_suppressNextClick: false, // 拖拽结束后拦截一次 click,防止误触发跳转
// PR-FRONTEND-DIY-SCROLLBAR (2026-09-18): "滑动时显示"模式专用
// 滚动中为 true → 给 scroll-view 加 is-scrolling 类展示滚动条, 停止 800ms 后隐藏
slideScrolling: false
}
},
created() {
@@ -75,12 +84,14 @@ export default {
var self = this;
this.$nextTick(function() {
self.bindSlideMouseEvents();
self.bindSlideScrollbar();
});
// #endif
},
beforeDestroy() {
// #ifdef H5
this.unbindSlideMouseEvents();
this.unbindSlideScrollbar();
// #endif
},
watch: {
@@ -89,9 +100,47 @@ export default {
}
},
computed: {
// PR-FRONTEND-DIY-IMAGE-NAV-SPACING (2026-09-18): 组件背景色
// 后台"组件背景"面板下发的字段是 componentBgColor(与图片轮播/搜索等组件一致);
// 兼容别名/数组形态/嵌套容器(避免后台换名或存成数组时前端读不到而完全不上色),
// 最后兜底组件级 pageBgColor(部分组件后台把组件背景存在这里)。
// 空值/非法值(图片URL等)返回空串, 此时不输出 background-color, 保持历史透明视觉。
componentBgColor() {
const v = this.value || {};
const norm = k => String(k).toLowerCase().replace(/[\s_-]/g, '');
const asColor = val => {
if (Array.isArray(val)) val = val[0];
if (typeof val !== 'string') return '';
const s = val.trim();
if (!s) return '';
// 排除图片/URL 等非颜色值
if (/^(url\(|https?:|data:|upload\/|addon\/|\/)/i.test(s)) return '';
return s;
};
const sources = [v];
['style', 'config', 'setting', 'data'].forEach(k => {
if (v[k] && typeof v[k] === 'object' && !Array.isArray(v[k])) sources.push(v[k]);
});
const candidates = [
'componentbgcolor', 'componentbackgroundcolor', 'componentcolor', 'componentbg',
'bgcolor', 'backgroundcolor', 'background',
'pagebgcolor'
];
for (let c = 0; c < candidates.length; c++) {
for (let s = 0; s < sources.length; s++) {
for (const key in sources[s]) {
if (norm(key) === candidates[c]) {
const color = asColor(sources[s][key]);
if (color) return color;
}
}
}
}
return '';
},
componentStyle() {
let style = '';
style += 'background-color:' + this.value.componentBgColor + ';';
if (this.componentBgColor) style += 'background-color:' + this.componentBgColor + ';';
if (this.value.componentAngle == 'round') {
style += 'border-top-left-radius:' + (2 * this.value.topAroundRadius) + 'rpx;';
@@ -123,9 +172,219 @@ export default {
isIndicatorDots() {
return this.value.carousel.type != 'hide' &&
1 != Math.ceil(this.value.list.length / (this.value.pageCount * this.value.rowCount));
},
// ========== PR-FRONTEND-DIY-SCROLLBAR (2026-09-18): 水平滚动条显隐三态 ==========
// 后台"水平滚动条"配置: 滑动时显示 / 始终显示 / 永不显示 → 归一化为 slide / always / never
// 字段名兼容: scrollBar / scrollbar / scrollBarType / horizontalScrollBar / horizontalScrollbar
// (忽略大小写/下划线/中划线差异, 并向下探测 value.scrollBar / value.style 等嵌套容器)
// 值兼容: 英文枚举(slide/auto/always/never/none...) / 中文枚举(滑动时显示/始终显示/永不显示)
// / 数字(0=滑动时显示 1=始终显示 2=永不显示) / 布尔(true=始终显示 false=永不显示)
// 兜底: 未下发或无法识别 → 'slide' (滑动时显示, 与历史行为一致)
scrollBarMode() {
const v = this.value || {};
const norm = k => String(k).toLowerCase().replace(/[\s_-]/g, '');
// 从配置对象中取"滚动条"字段的原始值
const pickRaw = obj => {
if (!obj || typeof obj != 'object') return undefined;
for (const key in obj) {
const k = norm(key);
if (k.indexOf('scrollbar') != -1 || k.indexOf('scrolltype') != -1) {
const val = obj[key];
if (val && typeof val === 'object') {
// 子配置对象形态: { scrollBar: { type: 'always' } }
return val.type !== undefined ? val.type : (val.value !== undefined ? val.value : val.mode);
}
if (val !== undefined && val !== null && val !== '') return val;
}
}
return undefined;
};
let raw = pickRaw(v);
// 嵌套容器兜底
if (raw === undefined) {
const containers = ['scrollBar', 'scrollbar', 'style', 'config', 'setting', 'data'];
for (let i = 0; i < containers.length; i++) {
if (v[containers[i]] && typeof v[containers[i]] === 'object') {
raw = pickRaw(v[containers[i]]);
if (raw !== undefined) break;
}
}
}
if (raw === undefined || raw === null || raw === '') return 'slide';
if (typeof raw === 'boolean') return raw ? 'always' : 'never';
if (raw === 0 || raw === '0') return 'slide';
if (raw === 1 || raw === '1') return 'always';
if (raw === 2 || raw === '2') return 'never';
const map = {
slide: 'slide', scroll: 'slide', auto: 'slide', scrolling: 'slide', whilescrolling: 'slide', moveslide: 'slide',
always: 'always', alwaysdisplay: 'always', show: 'always',
never: 'never', none: 'never', hidden: 'never', hide: 'never',
// 后台直出中文枚举时兜底
'滑动时显示': 'slide', '滑动时': 'slide', '滚动时显示': 'slide',
'始终显示': 'always', '一直显示': 'always', '永久显示': 'always',
'永不显示': 'never', '从不显示': 'never', '不显示': 'never'
};
const hit = map[String(raw).toLowerCase().trim()] || map[norm(raw)];
return hit || 'slide';
},
// scroll-view 上的滚动条显隐 class: scrollbar-slide / scrollbar-always / scrollbar-never
// "滑动时显示"在滚动中追加 is-scrolling (由 scroll 监听维护)
scrollBarClass() {
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): 图片自定义间距 ==========
// 后台"滑动图片"组件(componentName = ImageNav)的 3 项间距配置。
// 字段名已用线上接口 POST /api/diyview/info(uniacid=312, DIY_VIEW_INDEX) 核对, 实际为:
// imageSpacing 图片横向间距: 相邻图片项之间的横向间隔
// imageSpacingTop 图片上间距 : 图片区域内侧的上内边距
// imageSpacingBottom 图片下间距 : 图片区域内侧的下内边距
// 单位: 按项目惯例(与 diy-group 的 margin.top、图片轮播的 margin.both 一致)换算 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 自身 + 常见嵌套容器(顶层优先)
// 注意: 后端偶发把 margin 存成数组(线上该组件 margin: []), 数组不能当容器取裸键, 直接跳过
const sources = [v];
['imageSpacing', 'imageSpace', 'spacing', 'space', 'margin', 'padding', 'style', 'config', 'setting']
.forEach(k => {
if (v[k] && typeof v[k] === 'object' && !Array.isArray(v[k])) 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 {
// 图片横向间距 —— 后台现用命名 imageSpacing(首项命中即可, 后面都是兼容别名)
horizontal: pick(
['imagespacing', 'imagespacinghorizontal', 'imagehorizontalspacing', 'imagespacingboth', 'imagespacenum',
'imagemargin', 'imagemarginnum', 'imagespace', 'horizontalspacing', 'horizontalspacenum', 'imagegap',
'spacenum', 'marginnum', 'imageverticalspace', 'space', 'gap'],
['horizontalspacing', 'imagespacing', 'imagespace', 'imagemargin', 'spacenum', 'marginnum', 'imagegap', 'gap'],
['horizontal', 'horizontalnum', 'lr', 'leftright', 'both', 'bothnum', 'imagespacing', 'num', 'margin', 'space', 'gap']
),
// 图片上间距 —— 后台现用命名 imageSpacingTop
paddingTop: pick(
['imagespacingtop', 'imagetopspacing', 'imagespacetop', 'imagetopspace', 'imagepaddingtop',
'paddingtop', 'spacingtop', 'topspace', 'margintop'],
['imagespacingtop', 'paddingtop', 'spacingtop', 'topspacing', 'topspace', 'margintop'],
['top', 'topnum', 'paddingtop', 'margintop', 'spacetop', 'spacingtop']
),
// 图片下间距 —— 后台现用命名 imageSpacingBottom
paddingBottom: pick(
['imagespacingbottom', 'imagebottomspacing', 'imagespacebottom', 'imagebottomspace', 'imagepaddingbottom',
'paddingbottom', 'spacingbottom', 'bottomspace', 'marginbottom'],
['imagespacingbottom', 'paddingbottom', 'spacingbottom', 'bottomspacing', 'bottomspace', 'marginbottom'],
['bottom', 'bottomnum', 'paddingbottom', 'marginbottom', 'spacebottom', 'spacingbottom']
)
};
},
// 图片区域上/下内边距(作用在滚动容器上); 未配置时不输出内联样式, 沿用 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: {
// ========== PR-FRONTEND-DIY-SCROLLBAR (2026-09-18): H5 "滑动时显示"滚动条控制 ==========
// 背景: H5 端 scroll-view 的横向滚动条默认被 CSS 隐藏; "滑动时显示"需要监听 scroll,
// 滚动中给容器加 is-scrolling 类展示滚动条, 停止滚动 800ms 后移除类再隐藏。
// "始终显示/永不显示"为纯 CSS 类控制(scrollbar-always/scrollbar-never), 无需 JS。
bindSlideScrollbar() {
// #ifdef H5
if (this._slideScrollBound) return;
var el = this.getSlideContainer();
if (!el) return;
this._slideScrollEl = el;
this._slideScrollBound = true;
var self = this;
this._onSlideScroll = function() {
if (self.scrollBarMode != 'slide') return;
if (!self.slideScrolling) self.slideScrolling = true;
clearTimeout(self._slideScrollTimer);
self._slideScrollTimer = setTimeout(function() {
self.slideScrolling = false;
}, 800);
};
el.addEventListener('scroll', this._onSlideScroll, { passive: true });
// #endif
},
unbindSlideScrollbar() {
// #ifdef H5
if (this._slideScrollEl && this._onSlideScroll) {
this._slideScrollEl.removeEventListener('scroll', this._onSlideScroll);
}
clearTimeout(this._slideScrollTimer);
this._slideScrollEl = null;
this._onSlideScroll = null;
this._slideScrollBound = false;
this.slideScrolling = false;
// #endif
},
// 页面跳转
redirectTo(link) {
if (!link.wap_url || this.$util.getCurrRoute() != this.$util.MEMBER_PAGE_URL || this.storeToken) {
@@ -327,17 +586,61 @@ export default {
// #ifdef H5
overflow-x: auto;
overflow-y: hidden;
// 隐藏滚动条,视觉上仍保持单页布局;用户用鼠标拖拽/滚轮即可滚动
scrollbar-width: none; // Firefox
-ms-overflow-style: none; // IE/Edge legacy
&::-webkit-scrollbar {
display: none;
}
// 禁止鼠标选中文字(拖拽时选中会显得很别扭)
user-select: none;
-webkit-user-select: none;
// 拖拽时关闭过渡,避免 Chrome 的惯性滚动与 JS 抢控制权
-webkit-overflow-scrolling: touch;
// ========== PR-FRONTEND-DIY-SCROLLBAR (2026-09-18): 水平滚动条显隐三态 ==========
// 默认态(滑动时显示的静止态 / 永不显示): 隐藏滚动条,视觉上仍保持单页布局;
// 用户用鼠标拖拽/滚轮即可滚动(滚动能力不受滚动条显隐影响)
scrollbar-width: none; // Firefox
-ms-overflow-style: none; // IE/Edge legacy
&::-webkit-scrollbar {
display: none;
}
// uni scroll-view 在 H5 编译后真滚动容器是内层 .uni-scroll-view, 同步隐藏
::v-deep .uni-scroll-view {
scrollbar-width: none;
-ms-overflow-style: none;
}
::v-deep .uni-scroll-view::-webkit-scrollbar {
display: none;
}
// "始终显示" 与 "滑动时显示"(滚动中, 由 is-scrolling 类驱动): 展示细滚动条
&.scrollbar-always,
&.scrollbar-slide.is-scrolling {
scrollbar-width: thin; // Firefox
scrollbar-color: rgba(0, 0, 0, 0.25) transparent;
&::-webkit-scrollbar {
display: block;
height: 4px;
}
&::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.25);
border-radius: 2px;
}
&::-webkit-scrollbar-track {
background-color: transparent;
}
::v-deep .uni-scroll-view {
scrollbar-width: thin;
scrollbar-color: rgba(0, 0, 0, 0.25) transparent;
}
::v-deep .uni-scroll-view::-webkit-scrollbar {
display: block;
height: 4px;
}
::v-deep .uni-scroll-view::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.25);
border-radius: 2px;
}
::v-deep .uni-scroll-view::-webkit-scrollbar-track {
background-color: transparent;
}
}
// #endif
}