Files
lucky_shop/store/index.js
ZF sun 55a9ed5946 refactor: 重构底部导航高亮逻辑,新增跨平台兼容与工具脚本
1. 抽离底部导航归属逻辑为纯函数resolveEntryTab,优化分包页面高亮规则,对齐iOS HIG规范
2. 新增页面Tab通配符映射,简化PAGE_TAB_MAP维护
3. 修复鸿蒙平台webview返回按钮适配问题
4. 新增pages.json校验与导航标题自动注入脚本
5. 优化启动页动画性能,提升合成渲染效率
6. 修复diy-picture组件v-for缺少key的警告
7. 清理鸿蒙平台多余的分包构建配置
8. 更新统计数据与store注释说明
2026-07-16 11:47:29 +08:00

636 lines
21 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
import Http from '../common/js/http.js'
import { themeConfig } from '../common/js/config-external.js'
import configExternal from '../common/js/config-external.js'
import util from '../common/js/util.js'
import ThemeOverride from '../common/js/theme-override.js'
import { sanitizeFlatMap, mergeThemeBuckets } from '../common/js/merge-theme.js'
import PrivacyManager from '../common/js/privacy.js'
// themeColor 结果缓存(避免每次都全量遍历 theme 对象)
// key: JSON.stringify(theme) + tabBarHeight, value: CSS变量字符串
let _themeColorCache = null;
let _themeColorCacheKey = '';
const store = new Vuex.Store({
state: {
token: null,
siteInfo: null,
memberInfo: null,
tabBarList: null,
siteState: 1,
themeStyle: (function() { try { return uni.getStorageSync('themeStyle') || ''; } catch(e) { return ''; } })(),
themeColor: (function() { try { return uni.getStorageSync('themeColor') || ''; } catch(e) { return ''; } })(),
addonIsExist: {
bundling: 0,
coupon: 0,
discount: 0,
fenxiao: 0,
gift: 0,
groupbuy: 0,
manjian: 0,
memberconsume: 0,
memberrecharge: 0,
memberregister: 0,
membersignin: 0,
memberwithdraw: 0,
memberrecommend: 0,
pintuan: 0,
pointexchange: 0,
seckill: 0,
store: 0,
topic: 0,
bargain: 0,
membercancel: 0,
servicer: 0,
supermember: 0,
giftcard: 0,
divideticket: 0,
scenefestival: 0,
birthdaygift: 0,
pinfan: 0,
form: 0
},
authInfo: {}, // 授权信息
flRefresh: 0,
location: null, // 定位信息
defaultImg: {
goods: '',
head: '',
store: '',
article: '',
aiAgent: ''
},
cartList: {},
cartIds: [],
cartNumber: 0,
cartMoney: 0,
cartChange: 0,
wechatConfigStatus: 0,
bottomNavHidden: false, // 底部导航是否隐藏,true:隐藏,false:显示
aiUnreadCount: 10, // AI未读消息数量
globalAIKefuConfig: null, // AI客服配置
customerServiceType: 'ai',
globalStoreConfig: null, // 门店配置
globalStoreInfo: null, // 门店信息
defaultStoreInfo: null, // 默认门店
cartPosition: null, // 购物车所在位置
componentRefresh: 0, // 组件刷新
servicerConfig: null, // 客服配置
wxworkConfig: null, // 企业微信配置
diySeckillInterval: 0,
diyGroupPositionObj: {},
diyGroupShowModule: '',
tabBarHeight: '56px',
packageEntryTab: {}, // 记录每个分包的"入口 tab" { 'pages_goods': '/pages_goods/category', ... }
// 由 diy-bottom-nav.updateCurrentRoute → resolveEntryTab 写入
// 语义:
// - 同包内导航: 当前包第一个 tab (用户一直在包内浏览)
// - 跨包导航: history-walk 找最近的 tab 根页面 (从哪个 tab 出发)
// - 深链/冷启动 (无历史): 不写入 (iOS HIG — 无来源时不点亮)
mapConfig: {
tencent_map_key: '',
wap_is_open: 1,
wap_valid_time: 0,
},
copyright: null,
initStatus: false,
offlineWhiteList: ['pages_order/payment', 'pages_order/list', 'pages_order/detail'],//线下支付白名单
canReceiveRegistergiftInfo: {
status: false,
path: ''
},
},
mutations: {
// 设置是否可以领取新人礼
setCanReceiveRegistergiftInfo(state, data) {
state.canReceiveRegistergiftInfo = data;
},
// 设置那些组件展示
setDiyGroupShowModule(state, data) {
state.diyGroupShowModule = data;
},
// 设置diyGroup中组件原有高度,通过他们来实现在首页的定位
setDiyGroupPositionObj(state, data) {
state.diyGroupPositionObj = Object.assign({}, state.diyGroupPositionObj, data);
},
setSiteState(state, siteStateVal) {
state.siteState = siteStateVal;
},
setThemeStyle(state, value) {
state.themeStyle = value
uni.setStorageSync('themeStyle', value); // 初始化数据调用
},
setTabBarList(state, value) {
state.tabBarList = value;
},
setAddonIsExist(state, value) {
state.addonIsExist = value;
uni.setStorageSync('addonIsExist', value); // 初始化数据调用
},
setToken(state, value) {
state.token = value;
if (value) {
uni.setStorageSync('token', value); // 初始化数据调用
} else {
uni.removeStorageSync('token');
}
},
setAuthinfo(state, value) {
state.authInfo = value;
},
setflRefresh(state, flRefreshVal) {
state.flRefresh = flRefreshVal;
},
setLocation(state, value) {
var date = new Date();
date.setSeconds(60 * state.mapConfig.wap_valid_time);
value.valid_time = date.getTime() / 1000; // 定位信息 5分钟内有效,过期后将重新获取定位信息
state.location = value;
uni.setStorageSync('location', value); // 初始化数据调用
},
setDefaultImg(state, value) {
state.defaultImg = value;
uni.setStorageSync('defaultImg', value); // 初始化数据调用
},
setSiteInfo(state, value) {
state.siteInfo = value;
uni.setStorageSync('siteInfo', value); // 初始化数据调用
},
setShopInfo(state, value) {
state.shopInfo = value;
uni.setStorageSync('shopInfo', value); // 初始化数据调用
},
setCartChange(state) {
state.cartChange += 1;
},
setBottomNavHidden(state, value) {
state.bottomNavHidden = value;
},
setglobalAIKefuConfig(state, value) {
state.globalAIKefuConfig = value;
uni.setStorageSync('globalAIKefuConfig', value); // 初始化数据调用
},
setCustomerServiceType(state, type) {
state.customerServiceType = type;
},
setGlobalStoreConfig(state, value) {
state.globalStoreConfig = value;
uni.setStorageSync('globalStoreConfig', value); // 初始化数据调用
},
setGlobalStoreInfo(state, value) {
state.globalStoreInfo = value;
uni.setStorageSync('globalStoreInfo', value); // 初始化数据调用
},
setDefaultStoreInfo(state, value) {
state.defaultStoreInfo = value;
uni.setStorageSync('defaultStoreInfo', value); // 初始化数据调用
},
setCartPosition(state, value) {
state.cartPosition = value;
},
setComponentRefresh(state) {
state.componentRefresh += 1;
},
// 客服配置
setServicerConfig(state, value) {
state.servicerConfig = value;
uni.setStorageSync('servicerConfig', value);
},
// 企业微信配置
setWxworkConfig(state, value) {
state.wxworkConfig = value;
uni.setStorageSync('wxworkConfig', value);
},
setDiySeckillInterval(state, value) {
state.diySeckillInterval = value;
},
setTabBarHeight(state, value) {
state.tabBarHeight = value;
},
setPackageEntryTab(state, value) {
state.packageEntryTab = value;
},
setMapConfig(state, value) {
state.mapConfig = value;
uni.setStorageSync('mapConfig', value);
},
setCopyright(state, value) {
state.copyright = value;
uni.setStorageSync('copyright', value);
},
setMemberInfo(state, value) {
// 会员被锁定后,清除会员登录信息
if (value && value.status == 0) {
value = null;
}
state.memberInfo = value;
if (value) {
uni.setStorageSync('memberInfo', value);
} else {
// 会员为空时,清除会员登录信息
uni.removeStorageSync('memberInfo');
this.commit('setToken', '');
this.dispatch('emptyCart');
//uni.removeStorageSync('authInfo');
}
},
setCartNumber(state, cartNumber) {
state.cartNumber = cartNumber
},
setCartList(state, value) {
state.cartList = value;
},
setCartIds(state, value) {
state.cartIds = value;
},
setCartMoney(state, value) {
state.cartMoney = value;
},
setInitStatus(state, value) {
state.initStatus = value
},
setWechatConfigStatus(state, value) {
state.wechatConfigStatus = value
},
// 设置AI未读消息数量
setAiUnreadCount(state, value) {
state.aiUnreadCount = value;
},
// 设置主题颜色
setThemeColor(state, value) {
state.themeColor = value;
}
},
getters: {
// AI智能助手配置
globalAIKefuConfig: state => state.globalAIKefuConfig,
// AI未读消息数量
aiUnreadCount: state => state.aiUnreadCount,
},
actions: {
init() {
return new Promise(async (resolve, reject) => {
Http.sendRequest({
url: '/api/config/init',
success: async res => {
var data = res.data;
if (data) {
// 走新 action: 加载预设 + 远端覆写合并 + 应用
// (不直接 commit setThemeStyle + themeColorSet,避免漏远端)
await this.dispatch('applyThemeWithRemote', data.style_theme?.name || themeConfig.defaultTheme);
// 底部导航
this.commit('setTabBarList', data.diy_bottom_nav);
this.commit('setAddonIsExist', data.addon_is_exist);
this.commit('setDefaultImg', data.default_img);
this.commit('setSiteInfo', data.site_info);
this.commit('setServicerConfig', data.servicer);
// 企业微信配置
if (data?.wxwork_config) {
this.commit('setWxworkConfig', data.wxwork_config);
}
this.commit('setCopyright', data.copyright);
this.commit('setMapConfig', data.map_config);
this.commit('setglobalAIKefuConfig', data.ai_agent_config);
this.commit('setGlobalStoreConfig', data.store_config);
//联系我们
this.commit('setShopInfo', data.shop_info);
this.commit('setWechatConfigStatus', data?.wechat_config_status);
// 默认总店
if (data.store_info) {
this.commit('setDefaultStoreInfo', data.store_info);
} else {
// 清空不存在的门店信息
this.commit('setDefaultStoreInfo', null);
this.commit('setGlobalStoreInfo', null);
}
this.commit('setInitStatus', true)
// 远程下发隐私弹窗 URL/路由规则 (可选)
// 字段约定:
// data.privacy_url_rules = { mode?: 'all'|'only'|'skip',
// patterns?: string[],
// merge?: 'replace'|'union'|'intersect',
// sign?: string (HMAC 签名, 与后端约定) }
// 与 App.vue 中声明的本地 base 融合:
// - 字段缺失 → 用本地 base
// - mode 远程有值 → 远程覆盖; 否则用本地
// - patterns 按 merge 字段决定 (默认 replace)
// 签名校验 (可选):
// - privacyConfig.signSecret 非空 → PrivacyManager.verifyRemotePatch 校验 patch.sign
// - 校验失败 → applyWithMerge 内部 fallback 到 base, 不应用 patch
// 融合完成后 emit 'privacy:rules-updated', 持久化到 storage (跨冷启动恢复)
try {
if (data && data.privacy_url_rules && typeof data.privacy_url_rules === 'object') {
// base 必须从 PrivacyManager.__getBaseRules() 读取
// App.vue 顶层 script 已通过 setBaseRules() 登记
// 若登记失败 (例如 HMR / 测试环境), 退化为 'all'+[] 但跳过本次应用
const baseRules = (PrivacyManager && typeof PrivacyManager.__getBaseRules === 'function')
? PrivacyManager.__getBaseRules()
: null
if (baseRules && (baseRules.mode || (baseRules.patterns && baseRules.patterns.length))) {
// applyWithMerge 内部已调 verifyRemotePatch, 不通过的 patch 静默退回 base
PrivacyManager.applyWithMerge(baseRules, data.privacy_url_rules)
} else {
console.warn('[privacy] base rules not registered, skip remote patch apply')
}
}
} catch (e) {
console.warn('[privacy] apply remote rules fail', e)
}
resolve(data);
}
}
});
})
},
// 查询购物车列表、总数量、总价格
getCartNumber() {
Http.sendRequest({
url: '/api/cart/lists',
data: {},
success: res => {
if (res.code == 0) {
let list = {};
let ids = [];
let totalMoney = 0;
let totalNum = 0;
if (res.data.length) {
res.data.forEach((item) => {
let cart = {
cart_id: item.cart_id,
goods_id: item.goods_id,
sku_id: item.sku_id,
num: item.num,
discount_price: item.discount_price,
min_buy: item.min_buy,
stock: item.stock,
};
if (!list['goods_' + cart.goods_id]) {
list['goods_' + cart.goods_id] = {};
}
list['goods_' + cart.goods_id]['max_buy'] = item.max_buy;
list['goods_' + cart.goods_id]['goods_name'] = item.goods_name;
list['goods_' + cart.goods_id]['sku_' + cart.sku_id] = cart;
ids.push(cart.cart_id);
});
for (let goods in list) {
let num = 0;
let money = 0;
for (let sku in list[goods]) {
let item = list[goods][sku];
if (typeof item == 'object') {
num += item.num;
money += parseFloat(item.discount_price) * parseInt(item.num);
}
}
list[goods].num = num;
list[goods].total_money = money;
totalNum += num;
totalMoney += money;
}
}
this.commit('setCartList', list);
this.commit('setCartIds', ids);
this.commit('setCartNumber', totalNum);
this.commit('setCartMoney', totalMoney);
}
}
});
},
// 清空购物车 ns-goods-sku-index组件中引用
emptyCart() {
this.commit('setCartList', {});
this.commit('setCartIds', []);
this.commit('setCartNumber', 0);
this.commit('setCartMoney', 0);
},
// 计算购物车数量、价格
cartCalculate() {
let ids = [];
let totalMoney = 0;
let totalNum = 0;
for (let k in this.state.cartList) {
let item = this.state.cartList[k];
let num = 0;
let money = 0;
for (let sku in item) {
if (typeof item[sku] == 'object') {
num += item[sku].num;
money += parseFloat(item[sku].discount_price) * parseInt(item[sku].num);
ids.push(item[sku].cart_id);
}
}
item.num = num;
item.total_money = money;
totalNum += num;
totalMoney += money;
}
this.commit('setCartNumber', totalNum);
this.commit('setCartMoney', totalMoney);
this.commit('setCartIds', ids);
},
// 生成主题颜色CSS变量(带缓存)
themeColorSet() {
let theme = this.state.themeStyle;
if (!theme?.main_color || !theme?.aux_color) return;
try {
// H5 端 CSS 变量值若是 rpx 字符串,浏览器原生无法解析
// (uni-app PostCSS 只在静态 SCSS 中把 rpx 编译成 px,JS 运行时
// setProperty('--xxx','20rpx') 写入后 var() 解析失败)
// 这里把 token 值里的 rpx 串转成 px,其它值原样保留。
// 仅 #ifdef H5 生效;小程序原生支持 rpx,不需要转换。
const rpxToPx = (() => {
let pxPerRpx = 0;
try {
if (typeof window !== 'undefined' && window.innerWidth) {
pxPerRpx = window.innerWidth / 750;
}
} catch (e) { /* 非 H5 环境,保持原值 */ }
return (v) => {
if (pxPerRpx <= 0) return v;
if (typeof v !== 'string') return v;
if (v.indexOf('rpx') === -1) return v;
return v.replace(/(-?\d+(?:\.\d+)?)rpx\b/gi, (m, n) => {
return (parseFloat(n) * pxPerRpx) + 'px';
});
};
})();
const cacheKey = (typeof theme === 'object' ? JSON.stringify(theme) : String(theme))
+ '|' + (this.state.tabBarHeight || '')
+ '|' + (typeof window !== 'undefined' ? (window.innerWidth || 0) : 0);
if (_themeColorCache && _themeColorCacheKey === cacheKey) {
if (this.state.themeColor !== _themeColorCache) {
this.commit('setThemeColor', _themeColorCache);
}
return;
}
let themeColor = `--base-color:${theme.main_color};--base-help-color:${theme.aux_color};`;
if (this.state.tabBarHeight != '56px') themeColor += `--tab-bar-height:${this.state.tabBarHeight};`
Object.keys(theme).forEach(key => {
let data = theme[key];
if (typeof (data) == "object") {
Object.keys(data).forEach(k => {
themeColor += '--' + k.replace(/_/g, "-") + ':' + rpxToPx(data[k]) + ';';
});
} else if (typeof (key) == "string" && key) {
themeColor += '--' + key.replace(/_/g, "-") + ':' + rpxToPx(data) + ';';
}
});
for (let i = 9; i >= 5; i--) {
let color = util.colourBlend(theme.main_color, '#ffffff', (i / 10));
themeColor += `--base-color-light-${i}:${color};`;
}
_themeColorCache = themeColor;
_themeColorCacheKey = cacheKey;
this.commit('setThemeColor', themeColor);
} catch (e) {
console.error('设置主题颜色失败', e);
}
},
/**
* 加载预设 + 拉远端覆写 + 深合并 + 走原 setThemeStyle/themeColorSet 链路
* 关键: 应用层一行不动 — Vuex.themeStyle / themeColor / 页面 wrapper / CSS var
* 全部保持兼容,所有改动只发生在"加载"环节
*
* remote-only 模式兼容:
* - 首次调用时 loadAllPresets 可能尚未完成,loadTheme() 会拿到 webpack stub
* (只含 default preset). 如果用户选了非 default preset(green/blue/...),
* loadTheme 返回 undefined.
* - 兜底: 订阅 theme:presets-updated 事件,远端拉完后重新应用.
* 一次性订阅(once 语义,触发后即解绑),避免后续切主题时重复触发.
*
* @param {string} presetName style_color.js 中的 preset 名(default/green/blue/...)
* @returns {Promise<{ ok: boolean, source: string, applied: object }>}
*/
async applyThemeWithRemote({ state, commit, dispatch }, presetName) {
const finalPresetName = presetName || themeConfig.defaultTheme;
// 工具:订阅一次 presets-updated 后自动解绑
const subscribeOnce = (cb) => {
let called = false;
const handler = async () => {
if (called) return;
called = true;
try { uni.$off && uni.$off('theme:presets-updated', handler); } catch (e) {}
try { await cb(); } catch (e) { console.error('[theme] presets-updated reapply fail', e); }
};
try {
uni.$on && uni.$on('theme:presets-updated', handler);
} catch (e) {
console.warn('[theme] uni.$on bind fail', e);
}
return () => {
if (called) return;
called = true;
try { uni.$off && uni.$off('theme:presets-updated', handler); } catch (e) {}
};
};
// 工具:真正加载 + 合并 + 提交
// 关键: 调用前清空 configExternal.loadedConfigs 缓存,避免第一次拿到 stub 后
// 远端 preset 到达时 loadTheme 直接返回缓存而不读 ThemeOverride.
const doApply = async (name) => {
try {
if (configExternal && configExternal.loadedConfigs) {
delete configExternal.loadedConfigs[`theme_${name}`];
}
} catch (e) { /* 忽略 */ }
const base = await configExternal.loadTheme(name);
if (!base) return null;
const brandOverrides = ThemeOverride.getByTheme('brand') || {};
const merged = mergeThemeBuckets(
sanitizeFlatMap(base),
sanitizeFlatMap(brandOverrides)
);
commit('setThemeStyle', merged);
dispatch('themeColorSet');
return merged;
};
try {
// 1) 尝试同步加载
const first = await doApply(finalPresetName);
// 2) 加载失败(非 default preset 在 stub 中不存在)或 first 为空
// → 等远端 preset 拉完后重试
if (!first) {
console.log('[theme] preset 暂不可用,等待远端 preset 拉完:', finalPresetName);
// 不阻塞当前调用方,返回空结果(应用层会显示 stub default)
// 远端到后会自动重应用
subscribeOnce(async () => {
const retry = await doApply(finalPresetName);
if (retry) {
console.log('[theme] 远端 preset 到达,重应用成功:', finalPresetName);
} else {
console.warn('[theme] 远端 preset 到达后仍找不到:', finalPresetName);
}
});
return { ok: false, source: 'waiting-presets', applied: null };
}
// 3) 首次成功,但远端 preset 也许之后会到(用于其他 preset 的切换/覆写)
// 兜底订阅一次,如果远端 preset 包含了更全的 token 也能再补一次
subscribeOnce(async () => {
const retry = await doApply(finalPresetName);
if (retry) {
console.log('[theme] 远端 preset 补充,重应用:', finalPresetName);
}
});
return { ok: true, source: 'merged', applied: first };
} catch (e) {
console.error('[theme] applyThemeWithRemote 失败,回退到纯预设:', e);
// 兜底: 至少把预设应用了,避免页面无主题
try {
const base = await configExternal.loadTheme(finalPresetName);
if (base) {
commit('setThemeStyle', base);
dispatch('themeColorSet');
return { ok: false, source: 'preset-only-fallback', applied: base };
}
} catch (e2) {
console.error('[theme] 纯预设回退也失败:', e2);
}
return { ok: false, source: 'fail', applied: null };
}
}
}
})
export default store