Files
crmeb-mp/App.vue
Karlcarlo b45a190a59 chore: 入库生产基线(2026-10-26 工作树快照)
- 含首页壳/容器页/授权页/分享图/首页配置等此前未入库的在途改动
- 新增未跟踪的 pages/index/mall.vue(第 5 个 tabBar 页,pages.json 已引用)
- 排除本地备份文件(*.bak* / *.origin-bak)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-26 17:07:49 +08:00

481 lines
19 KiB
Vue
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
<script>
import {checkLogin} from './libs/login';
import {HTTP_REQUEST_URL} from './config/app';
import { LOGIN_STATUS, SUBSCRIBE_MESSAGE } from "./config/cache.js"
import { getShopConfig, silenceAuth, getLogo, remoteRegister } from '@/api/public';
import { silenceBindingSpread } from '@/utils';
import Auth from '@/libs/wechat.js';
import Routine from './libs/routine.js';
import { colorChange, copyRight } from '@/api/api.js';
import { getUserInfo } from "@/api/user.js"
import { mapGetters } from "vuex"
// #ifdef MP
// let livePlayer = requirePlugin('live-player-plugin')
// #endif
export default {
globalData: {
spid: 0,
code: 0,
isLogin: false,
userInfo: {},
globalData: false,
appShowAt: 0, // 本次前台化时刻(首页壳据此区分热启动恢复 / 切 tab)
restoreArmed: false, // 冷启动是否允许首页壳恢复「上次进入的 H5 应用」(见 onLaunch)
windowHeight: uni.getWindowInfo().windowHeight + 'px',
sysHeight:uni.getWindowInfo().statusBarHeight,
},
computed: mapGetters(['isLogin', 'cartNum']),
watch: {
cartNum(newCart, b) {
this.$store.commit('indexData/setCartNum', newCart + '')
if (newCart > 0) {
uni.setTabBarBadge({
index: 3,
text: newCart>99?'99+':newCart+''
})
} else {
uni.hideTabBarRedDot({
index: 3
})
}
}
},
onShow(options) {
/* 记录本次「小程序回到前台」的时刻(2026-09-22):首页壳 pages/index/index
靠它区分两种 onShow ——
· 热启动恢复(点胶囊关闭后再进):页面栈与 web-view 都还在,
用户就停在离开时那个 H5 应用里 → **绝不能重载 web-view**;
· 从其它 tab 切回首页:App.onShow **不会**触发 → 维持原行为(重载回兜爸心选首页)。
判据是时间窗:页面 onShow 紧跟 App.onShow 触发(毫秒级),切 tab 不在窗内。 */
try { this.globalData.appShowAt = Date.now(); } catch (e) {}
/* 诊断日志(过滤关键字 enter-app):
· onShow 触发 = 小程序回到前台(热启动恢复或冷启动都会走),
首页壳据此判断要不要重载 web-view;切 tab 不会打这条。 */
console.log('[enter-app] App.onShow(前台化)appShowAt=' + this.globalData.appShowAt +
'(首页壳 onShow 将据此识别热启动)');
const queryData = uni.getEnterOptionsSync(); // uni-app版本 3.5.1+ 支持
if (queryData.query.spid) {
this.$Cache.set('spread', queryData.query.spid);
this.globalData.spid = queryData.query.spid;
this.globalData.pid = queryData.query.spid;
silenceBindingSpread(this.globalData);
}
// #ifdef MP
if (queryData.query.scene) {
let param = this.$util.getUrlParams(decodeURIComponent(queryData.query.scene));
if (param.spid) {
this.$Cache.set('spread', param.spid);
this.globalData.spid = param.spid;
}
/** 直播间分享**/
// const sceneList = [1007, 1008, 1014, 1044, 1045, 1046, 1047, 1048, 1049, 1073, 1154, 1155];
// if (sceneList.includes(queryData.query.scene)) {
// livePlayer.getShareParams().then(res => {
// //记录推广人uid
// if(res.custom_params.pid){
// this.$Cache.set('spread', res.custom_params.pid);
// this.globalData.spid = res.custom_params.pid;
// }
// }).catch(err => {
// })
// }
silenceBindingSpread(this.globalData);
}
// #endif
},
onLaunch(option) {
/* 冷启动入口判定(2026-09-22):**只有「落在首页/引导页且没有显式落地 url」才允许
首页壳恢复上次应用**。否则(扫码进容器页 / 分享落地 / 朋友圈 / 商品页…)用户是冲着
某个页面来的;若此时在小程序内点左上角「小房子」回首页,恢复逻辑又会把他送回刚才那个
应用 —— 形成「点了小房子却回不了首页」的循环(用户实测:扫码→小房子→被送回应用→
多了个返回箭头→再点返回才到家)。
⚠️ 只在 onLaunch 置位(热启动/切 tab 不置),首页壳 onLoad 时一次性消费。
⚠️ 用 path 判据而不是时间窗:引导页可能停很久(看开屏广告),时间窗会误判。 */
try {
const _opt = option || {};
const _path = _opt.path || '';
const _q = _opt.query || {};
const _plainHome = (!_path || _path === 'pages/index/index' || _path === 'pages/guide/index') && !_q.url;
this.globalData.restoreArmed = _plainHome;
console.log('[enter-app] App.onLaunch(冷启动)path=' + _path +
' query=' + JSON.stringify(_q) +
' scene=' + (_opt.scene || '') +
' → restoreArmed=' + _plainHome + '(false=显式落地,首页壳不会恢复上次应用)');
} catch (e) {}
uni.hideTabBar()
//#ifdef APP
plus.screen.lockOrientation("portrait-primary");
//#endif
let that = this;
//获取并存储配置
this.setConfig();
//主题换色
this.setTheme();
// 通过vuex获取并储存公共配置
this.$store.dispatch("getBasicConfig");
// 通过vuex获取并储存底部菜单的数据
this.$store.dispatch("getPageFooter");
// 通过vuex获取并储存活动弹窗的数据
this.$store.dispatch("getActivityModal");
// #ifdef H5
if (!this.isLogin) {
if (option.query && option.query.hasOwnProperty('remote_token')) {
this.remoteRegister(option.query.remote_token);
} else {
let token = option.query && option.query.token ? option.query.token : '';
if (!token) token = this.getTokenFromHash();
if (token) this.loginByToken(token);
}
}
this.setScript();
const queryData = uni.getEnterOptionsSync();
uni.getSystemInfo({
success(e) {
/* 窗口宽度大于420px且不在PC页面且不在移动设备时跳转至 PC.html 页面 */
if (e.windowWidth > 420 && !window.top.isPC && !/iOS|Android/i.test(e.system)) {
window.location.pathname = '/static/html/pc.html';
}
}
});
// #endif
// #ifdef MP
// 小程序静默授权
console.log('[auth-flow] App onLaunch: isLogin=' + this.$store.getters.isLogin);
if (!this.$store.getters.isLogin) {
Routine.getCode().then(code => {
console.log('[auth-flow] App onLaunch: wx.login 拿到 code=' + (code ? code.substring(0, 10) + '...' : '(空)'));
this.silenceAuth(code);
}).catch(res => {
console.warn('[auth-flow] App onLaunch: wx.login 失败', res);
uni.hideLoading();
});
}else if(this.$store.getters.isLogin && this.getExpired()){
//处理小程序有token情况下但是静默授权失败
this.$Cache.clear(LOGIN_STATUS);
Routine.getCode().then(code => {
console.log('[auth-flow] App onLaunch: token过期,重新静默授权 code=' + (code ? code.substring(0, 10) + '...' : '(空)'));
this.silenceAuth(code);
})
}
// #endif
uni.setStorageSync('DEBUG_DIY_RENDER', true)
},
methods: {
getTokenFromHash() {
try {
let hash = window.location.hash || '';
let qIndex = hash.indexOf('?');
if (qIndex === -1) return '';
let queryStr = hash.slice(qIndex + 1);
let parts = queryStr.split('&');
for (let i = 0; i < parts.length; i++) {
if (!parts[i]) continue;
let eqIndex = parts[i].indexOf('=');
let k = eqIndex === -1 ? parts[i] : parts[i].slice(0, eqIndex);
if (k === 'token') {
let v = eqIndex === -1 ? '' : parts[i].slice(eqIndex + 1);
return decodeURIComponent(v);
}
}
return '';
} catch (e) {
return '';
}
},
cleanTokenFromUrl() {
try {
let href = window.location.href || '';
if (href.indexOf('token=') === -1) return;
let urlObj = new URL(href);
if (urlObj.searchParams && urlObj.searchParams.has('token')) {
urlObj.searchParams.delete('token');
let search = urlObj.searchParams.toString();
history.replaceState(null, '', urlObj.pathname + (search ? '?' + search : '') + urlObj.hash);
return;
}
} catch (e) {}
try {
let hash = window.location.hash || '';
let qIndex = hash.indexOf('?');
if (qIndex === -1) return;
let hashPath = hash.slice(1, qIndex);
let queryStr = hash.slice(qIndex + 1);
let parts = queryStr.split('&').filter(Boolean);
let newParts = [];
for (let i = 0; i < parts.length; i++) {
let eqIndex = parts[i].indexOf('=');
let k = eqIndex === -1 ? parts[i] : parts[i].slice(0, eqIndex);
if (k !== 'token') newParts.push(parts[i]);
}
let newHash = '#' + hashPath + (newParts.length ? '?' + newParts.join('&') : '');
history.replaceState(null, '', window.location.pathname + window.location.search + newHash);
} catch (e) {}
},
loginByToken(token) {
this.$store.commit("LOGIN", {
'token': token,
'time': 0
});
// 从 JWT payload 中立即提取 uid,无需等 getUserInfo
const payload = this.$util.parseJwt(token);
if (payload) {
if (payload.uid) this.$store.commit("SETUID", payload.uid);
// phone 和 openid 也可从 JWT 读取
// 后端需在签发 JWT 时加入 uid/phone/openid claims
}
getUserInfo().then(res => {
this.$store.commit("SETUID", res.data.uid);
this.$store.commit("UPDATE_USERINFO", res.data);
this.cleanTokenFromUrl();
}).catch(() => {
this.cleanTokenFromUrl();
})
},
remoteRegister(remote_token) {
remoteRegister({ remote_token }).then((res) => {
let data = res.data;
if (data.get_remote_login_url) {
location.href = data.get_remote_login_url
} else {
this.$store.commit("LOGIN", {
'token': data.token,
'time': data.expires_time - this.$Cache.time()
});
getUserInfo().then(res => {
this.$store.commit("SETUID", res.data.uid);
this.$store.commit("UPDATE_USERINFO", res.data);
location.reload()
}).catch(error => {
return this.$util.Tips({
title:err
})
})
}
});
},
// 小程序静默授权
silenceAuth(code) {
let that = this;
let spid = that.globalData.spid ? that.globalData.spid : '';
console.log('[auth-flow] silenceAuth 发起, code=' + (code ? code.substring(0, 10) + '...' : '(空)'));
const authPromise = silenceAuth({
code: code,
spread_spid: spid,
spread_code: that.globalData.code
})
.then(res => {
console.log('[auth-flow] silenceAuth 响应:', JSON.stringify(res.data || res).substring(0, 200));
if (res.data.token !== undefined && res.data.token) {
uni.hideLoading();
let time = res.data.expires_time - this.$Cache.time();
that.$store.commit('LOGIN', {
token: res.data.token,
time: time
});
/* ⚠️ 2026-09-20:`userInfo` 不是每个响应都有(v2/wechat/silence_auth
只回 token+expires_time),直接取 `.uid` 会抛异常,把后面的
commit 一起带走。加一层判空,缺了也不影响身份(openid 由 LOGIN 从 JWT 解出)。 */
if (res.data.userInfo) {
that.$store.commit('SETUID', res.data.userInfo.uid);
that.$store.commit('UPDATE_USERINFO', res.data.userInfo);
}
console.log('[auth-flow] ✅ silenceAuth 成功, token 已存 store (含 openid)');
} else if (res.data.openid) {
/* ⚠️⚠️ 2026-09-20 修:新用户 / 后台开启了「强制绑定手机号」时,
该接口返回的是 {auth_login:1, key, openid, unionid} —— **有 openid、没有 token**。
以前这段落到下面的 else,只打一条警告就把 openid 丢了;于是
store.openid / 缓存 OPENID / JWT 三处全空,首页壳 _hostOpenid() 拿不到身份
→ **匿名加载 mama-box**(H5 进访客态:孕周 `--`、档案空、跳应用跳裸 URL)。
这里必须把它落 store + 缓存。 */
that.$store.commit('SET_OPENID', res.data.openid);
try { uni.setStorageSync('OPENID', res.data.openid); } catch (e) {}
if (res.data.unionid) {
that.$store.commit('SET_UNIONID', res.data.unionid);
try { uni.setStorageSync('UNIONID', res.data.unionid); } catch (e) {}
}
console.log('[auth-flow] ✅ 无 token 但拿到 openid,已缓存(首页壳据此注入身份): ' + String(res.data.openid).substring(0, 8) + '...');
} else {
console.warn('[auth-flow] ⚠️ silenceAuth 返回无 token 也无 openid:', JSON.stringify(res.data || res).substring(0, 200));
}
}).catch(err => {
console.warn('[auth-flow] ❌ silenceAuth 失败:', JSON.stringify(err).substring(0, 300));
return that.$util.Tips({
title: err
})
});
/* ⚠️ 2026-09-20:把这次静默登录的 promise 挂到 globalData,
让宿主页(首页壳 / 容器页)在缓存未命中时**先 await 它**、而不是自己再换一次 code
(见 mixins/webviewHost.js 的 _awaitHostLogin)。
动机:微信 code 一次性,两边各换一次要多花 200–300ms(实测 code2session 就这么贵),
而 openid 本来就该由这一条链路落缓存(本文件上面的 SET_OPENID 分支 / LOGIN 从 JWT 提取)。
settle 后清掉,避免下次误等一个已经结束的 promise。 */
try { that.globalData._silenceAuthPromise = authPromise; } catch (e) {}
authPromise.then(() => {
try { if (that.globalData._silenceAuthPromise === authPromise) that.globalData._silenceAuthPromise = null; } catch (e) {}
});
return authPromise;
},
/**
* 检测当前的小程序
* 是否是最新版本,是否需要下载、更新
*/
checkUpdateVersion() {
//判断微信版本是否 兼容小程序更新机制API的使用
if (wx.canIUse('getUpdateManager')) {
const updateManager = wx.getUpdateManager();
//检测版本更新
updateManager.onCheckForUpdate(function(res) {
if (res.hasUpdate) {
updateManager.onUpdateReady(function() {
wx.showModal({
title: '温馨提示',
content: '检测到新版本,是否重启小程序?',
showCancel: false,
success: function(res) {
if (res.confirm) {
// 新的版本已经下载好,调用 applyUpdate 应用新版本并重启
updateManager.applyUpdate()
}
}
})
})
updateManager.onUpdateFailed(function() {
// 新版本下载失败
wx.showModal({
title: '已有新版本',
content: '请您删除小程序,重新搜索进入',
})
})
}
})
} else {
wx.showModal({
title: '溫馨提示',
content: '当前微信版本过低,无法使用该功能,请升级到最新微信版本后重试。'
})
}
},
async getExpired(){
// 通过一个个人中心的接口判断token是否生效,catch捕获到就证明token过期,返回true
try {
await getUserInfo();
return false; // token有效
} catch (err) {
return true; // token过期
}
},
setConfig(){
uni.removeStorageSync(SUBSCRIBE_MESSAGE);
this.$Cache.clear('homeTop');
let that = this;
// #ifdef MP
if (HTTP_REQUEST_URL == '') {
uni.showToast({
title: "请配置根目录下的config/app.js文件中的HTTP_REQUEST_URL",
icon:'error'
})
}
this.checkUpdateVersion();
let menuButtonInfo = uni.getMenuButtonBoundingClientRect();
that.globalData.navH = menuButtonInfo.top * 2 + menuButtonInfo.height / 2;
const version = wx.getAppBaseInfo().SDKVersion;
if (Routine.compareVersion(version, '2.21.2') >= 0) {
that.$Cache.set('MP_VERSION_ISNEW', true)
} else {
that.$Cache.set('MP_VERSION_ISNEW', false)
}
// #endif
getLogo().then(res => {
uni.setStorageSync('BASIC_CONFIG', res.data)
});
},
setTheme(){
let green =
'--view-theme: #42CA4D;--view-minorColorT:rgba(66, 202, 77, 0.1);--view-bntColor:#FE960F;--view-gradient:#4DEA4D'
let red =
'--view-theme: #e93323;--view-minorColorT:rgba(233, 51, 35, 0.1);--view-bntColor:#FE960F;--view-gradient:#FF7931'
let blue =
'--view-theme: #1DB0FC;--view-minorColorT:rgba(9, 139, 243, 0.1);--view-bntColor:#22CAFD;--view-gradient:#5ACBFF'
let pink =
'--view-theme: #FF448F;--view-minorColorT:rgba(255, 68, 143, 0.1);--view-bntColor:#282828;--view-gradient:#FF67AD'
let orange =
'--view-theme: #FE5C2D;--view-minorColorT:rgba(254, 92, 45, 0.1);--view-bntColor:#FDB000;--view-gradient:#FF9451'
let gold =
'--view-theme: #E0A558;--view-minorColorT:rgba(224, 165, 88, 0.1);--view-bntColor:#1A1A1A;--view-gradient:#FFCD8C'
colorChange('color_change').then(res => {
let statusColor = res.data.status; //判断显示啥颜色
let themeType = res.data.color_data?.theme_type || 1;
if(themeType == 1){
uni.setStorageSync('statusColor', statusColor);
uni.$emit('colorOk', statusColor);
let themeList = {
1:blue, 2:green, 3:red, 4:pink, 5:orange, 6:gold
};
Object.entries(themeList).forEach(([key, themeSelect]) => {
if (key == statusColor) {
uni.setStorageSync('viewColor', themeSelect);
uni.$emit('ok', themeSelect);
}
});
}else{
let colorData = res.data.color_data;
let themeStr = `--view-theme: ${colorData.theme_color};--view-minorColorT:${colorData.light_color};--view-bntColor:${colorData.aux_color};--view-gradient:${colorData.gradient_color}`;
uni.setStorageSync('viewColor', themeStr);
uni.$emit('ok', themeStr);
}
});
},
setScript(){
// 添加crmeb chat 统计
var __s = document.createElement('script');
__s.src = `${HTTP_REQUEST_URL}/api/get_script`;
document.head.appendChild(__s);
},
},
};
</script>
<style lang="scss">
/* #ifndef APP-PLUS-NVUE || APP-NVUE */
@import url('@/plugin/emoji-awesome/css/tuoluojiang.css');
@import 'static/css/base.css';
@import 'static/iconfont/iconfont.css';
@import 'static/css/unocss.css';
view {
box-sizing: border-box;
}
page {
font-family: PingFang SC;
}
.placeholder{
color: #ccc;
}
.mer-bg{
background-color: $primary-merchant;
}
.uni-scroll-view::-webkit-scrollbar {
/* 隐藏滚动条,但依旧具备可以滚动的功能 */
display: none;
}
::-webkit-scrollbar {
width: 0;
height: 0;
color: transparent;
}
.uni-system-open-location .map-content.fix-position {
height: 100vh;
top: 0;
bottom: 0;
}
/* #endif */
</style>