辣评移动端界面全面优化(二十四)

 

移动端优化概述

随着移动设备使用率的不断提升,移动端体验成为用户体验的重要组成部分。本次优化全面改进了移动端界面,包括导航栏布局、菜单交互、页面显示等多个方面。

优化目标

  • 优化导航栏布局
  • 改进菜单交互体验
  • 提升页面显示效果
  • 增强响应式适配
  • 提升整体用户体验

导航栏布局优化

优化前的问题

<!-- 优化前:用户菜单在左侧,不符合移动端习惯 -->
<div class="mobile-navbar">
  <div class="user-menu">👤</div>
  <div class="logo">辣评</div>
  <div class="menu-toggle"></div>
</div>

问题:

  • 用户菜单在左侧,不符合移动端操作习惯
  • Logo 位置不够突出
  • 菜单按钮位置不合理

优化后的布局

<template>
  <div class="mobile-navbar">
    <!-- 左侧:菜单按钮 -->
    <div class="navbar-left">
      <el-button
        class="menu-toggle"
        circle
        @click="toggleMenu"
      >
        <el-icon><Menu /></el-icon>
      </el-button>
    </div>

    <!-- 中间:Logo 和系统名称 -->
    <div class="navbar-center">
      <img :src="logoPath" alt="Logo" class="logo" />
      <span class="system-name"></span>
    </div>

    <!-- 右侧:用户菜单 -->
    <div class="navbar-right">
      <el-dropdown @command="handleUserCommand">
        <el-button circle>
          <el-icon><User /></el-icon>
        </el-button>
        <template #dropdown>
          <el-dropdown-menu>
            <el-dropdown-item command="profile">
              <el-icon><User /></el-icon>
              个人中心
            </el-dropdown-item>
            <el-dropdown-item command="settings">
              <el-icon><Setting /></el-icon>
              设置
            </el-dropdown-item>
            <el-dropdown-item divided command="logout">
              <el-icon><SwitchButton /></el-icon>
              退出登录
            </el-dropdown-item>
          </el-dropdown-menu>
        </template>
      </el-dropdown>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { Menu, User, Setting, SwitchButton } from '@element-plus/icons-vue'
import { useAuthStore } from '@/stores/auth'

const router = useRouter()
const authStore = useAuthStore()

const logoPath = ref('/assets/logo.svg')
const systemName = ref('辣评')

const emit = defineEmits(['toggle-menu'])

const toggleMenu = () => {
  emit('toggle-menu')
}

const handleUserCommand = (command) => {
  switch (command) {
    case 'profile':
      router.push('/profile')
      break
    case 'settings':
      router.push('/settings')
      break
    case 'logout':
      authStore.logout()
      router.push('/login')
      break
  }
}
</script>

<style scoped>
.mobile-navbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 56px;
  padding: 0 12px;
  background-color: #fff;
  border-bottom: 1px solid #ebeef5;
  position: sticky;
  top: 0;
  z-index: 1000;
}

.navbar-left,
.navbar-right {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
}

.navbar-center {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
}

.logo {
  width: 32px;
  height: 32px;
}

.system-name {
  font-size: 18px;
  font-weight: bold;
  color: var(--color-primary);
}

.menu-toggle {
  width: 40px;
  height: 40px;
}

/* 移动端适配 */
@media (max-width: 480px) {
  .mobile-navbar {
    height: 50px;
    padding: 0 8px;
  }

  .logo {
    width: 28px;
    height: 28px;
  }

  .system-name {
    font-size: 16px;
  }
}
</style>

侧边菜单优化

抽屉式菜单

