- 新增隐私管理器核心逻辑,支持URL路由规则匹配、远程patch签名校验与融合 - 实现启动期资源等待机制,确保弹窗在i18n、主题配置就绪后触发 - 新增启动进度条组件,展示启动阶段进度与状态 - 重构隐私弹窗组件生命周期,修复事件丢失问题并支持规则动态重评估 - 适配多端平台隐私检查逻辑,支持微信小程序、鸿蒙元服务与H5 - 支持uniacid租户隔离的持久化规则存储
631 lines
21 KiB
JavaScript
631 lines
21 KiB
JavaScript
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 |