import { request } from '../utils/request' import * as auth from '../utils/auth' import { STORAGE_KEYS } from '../utils/config' import { logger } from '../utils/logger' const LOG = 'tabbar' // 未读红点轮询最小间隔,避免频繁切 tab 时重复打接口 const SYNC_INTERVAL = 30 * 1000 let lastSyncAt = 0 Component({ data: { selected: 0, unread: 0, // 弹层打开时页面调用 setHidden(true) 淡出,避免悬浮标签栏压在弹层之上 hidden: false, list: [ { pagePath: '/pages/wardrobe/wardrobe', text: '衣橱', icon: 'icon-hanger' }, { pagePath: '/pages/outfit/outfit', text: '穿搭', icon: 'icon-shirt' }, { pagePath: '/pages/horoscope/horoscope', text: '运势', icon: 'icon-sparkles' }, { pagePath: '/pages/closet-share/closet-share', text: '分享', icon: 'icon-share' }, { pagePath: '/pages/profile/profile', text: '我的', icon: 'icon-user' }, ], }, lifetimes: { attached() { // 首帧渲染流程(FLOW_INITIAL_CREATION)还没结束就 setData,渲染层会抛 // 「Expected updated data but get first rendering data」/ // 「expect FLOW_INITIAL_CREATION end but get FLOW_CREATE_NODE」。 // 因此首帧内产生的更新先暂存,等首帧流程结束后再一次性提交。 ;(this as any).rendered = false ;(this as any).pending = {} // 每个 tab 页都有独立的 tabBar 实例,直接按当前页面路由推导选中态, // 页面侧无需再在 onShow 里回写 selected(那一步正是首帧冲突的来源) const index = this.matchRoute() if (index >= 0) (this as any).pending.selected = index const cached = (wx.getStorageSync(STORAGE_KEYS.UNREAD) as number) || 0 if (cached) (this as any).pending.unread = cached wx.nextTick(() => { ;(this as any).rendered = true const pending = (this as any).pending ;(this as any).pending = null if (pending && Object.keys(pending).length) this.setData(pending) // 未登录时 syncUnread 内部只清零不发请求,不会批量 401 this.syncUnread() }) }, }, methods: { // 当前页面路由在 list 中的下标,非 tab 页返回 -1 matchRoute(): number { const pages = typeof getCurrentPages === 'function' ? getCurrentPages() : [] const route = pages.length ? (pages[pages.length - 1] as any).route : '' if (!route) return -1 return this.data.list.findIndex((item) => item.pagePath === `/${route}`) }, // 统一出口:首帧未结束时先合并暂存,避免打断渲染层首帧流程 applyData(data: Record) { if ((this as any).rendered) { this.setData(data) return } const pending = (this as any).pending if (pending) Object.assign(pending, data) else this.setData(data) }, // 页面切换选中态(供页面调用,内部保证首帧安全) setSelected(index: number) { this.applyData({ selected: index }) }, // 弹层打开/关闭时淡出或恢复悬浮标签栏(供页面调用,内部保证首帧安全) setHidden(hidden: boolean) { const next = !!hidden // 值未变化时不触发渲染:页面每次 onShow 都会校准一次,避免无谓的 setData if (next === !!this.data.hidden) return this.applyData({ hidden: next }) }, // 直接更新红点(供页面在已知数量时调用,不再触发二次请求) setUnread(n: number) { const unread = Number(n) || 0 wx.setStorageSync(STORAGE_KEYS.UNREAD, unread) this.applyData({ unread }) }, // 拉取未读数:仅登录态才请求,且做节流 syncUnread(force?: boolean) { if (!auth.isLoggedIn()) { logger.debug(LOG, '未登录,红点置 0') this.setUnread(0) return } const now = Date.now() if (!force && now - lastSyncAt < SYNC_INTERVAL) { logger.debug(LOG, '红点轮询命中节流,跳过') return } lastSyncAt = now request<{ unread_count: number }>({ url: '/notification/unread', showError: false, silentAuth: true, }) .then((res) => { logger.debug(LOG, '红点同步成功, unread=', res.unread_count) this.setUnread(res.unread_count || 0) }) .catch((e: any) => { // 请求失败时回滚节流时间戳,否则要白等 30s 才能重试 lastSyncAt = 0 logger.warn(LOG, '红点同步失败, code=', e && e.code) }) }, switchTab(e: WechatMiniprogram.TouchEvent) { const { path, index } = e.currentTarget.dataset as { path: string; index: number } logger.debug(LOG, 'switchTab, path=', path, 'index=', index) wx.switchTab({ url: path, fail: (err: any) => logger.warn(LOG, 'switchTab 失败, path=', path, 'err=', err && err.errMsg), }) this.setSelected(Number(index) || 0) }, }, })