hookehuyr

feat: 添加 JLS 小程序底部导航支持

新增 JLS 底部导航组件,支持通过 navMode 查询参数切换导航模式。
提取导航模式解析和地图高度计算逻辑到独立模块,并添加相应测试。
更新 AGENTS.md 文档,明确运行环境要求。
......@@ -4,6 +4,7 @@
本仓库是一个基于 Vue 3 与 Vite 的地图项目。核心代码位于 `src/``views/` 放页面级地图与打卡流程,`components/` 放可复用组件,`api/` 放接口封装,`common/``utils/` 放共享工具。静态资源主要在 `src/assets/` 和顶层 `images/`。构建与接口辅助脚本位于 `scripts/`。现有测试样例主要放在 `src/test/`,多入口示例位于 `src/packages/mono1``src/packages/mono2`
## 构建、测试与开发命令
- 运行环境:进入仓库后先执行 `source "$HOME/.nvm/nvm.sh" && nvm use 18.13.0`,当前确认版本为 `node v18.13.0``npm v8.19.3`
- `npm run dev`:启动本地 Vite 开发服务。
- `npm run start`:以 `0.0.0.0` 暴露开发服务,便于真机联调。
- `npm run build`:按当前 `.env` 配置生成生产构建。
......
......@@ -25,6 +25,7 @@ declare module '@vue/runtime-core' {
InfoWindowLite: typeof import('./src/components/InfoWindowLite.vue')['default']
InfoWindowWarn: typeof import('./src/components/InfoWindowWarn.vue')['default']
InfoWindowYard: typeof import('./src/components/InfoWindowYard.vue')['default']
JlsBottomNav: typeof import('./src/components/JlsBottomNav.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SvgIcon: typeof import('./src/components/Floor/svgIcon.vue')['default']
......
const chai = require('chai');
const expect = chai.expect;
describe('checkin nav mode helpers', function () {
it('resolves nav mode and map height from route query', async function () {
const {
CHECKIN_NAV_MODE,
resolveCheckinNavMode,
getCheckinMapHeight,
} = await import('../src/views/checkin/nav-mode.js');
expect(resolveCheckinNavMode({})).to.equal(CHECKIN_NAV_MODE.LEGACY);
expect(resolveCheckinNavMode({ navMode: 'jls' })).to.equal(CHECKIN_NAV_MODE.JLS);
expect(resolveCheckinNavMode({ navMode: 'none' })).to.equal(CHECKIN_NAV_MODE.NONE);
expect(resolveCheckinNavMode({ navMode: 'unexpected' })).to.equal(
CHECKIN_NAV_MODE.LEGACY,
);
expect(
getCheckinMapHeight({
isMiniProgramWebView: false,
navMode: CHECKIN_NAV_MODE.LEGACY,
}),
).to.equal('100vh');
expect(
getCheckinMapHeight({
isMiniProgramWebView: true,
navMode: CHECKIN_NAV_MODE.LEGACY,
}),
).to.equal('calc(100vh - 80px)');
expect(
getCheckinMapHeight({
isMiniProgramWebView: true,
navMode: CHECKIN_NAV_MODE.JLS,
}),
).to.equal('calc(100vh - 80px)');
expect(
getCheckinMapHeight({
isMiniProgramWebView: true,
navMode: CHECKIN_NAV_MODE.NONE,
}),
).to.equal('100vh');
});
});
<template>
<div v-if="isMiniProgramWebView" class="bottom-nav jls-bottom-nav">
<div
v-for="item in navItems"
:key="item.path"
@click="navigate(item.path)"
:class="['nav-item', isActive(item.name) ? 'nav-item-active' : 'nav-item-inactive']"
>
<div class="nav-item-inner">
<div class="nav-icon">
<component :is="item.icon" size="18" :color="getIconColor(item.name)" />
</div>
<span class="nav-label">{{ item.label }}</span>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import HomeIcon from '@nutui/icons-vue/dist/es/icons/Home.js';
import MessageIcon from '@nutui/icons-vue/dist/es/icons/Message.js';
import MyIcon from '@nutui/icons-vue/dist/es/icons/My.js';
import wx from 'weixin-js-sdk';
const ACTIVE_COLOR = '#a67939';
const INACTIVE_COLOR = '#8b95a7';
const route = useRoute();
const isMiniProgramWebView = computed(() => {
return navigator.userAgent.includes('miniProgram');
});
const navItems = [
{ name: 'home', path: '/pages/index/index', icon: HomeIcon, label: '首页' },
{ name: 'message', path: '/pages/message/index', icon: MessageIcon, label: '消息' },
{ name: 'mine', path: '/pages/mine/index', icon: MyIcon, label: '我的' },
];
const reservedActiveTab = computed(() => {
const rawTab = Array.isArray(route.query.activeTab) ? route.query.activeTab[0] : route.query.activeTab;
return String(rawTab || '').trim().toLowerCase();
});
const isActive = () => {
// 地图页目前不属于 JLS tab 列表中的任何一项。
// activeTab 参数先预留,待业务确认后再决定是否映射到某个 tab 并启用高亮。
void reservedActiveTab.value;
return false;
};
const getIconColor = (name) => {
return isActive(name) ? ACTIVE_COLOR : INACTIVE_COLOR;
};
const navigate = (path) => {
if (!path) {
return;
}
if (wx?.miniProgram?.navigateTo) {
wx.miniProgram.navigateTo({
url: path,
fail: () => {
wx?.miniProgram?.reLaunch?.({ url: path });
},
});
return;
}
wx?.miniProgram?.reLaunch?.({ url: path });
};
</script>
<style scoped>
.bottom-nav {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
align-items: stretch;
justify-content: space-around;
box-sizing: border-box;
padding: 8px 12px;
z-index: 50;
min-height: 66px;
background: rgba(255, 255, 255, 0.98);
border-top: 1px solid rgba(166, 121, 57, 0.12);
backdrop-filter: blur(6px);
}
.nav-item {
display: flex;
flex: 1;
min-width: 0;
align-items: stretch;
justify-content: center;
padding: 0;
position: relative;
cursor: pointer;
transition: all 0.2s ease;
}
.nav-item-inner {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 50px;
gap: 5px;
border-radius: 10px;
color: #8b95a7;
transition: background-color 0.2s ease, color 0.2s ease, transform 0.2s ease;
}
.nav-item-active {
color: #a67939;
}
.nav-item-inactive {
color: #8b95a7;
}
.nav-icon {
display: flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
line-height: 1;
transition: transform 0.2s ease;
}
.nav-label {
font-size: 11px;
margin-top: 0;
line-height: 1.2;
font-weight: 600;
}
.jls-bottom-nav .nav-item-active .nav-item-inner {
color: #a67939;
}
@media screen and (min-width: 768px) {
.bottom-nav {
min-height: 72px;
padding: 8px 16px;
}
.nav-item-inner {
min-height: 56px;
gap: 6px;
}
.nav-icon {
width: 20px;
height: 20px;
}
.nav-label {
font-size: 12px;
}
}
@media (hover: hover) {
.nav-item:hover .nav-item-inner {
transform: translateY(-1px);
}
}
</style>
<!--
* @Date: 2023-05-19 14:54:27
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2026-02-10 13:58:46
* @LastEditTime: 2026-04-29 13:05:35
* @FilePath: /map-demo/src/views/checkin/map.vue
* @Description: 公众地图主体页面
-->
......@@ -124,7 +124,8 @@
</van-toast>
<!-- 底部导航组件 -->
<BottomNav />
<BottomNav v-if="shouldShowLegacyBottomNav" />
<JlsBottomNav v-else-if="shouldShowJlsBottomNav" />
</div>
</template>
......@@ -141,11 +142,13 @@ import wx from 'weixin-js-sdk'
import pageInfo from '@/views/checkin/info.vue'
import audioBackground1 from '@/components/audioBackground1.vue'
import BottomNav from '@/components/BottomNav.vue'
import JlsBottomNav from '@/components/JlsBottomNav.vue'
import { mapState, mapActions } from 'pinia'
import { mainStore } from '@/store'
import { parseQueryString, getAdaptiveFontSize, getAdaptivePadding } from '@/utils/tools'
import AMapLoader from '@amap/amap-jsapi-loader'
import { mapAudioAPI } from '@/api/map.js'
import { CHECKIN_NAV_MODE, resolveCheckinNavMode, getCheckinMapHeight } from '@/views/checkin/nav-mode.js'
// 地图缓存管理器
class MapCacheManager {
......@@ -332,7 +335,7 @@ window._AMapSecurityConfig = {
export default {
name: 'CheckinMap',
components: { pageInfo, audioBackground1, BottomNav },
components: { pageInfo, audioBackground1, BottomNav, JlsBottomNav },
computed: {
...mapState(mainStore, ['audio_entity', 'audio_src', 'audio_status']),
/**
......@@ -342,12 +345,24 @@ export default {
isMiniProgramWebView() {
return navigator.userAgent.includes('miniProgram');
},
currentCheckinNavMode() {
return resolveCheckinNavMode(this.$route?.query || {});
},
shouldShowLegacyBottomNav() {
return this.isMiniProgramWebView && this.currentCheckinNavMode === CHECKIN_NAV_MODE.LEGACY;
},
shouldShowJlsBottomNav() {
return this.isMiniProgramWebView && this.currentCheckinNavMode === CHECKIN_NAV_MODE.JLS;
},
/**
* 动态计算地图高度
* @returns {string} 地图容器高度
*/
mapHeight() {
return this.isMiniProgramWebView ? 'calc(100vh - 80px)' : '100vh';
return getCheckinMapHeight({
isMiniProgramWebView: this.isMiniProgramWebView,
navMode: this.currentCheckinNavMode,
});
},
/**
* 获取地图缩放级别
......
export const CHECKIN_NAV_MODE = {
LEGACY: 'legacy',
JLS: 'jls',
NONE: 'none',
};
const NAV_HEIGHT = '80px';
export function resolveCheckinNavMode(query = {}) {
const rawMode = Array.isArray(query.navMode) ? query.navMode[0] : query.navMode;
const normalizedMode = String(rawMode || '').trim().toLowerCase();
if (normalizedMode === CHECKIN_NAV_MODE.JLS) {
return CHECKIN_NAV_MODE.JLS;
}
if (normalizedMode === CHECKIN_NAV_MODE.NONE) {
return CHECKIN_NAV_MODE.NONE;
}
return CHECKIN_NAV_MODE.LEGACY;
}
export function getCheckinMapHeight({
isMiniProgramWebView = false,
navMode = CHECKIN_NAV_MODE.LEGACY,
navHeight = NAV_HEIGHT,
} = {}) {
if (!isMiniProgramWebView || navMode === CHECKIN_NAV_MODE.NONE) {
return '100vh';
}
return `calc(100vh - ${navHeight})`;
}