小白也会使用vitepress-theme-curve主题自定义页面


登录页面

效果:

image.png

增加登录页面:

image.png

.vitepress\theme\views\admin\Login.vue

<template>
    <div class="login-container">
      <div class="login-box">
        <div class="login-header">
          <h2>管理员登录</h2>
        </div>
        <div class="login-form">
          <div class="form-item">
            <span class="iconfont icon-user"></span>
            <input 
              type="text" 
              v-model="username" 
              placeholder="用户名"
              @keyup.enter="handleLogin"
            >
          </div>
          <div class="form-item">
            <span class="iconfont icon-password"></span>
            <input 
              :type="showPassword ? 'text' : 'password'" 
              v-model="password" 
              placeholder="密码"
              @keyup.enter="handleLogin"
            >
            <span 
              class="iconfont" 
              :class="showPassword ? 'icon-eye-open' : 'icon-eye-close'"
              @click="showPassword = !showPassword"
            ></span>
          </div>
          <button class="login-btn" @click="handleLogin">登录</button>
        </div>
      </div>
    </div>
  </template>
  
  <script setup>
  import { ref, onMounted } from 'vue'
  import { useRouter } from 'vitepress'
  
  const router = useRouter()
  const username = ref('')
  const password = ref('')
  const showPassword = ref(false)
  
  const handleLogin = () => {
    if (!username.value || !password.value) {
      window.$message.warning('请输入用户名和密码')
      return
    }
    
    
    
    window.$message.success('登录成功')
    // router.go('/admin/dashboard')
  }
  </script>
  
  <style lang="scss" scoped>
  .login-container {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: calc(100vh - var(--header-height));
    padding: 20px;
    
    .login-box {
      width: 100%;
      max-width: 400px;
      padding: 30px;
      border-radius: 16px;
      background-color: var(--main-card-background);
      border: 1px solid var(--main-card-border);
      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
      
      .login-header {
        text-align: center;
        margin-bottom: 30px;
        
        h2 {
          font-size: 24px;
          color: var(--main-font-color);
        }
      }
      
      .login-form {
        .form-item {
          position: relative;
          margin-bottom: 20px;
          
          .iconfont {
            position: absolute;
            left: 15px;
            top: 50%;
            transform: translateY(-50%);
            font-size: 18px;
            color: var(--main-font-second-color);
          }
          
          input {
            width: 100%;
            height: 45px;
            padding: 0 45px;
            border: 1px solid var(--main-card-border);
            border-radius: 8px;
            background-color: var(--main-card-second-background);
            color: var(--main-font-color);
            transition: all 0.3s;
            
            &:focus {
              border-color: var(--main-color);
              box-shadow: 0 0 0 2px var(--main-color-bg);
            }
          }
          
          .icon-eye-open,
          .icon-eye-close {
            left: auto;
            right: 15px;
            cursor: pointer;
            
            &:hover {
              color: var(--main-color);
            }
          }
        }
        
        .login-btn {
          width: 100%;
          height: 45px;
          border: none;
          border-radius: 8px;
          background-color: var(--main-color);
          color: #fff;
          font-size: 16px;
          font-weight: bold;
          cursor: pointer;
          transition: all 0.3s;
          
          &:hover {
            opacity: 0.9;
            transform: translateY(-1px);
          }
          
          &:active {
            transform: translateY(0);
          }
        }
      }
    }
  }
  </style> 

image.png

