Files
lgp-wx-plus/mixins/theme-tab-bar.js
2026-08-19 08:17:18 +08:00

286 lines
8.1 KiB
JavaScript
Raw Permalink 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 { getActiveLayout, getActiveVars } from '@/utils/theme'
import { paintTabIconStroke } from '@/utils/tabbar-icons'
import {
clearTabTap,
currentTabIndex,
getLastSelected,
markTabTap,
peekNativeBar,
registerNativeTabBar,
registerPageTabBar,
setLastSelected,
takeTabTap,
getTabPages,
unregisterNativeTabBar,
unregisterPageTabBar,
} from '@/utils/tabbar'
/**
* 底栏壳层逻辑:官方 custom-tab-bar 与页内兜底共用
* 样式吃 chrome.tabbar动画吃 tabbar_anim两色吃独立配置
*/
export default {
data() {
return {
tabs: getTabPages(),
selected: 0,
animate: false,
strokeGrow: false,
strokeOk: false,
showVisual: false,
mode: 'plain',
anim: 'slide',
surface: '#ffffff',
colorOn: '#B08D57',
colorOff: '#8A8178',
border: '#E7DFD3',
}
},
computed: {
animMs() {
if (this.anim === 'none') return 0
if (this.anim === 'spring') return 800
if (this.anim === 'fade') return 520
return 640
},
animEase() {
return this.anim === 'spring'
? 'cubic-bezier(0.34, 1.45, 0.32, 1)'
: 'cubic-bezier(0.32, 0.72, 0, 1)'
},
moveInd() {
return this.mode !== 'plain' && (this.anim === 'slide' || this.anim === 'spring')
},
shellStyle() {
return {
background: this.surface || '#ffffff',
borderColor: this.border || '#E7DFD3',
color: this.colorOn || '#B08D57',
'--ttb-ms': this.animMs + 'ms',
'--ttb-ease': this.animEase,
}
},
slideStyle() {
const on = this.animate && this.moveInd
return {
transitionDuration: on ? this.animMs + 'ms' : '0ms',
transitionTimingFunction: this.animEase,
}
},
indStyle() {
const pos = this.selected < 0 ? 0 : this.selected
const width = 100 / Math.max(this.tabs.length, 1)
return Object.assign({}, this.slideStyle, {
width: width + '%',
transform: 'translate3d(' + pos * 100 + '%,0,0)',
})
},
indCoreStyle() {
if (this.mode === 'pill') {
return { background: this.hexAlpha(this.colorOn, 0.16) }
}
return { background: this.colorOn }
},
dotStyle() {
const pos = this.selected < 0 ? 0 : this.selected
const width = 100 / Math.max(this.tabs.length, 1)
return Object.assign({}, this.slideStyle, {
width: width + '%',
transform: 'translate3d(' + pos * 100 + '%,0,0)',
})
},
dotCoreStyle() {
return { background: this.colorOn }
},
},
created() {
this.pullTheme()
if (this.isNative) {
this.showVisual = true
registerNativeTabBar(this)
const now = currentTabIndex()
this.selected = now >= 0 ? now : 0
this.animate = false
setLastSelected(this.selected)
return
}
registerPageTabBar(this)
this.selected = getLastSelected()
this.animate = false
this.showVisual = !peekNativeBar()
},
mounted() {
if (this.isNative) return
const self = this
setTimeout(() => {
self.showVisual = !peekNativeBar()
}, 80)
setTimeout(() => {
self.showVisual = !peekNativeBar()
}, 240)
},
beforeUnmount() {
this.unbindBar()
},
beforeDestroy() {
this.unbindBar()
},
methods: {
unbindBar() {
this.clearAnimTimers()
if (this.isNative) unregisterNativeTabBar(this)
else unregisterPageTabBar(this)
},
pullTheme() {
try {
this.tabs = getTabPages()
const vars = getActiveVars() || {}
const layout = getActiveLayout() || {}
const chrome = layout.chrome || {}
this.mode = chrome.tabbar || 'plain'
this.anim = chrome.tabbar_anim || 'slide'
this.surface = vars['--color-surface'] || '#ffffff'
this.border = vars['--color-border'] || '#E7DFD3'
this.colorOff = chrome.tabbar_color || vars['--color-text-soft'] || '#8A8178'
this.colorOn = chrome.tabbar_color_on || vars['--color-primary'] || '#B08D57'
} catch (err) {
this.mode = 'plain'
this.anim = 'slide'
}
},
itemOn(index) {
return this.selected === index
},
/**
* 点选且有动画时才沿图标轮廓描边;圆点态选中不显示图标,不描
*/
isDraw(index) {
return this.itemOn(index) && this.animate && this.mode !== 'dot'
},
textColor(index) {
return this.itemOn(index) ? this.colorOn : this.colorOff
},
stopIconStroke() {
if (this._strokeLoop) clearTimeout(this._strokeLoop)
this._strokeLoop = 0
},
/** 官方栏和页内兜底不能共用 canvas-id否则会报 ttbCv 已存在 */
canvasId(index) {
return (this.isNative ? 'ttbNv' : 'ttbPg') + index
},
clearStrokeCanvas(index) {
try {
const ctx = uni.createCanvasContext(this.canvasId(index), this)
ctx.clearRect(0, 0, 28, 28)
ctx.draw()
} catch (err) {
/* 栏未挂 canvas 时忽略 */
}
},
/**
* 按房子 / 单据夹 / 头像的路径一笔描出来,颜色跟选中色
*/
playIconStroke(index) {
this.stopIconStroke()
this.strokeOk = false
if (this.mode === 'dot') return
const item = this.tabs[index]
if (!item) return
const key = item.key
const color = this.colorOn
const start = Date.now()
const dur = Math.max(this.animMs * 0.85, 520)
const self = this
const tick = () => {
const t = Math.min(1, (Date.now() - start) / dur)
try {
const ctx = uni.createCanvasContext(self.canvasId(index), self)
ctx.clearRect(0, 0, 28, 28)
paintTabIconStroke(ctx, key, t, color, 28)
ctx.draw()
self.strokeOk = true
} catch (err) {
return
}
if (t < 1) self._strokeLoop = setTimeout(tick, 16)
}
tick()
},
clearAnimTimers() {
this.stopIconStroke()
if (this._applyOn) clearTimeout(this._applyOn)
if (this._strokeOn) clearTimeout(this._strokeOn)
if (this._animOff) clearTimeout(this._animOff)
this._applyOn = 0
this._strokeOn = 0
this._animOff = 0
},
/**
* 对齐栈顶路由后再播。
* 页面 onShow 和官方栏 show 会连打两次:只有第一次带点击,第二次不许把动画掐掉。
* 先把 duration 归零停一帧,再开动画改 selected每次切换才能滑/描。
*/
syncFromRoute(index) {
if (index < 0) return
this.pullTheme()
const fromTap = takeTabTap(index)
if (!fromTap) {
setLastSelected(index)
if (index !== this.selected && !this._applyOn) {
this.animate = false
this.strokeGrow = false
this.selected = index
}
return
}
this.clearAnimTimers()
this.animate = false
this.strokeGrow = false
if (this.anim === 'none') {
this.selected = index
setLastSelected(index)
return
}
const self = this
this._applyOn = setTimeout(() => {
self._applyOn = 0
self.selected = index
setLastSelected(index)
self.animate = true
self.$nextTick(() => {
self.playIconStroke(index)
})
self._animOff = setTimeout(() => {
self.animate = false
self.strokeGrow = false
self.stopIconStroke()
self.clearStrokeCanvas(index)
}, Math.max(self.animMs, 720) + 280)
}, 24)
},
onTap(index) {
const now = currentTabIndex()
if (now === index) return
markTabTap(now, index)
uni.switchTab({
url: '/' + this.tabs[index].pagePath,
fail() {
clearTabTap()
},
})
},
hexAlpha(hex, alpha) {
let raw = String(hex || '').replace('#', '')
if (raw.length === 3) {
raw = raw[0] + raw[0] + raw[1] + raw[1] + raw[2] + raw[2]
}
if (raw.length !== 6) return 'rgba(176,141,87,' + alpha + ')'
const n = parseInt(raw, 16)
const r = (n >> 16) & 255
const g = (n >> 8) & 255
const b = n & 255
return 'rgba(' + r + ',' + g + ',' + b + ',' + alpha + ')'
},
},
}