第一章:项目结构与路由配置
1.1 创建路由配置文件
首先按照项目文档创建路由配置文件,这一步建立应用的路由骨架:
第一步:创建路由配置文件
bash
复制下载
# 在src/router目录下创建routes.ts文件
# 这是路由配置文件,集中管理所有路由规则
touch src/router/routes.ts
第二步:编写路由配置代码
创建src/router/routes.ts文件,编写以下内容:
typescript
复制下载
export default [
{//登录路由
path:'/login',
component:() =>import('@/views/login/MyLogin.vue'),
name:'login'
},
{
//登录成功后的路由
path:'/',
component:()=>import('@/views/home/MyHome.vue'),
name:'home'
},
{//404
path:'/404',
component:()=>import('@/views/404/MyError.vue'),
name:'404'
},
{//其他任意路由
path:'/:pathMatch(.*)*',
redirect:'/404',
name:'Any'
}
]
路由配置解析:
- 登录路由:
path: '/login'定义了登录页面的访问路径,用户输入/login时加载MyLogin.vue组件。 - 动态导入:使用
() => import('@/路径')语法实现代码分割,每个路由对应的组件单独打包,减少初始加载时间。 - 路由名称:为每个路由设置
name属性,方便在代码中通过名称跳转,避免硬编码路径字符串。 - 通配符路由:
path: '/:pathMatch(.*)*'匹配所有未定义的路由,将用户重定向到404页面,提供更好的用户体验。
第三步:配置路由实例
在src/router/index.ts中引入路由配置:
typescript
复制下载
import {createRouter,createWebHashHistory} from 'vue-router'
//引入路由规则
import routes from "./routes"
const router = createRouter({
history:createWebHashHistory(),
routes,
//滚动行为
scrollBehavior() {
return {
left: 0,
top: 0,
}
}
})
export default router;
第二章:登录页面开发
2.1 创建登录页面组件
第一步:创建登录页面文件结构
bash
复制下载
# 创建登录页面目录和文件
mkdir -p src/views/login
touch src/views/login/MyLogin.vue
第二步:编写登录页面模板
按照项目文档,创建登录页面的模板部分:
vue
复制下载
<template>
<div class="login_container">
<el-row>
<el-col :span="12" :xs="0"></el-col>
<el-col :span="12" :xs="24">
<!-- 登录表单 -->
<el-form class="login_form" :model="loginUserInfo" :rules="rules" ref="loginForm">
<h1>Hello</h1>
<h2>欢迎拉到硅谷甑选</h2>
<el-form-item prop="username">
<el-input :prefix-icon="User" v-model="loginUserInfo.username"></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
type="password"
:prefix-icon="Lock"
v-model="loginUserInfo.password"
show-password
></el-input>
</el-form-item>
<el-form-item>
<el-button
class="login_btn"
type="primary"
size="default"
@click="login"
:loading="loading"
>登录</el-button
>
</el-form-item>
</el-form>
</el-col>
</el-row>
</div>
</template>
模板解析:
- 响应式布局:使用Element Plus的栅格系统,
span="12"表示在中等以上屏幕占12列(50%),xs="0"表示在超小屏幕隐藏,xs="24"表示在超小屏幕占24列(100%)。 - 表单绑定:
:model="loginUserInfo"将表单绑定到数据对象,:rules="rules"绑定验证规则。 - 图标使用:
:prefix-icon="User"使用Element Plus的图标组件,需要在script中导入。 - 密码框特性:
type="password"创建密码输入框,show-password添加眼睛图标切换密码可见性。
2.2 实现登录页面逻辑
第三步:编写登录页面脚本
按照项目文档,添加script部分:
vue
复制下载
<script setup lang="ts">
//引入icon图标
import { User, Lock } from "@element-plus/icons-vue";
import { reactive, ref } from "vue";
import useUserStore from "@/stores/modules/user";
import { useRouter } from "vue-router";
import { ElNotification } from "element-plus";
import { getTime } from "@/utils/time";
//获取用户store实例
const userStore = useUserStore();
//获取路由器
const router = useRouter();
//响应数据--用户信息
const loginUserInfo = reactive({
username: "admin",
password: "123456",
});
//响应式数据--loading状态
const loading = ref<boolean>(false);
//表单对象
const loginForm = ref();
//登录按钮回调
const login = async () => {
//表单校验成功后在发送请求
await loginForm.value.validate();
//loading
loading.value = true;
//发送请求
userStore
.userLogin(loginUserInfo)
.then((req) => {
console.log(req); //'ok'
//loading
loading.value = false;
//页面跳转
router.push("/");
//弹框
ElNotification({
type: "success",
message: "欢迎回来",
title: `Hi,${getTime()}好`,
});
})
.catch((error) => {
console.log(error);
//loading
loading.value = false;
//弹框
ElNotification({
type: "error",
message: error.message,
});
});
};
//自定义校验rules
const validatorUserName = (rule: any, value: any, callback: any) => {
if (value.length >= 5) {
callback();
} else {
callback(new Error("账号长度至少五位数"));
}
};
const validatorPassWrod = (rule: any, value: any, callback: any) => {
if (value.length >= 6) {
callback();
} else {
callback(new Error("密码长度至少五位数"));
}
};
//表单校验rules
const rules = {
username: [
{
trriger: "change",
//自定义表单校验
validator: validatorUserName,
},
],
password: [
{
trriger: "change",
validator: validatorPassWrod,
},
],
};
</script>
脚本解析:
-
响应式数据:
reactive({ username: '', password: '' }):创建响应式表单数据对象ref(false):创建响应式加载状态
-
自定义验证函数:
- Element Plus验证函数接收三个参数:
rule(规则对象)、value(字段值)、callback(回调函数) - 验证通过调用
callback(),失败调用callback(new Error('错误信息'))
- Element Plus验证函数接收三个参数:
-
表单验证流程:
- 先调用
loginForm.value.validate()验证所有字段 - 验证通过后才执行登录请求
- 先调用
-
登录处理流程:
- 设置
loading.value = true显示加载状态 - 调用
userStore.userLogin()发起登录请求 - 成功:跳转首页并显示欢迎通知
- 失败:显示错误通知
- 设置
2.3 添加登录页面样式
第四步:编写登录页面样式
vue
复制下载
<style scoped>
.login_container {
width: 100%;
height: 100vh;
background: url("@/assets/images/background.jpg") no-repeat;
background-size: cover;
}
.login_form {
position: relative;
width: 80%;
top: 30vh;
background: url("@/assets/images/login_form.png") no-repeat;
background-size: cover;
padding: 40px;
}
h1 {
color: white;
font-size: 40px;
}
h2 {
color: white;
font-size: 20px;
margin: 20px 0px;
}
.login_btn {
width: 100%;
}
</style>
第三章:API服务层封装
3.1 创建API类型定义
第一步:创建类型定义文件
bash
复制下载
# 创建API类型定义文件
touch src/api/user/type.ts
第二步:编写类型定义代码
typescript
复制下载
//登录接口需要携带参数type
export interface loginForm{
username:string,
password:string
}
//登录接口返回数据type
interface dataType{
token?:string,
message?:string
}
export interface loginResponseData{
code:number,
data:dataType
}
//查询用户信息接口返回数据type'
interface userInfo {
username:string,
password:string,
desc:string,
roles:string[],
buttons:string[],
routes:string[],
token:string
}
interface user {
checkUser:userInfo
}
export interface userResponseData {
code:number,
data:user
}
类型定义说明:
- 接口分离:将请求参数和响应数据分别定义接口,提高代码可读性。
- 可选属性:使用
?表示可选属性,如token?表示登录成功时返回,失败时不返回。 - 数组类型:
string[]表示字符串数组,用于权限列表。
3.2 创建API请求函数
第三步:创建API接口文件
bash
复制下载
# 创建用户API接口文件
touch src/api/user/index.ts
第四步:编写API接口代码
typescript
复制下载
//用户相关接口
import request from "@/utils/request";
//类型
import type { loginForm, loginResponseData, userResponseData } from "./type";
//统一管理接口
enum API {
LOGIN_URL = "user/login",
USERINFO_URL = "/user/info",
}
//暴露请求函数
//登录接口
export const reqLogin = (data: loginForm) => request.post<any, loginResponseData>(API.LOGIN_URL, data);
//获取用户信息接口
export const reqUserInfo = () =>request.get<any, userResponseData>(API.USERINFO_URL);
API设计要点:
- 枚举管理URL:使用TypeScript枚举集中管理所有接口URL,避免在代码中硬编码字符串。
- 类型安全:为每个API函数明确定义参数类型和返回类型,提供完整的TypeScript类型支持。
- 统一导出:每个API函数独立导出,方便按需导入。
第四章:状态管理实现
4.1 创建用户状态管理
第一步:创建用户store文件
bash
复制下载
# 创建用户状态管理文件
mkdir -p src/stores/modules
touch src/stores/modules/user.ts
第二步:编写用户store代码
typescript
复制下载
// src/stores/modules/user.ts
//用户仓库
import {defineStore} from 'pinia'
//用户登录接口
import {reqLogin} from '@/api/user'
//参数type
import type { loginForm ,loginResponseData} from '@/api/user/type'
import type { UserState } from '../types/type'
//存储和读取token方法
import { SET_TOKEN,GET_TOKEN } from '@/utils/token'
const useUserStore = defineStore('User',{
state:():UserState=>{
return {
token:GET_TOKEN()
}
},
actions:{
//用户登录方法
async userLogin(data:loginForm){
const result:loginResponseData = await reqLogin(data);
// console.log(result);
if(result.code === 200){
//登录成功,pinia存储token数据
console.log('登录成功');
this.token = result.data.token as string
//本地持久化token
SET_TOKEN(this.token as string)
//返回一个成功的promise
return 'ok'
}else{
return Promise.reject(new Error(result.data.message))
}
}
},
getters:{
getToken:(state)=>state.token
}
})
//暴露
export default useUserStore;
状态管理要点:
- 状态初始化:在
state中定义初始状态,token从localStorage读取。 - 异步动作:
actions中定义异步方法userLogin,处理登录逻辑。 - 状态更新:登录成功后更新store中的
token状态。 - 数据持久化:同时将token保存到localStorage,实现页面刷新后仍保持登录状态。
- 错误处理:登录失败时返回拒绝的Promise,携带错误信息。
4.2 创建token工具函数
第三步:创建token工具文件
bash
复制下载
# 创建token工具文件
touch src/utils/token.ts
第四步:编写token工具代码
typescript
复制下载
// src/utils/token.ts
//本地存储和读取token数据方法
export const SET_TOKEN = (token:string)=>{
localStorage.setItem('TOKEN',token);
}
export const GET_TOKEN = ()=>{
return localStorage.getItem('TOKEN' as string);
}
token管理说明:
- 统一管理:将token的存储和读取封装成函数,便于统一管理和修改。
- 简单可靠:使用localStorage存储,简单可靠,支持页面刷新后保持登录状态。
- 可扩展性:可以轻松添加其他token管理函数,如删除、检查等。
第五章:请求封装实现
5.1 创建请求封装文件
第一步:创建请求封装文件
bash
复制下载
# 创建axios封装文件
touch src/utils/request.ts
第二步:编写请求封装代码
typescript
复制下载
// src/utils/request.ts
//axios 二次封装
import axios from "axios"
import { ElMessage } from "element-plus";
const request = axios.create({
baseURL:import.meta.env.VITE_APP_BASE_API,
timeout:5000
});
//request实例添加请求与响应拦截器
request.interceptors.request.use((config) =>{
//confog配置对象,headers属性请求头,携带公共参数
return config;
})
//响应拦截器
request.interceptors.response.use((response)=>{
return response.data;
},(error)=>{
let message = '';
const status = error.response.status;
switch(status){
case 401:
message = 'TOKEN过期'
break;
case 403:
message = '无权访问'
break;
case 404:
message = '请求地址错误'
break;
case 500:
message = '服务器出现问题'
break;
default:
message = '网络出现问题'
break;
}
ElMessage({
type:'error',
message
});
return Promise.reject(error);
})
export default request;
请求封装要点:
-
实例创建:使用
axios.create()创建独立实例,可以设置默认配置。 -
环境变量:
import.meta.env.VITE_APP_BASE_API从Vite环境变量读取API基础URL。 -
请求拦截器:在请求发送前统一处理,如添加token到请求头。
-
响应拦截器:
- 成功:直接返回
response.data,简化业务代码 - 失败:根据HTTP状态码显示友好错误提示
- 成功:直接返回
-
错误统一处理:将HTTP错误转换为用户友好的中文提示。
第六章:Mock数据配置
6.1 创建Mock数据文件
第一步:创建Mock配置文件
bash
复制下载
# 创建Mock数据目录和文件
mkdir -p mock
touch mock/user.ts
第二步:编写Mock数据代码
typescript
复制下载
// mock/user.ts
// 用户相关的Mock数据,用于开发阶段模拟后端接口
/*
* @Description: Stay hungry,Stay foolish
* @Author: Huccct
* @Date: 2024-03-21
*/
// 模拟用户列表数据
const userList = [
{
id: 1,
username: 'admin',
password: '123456',
name: '超级管理员',
phone: '13800138000',
roleName: '超级管理员',
createTime: '2024-03-21',
updateTime: '2024-03-21',
status: 1,
},
{
id: 2,
username: 'test',
password: '123456',
name: '测试用户',
phone: '13800138001',
roleName: '普通管理员',
createTime: '2024-03-21',
updateTime: '2024-03-21',
status: 1,
},
]
export default [
// 用户登录接口
{
url: '/api/user/login',
method: 'post',
response: ({ body }) => {
const { username, password } = body
const checkUser = userList.find(
(item) => item.username === username && item.password === password,
)
if (!checkUser) {
return { code: 201, data: { message: '账号或者密码不正确' } }
}
return { code: 200, data: {token:'Admin Token' }}
},
},
// 获取用户信息
{
url: '/api/user/info',
method: 'get',
response: (request) => {
const token = request.headers.token
if (token === 'Admin Token') {
return {
code: 200,
data: {
name: 'admin',
avatar:
'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
roles: ['admin'],
buttons: ['cuser.detail'],
routes: [
'home',
'Acl',
'User',
'Role',
'Permission',
'Product',
'Trademark',
'Attr',
'Spu',
'Sku',
],
},
message: '获取用户信息成功',
}
}
return {
code: 201,
data: null,
message: '获取用户信息失败',
}
},
},
// 获取用户列表
{
url: '/api/acl/user/:page/:limit',
method: 'get',
response: ({ query }) => {
const { username } = query
let filteredList = userList
if (username) {
filteredList = userList.filter((user) =>
user.username.includes(username),
)
}
return {
code: 200,
data: {
records: filteredList,
total: filteredList.length,
},
}
},
},
// 添加/更新用户
{
url: '/api/acl/user/save',
method: 'post',
response: ({ body }) => {
const newUser = {
...body,
id: userList.length + 1,
createTime: new Date().toISOString().split('T')[0],
updateTime: new Date().toISOString().split('T')[0],
status: 1,
}
userList.push(newUser)
return { code: 200, data: null, message: '添加成功' }
},
},
{
url: '/api/acl/user/update',
method: 'put',
response: ({ body }) => {
const index = userList.findIndex((item) => item.id === body.id)
if (index !== -1) {
userList[index] = {
...userList[index],
...body,
updateTime: new Date().toISOString().split('T')[0],
}
}
return { code: 200, data: null, message: '更新成功' }
},
},
// 删除用户
{
url: '/api/acl/user/remove/:id',
method: 'delete',
response: (request) => {
const id = request.query.id
if (!id) {
return { code: 201, data: null, message: '参数错误' }
}
const index = userList.findIndex((item) => item.id === Number(id))
if (index !== -1) {
userList.splice(index, 1)
return { code: 200, data: null, message: '删除成功' }
}
return { code: 201, data: null, message: '用户不存在' }
},
},
// 批量删除用户
{
url: '/api/acl/user/batchRemove',
method: 'delete',
response: ({ body }) => {
const { idList } = body
idList.forEach((id) => {
const index = userList.findIndex((item) => item.id === id)
if (index !== -1) {
userList.splice(index, 1)
}
})
return { code: 200, data: null, message: '批量删除成功' }
},
},
// 获取用户角色
{
url: '/api/acl/user/toAssign/:userId',
method: 'get',
response: () => {
return {
code: 200,
data: {
assignRoles: [
{
id: 1,
roleName: '超级管理员',
createTime: '2024-03-21',
updateTime: '2024-03-21',
},
],
allRolesList: [
{
id: 1,
roleName: '超级管理员',
createTime: '2024-03-21',
updateTime: '2024-03-21',
},
{
id: 2,
roleName: '普通管理员',
createTime: '2024-03-21',
updateTime: '2024-03-21',
},
],
},
}
},
},
// 分配用户角色
{
url: '/api/acl/user/doAssignRole',
method: 'post',
response: () => {
return { code: 200, data: null, message: '分配角色成功' }
},
},
// 用户登出接口
{
url: '/api/user/logout',
method: 'post',
response: () => {
return { code: 200, data: null, message: '退出成功' }
},
},
]
Mock配置说明:
- 用户数据:定义模拟的用户数据,包含用户名、密码、角色和token。
- 登录验证:模拟后端的登录验证逻辑,检查用户名和密码是否匹配。
- token验证:获取用户信息时需要验证token,模拟真实的后端权限验证。
- 延迟设置:
timeout: 1000设置1秒延迟,模拟真实网络请求。
第七章:模块集成与测试
7.1 集成所有模块
第一步:在main.ts中集成路由和store
typescript
复制下载
// src/main.ts
import { createApp } from "vue";
import App from "./App.vue";
//引入element-plus插件
import ElementPlus from "element-plus";
//引入element-plus的样式
import "element-plus/dist/index.css";
//引入element的icons
import * as ElementPlusIconsVue from "@element-plus/icons-vue";
//引入路由
import router from "@/router/index";
//引入仓库
import pinia from "@/stores";
//全局样式重置
import "./styles/reset.css";
//创建应用实例
const app = createApp(App);
//使用插件
app.use(ElementPlus);
app.use(router);
app.use(pinia);
//全局注册icons
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component);
}
app.mount("#app");
第二步:创建并导出store
typescript
复制下载
//大仓库
import {createPinia} from 'pinia'
export default createPinia();
第八章:开发要点总结
8.1 核心实现要点
- 路由配置:使用动态导入实现路由懒加载,优化首屏性能。
- 表单验证:使用Element Plus表单验证,结合自定义验证函数实现复杂验证逻辑。
- 状态管理:使用Pinia集中管理用户状态,配合localStorage实现状态持久化。
- 请求封装:二次封装axios,统一处理请求和响应,简化错误处理。
- Mock数据:使用vite-plugin-mock提供开发阶段的API模拟,支持前后端并行开发。
8.2 代码组织规范
- 目录结构:按照功能模块组织代码,如
views/、api/、stores/、utils/。 - 类型安全:使用TypeScript为所有接口和数据定义类型,提高代码可靠性。
- 配置集中:将路由、API URL、验证规则等配置集中管理,便于维护。