vite+vue3+ts开发后台管理系统

655 阅读7分钟

开发前准备

可以跟着我的文章vite+vue3+ts后台项目搭建全过程,做好开发前准备。觉得有用的话,给个小赞吧~

正式开发

做好开发准备后,我们就可以正式开始开发了,使用vue3+typescript进行开发还是第一次,着实有点无从下手,这里假设大家已经去vue3官方文档看过组合式API怎么用了,因为官方文档写的很详细,我就不搬运了。

那我们就先大致了解下typescript怎么定义变量吧,下面我列了一些常用的,还有更多类型大家可以自己去Typescript官方文档查看。

// 布尔值
let isShow = false; // 用js
let isShow: boolean = false; // 用ts
// 数字
let num = 10; // 用js
let num: number = 10; // 用ts
// 字符串
let str = 'hello'; // 用js
let str: string = 'hello'; // 用ts
// 数组
let arr = [1, 2, 3]; // 用js
let arr: number[] = [1, 2, 3]; // 用ts第一种
let arr: Array<number> = [1, 2, 3]; // 用ts第二种
let arr2 = ['hello', 'world', 'hi']; // 用js
let arr2: string[] = ['hello', 'world', 'hi']; // 用ts第一种
let arr2: Array<string> = ['hello', 'world', 'hi']; // 用ts第二种
// any 可以是任何类型
let val: any = 10;

登录页面开发

好了,对typescript的使用心里有底后,我们就可以正式开始开发了。后台管理系统必不可少的登录页面,先不管登录逻辑(按照正常逻辑是先到首页,首页判断登录状态,过期的话跳转到登录页),我们先把登录页面开发出来,vue3+typescript的第一次应用嘛,先在src\router\index.ts文件下增加登录路由,代码如下:

import { createRouter, createWebHashHistory } from 'vue-router'

const router = createRouter({
  history: createWebHashHistory(),
  routes: [ // 增加的路由
    {
      path: "/",
      component: () => import("../views/Login.vue"),
    }
  ]
})

export default router

然后我们在src目录下新建一个views文件夹,在该文件夹下新增Login.vue文件,代码如下:

<template>
  <div class="login_wrap">
    <div class="login_box">
      <div class="ms_login">
        <div class="ms_title">后台管理系统</div>
        <el-form ref="ruleFormRef" :model="ruleForm" :rules="rules" label-width="0px" status-icon class="form_box">
          <el-form-item prop="username">
            <el-input v-model="ruleForm.username" placeholder="请输入用户名" :prefix-icon="User" />
          </el-form-item>
          <el-form-item prop="password">
            <el-input v-model="ruleForm.password" type="password" placeholder="请输入密码" show-password
              :prefix-icon="Lock" />
          </el-form-item>
          <div class="login_remember">
            <el-checkbox v-model="isRememberPW" label="记住密码" />
          </div>
          <div class="login_btn">
            <el-button type="primary" @click="handleLogin(ruleFormRef)">登 录</el-button>
          </div>
        </el-form>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { getCurrentInstance, reactive, ref } from "vue";
import type { FormInstance, FormRules } from "element-plus";
import { User, Lock } from "@element-plus/icons-vue";
import {
  setToken,
  getRemember,
  setRemember,
  removeRemember,
} from "../utils/storage";
import { ElMessage } from "element-plus";
import { useRouter } from "vue-router";

const ruleFormRef = ref<FormInstance>();
const ruleForm = reactive({
  username: "",
  password: "",
});
const rules = reactive<FormRules<typeof ruleForm>>({
  username: [{ required: true, message: "请输入用户名", trigger: "change" }],
  password: [{ required: true, message: "请输入密码", trigger: "change" }],
})

const router = useRouter();

let isRememberPW = ref(Boolean(getRemember()));
let loginBtnLoading = ref(false);

if (isRememberPW.value) {
  let { username, password } = JSON.parse(getRemember());
  ruleForm.username = username;
  ruleForm.password = password;
}

