diff --git a/common/js/empty-style-stub.js b/common/js/empty-style-stub.js index 70a9f57..cbdc5e9 100644 --- a/common/js/empty-style-stub.js +++ b/common/js/empty-style-stub.js @@ -68,6 +68,10 @@ export default { hover_nav_bg_color: '#FFFC', hover_nav_text_color: '#000', + hover_nav_font_size: '36rpx', // 悬浮按钮内文字/图标字号(后台可自定义, 默认与图标 36rpx 对齐) + hover_nav_lang_font_size: '36rpx', // 语言按钮内文字字号基准(文字实际按 0.667 缩放) + hover_nav_kefu_font_size: '36rpx', // 客服按钮内图标/emoji字号基准 + hover_nav_phone_font_size: '36rpx', // 电话按钮内图标字号基准 // ===== DIY 分类页字号 token ===== diy_category_tree_select_size: '28rpx', diff --git a/common/js/floating-button.js b/common/js/floating-button.js new file mode 100644 index 0000000..91072be --- /dev/null +++ b/common/js/floating-button.js @@ -0,0 +1,134 @@ +/** + * 浮动侧边栏配置消费 (字段来源: /api/config/init → data.floating_button) + * 纯函数模块, 无副作用, 可直接单测 + */ + +/** 归一化路由: 'pages/index/index' → '/pages/index/index' */ +export function normalizeRoute(route) { + if (!route) return ''; + return String(route).charAt(0) === '/' ? String(route) : '/' + String(route); +} + +/** 取当前页面路由 (兼容 H5 / 小程序; route 已含分包 root, 如 'pages_goods/detail') */ +export function getCurrentRoute() { + try { + const pages = getCurrentPages(); + const cur = pages[pages.length - 1]; + return normalizeRoute((cur && (cur.route || (cur.$page && cur.$page.path))) || ''); + } catch (e) { + return ''; + } +} + +/** + * 前缀命中判定 + * '/pages/index' 命中 '/pages/index' 与 '/pages/index/xxx' + * '/pages_tool/' 命中 '/pages_tool/xxx' (前缀自带尾 /) + * '/pages/order/payment' 只命中它自己及其子路径 + */ +export function hitPrefix(route, prefix) { + if (!route || !prefix) return false; + const p = prefix.charAt(prefix.length - 1) === '/' ? prefix : prefix + '/'; + return route === prefix || route.indexOf(p) === 0; +} + +/** 任一前缀命中 */ +function hitAny(route, prefixes) { + if (!Array.isArray(prefixes)) return false; + for (let i = 0; i < prefixes.length; i++) { + if (hitPrefix(route, prefixes[i])) return true; + } + return false; +} + +/** 找出路由命中的页面分组 key (未命中返回 null) */ +export function matchPageGroup(route, prefixMap) { + if (!prefixMap) return null; + const keys = Object.keys(prefixMap); + for (let i = 0; i < keys.length; i++) { + if (hitAny(normalizeRoute(route), prefixMap[keys[i]])) return keys[i]; + } + return null; +} + +/** + * 侧边栏在当前页面是否显示 —— 唯一判定入口 + * + * 优先级: 总开关 → 强制隐藏 → 显示范围 + * 1) fb 不存在 (老缓存响应) → fallbackVisible (默认 true, 保持老行为) + * 2) enabled != 1 → false + * 3) 命中 force_hidden_prefixes 任意前缀 → false (最高优先级) + * 4) 命中 custom_pages / page_prefixes._custom → true + * 5) 命中 pages[组] == 1 → true + * 6) 都不命中 (如 /pages_goods/*) → false + */ +export function shouldShowOnPage(fb, route, fallbackVisible) { + if (!fb || typeof fb !== 'object') return fallbackVisible !== false; + if (Number(fb.enabled) !== 1) return false; + + const r = normalizeRoute(route); + if (!r) return false; + + // 强制隐藏 — 优先级最高 + if (matchPageGroup(r, fb.force_hidden_prefixes)) return false; + + // 自定义前缀优先判定 + if (hitAny(r, fb.custom_pages) || hitAny(r, (fb.page_prefixes || {})['_custom'])) return true; + + // 预设分组 + const group = matchPageGroup(r, fb.page_prefixes); + if (group) return Number((fb.pages || {})[group]) === 1; + + return false; +} + +/** + * 当前页面要渲染的按钮列表 (已按 button_order 排序 + 显隐过滤) + * 返回 ['lang','kefu','phone'] 的子集, 空数组 = 整个侧边栏不渲染 + * 返回 null = 配置缺失, 由调用方决定老行为 + */ +export function resolveVisibleButtons(fb) { + if (!fb || typeof fb !== 'object') return null; + const order = Array.isArray(fb.button_order) ? fb.button_order : ['lang', 'kefu', 'phone']; + const map = fb.buttons || {}; + const out = []; + for (let i = 0; i < order.length; i++) { + const k = order[i]; + if (Number(map[k]) === 1) out.push(k); + } + return out; +} + +/** + * 语言按钮显示文案 + * @param {Object} fb data.floating_button + * @param {Array} langList [{ value:'zh-cn', name:'CN' }, ...] + * @param {Number} currentIdx 当前语言下标 + */ +export function resolveLangButtonText(fb, langList, currentIdx) { + const list = Array.isArray(langList) ? langList : []; + if (!list.length) return ''; + const style = (fb && fb.lang_switch && fb.lang_switch.style) || 'target'; + + if (style === 'text') { + const t = (fb && fb.lang_switch && fb.lang_switch.text) || ''; + if (t) return t; // 空文案兜底走 target + } + if (style === 'current' || list.length < 2) { + return (list[currentIdx] && list[currentIdx].name) || ''; + } + // target: 显示「点下去会切到的那个」语言 + // 2 种语言 → 另一个; >2 种 → 取第一个不是当前的那个 + let targetIdx = 0; + for (let i = 0; i < list.length; i++) { + if (i !== currentIdx) { targetIdx = i; break; } + } + return list[targetIdx] ? (list[targetIdx].name || '') : ''; +} + +/** 语言切换按钮是否可见 (含老「双语模式」fallback) */ +export function isLangButtonVisible(fb, shopInfo, langCount) { + if (Number(langCount) < 2) return false; + if (fb && fb.lang_switch) return Number(fb.lang_switch.enabled) === 1; + return !!(shopInfo && shopInfo.ischina); // 老行为兜底 +} diff --git a/common/js/startup-ad.js b/common/js/startup-ad.js new file mode 100644 index 0000000..575edff --- /dev/null +++ b/common/js/startup-ad.js @@ -0,0 +1,30 @@ +// 启动广告弹出信号 +// 用于判定"当前是否需要弹出启动广告": +// - 小程序冷启动(首次进入): 由模块级 _coldLaunchedShown 标记判定, 整个 app 进程只记一次 +// - 小程序重新进入(从后台切回 / 微信"重新进入小程序"菜单): +// 由 App.onShow 在"非首次展示"时调用 markNeedStartupAd() 置位 +// 信号消费一次即清除, 不落地到持久化存储, 因此无需清缓存即可每次重新进入都弹出。 +// 关键: 切换 Tab 回到首页只触发"页面级 onShow", 不会触发 App.onShow, 也不会重置 +// 模块级冷启动标记, 因此不会在 Tab 切换时误弹。 + +let _needStartupAd = false; +let _coldLaunchedShown = false; + +// App 层检测到"重新进入小程序"时调用, 标记本次需要弹出启动广告 +export function markNeedStartupAd() { + _needStartupAd = true; +} + +// 页面层读取并消费该信号(消费后自动复位) +export function consumeNeedStartupAd() { + const v = _needStartupAd; + _needStartupAd = false; + return v; +} + +// 是否为本 app 进程的首次冷启动(只返回一次 true, 之后恒为 false) +export function isColdLaunchPending() { + if (_coldLaunchedShown) return false; + _coldLaunchedShown = true; + return true; +} diff --git a/common/js/style_color.js b/common/js/style_color.js index 08d6395..8ace030 100644 --- a/common/js/style_color.js +++ b/common/js/style_color.js @@ -62,7 +62,13 @@ export default { }, ...{ hover_nav_bg_color: '#FFFC', //背景色: 红色 '#c6251b', 白色: '#FFFC' - hover_nav_text_color: '#000' // 文字颜色: 白色 '#FFFFFF', 黑色: '#000' + hover_nav_text_color: '#000', // 文字颜色: 白色 '#FFFFFF', 黑色: '#000' + hover_nav_font_size: '36rpx', // 悬浮按钮内文字/图标字号(后台可自定义, 默认与图标 36rpx 对齐) + // ===== 逐按钮字号(与 hover_nav_lang/kefu/phone 的 bg/text/size 同约定, 基准 36rpx) ===== + // 未单独配置时回退到上面的 hover_nav_font_size(全局旋钮) + hover_nav_lang_font_size: '36rpx', // 语言按钮内文字字号基准(文字实际按 0.667 缩放) + hover_nav_kefu_font_size: '36rpx', // 客服按钮内图标/emoji字号基准 + hover_nav_phone_font_size: '36rpx' // 电话按钮内图标字号基准 }, // ===== DIY 分类页字号 token(全 8 preset 一致, 远端可覆写) ===== diff --git a/components/hover-nav/hover-nav.vue b/components/hover-nav/hover-nav.vue index fa9f893..0878c7b 100644 --- a/components/hover-nav/hover-nav.vue +++ b/components/hover-nav/hover-nav.vue @@ -4,6 +4,7 @@ {{ langButtonText }} @@ -21,8 +22,8 @@