pages\admin[login.md](http://login.md/)

---
title: 管理员登录
aside: false
---

<script setup>
import Login from "@/views/admin/Login.vue"
</script>

<Login />

.vitepress\components.d.ts 这个配置在调试的时候会自动引入相应的组件

image.png

配置菜单导航栏

image.png

 // 导航栏菜单

.vitepress\theme\assets\themeConfig.mjs

{
      text: "管理员",
      items: [
        { text: "管理员登录", link: "/pages/admin/login", icon: "admin" },
      ],
    },

后台首页

效果图:

image.png

变动文件:

image.png

新增Dashboard.vue页面:

image.png

.vitepress\theme\views\admin\Dashboard.vue

<template>
    <AdminLayout>
      <div class="dashboard-container">
        <div class="dashboard-header">
          <h2>仪表盘</h2>
          <div class="header-right">
            <span class="welcome">欢迎回来,{{ username }}</span>
            <button class="logout-btn" @click="handleLogout">
              <span class="iconfont icon-logout"></span>
              退出登录
            </button>
          </div>
        </div>
        
        <div class="dashboard-content">
          <!-- 数据概览卡片 -->
          <div class="stats-cards">
            <div class="stat-card" v-for="stat in statistics" :key="stat.title">
              <div class="stat-icon">
                <span class="iconfont" :class="stat.icon"></span>
              </div>
              <div class="stat-info">
                <div class="stat-title">{{ stat.title }}</div>
                <div class="stat-value">{{ stat.value }}</div>
              </div>
            </div>
          </div>
          
          <!-- 最近活动 -->
          <div class="recent-activities">
            <h3>最近活动</h3>
            <div class="activity-list">
              <div class="activity-item" v-for="activity in activities" :key="activity.id">
                <span class="activity-time">{{ activity.time }}</span>
                <span class="activity-content">{{ activity.content }}</span>
              </div>
            </div>
          </div>
        </div>
      </div>
    </AdminLayout>
  </template>
  
  <script setup>
  import { ref, computed } from 'vue'
  import { useRouter } from 'vitepress'
  import AdminLayout from '@/components/admin/Layout.vue'
  
  const router = useRouter()
  
  // 统计数据
  const statistics = ref([
    { title: '文章总数', value: '125', icon: 'icon-article' },
    { title: '用户数量', value: '1,234', icon: 'icon-user' },
    { title: '评论数量', value: '3,456', icon: 'icon-comment' },
    { title: '访问量', value: '45,678', icon: 'icon-view' }
  ])
  
  // 最近活动
  const activities = ref([
    { id: 1, time: '2024-01-20 12:30', content: '新用户注册: user123' },
    { id: 2, time: '2024-01-20 11:45', content: '发布新文章: VitePress主题开发指南' },
    { id: 3, time: '2024-01-20 10:15', content: '更新了网站配置' },
    { id: 4, time: '2024-01-20 09:30', content: '回复了用户评论' }
  ])
  
  // 退出登录
  const handleLogout = () => {
    adminStore.logout()
    router.go('/pages/admin/login')
  }
  </script>
  
  <style lang="scss" scoped>
  .dashboard-container {
    padding: 20px;
    
    .dashboard-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 30px;
      padding: 20px;
      background-color: var(--main-card-background);
      border: 1px solid var(--main-card-border);
      border-radius: 12px;
      
      h2 {
        font-size: 24px;
        color: var(--main-font-color);
      }
      
      .header-right {
        display: flex;
        align-items: center;
        gap: 20px;
        
        .welcome {
          color: var(--main-font-second-color);
        }
        
        .logout-btn {
          display: flex;
          align-items: center;
          gap: 6px;
          padding: 8px 16px;
          border: none;
          border-radius: 8px;
          background-color: var(--main-error-color);
          color: #fff;
          cursor: pointer;
          transition: opacity 0.3s;
          
          &:hover {
            opacity: 0.9;
          }
        }
      }
    }
    
    .dashboard-content {
      .stats-cards {
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
        gap: 20px;
        margin-bottom: 30px;
        
        .stat-card {
          display: flex;
          align-items: center;
          padding: 20px;
          background-color: var(--main-card-background);
          border: 1px solid var(--main-card-border);
          border-radius: 12px;
          
          .stat-icon {
            display: flex;
            align-items: center;
            justify-content: center;
            width: 50px;
            height: 50px;
            margin-right: 15px;
            border-radius: 10px;
            background-color: var(--main-color-bg);
            
            .iconfont {
              font-size: 24px;
              color: var(--main-color);
            }
          }
          
          .stat-info {
            .stat-title {
              font-size: 14px;
              color: var(--main-font-second-color);
              margin-bottom: 5px;
            }
            
            .stat-value {
              font-size: 24px;
              font-weight: bold;
              color: var(--main-font-color);
            }
          }
        }
      }
      
      .recent-activities {
        padding: 20px;
        background-color: var(--main-card-background);
        border: 1px solid var(--main-card-border);
        border-radius: 12px;
        
        h3 {
          margin-bottom: 20px;
          font-size: 18px;
          color: var(--main-font-color);
        }
        
        .activity-list {
          .activity-item {
            display: flex;
            align-items: center;
            padding: 12px 0;
            border-bottom: 1px solid var(--main-card-border);
            
            &:last-child {
              border-bottom: none;
            }
            
            .activity-time {
              min-width: 150px;
              color: var(--main-font-second-color);
              font-size: 14px;
            }
            
            .activity-content {
              color: var(--main-font-color);
            }
          }
        }
      }
    }
  }
  
  @media (max-width: 768px) {
    .dashboard-container {
      .dashboard-header {
        flex-direction: column;
        gap: 15px;
        text-align: center;
        
        .header-right {
          flex-direction: column;
        }
      }
    }
  }
  </style>
  • 增加组件:

    image.png

    .vitepress\theme\components\admin\Layout.vue

    <template>
      <div class="admin-layout">
        <AdminSidebar />
        <div class="admin-main" :class="{ 'menu-collapsed': collapsed }">
          <AdminHeader />
          <div class="admin-content">
            <slot></slot>
          </div>
        </div>
      </div>
    </template>
    
    <script setup>
    import { computed } from 'vue'
    import AdminHeader from './Header.vue'
    import AdminSidebar from './Sidebar.vue'
    import { useSidebarCollapse } from '@/composables/useSidebarCollapse'
    
    const { collapsed } = useSidebarCollapse()
    
    </script>
    
    <style lang="scss" scoped>
    .admin-layout {
      position: fixed;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      display: flex;
      background-color: var(--main-background);
      z-index: 999;
      
      .admin-main {
        flex: 1;
        display: flex;
        flex-direction: column;
        width: calc(100% - 200px);
        transition: width 0.3s;
        
        &.menu-collapsed {
          width: calc(100% - 64px);
        }
        
        .admin-content {
          flex: 1;
          padding: 20px;
          overflow-y: auto;
    
          :deep(.VPDoc) {
            padding: 0 !important;
            
            .container {
              max-width: none !important;
              margin: 0 !important;
              padding: 0 !important;
            }
          }
        }
      }
    
      // 响应式布局
      @media (max-width: 768px) {
        .admin-main {
          width: 100% !important;
        }
      }
    }
    </style> 

    .vitepress\theme\components\admin\Header.vue

    <template>
      <div class="admin-header">
        <div class="header-left">
          <button class="collapse-btn" @click="toggleSidebar">
            <span class="iconfont" :class="isMobile ? 
              (collapsed ? 'icon-menu-unfold' : 'icon-menu-fold') : 
              (collapsed ? 'icon-menu-fold' : 'icon-menu-unfold')"
            ></span>
          </button>
          <div class="breadcrumb">
            <span v-for="(item, index) in breadcrumbs" 
              :key="index"
              class="breadcrumb-item"
            >
              {{ item }}
              <span v-if="index < breadcrumbs.length - 1" class="separator">/</span>
            </span>
          </div>
        </div>
        <div class="header-right">
          <div class="user-info">
            <span class="username hide-on-mobile">{{ username }}</span>
            <div class="avatar">
              <img :src="avatar" alt="avatar">
            </div>
          </div>
        </div>
      </div>
    </template>
    
    <script setup>
    import { ref, computed, onMounted, onUnmounted } from 'vue'
    import { useRoute } from 'vitepress'
    import { useSidebarCollapse } from '@/composables/useSidebarCollapse'
    
    const route = useRoute()
    const username = ref('Admin')
    const avatar = ref('/avatar.jpg')
    const { collapsed, toggleCollapsed } = useSidebarCollapse()
    const isMobile = ref(window.innerWidth <= 768)
    
    // 监听窗口大小变化
    onMounted(() => {
      const handleResize = () => {
        isMobile.value = window.innerWidth <= 768
      }
      window.addEventListener('resize', handleResize)
      onUnmounted(() => {
        window.removeEventListener('resize', handleResize)
      })
    })
    
    // 面包屑导航映射
    const breadcrumbMap = {
      dashboard: '仪表盘',
      users: '用户管理',
      posts: '文章管理',
      comments: '评论管理',
      settings: '系统设置'
    }
    
    // 面包屑导航
    const breadcrumbs = computed(() => {
      const path = route.path
      const parts = path.split('/').filter(Boolean)
      const lastPart = parts[parts.length - 1]
      return ['管理后台', breadcrumbMap[lastPart] || lastPart]
    })
    
    // 切换侧边栏
    const toggleSidebar = () => {
      toggleCollapsed()
    }
    </script>
    
    <style lang="scss" scoped>
    .admin-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      height: 60px;
      padding: 0 20px;
      background-color: var(--main-card-background);
      border-bottom: 1px solid var(--main-card-border);
      
      .header-left {
        display: flex;
        align-items: center;
        gap: 20px;
        
        .collapse-btn {
          display: flex;
          align-items: center;
          justify-content: center;
          width: 40px;
          height: 40px;
          border: none;
          border-radius: 8px;
          background-color: transparent;
          cursor: pointer;
          transition: background-color 0.3s;
          
          &:hover {
            background-color: var(--main-card-second-background);
          }
          
          .iconfont {
            font-size: 20px;
            color: var(--main-font-color);
          }
        }
        
        .breadcrumb {
          display: flex;
          align-items: center;
          
          .breadcrumb-item {
            color: var(--main-font-color);
            
            .separator {
              margin: 0 8px;
              color: var(--main-font-second-color);
            }
            
            &:last-child {
              color: var(--main-color);
            }
          }
        }
      }
      
      .header-right {
        .user-info {
          display: flex;
          align-items: center;
          gap: 10px;
          cursor: pointer;
          
          .username {
            color: var(--main-font-color);
          }
          
          .avatar {
            width: 40px;
            height: 40px;
            border-radius: 50%;
            overflow: hidden;
            
            img {
              width: 100%;
              height: 100%;
              object-fit: cover;
            }
          }
        }
      }
    
      @media (max-width: 768px) {
        padding: 0 15px;
        
        .header-left {
          gap: 10px;
          
          .breadcrumb {
            font-size: 14px;
          }
        }
        
        .header-right {
          .hide-on-mobile {
            display: none;
          }
        }
      }
    }
    </style> 

    .vitepress\theme\components\admin\Sidebar.vue

    <template>
      <div class="sidebar-wrapper">
        <!-- 遮罩层 -->
        <div 
          v-show="!collapsed && isMobile" 
          class="sidebar-mask"
          @click="toggleCollapsed"
        ></div>
        
        <div class="admin-sidebar" :class="{ collapsed, mobile: isMobile }">
          <div class="sidebar-header">
            <span class="iconfont icon-dashboard logo"></span>
            <h1 class="title" v-if="!collapsed">管理后台</h1>
          </div>
          
          <div class="sidebar-menu">
            <div 
              v-for="menu in menus" 
              :key="menu.path"
              class="menu-item"
              :class="{ active: isActive(menu.path) }"
              @click="handleMenuClick(menu.path)"
            >
              <span class="iconfont" :class="menu.icon"></span>
              <span class="label" v-if="!collapsed">{{ menu.label }}</span>
            </div>
          </div>
    
          <!-- 收缩/展开按钮 -->
          <button 
            class="collapse-trigger"
            @click="toggleCollapsed"
            :title="collapsed ? '展开菜单' : '收起菜单'"
          >
            <span class="iconfont" :class="collapsed ? 'icon-menu-unfold' : 'icon-menu-fold'"></span>
          </button>
        </div>
      </div>
    </template>
    
    <script setup>
    import { ref, onMounted, onUnmounted } from 'vue'
    import { useRoute, useRouter } from 'vitepress'
    import { useSidebarCollapse } from '@/composables/useSidebarCollapse'
    
    const route = useRoute()
    const router = useRouter()
    const { collapsed, toggleCollapsed } = useSidebarCollapse()
    
    // 是否为移动端
    const isMobile = ref(false)
    
    // 检查是否为移动端
    const checkMobile = () => {
      isMobile.value = window.innerWidth <= 768
    }
    
    // 监听窗口大小变化
    onMounted(() => {
      checkMobile()
      window.addEventListener('resize', checkMobile)
    })
    
    onUnmounted(() => {
      window.removeEventListener('resize', checkMobile)
    })
    
    // 处理菜单点击
    const handleMenuClick = (path) => {
      router.go(path)
    }
    
    // 菜单配置
    const menus = [
      { 
        label: '仪表盘',
        path: '/pages/admin/dashboard',
        icon: 'icon-dashboard'
      },
      {
        label: '用户管理',
        path: '/pages/admin/users',
        icon: 'icon-users'
      },
      {
        label: '文章管理',
        path: '/pages/admin/posts',
        icon: 'icon-article'
      },
      {
        label: '评论管理',
        path: '/pages/admin/comments',
        icon: 'icon-comment'
      },
      {
        label: '系统设置',
        path: '/pages/admin/settings',
        icon: 'icon-setting'
      }
    ]
    
    // 判断当前路由是否激活
    const isActive = (path) => {
      return route.path === path || route.path === path.replace('/pages', '')
    }
    </script>
    
    <style lang="scss" scoped>
    .sidebar-wrapper {
      position: relative;
      z-index: 1000;
    
      .sidebar-mask {
        position: fixed;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        background-color: rgba(0, 0, 0, 0.5);
        z-index: 999;
      }
    
      .admin-sidebar {
        position: relative;
        width: 200px;
        height: 100vh;
        display: flex;
        flex-direction: column;
        background-color: var(--main-card-background);
        border-right: 1px solid var(--main-card-border);
        transition: all 0.3s;
        z-index: 1000;
        
        // 收缩/展开触发器
        .collapse-trigger {
          position: absolute;
          right: -16px;
          top: 50%;
          transform: translateY(-50%);
          width: 32px;
          height: 32px;
          border-radius: 50%;
          background-color: var(--main-card-background);
          border: 1px solid var(--main-card-border);
          color: var(--main-font-color);
          display: flex;
          align-items: center;
          justify-content: center;
          cursor: pointer;
          transition: all 0.3s;
          z-index: 1001;
          
          &:hover {
            color: var(--main-color);
            background-color: var(--main-color-bg);
            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
          }
          
          .iconfont {
            font-size: 14px;
          }
        }
        
        &.collapsed {
          width: 64px;
          
          .sidebar-header {
            padding: 16px 0;
            justify-content: center;
            
            .logo {
              margin: 0;
            }
          }
          
          .menu-item {
            padding: 10px 0;
            justify-content: center;
            
            .iconfont {
              margin: 0;
              font-size: 18px;
            }
          }
          
          .sidebar-footer {
            padding: 12px 0;
            
            .collapse-btn {
              justify-content: center;
              padding: 8px 0;
              
              .iconfont {
                margin: 0;
              }
            }
          }
        }
        
        &.mobile {
          position: fixed;
          transform: translateX(0);
          
          &.collapsed {
            transform: translateX(-100%);
          }
    
          // 移动端按钮样式
          .collapse-trigger {
            right: -40px;
            width: 40px;
            height: 40px;
            border-radius: 0 4px 4px 0;
            border-left: none;
            
            &:hover {
              box-shadow: none;
            }
          }
        }
        
        .sidebar-header {
          display: flex;
          align-items: center;
          padding: 16px;
          border-bottom: 1px solid var(--main-card-border);
          
          .logo {
            font-size: 20px;
            margin-right: 8px;
            color: var(--main-color);
          }
          
          .title {
            font-size: 16px;
            color: var(--main-font-color);
            white-space: nowrap;
          }
        }
        
        .sidebar-menu {
          flex: 1;
          padding: 12px 0;
          overflow-y: auto;
          
          .menu-item {
            cursor: pointer;
            display: flex;
            align-items: center;
            padding: 10px 16px;
            color: var(--main-font-color);
            text-decoration: none;
            transition: all 0.3s;
            
            .iconfont {
              font-size: 16px;
              margin-right: 8px;
            }
            
            .label {
              font-size: 14px;
              white-space: nowrap;
            }
            
            &:hover {
              color: var(--main-color);
              background-color: var(--main-color-bg);
            }
            
            &.active {
              color: var(--main-color);
              background-color: var(--main-color-bg);
            }
          }
        }
        
        .sidebar-footer {
          padding: 12px 16px;
          border-top: 1px solid var(--main-card-border);
          
          .collapse-btn {
            width: 100%;
            display: flex;
            align-items: center;
            padding: 8px 12px;
            border: none;
            border-radius: 4px;
            background-color: transparent;
            color: var(--main-font-color);
            cursor: pointer;
            transition: all 0.3s;
            
            &:hover {
              background-color: var(--main-color-bg);
              color: var(--main-color);
            }
            
            .iconfont {
              font-size: 16px;
              margin-right: 8px;
            }
            
            .label {
              font-size: 14px;
              white-space: nowrap;
            }
          }
        }
      }
    
      // 移动端样式调整
      @media (max-width: 768px) {
        .admin-sidebar {
          &.mobile {
            width: 200px !important;
            
            &.collapsed {
              width: 200px !important;
            }
          }
        }
      }
    }
    </style> 
  • 增加收缩状态

    image.png

    .vitepress\theme\composables\useSidebarCollapse.ts

    import { ref } from 'vue'
    
    const collapsed = ref(false)
    
    export function useSidebarCollapse() {
      const toggleCollapsed = () => {
        collapsed.value = !collapsed.value
      }
    
      return {
        collapsed,
        toggleCollapsed
      }
    } 

增加

image.png

pages\admin[dashboard.md](http://dashboard.md/)渲染

---
title: 仪表盘
layout: admin
aside: false
---

<script setup>
import Dashboard from "@/views/admin/Dashboard.vue"
</script>

<Dashboard />

首页改进

效果:

image.png

改动代码:

image.png

新增后台布局,与首页区分开

image.png

.vitepress\theme\layouts\AdminLayout.vue

<template>
    <AdminLayout>
      <Content />
    </AdminLayout>
  </template>
  
  <script setup>
  import AdminLayout from '@/components/admin/Layout.vue'
  </script>
  
  <style lang="scss" scoped>
  :deep(.VPDoc) {
    padding: 0 !important;
    
    .container {
      max-width: none !important;
      margin: 0 !important;
      padding: 0 !important;
    }
  }
  </style> 

在App.vue中修改布局方式:

image.png

.vitepress\theme\App.vue

在原来导航布局上套一层


文章作者: keney
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 keney !
评论