286 lines
8.1 KiB
JavaScript
286 lines
8.1 KiB
JavaScript
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 + ')'
|
||
},
|
||
},
|
||
}
|