vue - 实战1 - adminDemo
一些设置
配置 @ 为src 地址
vite.config.ts
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueDevTools(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
})
路由 src\router\index.ts
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
name: 'main',
redirect: '/home',
component: () => import('@/views/Main.vue'),
children: [
{
path: '/home',
name: 'home',
component: () => import('@/views/Home.vue'),
},
{
path: '/user',
name: 'user',
component: () => import('@/views/Users/User.vue'),
},
],
},
]
const router = createRouter({
history: createWebHistory(),
routes,
})
export default router
允许使用any
import { globalIgnores } from 'eslint/config'
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
import pluginVue from 'eslint-plugin-vue'
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
// To allow more languages other than `ts` in `.vue` files, uncomment the following lines:
// import { configureVueProject } from '@vue/eslint-config-typescript'
// configureVueProject({ scriptLangs: ['ts', 'tsx'] })
// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup
export default defineConfigWithVueTs(
{
name: 'app/files-to-lint',
files: ['**/*.{ts,mts,tsx,vue}'],
},
globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
pluginVue.configs['flat/essential'],
vueTsConfigs.recommended,
skipFormatting,
{
rules: {
'@typescript-eslint/no-explicit-any': 'off', // 禁用该规则,允许使用 any
},
},
)
侧边栏
menu 静态数据加载
<template>
<el-menu active-text-color="#ffd04b" background-color="#545c64" class="el-aside" default-active="2"
text-color="#fff" @open="handleOpen" @close="handleClose" :collapse-transition="false">
<h3>通用管理界面</h3>
<template v-for="item in list" :key="item.path">
<el-sub-menu :index="item.path" v-if="item.children && item.children.length > 0">
<template #title>
<el-icon>
<component :is="item.icon" />
</el-icon>
<span>{{ item.label }}</span>
</template>
<el-menu-item v-for="children in item.children" :key="children.path" :index="children.path">
<el-icon>
<component :is="children.icon" />
</el-icon>
{{ children.label }}
</el-menu-item>
</el-sub-menu>
<el-menu-item v-else :index="item.path">
<el-icon>
<component :is="item.icon" />
</el-icon>
<span>{{ item.label }}</span>
</el-menu-item>
</template>
</el-menu>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const list = ref([
{
path: '/home',
name: 'home',
label: '首页',
icon: 'house',
url: 'Home'
},
{
path: '/mall',
name: 'mall',
label: '商品管理',
icon: 'video-play',
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 handleOpen = (key: string, keyPath: string[]) => {
console.log(key, keyPath)
}
const handleClose = (key: string, keyPath: string[]) => {
console.log(key, keyPath)
}
</script>
<style scoped>
h3 {
color: #fff;
text-align: center;
}
.el-aside {
height: 100vh;
border: none;
/* 移除边框 */
}
</style>
顶部Header
CommonHeader.vue
<template>
<div class="header">
<div class="l-content">
<el-button size="small">
<el-icon>
<component class="icons" :is="'menu'"></component>
</el-icon>
</el-button>
<el-breadcrumb separator="/" class="breadcrumb">
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
</el-breadcrumb>
</div>
<div class="r-content">
<el-dropdown>
<span class="el-dropdown-link">
<img class="user" :src="getImageUrl" alt="">
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>个人中心</el-dropdown-item>
<el-dropdown-item>退出</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</template>
<script setup lang="ts">
const getImageUrl = new URL("https://img2.baidu.com/it/u=2318884743,3754999155&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500").href;
</script>
<style scoped>
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
height: 50px;
}
.icons {
width: 20px;
height: 20px;
}
.l-content {
display: flex;
align-items: center;
.el-button {
margin-right: 20px;
}
}
.r-content {
.user {
height: 20px;
width: 20px;
border-radius: 50%;
}
}
:deep(.breadcrumb span) {
color: #fff !important;
cursor: pointer !important;
}
</style>
mock 的使用
npm install mockjs -D
第1步,Home.vue 有个接口
axios.get("/api/user").then(function (response) {
console.log(response.data);
});
第2步,创建
src\api\mockData\userMock.js
export default {
data: () => ({
id: '@id',
name: '@name',
age: '@integer(20,50)',
}),
}
src\api\mock.js
import Mock from 'mockjs'
import userMock from './mockData/userMock.js'
Mock.mock('/api/user', 'get', {
code: 200,
data: userMock.data(),
})
第3步子,main.ts 导入
import '@/api/mock'
axios 二次封装(拦截器)
第一步,创建
import axios, { type AxiosRequestConfig } from 'axios'
import { ElMessage } from 'element-plus'
// 创建axios实例
const service = axios.create({
timeout: 10000, // 请求超时时间
})
// 添加请求拦截器
service.interceptors.request.use(
function (config) {
// 在发送请求之前做些什么
return config
},
function (error) {
// 对请求错误做些什么
return Promise.reject(error)
},
)
// 添加响应拦截器
service.interceptors.response.use(
(res) => {
const { status, data, statusText } = res
if (status === 200) {
return data
} else {
const NETWORK_ERROR = '网络请求出错'
ElMessage.error(statusText || NETWORK_ERROR)
return Promise.reject(statusText || NETWORK_ERROR)
}
},
(error) => {
// 处理网络错误、超时等情况
if (error.response) {
// 服务器返回了错误状态码
ElMessage.error(`请求失败: ${error.response.status}`)
} else if (error.request) {
// 请求已发送但没有收到响应
ElMessage.error('网络连接失败')
} else {
// 设置请求时发生错误
ElMessage.error('请求配置错误')
}
return Promise.reject(error)
},
)
function request(options: AxiosRequestConfig) {
options.method = options.method || 'get'
return service(options)
}
export default request
第二步,创建API和导入 main.ts
src\api\homeApi.ts
import Mock from 'mockjs'
import userMock from './mockData/userMock.js'
Mock.mock('/api/home/getTableData', 'get', {
code: 200,
data: userMock.getDataTavle(),
})
// 添加一个默认导出,确保模块被正确加载
export default Mock
main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import '@/assets/sass/index.scss'
import App from './App.vue'
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 homeApi from '@/api/homeApi'
import '@/api/mock'
const app = createApp(App)
app.config.globalProperties.$homeApi = homeApi
app.use(createPinia())
app.use(router)
app.use(ElementPlus) //将 ElementPlus 插件注册到 Vue 应用中
app.mount('#app')
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
第三步,调用
Home.vue
<template setup>
<div class="content">
<div class="left">
<el-card>
<div class="userInfo">
<div class="userInfo_icon">
<img :src="getImageUrl" class="userIcon">
</div>
<div class="userInfo_info">
<h3>Admin</h3>
<p class="grey_text">超级管理员</p>
</div>
</div>
<template #footer>
<div class="logininfo">
<div>
<p class="grey_text">上次登录时间:<span>2023-08-01 10:00:00</span></p>
</div>
<div>
<p class="grey_text">上次登录地点:<span>中国</span></p>
</div>
</div>
</template>
</el-card>
<el-card class="card_table">
<el-table :data="tableData">
<el-table-column v-for="(value, key) in tableLabel" :key="key" :prop="key" :label="value" />
</el-table>
</el-card>
</div>
<div class="right">
</div>
</div>
</template>
<script setup lang="ts">
import type { TableItem } from '@/interface/TableItem';
import { getCurrentInstance, ref, onMounted } from 'vue'
const getImageUrl = new URL("https://img2.baidu.com/it/u=2318884743,3754999155&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500").href;
const instance = getCurrentInstance();
if (!instance) {
throw new Error('getCurrentInstance() returned null');
}
const { proxy } = instance;
// 定义表格数据类型接口
// 表格数据
const tableData = ref<TableItem[]>([]);
// 表格列标签
const tableLabel = ref({
name: "课程",
todayBuy: "今日购买",
monthBuy: "本月购买",
totalBuy: "总购买",
});
async function getTableData() {
try {
const res = await proxy!.$homeApi.getTableData();
console.log(res);
if (Array.isArray(res.data)) {
tableData.value = res.data;
} else if (res.data) {
// 如果是对象,将其包装在数组中
tableData.value = []
tableData.value = [...res.data];
} else {
// 如果没有数据,确保是空数组
tableData.value = [];
}
} catch (error) {
console.error('获取表格数据失败:', error);
tableData.value = [];
}
}
onMounted(() => {
getTableData();
});
</script>
<style scoped>
.left {
width: 30%;
background-color: bisque;
.userIcon {
width: 100px;
height: 100px;
background-image: url("https://img2.baidu.com/it/u=2318884743,3754999155&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500");
background-size: cover;
background-position: center;
border-radius: 50%;
}
.userInfo {
display: flex;
.userInfo_icon {
margin-right: 20px;
}
.userInfo_info {
display: flex;
flex-direction: column;
justify-content: center;
}
}
.logininfo {
span {
color: #000;
}
}
.card_table {
margin-top: 20px;
}
}
.grey_text {
color: grey;
}
.right {
width: 70%;
background-color: aqua;
}
:deep(.el-card__footer) {
width: 90%;
margin: 0 auto;
}
</style>
如果 proxy!.$homeApi
报 null
(一般TS才会报错)
/// <reference types="vite/client" />
import type homeApi from './src/api/homeApi'
declare module 'vue' {
interface ComponentCustomProperties {
$homeApi: typeof homeApi
}
}
数据交互使用配置文件config
第1步,创建config文件
src\config\index.ts
const env = import.meta.env.MODE || 'prod'
interface EnvConfigType {
[key: string]: {
baseURL: string
}
}
const EnvConfig: EnvConfigType = {
development: {
baseURL: 'http://localhost:8080',
},
prod: {
baseURL: 'https://api.github.com',
},
}
// 获取当前环境配置,默认使用prod
const currentEnvConfig = EnvConfig[env] || EnvConfig.prod
export default {
env,
baseURL: currentEnvConfig.baseURL,
mock: false,
}
第2步,修改request
src\api\request.ts
import axios, { type AxiosRequestConfig } from 'axios'
import { ElMessage } from 'element-plus'
import config from '../config'
// 创建axios实例
const service = axios.create({
//timeout: 30000, // 请求超时时间
baseURL: config.baseURL,
})
// 添加请求拦截器
service.interceptors.request.use(
function (config) {
// 在发送请求之前做些什么
return config
},
function (error) {
// 对请求错误做些什么
return Promise.reject(error)
},
)
// 添加响应拦截器
service.interceptors.response.use(
(res) => {
const { status, data, statusText } = res
if (status === 200) {
return data
} else {
const NETWORK_ERROR = '网络请求出错'
ElMessage.error(statusText || NETWORK_ERROR)
return Promise.reject(statusText || NETWORK_ERROR)
}
},
(error) => {
// 处理网络错误、超时等情况
if (error.response) {
// 服务器返回了错误状态码
ElMessage.error(`请求失败: ${error.response.status}`)
} else if (error.request) {
// 请求已发送但没有收到响应
ElMessage.error('网络连接失败')
} else {
// 设置请求时发生错误
ElMessage.error('请求配置错误')
}
return Promise.reject(error)
},
)
function request(options: AxiosRequestConfig) {
options.method = options.method || 'get'
if (options.method.toLowerCase() === 'get') {
options.params = options.data
}
// 在mock模式下,不设置baseURL,让Mock.js拦截请求
if (config.env !== 'prod' && config.mock) {
service.defaults.baseURL = ''
} else {
service.defaults.baseURL = config.baseURL
}
return service(options)
}
export default request
引入echarts(6.0有变化)
安装
npm install echarts -S
现在版本6.0,先试着引入一个DEMO
<template setup>
<div class="content">
<div class="left">
<el-card>
<div class="userInfo">
<div class="userInfo_icon">
<img :src="getImageUrl" class="userIcon">
</div>
<div class="userInfo_info">
<h3>Admin</h3>
<p class="grey_text">超级管理员</p>
</div>
</div>
<template #footer>
<div class="logininfo">
<div>
<p class="grey_text">上次登录时间:<span>2023-08-01 10:00:00</span></p>
</div>
<div>
<p class="grey_text">上次登录地点:<span>中国</span></p>
</div>
</div>
</template>
</el-card>
<el-card class="card_table">
<el-table :data="tableData">
<el-table-column v-for="(value, key) in tableLabel" :key="key" :prop="key" :label="value" />
</el-table>
</el-card>
</div>
<div class="right">
<el-card>
<div class="card_count">
<div class="card_count_item" v-for="value in tableCountData" :key="value.name">
<div class="icons_box" :style="{ backgroundColor: value.color }">
<component :is="value.icon" class="icons">
</component>
</div>
<div class="count_info">
<p class="count_text">¥{{ value.value }}</p>
<p class="count_name">{{ value.name }}</p>
</div>
</div>
</div>
</el-card>
<el-card>
<div id="chart" style=" height: 400px;"></div>
</el-card>
</div>
</div>
</template>
<script setup lang="ts">
import type { TableItem } from '@/interface/TableItem';
import { getCurrentInstance, ref, onMounted } from 'vue'
// 按需引入 ECharts 核心模块
import * as echarts from 'echarts/core';
// 引入柱状图类型
import { BarChart } from 'echarts/charts';
// 引入必要的组件(包括缺失的 xAxis、yAxis 和 Grid 组件)
import { TitleComponent, TooltipComponent, GridComponent } from 'echarts/components';
// 引入渲染器
import { CanvasRenderer } from 'echarts/renderers';
// 注册所有需要使用的图表、组件和渲染器
echarts.use([BarChart, TitleComponent, TooltipComponent, GridComponent, CanvasRenderer]);
const getImageUrl = new URL("https://img2.baidu.com/it/u=2318884743,3754999155&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500").href;
const instance = getCurrentInstance();
if (!instance) {
throw new Error('getCurrentInstance() returned null');
}
const { proxy } = instance;
// 表格数据
const tableData = ref<TableItem[]>([]);
const tableCountData = ref();
const chartData = ref();
// 表格列标签
const tableLabel = ref({
name: "课程",
todayBuy: "今日购买",
monthBuy: "本月购买",
totalBuy: "总购买",
});
async function getTableData() {
try {
const res = await proxy!.$homeApi.getTableData();
// console.log(res);
if (Array.isArray(res.data)) {
tableData.value = res.data;
} else if (res.data) {
// 如果是对象,将其包装在数组中
tableData.value = []
tableData.value = [...res.data];
} else {
// 如果没有数据,确保是空数组
tableData.value = [];
}
} catch (error) {
console.error('获取表格数据失败:', error);
tableData.value = [];
}
}
async function getCountData() {
try {
const res = await proxy!.$homeApi.getCountData();
tableCountData.value = res.data;
} catch (error) {
console.error('获取Count数据失败:', error);
}
}
async function getChartData() {
try {
const res = await proxy!.$homeApi.getChartData();
chartData.value = res.data;
console.log(res);
} catch (error) {
console.error('获取Chart数据失败:', error);
}
}
onMounted(() => {
getTableData();
getCountData();
getChartData();
// 在组件挂载后初始化 ECharts
const myChart = echarts.init(document.getElementById('chart'));
// 绘制图表的配置项和数据
const option = {
title: {
text: 'ECharts 入门示例'
},
tooltip: {},
xAxis: {
type: 'category',
data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']
},
yAxis: {
type: 'value'
},
series: [
{
name: '销量',
type: 'bar',
data: [5, 20, 36, 10, 10, 20]
}
]
};
myChart.setOption(option);
});
</script>
<style>
...
</style>
下面使用折线
<template setup>
<div class="content">
<div class="left">
<el-card>
<div class="userInfo">
<div class="userInfo_icon">
<img :src="getImageUrl" class="userIcon">
</div>
<div class="userInfo_info">
<h3>Admin</h3>
<p class="grey_text">超级管理员</p>
</div>
</div>
<template #footer>
<div class="logininfo">
<div>
<p class="grey_text">上次登录时间:<span>2023-08-01 10:00:00</span></p>
</div>
<div>
<p class="grey_text">上次登录地点:<span>中国</span></p>
</div>
</div>
</template>
</el-card>
<el-card class="card_table">
<el-table :data="tableData">
<el-table-column v-for="(value, key) in tableLabel" :key="key" :prop="key" :label="value" />
</el-table>
</el-card>
</div>
<div class="right">
<el-card>
<div class="card_count">
<div class="card_count_item" v-for="value in tableCountData" :key="value.name">
<div class="icons_box" :style="{ backgroundColor: value.color }">
<component :is="value.icon" class="icons">
</component>
</div>
<div class="count_info">
<p class="count_text">¥{{ value.value }}</p>
<p class="count_name">{{ value.name }}</p>
</div>
</div>
</div>
</el-card>
<el-card>
<div id="chart" style=" height: 400px;"></div>
</el-card>
</div>
</div>
</template>
<script setup lang="ts">
import type { TableItem } from '@/interface/TableItem';
import { getCurrentInstance, ref, onMounted, onUnmounted } from 'vue'
// 按需引入 ECharts 核心模块
import * as echarts from 'echarts/core';
// 引入柱状图类型
import { BarChart, LineChart } from 'echarts/charts';
// 引入必要的组件(包括缺失的 xAxis、yAxis 和 Grid 组件)
import { TitleComponent, TooltipComponent, GridComponent, ToolboxComponent, LegendComponent } from 'echarts/components';
// 引入渲染器
import { CanvasRenderer } from 'echarts/renderers';
// 注册所有需要使用的图表、组件和渲染器
echarts.use([BarChart, LineChart, TitleComponent, TooltipComponent, ToolboxComponent, LegendComponent, GridComponent, CanvasRenderer]);
const getImageUrl = new URL("https://img2.baidu.com/it/u=2318884743,3754999155&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500").href;
const instance = getCurrentInstance();
if (!instance) {
throw new Error('getCurrentInstance() returned null');
}
const { proxy } = instance;
// 表格数据
const tableData = ref<TableItem[]>([]);
const tableCountData = ref();
const chartData = ref();
// 表格列标签
const tableLabel = ref({
name: "课程",
todayBuy: "今日购买",
monthBuy: "本月购买",
totalBuy: "总购买",
});
async function getTableData() {
try {
const res = await proxy!.$homeApi.getTableData();
// console.log(res);
if (Array.isArray(res.data)) {
tableData.value = res.data;
} else if (res.data) {
// 如果是对象,将其包装在数组中
tableData.value = []
tableData.value = [...res.data];
} else {
// 如果没有数据,确保是空数组
tableData.value = [];
}
} catch (error) {
console.error('获取表格数据失败:', error);
tableData.value = [];
}
}
async function getCountData() {
try {
const res = await proxy!.$homeApi.getCountData();
tableCountData.value = res.data;
} catch (error) {
console.error('获取Count数据失败:', error);
}
}
interface orderData {
date: string[];
data: { [brand: string]: number }[];
}
let orderDataTemp: orderData = {
date: [],
data: []
};
interface SeriesData {
name: string;
type: 'line';
stack: 'Total';
data: number[];
}
async function getChartData() {
try {
const res = await proxy!.$homeApi.getChartData();
chartData.value = res.data;
console.log('orderData', res.data.data.orderData);
const { orderData, userData, videoData } = res.data.data;
orderDataTemp = orderData;
const titlekeys = orderData.data.map((item: string) => Object.keys(item));
const titles: string[] = Array.from(new Set(titlekeys.flat()));
console.log('titles', titles);
const serieses: SeriesData[] = [];
titles.forEach((title: string) => {
orderDataTemp.data.forEach((item) => {
const find = serieses.find((x: SeriesData) => x.name === title);
if (find) {
find.data.push(item[title]);
} else {
serieses.push({
name: title,
type: 'line',
stack: 'Total',
data: [item[title]],
});
}
});
})
// 在组件挂载后初始化 ECharts
myChart = echarts.init(document.getElementById('chart'));
// 绘制图表的配置项和数据
const option = {
title: {
// text: 'Stacked Line'
},
tooltip: {
trigger: 'axis'
},
legend: {
data: titles,
top: 'top'
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
// 替换containLabel为outerBounds配置
outerBounds: {
top: 50,
bottom: 30
}
},
toolbox: {
feature: {
// saveAsImage: {}
}
},
xAxis: {
type: 'category',
boundaryGap: false,
data: orderDataTemp.date
},
yAxis: {
type: 'value'
},
series: serieses
};
myChart.setOption(option);
// 添加窗口大小变化时的响应式处理
const handleResize = () => {
if (myChart) {
myChart.resize();
}
};
// 使用passive选项添加事件监听器,提高滚动性能
window.addEventListener('resize', handleResize, { passive: true });
} catch (error) {
console.error('获取Chart数据失败:', error);
}
}
let myChart: echarts.ECharts | null = null;
onMounted(() => {
getTableData();
getCountData();
getChartData();
});
onUnmounted(() => {
if (myChart) {
myChart.dispose();
myChart = null;
}
// 移除事件监听器
window.removeEventListener('resize', () => { });
});
</script>
遇到的问题
Home.vue:174 [Violation]Added non-passive event listener to a scroll-blocking 'mousewheel' event. Consider marking event handler as 'passive' to make the page more responsive. See https://www.chromestatus.com/feature/5745543795965952
Home.vue:174 [Violation]Added non-passive event listener to a scroll-blocking 'wheel' event. Consider marking event handler as 'passive' to make the page more responsive. See https://www.chromestatus.com/feature/5745543795965952
这个警告是由 ECharts 内部实现引起的,无法直接控制。但可以通过以下方式处理:
方案一:忽略警告(推荐)
这是一个无害的性能警告,不影响功能。如果项目中没有严格的性能要求,可以选择忽略。
方案二:使用第三方库修复
可以使用 default-passive-events
库来全局修复此类问题:
npm install default-passive-events
然后在 main.ts
中引入:
import 'default-passive-events';
方案三:等待 ECharts 官方修复
关注 ECharts 的更新,等待官方解决此问题。
表格和分页
创建mockjs
src\api\mockData\userMock.js
import Mock from 'mockjs'
// get请求从config.url获取参数,post从config.body中获取参数
function param2obj(url) {
const search = url.split('?')[1]
if (!search) {
return {}
}
return JSON.parse(
'{"' +
decodeURIComponent(search).replace(/"/g, '\\"').replace(/&/g, '","').replace(/=/g, '":"') +
'"}',
)
}
let List = []
const count = 200
// 模拟200条用户数据
for (let i = 0; i < count; i++) {
List.push(
Mock.mock({
id: Mock.Random.guid(),
name: Mock.Random.cname(),
addr: Mock.mock('@county(true)'),
'age|18-60': 1,
birth: Mock.Random.date(),
sex: Mock.Random.integer(0, 1),
}),
)
}
export default {
/**
* 获取列表
* 要带参数 name, page, limit; name可以不填,page,limit有默认值。
* @param name, page, limit
* @return {{code: number, count: number, data: *[]}}
*/
getUserList: (config) => {
const { name, page = 1, limit = 10 } = param2obj(config?.url || '')
const mockList = List.filter((user) => {
// 如果name存在会,根据name筛选数据
if (name && user.name.indexOf(name) === -1) return false
return true
})
// 分页
const pageList = mockList.filter(
(item, index) => index < limit * page && index >= limit * (page - 1),
)
return {
code: 200,
data: {
list: pageList,
count: mockList.length, // 数据总条数需要返回
},
}
},
}
带参数的API
src\api\ApiService\userApi.ts
import request from '../request'
export default {
getUserData(config: object) {
return request({
url: '/api/user/getUserData',
method: 'get',
data: config,
})
},
}
src\api\mock.js
import Mock from 'mockjs'
import homeMock from './mockData/homeMock.js'
import userMock from './mockData/userMock.js'
Mock.mock('/api/home/getTableData', 'get', {
code: 200,
data: homeMock.getDataTavle(),
})
Mock.mock('/api/home/getCountData', 'get', {
code: 200,
data: homeMock.getCountData(),
})
Mock.mock('/api/home/getChartData', 'get', {
code: 200,
data: homeMock.getChartData(),
})
Mock.mock(/\/api\/user\/getUserData.*$/, 'get', (options) => {
return {
code: 200,
result: userMock.getUserList(options),
}
})
// 添加一个默认导出,确保模块被正确加载
export default Mock
User.vue
<template>
<div class="search">
<el-button type="primary">新增</el-button>
<el-form ref="formRef" :inline="true" :model="searchForm" label-width="auto" class="demo-ruleForm">
<el-form-item label="请输入">
<el-input placeholder="请输入用户名" v-model="searchForm.name" type="text" autocomplete="off" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm(formRef)">搜索</el-button>
</el-form-item>
</el-form>
</div>
<div class="list">
<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 ? item.width : ''" />
<el-table-column fixed="right" label="操作" min-width="120">
<template #default>
<el-button type="primary" size="small" @click="handleClick">
编辑
</el-button>
<el-button type="danger" size="small">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination">
<el-pagination background layout="prev, pager, next" size="small" :total="userConfig.total"
@current-change="handleCurrentChange" />
</div>
</div>
</template>
<script setup lang="ts">
import { getCurrentInstance, onMounted, reactive, ref } from 'vue'
import type { FormInstance } from 'element-plus'
const instance = getCurrentInstance();
if (!instance) {
throw new Error('getCurrentInstance() returned null');
}
const { proxy } = instance;
const formRef = ref<FormInstance>()
const searchForm = reactive({
name: '',
})
const userConfig = reactive({
name: '',
total: 0,
page: 1
})
const submitForm = (formEl: FormInstance | undefined) => {
console.log('formEl', formEl)
console.log('searchForm', searchForm)
userConfig.name = searchForm.name
getUserData(userConfig);
}
// #region 表格数据
const handleClick = () => {
console.log('click')
}
const tableLabel = ref([{
prop: 'name',
label: '姓名',
}, {
prop: 'age',
label: '年龄',
}, {
prop: 'labelSex', // 性别根据数字显示文本
label: '性别',
}, {
prop: 'birth',
label: '出生日期',
}, {
prop: 'addr',
label: '地址',
width: 300,
}])
const tableData = ref<{
name: string;
age: number;
sex: number;
labelSex: string;
birth: string;
addr: string;
}[]>([])
async function getUserData(searchForm?: { name: string } | undefined) {
try {
const res = await proxy!.$userApi.getUserData(searchForm);
console.log(res);
if (res.code === 200) {
tableData.value = res.result.data.list;
tableData.value.forEach(item => {
item.labelSex = item.sex === 1 ? '男' : '女';
})
}
if (res.code === 200) {
userConfig.total = res.result.data.count;
}
} catch (error) {
console.error('获取表格数据失败:', error);
}
}
const handleCurrentChange = (val: number) => {
console.log(`current page: ${val}`)
userConfig.page = val
getUserData(userConfig);
}
// #endregion 表格数据
onMounted(() => {
getUserData();
})
</script>
<style scoped>
.search {
display: flex;
justify-content: space-between;
}
.list {
position: relative;
.pagination {
margin-top: 5px;
position: absolute;
right: 0;
}
}
</style>
删除用户
src\api\mockData\userMock.js
新增一个mock
/**
* 删除用户
* @param id
* @return {*}
*/
deleteUser: (config) => {
const { id } = param2obj(config.url)
if (!id) {
return {
code: -999,
message: '参数不正确',
}
} else {
List = List.filter((u) => u.id !== id)
return {
code: 200,
message: '删除成功',
}
}
},
import request from '../request'
export default {
getUserData(config: object) {
return request({
url: '/api/user/getUserData',
method: 'get',
data: config,
})
},
deleteUser(data: object) {
return request({
url: '/api/user/deleteUser',
method: 'get',
data,
})
},
}
src\api\mock.js
import Mock from 'mockjs'
import homeMock from './mockData/homeMock.js'
import userMock from './mockData/userMock.js'
Mock.mock('/api/home/getTableData', 'get', {
code: 200,
data: homeMock.getDataTavle(),
})
Mock.mock('/api/home/getCountData', 'get', {
code: 200,
data: homeMock.getCountData(),
})
Mock.mock('/api/home/getChartData', 'get', {
code: 200,
data: homeMock.getChartData(),
})
Mock.mock(/\/api\/user\/getUserData.*$/, 'get', (options) => {
return {
code: 200,
result: userMock.getUserList(options),
}
})
Mock.mock(/\/api\/user\/deleteUser.*$/, 'get', (options) => {
return {
code: 200,
result: userMock.deleteUser(options),
}
})
// 添加一个默认导出,确保模块被正确加载
export default Mock
User.vue
<template>
<div class="search">
<el-button type="primary">新增</el-button>
<el-form ref="formRef" :inline="true" :model="searchForm" label-width="auto" class="demo-ruleForm">
<el-form-item label="请输入">
<el-input placeholder="请输入用户名" v-model="searchForm.name" type="text" autocomplete="off" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm(formRef)">搜索</el-button>
</el-form-item>
</el-form>
</div>
<div class="list">
<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 ? item.width : ''" />
<el-table-column fixed="right" label="操作" min-width="120">
<template #default="scope">
<el-button type="primary" size="small" @click="handleClick">
编辑
</el-button>
<el-button type="danger" size="small" @click="handleDelete(scope.$index, scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination">
<el-pagination background layout="prev, pager, next" size="small" :total="userConfig.total"
@current-change="handleCurrentChange" />
</div>
</div>
</template>
<script setup lang="ts">
import { getCurrentInstance, onMounted, reactive, ref } from 'vue'
import { ElMessageBox, type FormInstance } from 'element-plus'
const instance = getCurrentInstance();
if (!instance) {
throw new Error('getCurrentInstance() returned null');
}
const { proxy } = instance;
const formRef = ref<FormInstance>()
const searchForm = reactive({
name: '',
})
const userConfig = reactive({
name: '',
total: 0,
page: 1
})
...
const handleDelete = async (index: number, row: { id: string }) => {
try {
ElMessageBox.confirm('确定删除该用户吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(async () => {
const res = await proxy!.$userApi.deleteUser({ id: row.id });
console.log(res);
if (res.code === 200) {
tableData.value.splice(index, 1);
}
});
} catch (error) {
console.error('删除用户失败:', error);
}
}
弹窗新增用户 (子组件)
src\views\Users\AddUser.vue
<template>
<el-dialog v-model="localDialogVisible" title="添加用户" width="800">
<el-form :model="form" :inline="true">
<el-row :span="24">
<el-col :span="12">
<el-form-item label="姓名" :label-width="formLabelWidth">
<el-input v-model="form.name" autocomplete="off" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label=" 年龄" :label-width="formLabelWidth">
<el-input v-model="form.age" type="number" autocomplete="off" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="性别" :label-width="formLabelWidth" style="display: flex;">
<el-select v-model="form.sex" placeholder="请选择性别">
<el-option label="男" :value="1" />
<el-option label="女" :value="2" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="出生日期" :label-width="formLabelWidth">
<el-date-picker v-model="form.birth" type="date" placeholder="选择日期" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="地址" :label-width="formLabelWidth">
<el-input v-model="form.addr" autocomplete="off" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="closeDialog">取消</el-button>
<el-button type="primary" @click="submitForm">
确认
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { ref, watch, defineProps, defineEmits } from 'vue'
const props = defineProps({
dialogVisible: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['update:dialogVisible'])
// 本地对话框可见性状态
const localDialogVisible = ref(false)
// 监听父组件传入的dialogVisible属性变化
watch(() => props.dialogVisible, (newVal) => {
localDialogVisible.value = newVal
})
// 监听本地对话框状态变化,通知父组件
watch(() => localDialogVisible.value, (newVal) => {
if (props.dialogVisible !== newVal) {
emit('update:dialogVisible', newVal)
}
})
const formLabelWidth = '120px'
const form = ref({
name: '',
age: '',
sex: 1,
birth: '',
addr: ''
})
// 关闭对话框
const closeDialog = () => {
localDialogVisible.value = false
}
// 提交表单
const submitForm = () => {
// 这里可以添加表单验证和提交逻辑
console.log('提交的表单数据:', form.value)
// 提交成功后关闭对话框
localDialogVisible.value = false
}
</script>
<style scoped></style>
el-form 使用inline=true 下的select
这里有个情况 el-form,使用:inline="true"
然后下面的select组件出现长度不够的情况:
修复满足2个条件:
使用
el-col
设定span
再给
el-form-item
设置display:flex
<el-col :span="12"> <el-form-item label="性别" :label-width="formLabelWidth" style="display: flex;"> <el-select v-model="form.sex" placeholder="请选择性别"> <el-option label="男" :value="1" /> <el-option label="女" :value="0" /> </el-select> </el-form-item> </el-col>
改下User.vue的位置
src\views\Users\User.vue
...
<div class="pagination">
<el-pagination background layout="prev, pager, next" size="small" :total="userConfig.total"
@current-change="handleCurrentChange" />
</div>
</div>
<!-- 引入AddUser组件并传递dialogVisible属性 -->
<AddUser :dialogVisible="dialogAddUserFormVisible" @update:dialogVisible="dialogAddUserFormVisible = $event" />
</template>
<script setup lang="ts">
import { getCurrentInstance, onMounted, reactive, ref } from 'vue'
import { ElMessageBox, type FormInstance } from 'element-plus'
import AddUser from './AddUser.vue'
...
表单验证
1 el-form
定义ref获取表单
<el-form ref="formRef" :model="form" :inline="true" :rules="rules">
2 el-form-item
定义prop
<el-form-item label="姓名" :label-width="formLabelWidth" prop="name">
<el-input v-model="form.name" autocomplete="off" />
</el-form-item>
3 定义规则
import type { FormInstance, FormRules } from 'element-plus'
...
// #region 表单验证
// 表单实例引用
const formRef = ref<FormInstance>()
interface RuleForm {
name: string,
age: string,
sex: number,
birth: string,
addr: string
}
const rules = reactive<FormRules<RuleForm>>({
name: [
{ required: true, message: '请输入姓名', trigger: 'blur' },
{ min: 2, max: 10, message: '长度应为 2 到 10 个字符', trigger: 'blur' },
],
// age: [
// { required: true, message: '请输入年龄', trigger: 'blur' },
// { type: 'number', message: '年龄必须为数字', trigger: 'blur' },
// ],
// sex: [
// { required: true, message: '请选择性别', trigger: 'change' },
// ],
// birth: [
// { required: true, message: '请选择出生日期', trigger: 'change' },
// ],
// addr: [
// { required: true, message: '请输入地址', trigger: 'blur' },
// { min: 5, max: 100, message: '地址长度应为 5 到 100 个字符', trigger: 'blur' },
// ]
})
const submitForm = async () => {
if (!formRef.value) return
await formRef.value.validate((valid, fields) => {
if (valid) {
// 这里可以添加表单验证和提交逻辑
console.log('提交的表单数据:', form.value)
// 提交成功后关闭对话框
localDialogVisible.value = false
} else {
console.log('error submit!', fields)
}
})
}
// #endregion
完整代码
<template>
<el-dialog v-model="localDialogVisible" title="添加用户" width="800">
<el-form ref="formRef" :model="form" :inline="true" :rules="rules">
<el-row :span="24">
<el-col :span="12">
<el-form-item label="姓名" :label-width="formLabelWidth" prop="name">
<el-input v-model="form.name" autocomplete="off" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label=" 年龄" :label-width="formLabelWidth">
<el-input v-model="form.age" type="number" autocomplete="off" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="性别" :label-width="formLabelWidth" style="display: flex;">
<el-select v-model="form.sex" placeholder="请选择性别">
<el-option label="男" :value="1" />
<el-option label="女" :value="0" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="出生日期" :label-width="formLabelWidth">
<el-date-picker v-model="form.birth" type="date" placeholder="选择日期" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="地址" :label-width="formLabelWidth">
<el-input v-model="form.addr" autocomplete="off" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="closeDialog">取消</el-button>
<el-button type="primary" @click="submitForm">
确认
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import type { FormInstance, FormRules } from 'element-plus'
import { ref, watch, defineProps, defineEmits, reactive } from 'vue'
const props = defineProps({
dialogVisible: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['update:dialogVisible'])
// 本地对话框可见性状态
const localDialogVisible = ref(false)
// 监听父组件传入的dialogVisible属性变化
watch(() => props.dialogVisible, (newVal) => {
localDialogVisible.value = newVal
})
// 监听本地对话框状态变化,通知父组件
watch(() => localDialogVisible.value, (newVal) => {
if (props.dialogVisible !== newVal) {
emit('update:dialogVisible', newVal)
}
})
const formLabelWidth = '120px'
const form = ref({
name: '',
age: '',
sex: 1,
birth: '',
addr: ''
})
// #region 表单验证
// 表单实例引用
const formRef = ref<FormInstance>()
interface RuleForm {
name: string,
age: string,
sex: number,
birth: string,
addr: string
}
const rules = reactive<FormRules<RuleForm>>({
name: [
{ required: true, message: '请输入姓名', trigger: 'blur' },
{ min: 2, max: 10, message: '长度应为 2 到 10 个字符', trigger: 'blur' },
],
// age: [
// { required: true, message: '请输入年龄', trigger: 'blur' },
// { type: 'number', message: '年龄必须为数字', trigger: 'blur' },
// ],
// sex: [
// { required: true, message: '请选择性别', trigger: 'change' },
// ],
// birth: [
// { required: true, message: '请选择出生日期', trigger: 'change' },
// ],
// addr: [
// { required: true, message: '请输入地址', trigger: 'blur' },
// { min: 5, max: 100, message: '地址长度应为 5 到 100 个字符', trigger: 'blur' },
// ]
})
// #endregion
// 关闭对话框
const closeDialog = () => {
localDialogVisible.value = false
}
// 提交表单
const submitForm = async () => {
if (!formRef.value) return
await formRef.value.validate((valid, fields) => {
if (valid) {
// 这里可以添加表单验证和提交逻辑
console.log('提交的表单数据:', form.value)
// 提交成功后关闭对话框
localDialogVisible.value = false
} else {
console.log('error submit!', fields)
}
})
}
</script>
<style scoped></style>
添加数据 (处理日期)
const formDate = (dateInput: string | Date): string => {
let birthFormatted = '';
if (typeof dateInput === 'object' && dateInput instanceof Date) {
birthFormatted = `${dateInput.getFullYear()}-${(dateInput.getMonth() + 1).toString().padStart(2, '0')}-${dateInput.getDate().toString().padStart(2, '0')}`;
} else if (typeof dateInput === 'string') {
// 如果是字符串,尝试解析为日期
const date = new Date(dateInput);
if (!isNaN(date.getTime())) {
birthFormatted = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`;
} else {
birthFormatted = dateInput;
}
}
return birthFormatted;
}
// 提交表单
const submitForm = async () => {
if (!formRef.value) return
await formRef.value.validate((valid, fields) => {
if (valid) {
// 格式化日期为YYYY-MM-DD
console.log(typeof form.value.birth)
let birthFormatted = '';
if (form.value.birth) {
birthFormatted = formDate(form.value.birth)
}
const formData = {
...form.value,
birth: birthFormatted
};
// 这里可以添加表单验证和提交逻辑
console.log('提交的表单数据:', formData)
proxy!.$userApi.createUser(formData).then(res => {
if (res.code === 200) {
(formRef.value as FormInstance).resetFields() //重置表单
emit('callback:refreshTable') //父组件刷新表格
// 提交成功后关闭对话框
localDialogVisible.value = false
} else {
ElMessage.error('添加用户失败')
}
})
} else {
console.log('error submit!', fields)
}
})
}
修改数据
usermock.js
/**
* 修改用户
* @param id, name, addr, age, birth, sex
* @return {{code: number, data: {message: string}}}
*/
updateUser: (config) => {
const { id, name, addr, age, birth, sex } = JSON.parse(config.body)
const sex_num = parseInt(sex)
List.some((u) => {
if (u.id === id) {
u.name = name
u.addr = addr
u.age = age
u.birth = birth
u.sex = sex_num
return true
}
})
return {
code: 200,
data: {
message: '编辑成功',
},
}
},
userApi.ts
updateUser(data: object) {
return request({
url: '/api/user/updateUser',
method: 'post',
data,
})
},
mock.js
Mock.mock('/api/user/updateUser', 'post', userMock.updateUser)
User.vue
<template>
<div class="search">
<el-button type="primary" @click="handleAddUser">新增</el-button>
<el-form ref="formRef" :inline="true" :model="searchForm" label-width="auto" class="demo-ruleForm">
<el-form-item label="请输入">
<el-input placeholder="请输入用户名" v-model="searchForm.name" type="text" autocomplete="off" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm(formRef)">搜索</el-button>
</el-form-item>
</el-form>
</div>
<div class="list">
<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 ? item.width : ''" />
<el-table-column fixed="right" label="操作" min-width="120">
<template #default="scope">
<el-button type="primary" size="small" @click="handleEditClick(scope.row)">
编辑
</el-button>
<el-button type="danger" size="small" @click="handleDelete(scope.$index, scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination">
<el-pagination background layout="prev, pager, next" size="small" :total="userConfig.total"
@current-change="handleCurrentChange" />
</div>
</div>
<!-- 引入AddUser组件并传递dialogVisible属性 -->
<AddUser :dialogVisible="dialogAddUserFormVisible" :action="action" :userData="userData"
@update:dialogVisible="dialogAddUserFormVisible = $event" @callback:refreshTable="getUserData" />
</template>
<script setup lang="ts">
import { getCurrentInstance, onMounted, reactive, ref } from 'vue'
import { ElMessageBox, type FormInstance } from 'element-plus'
import AddUser from './AddUser.vue'
const instance = getCurrentInstance();
if (!instance) {
throw new Error('getCurrentInstance() returned null');
}
const { proxy } = instance;
const formRef = ref<FormInstance>()
const searchForm = reactive({
name: '',
})
const userConfig = reactive({
name: '',
total: 0,
page: 1
})
const submitForm = (formEl: FormInstance | undefined) => {
console.log('formEl', formEl)
console.log('searchForm', searchForm)
userConfig.name = searchForm.name
getUserData(userConfig);
}
// #region 表格数据
const action = ref('add');
const userData = ref({});
//编辑
const handleEditClick = async (row) => {
action.value = 'edit';
dialogAddUserFormVisible.value = true
console.log('row', row)
Object.assign(userData.value, { ...row })
console.log('userData.value', userData.value)
}
const handleDelete = async (index: number, row: { id: string }) => {
try {
ElMessageBox.confirm('确定删除该用户吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(async () => {
const res = await proxy!.$userApi.deleteUser({ id: row.id });
console.log(res);
if (res.code === 200) {
tableData.value.splice(index, 1);
}
});
} catch (error) {
console.error('删除用户失败:', error);
}
}
// #region 新增用户
const dialogAddUserFormVisible = ref(false)
const handleAddUser = () => {
action.value = 'add';
dialogAddUserFormVisible.value = true
};
// #endregion 新增用户
const tableLabel = ref([{
prop: 'name',
label: '姓名',
}, {
prop: 'age',
label: '年龄',
}, {
prop: 'labelSex', // 性别根据数字显示文本
label: '性别',
}, {
prop: 'birth',
label: '出生日期',
}, {
prop: 'addr',
label: '地址',
width: 300,
}])
const tableData = ref<{
name: string;
age: number;
sex: number;
labelSex: string;
birth: string;
addr: string;
}[]>([])
async function getUserData(searchForm?: { name: string } | null) {
try {
const res = await proxy!.$userApi.getUserData(searchForm);
console.log(res);
if (res.code === 200) {
tableData.value = res.result.list;
tableData.value.forEach(item => {
item.labelSex = item.sex === 1 ? '男' : '女';
})
userConfig.total = res.result.count;
}
} catch (error) {
console.error('获取表格数据失败:', error);
}
}
const handleCurrentChange = (val: number) => {
console.log(`current page: ${val}`)
userConfig.page = val
getUserData(userConfig);
}
// #endregion 表格数据
onMounted(() => {
getUserData();
})
</script>
<style scoped>
.search {
display: flex;
justify-content: space-between;
}
.list {
position: relative;
.pagination {
margin-top: 5px;
position: absolute;
right: 0;
}
}
</style>
AddUser.vue
<template>
<el-dialog v-model="localDialogVisible" :title="action === 'add' ? '添加用户' : '编辑用户'" width="800">
<el-form ref="formRef" :model="form" :inline="true" :rules="rules">
<el-row :span="24">
<el-col :span="12">
<el-form-item label="姓名" :label-width="formLabelWidth" prop="name">
<el-input v-model="form.name" autocomplete="off" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label=" 年龄" :label-width="formLabelWidth" prop="age">
<el-input v-model="form.age" type="number" autocomplete="off" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="性别" :label-width="formLabelWidth" prop="sex" style="display: flex;">
<!-- 确保el-select的value和el-option的value类型一致 -->
<el-select v-model="form.sex" placeholder="请选择性别" clearable>
<el-option label="男" :value="1" />
<el-option label="女" :value="0" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="出生日期" :label-width="formLabelWidth">
<el-date-picker v-model="form.birth" type="date" placeholder="选择日期" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="地址" :label-width="formLabelWidth">
<el-input v-model="form.addr" autocomplete="off" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="closeDialog">取消</el-button>
<el-button type="primary" @click="submitForm">
确认
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
import { ref, watch, defineProps, defineEmits, reactive, getCurrentInstance } from 'vue'
const instance = getCurrentInstance();
if (!instance) {
throw new Error('getCurrentInstance() returned null');
}
const { proxy } = instance;
const props = defineProps({
dialogVisible: {
type: Boolean,
default: false
},
action: {
type: String,
default: 'add'
},
userData: {
type: Object,
default: () => ({})
}
})
const emit = defineEmits(['update:dialogVisible', 'callback:refreshTable'])
// 本地对话框可见性状态
const localDialogVisible = ref(false)
// 监听父组件传入的dialogVisible属性变化
watch(() => props.dialogVisible, (newVal) => {
localDialogVisible.value = newVal
})
// 监听本地对话框状态变化,通知父组件
watch(() => localDialogVisible.value, (newVal) => {
if (props.dialogVisible !== newVal) {
emit('update:dialogVisible', newVal)
}
})
// 在其他watch之后添加对userData的监听
watch(() => props.userData, (newVal) => {
console.log('newVal开始', newVal)
if (newVal && Object.keys(newVal).length > 0 && props.action === 'edit') {
// 深拷贝用户数据到表单中
form.value = { ...newVal }
// 确保age字段类型正确(根据表单定义,age应该是字符串)
form.value.age = newVal.age ? String(newVal.age) : ''
// 确保birth字段格式正确
if (newVal.birth) {
form.value.birth = newVal.birth
}
console.log('newVal结束', newVal)
}
}, { immediate: true, deep: true })
const formLabelWidth = '120px'
const form = ref({
name: '',
age: '',
sex: 1,
birth: '',
addr: ''
})
// #region 表单验证
// 表单实例引用
const formRef = ref<FormInstance>()
interface RuleForm {
name: string,
age: string,
sex: number,
birth: string,
addr: string
}
const rules = reactive<FormRules<RuleForm>>({
name: [
{ required: true, message: '请输入姓名', trigger: 'blur' },
{ min: 2, max: 10, message: '长度应为 2 到 10 个字符', trigger: 'blur' },
],
age: [
{ required: true, message: '请输入年龄', trigger: 'blur' },
],
// sex: [
// { required: true, message: '请选择性别', trigger: 'change' },
// ],
// birth: [
// { required: true, message: '请选择出生日期', trigger: 'change' },
// ],
// addr: [
// { required: true, message: '请输入地址', trigger: 'blur' },
// { min: 5, max: 100, message: '地址长度应为 5 到 100 个字符', trigger: 'blur' },
// ]
})
const formDate = (dateInput: string | Date): string => {
let birthFormatted = '';
if (typeof dateInput === 'object' && dateInput instanceof Date) {
birthFormatted = `${dateInput.getFullYear()}-${(dateInput.getMonth() + 1).toString().padStart(2, '0')}-${dateInput.getDate().toString().padStart(2, '0')}`;
} else if (typeof dateInput === 'string') {
// 如果是字符串,尝试解析为日期
const date = new Date(dateInput);
if (!isNaN(date.getTime())) {
birthFormatted = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`;
} else {
birthFormatted = dateInput;
}
}
return birthFormatted;
}
// 提交表单
const submitForm = async () => {
if (!formRef.value) return
await formRef.value.validate((valid, fields) => {
if (valid) {
// 格式化日期为YYYY-MM-DD
console.log(typeof form.value.birth)
let birthFormatted = '';
if (form.value.birth) {
birthFormatted = formDate(form.value.birth)
}
const formData = {
...form.value,
birth: birthFormatted
};
// 这里可以添加表单验证和提交逻辑
console.log('提交的表单数据:', formData)
if (props.action === 'add') {
proxy!.$userApi.createUser(formData).then(res => {
if (res.code === 200) {
(formRef.value as FormInstance).resetFields() //重置表单
emit('callback:refreshTable') //父组件刷新表格
// 提交成功后关闭对话框
localDialogVisible.value = false
} else {
ElMessage.error('添加用户失败')
}
})
} else {
proxy!.$userApi.updateUser(formData).then(res => {
if (res.code === 200) {
(formRef.value as FormInstance).resetFields() //重置表单
emit('callback:refreshTable') //父组件刷新表格
// 提交成功后关闭对话框
localDialogVisible.value = false
} else {
ElMessage.error('更新用户失败')
}
})
}
} else {
console.log('error submit!', fields)
}
})
}
// #endregion
// 关闭对话框
const closeDialog = () => {
localDialogVisible.value = false
// 重置表单数据和验证状态
if (formRef.value) {
(formRef.value as FormInstance).resetFields()
}
// 额外重置表单数据对象(确保完全清空)
form.value = {
name: '',
age: '',
sex: 1, // 默认值设为男
birth: '',
addr: ''
};
}
</script>
<style scoped></style>
问题1:性别女选择错误
我找了半天,以为传值不对,结果是 <el-option label="女" :value="2" />
的value 我开始写的2,前面替换一下
number 类型没问题
<el-form-item label="性别" :label-width="formLabelWidth" prop="sex" style="display: flex;">
<!-- 确保el-select的value和el-option的value类型一致 -->
<el-select v-model="form.sex" placeholder="请选择性别" clearable>
<el-option label="男" :value="1" />
<el-option label="女" :value="0" />
</el-select>
</el-form-item>
问题2:修改
和新增
按钮,切换时点取消
,数据加载不正确。
// 关闭对话框
const closeDialog = () => {
localDialogVisible.value = false
// 重置表单数据和验证状态
if (formRef.value) {
(formRef.value as FormInstance).resetFields()
}
// 额外重置表单数据对象(确保完全清空)
form.value = {
name: '',
age: '',
sex: 1, // 默认值设为男
birth: '',
addr: ''
};
}
tag 标签
1 刷新页面的时候,自动激活 menu 的文本颜色
:default-active="route.path"
<template>
<el-aside :width="width">
<el-menu active-text-color="#ffd04b" background-color="#545c64" class="el-aside" :default-active="route.path"
text-color="#fff" @open="handleOpen" @close="handleClose" :collapse="isCollapse" :collapse-transition="false">
2 页面顶部实现d打开过的链接 (tag标签)
useMenuStore.ts
import { defineStore } from 'pinia'
import { ref } from 'vue'
//组合式写法
export const useMenuStore = defineStore('menuStore', () => {
const isCollapse = ref(false)
const tags = ref([
{
label: '首页',
name: 'home',
path: '/home',
icon: 'home',
},
])
function changeIsCollapse() {
isCollapse.value = !isCollapse.value
}
function selectMenuTotags(item: any) {
const find = tags.value.find((tag) => tag.name === item.name)
if (find) {
// return find
} else {
tags.value.push(item)
}
console.log(tags.value)
}
function removTag(item: any) {
tags.value = tags.value.filter((tag) => tag.name !== item.name)
}
return { isCollapse, tags, changeIsCollapse, selectMenuTotags, updateTag }
})
创建 src\components\CommonTag.vue
<template>
<div class="tags">
<el-tag v-for="(tag, index) in tags" :key="tag.name" :closable="tag.name !== 'home'"
:effect="tag.name === route.name ? 'dark' : 'plain'" @click="handleGoRouter(tag)"
@close="handleClose(tag, index)">
{{ tag.label }}
</el-tag>
</div>
</template>
<script setup lang="ts">
import { useRoute } from 'vue-router';
import { useMenuStore } from '@/stores/useMenuStore';
import router from '@/router';
import { computed } from 'vue';
const tags = computed(() => useMenuStore().tags)
const route = useRoute();
const store = useMenuStore()
function handleGoRouter(tag: any) {
router.push(tag.path)
}
function handleClose(tag: any, index: number) {
if (tag.name === 'home') return
store.removTag(tag) //更新tag
if (tag.name !== route.name) return
//切换当前index的前一个tag
const preTag = tags.value[index - 1]
store.selectMenuTotags(preTag)
router.push(preTag.path)
}
</script>
<style scoped>
.el-tag {
margin: 0 10px 10px 0;
cursor: pointer;
}
</style>
src\views\Main.vue
<script setup lang="ts">
import CommonAside from "@/components/CommonAside.vue";
import CommonHeader from "@/components/CommonHeader.vue";
import CommonTag from '@/components/CommonTag.vue'
</script>
<template>
<div class="common-layout">
<el-container class="common-container">
<CommonAside />
<el-container>
<!-- 侧边栏 -->
<el-header class="common-header">
<CommonHeader />
</el-header>
<!-- 内容区域 -->
<el-main>
<CommonTag />
<RouterView />
</el-main>
</el-container>
</el-container>
</div>
</template>
<style scoped>
.common-layout,
.common-container {
height: 100vh;
}
.common-header {
background-color: #333;
}
</style>
src\components\CommonAside.vue
修复一些问题
<template>
<el-aside :width="width">
<el-menu active-text-color="#ffd04b" background-color="#545c64" class="el-aside" :default-active="route.path"
text-color="#fff" @open="handleOpen" @close="handleClose" :collapse="isCollapse" :collapse-transition="false">
<h3 v-show="!isCollapse">通用管理后台</h3>
<h3 v-show="isCollapse">后台</h3>
<template v-for="item in list" :key="item.path">
<el-sub-menu :index="item.path" v-if="item.children && item.children.length > 0">
<template #title>
<el-icon>
<component :is="item.icon" />
</el-icon>
<span>{{ item.label }}</span>
</template>
<el-menu-item v-for="children in item.children" :key="children.path" :index="children.path"
@click="handleGoRouter(children.path)">
<el-icon>
<component :is="children.icon" />
</el-icon>
{{ children.label }}
</el-menu-item>
</el-sub-menu>
<el-menu-item v-else :index="item.path" @click="handleGoRouter(item)">
<el-icon>
<component :is="item.icon" />
</el-icon>
<span>{{ item.label }}</span>
</el-menu-item>
</template>
</el-menu>
</el-aside>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { useMenuStore } from '@/stores/useMenuStore';
import { useRoute, useRouter } from 'vue-router';
const router = useRouter()
const route = useRoute()
const isCollapse = computed(() => useMenuStore().isCollapse);
const width = computed(() => isCollapse.value ? '64px' : '200px');
const list = ref([
{
path: '/home',
name: 'home',
label: '首页',
icon: 'house',
url: 'Home'
},
{
path: '/mall',
name: 'mall',
label: '商品管理',
icon: 'video-play',
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 handleOpen = (key: string, keyPath: string[]) => {
console.log(key, keyPath)
}
const handleClose = (key: string, keyPath: string[]) => {
console.log(key, keyPath)
}
const handleGoRouter = (item: any) => {
router.push(item.path)
useMenuStore().selectMenuTotags(item);
}
</script>
<style scoped>
h3 {
color: #fff;
text-align: center;
}
.el-aside {
height: 100vh;
border: none;
/* 移除边框 */
}
</style>
login 登陆
src\views\Login.vue
<script setup>
import { reactive } from 'vue'
const loginForm = reactive({
username: '',
password: ''
})
function handleLogin() {
console.log(loginForm)
}
</script>
<template>
<div class="body-login">
<el-form :model="loginForm" class="login-container">
<h1>欢迎登录</h1>
<el-form-item>
<el-input type="input" placeholder="请输入账号" v-model="loginForm.username"></el-input>
</el-form-item>
<el-form-item>
<el-input type="password" placeholder="请输入密码" v-model="loginForm.password"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleLogin">登录</el-button>
</el-form-item>
</el-form>
</div>
</template>
<style scoped>
.body-login {
width: 100%;
height: 100%;
background-size: 100%;
overflow: hidden;
}
.login-container {
width: 350px;
background-color: #fff;
border: 1px solid #eaeaea;
border-radius: 15px;
padding: 35px 35px 15px 35px;
box-shadow: 0 0 25px #cacaca;
margin: 250px auto;
h1 {
text-align: center;
margin-bottom: 20px;
color: #505450;
}
:deep(.el-form-item__content) {
justify-content: center;
}
}
</style>
Mockjs
src\api\mockData\permisson.js
import Mock from 'mockjs'
export default {
getMenu: (config) => {
const { username, password } = JSON.parse(config.body)
// 先判断用户是否存在
// 判断账号和密码是否对应
// menuList用于后面做权限分配,也就是用户可以展示的菜单
if (username === 'admin' && password === 'admin') {
return {
code: 200,
data: {
menuList: [
{
path: '/home',
name: 'home',
label: '首页',
icon: 'house',
url: 'Home',
},
{
path: '/mall',
name: 'mall',
label: '商品管理',
icon: 'video-play',
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',
},
],
},
],
token: Mock.Random.guid(),
message: '获取成功',
},
}
} else if (username === 'sansan' && password === 'sansan') {
return {
code: 200,
data: {
menuList: [
{
path: '/home',
name: 'home',
label: '首页',
icon: 'house',
url: 'Home',
},
{
path: '/user',
name: 'user',
label: '用户管理',
icon: 'user',
url: 'User',
},
],
token: Mock.Random.guid(),
message: '获取成功',
},
}
} else {
return {
code: -999,
data: {
message: '密码错误',
},
}
}
},
}
src\api\mock.js
import Mock from 'mockjs'
import homeMock from './mockData/homeMock.js'
import userMock from './mockData/userMock.js'
import permissionMock from './mockData/permisson.js'
Mock.mock('/api/home/getTableData', 'get', {
code: 200,
data: homeMock.getDataTavle(),
})
Mock.mock('/api/home/getCountData', 'get', {
code: 200,
data: homeMock.getCountData(),
})
Mock.mock('/api/home/getChartData', 'get', {
code: 200,
data: homeMock.getChartData(),
})
Mock.mock(/\/api\/user\/getUserData.*$/, 'get', (options) => {
return {
code: 200,
result: userMock.getUserList(options),
}
})
Mock.mock('/api/user/createUser', 'post', userMock.createUser)
Mock.mock('/api/user/updateUser', 'post', userMock.updateUser)
Mock.mock('/api/login', 'post', permissionMock.getMenu)
// 添加一个默认导出,确保模块被正确加载
export default Mock
src\api\ApiService\loginApi.ts
import request from '../request'
export default {
login(data: object) {
return request({
url: '/api/login',
method: 'post',
data,
})
},
}
使用 Pinia 和 localStorage 存储 menuList 和 同步路由
路由初始化流程
- 应用启动
- 在
main.ts
中创建 Vue 应用实例 - 注册 Pinia 状态管理
- 恢复本地存储的菜单数据
stores.restoreFromLocalStorage()
- 注册路由
app.use(router)
- 等待路由准备就绪后挂载应用
router.isReady().then(() => app.mount('#app'))
- 在
- 菜单和路由恢复
- 在应用启动时,从 localStorage 恢复之前保存的菜单数据
- 通过
restoreFromLocalStorage()
方法调用addMenus()
添加动态路由
动态路由机制
项目采用了动态路由添加机制,具体实现在 useMenuStore.ts
中:
- 路由添加过程:
addMenus()
方法接收菜单数组- 首先调用
removeDynamicRoutes()
清除已有的动态路由 - 遍历菜单项,使用
router.addRoute('main', routeConfig)
将每个菜单项作为main
路由的子路由添加
- 组件解析:
- 使用
resolveComponentByName()
方法根据组件名称映射到实际的组件文件 - 支持的组件包括: home, mall, user, addUser
- 使用
src\stores\useMenuStore.ts
import { defineStore } from 'pinia'
import { ref } from 'vue'
import router from '@/router' // 直接导入路由实例
//组合式写法
export const useMenuStore = defineStore('menuStore', () => {
const isCollapse = ref(false)
const tags = ref([
{
label: '首页',
name: 'home',
path: '/home',
icon: 'home',
},
])
const menuList = ref([])
function updateMenuList(list: any) {
menuList.value = list
localStorage.setItem('menuList', JSON.stringify(list))
}
// 根据组件名解析对应的组件文件路径
function resolveComponentByName(componentName: string) {
// 根据项目中的实际组件文件映射
const componentMap: Record<string, () => Promise<any>> = {
home: () => import('@/views/Home.vue'),
mall: () => import('@/views/Mall.vue'),
user: () => import('@/views/Users/User.vue'),
addUser: () => import('@/views/Users/AddUser.vue'),
// 可以根据需要添加更多组件映射
}
// 返回对应的组件导入函数,如果不存在则返回默认组件
return componentMap[componentName] || (() => import('@/views/Home.vue'))
}
// 移除所有动态添加的路由
function removeDynamicRoutes() {
try {
// 确保路由实例存在且方法可用
if (router && typeof router.getRoutes === 'function') {
const mainRoute = router.getRoutes().find((route) => route.name === 'main')
if (mainRoute && mainRoute.children) {
// 保留首页路由,移除其他动态添加的路由
mainRoute.children = mainRoute.children.filter((child) => child.name === 'home')
}
} else {
console.warn('路由实例未正确初始化,无法移除动态路由')
}
} catch (error) {
console.error('移除动态路由失败:', error)
}
}
function addMenus(routerItem: object[]) {
if (!routerItem || !Array.isArray(routerItem)) {
console.error('菜单数据无效:', routerItem)
return
}
// 1 先保存菜单数据
updateMenuList(routerItem)
// 2 移除所有动态路由(避免重复添加)
removeDynamicRoutes()
console.log('所有动态路由删除完成', router.getRoutes())
// 3 添加新的动态路由
routerItem.forEach((menuItem: any) => {
// 确保菜单项有必要的路由属性
if (menuItem.path && menuItem.name) {
try {
// 构建正确的路由配置
const routeConfig = {
path: menuItem.path,
name: menuItem.name,
meta: {
label: menuItem.label,
icon: menuItem.icon,
},
// 使用组件解析函数
component: resolveComponentByName(menuItem.name),
}
// 确保路由实例存在且方法可用
if (router && typeof router.addRoute === 'function') {
router.addRoute('main', routeConfig)
console.log(`已添加路由: ${menuItem.name} -> ${menuItem.path}`)
} else {
console.warn('路由实例未正确初始化,无法添加路由')
}
} catch (error) {
console.error(`添加路由失败: ${menuItem.name}`, error)
}
}
})
console.log('所有动态路由添加完成', router.getRoutes())
}
// 从 localStorage 恢复菜单和路由
function restoreFromLocalStorage() {
const savedMenuList = localStorage.getItem('menuList')
if (savedMenuList) {
try {
const menuListData = JSON.parse(savedMenuList)
addMenus(menuListData)
console.log('已从本地存储恢复菜单和路由')
} catch (error) {
console.error('恢复菜单数据失败:', error)
localStorage.removeItem('menuList')
}
} else {
console.log('本地存储中没有菜单数据')
}
}
function changeIsCollapse() {
isCollapse.value = !isCollapse.value
}
function selectMenuTotags(item: any) {
const find = tags.value.find((tag) => tag.name === item.name)
if (find) {
// return find
} else {
tags.value.push(item)
}
console.log(tags.value)
}
function removTag(item: any) {
tags.value = tags.value.filter((tag) => tag.name !== item.name)
}
return {
isCollapse,
tags,
menuList,
updateMenuList,
addMenus,
changeIsCollapse,
selectMenuTotags,
removTag,
restoreFromLocalStorage,
}
})
Login.vue
<script setup lang="ts">
import { reactive, getCurrentInstance } from 'vue'
import { useRouter } from 'vue-router'
import { useMenuStore } from '@/stores/useMenuStore'
const router = useRouter()
const loginForm = reactive({
username: '',
password: ''
})
const instance = getCurrentInstance();
if (!instance) {
throw new Error('getCurrentInstance() returned null');
}
const { proxy } = instance;
const store = useMenuStore();
async function handleLogin() {
console.log(loginForm)
const res = await proxy!.$loginApi.login(loginForm);
console.log(res);
if (res.code === 200) {
localStorage.setItem('token', res.data.token)
// store.updateMenuList(res.data.menuList)
// console.log(store.menuList)
store.addMenus(res.data.menuList);
router.push({
path: '/home'
})
}
}
</script>
<template>
<div class="body-login">
<el-form :model="loginForm" class="login-container">
<h1>欢迎登录</h1>
<el-form-item>
<el-input type="input" placeholder="请输入账号" v-model="loginForm.username"></el-input>
</el-form-item>
<el-form-item>
<el-input type="password" placeholder="请输入密码" v-model="loginForm.password"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleLogin">登录</el-button>
</el-form-item>
</el-form>
</div>
</template>
<style scoped>
.body-login {
width: 100%;
height: 100%;
background-size: 100%;
overflow: hidden;
}
.login-container {
width: 350px;
background-color: #fff;
border: 1px solid #eaeaea;
border-radius: 15px;
padding: 35px 35px 15px 35px;
box-shadow: 0 0 25px #cacaca;
margin: 250px auto;
h1 {
text-align: center;
margin-bottom: 20px;
color: #505450;
}
:deep(.el-form-item__content) {
justify-content: center;
}
}
</style>
修改 main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import '@/assets/sass/index.scss'
import App from './App.vue'
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 homeApi from '@/api/ApiService/homeApi'
import userApi from '@/api/ApiService/userApi'
import loginApi from '@/api/ApiService/loginApi'
import '@/api/mock'
import 'default-passive-events'
import { useMenuStore } from '@/stores/useMenuStore'
const app = createApp(App)
app.config.globalProperties.$homeApi = homeApi
app.config.globalProperties.$userApi = userApi
app.config.globalProperties.$loginApi = loginApi
app.use(createPinia())
const stores = useMenuStore()
stores.restoreFromLocalStorage()
app.use(router)
app.use(ElementPlus) //将 ElementPlus 插件注册到 Vue 应用中
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
router.isReady().then(() => {
app.mount('#app')
})
退出
src\stores\useMenuStore.ts
....
function removTag(item: any) {
tags.value = tags.value.filter((tag) => tag.name !== item.name)
}
function logout() {
localStorage.removeItem('menuList')
localStorage.removeItem('token')
removeDynamicRoutes()
router.push({ name: 'login' })
}
return {
isCollapse,
tags,
menuList,
updateMenuList,
addMenus,
changeIsCollapse,
selectMenuTotags,
removTag,
restoreFromLocalStorage,
logout,
}
})
src\components\CommonHeader.vue
<template>
<div class="header">
<div class="l-content">
<el-button size="small" @click="changeIsCollapse">
<el-icon>
<component class="icons" :is="'menu'"></component>
</el-icon>
</el-button>
<el-breadcrumb separator="/" class="breadcrumb">
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
</el-breadcrumb>
</div>
<div class="r-content">
<el-dropdown>
<span class="el-dropdown-link">
<img class="user" :src="getImageUrl" alt="">
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>个人中心</el-dropdown-item>
<el-dropdown-item @click="logout">退出</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</template>
<script setup lang="ts">
const getImageUrl = new URL("https://img2.baidu.com/it/u=2318884743,3754999155&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500").href;
import { useMenuStore } from '@/stores/useMenuStore'
const store = useMenuStore()
const changeIsCollapse = store.changeIsCollapse;
function logout() {
store.logout()
}
</script>
<style scoped>
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
height: 50px;
}
.icons {
width: 20px;
height: 20px;
}
.l-content {
display: flex;
align-items: center;
.el-button {
margin-right: 20px;
}
}
.r-content {
.user {
height: 20px;
width: 20px;
border-radius: 50%;
}
}
:deep(.breadcrumb span) {
color: #fff !important;
cursor: pointer !important;
}
</style>
page 404 (路由守卫)
先准备一张图
然后设置路由
src\router\index.ts
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
name: 'main',
// redirect: '/login',
component: () => import('@/views/Main.vue'),
children:[]
},
{
path: '/login',
name: 'login',
component: () => import('@/views/Login.vue'),
},
{
path: '/404',
name: '404',
component: () => import('@/views/404.vue'),
},
]
const router = createRouter({
history: createWebHistory(),
routes,
})
export default router
最后加一个全局路由守卫
src\main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import '@/assets/sass/index.scss'
import App from './App.vue'
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 homeApi from '@/api/ApiService/homeApi'
import userApi from '@/api/ApiService/userApi'
import loginApi from '@/api/ApiService/loginApi'
import '@/api/mock'
import 'default-passive-events'
import { useMenuStore } from '@/stores/useMenuStore'
//全局前置守卫beforeEach
router.beforeEach((to, from, next) => {
console.log(router.getRoutes())
const token = localStorage.getItem('token')
if (to.path !== '/login' && !token) {
next({
path: '/login',
})
return
}
const filerRouter = router.getRoutes().filter((item) => item.path === to.path)
if (filerRouter.length === 0) {
next({
name: '404',
})
return
}
next()
})
const app = createApp(App)
app.config.globalProperties.$homeApi = homeApi
app.config.globalProperties.$userApi = userApi
app.config.globalProperties.$loginApi = loginApi
app.use(createPinia())
const stores = useMenuStore()
stores.restoreFromLocalStorage()
app.use(router)
app.use(ElementPlus) //将 ElementPlus 插件注册到 Vue 应用中
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
router.isReady().then(() => {
app.mount('#app')
})
vite的一些配置
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/
export default defineConfig({
plugins: [vue(), vueDevTools()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
// server: {
// port: 3000,
// },
build: {
outDir: 'build', // 指定打包后的输出目录为 'build'
sourcemap: true, // 生成 source map 文件,便于调试
emptyOutDir: true, // 每次构建之前清空输出目录
minify: 'terser', // 使用 terser 作为压缩工具,它比默认的 esbuild 生成更小的文件
rollupOptions: {
// 自定义底层的 Rollup 打包选项
output: {
manualChunks: {
// 手动配置代码分割策略,将代码打包成不同的 chunk
vue: ['vue', 'vue-router', 'pinia'], // 将 vue 相关的核心库打包成一个名为 'vue' 的 chunk
element: ['element-plus'], // 将 element-plus UI 组件库单独打包成一个 chunk
'element-plus-icons-vue': ['element-plus-icons-vue'], // 将 element-plus 图标库单独打包
components: (id) => {
// 动态分割函数:根据模块路径决定是否单独打包
if (id.includes('/src/components/')) {
// 如果模块路径包含 '/src/components/'
return 'components' // 则将其归入 'components' 这个 chunk
}
},
},
},
},
},
})