普通视图

发现新文章,点击刷新页面。
昨天以前首页

脚手架搭建项目框架(create-vite、vue-cli、create-vue、quasar-cli)

作者 RONIN
2026年5月6日 17:04

脚手架脚手架搭建项目框架

一. create-vite

npm create vite@latest(yarn create vite)安装脚手架命令

二. vue-cli

npm i -g @vue-cli安装脚手架命令

vue create project1创建项目

三. create-vue(vue官方的项目脚手架工具,内置了vite构建工具)项目开发中使用的脚手架

1.技术栈:

Create-vue脚手架 + Vite构建工具 + vue3组合式API (vue3选项式API)+ typescript + vue-router + pinia状态管理 (vuex状态管理)+ axios网络库 + vant3 UI组件库(element-plus UI组件库) + eslint + pretter + sass

2.脚手架搭建项目框架步骤:

1) npm init vue@latest 安装脚手架命令。

根据预设生成相应的配置文件(选择ts、vue-router、pinia、eslint、pretter)。

image.png

2) pinia配置:

npm i pinia-plugin-persist -S 安装pinia持久化存储插件。

创建stores文件夹→index.ts(创建pinia根存储,集成插件)

import { createPinia } from 'pinia'
import piniaPluginPersist from 'pinia-plugin-persist'//引入pinia持久化存储插件

const storeRoot = createPinia()
storeRoot.use(piniaPluginPersist)//集成插件
export default storeRoot

main.js入口文件(集成pinia)

import { createApp } from "vue";
import router from "./router";
import store from "./store";
import App from "./App.vue";
import 'normalize.css' // 重置样式

const app = createApp(App)
app.use(router);
app.use(store);
app.mount("#app");

stores文件夹→user.ts(定义store存储对象,持久化存储增加persist选项)

import {defineStore} from 'pinia'
/**
 * 定义名为useUserStore的存储对象
 *  defineStore方法
 *   第一个参数: 模块名称是唯一的
 *   第二个参数: 选项对象  state, actions, getters
 *                        data    methods   computed
 */
export const useUserStore = defineStore('user',{
    state(){
        return {
            account:null // 账户数据 {name,nick,password}  
        }
    },
    actions:{
        // 具体业务逻辑,可以是同步或异步操作
        saveAccount(account){
            this.account = account
        }
    },
    getters:{
        //getters中定义的方法名/计算属性名不能与state相同
        // userAccount:state => {
        //     return state.account
        // }//定义方式1
        userAccount(){
            return this.account
        }//定义方式2
    },
    persist: {//持久化存储
        enabledtrue,
        strategies: [
            {
                key: user,
                storagelocalStorage,
                paths: ['account'],
            },
        ],
    },
})

3)Sass、axios网络库、UI组件库需手动安装集成。状态管理vuex也需要手动安装集成。

npm i normalize.css -S 安装样式重置库(兼容浏览器)。main.ts中引入import'normalize.css'
npm i sass -d 安装sass(css预处理器,开发环境用)。
npm install axios -s 安装网络库axios(前后端数据交互)。

创建utils文件夹→request.ts中配置(创建axios实例,封装请求/响应拦截器)

import axios from 'axios'
import { Toast } from 'vant';
// 服务根地址
// export const baseURL = 'http://10.7.163.165:8089'  // 开发环境
/**
 * 创建axios实例
 *   封装baseURL
 */
const axiosInstance = axios.create({
    baseURL, // 服务根地址
    timeout: 3000, // 超时时间
})
/**
 * 请求拦截器
 */
axiosInstance.interceptors.request.use(
    config => {
        const token = localStorage.getItem('TOKEN')
        if(token){
            config.headers['Authorization'] = token
        }
        return config
    },
    error => {
        return Promise.reject(error)
    }
)
/**
 * 响应拦截器
 */
axiosInstance.interceptors.response.use(
    response => {
        return response.data
    },
    error => {
        const { response } = error
        if (response) {
            const status = response.status
            switch (status) {
                case 404:
                    Toast('资源不存在 404')
                    break
                case 401:
                    Toast('Unauthorized 身份验证凭证缺失!')
                    break
                case 403:
                    Toast('403 Forbidden - 拒绝访问!')
                    break
                case 500:
                    Toast('服务器出错')
                    break
                default:
                    Toast('出现异想不到的错误!')
                    break
            }
        }else {
            // 说明服务器连结果都没有返回,可能的原因有两种:
            /**
             * 1. 服务器崩掉了
             * 2. 前端客户端断网状态
             */
            if (!window.navigator.onLine) {
                // 判断为断网,可以跳转到断网页面
                Toast('网络不可用,请检查您的网络连接!')
                return
            } else {
                Toast('连接服务端出错!' + error?.message)
                return Promise.reject(error)
            }
        }
        return Promise.reject(error)
    }
)
export default axiosInstance
vant组件库安装与配置
  • npm i vant@latest-v3 安装vant组件库
  • npm i unplugin-vue-components -D 安装vant按需引入插件。vite.config.ts中引入集成
  • npm i postcss-pxtorem -D安装移动端适配插件,将px单位转化为rem单位。vite.config.ts中引入集成
  • npm i amfe-flexible -D安装移动端适配插件,适配不同屏幕尺寸。main.ts中引入

vite.config.ts

import { fileURLToPath, URLfrom 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

import Components from 'unplugin-vue-components/vite'//引入按需引入插件
import { VantResolverfrom 'unplugin-vue-components/resolvers'//引入按需引入插件

import postCssPxToRem from 'postcss-pxtorem'//引入移动端适配插件

import { viteMockServe } from 'vite-plugin-mock'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    Components({
      resolvers: [VantResolver()],//集成按需引入插件
    }),
    viteMockServe({//集成mock模拟接口数据插件
      // 更多配置见最下方
      supportTstrue,
      loggerfalse,
      mockPath'./mock/', // 文件位置
    }),
  ],
  resolve: {
    alias: {
      '@'fileURLToPath(new URL('./src', import.meta.url))
    }
  },
  css:{
    postcss:{
      plugins:[
        postCssPxToRem({//自适应,px转rem
          rootValue:75,//换算的基数(设计图750的根字体为75)
          propList:['*']//需要转换的属性,*代表全部
        })
      ]
    }
  },
  server:{//代理服务器
    proxy: {
      '/api': {
        target'http://124.71.63.13:8088/',
        changeOrigintrue,
        // rewrite: path => path.replace(/^\/api/, '')
      }
    }
  },
})

main.ts

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './stores'
import 'normalize.css' // 重置样式

// 导入vant函数组件样式
import 'vant/es/toast/style'
import 'vant/es/dialog/style'
import 'vant/es/notify/style'
import 'vant/es/image-preview/style'

//引入amfe-flexible屏幕适配插件
import 'amfe-flexible'

const app = createApp(App)
app.use(store)
app.use(router)

app.mount('#app')
elementplus组件库安装与配置
  • npm install element-plus --save安装elementplus组件库
  • npm install -d unplugin-vue-components unplugin-auto-import安装两个插件自动按需引入

vite.config.js集成插件

import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

import AutoImport from 'unplugin-auto-import/vite'//引入插件
import Components from 'unplugin-vue-components/vite'//引入插件
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'//引入插件

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    AutoImport({//集成插件
        resolvers: [ElementPlusResolver()],
    }),
    Components({//集成插件
        resolvers: [ElementPlusResolver()],
    }),
  ],
  resolve: {
    alias: {
      '@'fileURLToPath(new URL('./src', import.meta.url))
    }
  },
})

main.js引入elementplus样式,否则可能出现使用组件没效果。

import { createApp } from "vue";
import router from "./router";
import store from "./store";
import 'element-plus/dist/index.css';//引入elementplus样式
import App from "./App.vue";

import ElementPlus from 'element-plus';//完整引入elementplus,文件大小会很大。可以考虑按需引入

const app = createApp(App);
app.use(router);
app.use(store);

app.use(ElementPlus);//完整引入elementplus,文件大小会很大。可以考虑按需引入
app.mount("#app");
状态管理vuex安装与配置
  • npm install vuex@next(4.0.2) --save安装vuex插件
  • npm i vuex-persistedstate -s安装vuex持久化存储插件

main.js引入集成到vue

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store  from './store'

const app = createApp(App)
app.use(router)
app.use(store)
app.mount('#app')

创建story文件夹→index.js文件集成持久化存储插件

