后台管理项目
贾成豪
潘家成 element-ui贡献者 vue-admin-template
后台管理项目介绍
特点
一般是没有注册只有登录[员工的账号与密码,boss:超级管理员]
项目结构一般左右结构
增删改查的业务
用户访问权限[菜单、按钮]
数据大屏
数据大屏项目 政府-
项目模版
使用vue创建项目,在vue项目的基础上再次进行封装,封装成一个项目模版,使用同一类的项目时,直接套用。
Vue2模版:vue-admin-template https://gitee.com/panjiachen/vue-admin-template.git
Vue3模版:vue-admin-template https://gitee.com/jch1011/vue3_admin_project.git
Vue3模版(已完成):vue-admin-template https://gitee.com/jch1011/cd_vue3_project
git clone git@gitee.com:jch1011/cd_vue3_project.git
https://www.bilibili.com/video/BV1Xh411V7b5/?spm_id_from=333.337.search-card.all.click
使用过程
# 首先克隆下来
git clone vue2模版
# 安装依赖
npm install -f (强制安装)
# 单独安装依赖
npm i core-js
阅读代码
从项目的入口文件开始阅读
代码注释
对代码进行注释
API在线文档
代理服务器地址
http://sph-h5-api.atguigu.cn
在线文档API
http://39.98.123.211:8510/swagger-ui.html(业务)
http://39.98.123.211:8170/swagger-ui.html(用户)
商品管理路由
创建商品管理相关路由
新增商品管理路由页面:
- attr管理:src/views/product/attr/index.vue
<template>
<div>
我是attr
</div>
</template>
<script setup lang='ts'>
</script>
<style scoped lang="less">
</style>
- spu管理:src/views/product/spu/index.vue
<template>
<div>
我是spu
</div>
</template>
<script setup lang='ts'>
</script>
<style scoped lang="less">
</style>
- trademark品牌管理:src/views/product/trademark/index.vue
<template>
<div>
我是trademark
</div>
</template>
<script setup lang='ts'>
</script>
<style scoped lang="less">
</style>
- sku管理:src/views/product/trademark/index.vue
<template>
<div>
我是sku
</div>
</template>
<script setup lang='ts'>
</script>
<style scoped lang="less">
</style>
新增商品管理模块路由:
新增路由页面配置:src/router/router.ts
# 每一项路由 都需要配置 name属性 (首字母大写)
import type { RouteRecordRaw } from 'vue-router';
/**
* 路由meta对象参数说明
* meta: {
* title: 菜单栏及 tagsView 栏、菜单搜索名称(国际化)
* hidden: 是否隐藏此路由
* icon: 菜单、tagsView 图标,阿里:加 `iconfont xxx`,fontawesome:加 `fa xxx`
* }
*/
/**
* 静态路由(默认路由)
*/
// 一级路由 404 login /
// 二级路由 /home
export const staticRoutes: Array<RouteRecordRaw> = [
{
// 路径全是小写的
path: '/login',
name: 'Login',
component: () => import('@/views/login/index.vue'),
// 路由元数据
meta: {
hidden: true
}
},
{
path: '/404',
name: '404',
component: () => import('@/views/error/404.vue'),
meta: {
hidden: true
}
},
{
path: '/',
component: () => import('@/layout/index.vue'),
redirect: '/home',
children: [{
path: 'home',
name: 'Home',
component: () => import('@/views/home/index.vue'),
meta: {
title: '首页',
icon: 'ele-HomeFilled',
}
}]
},
// 商品管理模块,一级路由
{
path: '/product', // 一级路由展示 layout组件
component: () => import('@/layout/index.vue'),
name:'Product',
meta:{
title:'商品管理',
icon:'ele-Goods'
},
children:[
{
path:'trademark',
component:()=>import('@/views/product/trademark/index.vue'),
name:'Trademark',
meta:{
title:'品牌管理',
icon:'ele-Apple'
}
},
{
path:'attr',
component:()=>import('@/views/product/attr/index.vue'),
name:'Attr',
meta:{
title:'属性管理',
icon:'ele-IceTea'
}
},
{
path:'spu',
component:()=>import('@/views/product/spu/index.vue'),
name:'Spu',
meta:{
title:'SPU管理',
icon:'ele-Burger'
}
},
{
path:'sku',
component:()=>import('@/views/product/sku/index.vue'),
name:'Sku',
meta:{
title:'SKU管理',
icon:'ele-Goblet'
}
}
]
},
/* 匹配任意的路由 必须最后注册 */
{
path: '/:pathMatch(.*)',
name: 'Any',
redirect: '/404',
meta: {
hidden: true
}
}
];
/**
* 定义动态路由
*/
export const allAsyncRoutes: Array<RouteRecordRaw> = [];
layout组件
用于一级路由页面的布局和渲染
layout是布局组件,是已经封装好的,当你配置了路由,Sidebar就自动的把这些路由渲染到了侧边栏,不需要自己再手动导入到侧边栏,渲染侧边栏。
登陆页面
设置代理跨域
设置vite构建工具的配置 vite.config.ts
https://cn.vitejs.dev/config/server-options.html
export default defineConfig((mode: ConfigEnv) => {
const env = loadEnv(mode.mode, process.cwd());
return {
plugins: [vue(), vueJsx()],
resolve: {}
css: {},
server:{
proxy:{
'/app-dev': {
target: 'http://sph-h5-api.atguigu.cn',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/app-dev/, ''),
},
}
}
};
});
重启服务
npm run dev
表单验证步骤
form组件表单表单校验
1 form组件添加model属性->告诉form组件表单的数据收集在那个对象的身上
2 form组件添加rules属性->书写表单校验的规则
3 el-form-item表单相组件添加prop属性,属性值即为校验字段的名字
新增路由组件
登陆组件:src/views/login.vue
<template>
<div class="login-container">
<el-form ref="formRef" :model="loginForm" :rules="loginRules" class="login-form" auto-complete="on" label-position="left">
<div class="title-container">
<h3 class="title">尚品汇后台管理</h3>
</div>
<el-form-item prop="username">
<span class="svg-container">
<svg-icon name="ele-UserFilled" />
</span>
<el-input ref="username" v-model="loginForm.username" placeholder="Username" name="username" type="text" tabindex="1" auto-complete="on" />
</el-form-item>
<el-form-item prop="password">
<span class="svg-container">
<svg-icon name="ele-Lock" />
</span>
<el-input :key="passwordType" ref="passwordRef" v-model="loginForm.password" :type="passwordType" placeholder="Password" name="password" tabindex="2" auto-complete="on" @keyup.enter.native="handleLogin" />
<span class="show-pwd" @click="showPwd">
<svg-icon :name="passwordType === 'password' ? 'ele-Hide' : 'ele-View'" />
</span>
</el-form-item>
<el-button :loading="loading" type="primary" style="width:100%;margin-bottom:30px;height: 40px;" @click.native.prevent="handleLogin">登 陆</el-button>
</el-form>
</div>
</template>
<script lang="ts">
export default {
name: 'Login'
}
</script>
<script lang="ts" setup>
import { useUserInfoStore } from '@/stores/userInfo'
import type { FormInstance } from 'element-plus'
import { nextTick, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
// 获取用户相关的小仓库
const userInfoStore = useUserInfoStore()
const route = useRoute()
const router = useRouter()
const loginForm = ref({
username: 'admin',
password: '111111'
})
const loading = ref(false)
// 密码表单元素type属性需要响应式数据:默认密码
const passwordType = ref('password')
const redirect = ref('')
// 获取密码选择框的实例
const passwordRef = ref<HTMLInputElement>()
const formRef = ref<FormInstance>()
const validateUsername = (rule: any, value: any, callback: any) => {
if (value.length < 4) {
// 提示信息并放行
callback(new Error('用户名长度不能小于4位'))
} else {
// 放行
callback()
}
}
const validatePassword = (rule: any, value: any, callback: any) => {
if (value.length < 6) {
callback(new Error('密码长度不能小于6位'))
} else {
callback()
}
}
const loginRules = {
username: [{ required: true, validator: validateUsername }],
password: [{ required: true, trigger: 'blur', validator: validatePassword }]
}
watch(route,() => {
redirect.value = route.query && (route.query.redirect as string)
},
{ immediate: true }
)
/*
切换密码的显示/隐藏
*/
const showPwd = () => {
if (passwordType.value === 'password') {
passwordType.value = 'text'
} else {
passwordType.value = 'password'
}
// 数据改变时dom立即更新
nextTick(() => {
// ? ts中的语法,当passwordRef.value存在时,执行passwordRef.value.focus(),不存在时不执行
passwordRef.value?.focus()
})
}
/*
点击登陆的回调
*/
const handleLogin = async () => {
// form表单验证通过之后再发送请求
await formRef.value?.validate()
loading.value = true
const { username, password } = loginForm.value
try {
// 用户小仓库执行login方法登陆
await userInfoStore.login(username, password)
// 编程式跳转路由
router.push({ path: redirect.value || '/' })
} finally {
loading.value = false
}
}
</script>
定义数据类型
src/api/user/type/index.ts
// 登陆接口 参数类型
export interface LoginParams {
password: string,
username: string
}
// 登陆接口 响应类型
export interface LoginResponseData{
token: string
}
// 用户信息 响应类型
export interface UserInfoResponseData{
name:string,
avatar:string,
roles:string[],
routes:string[],
buttons:string[]
}
封装api接口
src/api/user/index.ts
import request from '@/utils/request'
import type { LoginParams,LoginResponseData, UserInfoResponseData } from './type'
// 接口地址:枚举enum
enum API{
// 登陆URL
LOGIN_URL = "/admin/acl/index/login",
// 用户信息
USERINFO_URL = "/admin/acl/index/info",
// 退出登陆
USERLOGOUT_URL = "/admin/acl/index/logout"
}
// api接口
// 登陆
export const reqUserLogin = (data:LoginParams)=>request.post<any,LoginResponseData>(API.LOGIN_URL,data)
// 获取用户信息
export const getUserInfo = ()=>request.get<any,UserInfoResponseData>(API.USERINFO_URL)
// 退出登陆
export const reqUserlogout = () => {
return request.post<any,any>(API.USERLOGOUT_URL)
}
调用api接口
在pinia的user小仓库中调用登陆api请求
src/stores/userInfo.ts
// 引入定义小仓库方法
import { defineStore } from 'pinia';
// 本地存储操作token
import { getToken, removeToken, setToken } from '../utils/token-utils';
// state类型的数据类型
import type { UserInfoState } from './interface';
// 消息提示
import { ElMessage } from 'element-plus'
// 静态路由
import { staticRoutes } from '@/router/routes'
// 导入用户相关的API
import { getUserInfo, reqUserLogin, reqUserlogout } from '@/api/user';
// 引入类型
import type {LoginResponseData, UserInfoResponseData} from '@/api/user/type/index'
/**
* 用户信息
* @methods setUserInfos 设置用户信息
*/
export const useUserInfoStore = defineStore('userInfo', {
state: (): UserInfoState => ({
token: getToken() as string,
name: '',
avatar: '',
menuRoutes: []
}),
actions: {
// 登陆
async login(username: string, password: string) {
// 定义请求体
const data = {
username,
password
}
// 获取响应数据
const result:LoginResponseData = await reqUserLogin(data)
// 存储token数据(pinia)
this.token = result.token
// 存储token数据(localstore)
setToken(result.token)
},
// 获取用户信息(路由鉴权那里进行调用)
async getInfo() {
const result:UserInfoResponseData = await getUserInfo()
// console.log("result ",result)
// 设置小仓库中的数据状态
this.name = result.name
this.avatar = result.avatar
// 设置用户的路由
this.menuRoutes = staticRoutes
},
// 退出登陆
async reset() {
// 发送请求
await reqUserlogout()
// 删除local中保存的token
removeToken()
// 提交重置用户信息的mutation
this.token = ''
this.name = ''
this.avatar = ''
},
},
});
在组件中调用小仓库中的login方法
<template>
<div class="login-container">
<el-form ref="formRef" :model="loginForm" :rules="loginRules" class="login-form" auto-complete="on" label-position="left">
<div class="title-container">
<h3 class="title">尚品汇后台管理</h3>
</div>
<el-form-item prop="username">
<span class="svg-container">
<svg-icon name="ele-UserFilled" />
</span>
<el-input ref="username" v-model="loginForm.username" placeholder="Username" name="username" type="text" tabindex="1" auto-complete="on" />
</el-form-item>
<el-form-item prop="password">
<span class="svg-container">
<svg-icon name="ele-Lock" />
</span>
<el-input :key="passwordType" ref="passwordRef" v-model="loginForm.password" :type="passwordType" placeholder="Password" name="password" tabindex="2" auto-complete="on" @keyup.enter.native="handleLogin" />
<span class="show-pwd" @click="showPwd">
<svg-icon :name="passwordType === 'password' ? 'ele-Hide' : 'ele-View'" />
</span>
</el-form-item>
<el-button :loading="loading" type="primary" style="width:100%;margin-bottom:30px;height: 40px;" @click.native.prevent="handleLogin">登 陆</el-button>
</el-form>
</div>
</template>
<script lang="ts">
export default {
name: 'Login'
}
</script>
<script lang="ts" setup>
import { useUserInfoStore } from '@/stores/userInfo'
import type { FormInstance } from 'element-plus'
import { nextTick, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
// 获取用户相关的小仓库
const userInfoStore = useUserInfoStore()
const route = useRoute()
const router = useRouter()
const loginForm = ref({
username: 'admin',
password: '111111'
})
const loading = ref(false)
// 密码表单元素type属性需要响应式数据:默认密码
const passwordType = ref('password')
const redirect = ref('')
// 获取密码选择框的实例
const passwordRef = ref<HTMLInputElement>()
const formRef = ref<FormInstance>()
const validateUsername = (rule: any, value: any, callback: any) => {
if (value.length < 4) {
// 提示信息并放行
callback(new Error('用户名长度不能小于4位'))
} else {
// 放行
callback()
}
}
const validatePassword = (rule: any, value: any, callback: any) => {
if (value.length < 6) {
callback(new Error('密码长度不能小于6位'))
} else {
callback()
}
}
const loginRules = {
username: [{ required: true, validator: validateUsername }],
password: [{ required: true, trigger: 'blur', validator: validatePassword }]
}
watch(route,() => {
redirect.value = route.query && (route.query.redirect as string)
},
{ immediate: true }
)
/*
切换密码的显示/隐藏
*/
const showPwd = () => {
if (passwordType.value === 'password') {
passwordType.value = 'text'
} else {
passwordType.value = 'password'
}
// 数据改变时dom立即更新
nextTick(() => {
// ? ts中的语法,当passwordRef.value存在时,执行passwordRef.value.focus(),不存在时不执行
passwordRef.value?.focus()
})
}
/*
点击登陆的回调
*/
const handleLogin = async () => {
// form表单验证通过之后再发送请求
await formRef.value?.validate()
loading.value = true
const { username, password } = loginForm.value
try {
// 用户小仓库执行login方法登陆
await userInfoStore.login(username, password)
// 编程式跳转路由
router.push({ path: redirect.value || '/' })
} finally {
loading.value = false
}
}
</script>
品牌管理页面
静态页面布局
src/views/product/trademark/index.vue
<template>
<el-card shadow="hover">
<!-- 顶部按钮 -->
<el-button type="primary" :icon="Plus" @click="addTradeMark">添加品牌</el-button>
<el-button type="primary" :icon="Download" >导出数据</el-button>
<!-- 表格 -->
<el-table :data="tradeMarkList" style="width: 100%;margin:10px 0px" border>
<el-table-column label="序号" width="90" align="center" type="index"/>
<el-table-column label="品牌名称" prop="tmName"/>
<el-table-column label="品牌LOGO" prop="logoUrl">
<template #="{row,$index}">
<img :src="row.logoUrl" alt="" style="width: 100px;height:100px">
</template>
</el-table-column>
<el-table-column label="品牌操作">
<template #="scope">
<el-button type="warning" size="small" :icon="Edit" circle />
<el-button type="danger" size="small" :icon="Delete" circle />
</template>
</el-table-column>
</el-table>
<!-- 对话框 -->
<el-dialog
v-model="showDialog"
style="margin-top: 100px"
title="新增品牌" >
<el-form
label-width="100px"
style="width: 80%"
>
<el-form-item label="品牌名称">
<el-input type="text" placeholder="请输入品牌名称"/>
</el-form-item>
<el-form-item label="品牌LOGO">
<el-upload
class="avatar-uploader"
action="https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15"
:show-file-list="false"
:on-success="handleAvatarSuccess"
:before-upload="beforeAvatarUpload"
>
<img v-if="imageUrl" :src="imageUrl" class="avatar" />
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
</el-upload>
</el-form-item>
</el-form>
<template #footer>
<!-- 具名插槽:放置确定与取消按钮 -->
<el-button size="default" @click="handleCloseDialog">取消</el-button>
<el-button type="primary" size="default" >确定</el-button>
</template>
</el-dialog>
<!-- 分页 layout中的->:把右侧的放在最右侧 -->
<el-pagination
v-model:current-page="pageNo"
v-model:page-size="pageSize"
:page-sizes="[3, 5, 7]"
:size="size"
:background="true"
layout="prev, pager, next, jumper,->,sizes,total"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</el-card>
</template>
<script setup lang='ts'>
import { Plus,Download,Edit,Delete } from '@element-plus/icons-vue'
import { onMounted, ref } from 'vue'
import type { ComponentSize } from 'element-plus'
import { reqTradeMark } from '@/api/product/trademark'
import type {TradeMarkResponseData,Records} from '@/api/product/type/trademark'
import type { UploadProps } from 'element-plus'
import { ElMessage } from 'element-plus'
// 表格相关
const tradeMarkList = ref<Records>([])
// 分页相关
const pageNo = ref<number>(1)
const pageSize = ref(3)
const size = ref<ComponentSize>('default')
const total = ref<number>(0)
// 表单相关
const showDialog = ref<boolean>(false)
// 上传相关
const imageUrl = ref('')
const handleAvatarSuccess: UploadProps['onSuccess'] = (
response,
uploadFile
) => {
imageUrl.value = URL.createObjectURL(uploadFile.raw!)
}
const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => {
if (rawFile.type !== 'image/jpeg') {
ElMessage.error('Avatar picture must be JPG format!')
return false
} else if (rawFile.size / 1024 / 1024 > 2) {
ElMessage.error('Avatar picture size can not exceed 2MB!')
return false
}
return true
}
// 生命周期
onMounted(()=>{
getTradeMarkList()
})
// 获取品牌列表数据方法
const getTradeMarkList = async(val:number=1) =>{
pageNo.value = val
const result:TradeMarkResponseData = await reqTradeMark(pageNo.value,pageSize.value)
// 修改品牌列表数据
tradeMarkList.value = result.records
total.value = result.total
}
// 添加品牌
const addTradeMark = () =>{
showDialog.value = true;
}
const handleCloseDialog = () =>{
showDialog.value = false;
}
const handleSizeChange = (val: number) => {
pageSize.value = val
getTradeMarkList()
}
const handleCurrentChange = (val: number) => {
pageNo.value = val
getTradeMarkList(pageNo.value)
}
</script>
<style scoped>
.avatar-uploader .avatar {
width: 178px;
height: 178px;
display: block;
}
</style>
<style>
.avatar-uploader .el-upload {
border: 1px dashed var(--el-border-color);
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: var(--el-transition-duration-fast);
}
.avatar-uploader .el-upload:hover {
border-color: var(--el-color-primary);
}
.el-icon.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
text-align: center;
}
</style>
封装获取品牌接口
获取品牌列表数据:src/api/product/trademark.ts
import request from '@/utils/request'
import type { TradeMarkResponseData } from './type/trademark'
// 枚举地址
enum API {
GETTRADEMARK_URL = '/admin/product/baseTrademark/'
}
// 获取品牌列表数据
export const reqTradeMark = (page:number,limit:number) => request.get<any,TradeMarkResponseData>(API.GETTRADEMARK_URL+`${page}/${limit}`)
定义品牌api类型
定义品牌列表数据类型:src/api/product/type/trademark.ts
// 品牌item对象类型
export interface TradeMark{
id?:number,
tmName:string,
logoUrl:string
}
// 品牌数组
export type Records = TradeMark[]
// 品牌列表响应类型
export interface TradeMarkResponseData{
records:Records,
total:number,
size:number,
current:number,
searchCount:boolean,
pages:number
}
调用接口渲染数据
在组件中调用:src/views/product/trademark/index.vue
<template>
<el-card shadow="hover">
<!-- 顶部按钮 -->
<el-button type="primary" :icon="Plus" @click="addTradeMark">添加品牌</el-button>
<el-button type="primary" :icon="Download" >导出数据</el-button>
<!-- 表格 -->
<el-table :data="tradeMarkList" style="width: 100%;margin:10px 0px" border>
<el-table-column label="序号" width="90" align="center" type="index"/>
<el-table-column label="品牌名称" prop="tmName"/>
<el-table-column label="品牌LOGO" prop="logoUrl">
<template #="{row,$index}">
<img :src="row.logoUrl" alt="" style="width: 100px;height:100px">
</template>
</el-table-column>
<el-table-column label="品牌操作">
<template #="scope">
<el-button type="warning" size="small" :icon="Edit" circle />
<el-button type="danger" size="small" :icon="Delete" circle />
</template>
</el-table-column>
</el-table>
<!-- 对话框 -->
<el-dialog
v-model="showDialog"
style="margin-top: 100px"
title="新增品牌" >
<el-form
label-width="100px"
style="width: 80%"
>
<el-form-item label="品牌名称">
<el-input type="text" placeholder="请输入品牌名称"/>
</el-form-item>
<el-form-item label="品牌LOGO">
<el-upload
class="avatar-uploader"
action="https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15"
:show-file-list="false"
:on-success="handleAvatarSuccess"
:before-upload="beforeAvatarUpload"
>
<img v-if="imageUrl" :src="imageUrl" class="avatar" />
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
</el-upload>
</el-form-item>
</el-form>
<template #footer>
<!-- 具名插槽:放置确定与取消按钮 -->
<el-button size="default" @click="handleCloseDialog">取消</el-button>
<el-button type="primary" size="default" >确定</el-button>
</template>
</el-dialog>
<!-- 分页 layout中的->:把右侧的放在最右侧 -->
<el-pagination
v-model:current-page="pageNo"
v-model:page-size="pageSize"
:page-sizes="[3, 5, 7]"
:size="size"
:background="true"
layout="prev, pager, next, jumper,->,sizes,total"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</el-card>
</template>
<script setup lang='ts'>
import { Plus,Download,Edit,Delete } from '@element-plus/icons-vue'
import { onMounted, ref } from 'vue'
import type { ComponentSize } from 'element-plus'
import { reqTradeMark } from '@/api/product/trademark'
import type {TradeMarkResponseData,Records} from '@/api/product/type/trademark'
import type { UploadProps } from 'element-plus'
import { ElMessage } from 'element-plus'
// 表格相关
const tradeMarkList = ref<Records>([])
// 分页相关
const pageNo = ref<number>(1)
const pageSize = ref(3)
const size = ref<ComponentSize>('default')
const total = ref<number>(0)
// 表单相关
const showDialog = ref<boolean>(false)
// 上传相关
const imageUrl = ref('')
const handleAvatarSuccess: UploadProps['onSuccess'] = (
response,
uploadFile
) => {
imageUrl.value = URL.createObjectURL(uploadFile.raw!)
}
const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => {
if (rawFile.type !== 'image/jpeg') {
ElMessage.error('Avatar picture must be JPG format!')
return false
} else if (rawFile.size / 1024 / 1024 > 2) {
ElMessage.error('Avatar picture size can not exceed 2MB!')
return false
}
return true
}
// 生命周期
onMounted(()=>{
getTradeMarkList()
})
// 获取品牌列表数据方法
const getTradeMarkList = async(val:number=1) =>{
pageNo.value = val
const result:TradeMarkResponseData = await reqTradeMark(pageNo.value,pageSize.value)
// 修改品牌列表数据
tradeMarkList.value = result.records
total.value = result.total
}
// 添加品牌
const addTradeMark = () =>{
showDialog.value = true;
}
const handleCloseDialog = () =>{
showDialog.value = false;
}
const handleSizeChange = (val: number) => {
pageSize.value = val
getTradeMarkList()
}
const handleCurrentChange = (val: number) => {
pageNo.value = val
getTradeMarkList(pageNo.value)
}
</script>
<style scoped>
.avatar-uploader .avatar {
width: 178px;
height: 178px;
display: block;
}
</style>
<style>
.avatar-uploader .el-upload {
border: 1px dashed var(--el-border-color);
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: var(--el-transition-duration-fast);
}
.avatar-uploader .el-upload:hover {
border-color: var(--el-color-primary);
}
.el-icon.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
text-align: center;
}
</style>
Dialog对话框
静态布局
src/views/product/trademark/index.vue
...
<el-dialog
v-model="showDialog"
style="margin-top: 100px"
title="新增品牌" >
<el-form
label-width="100px"
style="width: 80%"
>
<el-form-item label="品牌名称">
<el-input type="text" placeholder="请输入品牌名称"/>
</el-form-item>
<el-form-item label="品牌LOGO">
<el-upload
class="avatar-uploader"
action="https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15"
:show-file-list="false"
:on-success="handleAvatarSuccess"
:before-upload="beforeAvatarUpload"
>
<img v-if="imageUrl" :src="imageUrl" class="avatar" />
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
</el-upload>
</el-form-item>
</el-form>
<template #footer>
<!-- 具名插槽:放置确定与取消按钮 -->
<el-button size="default" @click="handleCloseDialog">取消</el-button>
<el-button type="primary" size="default" >确定</el-button>
</template>
</el-dialog>
...
import type { UploadProps } from 'element-plus'
import { ElMessage } from 'element-plus'
// 表单相关
const showDialog = ref<boolean>(false)
// 上传相关
const imageUrl = ref('')
const handleAvatarSuccess: UploadProps['onSuccess'] = (
response,
uploadFile
) => {
imageUrl.value = URL.createObjectURL(uploadFile.raw!)
}
const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => {
if (rawFile.type !== 'image/jpeg') {
ElMessage.error('Avatar picture must be JPG format!')
return false
} else if (rawFile.size / 1024 / 1024 > 2) {
ElMessage.error('Avatar picture size can not exceed 2MB!')
return false
}
//发送请求
return true
}
<style scoped>
.avatar-uploader .avatar {
width: 178px;
height: 178px;
display: block;
}
</style>
<style>
.avatar-uploader .el-upload {
border: 1px dashed var(--el-border-color);
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: var(--el-transition-duration-fast);
}
.avatar-uploader .el-upload:hover {
border-color: var(--el-color-primary);
}
.el-icon.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
text-align: center;
}
</style>
deep选择器
可以使用 deep 选择器(通常是 ::v-deep)来为 ElDialog 等组件内部元素应用样式。deep 选择器允许您在 scoped 样式中嵌套地选择并覆盖第三方组件的内部样式,非常适用于这种情况。
注意:
::v-deep 是 Vue 3 中的写法,Vue 2 使用 /deep/ 或 >>>。请确认您的 Vue 版本是否支持此选择器。
::v-deep 选择器可以帮助您在不直接修改组件的情况下覆盖组件库的样式,但由于其跨作用域的特性,可能会增加样式复杂度,需谨慎使用。
这样,您可以在保持 scoped 的同时,自定义第三方组件的样式而不会影响其他组件。
使用案例:
<template>
<ElDialog :model-value="false" @update:model-value="fn">
<!-- 组件内容 -->
</ElDialog>
</template>
<style scoped>
/* 使用 ::v-deep 选择器 */
::v-deep .el-dialog {
margin-top: 100px;
}
</style>
项目代码:
<style scoped>
/* 使用 ::v-deep 选择器 */
::v-deep .el-dialog {
margin-top: 100px;
}
</style>
封装API接口(新增和更新)
定义数据类型 src/api/product/type/trademark.ts
// 品牌item对象类型
export interface TradeMark{
id?:number,
tmName:string,
logoUrl:string
}
// 品牌数组
export type Records = TradeMark[]
// 品牌列表响应类型
export interface TradeMarkResponseData{
records:Records,
total:number,
size:number,
current:number,
searchCount:boolean,
pages:number
}
封装新增和修改的品牌接口:src/api/product/trademark.ts
import type { TradeMarkResponseData,TradeMark } from './type/trademark'
// 枚举地址
enum API {
//获取已有的品牌
GETTRADEMARK_URL = '/admin/product/baseTrademark/',
//添加新增的品牌接口:post {tmName:'xxx',logoUrl:'xxx'}
ADDTRADEMARK_URL = "/admin/product/baseTrademark/save",
//更新已有的品牌 post {id:'zzz',tmName:'xxx',logoUrl:'xxx'}
UPDATETRADEMARK_URL = "/admin/product/baseTrademark/update",
//删除已有的品牌数据
DLETETRADENARK_URL = "/admin/product/baseTrademark/remove/"
}
//添加与更新品牌的函数
export const reqAddOrUpdateTradeMark = (data: TradeMark) => {
//更新已有的品牌
if (data.id) {
return request.put<any, any>(API.UPDATETRADEMARK_URL, data);
} else {
//新增品牌
return request.post<any, any>(API.ADDTRADEMARK_URL, data);
}
}
调用上传接口
获取from表单数据并配置上传的接口:src/views/product/trademark/index.vue
<el-dialog
v-model="showDialog"
style="margin-top: 100px"
title="新增品牌" >
<el-form
ref="formRef"
:model="tradeMark"
label-width="100px"
style="width: 80%"
>
<el-form-item label="品牌名称">
<el-input type="text" placeholder="请输入品牌名称" v-model="tradeMark.tmName"/>
</el-form-item>
<el-form-item label="品牌LOGO">
<!-- action:上传图片的请求地址 上传图片post 需要代理跨域 app-dev -->
<el-upload
class="avatar-uploader"
action="/app-dev/admin/product/fileUpload"
:show-file-list="false"
:on-success="handleAvatarSuccess"
:before-upload="beforeAvatarUpload"
>
<img v-if="tradeMark.logoUrl" :src="tradeMark.logoUrl" class="avatar" />
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
</el-upload>
</el-form-item>
</el-form>
<template #footer>
<!-- 具名插槽:放置确定与取消按钮 -->
<el-button size="default" @click="handleCloseDialog">取消</el-button>
<el-button type="primary" size="default" >确定</el-button>
</template>
</el-dialog>
...
let tradeMark = reactive<TradeMark>({
tmName: "", //收集新增品牌名称
logoUrl: "", //收集新增品牌图片地址
})
...
配置上传前的钩子函数
# 配置上传图片前的钩子函数:
参数是文件对象,文件对象可以获取上传文件的类型,大小,名字;因此可以限制上传文件的类型,大小,名字。
返回值:
return true 发送请求
return false 不发送请求
项目代码:src/views/product/trademark/index.vue
const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => {
if (rawFile.type !== 'image/jpeg') {
ElMessage.error('Avatar picture must be JPG format!')
return false
} else if (rawFile.size / 1024 / 1024 > 2) {
ElMessage.error('Avatar picture size can not exceed 2MB!')
return false
}
//发送请求
return true
}
配置上传后的钩子函数
配置上传后的钩子函数 用于获取成功后的服务器响应,把响应的图片地址收集到from表单中
//图片上传成功的钩子
const handleAvatarSuccess = (response: any, file: any) => {
//response:即为上传图片post 请求返回的数据
//file:即为上传图片对象
tradeMark.logoUrl = response.data;
//图片上传成功清除校验结果
formRef.value.clearValidate("logoUrl");
};
点击确定按钮发送请求
<el-button type="primary" size="default" @click="confirm" >确定</el-button>
...
const confirm = ()=>{
//收集参数
// 校验数据
//发请求
// 提示消息
}
表单校验
文本类表单校验函数,和上传类表单校验函数都是来自Element-plus官网的文档中。
表单校验
原理
这类表单校验是:文本类,checkbox,这种具有 change blur 事件的校验
校验函数 validator()
函数的第一个参数是 rule对象
第二个参数是 表单内容
第三个参数是 放行函数 通过校验时触发调用
const validatorTmName = (rule,value,callback) =>{
if(){
//放行
callback();
}else{
// 不符合条件时,在放行函数中 传入一个 Error对象
callback(new Error("不符合校验"));
}
}
项目代码
// 配置校验表单
<el-form
ref="formRef"
:model="tradeMark"
label-width="100px"
style="width: 80%"
:rules="rules"
>
<el-form-item label="品牌名称" prop="tmName">
<el-form-item label="品牌LOGO" prop="logoUrl">
</el-form>
// 设置校验规则
const rules = {
tmName:[{
required:true,
trigger:"change",
validator:validatorTmName
}],
logoUrl:[{
required:true,
trigger:"change",
validator:validatorTmName
}]
}
// 配置自定义校验函数
const validatorTmName = (rule,value,callback) =>{
if(){
//放行
callback();
}else{
// 不符合条件时,在放行函数中 传入一个 Error对象
callback(new Error("不符合校验"));
}
}
非文本表单校验
这类表单校验是:文本类,checkbox,这种没有 change blur 事件的校验
原理
使用表单form对象方法validate()
校验表单数据
参数:无
作用:校验表单内容
返回值:会返回一个Promise对象,全部表单校验成功即成功,只要有一个表单校验失败就失败
// 在元素上绑定ref属性
<el-form ref="formRef"></el-form>
// 定义ref对象
const formRef = ref<any>()
// 定义校验的rules
const rules = {
tmName:[{
required:true,
trigger:"blur",
validator:validatorTmName
}],
logoUrl:[{
required:true,
validator:validatorlogoUrl
}]
}
// 校验的方法
const validatorlogoUrl = (rule,value,callback)=>{
console.log("123")
}
// 使用from对象的validate方法调用(在提交的时候触发)
formRef.value.validate()
项目代码
// 自定义校验规则
const validatorTmName = (rule: any, value: any, callBack: any) =>{
//规则对象没有实际用途 rule
//表单内容 value
//放行函数 callBack
if (/^[\u4e00-\u9fa5]{2,}$/.test(value)) { // 如果前两位数是汉字
callBack();
} else {
//不符合条件
callBack(new Error("品牌名称至少两位汉字"));
}
}
const validatorLogoUrl = (rule: any, value: any, callBack:any)=>{
//value即为上传图片地址:如果有放行
if (value) {
callBack();
} else {
//value即为上传图片地址:如果没有返回错误信息
callBack(new Error("请上传图片LOGO"));
}
}
// 设置校验规则
const rules = {
tmName:[{
required:true,
trigger:"blur",
validator:validatorTmName
}],
logoUrl:[{
required:true,
validator:validatorLogoUrl
}]
}
清空表单校验结果
// 方式1 使用nextTick()
// 方式2 使用 ts 语法 ?
// ? 的语法 当?前的对象是undefined时,不执行方法,?前的对象存在时,执行方法
formRef.value?.clearValidate()
// 清除upload表单校验结果
在上传后的钩子函数中清除 upload校验结果
nextTick
作用:当响应式数据发生变化后,获取更新后的DOM
nextTick(()=>{
代码
}),是一个异步函数,需要DOM更新完毕后,执行里边的代码
项目代码
<el-form
ref="formRef"
:model="tradeMark"
label-width="100px"
style="width: 80%"
:rules="rules"
>
...
const formRef = ref()
...
// 打开添加品牌
const handleAddDialog = () =>{
showDialog.value = true
//清空表单校验的结果
resetFiledResult()
}
// 重置表单数据
const resetFromData = ()=>{
tradeMark.id = 0
tradeMark.tmName = ""
tradeMark.logoUrl = ""
}
//清空表单校验的结果
const resetFiledResult = () => {
formRef.value?.clearValidate("tmName")
formRef.value?.clearValidate("logoUrl")
}
// 在上传后的钩子函数
const handleAvatarSuccess: UploadProps['onSuccess'] = (response,uploadFile) => {
//response:即为上传图片post 请求返回的数据
//file:即为上传图片对象
tradeMark.logoUrl = response.data;
//图片上传成功清除校验结果
formRef.value.clearValidate("logoUrl");
}
确认提交
<el-button type="primary" size="default" @click="confirm" >确定</el-button>
const confirm = async()=>{
//收集参数
// 校验数据 调用 formRef对象的 validate() 方法
//通过form组件实例的validate方法进行全部的表单校验
//validate方法执行:会返回一个Promise->成功(全部表单相校验成功)、失败(只要有一个表单相校验失败)
await formRef.value.validate();
//发请求
try {
//添加品牌成功||修改品牌成功
await reqAddOrUpdateTradeMark(tradeMark);
//关闭对话框
showDialog.value = false
//提示
ElMessage({
type: "success",
message: tradeMark.id ? "更新成功" : "添加成功",
});
//再次获取全部已有的品牌
//更新留在当前页、添加留在第一页
getTradeMarkList(tradeMark.id ? pageNo.value : 1)
} catch (error) {
ElMessage({
type: "error",
message: tradeMark.id ? "更新失败" : "添加失败",
})
}
}
更新品牌
<el-table-column label="品牌操作">
<template #="scope">
<el-button type="warning" size="small" :icon="Edit" @click="updateTradeMark(scope.row)" circle />
<el-button type="danger" size="small" :icon="Delete" circle />
</template>
</el-table-column>
...
// 编辑品牌
const updateTradeMark = (row:TradeMark) =>{
//将已有的品牌数据赋值给tradeMark
tradeMark.id = row.id;
tradeMark.tmName = row.tmName;
tradeMark.logoUrl = row.logoUrl;
//显示对话框
showDialog.value = true;
}
删除品牌
封装删除品牌的接口 src/api/product/trademark.ts
// 删除品牌的接口
export const reqDeleteById = (id: number) => request.delete<any, any>(API.DLETETRADENARK_URL + id);
组件中调用 src/views/product/trademark/index.vue
<el-popconfirm
:title="`你确定要删除${scope.row.tmName}?`"
icon-color="red"
:icon="Delete"
width="250px"
@confirm="deleteTradeMark(scope.row.id)"
>
<template #reference>
<el-button type="danger" size="small" :icon="Delete" circle />
</template>
</el-popconfirm>
...
// 删除品牌
const deleteTradeMark = async (id:number) =>{
try {
await reqDeleteById(id);
//提示
ElMessage({
type: "success",
message: "删除成功",
});
//再次获取全部已有品牌数据(当前页)
getTradeMarkList(pageNo.value);
} catch (error) {
ElMessage({
type: "error",
message: "删除失败",
});
}
}
导出表格
使用插件 xlsx 格式
# 详细请查阅 npm xlsx 插件的 readme (搜索 append 关键字)
// 安装插件:
npm install xlsx
https://www.npmjs.com/package/xlsx
// 使用插件:
// 导入 全部引入 并且设置别名
import * as XLSX from 'xlsx'
/* generate worksheet and workbook */
// 创建json数据转Excel需要的格式
const worksheet = XLSX.utils.json_to_sheet(rows);
// 给Excel创建一个容器
const workbook = XLSX.utils.book_new();
// 把数据追加到Excel容器中
XLSX.utils.book_append_sheet(workbook, worksheet, "Dates");
// 导出数据:第二个参数是文件名
XLSX.writeFile(workbook, "Presidents.xlsx");
项目代码
<el-button type="primary" :icon="Download" @click="downLoad">导出数据</el-button>
...
import * as XLSX from 'xlsx'
// 导出数据
const downLoad = () => {
// 创建json数据转Excel需要的格式
const worksheet = XLSX.utils.json_to_sheet(tradeMarkList.value);
// 给Excel创建一个容器
const workbook = XLSX.utils.book_new();
// 把数据追加到Excel容器中
XLSX.utils.book_append_sheet(workbook, worksheet, "Dates");
// 导出数据:第二个参数是文件名
XLSX.writeFile(workbook, "Presidents.xlsx")
}
富文本