Compare commits
No commits in common. '1846e783a3704f47eaeedf9f402e7bd2e85ca94b' and 'a97d9223b6a8247c593f8be72b19bb644a13f5a4' have entirely different histories.
1846e783a3
...
a97d9223b6
@ -1 +0,0 @@
|
|||||||
VITE_APP_BASE_URL=http://192.168.5.106:48081
|
|
||||||
@ -1 +0,0 @@
|
|||||||
VITE_APP_BASE_URL=https://besure.ngsk.tech:7001
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@ -1,119 +0,0 @@
|
|||||||
<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>
|
|
||||||
@ -1,126 +0,0 @@
|
|||||||
<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>
|
|
||||||
@ -1,377 +0,0 @@
|
|||||||
<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>
|
|
||||||
@ -1,250 +0,0 @@
|
|||||||
<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>
|
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
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