概述
记录Vue3开发Web常规流程。
创建项目
# 首先,手动创建一个目录,如article_backend
# 然后,用vscode打开该目录
# 然后,在控制台创建vue3目录
npm init vue@latest
# 创建项目过程中,手动输入项目名称,然后一路选No即可
# 然后,按照提示执行一下命令:
cd article_backend
npm install
npm run dev
清理项目默认初始化内容
# 将assets目录下的文件全部删除
# 将components目录下的文件全部删除
# 将App.vue中的script, template, style标签中的内容清空
# 将main.js中导入main.css的语句删除
# 修改index.html中的标题
安装vue3开发常规库
# 组件库
npm install element-plus
npm install echarts # 如果项目中使用图标的话,才安装echarts
# 网络库
npm install axios
# sass
npm install sass -D
# 路由
npm install vue-router
# 状态管理
npm install pinia
手动创建vue3开发常规目录
src/api -- 网络访问api统一封装在这里
src/assets -- 静态资源,如图片、全局css样式、全局js等
src/components -- 非路由对应的页面(.vue文件)
src/pages -- 路由对应的页面(.vue文件)
src/router -- 路由配置
src/stores -- 状态管理
src/utils -- 工具文件,如时间处理函数、网络访问返回值统一处理函数等
常规代码
main.ts
引入路由、组件库等。
import { createApp } from 'vue'
import App from './App.vue'
// 引入路由组件
import router from './router'
//引入状态管理组件
import { createPinia } from 'pinia'
const pinia = createPinia()
// 引入UI组件
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import locale from 'element-plus/dist/locale/zh-cn' //中文
// 创建全局唯一变量
// 声明使用路由、状态管理和UI组件,并挂载根组件
const app = createApp(App)
app.use(router)
app.use(pinia)
app.use(ElementPlus, { locale })
app.mount('#app')
api
网络访问接口文件:index.js
备注:若功能复杂,可将index.js拆分成各功能对应的js文件,如article.js,user.js等。
import request from '@/utils/request.js'
//Login
export function login(username: string, password: string) {
return request.post('/login', { username: username, password: password })
}
components
Layout.vue
<template>
<div class="layout-style">
<Header></Header>
<div class="content-area">
<Menu></Menu>
<RouterView></RouterView>
</div>
</div>
</template>
<script lang="ts" setup>
import Header from './Header.vue'
import Menu from './Menu.vue'
import { RouterView } from 'vue-router'
</script>
<style lang="scss" scope>
.layout-style {
height: 100vh;
position: relative;
overflow: hidden;
// background-color: #fafafa;
background: url('../assets/page-back.png');
.content-area {
display: flex;
height: 100vh;
}
}
</style>
Header.vue
<template>
<div class="wapper">
<div class="header-style">
<span class="title-1">在线教育</span>
<span class="title-2">后台管理系统</span>
<div class="right">
<span><img src="../assets/user.png" alt="" />{{ userName }}</span>
<span class="logout" @click="logout"
><img src="../assets/logout.png" alt="" />退出登录</span
>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import api from '@/api'
import { useRouter } from 'vue-router'
const router = useRouter()
const userName = '李四'
const logout = () => {
console.log('退出登录')
api.logout().then(
(res) => {
router.push('/login')
},
(err) => {
console.error(err)
router.push('/login')
}
)
}
</script>
<style lang="scss" scoped>
.header-style {
height: 40px;
padding-top: 12px;
border-bottom: 1px rgb(184, 189, 190) solid;
font-family: 'KaiTi';
.title-1 {
color: rgb(250, 250, 250);
font-size: 26px;
margin-left: 10px;
}
.title-2 {
color: rgb(250, 250, 250);
font-size: 22px;
}
.right {
float: right;
margin-top: 6px;
color: rgb(250, 250, 250);
span {
display: inline-flex;
align-items: center;
margin-right: 20px;
font-size: 16px;
img {
margin-right: 3px;
width: 20px;
height: 20px;
}
}
.logout:hover {
cursor: pointer;
color: #a89e9e;
}
}
}
</style>
Menu.vue
<template>
<div class="menu">
<el-menu
:default-active="$route.path"
unique-opened
background-color="transparent"
text-color="#e5e5e7"
active-text-color="#FFF"
:router="true"
>
<el-menu-item index="/home">
<span>课程管理</span>
</el-menu-item>
<!-- <el-submenu index="1">
<template slot="title">
<span>课程管理</span>
</template>
<el-menu-item-group>
<el-menu-item index="/course/list">课程列表</el-menu-item>
</el-menu-item-group>
<el-menu-item-group>
<el-menu-item index="/course/add">添加课程</el-menu-item>
</el-menu-item-group>
</el-submenu> -->
<el-menu-item index="/user">
<span>用户管理</span>
</el-menu-item>
</el-menu>
</div>
</template>
<script lang="ts" setup></script>
<style lang="scss" scope>
span {
color: rgb(250, 250, 250);
font-size: 18px;
}
.menu {
height: 100vh;
border-left: 1px rgb(184, 189, 190) solid;
border-right: 1px rgb(184, 189, 190) solid;
.el-menu {
border: 0;
}
img {
width: 20px;
height: 20px;
padding-right: 20px;
}
}
</style>
pages
Login.vue
<template>
<div class="login-container">
<div class="login-backgroud-img">
<div class="login-title">在线教育后台管理系统</div>
<div class="login-about">
<div class="login-input">
<p>用户登录</p>
<el-form
ref="ruleFormRef"
style="max-width: 600px; margin: 20px"
:model="ruleForm"
:rules="rules"
class="demo-dynamic"
label-position="right"
label-width="auto"
@keyup.enter.native="onLoginClicked(ruleFormRef)"
>
<!---用户名-->
<el-form-item label="用户名" prop="username">
<el-input v-model="ruleForm.username" placeholder="请输入用户名" />
</el-form-item>
<!---密码-->
<el-form-item label="密码" prop="password">
<el-input
v-model="ruleForm.password"
type="password"
placeholder="请输入密码"
/>
</el-form-item>
<!--登录按钮-->
<el-button type="primary" @click="onLoginClicked(ruleFormRef)">
登 录
</el-button>
</el-form>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import api from '@/api'
import { useRouter } from 'vue-router'
const router = useRouter()
const ruleFormRef = ref<FormInstance>()
const ruleForm = reactive({
username: '',
password: '',
})
const validateRule = (rule: any, value: any, callback: any) => {
if (value === '') {
callback(new Error(''))
} else {
callback()
}
}
const rules = reactive<FormRules<typeof ruleForm>>({
username: [{ validator: validateRule, message: '请输入用户名', trigger: 'blur' }],
password: [{ validator: validateRule, message: '请输入密码', trigger: 'blur' }],
})
//登录
const onLoginClicked = async (formEl: FormInstance | undefined) => {
//表单校验
if (!formEl) return
await formEl.validate((valid, fields) => {
if (valid) {
console.log('username: ', ruleForm.username, ', password: ', ruleForm.password)
//登录
api.login(ruleForm.username, ruleForm.password).then(
(res) => {
console.log('res', res)
const { token } = res
localStorage.setItem('token', token)
router.push('/home')
},
(err) => {
console.log('err', err)
}
)
} else {
// console.log('表单校验不通过!', fields)
return
}
})
}
</script>
<style scoped>
.login-container {
background-image: url('../assets/login-back.png');
height: 100vh;
/* background-color: rgb(60, 60, 60); */
background-color: rgb(60, 179, 113);
position: relative;
}
.login-title {
height: 100%;
text-align: center;
color: rgb(240, 240, 240);
font-family: 'KaiTi';
font-size: 34px;
}
.login-about {
position: absolute;
width: 100%;
top: 50%;
transform: translate(0, -50%);
/* background-color: rgba(255, 255, 255, 0.2); */
padding-bottom: 20px;
}
.login-input {
width: 400px;
height: 200px;
margin-left: 50%;
padding-bottom: 5px;
border-radius: 10px;
color: rgb(80, 80, 80);
background-color: rgba(255, 255, 255, 0.2);
text-align: center;
font-size: 22px;
p {
padding-top: 10px;
}
}
</style>
Home.vue
<template>
<h1>Home页面</h1>
</template>
<script lang="ts" setup></script>
<style lang="scss" scoped></style>
User.vue
<template>
<div class="container" v-loading="loading">
<div class="content">
<!-- 新增按钮 -->
<div class="add-button">
<el-button type="primary" size="medium" @click="changeMessage(1)">新增</el-button>
</div>
<!-- 表格 -->
<el-table
border
class="table-content"
:header-cell-style="{ background: 'rgba(35, 101, 176, 0.3)' }"
:cell-style="{ textAlign: 'center' }"
:data="tableData"
style="width: 98.5%"
header-row-style="color: rgb(250, 250, 250)"
>
<el-table-column prop="userName" align="center" label="用户名"> </el-table-column>
<el-table-column prop="typeName" align="center" label="权限类型"> </el-table-column>
<el-table-column align="center" label="操作">
<template v-slot="scope">
<div class="icon-style">
<button
class="icon-btn-del"
type="button"
@click="delMessage(scope.row)"
>
删除
</button>
<button
class="icon-btn-change"
type="button"
@click="changeMessage(2, scope.row)"
>
修改
</button>
</div>
</template>
</el-table-column>
</el-table>
</div>
<!-- 弹窗:新增 / 修改 -->
<el-dialog
class="message-model"
v-model="addDialog"
:title="titleName"
center
:append-to-body="true"
:lock-scroll="false"
width="30%"
>
<div class="dialog-info">
<el-form
ref="ruleFormRef"
class="model-form"
:model="ruleForm"
:rules="rules"
label-width="70px"
>
<el-form-item label="用户名:" prop="userName">
<el-input v-model="ruleForm.userName" placeholder=""></el-input>
</el-form-item>
<el-form-item label="新密码:" prop="password">
<el-input
type="password"
v-model="ruleForm.password"
placeholder=""
></el-input>
</el-form-item>
<el-form-item label="类型:" prop="typeNo">
<el-select v-model="ruleForm.typeNo" ref="typeName">
<el-option
v-for="(item, index) in userTypeList"
:key="index"
:label="item.typeName"
:value="item.typeNo"
></el-option>
</el-select>
</el-form-item>
<div class="dialog-button">
<el-button type="primary" @click="subData(ruleFormRef)"> 提交 </el-button>
<el-button type="danger" @click="closeDialog()"> 取消 </el-button>
</div>
</el-form>
</div>
</el-dialog>
<!-- 新增 / 修改 end -->
</div>
</template>
<script lang="ts" setup>
import { ref, reactive } from 'vue'
import type { FormInstance, FormRules, ElMessage, ElMessageBox } from 'element-plus'
//data
let loading = ref(false)
let titleName = ref('')
let addDialog = ref(false)
let tableData = ref([])
let userTypeList = ref([])
let activeDialog = ref(0)
let scope = ref({})
const ruleFormRef = ref<FormInstance>()
const ruleForm = reactive({
typeNo: '',
typeName: '',
userName: '',
password: '',
})
const rules = reactive<FormRules<typeof ruleForm>>({
userName: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入新密码', trigger: 'blur' }],
typeNo: [{ required: true, message: '请选择用户类型', trigger: 'change' }],
})
//function
// 初始化
const userInit = () => {
api.userInit().then((res) => {
tableData.value = res.userList
userTypeList.value = res.userTypeList
ruleForm.typeNo = userTypeList[0].typeNo
changeList()
})
}
// 为列表赋值默认参数
const changeList = () => {
tableData.forEach((item, index) => {
tableData[index].userName = item.name
tableData[index].typeNo = item.type
userTypeList.forEach((i, v) => {
if (item.type == i.typeNo) {
tableData[index].typeName = i.typeName
}
})
})
}
// 根据不同的参数 修改展示弹层的标题信息
const changeMessage = (type, value) => {
console.log('changeMessage: ', type, value)
addDialog.value = true
activeDialog.value = type
if (type == 1) {
titleName.value = '新增用户信息'
} else {
titleName.value = '修改用户信息'
ruleForm = JSON.parse(JSON.stringify(value))
}
}
// 关闭弹层
const closeDialog = () => {
addDialog.value = false
ruleFormRef.value.resetFields()
}
// 根据不同的参数调取不同的接口
const subData = (ruleForm) => {
let isTrue = true
ruleFormRef.validate((valid) => {
if (!valid) {
isTrue = false
} else {
isTrue = true
}
})
if (!isTrue) {
return false
}
loading.value = true
if (activeDialog.value == 1) {
addUser()
} else {
updateUser()
}
}
// 新增用户
const addUser = () => {
api.userAdd({
typeNo: ruleForm.typeNo,
typeName: ruleFormRef.typeName.selectedLabel,
userName: this.ruleForm.userName,
password: this.ruleForm.password,
}).then((res) => {
loading.value = false
if (res.code == 200) {
addDialog.value = false
ElMessage('保存成功!')
userInit()
}
})
}
//修改用户信息
const updateUser = () => {
loading.value = true
api.userModify({
id: ruleForm.id,
typeNo: ruleForm.typeNo,
typeName: ruleFormRef.typeName.selectedLabel,
userName: ruleForm.userName,
password: ruleForm.password,
}).then((res) => {
loading.value = false
if (res.code == 200) {
addDialog.value = false
ElMessage('修改成功!')
userInit()
}
})
}
// 删除用户
const delMessage = (value) => {
ElMessageBox.confirm('您真的要删除吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
api.userDelete({
id: value.id,
}).then((res) => {
if (res.code == 200) {
addDialog.value = false
ElMessage('删除成功!')
userInit()
}
})
})
}
</script>
<style lang="scss" scoped>
.container {
width: 100%;
height: 100%;
// overflow-y: scroll;
//新增按钮
.add-button {
display: flex;
justify-content: flex-end;
margin-top: 10px;
margin-right: 10px;
}
//表格
.content {
height: 100%;
// background-color: rgba(192, 211, 234, 0.8);
overflow: hidden;
.table-content {
margin-left: 10px;
margin-top: 10px;
}
.icon-style {
button {
margin-right: 10px;
color: #333;
// background: transparent;
padding: 0 10px;
font-size: 12px;
}
.icon-btn-del {
background: #ed5a20;
}
.icon-btn-change {
background: #d9ac00;
}
}
}
}
.dialog-button {
display: flex;
justify-content: center;
}
</style>
router
路由配置文件:index.js
import { createRouter, createWebHistory } from 'vue-router'
import Login from '@/pages/Login.vue'
import Layout from '@/components/Layout.vue'
import Home from '@/pages/Home.vue'
import User from '@/pages/User.vue'
//路由
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
redirect: '/home',
},
{
path: '/login',
name: 'login',
component: Login,
},
{
path: '/home',
component: Layout,
children: [
{
// 主页
path: '/home',
name: 'home',
component: Home,
},
{
// 用户管理
path: '/user',
name: 'user',
component: User,
},
],
},
],
})
//路由守卫
router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token')
if (to.name != 'login' && !token) next({ name: 'login' })
else next()
})
export default router
stores
以计数器状态管理文件couter.ts为例:
import { defineStore } from 'pinia'
//本文件为示例代码
export const useCounterStore = defineStore('counterStore', {
state: () => ({
count: 0,
}),
actions: () => ({
increament() {
this.count++
},
}),
})
utils
网络访问返回值统一处理:request.js
import axios from 'axios'
//配置后台服务地址
const baseURL = 'http://localhost:8080'
//创建网络访问实例
const instance = axios.create({ baseURL })
//请求拦截器
instance.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token')
if (token) config.headers.Authorization = token
return config
},
(error) => {
Promise.reject(error)
}
)
//响应拦截器
instance.interceptors.response.use(
(response) => {
return response
},
(error) => {
return Promise.reject(error)
}
)
// 导出实例
export default instance