const handleLogin = async (formEl: FormInstance | undefined) => {
  if (loginBtnLoading.value) return;
  if (!formEl) return;
  formEl.validate((valid, fields) => {
    if (valid) {
      loginBtnLoading.value = true;
      if (isRememberPW.value) {
        setRemember(JSON.stringify({ username: ruleForm.username, password: ruleForm.password }));
      } else {
        removeRemember();
      }
      // 这里对接登录接口,省略
      setToken('接口返回的token');
      ElMessage({
        message: '登录成功',
        type: "success",
      });
      router.push({ path: "/" });
    }
  });
};
</script>

<style lang="less" scoped>
.login_wrap {
  position: relative;
  width: 100%;
  height: 100vh;
  background: #f1f4fd;

  .login_box {
    position: absolute;
    left: 0;
    right: 0;
    top: 48%;
    width: 450px;
    margin: 0 auto;
    height: 330px;
    transform: translate3d(0, -50%, 0);
    border-radius: 20px;
    box-shadow: 0 0 30px rgba(0, 0, 0, 0.1);

    .ms_login {
      position: absolute;
      left: 0;
      right: 0;
      top: 0;
      bottom: 0;
      background-color: #fff;
      border-radius: 20px;

      .ms_title {
        width: 100%;
        text-align: center;
        font-size: 36px;
        padding-top: 30px;
        color: #00a3cc;
        letter-spacing: 2px;
      }

      .form_box {
        padding: 30px 50px;
      }

      .login_btn {
        margin-top: 8px;

        button {
          width: 100%;
        }
      }
    }
  }
}
</style>

代码中引用了element-plus的form组件,可以自行去官方文档查看用法,同样不搬运了。

最终页面如下:

image.png

登录逻辑

我们平时使用后台管理系统,只要登录过,都是会保留一段时间的登录状态,即token的有效期,登录后的每个接口我们都要把token传给后端,通过响应结果可知道该token是否过期。

上面的登录页面我们已经假设通过登录拿到了token,我们现在开发主页面,主页面一般都有一个页面布局,我在这里分为头部、侧边菜单栏、中间页面内容,接下来就开始开发页面布局,最终效果如下。

image.png

开发页面布局

我们先改造一下路由,你跟着改造一遍后,后面增加修改路由都是这么来。

先打开src\router\index.ts文件,修改代码为如下:

import { createRouter, createWebHashHistory } from 'vue-router'
import routes from './routes'

const router = createRouter({
  history: createWebHashHistory(),
  routes
})

export default router

我们把路由都拿出来,放到另一个文件里维护,在src\router文件夹下新增一个routes.ts文件,代码如下:

export const sideMenus = [
  { name: 'Dashboard', id: 0, title: '首页', children: [] },
  {
    name: 'SystemSet', id: 1, title: '系统设置', children: [
      { name: 'UserManage', id: 2, title: '用户管理' }
    ]
  }
]
export let routes = [
  {
    path: "/",
    redirect: '/Dashboard',
    name: 'Home',
    component: () => import("../layout/Home.vue"),
    children: [
      // 主页面
      {
        path: "/Dashboard",
        name: 'Dashboard',
        component: () => import("../views/Dashboard.vue"),
        meta: { title: "首页" },
      },
      // 系统设置 - 用户管理
      {
        path: "/UserManage",
        name: 'UserManage',
        component: () => import("../views/SystemSet/UserManage/index.vue"),
        meta: { title: "用户管理" },
      }
    ],
  },
  {
    path: "/login",
    component: () => import("../views/Login.vue"),
  }
]

这样,sideMenus是我们用来显示侧边的菜单,routes是我们配好的路由。

然后,我们要创建对应的页面,首先,在src目录下新建layout文件夹,在该文件夹下新建Home.vue文件。接着,在src/views文件夹下新建Dashboard.vue文件、SystemSet/UserManage/index.vue文件。

Dashboard.vue代码如下:

<template>
    <div class="main">导航页</div>
</template>

<script setup lang="ts">
import { onMounted, ref } from 'vue';
</script>

<style lang="less" scoped></style>

SystemSet/UserManage/index.vue代码如下:

<template>
    <div class="main">用户管理页面</div>
</template>