import { createStore } from 'vuex'
import createPersistedState from 'vuex-persistedstate'

const store = createStore({
    // 集成持久化存储插件
    plugins:[createPersistedState({
        storage:sessionStorage,
        key:'storekey'
    })]
    state: {//状态数据
        count0
    },
    mutations: {//操作state状态。第一个参数:state对象,第二个参数:外部传入参数
        PLUS(state) {//方法
            // state.count = num
            state.count++
        },
    },
    /**
     * 定义操作mutations方法的方法
     * 供外部组件调用
     *   $store.dispatch('')
     *    1. 单向数流操作方式,保存状态数据以预期方式改变
     *    2. actions 异步操作
     *       mutations 同步操作
     */
    actions: {//操作mutations中的方法
        // plus(context) {//方法
        //     context.commit('PLUS')
        // }
        plus({ commit }) {//解构
            commit('PLUS')
        },
        chs({ commit },num) {//传参
            commit('CHS',num)
        }
    },
    getters: {//获取值,类似计算属性
        num(state) => { return state.count }
        // num: state => state.count
    }
})
export default store

3.解决引入vue组件ts报错(因为ts不识别.vue文件,需在env.d.ts中声明)

env.d.ts

// <reference types="vite/client" />
declare module '*.vue' {
    import { DefineComponent } from 'vue'
    const componentDefineComponent<{}, {}, any>
    export default component
}

4.目录结构介绍

image.png

  • npm install(npm i)下载依赖
  • npm run dev运行

5.打包

  • 将vue文件编译成浏览器能识别的js/html/css文件、ts编译成js文件、scss编译成css文件,压缩处理。
  • 编译后的文件存放在dist目录下(与src同级),都是html/css/js文件,将其部署到服务器上用户就可以用了。

接口环境配置: 开发环境(测试数据)/生产环境(用户使用的数据)

创建utils文件夹→request.ts中配置(创建axios实例,封装请求/响应拦截器,配置接口环境)

解决跨域问题走代理服务器:baseUrl地址为当前客户端地址,vite.config.ts配置代理服务器,代理服务器地址为目标地址(一般指测试地址/线上服务器地址)。

