Compare commits
7 Commits
a97d9223b6
...
1846e783a3
| Author | SHA1 | Date |
|---|---|---|
|
|
1846e783a3 | 2 days ago |
|
|
bdd15e5104 | 2 days ago |
|
|
4bc2589ced | 2 days ago |
|
|
af78c2ffa3 | 3 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