强制合并
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user