import axios from 'axios'
import { Toastfrom 'vant'

// 服务根地址
// export const baseURL = 'http://10.7.163.165:8089'  // 开发环境

// 生产环境 如果服务端没做跨域处理,使用代理服务器
// 走代理服务器,baseURL 地址配置为与当前客户端地址相同
export let baseURL = 'http://10.7.163.165:8089'  // 开发环境
/**
 * process.env.NODE_ENV
 *    production 生产环境
 *        npm run build
 *    development  开发环境
 *        npm run dev
 */
switch (process.env.NODE_ENV) {
    case 'production':
        baseURL = 'http://124.71.63.13'  // 生产环境
        break
    case 'development':
        baseURL = 'http://10.7.163.165:8089' // 开发环境
        break
}
/**
 * 创建axios实例
 *   封装baseURL
 */
const axiosInstance = axios.create({
    baseURL, // 服务根地址
    timeout3000, // 超时时间
})
/**
 * 请求拦截器
 */
axiosInstance.interceptors.request.use(
    config => {
        const token = localStorage.getItem('TOKEN')
        if (token) {
            config.headers['Authorization'] = token
        }
        return config
    },
    error => {
        return Promise.reject(error)
    }
)
/**
 * 响应拦截器
 */
axiosInstance.interceptors.response.use(
    response => {
        return response.data
    },
    error => {
        const { response } = error
        if (response) {
            const status = response.status
            switch (status) {
                case 404:
                    Toast('资源不存在 404')
                    break
                case 401:
                    Toast('Unauthorized 身份验证凭证缺失!')
                    break
                case 403:
                    Toast('403 Forbidden - 拒绝访问!')
                    break
                case 500:
                    Toast('服务器出错')
                    break
                default:
                    Toast('出现异想不到的错误!')
                    break
            }
        } else {
            // 说明服务器连结果都没有返回,可能的原因有两种:
            /**
             * 1. 服务器崩掉了
             * 2. 前端客户端断网状态
             */
            if (!window.navigator.onLine) {
                // 判断为断网,可以跳转到断网页面
                Toast('网络不可用,请检查您的网络连接!')
                return
            } else {
                Toast('连接服务端出错!' + error?.message)
                return Promise.reject(error)
            }
        }
        return Promise.reject(error)
    }
)
export default axiosInstance

tsconfig.json(解决request.ts中环境配置时process报错)

{
    "extends": "@vue/tsconfig/tsconfig.web.json",
    "include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
    "compilerOptions": {
        "baseUrl": ".",
        "paths": {
            "@/*": ["./src/*"]
        },
        // 解决process.env.NODE_ENV报错,
        // 1.安装npm i @types/node -S
        // 配置"types": ["node"]
        "types": ["node"]
    },
    "references": [
        {
            "path": "./tsconfig.config.json"
        }
    ]
}

vite.config.ts配置代理服务器

import { fileURLToPath, URLfrom 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

import Components from 'unplugin-vue-components/vite'//引入按需引入插件
import { VantResolverfrom 'unplugin-vue-components/resolvers'//引入按需引入插件

import postCssPxToRem from 'postcss-pxtorem'//引入移动端适配插件

import { viteMockServe } from 'vite-plugin-mock'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    Components({
      resolvers: [VantResolver()],//集成按需引入插件
    }),
    viteMockServe({//集成mock模拟接口数据插件
      // 更多配置见最下方
      supportTstrue,
      loggerfalse,
      mockPath'./mock/', // 文件位置
    }),
  ],

  resolve: {
    alias: {
      '@'fileURLToPath(new URL('./src', import.meta.url))
    }
  },
  css:{
    postcss:{
      plugins:[
        postCssPxToRem({//自适应,px转rem
          rootValue:75,//换算的基数(设计图750的根字体为75)
          propList:['*']//需要转换的属性,*代表全部
        })
      ]
    }
  },
  server:{//代理服务器
    proxy: {
      '/api': {
        target'http://124.71.63.13:8088/',//目标地址
        changeOrigintrue,//是否跨域
//这里理解成用'/api'代替target里面的地址,比如我要调用'http://40.00.100.100:3002/user/add',直接写'/api/user/add'即可
        rewrite(path) => path.replace(/^**\/** api/, ''),
      }
    }
  },
})

npm run build打包

四. quasar-cli项目开发中使用的脚手架

关于quasar要求:

  • Node 12+用于Quasar CLI与Webpack,Node 14+用于Quasar CLI与Vite。
  • Yarn v1(强烈推荐),PNPM,或NPM。

npm i -g @quasar/cli 安装脚手架命令

npm init quasar 初始化quasar根据预设生成相应的配置文件

image.png

此时回车,会生成项目文件和目录

image.png

提示安装项目依赖,选择yes回车

image.png

quasar dev(npm run dev)运行

mock模拟后端,生成伪数据接口

作者 RONIN
2026年5月6日 15:51

官网:github.com/nuysoft/Moc…

1. 安装

npm i mockjs vite-plugin-mock 安装 mockjs和 vite-plugin-mock插件

2. vite.config.ts中集成插件

import { fileURLToPath, URLfrom 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

import Components from 'unplugin-vue-components/vite'//引入按需引入vant插件

import { VantResolverfrom 'unplugin-vue-components/resolvers'

import { viteMockServe } from 'vite-plugin-mock'//引入mock插件

// https://vitejs.dev/config/

export default defineConfig({

  plugins: [

    vue(),

    Components({

      resolvers: [VantResolver()],

    }),

    viteMockServe({//配置mock插件

      // 更多配置见最下方

      supportTstrue,

      loggerfalse,

      mockPath'./mock/', // 文件位置

    }),

  ],

  resolve: {

    alias: {

      '@'fileURLToPath(new URL('./src', import.meta.url))

    }

  }

})

3. mock文件夹mock接口数据

scr同级创建mock文件夹→account.ts(模块拆分,post请求config.body取参)


const accountList = []

/**

 * 用户注册

 */

export const mockRegister = {

    url:'/mock/account/register',

    method:'post',

    response:(config)=>{

        // 注册业务逻辑

        // 1. 获取用户名和密码

        const {username,password,headerimg} = config.body

        // 2. 保存用户信息

        // localStorage.setItem('USER',JSON.stringify({username,password,headerimg}))

        const user = {username,password,headerimg}

        accountList.push(user)

        // 3. 响应内容

        return {

            resultCode:1,

            resultInfo:'注册成功'

        }

    }

}

 

/**

 * 用户登录

 */

export const mockLogin = {

    url:'/mock/account/login',

    method:'post',

    response:(config)=>{

        // 登录业务逻辑

        // 1. 获取用户名和密码

        const {username,password,headerimg} = config.body

        // 2. 判断用户是否注册

        // let userstr = localStorage.getItem('USER')

        // let user = userstr? JSON.parse(userstr):''

        const user = accountList.find(item=>item.username==username && item.password==password)

        if(user){

            return {

                resultCode:1,

                resultInfo: {

                        username,

                        password,

                        headerimg

                }

            }

        }else{

            return {

                resultCode:-1,

                resultInof:'账户出错'

            }

        }

    }

}

scr同级创建mock文件夹→good.ts(模块拆分,get请求config.query取参)

import { goodsListData, bannerListData } from './data/goodsData'

/**

 * 商品列表

 */

export const goodsList = {

    url'/mock/goods/list',

    method'get',

    response(config:any) => {

        //get请求参数

        let { pageNo, pageSize } = config.query

        pageNo = pageNo || 1

        pageSize = pageSize || 10

        console.log('pageNo ', pageNo, ' pageSize :', pageSize)

        // 分页

        const startIndex = (pageNo - 1) * pageSize

        const endIndex = pageNo * pageSize

        const list = goodsListData.slice(startIndex, endIndex)

        if(list.length>0){

            return {

                resultCode1,

                resultInfo: {

                    list,

                },

            }

        }else{

            return {

                resultCode:-1,

                resultInfo:'没有数据'

            }

        }

    },

}

 

/**

 *  banner轮播

 */

export const bannerList = {

    url'/mock/banner',

    method'get',

    response() => {

        return {

            resultCode1,

            resultInfo: {

                list: bannerListData,

            },

        }

    },

}

scr同级创建mock文件夹→data文件夹→goodsData.ts(模块拆分,存放数据)


export const goodsListData = [

    {

        id49,

        shop'【苏宁生鲜】鲜美来东海带鱼600g 海鲜水产 鲜活冷冻',

        picture:

            'https://image1.suning.cn/uimg/b2c/newcatentries/0010128947-000000000614428161_1_800x800.jpg',

        product'【苏宁生鲜】鲜美来东海带鱼600g 海鲜水产 鲜活冷冻',

        price18.88,

        oldprice69,

        putaway1,

        detailnull,

        categoryname'川湘菜',

        categoryId3,

    },

    {

        id48,

        shop'【苏宁生鲜】恒都巴西牛腩块1kg 进口牛肉 精选肉类',

        picture:

            'https://image1.suning.cn/uimg/b2c/newcatentries/0010128947-000000000614167327_1_800x800.jpg',

        product'【苏宁生鲜】恒都巴西牛腩块1kg 进口牛肉 精选肉类',

        price18.98,

        oldprice49,

        putaway1,

        detailnull,

        categoryname'川湘菜',

        categoryId3,

    },

    {

        id45,

        shop'贝克巴斯(BECBAS)E50 厨房家用食物垃圾处理器 厨余垃圾粉碎机 无线开关免打孔',

        picture:

            'https://image3.suning.cn/uimg/b2c/newcatentries/0000000000-000000000616963357_2_800x800.jpg',

        product:

            '贝克巴斯(BECBAS)E50 厨房家用食物垃圾处理器 厨余垃圾粉碎机 无线开关免打孔',

        price38,

        oldprice67,

        putaway1,

        detailnull,

        categoryname'大家电',

        categoryId9,

    },

    {

        id44,

        shop'科龙(Kelon) 正1.5匹 定速 冷暖 空调挂机 ',

        picture:

            'https://image4.suning.cn/uimg/b2c/newcatentries/0000000000-000000000178605073_1_800x800.jpg',

        product'科龙(Kelon) 正1.5匹 定速 冷暖 空调挂机 ',

        price156.9,

        oldprice190,

        putaway1,

        detail'这个商品不错',

        categoryname'排骨',

        categoryId7,

    },

    {

        id42,

        shop'华帝浴室花洒 增压花洒 主体全铜花洒 沐浴莲蓬头【自营】H-CS0014-B1D.W',

        picture:

            'https://image2.suning.cn/uimg/b2c/newcatentries/0000000000-000000000604135232_1_800x800.jpg',

        product:

            '华帝浴室花洒 增压花洒 主体全铜花洒 沐浴莲蓬头【自营】H-CS0014-B1D.W',

        price18.87,

        oldprice90,

        putaway1,

        detailnull,

        categoryname'排骨',

        categoryId7,

    },

    {

        id41,

        shop'长虹(CHANGHONG) 大1匹 冷暖定频 快速制冷热 空调挂机 KFR-26GW/DHID(W1-J)+2',

        picture:

            'https://image2.suning.cn/uimg/b2c/newcatentries/0000000000-000000000126066901_1_800x800.jpg',

        product:

            '长虹(CHANGHONG) 大1匹 冷暖定频 快速制冷热 空调挂机 KFR-26GW/DHID(W1-J)+2',

        price18.48,

        oldprice35,

        putaway1,

        detailnull,

        categoryname'火锅',

        categoryId8,

    },

    {

        id40,

        shop'小米智米(SMARTMI)智能马桶盖 家用全自动冲洗即热式加热外置过滤器杀菌洁便器',

        picture:

            'https://image4.suning.cn/uimg/b2c/newcatentries/0000000000-000000000673521926_1_800x800.jpg',

        product:

            '小米智米(SMARTMI)智能马桶盖 家用全自动冲洗即热式加热外置过滤器杀菌洁便器',

        price34.98,

        oldprice47,

        putaway1,

        detailnull,

        categoryname'火锅',

        categoryId8,

    },

]

 

/**

 * 轮播数据

 */

export const bannerListData = [

    {

        id1,

        url'https://img.alicdn.com/imgextra/i3/2053469401/O1CN01g8ituT2JJiCsKjmDz_!!2053469401.png',

        content'商家1',

        number1,

    },

    {

        id11,

        url'https://img2.baidu.com/it/u=717979571,3244631707&fm=253&fmt=auto&app=138&f=JPEG?w=1280&h=400',

        content'舒适度满分',

        number1,

    },

    {

        id12,

        url'https://img1.baidu.com/it/u=1733684228,4177721799&fm=253&fmt=auto&app=138&f=JPEG?w=1200&h=500',

        content'曲线在召唤',

        number1,

    },

]

scr同级创建创建mock文件夹→index.ts(模块集成暴露)


import { bannerList, goodsList } from './goods'

import { mockRegister, mockLogin } from './account'

 

const goods = [bannerList, goodsList]  // 商品模块接口

const account = [mockLogin, mockRegister] // 个人中心接口

 

export default [...goods,...account]

4. utils文件夹→requestMock.ts(封装axios,不封装直接使用axios也行)

import axios from 'axios'
import { Toastfrom 'vant'

export let baseURL = 'http://10.7.163.159:5173'  

/**

 * 创建axios实例

 *   封装baseURL

 */

const axiosInstance = axios.create({

    baseURL, // 服务根地址

    timeout3000, // 超时时间

})

 

/**

 * 请求拦截器

 */

axiosInstance.interceptors.request.use(

    config => {

        const token = localStorage.getItem('TOKEN')

        if (token) {

            config.headers['Authorization'] = token

        }

        return config

    },

    error => {

        return Promise.reject(error)

    }

)

/**

 * 响应拦截器

 */

axiosInstance.interceptors.response.use(

    response => {

        return response.data

    },

    error => {

        const { response } = error

        if (response) {

            const status = response.status

            switch (status) {

                case 404:

                    Toast('资源不存在 404')

                    break

                case 401:

                    Toast('Unauthorized 身份验证凭证缺失!')

                    break

                case 403:

                    Toast('403 Forbidden - 拒绝访问!')

                    break

                case 500:

                    Toast('服务器出错')

                    break

                default:

                    Toast('出现异想不到的错误!')

                    break

            }

        } else {

            // 说明服务器连结果都没有返回,可能的原因有两种:

            /**

             * 1. 服务器崩掉了

             * 2. 前端客户端断网状态

             */

            if (!window.navigator.onLine) {

                // 判断为断网,可以跳转到断网页面

                Toast('网络不可用,请检查您的网络连接!')

                return

            } else {

                Toast('连接服务端出错!' + error?.message)

                return Promise.reject(error)

            }

        }

        return Promise.reject(error)

    }

)

export default axiosInstance

5. api文件夹→index.ts文件中定义接口

import axiosInstance from '@/utils/request'
import axiosMockInstance from '@/utils/requestMock'
import type{IResponse} from '@/types/types'


/**

 * 产品列表

 *   shopKey: 店铺名称

 *   productKey: 产品名称

 * @returns 

 */

export const RequestShopList = (pageNo:number,pageSize:number):Promise<IResponse>=>{

    return axiosInstance({

        method:'get',

        url:'/api/shop',

        params:{

            pageSize,

            pageNo,

        }

    })

}

/**

 * 轮播

 */

export const RequestBanner = ():Promise<IResponse>=>{

    return axiosInstance({

        method:'get',

        url:'/api/banner'

    })

}

/**

 * 登录

 */

export const RequestLogin = (username:string,password:string):Promise<IResponse>=>{

    return axiosMockInstance({

        method:'post',

        // url:'/api/member/login',

        url:'/mock/account/login',

        data:{

            username,

            password

        }

    })

}

/**

 * 注册

 */

export const RequestRegister = (username:string,password:string,headerimg:string):Promise<IResponse>=>{

    return axiosMockInstance({

        method:'post',

        url:'/mock/account/register',

        data:{

            username,

            password,

            headerimg

        }

    })

}

.vue文件中引入使用即可

UI组件库elementplus

作者 RONIN
2026年4月27日 18:28

官网:www.he-fan.cn/zh-CN/

1. 安装

npm install element-plus --save安装elementplus

npm install -d unplugin-vue-components unplugin-auto-import安装两个插件自动按需引入

2. vite.config.js集成插件

import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    AutoImport({
        resolvers: [ElementPlusResolver()],
    }),
    Components({
        resolvers: [ElementPlusResolver()],
    }),
  ],
  resolve: {
    alias: {
      '@'fileURLToPath(new URL('./src', import.meta.url))
    }
  },

// 代理服务器
    // server: {
    //  proxy: {
    //      '/api': {
    //          target: 'http://43.136.34.132:8088', // 目标
    //          changeOrigin: true,
    //          // rewrite: path => path.replace(/^\/api/, ''),
    //      },
    //  },
    // },
})

3.main.js引入elementplus样式,否则可能出现使用组件没效果。


import { createApp } from "vue";
import router from "./router";
import store from "./store";
import 'element-plus/dist/index.css';//引入elementplus样式
import App from "./App.vue";
import './permission.js' //登录认证

import ElementPlus from 'element-plus';//完整引入elementplus,文件大小会很大。可以考虑按需引入

const app = createApp(App);
app.use(router);
app.use(store);
 
app.use(ElementPlus);//完整引入elementplus,文件大小会很大。可以考虑按需引入
app.mount("#app");

4.项目中使用组件示例

  • Layout网格布局组件;
  • form表单组件、button按钮组件、icon图标组件、ElMessage消息提示组件;
  • container布局容器、image图片组件、面包屑导航组件、dropdown下拉菜单、menu菜单组件;
  • table表格组件、dialog弹框组件、upload上传组件、popconfirm气泡确认框、pagination分页组件。

Layout网格布局组件:(默认每行分成24个栅栏)

1行1列

<el-row>
<el-col :span=”24”></el-col>
</el-row>

1行2列,row组件的gutter属性指定列之间的间距,默认0

<el-row :gutter=”20”>
  <el-col :span=”12”></el-col>
  <el-col :span=”12”></el-col>
</el-row>

1行2列,col组件的offset属性指定列偏移

<el-row>
  <el-col :span=”6”></el-col>
  <el-col :span=”12” :offset=”6”></el-col>
</el-row>

1行3列,默认flex布局,

使用justify属性定义对齐方式start/end/center/speace-between/speace-around/space-evenly

<el-row :justisy=”center”>
  <el-col :span=”6”></el-col>
  <el-col :span=”6”></el-col>
  <el-col :span=”6”></el-col>
</el-row>

form表单、button按钮组件、icon图标、ElMessage消息提示

  • <el-from>rules定义验证规则,:rules绑定验证规则,:model绑定数据
  • <el-from-item>绑定具体规则(prop属性设为需验证的特殊键值)
  • 输入框v-model双向数据绑定
  • <el-button type=’success’>success</el-button>
  • <el-icon>

npm install@element-plus/icons-vue下载图标包管理器

import { User, Lock } from '@element-plus/icons-vue'引入图标

components: {

        User,

        Lock,

 },注册后使用

  • import { ElMessage } from 'element-plus'

ElMessage({

       message: '成功了!',

       type: 'success',

})

Login.vue

<template>
    <div class="g-container">
        <div class="g-wrapper">
            <h2>xx系统</h2>
            <el-form
                class="g-login"
                :rules="rules"
                :model="user"
                ref="loginFormRef"
            >
                <el-form-item prop="name">
                    <el-input placeholder="请输入用户名" v-model="user.name">
                        <template #prefix>
                            <el-icon><User /></el-icon>
                        </template>
                    </el-input>
                </el-form-item>
                <el-form-item prop="password">
                    <el-input
                        placeholder="请输入密码"
                        v-model="user.password"
                        show-password
                    >
                        <template #prefix>
                            <el-icon><Lock /></el-icon>
                        </template>
                    </el-input>
                </el-form-item>
                <el-button type="primary" @click="bindLogin">登录</el-button>
            </el-form>
        </div>
    </div>
</template>

<script>
import { User, Lockfrom '@element-plus/icons-vue'
import { RequestLoginfrom '@/api/index.js'
import { ElMessagefrom 'element-plus'

export default {
    components: {
        User,
        Lock,
    },

    data() {
        return {
            user: {
                name'root',
                password'root',
            },

            // 定义校验规则,在data选项中,
            // 在form-item中使用prop绑定规则
            // 双向数据绑定 v-model
            // form单表中使用 :model="user"
            rules: {
                name: [
                    {
                        requiredtrue,
                        message'请输入用户名',
                        trigger'blur',
                    },
                ],
                password: [
                    { requiredtrue, message'请输入密码', trigger'blur' },
                ],
            },
        }
    },

    methods: {
        bindLogin() {
            const { name, password } = this.user
            const formRef = this.$refs.loginFormRef
            formRef.validate(async valid => {
                // 1. 表单校验
                if (valid) {
                    // 2. 调用登录接口,验证账户
                    const data = await RequestLogin(name, password)
                    const { resultCode, resultInfo } = data
                    if (resultCode === 1) {
                        // 3. 保存用户昵称和头像,用于主界面显示
                        const userInfo = {
                            nick: resultInfo.nick,
                            headerimg: resultInfo.headerimg,
                        }
                        this.$store.dispatch('member/saveUser', userInfo)
                        // 4. 保存token
                        // eslint-disable-next-line no-undef
                        localStorage.setItem('TOKEN', token)
                        // 5. 跳转到主界
                        this.$router.push({ path: '/home' })
                    } else {
                        ElMessage({
                            message'账号出错!',
                            type'error',
                        })
                    }
                }
            })
        },
    },
}
</script>

<style lang="scss" scoped>
.g-container {
    width: 100%;
    height: 100vh;
    background-color: #2b3c4d;
    position: relative;
    .g-wrapper {
        position: absolute;
        left: 50%;
        top: 50%;
        transform: translate(-50%, -50%);
        width: 400px;
        h2 {
            text-align: center;
            color: white;
            margin-bottom: 20px;
        }
        .g-login {
            background-color: white;
            border-radius: 5px;
            padding: 40px 30px;
            .el-button {
                width: 100%;
            }
        }
    }
}
</style>

story→index.js(vuex持久化存储插件,解决刷新用户信息丢失问题)

/* eslint-disable prettier/prettier */
import { createStore } from "vuex";
import member from "./modules/member.js";
import createPersistedState from "vuex-persistedstate";

const store = createStore({
  modules: {
    member,
  },

  // 集成插件
  plugins: [
    createPersistedState({
      storage: sessionStorage,
      key"storekey",
    }),
  ],
});
export default store;

story→modules→member.js


/* eslint-disable prettier/prettier */

const member = {
  namespacedtrue,
  // 内存中
  state: {
    // user:{name:'',password:''},
    usernull,
  },

  mutations: {
    SAVE_USER(state, _user) {
      state.user = _user; // 保存内存
      // localStorage.setItem('USER', JSON.stringify(_user)) // 持久化存储
    },
  },

  actions: {
    saveUser({ commit }, _user) {
      commit("SAVE_USER", _user);
    },
  },

  getters: {
    user(state) => state.user,
  },
};
export default member;

permission.js统一登录认证封装

import router from './router'
/**

 * 全局前置导航守卫
 *   路由router对象的一个方法 beforeEach
 */
// eslint-disable-next-line no-unused-vars

router.beforeEach((to, from) => {
    // 1. 加入白名单: 有些路由是不需要登录身份认证 path: /login ,  /

    // if (to.path === '/login') {
    //     return true //放行
    // }

    const whiteList = ['/login', '/']

    if (whiteList.includes(to.path)) {
        return true //放行
    }

    // 2. 登录认证,检查token

    let token = localStorage.getItem('TOKEN')

    if (token) {
        return true // 放行
    } else {
        // 如果不存在, 重定向到登录界面
        router.replace({ path'/login' })
        return false
    }
})

container布局容器(上中下布局、左右布局等)、image图片、面包屑导航、dropdown下拉菜单

  • <el-container>外层容器,子元素中含<el-header><el-footer>时全部子元素垂直上下排列,否则水平排列

  • <el-header>顶栏容器

  • <el-main>主要区域容器

  • <el-aside>侧边栏容器

  • <el-footer>底栏容器

  • <el-image :src="url"></el-image>

  • <el-breadcrumb separator-class="el-icon-arrow-right">

  • <el-breadcrumb-item :to="{ path: '' ,name:’’}">  

  • 收缩菜单功能:(图标改变、宽度改变)

<component :is="componentName"></component>动态组件切换图标

<el-menu :collapse="!isCollapse" >  通过 :collapse控制收缩(默认false打开,true收起)

 

  • <el-dropdown trigger=”click”>  trigger触发方式,默认移上去触发,也可设置为点击触发

<el-dropdown-menu>

<el-dropdown-item>

  • 可选链运算 ? (先判断前面的对象是否为真,如果为真,执行后面的点语法)

<p>欢迎您:{{ userInfo?.nick }}</p>

Home.vue

<template>

    <el-container>

        <!-- 左侧区域 -->

        <el-aside :width="asideWidth">

            <div class="g-title">

                <el-image :src="url"></el-image>

                <h3 v-show="isCollapse">xx管理</h3>

            </div>

            <p>首页</p>

            <Menu :isCollapse="isCollapse"></Menu>

        </el-aside>

        <!-- 右侧区域 -->

        <el-container>

            <!-- 头部区域 -->

            <el-header>

                <el-icon size="25" @click="bindCollapse">

                    <!-- <Fold v-if="isCollapse" @click="bindCollapse" />

                  <Expand v-else @click="bindCollapse"/>  -->

                    <component :is="componentName"></component>

                </el-icon>

 

                <div>

                    <el-dropdown>

                        <div class="g-header-r">

                           <!-- 可选链运算 ?. 先判断前面的对象是否为真,如果为真执行点语法-->

                            <p>欢迎您:{{ userInfo?.nick }}</p>

                            <el-image :src="userInfo?.headerimg"></el-image>

                            <!-- <p>欢迎您:{{ userInfo?userInfo.nick:'' }}</p>

                            <el-image :src="userInfo?userInfo.headerimg:''"></el-image> -->

                        </div>

                        <template #dropdown>

                            <el-dropdown-menu>

                                <el-dropdown-item>个人中心</el-dropdown-item>

                                <el-dropdown-item>切换用户</el-dropdown-item>

                                <el-dropdown-item @click="bindExit"

                                    >退出登录</el-dropdown-item

                                >

                            </el-dropdown-menu>

                        </template>

                    </el-dropdown>

                </div>

            </el-header>

            <!-- 内容区域 -->

            <el-main>

                <!-- 面包屑导航 -->

               <BreadCrumb></BreadCrumb>

                <!-- 子路由输出 -->

                <router-view></router-view>

            </el-main>

        </el-container>

    </el-container>

</template>

<script>

import Menu from '@/components/Menu.vue'

import BreadCrumb from '@/components/BreadCrumb.vue'

import { Fold, Expandfrom '@element-plus/icons-vue'

export default {

    components: {

        Menu,

        Fold,

        Expand,

        BreadCrumb,

    },

    data() {

        return {

            url'https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg',

            isCollapsetrue,

            asideWidth'200px',

            componentNameFold,

        }

    },

    computed: {

        userInfo() {

            return this.$store.getters['member/user']

        },

    },

    methods: {

        bindCollapse() {

            this.isCollapse = !this.isCollapse

            this.asideWidththis.isCollapse'200px' : '70px'

            this.componentNamethis.isCollapseFoldExpand

        },

        bindExit() {

            localStorage.remove('TOKEN')//退出登录清除token

            this.$router.replace({ path'/login' })

        },

    },

}

</script>

<!-- eslint-disable prettier/prettier -->

<style lang="scss" scoped>

.el-container {

    width: 100%;

    height: 100vh;

 

    .el-aside {

        background-color: #2d3436;

        color: white;

        transition: 0.5s;

        .g-title {

            display: flex;

            margin: 20px;

        }

        .el-image {

            width: 25px;

            height: 25px;

        }

        p {

            margin-left: 20px;

        }

        .el-menu {

            border-right: 0;

        }

    }

    .el-container {

        .el-header {

            display: flex;

            justify-content: space-between;

            padding: 10px 20px;

            background-color: #7f8fa6;

            height: 60px;

            .g-header-r {

                display: flex;

                p {

                    color: white;

                    // padding-top: 10px;

                }

                .el-image {

                    width: 30px;

                    height: 30px;

                }

            }

        }

        .el-breadcrumb {

            margin: 10px 0;

        }

    }

}

</style>

BreadCrumb.vue面包屑导航组件化

<!-- eslint-disable prettier/prettier -->
<template>
    <el-breadcrumb separator="/">
        <el-breadcrumb-item :to="{ path: '/index' }">首页</el-breadcrumb-item>
        <el-breadcrumb-item v-for="(item,index) in navigateList" :key="index" :to="{ path: item.path }">{{item.title}}</el-breadcrumb-item>
    </el-breadcrumb>
</template>

<!-- eslint-disable prettier/prettier -->
<script>
export default{
    data(){
        return{
            navigateList:[]
        }
    },

    watch:{
        // 路由变化,更新面包屑导航
        //侦听$route
        //获取matched数组中元素,更新面包屑导航
        $route(value){
            if(value.path === '/index'){
                this.navigateList = []
                return
            }
            // console.log(this.matched);
            // [{path:'/home1',title:'产品管理'},{path:'/product/list',title:'产品列表'},]
            const navigateList = value.matched.map(item =>{
                return{path:item.path,title:item.meta.title}
            })
            console.log(navigateList);
            this.navigateList = navigateList
        }
    }
}
</script>
<!-- eslint-disable prettier/prettier -->
<style lang="scss" scoped></style>

menu菜单组件:

<el-menu>菜单

<el-sub-menu index="1">二级菜单

<el-menu-item index="1-1">菜单项

Menu.vue

<template>

    <el-menu

        active-text-color="#ffd04b"

        background-color="#2d3436"

        text-color="#fff"

        :collapse="!isCollapse"

    >

        <el-sub-menu index="1">

            <template #title>

                <el-icon><Location /></el-icon> <span>产品管理</span>

            </template>

            <el-menu-item index="1-1">

                <el-icon><Handbag /></el-icon>

                <router-link to="/product/list">产品列表</router-link>

            </el-menu-item>

            <el-menu-item index="1-2">

                <el-icon><ReadingLamp /></el-icon>

                <router-link to="/product/category">产品分类</router-link>

            </el-menu-item>

            <el-menu-item index="1-3">

                <el-icon><ReadingLamp /></el-icon>

                <router-link to="/product/map">产品地图</router-link>

            </el-menu-item>

        </el-sub-menu>

 

        <el-sub-menu index="2">

            <template #title>

                <el-icon><OfficeBuilding /></el-icon><span>账户管理</span>

            </template>

            <el-menu-item index="2-1"

                ><el-icon><Mic /></el-icon

                ><router-link to="/account/list"

                    >账户列表</router-link

                ></el-menu-item

            >

            <el-menu-item index="2-2"

                ><el-icon><Camera /></el-icon

                ><router-link to="/account/add"

                    >账户添加</router-link

                ></el-menu-item

            >

        </el-sub-menu>

        <el-sub-menu index="3">

            <template #title>

                <el-icon><OfficeBuilding /></el-icon><span>OA管理</span>

            </template>

            <el-menu-item index="3-1"

                ><el-icon><Mic /></el-icon

                ><router-link to="/log/list"

                    >日志列表</router-link

                ></el-menu-item

            >

            <el-menu-item index="3-2"

                ><el-icon><Camera /></el-icon

                ><router-link to="/log/reply"

                    >日志回复</router-link

                ></el-menu-item

            >

            <el-menu-item index="3-3"

                ><el-icon><Camera /></el-icon

                ><router-link to="/log/add"

                    >日志添加</router-link

                ></el-menu-item

            >

        </el-sub-menu>

        <!-- <el-sub-menu index="1">

          <template #title>

              <el-icon><Location /></el-icon> <span>产品管理</span>

          </template>

          <el-menu-item

              :index="index"

              v-for="(menu, index) in menuList"

              :key="index"

              ><el-icon><Handbag /></el-icon

              ><router-link :to="menu.path">{{

                  menu.meta.title

              }}</router-link></el-menu-item

          >

      </el-sub-menu> -->

    </el-menu>

</template>

<script>

import {

    Location,

    Handbag,

    ReadingLamp,

    Camera,

    Mic,

    OfficeBuilding,

} from '@element-plus/icons-vue'

export default {

    props: {

        isCollapseBoolean,

    },

    components: { Location, Handbag, ReadingLamp, Camera, Mic, OfficeBuilding },

    computed: {

        menuList() {

            const list = this.$router.options.routes

            const menu = list[2].children

            const list1 = menu.filter(item => item.meta.hidden)

            return list1

        },

    },

}

</script>

<style lang="scss" scoped></style>

router→index.js

import { createRouter, createWebHistory } from 'vue-router'

import Home from '../views/Home.vue'

import Login from '../views/Login.vue'

import Index from '../views/Index.vue'

 

const router = createRouter({

    history: createWebHistory(import.meta.env.BASE_URL),

    routes: [

        {

            path: '/',

            redirect: '/login',

        },

        {

            path: '/login',

            component: Login,

        },

        {

            path: '/home',

            component: Home,

            redirect: '/index',

            children: [

                {

                    path: '/index',

                    component: Index,

                    meta: { hidden: false },

                },

            ],

        },

         // 产品管理模块

        {

            path: '/home1',

            meta: { title: '产品管理' },

            component: Home,

            children: [

                {

                    path: '/product/list',

                    component: () =>

                        import('../views/plateform/product/list.vue'),

                    meta: { hidden: true, title: '产品列表' },

                },

                {

                    path: '/product/category',

                    component: () =>

                        import('../views/plateform/product/category.vue'),

                    meta: { hidden: true, title: '产品分类' },

                },

                {

                    path: '/product/map',

                    component: () =>

                        import('../views/plateform/product/map.vue'),

                    meta: { hidden: true, title: '产品地图' },

                },

            ],

        },

        // 系统设置

        {

            path: '/home2',

            meta: { title: '账户管理' },

            component: Home,

            children: [

                {

                    path: '/account/list',

                    component: () =>

                        import('../views/plateform/account/list.vue'),

                    meta: { hidden: true, title: '账户列表' },

                },

                {

                    path: '/account/add',

                    component: () =>

                        import('../views/plateform/account/add.vue'),

                    meta: { hidden: true, title: '添加账户' },

                },

            ],

        },

        // oa管理

        {

            path: '/home3',

            meta: { title: 'OA管理' },

            component: Home,

            children: [

                {

                    path: '/log/list',

                    component: () => import('../views/plateform/log/list.vue'),

                    meta: { hidden: true, title: '日志管理' },

                },

                {

                    path: '/log/reply',

                    component: () => import('../views/plateform/log/reply.vue'),

                    meta: { hidden: true, title: '日志回复' },

                },

                {

                    path: '/log/add',

                    component: () => import('../views/plateform/log/add.vue'),

                    meta: { hidden: true, title: '添加日志' },

                },

            ],

        },

        // 404路由不存在匹配,放在路由最下面

        {

            path: '/:pathMatch(.*)*',

            component: () => import('@/views/NotPage.vue'),

        },

    ],

})

export default router

table表格组件、dialog弹框组件、upload上传组件、popconfirm气泡确认框组件、pagination分页组件

  • <el-table :data="goodsList" @selection-chanege=”onchange”>@selection-change拿到复选框选中的数据(批量删除)
  • <el-table-column prop=”date” label=”Date” type=”selection”>表格列,prop对应键名填入数据,label表格列名,type表格复选框(批量删除)
  • <el-dialog v-model=”” ttitle=””>设v-mode接收boolean,true是显示弹框
  • <el-upload>图片上传
  • <el-popconfirm>气泡确认框
  • <el-pagination>分页

增删改查功能

List.vue

<!-- eslint-disable prettier/prettier -->
<template>

    <div> 

         <!-- 搜索 -->

        <el-row :gutter="20" style="margin-bottom: 10px">

            <el-col :span="4" :offset="0">

                <el-input v-model="product.name" placeholder="名称搜索" clearable></el-input>

            </el-col>

            <el-col :span="4" :offset="0">

                <el-input v-model="product.shop" placeholder="店铺名称搜索" clearable></el-input>

            </el-col>

            <el-col :span="4" :offset="0">

                <el-input v-model="product.startPrice" placeholder="开始价格搜索" clearable></el-input>

            </el-col>

            <el-col :span="4" :offset="0">

                <el-input v-model="product.endPrice" placeholder="结束价格搜索" clearable></el-input>

            </el-col>

            <el-col :span="4" :offset="0">

                <el-button type="primary" @click="bindSearch">搜索产品</el-button>

            </el-col>

 

        </el-row>

        <el-button-group>

            <el-button type="success" size="small" @click="bindAddGood">添加</el-button>

            <el-button type="success" size="small" @click="bindRefresh">刷新</el-button>

            <el-button type="success" size="small" @click="bindBatchDelete">批量删除</el-button>

            <el-button type="success" size="small" @click="bindExcelExport">导出excel</el-button>

        </el-button-group>

        <!-- 表格 -->

        <el-table :data="goodsList" style="width: 100%" @selection-change="handleSelectionChange">

            <el-table-column type="selection" ></el-table-column>

            <el-table-column label="序列号" prop="id" width="100"></el-table-column>

            <el-table-column label="名称" prop="product"></el-table-column>

            <el-table-column label="店铺名称" prop="shop"></el-table-column>

            <el-table-column label="图片">

                <template #default="scope">

                    <!-- <el-image

                        :src="scope.row.picture?.indexOf('http') === -1 ? 'http://10.7.163.142:8089/' + scope.row.picture : scope.row.picture"

                        style="width: 100px; height: 100px"></el-image> -->

                    <el-image :src="filterUrl(scope.row.picture)" style="width: 100px; height: 100px"></el-image>

                </template>

            </el-table-column>

            <el-table-column label="价格" prop="price"></el-table-column>

            <el-table-column label="类型" prop="categoryname"></el-table-column>

            <el-table-column label="操作">

                <template #default="scope">

                    <el-button type="success" size="small" @click="bindEdit(scope.row)">编辑</el-button>

                    <!-- <el-button type="primary" size="small" @click="bindDelete(scope.row.id)">删除</el-button> -->

                    <el-popconfirm title="确认要删除此记录吗?" @confirm="bindDelete(scope.row.id)" confirm-button-text="Yes"

                        cancel-button-text="No">

                        <template #reference>

                            <el-button type="primary" size="small">删除</el-button>

                        </template>

                    </el-popconfirm>

                </template>

            </el-table-column>

        </el-table>

        <!-- 分页 -->

        <el-pagination background layout="total, sizes, prev, pager, next,jumper" :total="total"

            :page-sizes="[5, 10, 20]" @size-change="handleSizeChange" @current-change="handleCurrentChange" />

        <!-- total-总记录条数

        sizes-选择每页几条

        :page-sizes="[5,10,20]"

        jumper-跳转

        @size-change="handleSizeChange"//page-size 改变时触发

         -->

        <!-- 弹出对话框 -->

        <el-dialog :title="type === 'ADD' ? '添加产品' : '编辑产品'" v-model="dialogGoodsFormVisible" width="40%">

            <GoodsDialog v-if="dialogGoodsFormVisible" @close="bindClose" :goods="goods" :type="type"></GoodsDialog>

        </el-dialog>

    </div>

</template>

<!-- eslint-disable prettier/prettier -->

<script>

import { RequestShopList, RequestDeleteGoods, RequestBatchDeletefrom '@/api/index.js'

import GoodsDialog from '@/components/GoodsDialog.vue'

import { ElMessagefrom 'element-plus'

import { excelExport2 } from '@/utils/xlsxutil.js'

export default {

    components: {

        GoodsDialog,

    },

    data() {

        return {

            // tableData: [

            //     { id:1,name: '回锅肉', price: 30, category: '荤', url: 'https://image5.suning.cn/uimg/b2c/newcatentries/0000000000-000000000834870991_1_800x800.jpg' },

            //     { id:2,name: '土豆丝', price: 20, category: '素', url: 'https://image5.suning.cn/uimg/b2c/newcatentries/0000000000-000000000834870991_1_800x800.jpg' },

            // ]

            goodsList: [],

            dialogGoodsFormVisiblefalse,

            total'',//总记录条数

            pageSize5,//每页记录条数

            pageNo1,//当前页号

            type'ADD',//EDIT 编辑 ADD 添加

            goodsnull,

            product: {

                name'',//搜索产品名称

                shop'',//搜索店铺名称

                price'',//搜索价格

                startPrice'',

                endPrice'',

            },

            ids'',//删除商品id集合

        }

    },

    created() {

        this.getShopList()

    },

    methods: {

        filterUrl(url) {

            return url?.indexOf('http') === -1 ? 'http://10.7.163.142:8089/' + url : url

        },

        /**

         * 产品列表

         */

        async getShopList() {

            const data = await RequestShopList(this.pageSize, this.pageNo, this.product.name, this.product.shop, this.product.startPrice, this.product.endPrice)

            const { resultCode, resultInfo } = data

            if (resultCode === 1) {

                this.goodsList = resultInfo.list

                this.total = resultInfo.total// 总记录条数

            }

        },

        /**

         * 编辑产品

         */

        async bindEdit(row) {

            // console.log('row ', row)

            this.goods = row

            this.type'EDIT'

            this.dialogGoodsFormVisibletrue

        },

        /**

         * 删除商品

         */

        async bindDelete(id) {

            const data = await RequestDeleteGoods(id)

            const { resultCode } = data

            if (resultCode === 1) {

                ElMessage({

                    message'删除成功',

                    type'success',

                })

                this.getShopList()

            }

        },

        /**

         * 添加-弹出添加表单对话框

         */

        bindAddGood() {

            this.type'ADD'

            this.dialogGoodsFormVisibletrue

        },

        /**

        * 页大小改变事件

        */

        handleSizeChange(value) {

            this.pageSize = value

            this.getShopList()

        },

        /**

         * 页号改变事件

         */

        handleCurrentChange(value) {

            this.pageNo = value

            this.getShopList()

        },

        /**

         * 刷新

         */

        bindRefresh() {

            this.product = {}//重置搜索数据

            this.getShopList()

        },

        bindClose() {

            this.dialogGoodsFormVisiblefalse

            this.getShopList()

        },

        /**

         * 搜索产品

         */

        bindSearch() {

            this.getShopList()

        },

        /*

        多选

         */

        handleSelectionChange(value) {

            // console.log('value ', value) // [{id:10,name:''}] => [10,12,34] => '10,12,34'

            const list = value.map(item => item.id)

            const ids = list.join(',')

            this.ids = ids

        },

        /*

        批量删除

         */

        async bindBatchDelete() {

            if (this.ids?.split(',').length <= 0) {

                ElMessage({

                    message'请选择删除产品',

                    type'info',

                })

                return

            }

            const data = await RequestBatchDelete(this.ids)

            const { resultCode } = data

            if (resultCode === 1) {

                ElMessage({

                    message'批量删除成功',

                    type'success',

                })

                this.getShopList()

            }

        },

        /**

         * 导出excel

         */

         bindExcelExport() {

            excelExport2(

                this.goodsList,

                {

                    id'序列号',

                    product'产品名称',

                    shop'店铺名称',

                    picture'图片',

                    price'价格',

                    oldprice'原价',

                    categoryname'类型名称',

                },

                '产品列表'

            )

        },

    }

}

</script>

<!-- eslint-disable prettier/prettier -->

<style lang="scss" scoped>

</style>

GoodsDialog.vue

<!-- eslint-disable prettier/prettier -->

<template>

    <el-form :model="goodForm" ref="goodFormRef" :rules="rules" label-width="80px">

        <el-form-item label="店铺名称">

            <el-input v-model="goodForm.shop"></el-input>

        </el-form-item>

        <el-form-item label="产品名称" prop="product">

            <el-input v-model="goodForm.product"></el-input>

        </el-form-item>

        <el-form-item label="产品分类">

            <el-select v-model="goodForm.categoryId" clearable placeholder="选择产品分类">

                <el-option v-for="item in category" :key="item.id" :label="item.name" :value="item.id" />

            </el-select>

        </el-form-item>

        <el-form-item label="现价">

            <el-input v-model="goodForm.price"></el-input>

        </el-form-item>

        <el-form-item label="原价">

            <el-input v-model="goodForm.oldprice"></el-input>

        </el-form-item>

        <el-form-item label="图片">

            <!--

                list-type: picture-card 卡片样式

                action: 图片上传url地址

                auto-upload: 是否自动上传,true自动上传,选中图片直接上传到action指定url

                show-file-list: false单文件上传,true支持多文件上传

                before-upload: 上传前回调, 当前auto-upload为true时执行,回调函数中返回false终止上传

             -->

            <!-- <el-input v-model="goodForm.picture"></el-input> -->

            <el-upload list-type="picture-card" action="#" :auto-upload="true" :show-file-list="false"

                :before-upload="beforeAvatarUpload">

                <img v-if="imageUrl" :src="imageUrl" class="avatar" />

                <el-icon v-else>

                    <Plus />

                </el-icon>

            </el-upload>

        </el-form-item>

        <el-form-item label="上下架">

            <el-radio-group v-model="goodForm.putaway">

                <el-radio :label="1">上架</el-radio>

                <el-radio :label="0">下架</el-radio>

            </el-radio-group>

        </el-form-item>

        <el-form-item>

            <el-button type="primary" @click="onSubmit">确定</el-button>

            <el-button type="primary" @click="onClose">关闭</el-button>

        </el-form-item>

    </el-form>

</template>

<!-- eslint-disable prettier/prettier -->

<script>

import {

    RequestCategoryList,

    RequestAddGoods,

    RequestUpdateGoods,

} from '@/api/index.js'

import { ElMessagefrom 'element-plus'

import { Plusfrom '@element-plus/icons-vue'

export default {

    components: {

        Plus,

    },

    props: ['goods', 'type'],

    emits: ['close'],

    data() {

        return {

            goodForm: {

                shop'',

                product'',

                price'',

                oldprice'',

                picture'',

                putaway1,

                categoryId'',

            },

            category: [

                // { label: '海鲜', value: 1 },

                // { label: '川湘菜', value: 2 },

                // { label: '日韩料理', value: 3 },

            ],

            rules: {

                product: [

                    {

                        requiredtrue,

                        message'请输入产品名称',

                        trigger'blur',

                    },

                ],

            },

            imageUrl'', //图片预览地址

            imageFilenull, //上传图片文件

        }

    },

    created() {

        if (this.type === 'EDIT') {

            this.goodForm = { ...this.goods }

            this.imageUrlthis.filterUrl(this.goodForm.picture)

        }

        this.getCategoryList()

    },

    methods: {

        filterUrl(url) {

            return url?.indexOf('http') === -1

                ? 'http://10.7.163.142:8089/' + url

                : url

        },

        /**

         * 分类列表

         */

        async getCategoryList() {

            const data = await RequestCategoryList()

            const { resultCode, resultInfo } = data

            if (resultCode === 1) {

                this.category = resultInfo.list

            }

        },

        /**

         * 保存产品

         *  图片上传http注意事项

         *     1. post请求

         *     2. 请求头header 上传内容类型 content-type:multipart/form-data

         *         headers: { 'Content-Type': 'multipart/form-data' },

         *     3. FormData

         *         const formData = new FormData()

         *         formData.append('product',product)

         *         formData.append('picture',file)

         */

        async onSubmit() {

            // if (this.type === 'ADD') {

            //     // console.log('this.goodForm ', this.goodForm)

            //     const formData = new FormData()

            //     formData.append('categoryId', this.goodForm.categoryId)

            //     formData.append('product', this.goodForm.product)

            //     formData.append('picture', this.imageFile)

            //     formData.append('shop', this.goodForm.shop)

            //     formData.append('price', this.goodForm.price)

            //     formData.append('oldprice', this.goodForm.oldprice)

            //     formData.append('putaway', this.goodForm.putaway)

            //     const data = await RequestAddGoods(formData)

            //     const { resultCode } = data

            //     if (resultCode === 1) {

            //         ElMessage({

            //             message: '添加产品成功!',

            //             type: 'success',

            //         })

            //     }

            //     this.$emit('close')

            // } else {

            //     const formData = new FormData()

            //     formData.append('id', this.goodForm.id)

            //     formData.append('categoryId', this.goodForm.categoryId)

            //     formData.append('product', this.goodForm.product)

            //     formData.append('picture', this.imageFile)

            //     formData.append('shop', this.goodForm.shop)

            //     formData.append('price', this.goodForm.price)

            //     formData.append('oldprice', this.goodForm.oldprice)

            //     formData.append('putaway', this.goodForm.putaway)

            //     const data = await RequestUpdateGoods(formData)

            //     const { resultCode } = data

            //     if (resultCode === 1) {

            //         ElMessage({

            //             message: '编辑产品成功!',

            //             type: 'success',

            //         })

            //     }

            //     this.$emit('close')

            // }

            // console.log('this.goodForm ', this.goodForm)

            const formData = new FormData()

            formData.append('categoryId', this.goodForm.categoryId)

            formData.append('product', this.goodForm.product)

            formData.append('picture', this.imageFile)

            formData.append('shop', this.goodForm.shop)

            formData.append('price', this.goodForm.price)

            formData.append('oldprice', this.goodForm.oldprice)

            formData.append('putaway', this.goodForm.putaway)

            // 添加  编辑

            let data

            if (this.type === 'EDIT') {

                formData.append('id', this.goodForm.id)

                data = await RequestUpdateGoods(formData)

            } else {

                data = await RequestAddGoods(formData)

            }

            const { resultCode } = data

            if (resultCode === 1) {

                ElMessage({

                    messagethis.type === 'EDIT' ? '编辑产品成功!' : '添加产品成功!',

                    type'success',

                })

            }

            this.onClose()

        },

        onClose() {

            this.$emit('close')

        },

        /**

         * 文件上传之前执行

         */

        beforeAvatarUpload(rawFile) {

            const arr = ['image/jpeg', 'image/png', 'image/jpg']

            // 图片格式验证

            if (!arr.includes(rawFile.type)) {

                ElMessage({

                    message'上传图片格式不正确!!',

                    type'error',

                })

                return false

            }

            // 图片大小验证

            if (rawFile.size1024 / 1024 > 2) {

                ElMessage({

                    message'上传图片大小不能超过2M!',

                    type'error',

                })

                return false

            }

            // 图片预览

            //1. 选中的本地图片转成Base64编码 赋值给 imageUrl

            //2. FileReader 读文件

            this.imageUrlURL.createObjectURL(rawFile)

            // 上传图片

            this.imageFile = rawFile

            return false // 不向下执行

        },

    },

}

</script>

<!-- eslint-disable prettier/prettier -->
<style lang="scss" scoped>

.avatar {
    width: 170px;
    height: 170px;
    display: block;
}

</style>

util→request.js

import axios from 'axios'

import { ElMessage } from 'element-plus'

 

const instance = axios.create({

    baseURL'http://10.7.163.142:8089', //服务器根地址

    // baseURL: 'http://127.0.0.1:5173', //服务器根地址

    timeout3000, //超时时间

})

/**

 * 请求拦截器

 */

instance.interceptors.request.use(

    config => {

        // 请求拦截处理

        // console.log('请求拦截处理 >>> ', config)

        const token = localStorage.getItem('TOKEN')

        if(token){

            config.headers['Authorization'] = token

        }

        // token && config.headers['Authorization'] = 'token'

        return config

    },

    error => {

        // 请求出错处理

        return Promise.reject(error)

    }

)

/**

 * 响应拦截器

 */

instance.interceptors.response.use(

    response => {

        // 响应拦截处理

        // console.log('响应拦截器处理 >>> ', response)

        // return response

        return response.data

    },

    error => {

        const { response } = error

        if (response) {

            const status = response.status

            switch (status) {

                case 404:

                    // console.log('资源不存在 404')

                    ElMessage({

                        message'资源不存在 404',

                        type'error',

                    })

                    break

                case 401:

                    // console.log('Unauthorized 身份验证凭证缺失!')

                    ElMessage({

                        message'Unauthorized 身份验证凭证缺失!',

                        type'error',

                    })

                    break

                case 403:

                    // console.log('403 Forbidden - 拒绝访问!')

                    ElMessage({

                        message'403 Forbidden - 拒绝访问!',

                        type'error',

                    })

                    break

                case 500:

                    // console.log('服务器出错!')

                    ElMessage({

                        message'服务器出错!',

                        type'error',

                    })

                    break

                default:

                    // console.log('出现异想不到的错误!')

                    ElMessage({

                        message'出现异想不到的错误!',

                        type'error',

                    })

                    break

            }

        } else {

            // 说明服务器连结果都没有返回,可能的原因有两种:

            /**

             * 1. 服务器崩掉了

             * 2. 前端客户端断网状态

             */

            if (!window.navigator.onLine) {

                // 判断为断网,可以跳转到断网页面

                // console.log('网络不可用,请检查您的网络连接!')

                ElMessage({

                    message'网络不可用,请检查您的网络连接!',

                    type'error',

                })

                return

            } else {

                // console.log('连接服务端出错!' + error?.message)

                ElMessage({

                    message'连接服务端出错!' + error?.message,

                    type'error',

                })

                return Promise.reject(error)

            }

        }

        return Promise.reject(error)

    }

)

export default instance

api→index.js

/* eslint-disable prettier/prettier */

import instance from "@/utils/request.js";

/**

 * 登录接口

 * @param {*} username

 * @param {*} password

 * @returns

 */

export const RequestLogin = (username, password) => {

  return instance({

    method"post",

    url"/api/login",

    //post请求参数使用data选项, get参数 params选项

    data: {

      username,

      password,

    },

  });

};

 

/**

 * 商品列表接口

 * @returns

 */

export const RequestShopList = (pageSize,pageNo,productKey,shopKey,startPrice,endPrice) => {

  return instance({

    method"get",

    url"/api/shop",

    //post请求参数使用data选项, get参数 params选项

    params:{

      pageSize,

      pageNo,

      productKey,//商品名

      shopKey,//产品名

      startPrice,

      endPrice

    }

  });

};

/**

 * 商品分类接口

 * @returns

 */

export const RequestCategoryList = () => {

  return instance({

    method"get",

    url"/api/category",

  });

};

/**

 * 添加产品

 */

export const RequestAddGoods = (formData)=>{

  // const token = localStorage.getItem('TOKEN')

  return instance({

      method:'post',

      url:'/api/shop/insert',

      headers: { 'Content-Type''multipart/form-data' },

      data:formData

  })

}

/**

 * 删除产品

 */

export const RequestDeleteGoods = (id)=>{

  // const token = localStorage.getItem('TOKEN')

  return instance({

      method:'get',

      url:'/api/shop/delete',

      params:{

        id

      }

  })

}

/**

 * 编辑产品

 */

export const RequestUpdateGoods = (formData)=>{

  // const token = localStorage.getItem('TOKEN')

  return instance({

      method:'post',

      url:'/api/shop/edit',

      headers: { 'Content-Type''multipart/form-data' },

      data:formData

  })

}

/**

 * 批量删除产品

*  ids: 商品id '1,2,3,4'

 */

export const RequestBatchDelete = (ids)=>{

  // const token = localStorage.getItem('TOKEN')

  return instance({

      method:'get',

      url:'/api/shop/batchdelete',

      params:{

        ids

      }

  })

}

❌
❌