<script setup lang="ts">
import { onMounted, ref } from 'vue';
</script>

<style lang="less" scoped></style>

这样该有的页面我们都有了,重点是Home.vue下的代码开发,在这个文件我们编写我们的布局代码,代码如下:

<template>
  <div class="app_home">
    <v-header></v-header>
    <v-sidebar></v-sidebar>
    <div class="content_box" :class="{ content_collapse: collapse }">
      <v-tabs></v-tabs>
      <div class="content_main" id="content_main">
        <router-view></router-view>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import store from "@/store/index";
import { computed } from "@vue/runtime-core";
import VHeader from "./Header.vue";
import VSidebar from "./Sidebar.vue";
import VTabs from "./Tabs.vue";

const collapse = computed(() => store.webStore.collapse);
</script>

<style lang="less" scoped>
.app_home {
  width: 100%;
  height: 100vh;
  overflow: hidden;
  background-color: #ebf1f6;
  position: relative;

  .content_box {
    position: absolute;
    left: 202px;
    right: 0;
    top: 51px;
    bottom: 0;
    transition: left 0.3s ease-in-out;
  }

  .content_main {
    width: auto;
    min-width: 1100px;
    height: calc(100vh - 91px);
    overflow-y: auto;
    box-sizing: border-box;
    background-color: #fff;
    border-radius: 2px;
  }

  .content_collapse {
    left: 67px;
  }
}
</style>

在这里我们引用了三个文件,我们给他创建一下,在src\layout目录下创建Header.vueSidebar.vueTabs.vue这三个文件,分别对应头部、侧边菜单栏、中间内容的标签页。

Header.vue代码如下:

<template>
  <div class="header">
    <div class="app_name">后台管理系统</div>
    <div class="header_user">
      <el-dropdown @command="handleCommand">
        <span class="el-dropdown-link">
          <el-icon class="user_avatar">
            <Avatar />
          </el-icon>
          {{ userName }}<el-icon class="el-icon--right"><arrow-down /></el-icon>
        </span>
        <template #dropdown>
          <el-dropdown-menu>
            <el-dropdown-item command="logout">退出登录</el-dropdown-item>
          </el-dropdown-menu>
        </template>
      </el-dropdown>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ArrowDown, Avatar } from '@element-plus/icons-vue'
import { useRouter } from "vue-router";
import { ElMessage } from "element-plus";

const router = useRouter();
const userName = 'admin'
const handleLogout = () => {
  router.push({ path: "/login" });
  ElMessage({
    message: '退出成功',
    type: "success",
  });
};
const handleCommand = (command: string | number | object) => {
  if (command === 'logout') {
    handleLogout()
  }
}
</script>

<style lang="less" scoped>
.header {
  position: relative;
  box-sizing: border-box;
  width: 100%;
  height: 50px;
  font-size: 18px;
  color: #fff;
  background-color: #057de3;

  .app_name {
    float: left;
    line-height: 50px;
    font-size: 18px;
    padding: 0 21px;
  }

  .header_user {
    float: right;
    padding-right: 40px;

    .user_avatar {
      font-size: 18px;
      top: 2px;
      margin-right: 5px;
    }

    .el-dropdown {
      line-height: 50px;
      color: #fff;
    }
  }
}
</style>

handleLogout方法是退出登录,自己替换成接口就行。

Sidebar.vue代码如下:

<template>
  <div class="sidebar" :style="{ width: collapse ? '65px' : '200px' }">
    <div class="collapse_icon">
      <el-icon @click="handleCollapse">
        <Menu />
      </el-icon>
    </div>
    <div class="side_nemu">
      <el-menu :default-active="onRoutes" router unique-opened class="sidebar_menu" :collapse="collapse">
        <template v-for="item in menus">
          <template v-if="item.children.length > 0">
            <el-sub-menu :index="item.id + ''">
              <template #title>
                <el-icon>
                  <Coin />
                </el-icon>
                <span>{{ item.title }}</span>
              </template>
              <template v-for="subItem in item.children">
                <el-menu-item :index="subItem.name">
                  <el-icon>
                    <Coin />
                  </el-icon>
                  <span>{{ subItem.title }}</span>
                </el-menu-item>
              </template>
            </el-sub-menu>
          </template>
          <template v-else>
            <el-menu-item :index="item.name">
              <el-icon>
                <Help />
              </el-icon>
              <template #title>{{ item.title }}</template>
            </el-menu-item>
          </template>
        </template>
      </el-menu>
    </div>
  </div>
