概述

记录Vue3开发Web常规流程。

创建项目

# 首先,手动创建一个目录,如article_backend
# 然后,用vscode打开该目录
# 然后,在控制台创建vue3目录

npm init vue@latest

# 创建项目过程中,手动输入项目名称,然后一路选No即可
# 然后,按照提示执行一下命令:

cd article_backend
npm install
npm run dev

Vue3开发常规流程_vue

Vue3开发常规流程_vue_02

Vue3开发常规流程_vue_03

清理项目默认初始化内容

# 将assets目录下的文件全部删除
# 将components目录下的文件全部删除
# 将App.vue中的script, template, style标签中的内容清空
# 将main.js中导入main.css的语句删除
# 修改index.html中的标题

Vue3开发常规流程_vue_04

安装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