feat(JlsBottomNav): 优化底部导航,新增自定义激活色与滚动提示
- 移除未使用的ElMessage自动导入配置 - 调整导航栏默认高度从80px改为88px - 新增颜色处理工具函数,支持读取接口或mock数据中的激活态颜色 - 重构组件模板与样式,优化滚动布局并添加滚动提示动画 - 完善小程序跳转逻辑,区分首页与其他页面的跳转方式 - 更新组件文档与mock测试数据
Showing
8 changed files
with
238 additions
and
66 deletions
| ... | @@ -2,7 +2,6 @@ | ... | @@ -2,7 +2,6 @@ |
| 2 | export {} | 2 | export {} |
| 3 | declare global { | 3 | declare global { |
| 4 | const EffectScope: typeof import('vue')['EffectScope'] | 4 | const EffectScope: typeof import('vue')['EffectScope'] |
| 5 | - const ElMessage: typeof import('element-plus/es')['ElMessage'] | ||
| 6 | const computed: typeof import('vue')['computed'] | 5 | const computed: typeof import('vue')['computed'] |
| 7 | const createApp: typeof import('vue')['createApp'] | 6 | const createApp: typeof import('vue')['createApp'] |
| 8 | const customRef: typeof import('vue')['customRef'] | 7 | const customRef: typeof import('vue')['customRef'] | ... | ... |
| 1 | <template> | 1 | <template> |
| 2 | <div v-if="visible" class="jls-bottom-nav"> | 2 | <div v-if="visible" class="jls-bottom-nav"> |
| 3 | - <div ref="panelRef" class="jls-bottom-nav__panel" @scroll="syncScrollState"> | 3 | + <div class="jls-bottom-nav__placeholder" /> |
| 4 | - <div class="jls-bottom-nav__content" :class="{ 'is-scrollable': isScrollable }"> | 4 | + |
| 5 | - <button | 5 | + <div class="jls-bottom-nav__wrap"> |
| 6 | - v-for="item in tabItems" | 6 | + <div ref="panelRef" class="jls-bottom-nav__panel" @scroll="handlePanelScroll"> |
| 7 | - :key="item.key" | 7 | + <div |
| 8 | - type="button" | 8 | + class="jls-bottom-nav__content" |
| 9 | - class="jls-bottom-nav__item" | 9 | + :class="{ 'is-scrollable': isScrollable }" |
| 10 | - :class="{ 'is-active': isActive(item.key), 'is-scrollable': isScrollable }" | 10 | + :style="contentStyle" |
| 11 | - :style="itemStyle" | ||
| 12 | - @click="navigate(item)" | ||
| 13 | > | 11 | > |
| 14 | - <span class="jls-bottom-nav__item-inner"> | 12 | + <button |
| 15 | - <span class="jls-bottom-nav__icon"> | 13 | + v-for="item in tabItems" |
| 16 | - <i class="jls-bottom-nav__icon-font" :class="getIconClasses(item)" aria-hidden="true"></i> | 14 | + :key="item.key" |
| 15 | + type="button" | ||
| 16 | + class="jls-bottom-nav__item" | ||
| 17 | + :class="{ 'is-active': isActive(item.key), 'is-scrollable': isScrollable }" | ||
| 18 | + :style="itemStyle" | ||
| 19 | + @click="navigate(item)" | ||
| 20 | + > | ||
| 21 | + <span class="jls-bottom-nav__item-inner" :style="getItemInnerStyle(item)"> | ||
| 22 | + <span class="jls-bottom-nav__icon"> | ||
| 23 | + <i class="jls-bottom-nav__icon-font" :class="getIconClasses(item)" aria-hidden="true"></i> | ||
| 24 | + </span> | ||
| 25 | + <span class="jls-bottom-nav__label">{{ item.title }}</span> | ||
| 17 | </span> | 26 | </span> |
| 18 | - <span class="jls-bottom-nav__label">{{ item.title }}</span> | 27 | + </button> |
| 19 | - </span> | 28 | + </div> |
| 20 | - </button> | ||
| 21 | </div> | 29 | </div> |
| 22 | - </div> | ||
| 23 | 30 | ||
| 24 | - <div v-if="showScrollFade" class="jls-bottom-nav__fade" /> | 31 | + <div v-if="showScrollHint" class="jls-bottom-nav__fade"> |
| 32 | + <div v-if="showScrollArrow" class="jls-bottom-nav__hint-arrow" /> | ||
| 33 | + </div> | ||
| 34 | + </div> | ||
| 25 | </div> | 35 | </div> |
| 26 | </template> | 36 | </template> |
| 27 | 37 | ||
| 28 | <script setup> | 38 | <script setup> |
| 29 | -import { computed, nextTick, onMounted, ref, watch } from 'vue'; | 39 | +import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'; |
| 30 | import { useRoute } from 'vue-router'; | 40 | import { useRoute } from 'vue-router'; |
| 31 | import wx from 'weixin-js-sdk'; | 41 | import wx from 'weixin-js-sdk'; |
| 32 | import './iconfont.css'; | 42 | import './iconfont.css'; |
| ... | @@ -34,7 +44,10 @@ import { useJlsTabbar } from './useTabbar'; | ... | @@ -34,7 +44,10 @@ import { useJlsTabbar } from './useTabbar'; |
| 34 | import { getTabbarIconClasses } from './tabbar.utils'; | 44 | import { getTabbarIconClasses } from './tabbar.utils'; |
| 35 | import { resolveJlsCheckinActiveTab } from './nav-state'; | 45 | import { resolveJlsCheckinActiveTab } from './nav-state'; |
| 36 | 46 | ||
| 37 | -const SCROLLABLE_ITEM_WIDTH = 92; | 47 | +const SCROLLABLE_ITEM_WIDTH = 108; |
| 48 | +const SCROLLABLE_SIDE_PADDING = 24; | ||
| 49 | +const SCROLL_HINT_STORAGE_KEY = 'jls_bottom_nav_scroll_hint_seen_v2'; | ||
| 50 | +const SCROLL_HINT_OFFSET = 36; | ||
| 38 | const defaultLoadOptions = Object.freeze({ | 51 | const defaultLoadOptions = Object.freeze({ |
| 39 | useMock: false, | 52 | useMock: false, |
| 40 | mockData: null, | 53 | mockData: null, |
| ... | @@ -57,10 +70,25 @@ const props = defineProps({ | ... | @@ -57,10 +70,25 @@ const props = defineProps({ |
| 57 | const route = useRoute(); | 70 | const route = useRoute(); |
| 58 | const panelRef = ref(null); | 71 | const panelRef = ref(null); |
| 59 | const isScrolledToEnd = ref(false); | 72 | const isScrolledToEnd = ref(false); |
| 73 | +const hasPlayedScrollHint = ref(false); | ||
| 74 | +const hasUserScrolled = ref(false); | ||
| 75 | +const isPlayingAutoScrollHint = ref(false); | ||
| 60 | const tabbar = useJlsTabbar(); | 76 | const tabbar = useJlsTabbar(); |
| 61 | 77 | ||
| 62 | const tabItems = computed(() => tabbar.visibleTabItems.value); | 78 | const tabItems = computed(() => tabbar.visibleTabItems.value); |
| 79 | +const activeColor = computed(() => tabbar.activeColor.value); | ||
| 63 | const isScrollable = computed(() => tabItems.value.length > 4); | 80 | const isScrollable = computed(() => tabItems.value.length > 4); |
| 81 | +const showScrollHint = computed(() => isScrollable.value && !isScrolledToEnd.value); | ||
| 82 | +const showScrollArrow = computed(() => showScrollHint.value && !hasUserScrolled.value); | ||
| 83 | +const contentStyle = computed(() => { | ||
| 84 | + if (!isScrollable.value) { | ||
| 85 | + return null; | ||
| 86 | + } | ||
| 87 | + | ||
| 88 | + return { | ||
| 89 | + width: `${(tabItems.value.length * SCROLLABLE_ITEM_WIDTH) + SCROLLABLE_SIDE_PADDING}px`, | ||
| 90 | + }; | ||
| 91 | +}); | ||
| 64 | const itemStyle = computed(() => { | 92 | const itemStyle = computed(() => { |
| 65 | if (!isScrollable.value) { | 93 | if (!isScrollable.value) { |
| 66 | return null; | 94 | return null; |
| ... | @@ -71,13 +99,39 @@ const itemStyle = computed(() => { | ... | @@ -71,13 +99,39 @@ const itemStyle = computed(() => { |
| 71 | minWidth: `${SCROLLABLE_ITEM_WIDTH}px`, | 99 | minWidth: `${SCROLLABLE_ITEM_WIDTH}px`, |
| 72 | }; | 100 | }; |
| 73 | }); | 101 | }); |
| 74 | -const showScrollFade = computed(() => isScrollable.value && !isScrolledToEnd.value); | ||
| 75 | const currentActiveTab = computed(() => resolveJlsCheckinActiveTab(route.query || {})); | 102 | const currentActiveTab = computed(() => resolveJlsCheckinActiveTab(route.query || {})); |
| 76 | 103 | ||
| 104 | +let scrollHintForwardTimer = null; | ||
| 105 | +let scrollHintResetTimer = null; | ||
| 106 | + | ||
| 77 | const isActive = (key) => key === currentActiveTab.value; | 107 | const isActive = (key) => key === currentActiveTab.value; |
| 78 | 108 | ||
| 79 | const getIconClasses = (item) => getTabbarIconClasses(item); | 109 | const getIconClasses = (item) => getTabbarIconClasses(item); |
| 80 | 110 | ||
| 111 | +const getItemInnerStyle = (item) => { | ||
| 112 | + if (!isActive(item?.key)) { | ||
| 113 | + return null; | ||
| 114 | + } | ||
| 115 | + | ||
| 116 | + return { | ||
| 117 | + color: activeColor.value, | ||
| 118 | + }; | ||
| 119 | +}; | ||
| 120 | + | ||
| 121 | +const clearScrollHintTimers = () => { | ||
| 122 | + if (scrollHintForwardTimer) { | ||
| 123 | + clearTimeout(scrollHintForwardTimer); | ||
| 124 | + scrollHintForwardTimer = null; | ||
| 125 | + } | ||
| 126 | + | ||
| 127 | + if (scrollHintResetTimer) { | ||
| 128 | + clearTimeout(scrollHintResetTimer); | ||
| 129 | + scrollHintResetTimer = null; | ||
| 130 | + } | ||
| 131 | + | ||
| 132 | + isPlayingAutoScrollHint.value = false; | ||
| 133 | +}; | ||
| 134 | + | ||
| 81 | const syncScrollState = () => { | 135 | const syncScrollState = () => { |
| 82 | const panel = panelRef.value; | 136 | const panel = panelRef.value; |
| 83 | 137 | ||
| ... | @@ -90,6 +144,62 @@ const syncScrollState = () => { | ... | @@ -90,6 +144,62 @@ const syncScrollState = () => { |
| 90 | isScrolledToEnd.value = panel.scrollLeft >= maxScrollLeft - 4; | 144 | isScrolledToEnd.value = panel.scrollLeft >= maxScrollLeft - 4; |
| 91 | }; | 145 | }; |
| 92 | 146 | ||
| 147 | +const handlePanelScroll = () => { | ||
| 148 | + const panel = panelRef.value; | ||
| 149 | + | ||
| 150 | + if (!panel) { | ||
| 151 | + isScrolledToEnd.value = false; | ||
| 152 | + return; | ||
| 153 | + } | ||
| 154 | + | ||
| 155 | + syncScrollState(); | ||
| 156 | + | ||
| 157 | + if (isPlayingAutoScrollHint.value || hasUserScrolled.value) { | ||
| 158 | + return; | ||
| 159 | + } | ||
| 160 | + | ||
| 161 | + if (panel.scrollLeft > 6) { | ||
| 162 | + hasUserScrolled.value = true; | ||
| 163 | + } | ||
| 164 | +}; | ||
| 165 | + | ||
| 166 | +const playScrollHint = async () => { | ||
| 167 | + if (!showScrollHint.value || hasPlayedScrollHint.value || typeof window === 'undefined') { | ||
| 168 | + return; | ||
| 169 | + } | ||
| 170 | + | ||
| 171 | + if (window.localStorage?.getItem(SCROLL_HINT_STORAGE_KEY)) { | ||
| 172 | + hasPlayedScrollHint.value = true; | ||
| 173 | + return; | ||
| 174 | + } | ||
| 175 | + | ||
| 176 | + hasPlayedScrollHint.value = true; | ||
| 177 | + await nextTick(); | ||
| 178 | + | ||
| 179 | + const panel = panelRef.value; | ||
| 180 | + if (!panel) { | ||
| 181 | + return; | ||
| 182 | + } | ||
| 183 | + | ||
| 184 | + isPlayingAutoScrollHint.value = true; | ||
| 185 | + | ||
| 186 | + scrollHintForwardTimer = window.setTimeout(() => { | ||
| 187 | + panel.scrollTo({ | ||
| 188 | + left: SCROLL_HINT_OFFSET, | ||
| 189 | + behavior: 'smooth', | ||
| 190 | + }); | ||
| 191 | + }, 240); | ||
| 192 | + | ||
| 193 | + scrollHintResetTimer = window.setTimeout(() => { | ||
| 194 | + panel.scrollTo({ | ||
| 195 | + left: 0, | ||
| 196 | + behavior: 'smooth', | ||
| 197 | + }); | ||
| 198 | + isPlayingAutoScrollHint.value = false; | ||
| 199 | + window.localStorage?.setItem(SCROLL_HINT_STORAGE_KEY, '1'); | ||
| 200 | + }, 1080); | ||
| 201 | +}; | ||
| 202 | + | ||
| 93 | const navigate = (item) => { | 203 | const navigate = (item) => { |
| 94 | const targetUrl = tabbar.resolveTargetUrl(item); | 204 | const targetUrl = tabbar.resolveTargetUrl(item); |
| 95 | 205 | ||
| ... | @@ -97,7 +207,7 @@ const navigate = (item) => { | ... | @@ -97,7 +207,7 @@ const navigate = (item) => { |
| 97 | return; | 207 | return; |
| 98 | } | 208 | } |
| 99 | 209 | ||
| 100 | - if (wx?.miniProgram?.redirectTo) { | 210 | + if (item?.key === 'home' && wx?.miniProgram?.redirectTo) { |
| 101 | wx.miniProgram.redirectTo({ | 211 | wx.miniProgram.redirectTo({ |
| 102 | url: targetUrl, | 212 | url: targetUrl, |
| 103 | fail: () => { | 213 | fail: () => { |
| ... | @@ -107,6 +217,21 @@ const navigate = (item) => { | ... | @@ -107,6 +217,21 @@ const navigate = (item) => { |
| 107 | return; | 217 | return; |
| 108 | } | 218 | } |
| 109 | 219 | ||
| 220 | + if (wx?.miniProgram?.navigateTo) { | ||
| 221 | + wx.miniProgram.navigateTo({ | ||
| 222 | + url: targetUrl, | ||
| 223 | + fail: () => { | ||
| 224 | + wx?.miniProgram?.redirectTo?.({ | ||
| 225 | + url: targetUrl, | ||
| 226 | + fail: () => { | ||
| 227 | + wx?.miniProgram?.reLaunch?.({ url: targetUrl }); | ||
| 228 | + }, | ||
| 229 | + }); | ||
| 230 | + }, | ||
| 231 | + }); | ||
| 232 | + return; | ||
| 233 | + } | ||
| 234 | + | ||
| 110 | wx?.miniProgram?.reLaunch?.({ url: targetUrl }); | 235 | wx?.miniProgram?.reLaunch?.({ url: targetUrl }); |
| 111 | }; | 236 | }; |
| 112 | 237 | ||
| ... | @@ -114,11 +239,17 @@ watch( | ... | @@ -114,11 +239,17 @@ watch( |
| 114 | () => tabItems.value.length, | 239 | () => tabItems.value.length, |
| 115 | async () => { | 240 | async () => { |
| 116 | await nextTick(); | 241 | await nextTick(); |
| 117 | - syncScrollState(); | 242 | + handlePanelScroll(); |
| 118 | }, | 243 | }, |
| 119 | { immediate: true }, | 244 | { immediate: true }, |
| 120 | ); | 245 | ); |
| 121 | 246 | ||
| 247 | +watch(showScrollHint, (value) => { | ||
| 248 | + if (value) { | ||
| 249 | + playScrollHint(); | ||
| 250 | + } | ||
| 251 | +}, { immediate: true }); | ||
| 252 | + | ||
| 122 | onMounted(async () => { | 253 | onMounted(async () => { |
| 123 | if (!props.visible) { | 254 | if (!props.visible) { |
| 124 | return; | 255 | return; |
| ... | @@ -126,27 +257,38 @@ onMounted(async () => { | ... | @@ -126,27 +257,38 @@ onMounted(async () => { |
| 126 | 257 | ||
| 127 | await tabbar.ensureLoaded(props.loadOptions || defaultLoadOptions); | 258 | await tabbar.ensureLoaded(props.loadOptions || defaultLoadOptions); |
| 128 | await nextTick(); | 259 | await nextTick(); |
| 129 | - syncScrollState(); | 260 | + handlePanelScroll(); |
| 261 | +}); | ||
| 262 | + | ||
| 263 | +onBeforeUnmount(() => { | ||
| 264 | + clearScrollHintTimers(); | ||
| 130 | }); | 265 | }); |
| 131 | </script> | 266 | </script> |
| 132 | 267 | ||
| 133 | <style scoped> | 268 | <style scoped> |
| 134 | .jls-bottom-nav { | 269 | .jls-bottom-nav { |
| 270 | + --jls-bottom-nav-height: 88px; | ||
| 271 | +} | ||
| 272 | + | ||
| 273 | +.jls-bottom-nav__placeholder { | ||
| 274 | + height: var(--jls-bottom-nav-height); | ||
| 275 | + flex-shrink: 0; | ||
| 276 | +} | ||
| 277 | + | ||
| 278 | +.jls-bottom-nav__wrap { | ||
| 135 | position: fixed; | 279 | position: fixed; |
| 136 | bottom: 0; | 280 | bottom: 0; |
| 137 | left: 0; | 281 | left: 0; |
| 138 | right: 0; | 282 | right: 0; |
| 139 | z-index: 50; | 283 | z-index: 50; |
| 140 | - height: 80px; | ||
| 141 | - min-height: 80px; | ||
| 142 | - background: rgba(255, 255, 255, 0.98); | ||
| 143 | - border-top: 1px solid rgba(166, 121, 57, 0.12); | ||
| 144 | - backdrop-filter: blur(6px); | ||
| 145 | overflow: hidden; | 284 | overflow: hidden; |
| 146 | } | 285 | } |
| 147 | 286 | ||
| 148 | .jls-bottom-nav__panel { | 287 | .jls-bottom-nav__panel { |
| 149 | - height: 100%; | 288 | + height: var(--jls-bottom-nav-height); |
| 289 | + background: rgba(255, 255, 255, 0.98); | ||
| 290 | + border-top: 1px solid rgba(166, 121, 57, 0.12); | ||
| 291 | + backdrop-filter: blur(8px); | ||
| 150 | overflow-x: auto; | 292 | overflow-x: auto; |
| 151 | overflow-y: hidden; | 293 | overflow-y: hidden; |
| 152 | scrollbar-width: none; | 294 | scrollbar-width: none; |
| ... | @@ -160,16 +302,14 @@ onMounted(async () => { | ... | @@ -160,16 +302,14 @@ onMounted(async () => { |
| 160 | .jls-bottom-nav__content { | 302 | .jls-bottom-nav__content { |
| 161 | display: flex; | 303 | display: flex; |
| 162 | align-items: stretch; | 304 | align-items: stretch; |
| 163 | - justify-content: space-around; | ||
| 164 | box-sizing: border-box; | 305 | box-sizing: border-box; |
| 165 | height: 100%; | 306 | height: 100%; |
| 166 | - padding: 8px 12px; | 307 | + padding: 10px 12px; |
| 167 | } | 308 | } |
| 168 | 309 | ||
| 169 | .jls-bottom-nav__content.is-scrollable { | 310 | .jls-bottom-nav__content.is-scrollable { |
| 170 | - justify-content: flex-start; | 311 | + display: inline-flex; |
| 171 | - width: max-content; | 312 | + white-space: nowrap; |
| 172 | - min-width: 100%; | ||
| 173 | } | 313 | } |
| 174 | 314 | ||
| 175 | .jls-bottom-nav__item { | 315 | .jls-bottom-nav__item { |
| ... | @@ -181,12 +321,13 @@ onMounted(async () => { | ... | @@ -181,12 +321,13 @@ onMounted(async () => { |
| 181 | padding: 0; | 321 | padding: 0; |
| 182 | border: 0; | 322 | border: 0; |
| 183 | background: transparent; | 323 | background: transparent; |
| 184 | - color: #8b95a7; | ||
| 185 | cursor: pointer; | 324 | cursor: pointer; |
| 186 | } | 325 | } |
| 187 | 326 | ||
| 188 | .jls-bottom-nav__item.is-scrollable { | 327 | .jls-bottom-nav__item.is-scrollable { |
| 189 | flex: 0 0 auto; | 328 | flex: 0 0 auto; |
| 329 | + padding-right: 4px; | ||
| 330 | + box-sizing: border-box; | ||
| 190 | } | 331 | } |
| 191 | 332 | ||
| 192 | .jls-bottom-nav__item-inner { | 333 | .jls-bottom-nav__item-inner { |
| ... | @@ -195,34 +336,29 @@ onMounted(async () => { | ... | @@ -195,34 +336,29 @@ onMounted(async () => { |
| 195 | align-items: center; | 336 | align-items: center; |
| 196 | justify-content: center; | 337 | justify-content: center; |
| 197 | width: 100%; | 338 | width: 100%; |
| 198 | - height: 100%; | 339 | + min-height: 64px; |
| 199 | - gap: 5px; | 340 | + gap: 8px; |
| 200 | - border-radius: 10px; | 341 | + border-radius: 12px; |
| 201 | - color: inherit; | 342 | + color: #8b95a7; |
| 202 | - transition: color 0.2s ease, transform 0.2s ease; | 343 | + transition: background-color 0.2s ease, color 0.2s ease, transform 0.2s ease; |
| 203 | -} | ||
| 204 | - | ||
| 205 | -.jls-bottom-nav__item.is-active { | ||
| 206 | - color: #a67939; | ||
| 207 | } | 344 | } |
| 208 | 345 | ||
| 209 | .jls-bottom-nav__icon { | 346 | .jls-bottom-nav__icon { |
| 210 | display: flex; | 347 | display: flex; |
| 211 | align-items: center; | 348 | align-items: center; |
| 212 | justify-content: center; | 349 | justify-content: center; |
| 213 | - width: 18px; | ||
| 214 | - height: 18px; | ||
| 215 | line-height: 1; | 350 | line-height: 1; |
| 351 | + color: inherit; | ||
| 216 | } | 352 | } |
| 217 | 353 | ||
| 218 | .jls-bottom-nav__icon-font { | 354 | .jls-bottom-nav__icon-font { |
| 219 | - font-size: 18px; | 355 | + font-size: 24px; |
| 220 | line-height: 1; | 356 | line-height: 1; |
| 221 | } | 357 | } |
| 222 | 358 | ||
| 223 | .jls-bottom-nav__label { | 359 | .jls-bottom-nav__label { |
| 224 | - font-size: 11px; | 360 | + font-size: 13px; |
| 225 | - line-height: 1.2; | 361 | + line-height: 1.25; |
| 226 | font-weight: 600; | 362 | font-weight: 600; |
| 227 | } | 363 | } |
| 228 | 364 | ||
| ... | @@ -231,36 +367,48 @@ onMounted(async () => { | ... | @@ -231,36 +367,48 @@ onMounted(async () => { |
| 231 | top: 0; | 367 | top: 0; |
| 232 | right: 0; | 368 | right: 0; |
| 233 | bottom: 0; | 369 | bottom: 0; |
| 234 | - width: 48px; | 370 | + width: 56px; |
| 235 | - pointer-events: none; | 371 | + display: flex; |
| 372 | + align-items: center; | ||
| 373 | + justify-content: center; | ||
| 236 | background: linear-gradient( | 374 | background: linear-gradient( |
| 237 | 90deg, | 375 | 90deg, |
| 238 | rgba(255, 255, 255, 0) 0%, | 376 | rgba(255, 255, 255, 0) 0%, |
| 239 | - rgba(255, 255, 255, 0.36) 42%, | 377 | + rgba(255, 255, 255, 0.2) 26%, |
| 378 | + rgba(255, 255, 255, 0.74) 68%, | ||
| 240 | rgba(255, 255, 255, 0.98) 100% | 379 | rgba(255, 255, 255, 0.98) 100% |
| 241 | ); | 380 | ); |
| 381 | + pointer-events: none; | ||
| 382 | +} | ||
| 383 | + | ||
| 384 | +.jls-bottom-nav__hint-arrow { | ||
| 385 | + width: 8px; | ||
| 386 | + height: 8px; | ||
| 387 | + margin-right: 8px; | ||
| 388 | + border-top: 1px solid rgba(166, 121, 57, 0.32); | ||
| 389 | + border-right: 1px solid rgba(166, 121, 57, 0.32); | ||
| 390 | + transform: rotate(45deg); | ||
| 242 | } | 391 | } |
| 243 | 392 | ||
| 244 | @media screen and (min-width: 768px) { | 393 | @media screen and (min-width: 768px) { |
| 245 | - .jls-bottom-nav__content { | 394 | + .jls-bottom-nav { |
| 246 | - padding: 8px 16px; | 395 | + --jls-bottom-nav-height: 92px; |
| 247 | } | 396 | } |
| 248 | 397 | ||
| 249 | - .jls-bottom-nav__item-inner { | 398 | + .jls-bottom-nav__content { |
| 250 | - gap: 6px; | 399 | + padding: 10px 16px; |
| 251 | } | 400 | } |
| 252 | 401 | ||
| 253 | - .jls-bottom-nav__icon { | 402 | + .jls-bottom-nav__item-inner { |
| 254 | - width: 20px; | 403 | + gap: 9px; |
| 255 | - height: 20px; | ||
| 256 | } | 404 | } |
| 257 | 405 | ||
| 258 | .jls-bottom-nav__icon-font { | 406 | .jls-bottom-nav__icon-font { |
| 259 | - font-size: 20px; | 407 | + font-size: 26px; |
| 260 | } | 408 | } |
| 261 | 409 | ||
| 262 | .jls-bottom-nav__label { | 410 | .jls-bottom-nav__label { |
| 263 | - font-size: 12px; | 411 | + font-size: 14px; |
| 264 | } | 412 | } |
| 265 | } | 413 | } |
| 266 | 414 | ... | ... |
| ... | @@ -78,6 +78,7 @@ import JlsBottomNav from '@/components/JlsBottomNav'; | ... | @@ -78,6 +78,7 @@ import JlsBottomNav from '@/components/JlsBottomNav'; |
| 78 | 78 | ||
| 79 | - `fa-` 开头:按 `font-awesome` 渲染 | 79 | - `fa-` 开头:按 `font-awesome` 渲染 |
| 80 | - 其他 class:按 `iconfont` 渲染 | 80 | - 其他 class:按 `iconfont` 渲染 |
| 81 | +- 激活态颜色优先读取接口根级 `color` | ||
| 81 | 82 | ||
| 82 | 示例: | 83 | 示例: |
| 83 | 84 | ||
| ... | @@ -134,7 +135,9 @@ import { getJlsTabbarPreviewOptions } from '@/components/JlsBottomNav/preview'; | ... | @@ -134,7 +135,9 @@ import { getJlsTabbarPreviewOptions } from '@/components/JlsBottomNav/preview'; |
| 134 | 135 | ||
| 135 | ## 注意点 | 136 | ## 注意点 |
| 136 | 137 | ||
| 137 | -- 组件默认按小程序 WebView 跳转方式调用 `wx.miniProgram.redirectTo / reLaunch`。 | 138 | +- 组件默认按当前小程序规则跳转: |
| 139 | + - `home` 优先走 `wx.miniProgram.redirectTo` | ||
| 140 | + - 非 `home` 优先走 `wx.miniProgram.navigateTo`,失败后再降级 | ||
| 138 | - 底栏显示兜底规则是“只保留首页”: | 141 | - 底栏显示兜底规则是“只保留首页”: |
| 139 | - 只有接口成功并返回有效菜单时,才显示首页之外的其他项 | 142 | - 只有接口成功并返回有效菜单时,才显示首页之外的其他项 |
| 140 | - 接口空数组、失败、异常、无法归一化时,都只显示 `home` | 143 | - 接口空数组、失败、异常、无法归一化时,都只显示 `home` |
| ... | @@ -143,4 +146,5 @@ import { getJlsTabbarPreviewOptions } from '@/components/JlsBottomNav/preview'; | ... | @@ -143,4 +146,5 @@ import { getJlsTabbarPreviewOptions } from '@/components/JlsBottomNav/preview'; |
| 143 | - 其他项不会直接跳裸 H5 URL,而是优先使用接口返回的 `page_url` | 146 | - 其他项不会直接跳裸 H5 URL,而是优先使用接口返回的 `page_url` |
| 144 | - 如果接口没有返回 `page_url`,则会把 `webview_url` 包装成小程序承接页地址,例如 `/pages/webview-preview/index?url=...` | 147 | - 如果接口没有返回 `page_url`,则会把 `webview_url` 包装成小程序承接页地址,例如 `/pages/webview-preview/index?url=...` |
| 145 | - 也就是说,当前实现仍然是“首页回小程序页,其他项走小程序承接页再打开 H5”,不是“其他项直接在当前 H5 中跳转到接口 URL”。 | 148 | - 也就是说,当前实现仍然是“首页回小程序页,其他项走小程序承接页再打开 H5”,不是“其他项直接在当前 H5 中跳转到接口 URL”。 |
| 149 | +- 非首页保留页面栈的原因和小程序一致:进入 `webview-preview` 后,需要保住微信原生左上角返回按钮。 | ||
| 146 | - 当前样式尺寸以 H5 版本为准,没有照搬小程序 `rpx` 布局。 | 150 | - 当前样式尺寸以 H5 版本为准,没有照搬小程序 `rpx` 布局。 | ... | ... |
| ... | @@ -45,6 +45,7 @@ const KNOWN_TABBAR_ITEM_MAP = { | ... | @@ -45,6 +45,7 @@ const KNOWN_TABBAR_ITEM_MAP = { |
| 45 | 45 | ||
| 46 | const TABBAR_ORDER = ['home', 'news', 'list', 'user']; | 46 | const TABBAR_ORDER = ['home', 'news', 'list', 'user']; |
| 47 | const DEFAULT_ICON_CLASS = 'fa-circle-o'; | 47 | const DEFAULT_ICON_CLASS = 'fa-circle-o'; |
| 48 | +const DEFAULT_TABBAR_ACTIVE_COLOR = '#a67939'; | ||
| 48 | 49 | ||
| 49 | const normalizeVisibleValue = (rawValue, fallbackValue = true) => { | 50 | const normalizeVisibleValue = (rawValue, fallbackValue = true) => { |
| 50 | if (typeof rawValue === 'boolean') { | 51 | if (typeof rawValue === 'boolean') { |
| ... | @@ -113,6 +114,19 @@ export const getDefaultTabbarItems = () => | ... | @@ -113,6 +114,19 @@ export const getDefaultTabbarItems = () => |
| 113 | 114 | ||
| 114 | export const getHomeOnlyTabbarItems = () => [getDefaultTabbarItem('home')].filter(Boolean); | 115 | export const getHomeOnlyTabbarItems = () => [getDefaultTabbarItem('home')].filter(Boolean); |
| 115 | 116 | ||
| 117 | +export const getDefaultTabbarActiveColor = () => DEFAULT_TABBAR_ACTIVE_COLOR; | ||
| 118 | + | ||
| 119 | +export const normalizeTabbarColor = (rawPayload = null) => { | ||
| 120 | + const rawColor = rawPayload?.color; | ||
| 121 | + | ||
| 122 | + if (typeof rawColor !== 'string') { | ||
| 123 | + return DEFAULT_TABBAR_ACTIVE_COLOR; | ||
| 124 | + } | ||
| 125 | + | ||
| 126 | + const normalizedColor = rawColor.trim(); | ||
| 127 | + return normalizedColor || DEFAULT_TABBAR_ACTIVE_COLOR; | ||
| 128 | +}; | ||
| 129 | + | ||
| 116 | const buildTabbarPageUrl = (key, webviewUrl, webviewTitle, rawPageUrl = '') => { | 130 | const buildTabbarPageUrl = (key, webviewUrl, webviewTitle, rawPageUrl = '') => { |
| 117 | if (key === 'home') { | 131 | if (key === 'home') { |
| 118 | return rawPageUrl || '/pages/index/index'; | 132 | return rawPageUrl || '/pages/index/index'; | ... | ... |
| 1 | import { computed, reactive, readonly } from 'vue'; | 1 | import { computed, reactive, readonly } from 'vue'; |
| 2 | import { getTabbarConfigAPI } from './tabbar.api'; | 2 | import { getTabbarConfigAPI } from './tabbar.api'; |
| 3 | import { | 3 | import { |
| 4 | + getDefaultTabbarActiveColor, | ||
| 4 | getDefaultTabbarItem, | 5 | getDefaultTabbarItem, |
| 5 | getHomeOnlyTabbarItems, | 6 | getHomeOnlyTabbarItems, |
| 7 | + normalizeTabbarColor, | ||
| 6 | normalizeTabbarKey, | 8 | normalizeTabbarKey, |
| 7 | normalizeTabbarPayload, | 9 | normalizeTabbarPayload, |
| 8 | resolveTabbarTargetUrl, | 10 | resolveTabbarTargetUrl, |
| ... | @@ -10,6 +12,7 @@ import { | ... | @@ -10,6 +12,7 @@ import { |
| 10 | 12 | ||
| 11 | const state = reactive({ | 13 | const state = reactive({ |
| 12 | tabItems: getHomeOnlyTabbarItems(), | 14 | tabItems: getHomeOnlyTabbarItems(), |
| 15 | + activeColor: getDefaultTabbarActiveColor(), | ||
| 13 | loaded: false, | 16 | loaded: false, |
| 14 | loading: false, | 17 | loading: false, |
| 15 | loadMode: 'api', | 18 | loadMode: 'api', |
| ... | @@ -19,6 +22,7 @@ let tabbarRequestPromise = null; | ... | @@ -19,6 +22,7 @@ let tabbarRequestPromise = null; |
| 19 | 22 | ||
| 20 | const applyFallbackItems = () => { | 23 | const applyFallbackItems = () => { |
| 21 | state.tabItems = getHomeOnlyTabbarItems(); | 24 | state.tabItems = getHomeOnlyTabbarItems(); |
| 25 | + state.activeColor = getDefaultTabbarActiveColor(); | ||
| 22 | }; | 26 | }; |
| 23 | 27 | ||
| 24 | const resolveLoadMode = (options = {}) => (options.useMock ? 'mock' : 'api'); | 28 | const resolveLoadMode = (options = {}) => (options.useMock ? 'mock' : 'api'); |
| ... | @@ -40,6 +44,7 @@ export const fetchTabbarConfig = async (force = false, options = {}) => { | ... | @@ -40,6 +44,7 @@ export const fetchTabbarConfig = async (force = false, options = {}) => { |
| 40 | try { | 44 | try { |
| 41 | if (loadMode === 'mock') { | 45 | if (loadMode === 'mock') { |
| 42 | state.tabItems = normalizeTabbarPayload(options.mockData); | 46 | state.tabItems = normalizeTabbarPayload(options.mockData); |
| 47 | + state.activeColor = normalizeTabbarColor(options.mockData); | ||
| 43 | state.loadMode = loadMode; | 48 | state.loadMode = loadMode; |
| 44 | return state.tabItems; | 49 | return state.tabItems; |
| 45 | } | 50 | } |
| ... | @@ -48,6 +53,7 @@ export const fetchTabbarConfig = async (force = false, options = {}) => { | ... | @@ -48,6 +53,7 @@ export const fetchTabbarConfig = async (force = false, options = {}) => { |
| 48 | 53 | ||
| 49 | if (response?.code === 1) { | 54 | if (response?.code === 1) { |
| 50 | state.tabItems = normalizeTabbarPayload(response?.data); | 55 | state.tabItems = normalizeTabbarPayload(response?.data); |
| 56 | + state.activeColor = normalizeTabbarColor(response?.data); | ||
| 51 | } else { | 57 | } else { |
| 52 | applyFallbackItems(); | 58 | applyFallbackItems(); |
| 53 | } | 59 | } |
| ... | @@ -91,6 +97,7 @@ export const useJlsTabbar = () => { | ... | @@ -91,6 +97,7 @@ export const useJlsTabbar = () => { |
| 91 | state: readonly(state), | 97 | state: readonly(state), |
| 92 | visibleTabItems, | 98 | visibleTabItems, |
| 93 | getTabItem, | 99 | getTabItem, |
| 100 | + activeColor: computed(() => state.activeColor), | ||
| 94 | ensureLoaded: ensureTabbarLoaded, | 101 | ensureLoaded: ensureTabbarLoaded, |
| 95 | fetchTabbarConfig, | 102 | fetchTabbarConfig, |
| 96 | resolveTargetUrl: resolveTabbarTargetUrl, | 103 | resolveTargetUrl: resolveTabbarTargetUrl, | ... | ... |
| ... | @@ -8,7 +8,7 @@ export const CHECKIN_NAV_MODE = { | ... | @@ -8,7 +8,7 @@ export const CHECKIN_NAV_MODE = { |
| 8 | 8 | ||
| 9 | export const CHECKIN_ACTIVE_TAB = JLS_CHECKIN_ACTIVE_TAB; | 9 | export const CHECKIN_ACTIVE_TAB = JLS_CHECKIN_ACTIVE_TAB; |
| 10 | 10 | ||
| 11 | -const NAV_HEIGHT = '80px'; | 11 | +const NAV_HEIGHT = '88px'; |
| 12 | 12 | ||
| 13 | export function resolveCheckinNavMode(query = {}) { | 13 | export function resolveCheckinNavMode(query = {}) { |
| 14 | const rawMode = Array.isArray(query.navMode) ? query.navMode[0] : query.navMode; | 14 | const rawMode = Array.isArray(query.navMode) ? query.navMode[0] : query.navMode; | ... | ... |
-
Please register or login to post a comment