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>

image

顶部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>

image

mock 的使用

npm install mockjs -D

image

第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!.$homeApinull(一般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>

下面使用折线

image

<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 的更新,等待官方解决此问题。

表格和分页

image

创建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);
    }
}

弹窗新增用户 (子组件)

image

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组件出现长度不够的情况:

image

修复满足2个条件:

  1. 使用 el-col 设定span

  2. 再给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>

image

添加数据 (处理日期)

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标签)

image

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 登陆

image

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 和 同步路由

路由初始化流程

  1. 应用启动
    • main.ts 中创建 Vue 应用实例
    • 注册 Pinia 状态管理
    • 恢复本地存储的菜单数据 stores.restoreFromLocalStorage()
    • 注册路由 app.use(router)
    • 等待路由准备就绪后挂载应用 router.isReady().then(() => app.mount('#app'))
  2. 菜单和路由恢复
    • 在应用启动时,从 localStorage 恢复之前保存的菜单数据
    • 通过 restoreFromLocalStorage() 方法调用 addMenus() 添加动态路由

动态路由机制

项目采用了动态路由添加机制,具体实现在 useMenuStore.ts 中:

  1. 路由添加过程:
    • addMenus() 方法接收菜单数组
    • 首先调用 removeDynamicRoutes() 清除已有的动态路由
    • 遍历菜单项,使用 router.addRoute('main', routeConfig) 将每个菜单项作为 main 路由的子路由添加
  2. 组件解析:
    • 使用 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
            }
          },
        },
      },
    },
  },
})
posted @ 2025-09-19 15:11  【唐】三三  阅读(6)  评论(0)    收藏  举报