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