SideMenu.vue 8.12 KB
<template>
  <view v-if="isShown" class="drawer-mask" @click="close">
    <view class="drawer-panel" :class="animClass" :style="{ paddingTop: statusBarHeight + 'px' }" @click.stop>
      <view class="drawer-top">
        <view class="user-row">
          <view class="avatar-circle">
            <AppIcon name="user" size="sm" color="white" />
          </view>
          <view class="user-texts">
            <text class="user-name">{{ userName }}</text>
            <text class="store-label">{{ storeName }}</text>
          </view>
        </view>
      </view>

      <view class="drawer-menu">
        <template v-for="item in items" :key="item.key">
          <!-- 有子菜单的项 -->
          <view v-if="item.children" class="menu-group">
            <view
              class="menu-item menu-item-parent"
              :class="{ expanded: expandedKey === item.key }"
              @click="toggleExpand(item.key)"
            >
              <view class="menu-icon">
                <AppIcon :name="item.icon" size="md" color="gray" />
              </view>
              <text class="menu-label">{{ t(item.labelKey) }}</text>
              <view class="menu-chevron">
                <AppIcon
                  :name="expandedKey === item.key ? 'chevronUp' : 'chevronDown'"
                  size="sm"
                  color="gray"
                />
              </view>
            </view>
            <view v-show="expandedKey === item.key" class="menu-children">
              <view
                v-for="child in item.children"
                :key="child.path"
                class="menu-item child"
                :class="{ active: isActive(child.path) }"
                @click="handleItemClick(child)"
              >
                <view class="menu-icon" />
                <text class="menu-label">{{ t(child.labelKey) }}</text>
              </view>
            </view>
          </view>
          <!-- 普通菜单项 -->
          <view
            v-else
            class="menu-item"
            :class="{ active: isActive(item.path), danger: item.danger }"
            @click="handleItemClick(item)"
          >
            <view class="menu-icon">
              <AppIcon
                :name="item.icon"
                size="md"
                :color="isActive(item.path) || item.danger ? 'white' : 'gray'"
              />
            </view>
            <text class="menu-label">{{ t(item.labelKey) }}</text>
          </view>
        </template>
      </view>

      <view class="drawer-footer" :style="{ paddingBottom: (bottomSafeArea + 24) + 'px' }">
        <image class="footer-logo-img" src="/static/logo_us.png" mode="aspectFit" />
      </view>
    </view>
  </view>
</template>

<script setup lang="ts">
import { computed, ref, watch, nextTick } from 'vue'
import { useI18n } from 'vue-i18n'
import AppIcon from './AppIcon.vue'
import { getStatusBarHeight, getBottomSafeArea } from '../utils/statusBar'

const props = defineProps<{
  modelValue: boolean
}>()

const emit = defineEmits<{
  (e: 'update:modelValue', value: boolean): void
}>()

const { t } = useI18n()
const statusBarHeight = getStatusBarHeight()
const bottomSafeArea = getBottomSafeArea()

const isShown = ref(false)
const animClass = ref('')

const userName = computed(() => {
  const stored = uni.getStorageSync('userName')
  return stored || 'John Smith'
})

const storeName = computed(() => {
  const stored = uni.getStorageSync('storeName')
  return stored || 'MedVantage'
})

const expandedKey = ref<string | null>(null)

const items = [
  { key: 'home', path: '/pages/index/index', icon: 'home', labelKey: 'Home' },
  { key: 'Labeling', path: '/pages/labels/labels', icon: 'tag', labelKey: 'Labeling' },
  {
    key: 'report',
    icon: 'fileText',
    labelKey: 'more.report',
    children: [
      { path: '/pages/more/print-log', labelKey: 'more.printLog' },
      { path: '/pages/more/label-report', labelKey: 'more.labelReport' },
    ],
  },
  { key: 'profile', path: '/pages/more/profile', icon: 'user', labelKey: 'more.profile' },
  { key: 'location', path: '/pages/more/location', icon: 'mapPin', labelKey: 'more.location' },
  { key: 'sync', path: '/pages/more/sync', icon: 'refresh', labelKey: 'more.sync' },
  { key: 'support', path: '/pages/more/support', icon: 'help', labelKey: 'more.support' },
  { key: 'logout', path: '__logout__', icon: 'logout', labelKey: 'more.logout', danger: true },
]

const toggleExpand = (key: string) => {
  expandedKey.value = expandedKey.value === key ? null : key
}

