refactor:重构功能导航模块

master
黄伟杰 4 days ago
parent b849d644ee
commit af78c2ffa3

@ -33,17 +33,27 @@ export function register(data) {
}) })
} }
// 获取用户详细信息 function getPermissionInfo(params = {}) {
export function getInfo() {
return request({ return request({
url: '/admin-api/system/auth/get-permission-info', url: '/admin-api/system/auth/get-permission-info',
method: 'get', method: 'get',
params: { params: {
clientType: 2 clientType: 2,
...params
} }
}) })
} }
// 获取用户详细信息
export function getInfo() {
return getPermissionInfo()
}
// 获取功能导航菜单
export function getNavPermissionInfo() {
return getPermissionInfo()
}
// 退出方法 // 退出方法
export function logout() { export function logout() {
return request({ return request({

@ -0,0 +1,16 @@
import request from '@/utils/request'
export function getUserNavMenuList() {
return request({
url: '/admin-api/system/user-nav-menu/list',
method: 'get'
})
}
export function updateUserNavMenuList(data) {
return request({
url: '/admin-api/system/user-nav-menu/update-list',
method: 'put',
data
})
}

@ -91,7 +91,7 @@ onShow(() => {
function handleBack() { function handleBack() {
uni.navigateBack({ uni.navigateBack({
fail: () => { fail: () => {
uni.switchTab({ uni.reLaunch({
url: '/pages/index' url: '/pages/index'
}) })
} }
@ -103,7 +103,7 @@ function handleBack() {
.custom-navbar { .custom-navbar {
position: relative; position: relative;
width: 100%; width: 100%;
z-index: 999; z-index: 0;
} }
.navbar-body { .navbar-body {

@ -85,9 +85,7 @@
<script setup> <script setup>
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import useUserStore from '@/store/modules/user' import useUserStore from '@/store/modules/user'
import { applyTabBarLanguage } from '@/locales'
import { buildPageModules, findTabMenuByPage, getMenuSymbol, getModuleColor, resolveMenuUrl } from '@/utils/permissionMenu' import { buildPageModules, findTabMenuByPage, getMenuSymbol, getModuleColor, resolveMenuUrl } from '@/utils/permissionMenu'
const props = defineProps({ const props = defineProps({
@ -203,9 +201,7 @@ function handleClick(menu) {
}) })
} }
onShow(() => {
applyTabBarLanguage()
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

@ -0,0 +1,165 @@
<template>
<view class="custom-tabbar" :style="{ paddingBottom: safeAreaBottom + 'px' }">
<view class="tabbar-inner">
<view
v-for="(item, index) in tabList"
:key="index"
class="tabbar-item"
:class="{ active: activeIndex === index }"
@click="handleTabClick(item, index)"
>
<image
:src="activeIndex === index ? item.selectedIcon : item.icon"
class="tabbar-icon"
mode="widthFix"
/>
<text class="tabbar-text" :style="{ color: activeIndex === index ? activeColor : color }">
{{ item.text }}
</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted, watch } from 'vue'
import useUserStore from '@/store/modules/user'
import { storeToRefs } from 'pinia'
import { getDynamicTabMenus } from '@/utils/permissionMenu'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const userStore = useUserStore()
const { menus } = storeToRefs(userStore)
const color = '#666666'
const activeColor = '#1a3a5c'
const activeIndex = ref(0)
const homeIcon = '/static/images/tabbar/home.png'
const homeSelectedIcon = '/static/images/tabbar/home_.png'
const reportIcon = '/static/images/tabbar/report.png'
const reportSelectedIcon = '/static/images/tabbar/report_.png'
const workIcon = '/static/images/tabbar/work.png'
const workSelectedIcon = '/static/images/tabbar/work_.png'
const mineIcon = '/static/images/tabbar/mine.png'
const mineSelectedIcon = '/static/images/tabbar/mine_.png'
const routeMap = {
'pages/index': 0,
'pages/report': 1,
'pages/work': 2,
'pages/mine': 3
}
function getCurrentActiveIndex() {
const pages = getCurrentPages()
if (pages && pages.length > 0) {
const route = pages[pages.length - 1].route
return routeMap[route] !== undefined ? routeMap[route] : 0
}
return 0
}
const tabList = computed(() => {
const dynamicMenus = getDynamicTabMenus(menus.value)
return [
{
text: t('nav.home'),
icon: homeIcon,
selectedIcon: homeSelectedIcon,
path: '/pages/index'
},
{
text: dynamicMenus[0]?.name || t('tab.report'),
icon: reportIcon,
selectedIcon: reportSelectedIcon,
path: '/pages/report'
},
{
text: dynamicMenus[1]?.name || dynamicMenus[0]?.name || t('tab.work'),
icon: workIcon,
selectedIcon: workSelectedIcon,
path: '/pages/work'
},
{
text: t('nav.mine'),
icon: mineIcon,
selectedIcon: mineSelectedIcon,
path: '/pages/mine'
}
]
})
const systemInfo = uni.getSystemInfoSync()
const safeAreaBottom = computed(() => {
const model = (systemInfo.model || '').toLowerCase()
const hasNotch = model.includes('iphone x') || model.includes('iphone 1') || (systemInfo.safeAreaInsets && systemInfo.safeAreaInsets.bottom > 20)
return hasNotch ? 0 : 0
})
const safeAreaInsetBottom = ref(0)
onMounted(() => {
activeIndex.value = getCurrentActiveIndex()
const insets = systemInfo.safeAreaInsets
if (insets && insets.bottom > 0) {
safeAreaInsetBottom.value = insets.bottom
}
})
function handleTabClick(item, index) {
if (activeIndex.value === index) return
activeIndex.value = index
uni.reLaunch({
url: item.path
})
}
watch(() => useUserStore().menus, () => {
activeIndex.value = getCurrentActiveIndex()
})
</script>
<style lang="scss" scoped>
.custom-tabbar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 10;
background-color: #ffffff;
box-shadow: 0 -2rpx 12rpx rgba(0, 0, 0, 0.06);
}
.tabbar-inner {
display: flex;
align-items: center;
height: 100rpx;
}
.tabbar-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
&:active {
opacity: 0.7;
}
}
.tabbar-icon {
width: 48rpx;
height: 48rpx;
margin-bottom: 4rpx;
}
.tabbar-text {
font-size: 20rpx;
line-height: 1.2;
}
</style>

@ -0,0 +1,634 @@
<template>
<view v-if="visible" class="nav-menu-editor-mask" @click="handleClose">
<view class="nav-menu-editor" @click.stop @touchmove.stop.prevent="handleDragMove" @touchend.stop="handleDragEnd"
@touchcancel.stop="handleDragEnd">
<view class="editor-header">
<text class="editor-title">{{ t('dashboard.editNavMenu') }}</text>
<view class="editor-close" @click="handleClose">
<uni-icons type="close" size="24" color="#999"></uni-icons>
</view>
</view>
<view class="editor-content">
<view class="nav-section">
<text class="nav-section-title">{{ t('dashboard.configuredNav') }}</text>
<view id="configured-zone" class="nav-grid" :class="{ 'nav-grid-target': isSectionDropTarget('configured') }">
<view v-for="item in configuredMenuList" :key="item.id" :id="`configured-item-${item.id}`" class="nav-item"
:class="{
'is-drop-target': isItemDropTarget('configured', item.id),
'is-dragging-source': draggingMenu?.id === item.id
}" @click="handleConfiguredClick(item)"
@longpress.stop.prevent="handleItemLongPress(item, 'configured', $event)">
<view class="nav-icon" :style="{ background: hexToRgba(item.accentColor, 0.1) }">
<uni-icons v-if="isUniIcon(item.icon)" :type="getUniIconName(item.icon)" size="24" :color="item.accentColor" />
<u-icon v-else-if="isUviewIcon(item.icon)" :name="getUviewIconName(item.icon)" size="24" :color="item.accentColor"></u-icon>
<text v-else class="nav-icon-text">{{ item.symbol }}</text>
</view>
<text class="nav-text">{{ item.displayName }}</text>
</view>
</view>
</view>
<view class="divider-line"></view>
<view class="nav-section">
<text class="nav-section-title">{{ t('dashboard.unconfiguredNav') }}</text>
<view id="unconfigured-zone" class="nav-grid"
:class="{ 'nav-grid-target': isSectionDropTarget('unconfigured') }">
<view v-for="item in unconfiguredMenuList" :key="item.id" :id="`unconfigured-item-${item.id}`"
class="nav-item nav-item-disabled" :class="{
'is-drop-target': isItemDropTarget('unconfigured', item.id),
'is-dragging-source': draggingMenu?.id === item.id
}" @click="handleUnconfiguredClick(item)"
@longpress.stop.prevent="handleItemLongPress(item, 'unconfigured', $event)">
<view class="nav-icon nav-icon-disabled" :style="{ background: hexToRgba(item.accentColor, 0.1) }">
<uni-icons v-if="isUniIcon(item.icon)" :type="getUniIconName(item.icon)" size="24" :color="item.accentColor" />
<u-icon v-else-if="isUviewIcon(item.icon)" :name="getUviewIconName(item.icon)" size="24" :color="item.accentColor"></u-icon>
<text v-else class="nav-icon-text">{{ item.symbol }}</text>
</view>
<text class="nav-text nav-text-disabled">{{ item.displayName }}</text>
</view>
</view>
</view>
<text class="click-hint">{{ t('dashboard.dragHint') }}</text>
<text class="click-hint secondary-hint">{{ t('dashboard.clickHint') }}</text>
</view>
<view class="editor-footer">
<view class="btn-reset" @click="handleReset">
<text>{{ t('common.reset') }}</text>
</view>
<view class="btn-confirm" @click="handleConfirm">
<text>{{ t('common.complete') }}</text>
</view>
</view>
<view v-if="draggingMenu" class="drag-ghost" :style="dragGhostStyle">
<view class="nav-icon" :style="{ background: hexToRgba(draggingMenu.accentColor, 0.1) }">
<uni-icons v-if="isUniIcon(draggingMenu.icon)" :type="getUniIconName(draggingMenu.icon)" size="24" :color="draggingMenu.accentColor" />
<u-icon v-else-if="isUviewIcon(draggingMenu.icon)" :name="getUviewIconName(draggingMenu.icon)" size="24" :color="draggingMenu.accentColor"></u-icon>
<text v-else class="nav-icon-text">{{ draggingMenu.symbol }}</text>
</view>
<text class="nav-text">{{ draggingMenu.displayName }}</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, watch, nextTick, getCurrentInstance } from 'vue'
import { useI18n } from 'vue-i18n'
import useUserStore from '@/store/modules/user'
import { getUserNavMenuList, updateUserNavMenuList } from '@/api/system/userNavMenu'
import { getNavPermissionInfo } from '@/api/login'
import { buildNavMenuViewModels } from '@/utils/permissionMenu'
const { t } = useI18n()
const userStore = useUserStore()
const instance = getCurrentInstance()
function hexToRgba(hex, alpha) {
const value = String(hex || '').replace('#', '')
if (value.length !== 6) {
return `rgba(45, 90, 135, ${alpha})`
}
const red = parseInt(value.slice(0, 2), 16)
const green = parseInt(value.slice(2, 4), 16)
const blue = parseInt(value.slice(4, 6), 16)
return `rgba(${red}, ${green}, ${blue}, ${alpha})`
}
function isUniIcon(icon) {
return String(icon || '').startsWith('uni-icons:')
}
function isUviewIcon(icon) {
return String(icon || '').startsWith('uview-plus:')
}
function getUniIconName(icon) {
return String(icon || '').replace(/^uni-icons:/, '').trim()
}
function getUviewIconName(icon) {
return String(icon || '').replace(/^uview-plus:/, '').trim()
}
const props = defineProps({
visible: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['close', 'update'])
const userMenuList = ref([])
const configuredRecords = ref([])
const originConfIds = ref([])
const configuredIds = ref([])
const dragTargets = ref([])
const dropTarget = ref(null)
const draggingState = ref(null)
const lastDragEndAt = ref(0)
const menuMap = computed(() => {
const map = {}
userMenuList.value.forEach(item => {
map[item.id] = item
})
return map
})
const draggingMenu = computed(() => {
if (!draggingState.value) {
return null
}
return menuMap.value[draggingState.value.id] || null
})
const dragGhostStyle = computed(() => {
if (!draggingState.value) {
return {}
}
return {
left: `${draggingState.value.x - 48}px`,
top: `${draggingState.value.y - 48}px`
}
})
const configuredMenuList = computed(() => {
return configuredIds.value
.map(id => menuMap.value[id])
.filter(Boolean)
})
const unconfiguredMenuList = computed(() => {
return userMenuList.value
.filter(item => !configuredIds.value.includes(item.id))
})
async function loadData() {
try {
const menuRes = await getNavPermissionInfo()
userMenuList.value = buildNavMenuViewModels(menuRes?.data?.menus)
const navRes = await getUserNavMenuList()
configuredRecords.value = Array.isArray(navRes?.data) ? [...navRes.data] : []
configuredRecords.value.sort((left, right) => Number(left?.sort || 0) - Number(right?.sort || 0))
configuredIds.value = configuredRecords.value
.map(item => item.menuId)
.filter(menuId => !!menuMap.value[menuId])
originConfIds.value = [...configuredIds.value]
await nextTick()
collectDropTargets()
} catch (error) {
console.error('加载菜单数据失败:', error)
}
}
function handleConfiguredClick(item) {
if (shouldIgnoreClick()) {
return
}
configuredIds.value = configuredIds.value.filter(id => id !== item.id)
}
function handleUnconfiguredClick(item) {
if (shouldIgnoreClick()) {
return
}
if (!configuredIds.value.includes(item.id)) {
configuredIds.value.push(item.id)
}
}
function handleReset() {
configuredIds.value = [...originConfIds.value]
}
async function handleConfirm() {
try {
const userId = userStore.userId
const data = configuredIds.value.map((menuId, index) => ({
menuId,
userId,
sort: index,
status: 1
}))
await updateUserNavMenuList(data)
uni.showToast({ title: t('common.updateSuccess'), icon: 'success' })
emit('update')
handleClose()
} catch (error) {
console.error('保存失败:', error)
uni.showToast({ title: t('common.saveFailed'), icon: 'none' })
}
}
function handleClose() {
resetDragState()
emit('close')
}
function shouldIgnoreClick() {
return Date.now() - lastDragEndAt.value < 300
}
function getTouchPoint(event) {
const touch = event?.changedTouches?.[0] || event?.touches?.[0]
if (touch) {
return {
x: touch.clientX ?? touch.pageX ?? 0,
y: touch.clientY ?? touch.pageY ?? 0
}
}
if (typeof event?.detail?.x === 'number' && typeof event?.detail?.y === 'number') {
return {
x: event.detail.x,
y: event.detail.y
}
}
return null
}
function collectDropTargets() {
if (!instance?.proxy) {
return
}
const selectors = [
{ selector: '#configured-zone', kind: 'section', section: 'configured' },
{ selector: '#unconfigured-zone', kind: 'section', section: 'unconfigured' }
]
configuredMenuList.value.forEach((item) => {
selectors.push({
selector: `#configured-item-${item.id}`,
kind: 'item',
section: 'configured',
id: item.id
})
})
unconfiguredMenuList.value.forEach((item) => {
selectors.push({
selector: `#unconfigured-item-${item.id}`,
kind: 'item',
section: 'unconfigured',
id: item.id
})
})
const query = uni.createSelectorQuery().in(instance.proxy)
selectors.forEach((item) => {
query.select(item.selector).boundingClientRect()
})
query.exec((rects = []) => {
dragTargets.value = rects
.map((rect, index) => {
if (!rect) {
return null
}
return {
...selectors[index],
...rect
}
})
.filter(Boolean)
})
}
function updateDropTarget(point) {
const itemTarget = dragTargets.value.find((item) => {
return item.kind === 'item'
&& point.x >= item.left
&& point.x <= item.right
&& point.y >= item.top
&& point.y <= item.bottom
})
if (itemTarget) {
dropTarget.value = {
section: itemTarget.section,
id: itemTarget.id,
position: point.y < itemTarget.top + itemTarget.height / 2 ? 'before' : 'after'
}
return
}
const sectionTarget = dragTargets.value.find((item) => {
return item.kind === 'section'
&& point.x >= item.left
&& point.x <= item.right
&& point.y >= item.top
&& point.y <= item.bottom
})
dropTarget.value = sectionTarget
? { section: sectionTarget.section, id: null, position: 'end' }
: null
}
async function handleItemLongPress(item, section, event) {
const point = getTouchPoint(event) || { x: 0, y: 0 }
draggingState.value = {
id: item.id,
section,
x: point.x,
y: point.y
}
dropTarget.value = {
section,
id: item.id,
position: 'after'
}
await nextTick()
collectDropTargets()
updateDropTarget(point)
}
function handleDragMove(event) {
if (!draggingState.value) {
return
}
const point = getTouchPoint(event)
if (!point) {
return
}
draggingState.value = {
...draggingState.value,
x: point.x,
y: point.y
}
updateDropTarget(point)
}
function applyDrop() {
if (!draggingState.value || !dropTarget.value) {
return
}
if (dropTarget.value.section === 'unconfigured') {
configuredIds.value = configuredIds.value.filter(id => id !== draggingState.value.id)
return
}
if (dropTarget.value.section !== 'configured') {
return
}
const nextIds = configuredIds.value.filter(id => id !== draggingState.value.id)
let insertIndex = nextIds.length
if (dropTarget.value.id != null) {
const targetIndex = nextIds.indexOf(dropTarget.value.id)
if (targetIndex !== -1) {
insertIndex = dropTarget.value.position === 'before' ? targetIndex : targetIndex + 1
}
}
nextIds.splice(insertIndex, 0, draggingState.value.id)
configuredIds.value = nextIds
}
function handleDragEnd() {
if (!draggingState.value) {
return
}
applyDrop()
lastDragEndAt.value = Date.now()
resetDragState()
}
function resetDragState() {
dropTarget.value = null
draggingState.value = null
}
function isItemDropTarget(section, itemId) {
return dropTarget.value?.section === section && dropTarget.value?.id === itemId
}
function isSectionDropTarget(section) {
return dropTarget.value?.section === section && dropTarget.value?.id == null
}
watch(() => props.visible, async (val) => {
if (val) {
await loadData()
return
}
resetDragState()
})
watch([configuredMenuList, unconfiguredMenuList], async () => {
if (!props.visible) {
return
}
await nextTick()
collectDropTargets()
})
</script>
<style lang="scss" scoped>
.nav-menu-editor-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
display: flex;
align-items: flex-end;
padding-bottom: calc(100rpx + env(safe-area-inset-bottom));
box-sizing: border-box;
}
.nav-menu-editor {
width: 100%;
max-height: 66.6vh;
background: #ffffff;
border-radius: 32rpx 32rpx 0 0;
display: flex;
flex-direction: column;
}
.editor-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 32rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.editor-title {
font-size: 34rpx;
font-weight: 600;
color: #333;
}
.editor-close {
padding: 16rpx;
}
.editor-content {
flex: 1;
overflow-y: auto;
padding: 24rpx;
}
.divider-line {
height: 1rpx;
background: #f0f0f0;
margin: 24rpx 0;
}
.nav-section {
margin-bottom: 8rpx;
}
.nav-section-title {
font-size: 28rpx;
color: #999;
margin-bottom: 20rpx;
display: block;
}
.nav-grid {
display: flex;
flex-wrap: wrap;
min-height: 120rpx;
border-radius: 24rpx;
transition: background-color 0.15s ease;
}
.nav-grid-target {
background: rgba(34, 72, 110, 0.06);
}
.nav-item {
width: 25%;
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 24rpx;
padding: 16rpx;
border-radius: 16rpx;
transition: all 0.15s;
&:active {
opacity: 0.7;
}
&.nav-item-disabled {
opacity: 0.72;
}
}
.is-drop-target {
background: rgba(34, 72, 110, 0.08);
}
.is-dragging-source {
opacity: 0.25;
}
.nav-icon {
width: 96rpx;
height: 96rpx;
border-radius: 24rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 12rpx;
&.nav-icon-disabled {
opacity: 0.55;
}
.nav-icon-text {
font-size: 28rpx;
color: #1a3a5c;
font-weight: 700;
}
}
.nav-text {
font-size: 24rpx;
color: #333;
text-align: center;
&.nav-text-disabled {
color: #999;
}
}
.click-hint {
font-size: 24rpx;
color: #999;
text-align: center;
display: block;
margin-top: 16rpx;
}
.secondary-hint {
padding-bottom: 24rpx;
}
.editor-footer {
display: flex;
gap: 24rpx;
padding: 24rpx 32rpx;
border-top: 1rpx solid #f0f0f0;
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
}
.btn-reset,
.btn-confirm {
flex: 1;
height: 88rpx;
border-radius: 44rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
}
.btn-reset {
background: #f5f5f5;
color: #666;
}
.btn-confirm {
background: #22486e;
color: #ffffff;
}
.drag-ghost {
position: fixed;
z-index: 1002;
width: 96rpx;
margin-left: -8rpx;
pointer-events: none;
display: flex;
flex-direction: column;
align-items: center;
}
</style>

@ -0,0 +1,225 @@
<template>
<view v-if="visible" class="nav-menu-more-mask" @click="handleClose">
<view class="nav-menu-more" @click.stop>
<view class="more-header">
<text class="more-title">{{ t('dashboard.allNavMenu') }}</text>
<view class="more-actions">
<view class="action-btn edit-btn" @click="handleEdit">
<uni-icons type="compose" size="22" color="#22486e"></uni-icons>
</view>
</view>
</view>
<scroll-view scroll-y class="more-content">
<view v-if="displayMenuList.length === 0" class="empty-state">
<text class="empty-text">{{ t('dashboard.clickHint') }}</text>
</view>
<view v-else class="nav-grid">
<view
v-for="item in displayMenuList"
:key="item.id"
class="nav-item"
@click="handleNavClick(item)"
>
<view class="nav-icon" :style="{ background: hexToRgba(item.accentColor, 0.1) }">
<uni-icons v-if="isUniIcon(item.icon)" :type="getUniIconName(item.icon)" size="24" :color="item.accentColor" />
<u-icon v-else-if="isUviewIcon(item.icon)" :name="getUviewIconName(item.icon)" size="24" :color="item.accentColor"></u-icon>
<text v-else class="nav-icon-text">{{ item.symbol }}</text>
</view>
<text class="nav-text">{{ item.displayName }}</text>
</view>
</view>
</scroll-view>
</view>
</view>
</template>
<script setup>
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
function hexToRgba(hex, alpha) {
const value = String(hex || '').replace('#', '')
if (value.length !== 6) {
return `rgba(45, 90, 135, ${alpha})`
}
const red = parseInt(value.slice(0, 2), 16)
const green = parseInt(value.slice(2, 4), 16)
const blue = parseInt(value.slice(4, 6), 16)
return `rgba(${red}, ${green}, ${blue}, ${alpha})`
}
function isUniIcon(icon) {
return String(icon || '').startsWith('uni-icons:')
}
function isUviewIcon(icon) {
return String(icon || '').startsWith('uview-plus:')
}
function getUniIconName(icon) {
return String(icon || '').replace(/^uni-icons:/, '').trim()
}
function getUviewIconName(icon) {
return String(icon || '').replace(/^uview-plus:/, '').trim()
}
const props = defineProps({
visible: {
type: Boolean,
default: false
},
menuList: {
type: Array,
default: () => []
}
})
const emit = defineEmits(['close', 'edit'])
const displayMenuList = computed(() => Array.isArray(props.menuList) ? props.menuList : [])
function handleNavClick(item) {
if (item.route) {
uni.navigateTo({ url: item.route })
} else {
uni.showToast({ title: t('common.moduleBuilding'), icon: 'none' })
}
}
function handleEdit() {
emit('edit')
}
function handleClose() {
emit('close')
}
</script>
<style lang="scss" scoped>
.nav-menu-more-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
display: flex;
align-items: flex-end;
padding-bottom: calc(100rpx + env(safe-area-inset-bottom));
box-sizing: border-box;
}
.nav-menu-more {
width: 100%;
max-height: 66.6vh;
background: #ffffff;
border-radius: 32rpx 32rpx 0 0;
display: flex;
flex-direction: column;
}
.more-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 32rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.more-title {
font-size: 34rpx;
font-weight: 600;
color: #333;
}
.more-actions {
display: flex;
align-items: center;
gap: 24rpx;
}
.edit-btn {
justify-content: center;
}
.action-btn {
display: flex;
align-items: center;
gap: 8rpx;
padding: 12rpx 20rpx;
&.edit-btn {
background: rgba(34, 72, 110, 0.1);
border-radius: 20rpx;
font-size: 26rpx;
color: #22486e;
}
&.close-btn {
padding: 12rpx;
}
}
.more-content {
flex: 1;
padding: 24rpx;
}
.empty-state {
min-height: 220rpx;
display: flex;
align-items: center;
justify-content: center;
}
.empty-text {
font-size: 26rpx;
color: #999;
text-align: center;
}
.nav-grid {
display: flex;
flex-wrap: wrap;
}
.nav-item {
width: 20%;
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 32rpx;
padding: 16rpx;
&:active {
opacity: 0.7;
}
}
.nav-icon {
width: 100rpx;
height: 100rpx;
border-radius: 28rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16rpx;
.nav-icon-text {
font-size: 28rpx;
color: #1a3a5c;
font-weight: 700;
}
}
.nav-text {
font-size: 26rpx;
color: #333;
text-align: center;
}
</style>

@ -1,46 +1,156 @@
<template> <template>
<view class="nav-section"> <view class="nav-section">
<view class="section-title">{{ t('dashboard.functionNav') }}</view> <view class="section-header">
<text class="section-title">{{ t('dashboard.functionNav') }}</text>
<view class="more-btn" @click="handleMoreClick">
<text class="more-text">{{ t('common.more') }}</text>
<uni-icons type="arrow-right" size="18" color="#999"></uni-icons>
</view>
</view>
<view class="nav-grid"> <view class="nav-grid">
<view v-for="(item, index) in navList" :key="index" class="nav-item" @click="handleNavClick(item)"> <view v-for="(item, index) in displayNavList" :key="item.id || index" class="nav-item" @click="handleNavClick(item)">
<view class="nav-icon" :style="{ backgroundColor: item.bgColor }"> <view class="nav-icon" :style="{ background: hexToRgba(item.accentColor, 0.1) }">
<text class="nav-icon-text">{{ item.icon }}</text> <uni-icons v-if="isUniIcon(item.icon)" :type="getUniIconName(item.icon)" size="24" :color="item.accentColor" />
<u-icon v-else-if="isUviewIcon(item.icon)" :name="getUviewIconName(item.icon)" size="24" :color="item.accentColor"></u-icon>
<text v-else class="nav-icon-text">{{ item.symbol }}</text>
</view> </view>
<text class="nav-text">{{ t(`dashboard.${item.key}`) }}</text> <text class="nav-text">{{ item.displayName }}</text>
</view> </view>
</view> </view>
<NavMenuMore
:visible="showMoreModal"
:menu-list="moreNavList"
@close="showMoreModal = false"
@edit="handleEdit"
/>
<NavMenuEditor
:visible="showEditModal"
@close="showEditModal = false"
@update="handleUpdate"
/>
</view> </view>
</template> </template>
<script setup> <script setup>
import { reactive } from 'vue' import { ref, computed, onMounted, watch } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { getUserNavMenuList } from '@/api/system/userNavMenu'
import { getNavPermissionInfo } from '@/api/login'
import { buildNavMenuViewModels } from '@/utils/permissionMenu'
import NavMenuMore from './NavMenuMore.vue'
import NavMenuEditor from './NavMenuEditor.vue'
const { t } = useI18n() const { t } = useI18n()
const navList = reactive([ function hexToRgba(hex, alpha) {
{ key: 'mold', icon: '🔧', bgColor: '#1a3a5c', path: '/pages_function/mold' }, const value = String(hex || '').replace('#', '')
{ key: 'equipment', icon: '⚙️', bgColor: '#2d5a87', path: '/pages_function/equipment' }, if (value.length !== 6) {
{ key: 'keypart', icon: '🔩', bgColor: '#3d7ab5', path: '/pages_function/keypart' }, return `rgba(45, 90, 135, ${alpha})`
{ key: 'spare', icon: '📦', bgColor: '#4a90c2', path: '/pages_function/spare' }, }
{ key: 'product', icon: '🧾', bgColor: '#5aa0d2', path: '/pages_function/product' } const red = parseInt(value.slice(0, 2), 16)
]) const green = parseInt(value.slice(2, 4), 16)
const blue = parseInt(value.slice(4, 6), 16)
return `rgba(${red}, ${green}, ${blue}, ${alpha})`
}
function handleNavClick(item) { function isUniIcon(icon) {
const navMap = { return String(icon || '').startsWith('uni-icons:')
mold: '/pages_function/pages/mold/index', }
equipment: '/pages_function/pages/equipment/index',
spare: '/pages_function/pages/spare/index', function isUviewIcon(icon) {
keypart: '/pages_function/pages/keypart/index', return String(icon || '').startsWith('uview-plus:')
product: '/pages_function/pages/product/index' }
function getUniIconName(icon) {
return String(icon || '').replace(/^uni-icons:/, '').trim()
}
function getUviewIconName(icon) {
return String(icon || '').replace(/^uview-plus:/, '').trim()
}
const showMoreModal = ref(false)
const showEditModal = ref(false)
const configuredMenuList = ref([])
const allMenuList = ref([])
const defaultNavList = [
{ id: 'default-mold', displayName: '模具', symbol: '🔧', accentColor: '#1a3a5c', route: '/pages_function/pages/mold/index' },
{ id: 'default-equipment', displayName: '设备', symbol: '⚙️', accentColor: '#2d5a87', route: '/pages_function/pages/equipment/index' },
{ id: 'default-keypart', displayName: '配件', symbol: '🔩', accentColor: '#3d7ab5', route: '/pages_function/pages/keypart/index' },
{ id: 'default-spare', displayName: '备件', symbol: '📦', accentColor: '#4a90c2', route: '/pages_function/pages/spare/index' },
{ id: 'default-product', displayName: '产品', symbol: '🧾', accentColor: '#5aa0d2', route: '/pages_function/pages/product/index' }
]
const displayNavList = computed(() => {
if (configuredMenuList.value.length === 0) {
return defaultNavList
}
return configuredMenuList.value.slice(0, 5)
})
const moreNavList = computed(() => {
if (configuredMenuList.value.length === 0) {
return defaultNavList
}
return configuredMenuList.value
})
async function loadConfiguredMenus() {
try {
const menuRes = await getNavPermissionInfo()
allMenuList.value = buildNavMenuViewModels(menuRes?.data?.menus)
const navRes = await getUserNavMenuList()
const configuredRecords = Array.isArray(navRes?.data) ? [...navRes.data] : []
configuredRecords.sort((left, right) => Number(left?.sort || 0) - Number(right?.sort || 0))
const configuredIds = configuredRecords.map(item => item.menuId)
if (configuredIds.length === 0) {
configuredMenuList.value = []
return
}
const configuredMenus = allMenuList.value
.filter(item => configuredIds.includes(item.id))
.sort((a, b) => {
const indexA = configuredIds.indexOf(a.id)
const indexB = configuredIds.indexOf(b.id)
return indexA - indexB
})
configuredMenuList.value = configuredMenus
} catch (error) {
console.error('加载配置菜单失败:', error)
configuredMenuList.value = []
} }
const url = navMap[item.key] }
if (url) {
uni.navigateTo({ url }) function handleNavClick(item) {
if (item.route) {
uni.navigateTo({ url: item.route })
} else { } else {
uni.showToast({ title: t('common.moduleBuilding'), icon: 'none' }) uni.showToast({ title: t('common.moduleBuilding'), icon: 'none' })
} }
} }
function handleMoreClick() {
showMoreModal.value = true
}
function handleEdit() {
showMoreModal.value = false
showEditModal.value = true
}
function handleUpdate() {
loadConfiguredMenus()
}
onMounted(() => {
loadConfiguredMenus()
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -52,11 +162,31 @@ function handleNavClick(item) {
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05); box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
} }
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
}
.section-title { .section-title {
font-size: 32rpx; font-size: 32rpx;
font-weight: 600; font-weight: 600;
color: #1a3a5c; color: #1a3a5c;
margin-bottom: 24rpx; }
.more-btn {
display: flex;
align-items: center;
gap: 6rpx;
padding: 8rpx 16rpx;
border-radius: 16rpx;
background: rgba(34, 72, 110, 0.08);
}
.more-text {
font-size: 26rpx;
color: #999;
} }
.nav-grid { .nav-grid {
@ -84,7 +214,9 @@ function handleNavClick(item) {
margin-bottom: 16rpx; margin-bottom: 16rpx;
.nav-icon-text { .nav-icon-text {
font-size: 44rpx; font-size: 28rpx;
color: #1a3a5c;
font-weight: 700;
} }
} }
@ -92,4 +224,4 @@ function handleNavClick(item) {
font-size: 26rpx; font-size: 26rpx;
color: #333333; color: #333333;
} }
</style> </style>

@ -7,7 +7,12 @@ export default {
moduleBuilding: 'This module is under construction', moduleBuilding: 'This module is under construction',
updateSuccess: 'Updated successfully', updateSuccess: 'Updated successfully',
confirmLogout: 'Are you sure you want to log out?', confirmLogout: 'Are you sure you want to log out?',
languageSwitched: 'Language switched' languageSwitched: 'Language switched',
more: 'More',
reset: 'Reset',
complete: 'Done',
edit: 'Edit',
saveFailed: 'Save failed'
}, },
tab: { tab: {
home: 'Home', home: 'Home',
@ -30,6 +35,12 @@ export default {
welcome: 'Welcome to', welcome: 'Welcome to',
subtitle: 'Besure Digital Intelligent Control Platform', subtitle: 'Besure Digital Intelligent Control Platform',
functionNav: 'Function Navigation', functionNav: 'Function Navigation',
editNavMenu: 'Edit Shortcuts',
configuredNav: 'Added',
unconfiguredNav: 'Add More',
dragHint: 'Drag icons to reorder, icons above will be displayed in control center',
clickHint: 'Tap icons to add or remove from configuration',
allNavMenu: 'All Functions',
productionOverview: 'Production Overview', productionOverview: 'Production Overview',
qualityOverview: 'Quality Overview', qualityOverview: 'Quality Overview',
productionPlan: 'Production Summary', productionPlan: 'Production Summary',

@ -1,8 +1,6 @@
import { createI18n } from 'vue-i18n' import { createI18n } from 'vue-i18n'
import zhCN from './zh-CN' import zhCN from './zh-CN'
import enUS from './en-US' import enUS from './en-US'
import useUserStore from '@/store/modules/user'
import { syncTabBarMenus } from '@/utils/permissionMenu'
const LOCALE_STORAGE_KEY = 'app_locale' const LOCALE_STORAGE_KEY = 'app_locale'
const LOCALE_CHANGE_EVENT = 'app-locale-changed' const LOCALE_CHANGE_EVENT = 'app-locale-changed'
@ -85,26 +83,6 @@ const literalMap = {
'点检记录详情': 'moldWorkOrder.detailTitle' '点检记录详情': 'moldWorkOrder.detailTitle'
} }
export function applyTabBarLanguage() {
try {
const pages = getCurrentPages()
if (!pages || pages.length === 0) return
const currentPage = pages[pages.length - 1]
if (!currentPage) return
const route = currentPage.route || ''
const tabBarPages = ['pages/index', 'pages/report', 'pages/work', 'pages/mine']
if (!tabBarPages.includes(route)) return
syncTabBarMenus(useUserStore().menus, {
homeText: i18n.global.t('tab.home'),
reportFallback: i18n.global.t('tab.report'),
workFallback: i18n.global.t('tab.work'),
mineText: i18n.global.t('tab.mine')
})
} catch (e) {
}
}
export function getCurrentLocale() { export function getCurrentLocale() {
return i18n.global.locale.value return i18n.global.locale.value
} }
@ -113,7 +91,6 @@ export function setLocale(locale) {
const nextLocale = normalizeLocale(locale) const nextLocale = normalizeLocale(locale)
i18n.global.locale.value = nextLocale i18n.global.locale.value = nextLocale
uni.setStorageSync(LOCALE_STORAGE_KEY, nextLocale) uni.setStorageSync(LOCALE_STORAGE_KEY, nextLocale)
applyTabBarLanguage()
uni.$emit(LOCALE_CHANGE_EVENT, nextLocale) uni.$emit(LOCALE_CHANGE_EVENT, nextLocale)
return nextLocale return nextLocale
} }

@ -7,7 +7,12 @@ export default {
moduleBuilding: '模块建设中~', moduleBuilding: '模块建设中~',
updateSuccess: '修改成功', updateSuccess: '修改成功',
confirmLogout: '确定注销并退出系统吗', confirmLogout: '确定注销并退出系统吗',
languageSwitched: '语言已切换' languageSwitched: '语言已切换',
more: '更多',
reset: '重置',
complete: '完成',
edit: '编辑',
saveFailed: '保存失败'
}, },
tab: { tab: {
home: '首页', home: '首页',
@ -30,6 +35,12 @@ export default {
welcome: '欢迎您使用', welcome: '欢迎您使用',
subtitle: '必硕数字化智能中控平台', subtitle: '必硕数字化智能中控平台',
functionNav: '功能导航', functionNav: '功能导航',
editNavMenu: '编辑快捷开关',
configuredNav: '已添加',
unconfiguredNav: '添加更多',
dragHint: '拖动图标进行排序,上方图标将显示在控制中心',
clickHint: '点击图标添加到已配置或取消配置',
allNavMenu: '全部功能',
productionOverview: '生产整体概况', productionOverview: '生产整体概况',
qualityOverview: '质量概况', qualityOverview: '质量概况',
productionPlan: '生产概括', productionPlan: '生产概括',

@ -625,36 +625,5 @@
] ]
} }
], ],
"tabBar": { "preloadRule": {}
"color": "#666666",
"selectedColor": "#1a3a5c",
"borderStyle": "white",
"backgroundColor": "#ffffff",
"list": [
{
"pagePath": "pages/index",
"iconPath": "static/images/tabbar/home.png",
"selectedIconPath": "static/images/tabbar/home_.png",
"text": "首页"
},
{
"pagePath": "pages/report",
"iconPath": "static/images/tabbar/report.png",
"selectedIconPath": "static/images/tabbar/report_.png",
"text": "报表"
},
{
"pagePath": "pages/work",
"iconPath": "static/images/tabbar/work.png",
"selectedIconPath": "static/images/tabbar/work_.png",
"text": "管理"
},
{
"pagePath": "pages/mine",
"iconPath": "static/images/tabbar/mine.png",
"selectedIconPath": "static/images/tabbar/mine_.png",
"text": "我的"
}
]
}
} }

@ -13,6 +13,8 @@
<view v-if="showGoTop" class="go-top-btn" @click="goTop"> <view v-if="showGoTop" class="go-top-btn" @click="goTop">
<text class="go-top-icon"></text> <text class="go-top-icon"></text>
</view> </view>
<TabBar />
</view> </view>
</template> </template>
@ -21,6 +23,7 @@ import { onMounted, onUnmounted, ref, computed } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { onLocaleChange, offLocaleChange, setNavigationTitle } from '@/locales' import { onLocaleChange, offLocaleChange, setNavigationTitle } from '@/locales'
import NavBar from '@/components/common/NavBar.vue' import NavBar from '@/components/common/NavBar.vue'
import TabBar from '@/components/common/TabBar.vue'
import BannerSection from '@/components/dashboard/BannerSection.vue' import BannerSection from '@/components/dashboard/BannerSection.vue'
import NavSection from '@/components/dashboard/NavSection.vue' import NavSection from '@/components/dashboard/NavSection.vue'
import StatsSection from '@/components/dashboard/StatsSection.vue' import StatsSection from '@/components/dashboard/StatsSection.vue'
@ -58,6 +61,7 @@ function goTop() {
flex-direction: column; flex-direction: column;
height: 100vh; height: 100vh;
background-color: #f0f2f5; background-color: #f0f2f5;
padding-bottom: 100rpx;
} }
.main-scroll { .main-scroll {

@ -106,7 +106,7 @@ async function pwdLogin() {
function loginSuccess(result) { function loginSuccess(result) {
userStore.getInfo().then(res => { userStore.getInfo().then(res => {
uni.switchTab({ uni.reLaunch({
url: '/pages/index' url: '/pages/index'
}); });
}) })

@ -80,6 +80,7 @@
</uni-popup-dialog> </uni-popup-dialog>
</uni-popup> </uni-popup>
</view> </view>
<TabBar />
</view> </view>
</template> </template>
@ -89,6 +90,7 @@ import { useI18n } from 'vue-i18n'
import useUserStore from '@/store/modules/user' import useUserStore from '@/store/modules/user'
import { onLocaleChange, offLocaleChange } from '@/locales' import { onLocaleChange, offLocaleChange } from '@/locales'
import NavBar from '@/components/common/NavBar.vue' import NavBar from '@/components/common/NavBar.vue'
import TabBar from '@/components/common/TabBar.vue'
const userStore = useUserStore() const userStore = useUserStore()
const name = userStore.name; const name = userStore.name;

@ -6,6 +6,7 @@
title="报表中心" title="报表中心"
subtitle="数据驱动决策 · 智能分析" subtitle="数据驱动决策 · 智能分析"
/> />
<TabBar />
</view> </view>
</template> </template>
@ -13,6 +14,7 @@
import { computed } from 'vue' import { computed } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue' import NavBar from '@/components/common/NavBar.vue'
import TabBar from '@/components/common/TabBar.vue'
import PermissionMenuPage from '@/components/common/PermissionMenuPage.vue' import PermissionMenuPage from '@/components/common/PermissionMenuPage.vue'
const { t } = useI18n() const { t } = useI18n()
@ -25,5 +27,6 @@ const pageTitle = computed(() => t('tab.report'))
flex-direction: column; flex-direction: column;
height: 100vh; height: 100vh;
background-color: #f5f6f7; background-color: #f5f6f7;
padding-bottom: 100rpx;
} }
</style> </style>

@ -8,6 +8,7 @@
:searchable="true" :searchable="true"
:show-go-top="true" :show-go-top="true"
/> />
<TabBar />
</view> </view>
</template> </template>
@ -15,6 +16,7 @@
import { computed } from 'vue' import { computed } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue' import NavBar from '@/components/common/NavBar.vue'
import TabBar from '@/components/common/TabBar.vue'
import PermissionMenuPage from '@/components/common/PermissionMenuPage.vue' import PermissionMenuPage from '@/components/common/PermissionMenuPage.vue'
const { t } = useI18n() const { t } = useI18n()
@ -27,5 +29,6 @@ const pageTitle = computed(() => t('tab.work'))
flex-direction: column; flex-direction: column;
height: 100vh; height: 100vh;
background-color: #f5f6f7; background-color: #f5f6f7;
padding-bottom: 100rpx;
} }
</style> </style>

@ -23,7 +23,7 @@ export default {
}, },
/** /**
* tabBar tabBar * tabbar 使 reLaunch switchTab
* @param url * @param url
* @returns * @returns
*/ */
@ -32,7 +32,7 @@ export default {
console.log(!!params?url + '?' + tansParams(params):url); console.log(!!params?url + '?' + tansParams(params):url);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
uni.switchTab({ uni.reLaunch({
url: !!params?url + '?' + tansParams(params):url, url: !!params?url + '?' + tansParams(params):url,
success: resolve, success: resolve,
fail: reject fail: reject

@ -3,7 +3,6 @@ import { getToken, setToken, removeToken } from "@/utils/auth";
import defAva from "@/static/images/profile.jpg"; import defAva from "@/static/images/profile.jpg";
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { initAllDict } from "@/utils/dict"; import { initAllDict } from "@/utils/dict";
import { syncTabBarMenus } from "@/utils/permissionMenu";
export interface LoginForm { export interface LoginForm {
username: string; username: string;
@ -65,7 +64,6 @@ const useUserStore = defineStore("user", {
} }
this.permissions = res.data.permissions; this.permissions = res.data.permissions;
this.menus = Array.isArray(res.data.menus) ? res.data.menus : []; this.menus = Array.isArray(res.data.menus) ? res.data.menus : [];
syncTabBarMenus(this.menus);
this.name = user.nickname; this.name = user.nickname;
this.userId = user.id; this.userId = user.id;
this.avatar = avatar; this.avatar = avatar;

@ -64,6 +64,19 @@ function toArray(value) {
return Array.isArray(value) ? value : [] return Array.isArray(value) ? value : []
} }
function walkMenus(menus, visitor, parent = null) {
toArray(menus).forEach((menu, index) => {
if (!menu || typeof menu !== 'object') {
return
}
visitor(menu, parent, index)
const children = toArray(menu.children)
if (children.length > 0) {
walkMenus(children, visitor, menu)
}
})
}
function normalizeMenuKey(value) { function normalizeMenuKey(value) {
return String(value || '') return String(value || '')
.trim() .trim()
@ -112,6 +125,42 @@ export function getTopLevelMenus(menus) {
return toArray(menus).filter((menu) => menu && typeof menu === 'object') return toArray(menus).filter((menu) => menu && typeof menu === 'object')
} }
export function flattenMenus(menus) {
const result = []
walkMenus(menus, (menu, parent, index) => {
result.push({
...menu,
_parent: parent || null,
_levelIndex: index
})
})
return result
}
export function getConfigurableNavMenus(menus) {
const dedupe = new Set()
return flattenMenus(menus).filter((menu) => {
if (menu.type !== 2 || menu.id == null || dedupe.has(menu.id)) {
return false
}
dedupe.add(menu.id)
return true
})
}
export function buildNavMenuViewModels(menus) {
return getConfigurableNavMenus(menus).map((menu, index) => {
const displayName = String(menu.name || menu.enName || '').trim() || `菜单${index + 1}`
return {
...menu,
displayName,
route: resolveMenuUrl(menu),
symbol: getMenuSymbol(displayName, index),
accentColor: getModuleColor(index)
}
})
}
export function getDynamicTabMenus(menus) { export function getDynamicTabMenus(menus) {
return getTopLevelMenus(menus).filter((menu) => !looksLikeHomeMenu(menu)) return getTopLevelMenus(menus).filter((menu) => !looksLikeHomeMenu(menu))
} }
@ -167,17 +216,10 @@ export function getMenuSymbol(name, index) {
export function syncTabBarMenus(menus, options = {}) { export function syncTabBarMenus(menus, options = {}) {
const dynamicMenus = getDynamicTabMenus(menus) const dynamicMenus = getDynamicTabMenus(menus)
const labels = [ return [
options.homeText || '首页', options.homeText || '首页',
dynamicMenus[0]?.name || options.reportFallback || '报表', dynamicMenus[0]?.name || options.reportFallback || '报表',
dynamicMenus[1]?.name || dynamicMenus[0]?.name || options.workFallback || '管理', dynamicMenus[1]?.name || dynamicMenus[0]?.name || options.workFallback || '管理',
options.mineText || '我的' options.mineText || '我的'
] ]
labels.forEach((text, index) => {
uni.setTabBarItem({
index,
text
})
})
} }
Loading…
Cancel
Save