Vue3项目实战(vue3+vite+pinia+element-plus+axios+mock)
许苑后台管理系统
- 一、项目介绍
- 1、技术栈
- 2、业务功能
- 3、应用场景
- 4、项目源码
- 二、项目实战
- 2.1、项目初始化
- 2.2、项目实战
- 1、引入router配置
- 2、App.vue组件引入
- 3、创建RootView根路由组件
- 4、依次创建
- 5、进行对应各个环境的配置环境设置
一、项目介绍
1、技术栈
- vue3+vite+vue-router
- pinia
- element-plus
- axios
- mock
- Echarts
2、业务功能
- 登录
- 首页
- 商品
- 用户管理
3、应用场景
- 进行后台管理项目的
- 根据不同用户的权限授予不同的功能
4、项目源码
xuyuan-upward 希望每位大佬获取时候点个小小的赞!
二、项目实战
2.1、项目初始化
1、构建项目
# npm 7+,需要添加额外的 --:
npm create vite@latest my-vue-app -- --template vue
2、安装成功后进行安装依赖
npm install
3、修改路径替代符
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vite.dev/config/
export default defineConfig({
plugins: [vue()],
/* 添加的是别名 */
resolve: {
alias: {
'@': '/src',
},
},
})
4、引入element-plus依赖,axios,router
安装依赖内容过于简单,请自行取相关内容官网查看文档进行安装
element-plus官网
axios官网
router
2.2、项目实战
1、引入router配置
import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
name: 'main',
component: () => import('@/views/RootView.vue'),
redirect: '/home',
/* 访问 /home 时的行为
当你访问 /home 时,会发生以下情况:
匹配父路由:
首先匹配到 path: '/' 的路由配置。
由于 Main.vue 是父组件,它会被渲染。
匹配子路由:
在 Main.vue 内部的 <router-view> 中,会匹配到 path: '/home' 的子路由。
因此,Home.vue 会在 Main.vue 的 <router-view> 中渲染。 */
children: [
{
path: '/home',
name: 'home',
component: () => import('@/views/MainView.vue')
},
]
},
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
接着还需要导入main.js一些配置文件
- 全局样式
- pinia等等
import { createApp } from 'vue'
import App from './App.vue'
// 导入全局样式
import "@/assets/less/index.less"
import router from '@/router'
import ElementPlus from 'element-plus'
// 导入element-plus组件的全局样式
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import { createPinia } from 'pinia'
import { useAllDataStore } from '@/stores'
/* 引入mock */
import "@/apis/mock.js"
/* 引入apis 管理请求接口 */
import apis from "@/apis/apis.js"
const pinia = createPinia()
const app = createApp(App)
/* 定义全局配置使用 */
app.config.globalProperties.$apis = apis
app.use(ElementPlus)
app.use(pinia)
// localStorage.removeItem("store")
const store = useAllDataStore()
store.addRoutes(router, "refresh")
app.use(router)
/*function isRoute(to) {
let routes = router.getRoutes()
console.log("routes", routes);
let resFil = routes.filter(item =>
/* 相当于return */
item.path === to.path
)
/* let resFil = routes.filter(item => {
相当于一段代码,只有return为true时候才会保留对应的数据
item.path === to.path}
) */
return resFil
}
/*
router.beforeEach((to, from, next) => {
console.log("store.state.token", store.state.token);
if (!store.state.token && to.path !== '/login') {
console.log("to.path1", to.path);
next({ name: 'login' })
}
if (store.state.token && to.path === '/login') {
console.log("to.path2", to.path);
next({ name: 'home' })
}
if (store.state.token && to.path !== '/login') {
console.log("to.path3", to.path);
console.log("isRoute", isRoute(to));
if (isRoute(to).length === 0) {
console.log("to.path3", to.path);
next({ name: '404' })
}
}
console.log("to.path4", to.path);
next()
})
*/
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.mount('#app')
2、App.vue组件引入
<script setup>
</script>
<template>
<div class="app">
<router-view />
</div>
</template>
<style >
#app,
.app {
width: 100%;
height: 100%;
overflow: hidden;
}
</style>
3、创建RootView根路由组件
<template>
<div class="common-layout">
<el-container class="main-container">
<div class="aside-container">
<CommonAside />
</div>
<el-container class="right-container">
<el-header style="height: 40px;">
<CommonHeader />
</el-header>
<el-divider style="margin: 5px 0;" />
<CommonTab />
<el-main>
<router-view />
</el-main>
<el-footer>
<Footer />
</el-footer>
</el-container>
</el-container>
</div>
</template>
<script setup>
import { reactive, toRefs, onMounted, nextTick } from 'vue'
import CommonAside from '@/components/CommonAside.vue';
import CommonHeader from '@/components/CommonHeader.vue';
import CommonTab from '@/components/CommonTab.vue';
import Footer from '@/components/Footer.vue';
</script>
<style lang='less' scoped>
.common-layout,
.main-container,
right-container {
height: 100%;
background-color: #f0f2f5;
.el-main {
padding: 8px;
}
}
</style>
4、依次创建
三个普通组件 CommonAside CommonHeader Footer 以及每个页面的路由组件 router-view
- CommonAside.vue
<template>
<!-- default-active通常与对应的index相关 -->
<el-aside :width="width">
<el-menu text-color="#fff" background-color="#545c64" :collapse="isCollapse" :collapse-transition="false"
:default-active="activeMenu"
class="el-menu-vertical-demo">
<div class="title">
<i class="iconfont icon-quanpingtai"> </i>
<h4 v-show="!isCollapse">许苑后台管理</h4>
</div>
<!-- 没有子菜单 -->
<el-menu-item v-for="item in noChildren" :key="item.path" :index="item.path" @click="handleMenu(item)">
<component :is="item.icon" class="icon" />
<span style="margin-left: 10px">{{ item.label }}</span>
</el-menu-item>
<!-- 有子菜单 -->
<el-sub-menu v-for="item in hasChildren" :key="item.path" :index="item.path">
<template #title>
<component :is="item.icon" class="icon" />
<span style="margin-left: 10px">{{ item.label }}</span>
</template>
<el-menu-item-group>
<el-menu-item v-for="(subItem) in item.children" :index="subItem.path" :key="subItem.path">
<component :is="subItem.icon" class="icon" />
<span>{{ subItem.label }}</span>
</el-menu-item>
</el-menu-item-group>
</el-sub-menu>
</el-menu>
</el-aside>
</template>
<script setup>
import { ref, reactive, toRefs, onMounted, nextTick, computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useAllDataStore } from '@/stores'
const store = useAllDataStore()
const router = useRouter()
const route = useRoute()
// 测试数据,初始化,刚开始可以使用,后续通过配置路由权限获取
/* const list = ref([
{
path: '/home',
name: 'home',
label: '首页',
icon: 'house',
url: 'Home'
},
{
path: '/mall',
name: 'mall',
label: '商品管理',
icon: 'ShoppingBag',
url: 'Mall'
},
{
path: '/user',
name: 'user',
label: '用户管理',
icon: 'user',
url: 'User'
},
{
path: 'other',
label: '其他',
icon: 'location',
children: [
{
path: '/page1',
name: 'page1',
label: '页面1',
icon: 'setting',
url: 'Page1'
},
{
path: '/page2',
name: 'page2',
label: '页面2',
icon: 'setting',
url: 'Page2'
}
]
}
])
*/
const list = computed(() => store.state.menuList)
const noChildren = computed(() => list.value.filter(item => !item.children))
const hasChildren = computed(() => list.value.filter(item => item.children))
const width = computed(() => store.state.isCollapse ? '60px' : '200px')
// 涉及组件之间的传递 => 使用pinia进行各组件之间的传递
const isCollapse = computed(() => store.state.isCollapse)
const activeMenu = computed(() => route.path)
const handleMenu = (item) => {
if (item.children) {
return
}
router.push(item.path)
store.selectMenu(item)
}
</script>
<style lang='less' scoped>
.icon {
width: 18px;
height: 18px;
margin-right: 5px;
}
.el-aside {
background-color: #545c64;
height: 100vh;
.el-menu {
border-right: none;
.title {
display: flex;
align-items: center;
justify-content: center;
}
h4 {
color: #fff;
font-size: 17px;
margin: 20px;
font-weight: 500px;
text-align: center;
}
}
}
</style>
- CommonHeader.vue
<template>
<div class="header">
<div class="l-header">
<el-button size="small" @click="store.state.isCollapse = !store.state.isCollapse">
<el-icon>
<Menu />
</el-icon>
</el-button>
</div>
<div class="r-header">
<el-dropdown>
<span class="el-dropdown-link">
<img src="@/assets/images/xuyuan.jpg" alt="" class="r-header-avatar">
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>个人中心</el-dropdown-item>
<el-dropdown-item @click="handlerLogout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</template>
<script setup>
import { ref, reactive, toRefs, onMounted, nextTick, computed } from 'vue'
import { useAllDataStore } from '@/stores'
import { ArrowRight } from '@element-plus/icons-vue'
import { useRouter } from 'vue-router'
const store = useAllDataStore()
const router = useRouter()
let currentPath = computed(() => {
console.log("store.state.currentMenu", store.state.currentMenu);
return store.state.currentMenu;
})
const handlerLogout = () => {
store.clean()
router.push('/login')
}
</script>
<style lang='less' scoped>
.header {
width: 100%;
height: 100%;
display: flex;
justify-content: space-between;
align-items: center;
.l-header {
display: flex;
justify-content: center;
align-items: center;
.el-button {
margin-right: 15px;
}
}
}
.r-header-avatar {
width: 30px;
height: 30px;
border-radius: 50%;
margin-right: 10px;
}
</style>
<!-- -- 面包屑功能 <el-breadcrumb :separator-icon="ArrowRight">
<el-breadcrumb-item :to=" '/' ">首页</el-breadcrumb-item>
<el-breadcrumb-item v-if="currentPath" :to="currentPath.path" @cl>{{ currentPath.label
}}
</el-breadcrumb-item>
</el-breadcrumb> -->
- Footer.vue
<template>
<div class="footer">
<a href="https://github.com/xuyuan-upward" class="toLearn">
<span> <i class="iconfont icon-github"></i>站长: 许苑向上</span>
</a>
<a href="https://blog.csdn.net/a147775" class="toLearn">
<span> <i class="iconfont icon-bokeyuan"></i>博客: xuyuan-upward</span>
</a>
<a href="https://user.qzone.qq.com/2517115657/main" class="toLearn">
<span> <i class="iconfont icon-shouye"></i>联系方式: 许苑向上</span>
</a>
</div>
</template>
<script setup>
import { ref, reactive, toRefs, onMounted, nextTick } from 'vue'
</script>
<style lang='less' scoped>
.toGithub {
text-decoration: none;
font-size: 14px;
font-weight: bold;
padding: 10px 0;
display: block;
text-align: center;
border-radius: 5px;
transition: all 0.3s ease-in-out;
}
.iconfont {
margin-right: 5px;
}
.footer {
height: 100%;
display: flex;
justify-content: center;
a {
margin-right: 40px;
color: #00000073;
span {
line-height: 60px;
}
}
}
</style>
- MainView.vue路由组件
<template>
<div class="home">
<el-row>
<!-- 左侧 -->
<el-col :span="7">
<div class="l-user">
<el-card style="max-width: 480px" shadow="hover" class="user-info">
<div class="user">
<img src="@/assets/images/xuyuan.jpg" alt=""
style="width: 100px;height: 100px;border-radius: 50%;margin-right: 10px;">
<div class="userInfo">
<h>admin</h>
<p style="margin-top: 20px; color: #999;">超级管理员</p>
</div>
</div>
<el-divider />
<div class="login-info">
<p>上次登录时间:<span>2024-11-18 1:00:00</span></p>
<p style="margin-top: 10px;">上次登录地点:<span>广西</span></p>
</div>
</el-card>
<el-card style="max-width: 480px" shadow="hover" class="user-table">
<el-table :data="tableData" style="width: 100%">
<!-- 遍历val是值 key是键 -->
<el-table-column v-for="(val, key) in tableLabel" :key="key" :prop="key" :label="val">
</el-table-column>
</el-table>
</el-card>
</div>
</el-col>
<!-- 右侧 -->
<el-col :span="17">
<div class="r-echart">
<div class="top">
<el-card v-for="(item) in counterData" :key="item.name"
:body-style="{ padding: '20px', display: 'flex' }" shadow="hover">
<component :is="item.icon" class="icons" :style="{ background: item.color }" />
<div class="detail">
<p class="num">¥{{ item.value }}</p>
<p class="txt">¥{{ item.name }}</p>
</div>
</el-card>
</div>
<div class="bottom">
<!-- 三个图表容器 -->
<div class="echart-top">
<el-card shadow="hover">
<div ref="echart" style="height: 220px;"></div>
</el-card>
</div>
<div class="echart-bottom">
<el-card shadow="hover">
<div ref="userEchart" style="height: 140px"></div>
</el-card>
<el-card shadow="hover">
<div ref="videoEchart" style="height: 140px"></div>
</el-card>
</div>
</div>
</div>
</el-col>
</el-row>
</div>
</template>
<script setup>
import { ref, reactive, toRefs, onMounted, nextTick, getCurrentInstance } from 'vue'
import * as echarts from 'echarts';
//这个tableData是假数据,等会我们使用axios请求mock数据
const { proxy } = getCurrentInstance()
const tableData = ref([])
const counterData = ref([])
//observer 接收观察器实例对象
const observer = ref(null)
//这个是折线图和柱状图 两个图表共用的公共配置
const xOptions = reactive({
// 图例文字颜色
textStyle: {
color: "#333",
},
legend: {},
grid: {
left: "20%",
},
// 提示框
tooltip: {
trigger: "axis",
},
xAxis: {
type: "category", // 类目轴
data: [],
axisLine: {
lineStyle: {
color: "#17b3a3",
},
},
axisLabel: {
interval: 0,
color: "#333",
},
},
yAxis: [
{
type: "value",
axisLine: {
lineStyle: {
color: "#17b3a3",
},
},
},
],
color: ["#2ec7c9", "#b6a2de", "#5ab1ef", "#ffb980", "#d87a80", "#8d98b3"],
series: [],
})
const pieOptions = reactive({
tooltip: {
trigger: "item",
},
legend: {},
color: [
"#0f78f4",
"#dd536b",
"#9462e5",
"#a6a6a6",
"#e1bb22",
"#39c362",
"#3ed1cf",
],
series: []
})
const getTableData = async () => {
const data = await proxy.$apis.getTableData()
console.log("home,tableData获取到的数据:", data);
tableData.value = data.tableData
}
const getCounterData = async () => {
const data = await proxy.$apis.getCounterData()
console.log("home,counterData获取到的数据:", data);
counterData.value = data
}
const getChartData = async () => {
// 获取图标信息 ,解构
const { orderData, userData, videoData } = await proxy.$apis.getChartData()
console.log("home,orderData获取到的数据:", orderData);
//对第一个图表的xAxis和series赋值
xOptions.xAxis.data = orderData.date
xOptions.series = Object.keys(orderData.data[0]).map(val => {
return {
name: val,
data: orderData.data.map(item => item[val]),
type: "line"
}
}
)
//one echarts.init方法初始化ECharts实例,需要传入dom对象
const OneEcharts = echarts.init(proxy.$refs["echart"])
//setOption方法应用配置对象
OneEcharts.setOption(xOptions)
//对第二个图表的xAxis和series赋值
xOptions.xAxis.data = userData.map((item) => item.date)
xOptions.series = [
{
name: "新增用户",
data: userData.map((item) => item.new),
type: "bar",
},
{
name: "活跃用户",
data: userData.map((item) => item.active),
type: "bar",
}
]
//two
const TwoEcharts = echarts.init(proxy.$refs["userEchart"])
TwoEcharts.setOption(xOptions)
//对第三个图表的series赋值
pieOptions.series = [
{
data: videoData,
type: "pie",
},
]
//three
const ThreeEcharts = echarts.init(proxy.$refs["videoEchart"])
ThreeEcharts.setOption(pieOptions);
//ResizeObserver 如果监视的容器大小变化,如果改变会执行传递的回调
observer.value = new ResizeObserver(entries => {
OneEcharts.resize()
TwoEcharts.resize()
ThreeEcharts.resize()
})
//如果这个容器存在
if (proxy.$refs["echart"]) {
//则调用监视器的observe方法,监视这个容器的大小
observer.value.observe(proxy.$refs["echart"]);
}
}
onMounted(() => {
getTableData()
getCounterData()
getChartData()
console.log(proxy);
})
const tableLabel = ref({
name: "课程",
todayBuy: "今日购买",
monthBuy: "本月购买",
totalBuy: "总购买",
})
</script>
<style lang='less' scoped>
.home {
height: 100%;
overflow: hidden;
.l-user {
.user-info {
.user {
display: flex;
align-items: center;
.userInfo {
margin-left: 30px;
}
}
.login-info {
p {
font-size: 14px;
color: #999;
span {
color: #666;
margin-left: 30px;
}
}
}
}
.user-table {
margin-top: 50px;
}
}
.r-echart {
.top {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
.el-card {
width: 30%;
margin-bottom: 10px;
margin-left: 20px;
}
.icons {
width: 50px;
height: 50px;
border-radius: 50%;
margin-right: 20px;
}
.detail {
display: flex;
flex-direction: column;
justify-content: center;
.num {
margin-bottom: 10px;
}
}
}
.bottom {
margin-left: 20px;
.echart-top {
margin-bottom: 20px;
}
.echart-bottom {
display: flex;
justify-content: space-between;
align-items: center;
.el-card {
width: 48%;
}
}
}
}
}
</style>
- UserView.vue组件
<template>
<div class="user">
<div class="user-head">
<el-button type="primary" @click="handleAdd">新增</el-button>
<el-form :inline="true" :model="formData">
<el-form-item label="请输入">
<el-input placeholder="请输入姓名" v-model="formData.keyWord"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handlerSearch">搜索</el-button>
</el-form-item>
</el-form>
</div>
<div class="user-table">
<el-dialog v-model="dialogVisible" :title="action == 'add' ? '新增用户' : '编辑用户'" width="35%"
:before-close="handleClose">
<!--需要注意的是设置了:inline="true",
会对el-select的样式造成影响,我们通过给他设置一个class=select-
在css进行处理-->
<el-form :inline="true" :model="formUser" :rules="rules" ref="userForm">
<el-row>
<el-col :span="12">
<el-form-item label="姓名" prop="name">
<el-input v-model="formUser.name" placeholder="请输入姓名" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="年龄" prop="age">
<el-input v-model.number="formUser.age" placeholder="请输入年龄" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<!-- 使用:inline="true"会对select造成影响,此时长度应该设置最大 -->
<el-form-item label="性别" prop="sex" style="width: 80%;">
<el-select v-model="formUser.sex" placeholder="请选择" class="select-clean">
<el-option label="男" :value="1" />
<!-- 注意这里的 :value 表示绑定一个表达式即所谓的"1" 其实代表的是number类型1 -->
<el-option label="女" :value="0" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="出生日期" prop="birth">
<el-date-picker v-model="formUser.birth" type="date" placeholder="请输入"
style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-form-item label="地址" prop="addr">
<el-input v-model="formUser.addr" placeholder="请输入地址" />
</el-form-item>
</el-row>
<el-row style="justify-content: flex-end">
<el-form-item>
<el-button type="primary" @click="handleCancel">取消</el-button>
<el-button type="primary" @click="onSubmit">确定</el-button>
</el-form-item>
</el-row>
</el-form>
</el-dialog>
<el-table :data="tableData" style="width: 100%">
<el-table-column v-for="item in tableLabel" :key="item.prop" :prop="item.prop" :label="item.label"
:width="item.width" />
<el-table-column fixed="right" label="Operations" min-width="120">
<template #="scoped">
<el-button type="primary" size="small" @click="onEdit(scoped.row)">
编辑
</el-button>
<el-button type="danger" size="small" @click="onDelete(scoped.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination layout="prev, pager, next" :total="config.total" @current-change="handlerChangePage"
class="page" />
</div>
</div>
</template>
<script setup>
import { ref, reactive, toRefs, onMounted, nextTick, getCurrentInstance } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
const { proxy } = getCurrentInstance()
const tableData = ref([])
const tableLabel = reactive([
{
prop: "name",
label: "姓名",
},
{
prop: "age",
label: "年龄",
},
{
prop: "sex",
label: "性别",
},
{
prop: "birth",
label: "出生日期",
width: 200,
},
{
prop: "addr",
label: "地址",
width: 400,
},
])
const config = reactive({
name: "",
total: 0,
}
)
const formData = reactive({
keyWord: ""
})
const dialogVisible = ref(false)
const action = ref("add")
const formUser = ref({
sex: 0,
})
//表单校验规则
const rules = reactive({
name: [{ required: true, message: "姓名是必填项", trigger: "blur" }],
age: [
{ required: true, message: "年龄是必填项", trigger: "blur" },
{ type: "number", message: "年龄必须是数字" },
],
sex: [{ required: true, message: "性别是必选项", trigger: "change" }],
birth: [{ required: true, message: "出生日期是必选项" }],
addr: [{ required: true, message: '地址是必填项' }]
})
const handlerSearch = () => {
console.log("搜索", formData.keyWord);
config.name = formData.keyWord
// console.log("搜索", searchText);
getUserData(config)
}
const getUserData = async (query) => {
const data = await proxy.$apis.getUserData(query)
console.log("UserView的数据", data);
config.total = data.count
tableData.value = data.list.map((item) => {
return {
...item,
sex: item.sex === 1 ? '女' : '男'
}
})
}
onMounted(() => {
getUserData()
})
const handlerChangePage = (value) => {
console.log("当前页码", value);
config.page = value
getUserData(config)
}
const onDelete = (row) => {
console.log("删除", row);
ElMessageBox.confirm(
'你确定要删除吗?',
'删除提示',
{
confirmButtonText: '确定删除',
cancelButtonText: '取消',
type: 'danger ',
}
)
.then(() => {
proxy.$apis.deleteUser({ id: row.id })
ElMessage({
type: 'success',
message: '删除成功',
})
getUserData()
})
.catch(() => {
ElMessage({
type: 'info',
message: '取消删除',
})
})
}
const onEdit = (row) => {
console.log("编辑", row);
action.value = "edit"
dialogVisible.value = true
/* nextTick 确保在 DOM 更新完成之后再执行回调函数
也就是编辑表单
*/
nextTick(() => {
formUser.value = {
...row,
}
}
)
/* formUser.value = {
...row,
} */
}
//这个方法之前定义过
const handleAdd = () => {
action.value = "add"
//打开对话窗
dialogVisible.value = true
}
//对话框右上角的关闭事件
const handleClose = () => {
//获取到表单dom,执行resetFields重置表单
//关闭对话框
dialogVisible.value = false
proxy.$refs["userForm"].resetFields()
}
//对话框右下角的取消事件
const handleCancel = () => {
dialogVisible.value = false
proxy.$refs["userForm"].resetFields()
}
//格式化日期,格式化为:1997-01-02这种
const timeFormat = (time) => {
var time = new Date(time);
var year = time.getFullYear();
var month = time.getMonth() + 1;
var date = time.getDate();
function add(m) {
return m < 10 ? "0" + m : m;
}
return year + "-" + add(month) + "-" + add(date);
}
const onSubmit = async () => {
// 获取表单数据
console.log("添加的xxx", formUser.value);
// 先进行校验
proxy.$refs["userForm"].validate(async (validate) => {
if (validate) {
let res = null;
//这里无论是新增或者是编辑,我们都要对这个日期进行一个格式化
//如果不是1997-01-02这种格式,使用timeFormat方法进行格式化
formUser.birth = /^\d{4}-\d{2}-\d{2}$/.test(formUser.birth)
? formUser.birth
: timeFormat(formUser.birth)
// 提交表单时候,还需要判断是add or edit
if (action.value === "add") {
res = await proxy.$apis.addUser(formUser.value)
} else {
res = await proxy.$apis.editUser(formUser.value)
}
if (res) {
ElMessage({
type: 'success',
message: action.value === "add" ? '添加成功' : "编辑成功",
})
dialogVisible.value = false
proxy.$refs["userForm"].resetFields()
// 刷新页面数据
getUserData()
}
}
else {
ElMessage({
type: 'error',
message: "请输入正确内容",
})
}
})
// 校验通过,执行添加操作
proxy.$apis.addUser(formUser.value)
}
</script>
<style lang='less' scoped>
.user {
height: 100%;
.user-head {
display: flex;
justify-content: space-between;
}
.user-table {
height: 540px;
position: relative;
.page {
position: absolute;
bottom: 50px;
right: 50px;
}
}
}
</style>
5、每个组件之间需要共享配置导入pinia配置进行信息共享和传递。
pinia.js
import { defineStore } from 'pinia'
import { ref, watch } from 'vue'
function initData() {
return {
isCollapse: false,
tags: [
{
path: '/home',
name: 'home',
label: '首页',
icon: 'hone'
}
],
currentMenu: null,
/* 展示菜单列表的数组 */
menuList: [],
token: null,
routerList: [],
}
}
export const useAllDataStore = defineStore('allData', () => {
// 全部数据的获取和修改
const state = ref(initData())
// 进行数据持久化
watch(state, newObj => {
if (!newObj.token) return
localStorage.setItem('store', JSON.stringify(newObj))
}, {
deep: true,
})
function selectMenu(val) {
if (val.name === 'home') {
state.value.currentMenu = null
}
else {
state.value.currentMenu = val
let index = state.value.tags.findIndex(item => item.name === val.name)
index === -1 ? state.value.tags.push(val) : ""
}
}
function deleteMenu(tag) {
let index = state.value.tags.findIndex(item => item.name == tag.name)
// 将当前tags切除
state.value.tags.splice(index, 1);
}
function updateMenuList(val) {
// 将当前tags切除
state.value.menuList = val;
}
function clean() {
// 将所有路由移除
state.value.routerList.forEach(item => {
if (item) item();
state.value = initData();
// 删除本地的缓存
localStorage.removeItem('store')
})
}
function addRoutes(router, type) {
// 刷新页面时候
if (type === 'refresh') {
if (JSON.parse(localStorage.getItem('store'))) {
state.value = JSON.parse(localStorage.getItem('store'))
//
state.value.routerList = []
}
else {
return;
}
}
// 将当前tags切除
const menu = state.value.menuList;
console.log("menu", menu);
/* 执行该代码后 import.meta.glob可能返回的是这样的对象
'@/views/Home.vue': () => import('@/views/Home.vue'),
'@/views/About.vue': () => import('@/views/About.vue'),
'@/views/User/Profile.vue': () => import('@/views/User/Profile.vue')
*/
const module = import.meta.glob('../views/*.vue')
console.log("module", module);
const routeArr = []
menu.forEach(item => {
if (item.children) {
item.children.forEach(child => {
let url = `../views/${child.url}.vue`
console.log("url", url);
child.component = module[url]
console.log("child.component", child.component);
routeArr.push(...item.children)
})
}
else {
let url = `../views/${item.url}.vue`
console.log("url", url);
item.component = module[url]
console.log("item.component", item.component);
routeArr.push(item)
}
routeArr.forEach(item => {
state.value.routerList.push(router.addRoute("main", item));
})
})
console.log("state.value.routerList", state.value.routerList);
console.log("state.value.routeArr", routeArr);
}
return {
/* 其实是直接返回的是state.value */
state,
selectMenu,
deleteMenu,
updateMenuList,
addRoutes,
clean,
}
})
5、进行对应各个环境的配置环境设置
config.js
// 用于获取对应的环境变量
const env = process.env.NODE_ENV || "prod";
const EnvConfig = {
development: {
baseURL: "/api",
mockApi: "https://mock.apipark.cn/m1/4068509-0-default/api"
},
test: {
baseURL: "//test.xuyuan.com/api",
mockApi: "https://mock.apipark.cn/m1/4068509-0-default/api"
},
prod: {
baseURL: "//xuyuan.com/api",
mockApi: "https://mock.apipark.cn/m1/4068509-0-default/api"
},
}
export default {
env,
/* 将其重新解构成一个对象,并将其合并到默认配置中 */
...EnvConfig[env],
isMock:false,
};