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, Lock } from '@element-plus/icons-vue'
import { RequestLogin } from '@/api/index.js'
import { ElMessage } from '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: [
{
required: true,
message: '请输入用户名',
trigger: 'blur',
},
],
password: [
{ required: true, 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 = {
namespaced: true,
// 内存中
state: {
// user:{name:'',password:''},
user: null,
},
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, Expand } from '@element-plus/icons-vue'
export default {
components: {
Menu,
Fold,
Expand,
BreadCrumb,
},
data() {
return {
url: 'https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg',
isCollapse: true,
asideWidth: '200px',
componentName: Fold,
}
},
computed: {
userInfo() {
return this.$store.getters['member/user']
},
},
methods: {
bindCollapse() {
this.isCollapse = !this.isCollapse
this.asideWidth = this.isCollapse ? '200px' : '70px'
this.componentName = this.isCollapse ? Fold : Expand
},
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: {
isCollapse: Boolean,
},
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, RequestBatchDelete } from '@/api/index.js'
import GoodsDialog from '@/components/GoodsDialog.vue'
import { ElMessage } from '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: [],
dialogGoodsFormVisible: false,
total: '',//总记录条数
pageSize: 5,//每页记录条数
pageNo: 1,//当前页号
type: 'ADD',//EDIT 编辑 ADD 添加
goods: null,
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.dialogGoodsFormVisible = true
},
/**
* 删除商品
*/
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.dialogGoodsFormVisible = true
},
/**
* 页大小改变事件
*/
handleSizeChange(value) {
this.pageSize = value
this.getShopList()
},
/**
* 页号改变事件
*/
handleCurrentChange(value) {
this.pageNo = value
this.getShopList()
},
/**
* 刷新
*/
bindRefresh() {
this.product = {}//重置搜索数据
this.getShopList()
},
bindClose() {
this.dialogGoodsFormVisible = false
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 { ElMessage } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
export default {
components: {
Plus,
},
props: ['goods', 'type'],
emits: ['close'],
data() {
return {
goodForm: {
shop: '',
product: '',
price: '',
oldprice: '',
picture: '',
putaway: 1,
categoryId: '',
},
category: [
// { label: '海鲜', value: 1 },
// { label: '川湘菜', value: 2 },
// { label: '日韩料理', value: 3 },
],
rules: {
product: [
{
required: true,
message: '请输入产品名称',
trigger: 'blur',
},
],
},
imageUrl: '', //图片预览地址
imageFile: null, //上传图片文件
}
},
created() {
if (this.type === 'EDIT') {
this.goodForm = { ...this.goods }
this.imageUrl = this.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({
message: this.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.size / 1024 / 1024 > 2) {
ElMessage({
message: '上传图片大小不能超过2M!',
type: 'error',
})
return false
}
// 图片预览
//1. 选中的本地图片转成Base64编码 赋值给 imageUrl
//2. FileReader 读文件
this.imageUrl = URL.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', //服务器根地址
timeout: 3000, //超时时间
})
/**
* 请求拦截器
*/
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
}
})
}