开发前准备
可以跟着我的文章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组件,可以自行去官方文档查看用法,同样不搬运了。
最终页面如下:
登录逻辑
我们平时使用后台管理系统,只要登录过,都是会保留一段时间的登录状态,即token
的有效期,登录后的每个接口我们都要把token
传给后端,通过响应结果可知道该token
是否过期。
上面的登录页面我们已经假设通过登录拿到了token
,我们现在开发主页面,主页面一般都有一个页面布局,我在这里分为头部、侧边菜单栏、中间页面内容,接下来就开始开发页面布局,最终效果如下。
开发页面布局
我们先改造一下路由,你跟着改造一遍后,后面增加修改路由都是这么来。
先打开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.vue
、Sidebar.vue
、Tabs.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>
到此,我们最简单的一个后台管理系统框架就搭好了,接下来我们要做的就是根据业务需要开始编写代码。