</template>

<script setup lang="ts">
import store from "@/store/index";
import { computed } from "@vue/runtime-core";
import { Menu, Coin, Help } from '@element-plus/icons-vue'
import { useRoute } from "vue-router";
import { sideMenus } from '../router/routes'

const route = useRoute()
const onRoutes = computed(() => {
  return route.path.replace("/", "")
})

const menus: { name: String, id: Number, title: String, children: any[] }[] = sideMenus

const collapse = computed(() => store.webStore.collapse);
const handleCollapse = () => {
  store.webStore.toggleCollapse()
}
</script>

<style lang="less" scoped>
.sidebar {
  position: absolute;
  left: 0;
  top: 52px;
  bottom: 0;
  transition: all 0.3s ease-in-out;
  overflow: hidden;
  border-top-right-radius: 5px;
  background-color: #fff;

  .collapse_icon {
    height: 40px;
    line-height: 40px;
    color: #606266;
    text-align: center;

    .el-icon {
      font-size: 18px;
      cursor: pointer;
    }
  }

  .side_nemu {
    position: absolute;
    left: 0;
    top: 40px;
    bottom: 0;
    overflow-y: scroll;
    overflow-x: hidden;
  }
}

.sidebar_menu:not(.el-menu--collapse) {
  width: 200px;
}
</style>

Tabs.vue代码如下:

<template>
  <el-tabs v-model="activeName" type="card" closable @tab-remove="removeTab" @tab-click="handleClickTab">
    <el-tab-pane v-for="item in tabList" :key="item.name" :label="item.title" :name="item.name">
    </el-tab-pane>
  </el-tabs>
  <el-dropdown class="tag_handle" @command="handleCommand">
    <el-button type="primary">
      标签操作<el-icon class="el-icon--right"><arrow-down /></el-icon>
    </el-button>
    <template #dropdown>
      <el-dropdown-menu>
        <el-dropdown-item command="closeAllTag">关闭所有标签</el-dropdown-item>
        <el-dropdown-item command="closeOtherTag">关闭其他标签</el-dropdown-item>
      </el-dropdown-menu>
    </template>
  </el-dropdown>
</template>

<script setup lang="ts">
import { ArrowDown } from '@element-plus/icons-vue'
import { watch, onMounted, ref } from 'vue'
import store from "@/store";
import { computed } from '@vue/reactivity';
import { useRoute, useRouter } from "vue-router";

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

let activeName = ref(route.name)
const tabList = computed(() => store.tabsStore.tabList)

watch(route, (newValue) => {
  if (newValue.name) {
    activeName.value = newValue.name
    addTab(newValue)
  }
})

onMounted(() => {
  addTab(route)
})

// 标签被选中
const handleClickTab = (tab: any) => router.push({ name: tab.props.name || '' })

// 添加标签
const addTab = (route: any) => store.tabsStore.addTab(route)

// 移除标签
const removeTab = (targetName: String) => {
  const curIndex = store.tabsStore.tabList.findIndex((el: any) => el.name == targetName);
  store.tabsStore.closeTab(curIndex, route);
}

// 下拉事件
const handleCommand = (command: string) => {
  switch (command) {
    case 'closeAllTag':
      store.tabsStore.closeAllTabs()
      break
    case 'closeOtherTag':
      store.tabsStore.closeOtherTab(route)
      break
  }
}
</script>

<style lang="less" scoped>
:deep(.el-tabs__header) {
  margin: 0 140px 0 0;
}

:deep(.el-tabs__item.is-active) {
  background: #fff;
}

.tag_handle {
  position: absolute;
  top: 3px;
  right: 8px;
}
</style>

到此,我们最简单的一个后台管理系统框架就搭好了,接下来我们要做的就是根据业务需要开始编写代码。