<template>
  <el-drawer
    v-model="visible"
    direction="ltr"
    :size="280"
    :show-close="false"
  >
    <template #header>
      <div class="drawer-header">
        <img :src="logoPath" alt="Logo" class="logo" />
        <span class="system-name"></span>
      </div>
    </template>

    <div class="drawer-content">
      <!-- 用户信息卡片 -->
      <div class="user-card" @click="goToProfile">
        <el-avatar :src="userAvatar" :size="60" />
        <div class="user-info">
          <div class="username"></div>
          <div class="user-role">
            <el-tag size="small" :type="roleType"></el-tag>
          </div>
        </div>
        <el-icon class="arrow-icon"><ArrowRight /></el-icon>
      </div>

      <!-- 菜单列表 -->
      <el-menu
        :default-active="activeMenu"
        class="drawer-menu"
        @select="handleMenuSelect"
      >
        <el-menu-item index="/home">
          <el-icon><House /></el-icon>
          <span>首页</span>
        </el-menu-item>

        <el-menu-item index="/submissions">
          <el-icon><Document /></el-icon>
          <span>我的投稿</span>
        </el-menu-item>

        <el-menu-item index="/comments">
          <el-icon><ChatDotRound /></el-icon>
          <span>我的评论</span>
        </el-menu-item>

        <el-menu-item index="/tasks">
          <el-icon><List /></el-icon>
          <span>任务追踪</span>
        </el-menu-item>

        <el-menu-item index="/ranking">
          <el-icon><TrendCharts /></el-icon>
          <span>排行榜</span>
        </el-menu-item>

        <el-menu-item index="/manual">
          <el-icon><Reading /></el-icon>
          <span>用户手册</span>
        </el-menu-item>

        <el-sub-menu v-if="isAdmin" index="admin">
          <template #title>
            <el-icon><Setting /></el-icon>
            <span>管理后台</span>
          </template>
          <el-menu-item index="/admin/dashboard">仪表盘</el-menu-item>
          <el-menu-item index="/admin/users">用户管理</el-menu-item>
          <el-menu-item index="/admin/submissions">投稿管理</el-menu-item>
          <el-menu-item index="/admin/comments">评论管理</el-menu-item>
        </el-sub-menu>
      </el-menu>
    </div>

    <template #footer>
      <div class="drawer-footer">
        <el-button type="danger" plain @click="handleLogout">
          <el-icon><SwitchButton /></el-icon>
          退出登录
        </el-button>
      </div>
    </template>
  </el-drawer>
</template>

