SideMenu.vue 6.11 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">
        <view
          v-for="item in items"
          :key="item.key"
          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>
      </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 items = [
  { key: 'home', path: '/pages/index/index', icon: 'home', labelKey: 'home' },
  { key: 'labels', path: '/pages/labels/labels', icon: 'tag', labelKey: 'nav.labels' },
  { 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: 'language', path: '/pages/more/language', icon: 'globe', labelKey: 'more.language' },
  { key: 'support', path: '/pages/more/support', icon: 'help', labelKey: 'more.support' },
  { key: 'logout', path: '__logout__', icon: 'logout', labelKey: 'more.logout', danger: true },
]

watch(
  () => props.modelValue,
  (val) => {
    if (val) {
      isShown.value = true
      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 && 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: #111827;
  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: var(--theme-primary);
}

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

.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>