watch(
  () => props.modelValue,
  (val) => {
    if (val) {
      isShown.value = true
      if (currentPath.value.startsWith('/pages/more/print-log') || currentPath.value.startsWith('/pages/more/label-report')) {
        expandedKey.value = 'report'
      }
      nextTick(() => {
        animClass.value = 'opening'
      })
    } else if (isShown.value) {
      animClass.value = 'closing'
      setTimeout(() => {
        isShown.value = false
        animClass.value = ''
      }, 220)
    }
  },
  { immediate: true }
)

const close = () => {
  emit('update:modelValue', false)
}

const currentPath = computed(() => {
  const pages = getCurrentPages()
  const cur = pages[pages.length - 1] as any
  const route = (cur && cur.route) ? '/' + cur.route : ''
  return route
})

const isActive = (path: string) => !!path && path !== '__logout__' && currentPath.value === path

const handleLogout = () => {
  uni.removeStorageSync('isLoggedIn')
  uni.removeStorageSync('userName')
  uni.removeStorageSync('storeName')
  uni.removeStorageSync('storeId')
  uni.redirectTo({ url: '/pages/login/login' })
}

const handleItemClick = (item: any) => {
  if (item.isDivider) return
  emit('update:modelValue', false)
  if (item.path === '__logout__') {
    handleLogout()
    return
  }
  if (item.path && currentPath.value !== item.path) {
    uni.redirectTo({ url: item.path })
  }
}
</script>

<style scoped>
.drawer-mask {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  background: rgba(0, 0, 0, 0.4);
  z-index: 1000;
  display: flex;
  justify-content: flex-start;
  align-items: stretch;
}

.drawer-panel {
  width: 76%;
  max-width: 640rpx;
  background: #1F3A8A;
  padding: 16rpx 32rpx 0;
  box-shadow: 4rpx 0 32rpx rgba(0, 0, 0, 0.5);
  display: flex;
  flex-direction: column;
  height: 100%;
  box-sizing: border-box;
}

.drawer-panel.opening {
  animation: slide-in-left 220ms ease-out forwards;
}

.drawer-panel.closing {
  animation: slide-out-left 220ms ease-in forwards;
}

@keyframes slide-in-left {
  from {
    transform: translateX(-100%);
  }
  to {
    transform: translateX(0);
  }
}

@keyframes slide-out-left {
  from {
    transform: translateX(0);
  }
  to {
    transform: translateX(-100%);
  }
}

.drawer-top {
  padding-bottom: 32rpx;
  border-bottom: 1rpx solid rgba(148, 163, 184, 0.2);
}

.avatar-circle {
  width: 64rpx;
  height: 64rpx;
  border-radius: 50%;
  background: var(--theme-primary);
  display: flex;
  align-items: center;
  justify-content: center;
}

.user-row {
  display: flex;
  align-items: center;
  gap: 16rpx;
}

.user-texts {
  flex: 1;
  min-width: 0;
}

.user-name {
  font-size: 30rpx;
  font-weight: 600;
  color: #ffffff;
  display: block;
  margin-bottom: 4rpx;
}

.store-label {
  font-size: 24rpx;
  color: #9ca3af;
  display: block;
}

.drawer-menu {
  margin-top: 24rpx;
  flex: 1;
}

.menu-item {
  padding: 18rpx 12rpx;
  display: flex;
  align-items: center;
  gap: 20rpx;
  border-radius: 12rpx;
}

.menu-icon {
  width: 56rpx;
  display: flex;
  align-items: center;
  justify-content: center;
}

.menu-label {
  font-size: 30rpx;
  color: #e5e7eb;
}

.menu-item.active {
  background: #1447E6;
}

.menu-item.active .menu-label {
  color: #ffffff;
}

.menu-group {
  margin-bottom: 4rpx;
}

.menu-chevron {
  margin-left: auto;
}

.menu-children {
  padding-left: 76rpx;
  padding-bottom: 8rpx;
}

.menu-item.child {
  padding: 12rpx 12rpx;
}

.menu-item.child .menu-icon {
  width: 0;
  min-width: 0;
}

.drawer-footer {
  padding: 24rpx 0 48rpx;
  border-top: 1rpx solid rgba(148, 163, 184, 0.2);
  flex-shrink: 0;
}

.footer-logo-img {
  width: 260rpx;
  height: 72rpx;
  opacity: 0.85;
  background: rgba(255,255,255,0.92);
  border-radius: 10rpx;
  padding: 8rpx 12rpx;
}
</style>