<script setup>
import { ref, computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import {
  House,
  Document,
  ChatDotRound,
  List,
  TrendCharts,
  Reading,
  Setting,
  SwitchButton,
  ArrowRight
} from '@element-plus/icons-vue'

const props = defineProps({
  modelValue: Boolean
})

const emit = defineEmits(['update:modelValue'])

const router = useRouter()
const route = useRoute()
const authStore = useAuthStore()

const visible = computed({
  get: () => props.modelValue,
  set: (val) => emit('update:modelValue', val)
})

const logoPath = ref('/assets/logo.svg')
const systemName = ref('辣评')

const username = computed(() => authStore.user?.username || '用户')
const userAvatar = computed(() => authStore.user?.avatar || '')
const isAdmin = computed(() => authStore.hasRole('admin'))

const roleName = computed(() => {
  if (authStore.hasRole('admin')) return '管理员'
  if (authStore.hasRole('author')) return '作者'
  if (authStore.hasRole('reader')) return '读者'
  return '用户'
})

const roleType = computed(() => {
  if (authStore.hasRole('admin')) return 'danger'
  if (authStore.hasRole('author')) return 'primary'
  if (authStore.hasRole('reader')) return 'success'
  return 'info'
})

const activeMenu = computed(() => route.path)

const handleMenuSelect = (index) => {
  router.push(index)
  visible.value = false
}

const goToProfile = () => {
  router.push('/profile')
  visible.value = false
}

const handleLogout = () => {
  authStore.logout()
  router.push('/login')
  visible.value = false
}
</script>

<style scoped>
.drawer-header {
  display: flex;
  align-items: center;
  gap: 12px;
}

.drawer-header .logo {
  width: 36px;
  height: 36px;
}

.drawer-header .system-name {
  font-size: 18px;
  font-weight: bold;
  color: var(--color-primary);
}

.drawer-content {
  height: 100%;
  display: flex;
  flex-direction: column;
}

.user-card {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 16px;
  margin-bottom: 16px;
  background-color: #f5f7fa;
  border-radius: 8px;
  cursor: pointer;
  transition: all 0.3s;
}

.user-card:hover {
  background-color: #ecf5ff;
}

.user-info {
  flex: 1;
}

.username {
  font-size: 16px;
  font-weight: bold;
  color: #303133;
  margin-bottom: 4px;
}

.user-role {
  font-size: 12px;
}

.arrow-icon {
  color: #909399;
}

.drawer-menu {
  flex: 1;
  border: none;
}

.drawer-footer {
  padding: 16px;
  border-top: 1px solid #ebeef5;
}

.drawer-footer .el-button {
  width: 100%;
}
</style>

底部标签栏

移动端导航标签栏

<template>
  <div class="mobile-tabbar">
    <div
      v-for="item in tabs"
      :key="item.path"
      class="tabbar-item"
      :class="{ active: isActive(item.path) }"
      @click="handleTabClick(item.path)"
    >
      <el-icon :size="24">
        <component :is="item.icon" />
      </el-icon>
      <span class="tabbar-label"></span>
      <el-badge
        v-if="item.badge"
        :value="item.badge"
        :max="99"
        class="tabbar-badge"
      />
    </div>
  </div>
</template>

<script setup>
import { computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import {
  House,
  Document,
  ChatDotRound,
  User
} from '@element-plus/icons-vue'

const router = useRouter()
const route = useRoute()

const tabs = [
  {
    path: '/home',
    label: '首页',
    icon: House
  },
  {
    path: '/submissions',
    label: '投稿',
    icon: Document
  },
  {
    path: '/comments',
    label: '评论',
    icon: ChatDotRound
  },
  {
    path: '/profile',
    label: '我的',
    icon: User
  }
]

const isActive = (path) => {
  return route.path.startsWith(path)
}

const handleTabClick = (path) => {
  if (route.path !== path) {
    router.push(path)
  }
}
</script>

<style scoped>
.mobile-tabbar {
  display: flex;
  height: 56px;
  background-color: #fff;
  border-top: 1px solid #ebeef5;
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 1000;
  padding-bottom: env(safe-area-inset-bottom);
}

.tabbar-item {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  cursor: pointer;
  transition: all 0.3s;
  position: relative;
}

.tabbar-item:active {
  background-color: #f5f7fa;
}

.tabbar-item .el-icon {
  color: #909399;
  transition: color 0.3s;
}

.tabbar-label {
  font-size: 12px;
  color: #909399;
  transition: color 0.3s;
}

.tabbar-item.active .el-icon,
.tabbar-item.active .tabbar-label {
  color: var(--color-primary);
}

.tabbar-badge {
  position: absolute;
  top: 8px;
  right: 50%;
  transform: translateX(12px);
}

/* iPhone X 及以上机型适配 */
@supports (padding-bottom: env(safe-area-inset-bottom)) {
  .mobile-tabbar {
    padding-bottom: calc(env(safe-area-inset-bottom) + 56px);
  }
}
</style>

页面显示优化

卡片式布局

<template>
  <div class="mobile-page">
    <!-- 页面头部 -->
    <div class="page-header">
      <h2></h2>
      <div class="header-actions">
        <slot name="header-actions"></slot>
      </div>
    </div>

    <!-- 页面内容 -->
    <div class="page-content">
      <slot></slot>
    </div>
  </div>
</template>

<script setup>
defineProps({
  pageTitle: String
})
</script>

<style scoped>
.mobile-page {
  min-height: 100vh;
  background-color: #f5f7fa;
  padding-bottom: 72px; /* 底部标签栏高度 + 间距 */
}

.page-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 16px;
  background-color: #fff;
  border-bottom: 1px solid #ebeef5;
}

.page-header h2 {
  margin: 0;
  font-size: 18px;
  font-weight: bold;
  color: #303133;
}

.page-content {
  padding: 12px;
}

/* 卡片样式 */
.mobile-page :deep(.content-card) {
  background-color: #fff;
  border-radius: 8px;
  padding: 16px;
  margin-bottom: 12px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}

.mobile-page :deep(.content-card:last-child) {
  margin-bottom: 0;
}
</style>

列表项优化

<template>
  <div class="mobile-list-item" @click="handleClick">
    <div class="item-icon" v-if="icon">
      <el-icon :size="20">
        <component :is="icon" />
      </el-icon>
    </div>

    <div class="item-content">
      <div class="item-title"></div>
      <div class="item-description" v-if="description">
        
      </div>
      <div class="item-meta" v-if="meta">
        <span v-for="(item, index) in meta" :key="index" class="meta-item">
          
        </span>
      </div>
    </div>

    <div class="item-extra" v-if="$slots.extra">
      <slot name="extra"></slot>
    </div>

    <div class="item-arrow" v-if="showArrow">
      <el-icon><ArrowRight /></el-icon>
    </div>
  </div>
</template>

<script setup>
import { ArrowRight } from '@element-plus/icons-vue'

defineProps({
  icon: Object,
  title: String,
  description: String,
  meta: Array,
  showArrow: {
    type: Boolean,
    default: true
  }
})

const emit = defineEmits(['click'])

const handleClick = () => {
  emit('click')
}
</script>

<style scoped>
.mobile-list-item {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 16px;
  background-color: #fff;
  border-bottom: 1px solid #f5f7fa;
  cursor: pointer;
  transition: background-color 0.3s;
}

.mobile-list-item:active {
  background-color: #f5f7fa;
}

.mobile-list-item:last-child {
  border-bottom: none;
}

.item-icon {
  flex: 0 0 auto;
  color: var(--color-primary);
}

.item-content {
  flex: 1;
  min-width: 0;
}

.item-title {
  font-size: 16px;
  font-weight: 500;
  color: #303133;
  margin-bottom: 4px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.item-description {
  font-size: 14px;
  color: #909399;
  margin-bottom: 4px;
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}

.item-meta {
  display: flex;
  gap: 12px;
  font-size: 12px;
  color: #c0c4cc;
}

.meta-item {
  display: flex;
  align-items: center;
  gap: 4px;
}

.item-extra {
  flex: 0 0 auto;
}

.item-arrow {
  flex: 0 0 auto;
  color: #c0c4cc;
}
</style>

响应式调整

断点定义

/* admin-vue/src/styles/responsive.css */

/* 超小屏幕(手机竖屏) */
@media (max-width: 480px) {
  .container {
    padding: 12px;
  }

  .page-title {
    font-size: 18px;
  }

  .el-button {
    padding: 8px 12px;
    font-size: 14px;
  }
}

/* 小屏幕(手机横屏、小平板) */
@media (min-width: 481px) and (max-width: 768px) {
  .container {
    padding: 16px;
  }

  .page-title {
    font-size: 20px;
  }
}

/* 中等屏幕(平板) */
@media (min-width: 769px) and (max-width: 1024px) {
  .container {
    padding: 20px;
  }

  /* 隐藏移动端专用元素 */
  .mobile-only {
    display: none !important;
  }
}

/* 大屏幕(桌面) */
@media (min-width: 1025px) {
  /* 隐藏移动端专用元素 */
  .mobile-only {
    display: none !important;
  }

  /* 隐藏底部标签栏 */
  .mobile-tabbar {
    display: none !important;
  }
}

触摸优化

触摸反馈

/* 触摸反馈效果 */
.touchable {
  cursor: pointer;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
  transition: all 0.2s;
}

.touchable:active {
  transform: scale(0.98);
  opacity: 0.8;
}

/* 按钮触摸优化 */
.el-button {
  -webkit-tap-highlight-color: transparent;
}

/* 链接触摸优化 */
a {
  -webkit-tap-highlight-color: transparent;
}

滑动优化

/* 滚动优化 */
.scrollable {
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}

/* 隐藏滚动条 */
.scrollable::-webkit-scrollbar {
  display: none;
}

.scrollable {
  -ms-overflow-style: none;
  scrollbar-width: none;
}

总结

移动端界面全面优化显著提升了移动设备上的用户体验。通过优化导航栏布局、改进菜单交互、完善页面显示、增强响应式适配,我们打造了一个友好、流畅的移动端界面。

关键改进

  • 导航栏布局更符合移动端习惯
  • 侧边菜单交互更加流畅
  • 底部标签栏便于快速导航
  • 卡片式布局提升视觉效果
  • 响应式适配更加完善

技术亮点

  • 抽屉式菜单
  • 底部标签栏导航
  • 触摸反馈优化
  • 安全区域适配
  • 响应式断点

用户反馈

  • 移动端操作更加便捷
  • 界面更加美观
  • 导航更加清晰
  • 整体体验显著提升

这次移动端优化为用户提供了更好的移动设备使用体验,也为平台的移动端发展奠定了基础。

本文遵守 Attribution-NonCommercial 4.0 International 许可协议。 Attribution-NonCommercial 4.0 International