chore:悬浮按钮可以自定义调节

This commit is contained in:
2026-10-08 17:59:37 +08:00
parent fe0381a298
commit 3b354992e3
4 changed files with 117 additions and 37 deletions

View File

@@ -257,7 +257,7 @@ const localDevConfig = ({
// })['2811']; // 本地
// })['5g-317']; // 电磁兼容
// })['5g-351']; //
})['5g-248']; // 本地
})['5g-97']; // 本地
// })['local-2']; // 本地
// })['local-11']; // 本地
// })['1-test']; // test

View File

@@ -629,16 +629,16 @@ function loadLangPackSync(lang, path) {
* @returns
*/
/**
* 语言切换开关钩子(由后台 shop_info.ischia 决定,经 store/index.js 的 setShopInfo 写入)。
* 语言切换开关钩子(由后台 shop_info.ischina 决定,经 store/index.js 的 setShopInfo 写入)。
*
* 设计原则: 只"在关闭时清理持久化语种", 绝不在开启态加任何门控 —— 否则一旦 ischia
* 设计原则: 只"在关闭时清理持久化语种", 绝不在开启态加任何门控 —— 否则一旦 ischina
* 取值/语义判断偏差, 就会误伤"开启双语后切换英文"的正常行为(回归)。
*
* 行为:
* - 后台关闭双语/小程序(ischia 为假): 清掉用户此前持久化到 storage 的语种 + 缓存,
* - 后台关闭双语/小程序(ischina 为假): 清掉用户此前持久化到 storage 的语种 + 缓存,
* 使刷新/重新进入时 getCurrentLocale() 回退默认中文, 满足
* "后台关闭双语后, 刷新重新进入必须是中文" 的需求。
* - 后台开启双语(ischia 为真): 不做任何拦截, 完全保持原有"切换英文"行为不变。
* - 后台开启双语(ischina 为真): 不做任何拦截, 完全保持原有"切换英文"行为不变。
*/
function setLangSwitchEnabled(enabled) {
if (!enabled) {
@@ -994,7 +994,7 @@ export default {
getCurrentLocale,
/**
* 语言切换功能开关(后台 shop_info.ischia 决定)。
* 语言切换功能开关(后台 shop_info.ischina 决定)。
* 关闭后 getCurrentLocale() 强制返回默认语言(中文),并清掉已持久化的语种。
* 详见本文件 setLangSwitchEnabled 函数与 store/index.js 的 setShopInfo 钩子。
*/

View File

@@ -1,7 +1,7 @@
<template>
<!-- Vue 2 / uni-app 单根约束:用 view 包一层 -->
<view>
<view v-if="pageCount === 1 || need" class="fixed-box"
<view v-if="(pageCount === 1 || need) && fbVisible && hasAnyButton" class="fixed-box"
:style="[customContainerStyle, {
height: containerHeight,
bottom: containerBottom,
@@ -11,18 +11,18 @@
}]">
<!-- 多语言切换按钮:根据语言数智能切换交互 -->
<view
v-if="isLanguageSwitchEnabled && fixBtnShow"
v-if="isLanguageSwitchEnabled && hasLangButton"
class="btn-item common-bg"
:style="btnItemStyle"
:style="[btnItemStyle, { order: orderIndex.lang }]"
@click="handleLangButtonClick"
>
<view class="lang-btn-text">{{ currentLangDisplayName }}</view>
<view class="lang-btn-text">{{ langButtonText }}</view>
</view>
<!-- 微信官方客服按钮-->
<button
v-if="fixBtnShow && hasValidKefuConfig && isWeappOfficialKefu"
v-if="hasKefuButton && hasValidKefuConfig && isWeappOfficialKefu"
class="btn-item common-bg"
:style="btnItemStyle"
:style="[btnItemStyle, { order: orderIndex.kefu }]"
open-type="contact"
>
<text class="icox icox-kefu" v-if="!kefuimg"></text>
@@ -30,9 +30,9 @@
<!-- 其他类型客服按钮(智能客服、企业微信等)-->
<view
v-if="fixBtnShow && hasValidKefuConfig && !isWeappOfficialKefu && !showWeappSystemKefu"
v-if="hasKefuButton && hasValidKefuConfig && !isWeappOfficialKefu && !showWeappSystemKefu"
class="btn-item common-bg"
:style="btnItemStyle"
:style="[btnItemStyle, { order: orderIndex.kefu }]"
@click="handleUnifiedKefuClick"
>
<image
@@ -48,9 +48,9 @@
<!-- 智能客服按钮(当同时显示小程序系统客服时)-->
<view
v-if="fixBtnShow && hasValidKefuConfig && !isWeappOfficialKefu && showWeappSystemKefu"
v-if="hasKefuButton && hasValidKefuConfig && !isWeappOfficialKefu && showWeappSystemKefu"
class="btn-item common-bg"
:style="btnItemStyle"
:style="[btnItemStyle, { order: orderIndex.kefu }]"
@click="handleUnifiedKefuClick"
>
<image
@@ -66,9 +66,9 @@
<!-- 小程序系统客服按钮 -->
<button
v-if="fixBtnShow && hasValidKefuConfig && showWeappSystemKefu"
v-if="hasKefuButton && hasValidKefuConfig && showWeappSystemKefu"
class="btn-item common-bg"
:style="btnItemStyle"
:style="[btnItemStyle, { order: orderIndex.kefu }]"
open-type="contact"
>
<text class="icox icox-kefu" v-if="!kefuimg"></text>
@@ -76,9 +76,9 @@
<!-- 电话按钮 -->
<view
v-if="fixBtnShow"
v-if="hasPhoneButton"
class="btn-item common-bg"
:style="[btnItemStyle, { backgroundImage: phoneimg ? `url(${phoneimg})` : '', backgroundSize: '100% 100%' }, customButtonStyle]"
:style="[btnItemStyle, { order: orderIndex.phone }, { backgroundImage: phoneimg ? `url(${phoneimg})` : '', backgroundSize: '100% 100%' }, customButtonStyle]"
@click="call"
>
<text class="iconfont icon-dianhua" v-if="!phoneimg"></text>
@@ -109,6 +109,10 @@
<script>
import { createCustomerService } from '@/common/js/customer-service.js';
import i18nLazyMixin from '@/common/mixin/i18n-lazy.js';
import {
getCurrentRoute, shouldShowOnPage, resolveVisibleButtons,
resolveLangButtonText, isLangButtonVisible,
} from '@/common/js/floating-button.js';
// 语言短码映射表(currentLangDisplayName 展示用,可按需扩展)
// key: 语言 value,value: 显示短码(按钮上展示)
@@ -170,7 +174,7 @@ export default {
data() {
return {
pageCount: 0,
fixBtnShow: true,
currentRoute: '', // 当前页面路由(onShow / created 时刷新, 供页面范围判定)
shopInfo: null,
currentLangIndex: 0,
langIndexMap: {},
@@ -237,6 +241,57 @@ export default {
customButtonStyle() {
return this.shopInfo?.floatingButton?.button || {};
},
// ============================================================
// 浮动侧边栏配置消费 (来源: /api/config/init → data.floating_button,
// store/index.js 的 floatingButtonConfig, 初始 state 已声明 → 响应式)
// fbConfig 为 null (老缓存响应缺字段) 时, 各判定全部走老行为兜底。
// ============================================================
// 接口下发的浮动侧边栏配置 (老缓存响应 → null)
fbConfig() {
return (this.$store && this.$store.state && this.$store.state.floatingButtonConfig) || null;
},
// 总开关 + 强制隐藏 + 页面范围 判定结果
fbVisible() {
return shouldShowOnPage(this.fbConfig, this.currentRoute, true);
},
// 最终要渲染的按钮 (已按 button_order 排序 + 显隐过滤); null = 用老行为
visibleButtons() {
return resolveVisibleButtons(this.fbConfig);
},
hasAnyButton() {
const b = this.visibleButtons;
return b ? b.length > 0 : true; // 老行为: 电话按钮恒显
},
hasLangButton() {
const b = this.visibleButtons;
return b ? b.indexOf('lang') !== -1
: isLangButtonVisible(this.fbConfig, this.shopInfo, this.langList.length);
},
hasKefuButton() {
const b = this.visibleButtons;
return b ? b.indexOf('kefu') !== -1 : true;
},
hasPhoneButton() {
const b = this.visibleButtons;
return b ? b.indexOf('phone') !== -1 : true;
},
// 语言按钮文案: 商家可配「目标语言 / 当前语言 / 固定文案」。
// 配置缺失时回退老行为 currentLangDisplayName(展示当前语言)。
langButtonText() {
if (!this.fbConfig) return this.currentLangDisplayName;
return resolveLangButtonText(this.fbConfig, this.langList, this.currentLangIndex);
},
// 按钮渲染顺序 (button_order 自上而下); .fixed-box 是 flex-direction: column,
// 用 CSS order 控制三段(语言/客服/电话)的先后, 配置缺失时退回老顺序。
orderIndex() {
const order = (this.fbConfig && Array.isArray(this.fbConfig.button_order))
? this.fbConfig.button_order
: ['lang', 'kefu', 'phone'];
const map = {};
for (let i = 0; i < order.length; i++) map[order[i]] = i + 1;
map.lang = map.lang || 1; map.kefu = map.kefu || 2; map.phone = map.phone || 3;
return map;
},
// 所有 .btn-item 的 inline-style 兜底:
// 1) 测试发现 .btn-item 的 min-*/max-*/aspect-ratio/flex-shrink 等 !important
// 规则在 scoped 样式下,被某些 flex 布局的隐式约束吃掉,只 width/height/border-radius
@@ -301,18 +356,19 @@ export default {
// 每个按钮槽位:72rpx(按钮,与 to-top 一致) + 14rpx(上 margin) + 14rpx(下 margin) = 100rpx
// 容器 padding:20rpx 上 + 20rpx 下 = 40rpx
// 总高度 (rpx):buttonCount × 100 + 40
// 修复:改返回 rpx 而非 px(原 *0.5 硬编码 iPhone 6 375px 转换比),
// 在微信小程序/其他设备(iPhone 13 390px、iPad 等)下由 uni-app 框架
// 按 windowWidth 动态换算,避免 4+ 按钮时容器偏小导致内容被裁切
// 关联:按钮尺寸与 to-top 同步(72rpx),所以槽位高度也是 100rpx;
// 之前按钮 80rpx 时槽位 108rpx,改为 72rpx 后槽位缩小 8rpx,容器整体更紧凑
// 新配置生效时按 visibleButtons 的实际按钮数算,避免隐藏按钮后留下空白档;
// 全部按钮被关掉时返回 0rpx(根节点 v-if 已含 hasAnyButton, 这里是双保险);
// 配置缺失(fbConfig 为 null)时保持老行为: 电话恒显 + 可选语言 + 可选系统客服。
// extraKefuSlot: 小程序系统客服与客服主按钮并存时模板会多渲染一个按钮,
// 需要为它多留一个槽位(fallback 分支与老逻辑等价)。
containerHeight() {
if (!this.fixBtnShow) return '320rpx';
const b = this.visibleButtons;
if (b && b.length === 0) return '0rpx';
let buttonCount = 1;
if (this.isLanguageSwitchEnabled) buttonCount++;
if (this.showWeappSystemKefu) buttonCount++;
buttonCount++;
const extraKefuSlot = (this.hasKefuButton && this.showWeappSystemKefu) ? 1 : 0;
const buttonCount = b
? b.length + extraKefuSlot
: 1 + (this.isLanguageSwitchEnabled ? 1 : 0) + extraKefuSlot;
const totalRpx = buttonCount * 100 + 40;
return `${totalRpx}rpx`;
@@ -337,7 +393,6 @@ export default {
// 与 Top 按钮的 .mescroll-totop { margin-bottom: var(--window-bottom) } 一致,
// 两者会被安全区同步上推,space 保持不变
containerBottom() {
if (!this.fixBtnShow) return '240rpx';
let space = 18 * 5;
// #ifdef MP-WEIXIN
space = 18 * 1;
@@ -357,7 +412,6 @@ export default {
// - right edge 对齐(46rpx):左侧突出 28rpx,中心错位 14rpx
// center 对齐不仅中心精准,突出量也更小
containerRight() {
if (!this.fixBtnShow) return '0rpx';
const offset = this.layoutTopRight + this.layoutTopSize / 2 - this.layoutFloatingWidth / 2;
return `${offset}rpx`;
},
@@ -420,9 +474,14 @@ export default {
}
}
},
// 页面生命周期: hover-nav 是常驻组件, 路由变化必须靠 onShow 通知,
// 否则 currentRoute 停留在首次加载页, 页面范围判定(fbVisible)会失效。
onShow() {
this.refreshRoute();
},
created() {
this.initLanguage();
this.pageCount = getCurrentPages().length;
this.refreshRoute();
// 三源兜底(store / 同步 storage / 异步 storage),
// 任何一个先就绪就先使用,后续由 initStatus watcher 再次同步
@@ -523,6 +582,11 @@ export default {
// #endif
},
methods: {
// 刷新当前路由与页面栈深度(供 fbVisible 的页面范围判定 / pageCount 老判定)
refreshRoute() {
this.currentRoute = getCurrentRoute();
this.pageCount = getCurrentPages().length;
},
// #ifdef H5
/**
* 从 :root 读取 CSS 变量(App.vue 中定义)到 data,

View File

@@ -98,6 +98,7 @@ const store = new Vuex.Store({
wap_valid_time: 0,
},
copyright: null,
floatingButtonConfig: null, // 来自 /api/config/init 的 data.floating_button (浮动侧边栏配置)
initStatus: false,
offlineWhiteList: ['pages_order/payment', 'pages_order/list', 'pages_order/detail'],//线下支付白名单
canReceiveRegistergiftInfo: {
@@ -164,10 +165,23 @@ const store = new Vuex.Store({
setShopInfo(state, value) {
state.shopInfo = value;
uni.setStorageSync('shopInfo', value); // 初始化数据调用
// 语言切换开关: 后台 shop_info.ischia 决定。
// 关闭(ischia 为假)时强制返回默认中文,并清掉用户此前持久化到 storage 的语种,
// 语言切换开关 fallback: 后台 shop_info.ischina 决定(老「双语模式」)。
// 新配置下发后由 setFloatingButtonConfig 里的 lang_switch.enabled 接管,
// 这里的 ischina 只是配置缺失(老缓存响应)时的兜底。
// 关闭时强制返回默认中文,并清掉用户此前持久化到 storage 的语种,
// 满足"后台关闭双语/小程序后,刷新重新进入必须是中文"的需求。
try { LangConfig.setLangSwitchEnabled(!!(value && value.ischia)); } catch (e) {}
try { LangConfig.setLangSwitchEnabled(!!(value && value.ischina)); } catch (e) {}
},
// 浮动侧边栏配置 (来自 /api/config/init → data.floating_button)
setFloatingButtonConfig(state, value) {
state.floatingButtonConfig = value || null;
uni.setStorageSync('floatingButtonConfig', value || null);
// 与 setShopInfo 的 setLangSwitchEnabled 配对: 新配置存在时以新配置为准
try {
if (value && value.lang_switch) {
LangConfig.setLangSwitchEnabled(Number(value.lang_switch.enabled) === 1);
}
} catch (e) {}
},
setCartChange(state) {
state.cartChange += 1;
@@ -315,6 +329,8 @@ const store = new Vuex.Store({
//联系我们
this.commit('setShopInfo', data.shop_info);
// 浮动侧边栏配置; 缺失时显式置 null → 前端走老行为兜底
this.commit('setFloatingButtonConfig', data.floating_button || null);
this.commit('setWechatConfigStatus', data?.wechat_config_status);
// 默认总店
if (data.store_info) {