hookehuyr

feat(JlsBottomNav): 优化底部导航,新增自定义激活色与滚动提示

- 移除未使用的ElMessage自动导入配置
- 调整导航栏默认高度从80px改为88px
- 新增颜色处理工具函数,支持读取接口或mock数据中的激活态颜色
- 重构组件模板与样式,优化滚动布局并添加滚动提示动画
- 完善小程序跳转逻辑,区分首页与其他页面的跳转方式
- 更新组件文档与mock测试数据
1 { 1 {
2 "globals": { 2 "globals": {
3 "EffectScope": true, 3 "EffectScope": true,
4 - "ElMessage": true,
5 "computed": true, 4 "computed": true,
6 "createApp": true, 5 "createApp": true,
7 "customRef": true, 6 "customRef": true,
......
...@@ -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 + <div class="jls-bottom-nav__wrap">
6 + <div ref="panelRef" class="jls-bottom-nav__panel" @scroll="handlePanelScroll">
7 + <div
8 + class="jls-bottom-nav__content"
9 + :class="{ 'is-scrollable': isScrollable }"
10 + :style="contentStyle"
11 + >
5 <button 12 <button
6 v-for="item in tabItems" 13 v-for="item in tabItems"
7 :key="item.key" 14 :key="item.key"
...@@ -11,7 +18,7 @@ ...@@ -11,7 +18,7 @@
11 :style="itemStyle" 18 :style="itemStyle"
12 @click="navigate(item)" 19 @click="navigate(item)"
13 > 20 >
14 - <span class="jls-bottom-nav__item-inner"> 21 + <span class="jls-bottom-nav__item-inner" :style="getItemInnerStyle(item)">
15 <span class="jls-bottom-nav__icon"> 22 <span class="jls-bottom-nav__icon">
16 <i class="jls-bottom-nav__icon-font" :class="getIconClasses(item)" aria-hidden="true"></i> 23 <i class="jls-bottom-nav__icon-font" :class="getIconClasses(item)" aria-hidden="true"></i>
17 </span> 24 </span>
...@@ -21,12 +28,15 @@ ...@@ -21,12 +28,15 @@
21 </div> 28 </div>
22 </div> 29 </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` 布局。
......
1 export const getJlsTabbarMockData = () => ({ 1 export const getJlsTabbarMockData = () => ({
2 + color: '#86190C',
2 home: { 3 home: {
3 title: '首页', 4 title: '首页',
4 icon: 'fa-home', 5 icon: 'fa-home',
......
...@@ -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;
......