Files
lucky_shop/store/index.js
ZF sun a913961be5 feat: 实现隐私弹窗全链路功能,支持本地配置与远程patch融合
- 新增隐私管理器核心逻辑,支持URL路由规则匹配、远程patch签名校验与融合
- 实现启动期资源等待机制,确保弹窗在i18n、主题配置就绪后触发
- 新增启动进度条组件,展示启动阶段进度与状态
- 重构隐私弹窗组件生命周期,修复事件丢失问题并支持规则动态重评估
- 适配多端平台隐私检查逻辑,支持微信小程序、鸿蒙元服务与H5
- 支持uniacid租户隔离的持久化规则存储
2026-07-11 18:28:36 +08:00

631 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' }
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