Compare commits
7 Commits
a97d9223b6
...
1846e783a3
| Author | SHA1 | Date |
|---|---|---|
|
|
1846e783a3 | 2 days ago |
|
|
bdd15e5104 | 2 days ago |
|
|
4bc2589ced | 2 days ago |
|
|
af78c2ffa3 | 2 days ago |
|
|
b849d644ee | 3 days ago |
|
|
b0004e30c7 | 3 days ago |
|
|
e632a6dd8f | 3 days ago |
@ -0,0 +1 @@
|
||||
VITE_APP_BASE_URL=http://192.168.5.106:48081
|
||||
@ -0,0 +1 @@
|
||||
VITE_APP_BASE_URL=https://besure.ngsk.tech:7001
|
||||
@ -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
|
||||
})
|
||||
}
|
||||
@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<view>
|
||||
<up-navbar
|
||||
:title="translatedTitle"
|
||||
:bgColor="navbarBgColor"
|
||||
:titleStyle="titleStyleObj"
|
||||
:leftIcon="showBackBtn ? 'arrow-left' : ''"
|
||||
:leftIconColor="navTextColor"
|
||||
:leftIconSize="20"
|
||||
:autoBack="false"
|
||||
:placeholder="true"
|
||||
:safeAreaInsetTop="true"
|
||||
@leftClick="handleBack"
|
||||
>
|
||||
<template #right>
|
||||
<slot name="right"></slot>
|
||||
</template>
|
||||
</up-navbar>
|
||||
<view v-if="subTitle" class="navbar-subtitle-wrap">
|
||||
<text class="navbar-subtitle">{{ translatedSubTitle }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { translateLiteral } from '@/locales'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
subTitle: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
backgroundColor: {
|
||||
type: String,
|
||||
default: '#22486e'
|
||||
},
|
||||
textColor: {
|
||||
type: String,
|
||||
default: '#ffffff'
|
||||
},
|
||||
showBack: {
|
||||
type: Boolean,
|
||||
default: undefined
|
||||
}
|
||||
})
|
||||
|
||||
const currentPagesLength = ref(1)
|
||||
|
||||
const isLoginPage = computed(() => {
|
||||
const pages = getCurrentPages()
|
||||
if (pages && pages.length > 0) {
|
||||
return pages[pages.length - 1].route === 'pages/login'
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
const showBackBtn = computed(() => {
|
||||
if (props.showBack !== undefined) return props.showBack
|
||||
return currentPagesLength.value > 1
|
||||
})
|
||||
|
||||
const navbarBgColor = computed(() => {
|
||||
return isLoginPage.value ? '#ffffff' : props.backgroundColor
|
||||
})
|
||||
|
||||
const navTextColor = computed(() => {
|
||||
return isLoginPage.value ? '#000000' : props.textColor
|
||||
})
|
||||
|
||||
const translatedTitle = computed(() => translateLiteral(props.title))
|
||||
|
||||
const translatedSubTitle = computed(() => translateLiteral(props.subTitle))
|
||||
|
||||
const titleStyleObj = computed(() => {
|
||||
return {
|
||||
color: navTextColor.value,
|
||||
fontWeight: '700',
|
||||
fontSize: '34rpx'
|
||||
}
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
const pages = getCurrentPages()
|
||||
currentPagesLength.value = pages ? pages.length : 1
|
||||
})
|
||||
|
||||
function handleBack() {
|
||||
if (!showBackBtn.value) return
|
||||
uni.navigateBack({
|
||||
fail: () => {
|
||||
uni.reLaunch({
|
||||
url: '/pages/index'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.navbar-subtitle-wrap {
|
||||
padding: 14rpx 24rpx 20rpx;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.navbar-subtitle {
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
color: #1a3a5c;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,126 @@
|
||||
<template>
|
||||
<up-tabbar
|
||||
:value="activeIndex"
|
||||
:activeColor="activeColor"
|
||||
:inactiveColor="inactiveColor"
|
||||
:safeAreaInsetBottom="true"
|
||||
:fixed="true"
|
||||
:placeholder="true"
|
||||
:border="false"
|
||||
@change="handleChange"
|
||||
zIndex="100"
|
||||
>
|
||||
<up-tabbar-item
|
||||
v-for="(item, index) in tabList"
|
||||
:key="index"
|
||||
:text="item.text"
|
||||
:name="index"
|
||||
>
|
||||
<template #active-icon>
|
||||
<image :src="item.selectedIcon" class="tabbar-icon" mode="widthFix" />
|
||||
</template>
|
||||
<template #inactive-icon>
|
||||
<image :src="item.icon" class="tabbar-icon" mode="widthFix" />
|
||||
</template>
|
||||
</up-tabbar-item>
|
||||
</up-tabbar>
|
||||
</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 inactiveColor = '#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'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
activeIndex.value = getCurrentActiveIndex()
|
||||
})
|
||||
|
||||
function handleChange(index) {
|
||||
if (activeIndex.value === index) return
|
||||
activeIndex.value = index
|
||||
const item = tabList.value[index]
|
||||
if (item) {
|
||||
uni.reLaunch({
|
||||
url: item.path
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => useUserStore().menus, () => {
|
||||
activeIndex.value = getCurrentActiveIndex()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tabbar-icon {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,377 @@
|
||||
<template>
|
||||
<view v-if="renderVisible" class="nav-menu-editor-mask" :class="{ 'mask-hidden': !animVisible }" @click="handleClose">
|
||||
<view class="nav-menu-editor" :class="{ 'panel-hidden': !animVisible }" @click.stop>
|
||||
<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 class="nav-grid">
|
||||
<view v-for="item in configuredMenuList" :key="item.id" class="nav-item is-selected">
|
||||
<view class="nav-icon" :style="{ background: '#e1e6eb' }">
|
||||
<uni-icons v-if="isUniIcon(item.icon)" :type="getUniIconName(item.icon)" size="28"
|
||||
:color="item.accentColor" />
|
||||
<u-icon v-else-if="isUviewIcon(item.icon)" :name="getUviewIconName(item.icon)" size="28"
|
||||
:color="item.accentColor"></u-icon>
|
||||
<text v-else class="nav-icon-text">{{ item.symbol }}</text>
|
||||
</view>
|
||||
<text class="nav-text">{{ item.displayName }}</text>
|
||||
<view class="check-badge" @click.stop="handleConfiguredClick(item)">
|
||||
<u-icon name="minus-circle-fill" size="18" color="#de3730"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="divider-line"></view>
|
||||
|
||||
<view class="nav-section">
|
||||
<text class="nav-section-title">{{ t('dashboard.unconfiguredNav') }}</text>
|
||||
<view class="nav-grid">
|
||||
<view v-for="item in unconfiguredMenuList" :key="item.id" class="nav-item"
|
||||
@click="handleUnconfiguredClick(item)">
|
||||
<view class="nav-icon nav-icon-disabled" :style="{ background: '#e1e6eb' }">
|
||||
<uni-icons v-if="isUniIcon(item.icon)" :type="getUniIconName(item.icon)" size="28"
|
||||
:color="item.accentColor" />
|
||||
<u-icon v-else-if="isUviewIcon(item.icon)" :name="getUviewIconName(item.icon)" size="28"
|
||||
: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.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>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, nextTick } 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()
|
||||
|
||||
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 renderVisible = ref(false)
|
||||
const animVisible = ref(false)
|
||||
|
||||
watch(() => props.visible, (val) => {
|
||||
if (val) {
|
||||
renderVisible.value = true
|
||||
nextTick(() => {
|
||||
setTimeout(() => { animVisible.value = true }, 30)
|
||||
})
|
||||
} else {
|
||||
animVisible.value = false
|
||||
setTimeout(() => { renderVisible.value = false }, 300)
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
const userMenuList = ref([])
|
||||
const configuredRecords = ref([])
|
||||
const originConfIds = ref([])
|
||||
const configuredIds = ref([])
|
||||
|
||||
const menuMap = computed(() => {
|
||||
const map = {}
|
||||
userMenuList.value.forEach(item => {
|
||||
map[item.id] = item
|
||||
})
|
||||
return map
|
||||
})
|
||||
|
||||
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]
|
||||
} catch (error) {
|
||||
console.error('加载菜单数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
function handleConfiguredClick(item) {
|
||||
configuredIds.value = configuredIds.value.filter(id => id !== item.id)
|
||||
}
|
||||
|
||||
function handleUnconfiguredClick(item) {
|
||||
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() {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
watch(() => props.visible, async (val) => {
|
||||
if (val) {
|
||||
await loadData()
|
||||
}
|
||||
})
|
||||
</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(90rpx + env(safe-area-inset-bottom));
|
||||
box-sizing: border-box;
|
||||
transition: opacity 0.3s ease;
|
||||
|
||||
&.mask-hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-menu-editor {
|
||||
width: 100%;
|
||||
max-height: 75vh;
|
||||
background: #ffffff;
|
||||
border-radius: 32rpx 32rpx 0 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: transform 0.3s ease;
|
||||
|
||||
&.panel-hidden {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
width: 25%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 24rpx;
|
||||
padding: 16rpx;
|
||||
border-radius: 16rpx;
|
||||
position: relative;
|
||||
transition: all 0.15s;
|
||||
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 12rpx;
|
||||
|
||||
.nav-icon-text {
|
||||
font-size: 32rpx;
|
||||
color: #1a3a5c;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-text {
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
|
||||
&.nav-text-disabled {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.check-badge {
|
||||
position: absolute;
|
||||
top: 8rpx;
|
||||
right: 26rpx;
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.click-hint {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
text-align: center;
|
||||
display: block;
|
||||
margin-top: 16rpx;
|
||||
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;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,250 @@
|
||||
<template>
|
||||
<view v-if="renderVisible" class="nav-menu-more-mask" :class="{ 'mask-hidden': !animVisible }" @click="handleClose">
|
||||
<view class="nav-menu-more" :class="{ 'panel-hidden': !animVisible }" @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: '#e1e6eb' }">
|
||||
<uni-icons v-if="isUniIcon(item.icon)" :type="getUniIconName(item.icon)" size="28" :color="item.accentColor" />
|
||||
<u-icon v-else-if="isUviewIcon(item.icon)" :name="getUviewIconName(item.icon)" size="28" :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, ref, watch, nextTick } 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 renderVisible = ref(false)
|
||||
const animVisible = ref(false)
|
||||
|
||||
watch(() => props.visible, (val) => {
|
||||
if (val) {
|
||||
renderVisible.value = true
|
||||
nextTick(() => {
|
||||
setTimeout(() => { animVisible.value = true }, 30)
|
||||
})
|
||||
} else {
|
||||
animVisible.value = false
|
||||
setTimeout(() => { renderVisible.value = false }, 300)
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
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(90rpx + env(safe-area-inset-bottom));
|
||||
box-sizing: border-box;
|
||||
transition: opacity 0.3s ease;
|
||||
|
||||
&.mask-hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-menu-more {
|
||||
width: 100%;
|
||||
max-height: 66.6vh;
|
||||
background: #ffffff;
|
||||
border-radius: 32rpx 32rpx 0 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: transform 0.3s ease;
|
||||
|
||||
&.panel-hidden {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
}
|
||||
|
||||
.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: 25%;
|
||||
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: 32rpx;
|
||||
color: #1a3a5c;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-text {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@ -1,48 +0,0 @@
|
||||
const DEFAULT_NAV_BACKGROUND = '#22486e'
|
||||
const DEFAULT_NAV_FRONT = '#ffffff'
|
||||
const LOGIN_NAV_BACKGROUND = '#ffffff'
|
||||
const LOGIN_NAV_FRONT = '#000000'
|
||||
|
||||
function getCurrentRoute() {
|
||||
try {
|
||||
const pages = getCurrentPages()
|
||||
if (!pages || pages.length === 0) return ''
|
||||
return pages[pages.length - 1]?.route || ''
|
||||
} catch (error) {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
function getNavigationTheme(route = '') {
|
||||
const currentRoute = route || getCurrentRoute()
|
||||
if (currentRoute === 'pages/login') {
|
||||
return {
|
||||
frontColor: LOGIN_NAV_FRONT,
|
||||
backgroundColor: LOGIN_NAV_BACKGROUND
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
frontColor: DEFAULT_NAV_FRONT,
|
||||
backgroundColor: DEFAULT_NAV_BACKGROUND
|
||||
}
|
||||
}
|
||||
|
||||
export function applyNavigationBarTheme(route = '') {
|
||||
const theme = getNavigationTheme(route)
|
||||
try {
|
||||
uni.setNavigationBarColor({
|
||||
frontColor: theme.frontColor,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
animation: {
|
||||
duration: 0,
|
||||
timingFunc: 'linear'
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
}
|
||||
}
|
||||
|
||||
export function getDefaultNavigationBackground() {
|
||||
return DEFAULT_NAV_BACKGROUND
|
||||
}
|
||||
Loading…
Reference in New Issue