技术栈:vue3+TypeScript+vue-router+pinia+element-plus+axios+echarts等技术栈。
1、vue3组件通信方式
通信仓库地址:vue3_communication: 当前仓库为贾成豪老师使用组件通信案例
不管是vue2还是vue3,组件通信方式很重要,不管是项目还是面试都是经常用到的知识点。
比如:vue2组件通信方式
**props:**可以实现父子组件、子父组件、甚至兄弟组件通信
自定义事件:可以实现子父组件通信
全局事件总线$bus:可以实现任意组件通信
**pubsub:**发布订阅模式实现任意组件通信
vuex:集中式状态管理容器,实现任意组件通信
ref:父组件获取子组件实例VC,获取子组件的响应式数据以及方法
**slot:**插槽(默认插槽、具名插槽、作用域插槽)实现父子组件通信…
1.1prop通信
props可以实现父子组件通信,在vue3中我们可以通过defineProps获取父组件传递的数据。且在组件内部不需要引入defineProps方法可以直接使用!
<template>
<div class="box">
<h1>props:我是父组件曹操</h1>
<hr />
<Child info="我是曹操" :money="money"></Child>
</div>
</template>
<script setup lang="ts">
//props:可以实现父子组件通信,props数据还是只读的!!不可以修改
import Child from "./Child.vue";
import { ref } from "vue";
// 当引用对象中的值发生变化时,依赖于它的组件会自动重新渲染。你可以通过引用对象的 .value 属性来访问和修改它所引用的值。
let money = ref(10000);//money 是通过 ref 创建的一个引用对象,初始值为 10000。
</script>
传参,动态数据money
<Child info="我是曹操" :money="money"></Child>
<template>
<div class="son">
<h1>我是子组件:曹植</h1>
<p>{{props.info}}</p>
<p>{{props.money}}</p>
<!--props可以省略前面的名字--->
<p>{{info}}</p>
<p>{{money}}</p>
<button @click="updateProps">修改props数据</button>
</div>
</template>
<script setup lang="ts">
//需要使用到defineProps方法去接受父组件传递过来的数据
//defineProps是Vue3提供方法,不需要引入直接使用
let props = defineProps(['info','money']); //数组|对象写法都可以
//按钮点击的回调
const updateProps = ()=>{
// props.money+=10; props:只读的
console.log(props.info)
}
</script>
子组件获取方式1
let props = defineProps({
info:{
type:String,//接受的数据类型
default:'默认参数',//接受默认数据
},
money:{
type:Number,
default:0
}})
获取方式2
let props = defineProps(["info",'money']);
子组件获取到props数据就可以在模板中使用了,但是切记props是只读的(只能读取,不能修改)
1.2自定义事件
在vue框架中事件分为两种:一种是原生的DOM事件,另外一种自定义事件。
原生DOM事件可以让用户与网页进行交互,比如click、dbclick、change、mouseenter、mouseleave…
自定义事件可以实现子组件给父组件传递数据
1.2.1原生DOM事件
<pre @click="handler">
我是祖国的老花骨朵
</pre>
当前代码级给pre标签绑定原生DOM事件点击事件,默认会给事件回调注入event事件对象。当然点击事件想注入多个参数可以按照下图操作。但是切记注入的事件对象务必叫做$event.
<div @click="handler1(1,2,3,$event)">我要传递多个参数</div>
在vue3框架click、dbclick、change(这类原生DOM事件),不管是在标签、自定义标签上(组件标签)都是原生DOM事件。
1.2.2自定义事件
自定义事件可以实现子组件给父组件传递数据.在项目中是比较常用的。
比如在父组件内部给子组件(Event2)绑定一个自定义事件
<Event2 @xxx="handler3" @click="handler4"></Event2>
<template>
<div class="child">
<p>我是子组件2</p>
<button @click="handler">点击我触发自定义事件xxx</button>
<button @click="$emit('click','AK47','J20')">点击我触发自定义事件click</button>
</div>
</template>
<script setup lang="ts">
//利用defineEmits方法返回函数触发自定义事件
//defineEmits方法不需要引入直接使用
let $emit = defineEmits(['xxx','click']);
//按钮点击回调
const handler = () => {
//第一个参数:事件类型 第二个|三个|N参数即为注入数据
$emit('xxx','东风导弹','航母');
};
</script>
我们会发现在script标签内部,使用了defineEmits方法,此方法是vue3提供的方法,不需要引入直接使用。defineEmits方法执行,传递一个数组,数组元素即为将来组件需要触发的自定义事件类型,此方执行会返回一个$emit方法用于触发自定义事件。
当点击按钮的时候,事件回调内部调用$emit方法去触发自定义事件,第一个参数为触发事件类型,第二个、三个、N(东风导弹,航母)个参数即为传递给父组件的数据。
需要注意的是
<Event2 @xxx="handler3" @click="handler"></Event2>
正常说组件标签书写@click应该为原生DOM事件,但是如果子组件内部通过defineEmits定义就变为自定义事件了。(就不是DOM事件了)
let $emit = defineEmits(["xxx",'click']);
父组件
<template>
<div>
<h1>事件</h1>
<!-- 原生DOM事件 -->
<pre @click="handler">
大江东去浪淘尽,千古分流人物
</pre>
<button @click="handler1(1,2,3,$event)">点击我传递多个参数</button>
<hr>
<!--
vue2框架当中:这种写法自定义事件,可以通过.native修饰符变为原生DOM事件
vue3框架下面写法其实即为原生DOM事件
vue3:原生的DOM事件不管是放在标签身上、组件标签身上都是原生DOM事件
-->
<Event1 @click="handler2"></Event1>
<hr>
<!-- 绑定自定义事件xxx:实现子组件给父组件传递数据 -->
<Event2 @xxx="handler3" @click="handler4"></Event2>
</div>
</template>
<script setup lang="ts">
//引入子组件
import Event1 from './Event1.vue';
//引入子组件
import Event2 from './Event2.vue';
//事件回调--1
const handler = (event)=>{
//event即为事件对象
console.log(event);
}
//事件回调--2
const handler1 = (a,b,c,$event)=>{
console.log(a,b,c,$event)
}
//事件回调---3
const handler2 = ()=>{
console.log(123);
}
//事件回调---4
const handler3 = (param1,param2)=>{
console.log(param1,param2,'dsfjiods');
}
//事件回调--5
const handler4 = (param1,param2)=>{
console.log(param1,param2);
}
</script>
<style scoped>
</style>
1.3全局事件总线
全局事件总线可以实现任意组件通信,在vue2中可以根据VM与VC关系推出全局事件总线。
但是在vue3中没有Vue构造函数,也就没有Vue.prototype.以及组合式API写法没有this,
那么在Vue3想实现全局事件的总线功能就有点不现实啦,如果想在Vue3中使用全局事件总线功能
可以使用插件mitt实现。
mitt:官网地址:mitt - npm
兄弟同信
需要在创建一个bus目录,index.ts文件
//引入mitt插件:mitt一个方法,方法执行会返回bus对象
import mitt from 'mitt';
const $bus = mitt();
export default $bus;
<template>
<div class="child2">
<h2>我是子组件2:曹丕</h2>
<button @click="handler">点击我给兄弟送一台法拉利</button>
</div>
</template>
<script setup lang="ts">
//引入$bus对象
import $bus from '../../bus';
//点击按钮回调
const handler = ()=>{
$bus.emit('car',"兰博基尼");
}
</script>
<style scoped>
.child2{
width: 300px;
height: 300px;
background: skyblue;
}
</style>
<template>
<div class="child1">
<h3>我是子组件1:曹植</h3>
</div>
</template>
<script setup lang="ts">
import $bus from "../../bus";
//组合式API函数
import { onMounted } from "vue";
//组件挂载完毕的时候,当前组件绑定一个事件,接受将来兄弟组件传递的数据
onMounted(() => {
//第一个参数:即为事件类型 第二个参数:即为事件回调
$bus.on("car", (car) => {
console.log(car,'123');
});
});
</script>
1.4)v-model
v-model指令: 收集表单数据,数据双向绑定。
v-model也可以实现组件之间的通信,实现父子组件数据同步的业务。
- 父亲给子组件数据 props
- 子组件给父组件数据 自定义事件
- 引入子组件
<div>
<!-- props:父亲给儿子数据 -->
<!-- <Child :modelValue="money” @update:modelValue="handler">/Child>-->
<!--
v-mode1组件身上使用
第一:相当有给子组件传递props[modelValue] = 10000
第二:相当于给子组件绑定自定义事件update:modelValue
-->
<Child v-model="money"></Child>
</div>
v-model指令可是收集表单数据(数据双向绑定),除此之外它也可以实现父子组件数据同步。
而v-model实指利用props[modelValue]与自定义事件[update:modelValue]实现的。
下方代码:相当于给组件Child传递一个props(modelValue)与绑定一个自定义事件update:modelValue
实现父子组件数据同步
<Child v-model="money"></Child>
在vue3中一个组件可以通过使用多个v-model,让父子组件多个数据同步,下方代码相当于给组件Child传递两个props分别是pageNo与pageSize,以及绑定两个自定义事件update:pageNo与update:pageSize实现父子数据同步。
<Child1 v-model:pageNo="pageNo" v-model:pageSize="pageSize"></Child1>
1.5)useAttrs
vue3框架提供一个方法useAttrs方法,它可以获取组件身上的属性与事件!(所有,但优先级不如props)
useAttrs 是 Vue.js 3 中的一个新特性,用于在组件中获取所有传递给组件的非响应式属性(non-reactive props)。与 props 属性不同,useAttrs 返回的是一个对象,包含了所有未声明为 props 的属性。这些属性通常被称为“剩余属性”(rest attributes)或“未知属性”(unknown attributes),因为它们在组件内部可能不会被直接使用,而是被传递到其他子组件或用于样式类等。
在Vue3中可以利用useAttrs方法获取组件的属性与事件(包含:原生DOM事件或者自定义事件),次函数功能类似于Vue2框架中$attrs属性与$listeners方法。
比如:在父组件内部使用一个子组件HintButton
<HintButton type="primary" size="small" :icon="Edit" title="编辑按钮" @click="handler" @xxx="handler"></HintButton>
<template>
<div :title="title">
<el-button :="$attrs"></el-button>
</div>
</template>
<script setup lang="ts">
//引入useAttrs方法:获取组件标签身上属性与事件
import {useAttrs} from 'vue';
//此方法执行会返回一个对象
let $attrs = useAttrs();
//万一用props接受title
let props =defineProps(['title']);
console.log(props,'123')
//props与useAttrs方法都可以获取父组件传递过来的属性与属性值
//但是props接受了useAttrs方法就获取不到了
console.log($attrs);
</script>
子组件内部可以通过useAttrs方法获取组件属性与事件.因此你也发现了,它类似于props,可以接受父组件传递过来的属性与属性值。需要注意如果defineProps接受了某一个属性,useAttrs方法返回的对象身上就没有相应属性与属性值。因为他的优先级不够props高,props获取了$attrs就获取不到了
1.6)ref与$parent
ref 提及到 ref 可能会想到它可以获取元素的DOM或者获取子组件实例的VC。既然可以在父组件内部通过ref获取子组件实例VC,那么子组件内部的方法与响应式数据父组件可以使用的。
ref:可以获取真实的DOM节点,可以获取到子组件实例VC
$parent:可以在子组件内部获取到父组件的实例
比如:
在父组件挂载完毕获取组件实例
父组件内部代码:
<template>
<div class="box">
<h1>我是父亲曹操:{{money}}</h1>
<button @click="handler">找我的儿子曹植借10元</button>
<hr>
<Son ref="son"></Son>
<hr>
<Dau></Dau>
</div>
</template>
<script setup lang="ts">
//ref:可以获取真实的DOM节点,可以获取到子组件实例VC
//$parent:可以在子组件内部获取到父组件的实例
//引入子组件
import Son from './Son.vue'
import Dau from './Daughter.vue'
import {ref} from 'vue';
//父组件钱数
let money = ref(100000000);
//获取子组件的实例.要同名,否则获取不到vc
let son = ref();
//父组件内部按钮点击回调
const handler = ()=>{
money.value+= 10;
//儿子钱数减去10
son.value.money-=10;
son.value.fly();
}
//对外暴露
defineExpose({
money
})
</script>
但是需要注意,如果想让父组件获取子组件的数据或者方法需要通过defineExpose对外暴露,因为vue3中组件内部的数据对外“关闭的”,外部不能访问。
<template>
<div class="son">
<h3>我是子组件:曹植{{money}}</h3>
</div>
</template>
<script setup lang="ts">
import {ref} from 'vue';
//儿子钱数
let money = ref(666);
const fly = ()=>{
console.log('我可以飞');
}
//组件内部数据对外关闭的,别人不能访问
//如果想让外部访问需要通过defineExpose方法对外暴露
defineExpose({
money,
fly
})
$parent 可以获取某一个组件的父组件实例VC,因此可以使用父组件内部的数据与方法。必须子组件内部拥有一个按钮点击时候获取父组件实例,当然父组件的数据与方法需要通过 defineExpose 方法对外暴露。
<template>
<div class="dau">
<h1>我是闺女曹杰{{money}}</h1>
<button @click="handler($parent)">点击我爸爸给我10000元</button>
</div>
</template>
<script setup lang="ts">
import {ref} from 'vue';
//闺女钱数
let money = ref(999999);
//闺女按钮点击回调
const handler = ($parent)=>{
money.value+=10000;
//ref对象才用.value,proxy对象不用.value
$parent.money-=10000;
}
</script>
(1.7)provide与inject
provide[提供]
inject[注入]
vue3提供两个方法 provide 与 inject, 可以实现隔辈组件传递参数。
组件组件提供数据:
provide方法用于提供数据,此方法执需要传递两个参数, 分别提供数据的key与祖先组件提供数据value。
<template>
<div class="box">
<h1>Provide与Inject{{car}}</h1>
<hr />
<Child></Child>
</div>
</template>
<script setup lang="ts">
import Child from "./Child.vue";
//vue3提供provide(提供)与inject(注入),可以实现隔辈组件传递数据
import { ref, provide } from "vue";
let car = ref("法拉利");
//祖先组件给后代组件提供数据
//两个参数:第一个参数就是提供的数据key
//第二个参数:祖先组件提供数据
provide("TOKEN", car);
</script>
<template>
<div class="child">
<h1>我是子组件1</h1>
<Child></Child>
</div>
</template>
<script setup lang="ts">
import Child from './GrandChild.vue';
</script>
后代组件可以通过 inject 方法获取数据,通过 key 获取存储的数值。
<template>
<div class="child1">
<h1>孙子组件</h1>
<p>{{car}}</p>
<button @click="updateCar">更新数据</button>
</div>
</template>
<script setup lang="ts">
import {inject} from 'vue';
//注入祖先组件提供数据
//需要参数:即为祖先提供数据的key
let car = inject('TOKEN');
const updateCar = ()=>{
car.value = '自行车';
}
</script>
(1.8)pinia
中文文档:Pinia 中文文档
pinia也是集中式管理状态容器,类似于vuex。但是核心概念没有mutation、modules,使用方式参照官网。
简单使用:
需要定义一个store,
定义一个仓库
//创建大仓库
import { createPinia } from 'pinia';
//createPinia方法可以用于创建大仓库
let store = createPinia();
//对外暴露,安装仓库
export default store;
定义一个小仓库(大仓库管理小仓库),都得要暴露方法
//定义info小仓库
import { defineStore } from "pinia";
//第一个仓库:小仓库名字 第二个参数:小仓库配置对象
//defineStore方法执行会返回一个函数,函数作用就是让组件可以获取到仓库数据
let useInfoStore = defineStore("info", {
//存储数据:state
state: () => {
return {
count: 99,
arr: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
}
},
actions: {
//注意:函数没有context上下文对象
//没有commit、没有mutations去修改数据
updateNum(a: number, b: number) {
this.count += a;
}
},
getters: {
total() {
let result:any = this.arr.reduce((prev: number, next: number) => {
return prev + next;
}, 0);
return result;
}
}
});
//对外暴露方法
export default useInfoStore;
子组件:,通过useInfoStore获取到小仓库数据、方法
<template>
<div class="child">
<h1>{{ infoStore.count }}---{{infoStore.total}}</h1>
<button @click="updateCount">点击我修改仓库数据</button>
</div>
</template>
<script setup lang="ts">
import useInfoStore from "../../store/modules/info";
//获取小仓库对象
let infoStore = useInfoStore();
console.log(infoStore);
//修改数据方法
const updateCount = () => {
//仓库调用自身的方法去修改仓库的数据
infoStore.updateNum(66,77);
};
</script>
子组件2(组合式api)
方法可以写在return,可以通过引用computed
//定义组合式API仓库
import { defineStore } from "pinia";
import { ref, computed,watch} from 'vue';
//创建小仓库
let useTodoStore = defineStore('todo', () => {
let todos = ref([{ id: 1, title: '吃饭' }, { id: 2, title: '睡觉' }, { id: 3, title: '打豆豆' }]);
let arr = ref([1,2,3,4,5]);
const total = computed(() => {
return arr.value.reduce((prev, next) => {
return prev + next;
}, 0)
})
//务必要返回一个对象:属性与方法可以提供给组件使用
return {
todos,
arr,
total,
updateTodo() {
todos.value.push({ id: 4, title: '组合式API方法' });
}
}
});
export default useTodoStore;
<template>
<div class="child1">
{{ infoStore.count }}
<p @click="updateTodo">{{ todoStore.arr }}{{todoStore.total}}</p>
</div>
</template>
<script setup lang="ts">
import useInfoStore from "../../store/modules/info";
//获取小仓库对象
let infoStore = useInfoStore();
//引入组合式API函数仓库
import useTodoStore from "../../store/modules/todo";
let todoStore = useTodoStore();
//点击p段落去修改仓库的数据
const updateTodo = () => {
todoStore.updateTodo();
};
</script>
<style scoped>
.child1 {
width: 200px;
height: 200px;
background: hotpink;
}
</style>
1.9)slot
插槽:默认插槽、具名插槽、作用域插槽可以实现父子组件通信.
1️⃣默认插槽:
在子组件内部的模板中书写slot全局组件标签
<template>
<div class="box">
<h1>我是子组件默认插槽</h1>
<!-- 默认插槽 -->
<slot></slot>
<h1>我是子组件默认插槽</h1>
<h1>具名插槽填充数据</h1>
<slot name="a"></slot>
<h1>具名插槽填充数据</h1>
<h1>具名插槽填充数据</h1>
<slot name="b"></slot>
<h1>具名插槽填充数据</h1>
</div>
在父组件内部提供结构:Todo即为子组件,在父组件内部使用的时候,在双标签内部书写结构传递给子组件
注意 :开发项目的时候默认插槽一般只有一个。
2️⃣具名插槽:
顾名思义,此插槽带有名字在组件内部留多个指定名字的插槽。
下面是一个子组件内部,模板中留两个插槽。
<template>
<div class="box">
<h1>具名插槽填充数据</h1>
<slot name="a"></slot>
<h1>具名插槽填充数据</h1>
<h1>具名插槽填充数据</h1>
<slot name="b"></slot>
<h1>具名插槽填充数据</h1>
</div>
父组件内部向指定的具名插槽传递结构。需要注意 v-slot:可以替换为 #
<template>
<div>
<h1>slot</h1>
<Todo>
<template v-slot:a> //可以用#a替换
<div>填入组件A部分的结构</div>
</template>
<template v-slot:b>//可以用#b替换
<div>填入组件B部分的结构</div>
</template>
</Todo>
</div>
</template>
<script setup lang="ts">
import Todo from "./Todo.vue";
作用域插槽
作用域插槽:可以理解为,子组件数据由父组件提供,但是子组件内部决定不了自身结构与外观(样式)
子组件Todo代码如下:
<template>
<div>
<h1>todo</h1>
<ul>
<!--组件内部遍历数组-->
<li v-for="(item,index) in todos" :key="item.id">
<!--作用域插槽将数据回传给父组件-->
<slot :$row="item" :$index="index"></slot>
</li>
</ul>
</div>
</template>
<script setup lang="ts">
defineProps(['todos']);//接受父组件传递过来的数据
</script>
<style scoped>
</style>
父组件内部代码如下:(父组件传todo过去给子组件,根据插槽可以决定自身结构或者外观(样式))
<template>
<div>
<h1>slot</h1>
<Todo :todos="todos">
<template v-slot="{$row,$index}">
<!--父组件决定子组件的结构与外观-->
<span :style="{color:$row.done?'green':'red'}">{{$row.title}}</span>
</template>
</Todo>
</div>
</template>
<script setup lang="ts">
import Todo from "./Todo.vue";
import { ref } from "vue";
//父组件内部数据
let todos = ref([
{ id: 1, title: "吃饭", done: true },
{ id: 2, title: "睡觉", done: false },
{ id: 3, title: "打豆豆", done: true },
]);
</script>
<style scoped>
</style>
一、项目的资源地址
贾成豪老师代码仓库地址:vue3_admin_template: 此仓库即为贾成豪老师源码仓库
项目在线文档:
服务器域名:http://sph-api.atguigu.cn
swagger文档:
echarts:国内镜像网站
二、搭建后台管理系统模板
(1)项目初始化
一个项目要有统一的规范,需要使用 eslint + stylelint + prettier 来对我们的代码质量做检测和修复,需要使用 husky 来做 commit 拦截,需要使用 commitlint 来统一提交规范,需要使用 preinstall 来统一包管理工具。
1.环境准备
- node v16.14.2
- pnpm 8.0.0
2.初始化项目
本项目使用vite进行构建,vite官方中文文档参考:cn.vitejs.dev/guide/
pnpm:performant npm ,意味“高性能的 npm”。pnpm由npm/yarn衍生而来,解决了npm/yarn内部潜在的bug,极大的优化了性能,扩展了使用场景。被誉为“最先进的包管理工具”
(2)项目配置
1.eslint 配置
eslint中文官网:Getting Started with ESLint - ESLint中文
ESLint最初是由Nicholas C. Zakas 于2013年6月创建的开源项目。它的目标是提供一个插件化的javascript代码检测工具。
安装eslint
pnpm i eslint -D
生成配置文件:.eslint.cjs
npx eslint --init
module.exports = {
//运行环境
"env": {
"browser": true,//浏览器端
"es2021": true,//校验语法 es2021
},
//规则继承
"extends": [
//全部规则默认是关闭的,这个配置项开启推荐规则,推荐规则参照文档
//比如:函数不能重名、对象不能出现重复key
"eslint:recommended",
//vue3语法规则
"plugin:vue/vue3-essential",
//ts语法规则
"plugin:@typescript-eslint/recommended"
],
//要为特定类型的文件指定处理器
"overrides": [
],
//指定解析器:解析器
//Esprima 默认解析器
//Babel-ESLint babel解析器
//@typescript-eslint/parser ts解析器
"parser": "@typescript-eslint/parser",
//指定解析器选项
"parserOptions": {
"ecmaVersion": "latest",//校验ECMA最新版本
"sourceType": "module"//设置为"script"(默认),或者"module"代码在ECMAScript模块中
},
//ESLint支持使用第三方插件。在使用插件之前,您必须使用npm安装它
//该eslint-plugin-前缀可以从插件名称被省略
"plugins": [
"vue",
"@typescript-eslint"
],
//eslint规则
"rules": {
}
}
vue3环境代码校验插件
# 让所有与prettier规则存在冲突的Eslint rules失效,并使用prettier进行代码检查
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-node": "^11.1.0",
# 运行更漂亮的Eslint,使prettier规则优先级更高,Eslint优先级低
"eslint-plugin-prettier": "^4.2.1",
# vue.js的Eslint插件(查找vue语法错误,发现错误指令,查找违规风格指南
"eslint-plugin-vue": "^9.9.0",
# 该解析器允许使用Eslint校验所有babel code
"@babel/eslint-parser": "^7.19.1",
vue3 环境代码校验插件
# 让所有与prettier规则存在冲突的Eslint rules失效,并使用prettier进行代码检查
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-node": "^11.1.0",
# 运行更漂亮的Eslint,使prettier规则优先级更高,Eslint优先级低
"eslint-plugin-prettier": "^4.2.1",
# vue.js的Eslint插件(查找vue语法错误,发现错误指令,查找违规风格指南
"eslint-plugin-vue": "^9.9.0",
# 该解析器允许使用Eslint校验所有babel code
"@babel/eslint-parser": "^7.19.1",
安装指令
pnpm install -D eslint-plugin-import eslint-plugin-vue eslint-plugin-node eslint-plugin-prettier eslint-config-prettier eslint-plugin-node @babel/eslint-parser
1.2修改.eslintrc.cjs配置文件
// @see https://eslint.bootcss.com/docs/rules/
安装指令
pnpm install -D eslint-plugin-import eslint-plugin-vue eslint-plugin-node eslint-plugin-prettier eslint-config-prettier eslint-plugin-node @babel/eslint-parser
修改.eslintrc.cjs配置文件
module.exports = {
env: {
browser: true,
es2021: true,
node: true,
jest: true,
},
/* 指定如何解析语法 */
parser: 'vue-eslint-parser',
/** 优先级低于 parse 的语法解析配置 */
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
parser: '@typescript-eslint/parser',
jsxPragma: 'React',
ecmaFeatures: {
jsx: true,
},
},
/* 继承已有的规则 */
extends: [
'eslint:recommended',
'plugin:vue/vue3-essential',
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
plugins: ['vue', '@typescript-eslint'],
/*
* "off" 或 0 ==> 关闭规则
* "warn" 或 1 ==> 打开的规则作为警告(不影响代码执行)
* "error" 或 2 ==> 规则作为一个错误(代码不能执行,界面报错)
*/
rules: {
// eslint(https://eslint.bootcss.com/docs/rules/)
'no-var': 'error', // 要求使用 let 或 const 而不是 var
'no-multiple-empty-lines': ['warn', { max: 1 }], // 不允许多个空行
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-unexpected-multiline': 'error', // 禁止空余的多行
'no-useless-escape': 'off', // 禁止不必要的转义字符
// typeScript (https://typescript-eslint.io/rules)
'@typescript-eslint/no-unused-vars': 'error', // 禁止定义未使用的变量
'@typescript-eslint/prefer-ts-expect-error': 'error', // 禁止使用 @ts-ignore
'@typescript-eslint/no-explicit-any': 'off', // 禁止使用 any 类型
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-namespace': 'off', // 禁止使用自定义 TypeScript 模块和命名空间。
'@typescript-eslint/semi': 'off',
// eslint-plugin-vue (https://eslint.vuejs.org/rules/)
'vue/multi-word-component-names': 'off', // 要求组件名称始终为 “-” 链接的单词
'vue/script-setup-uses-vars': 'error', // 防止<script setup>使用的变量<template>被标记为未使用
'vue/no-mutating-props': 'off', // 不允许组件 prop的改变
'vue/attribute-hyphenation': 'off', // 对模板中的自定义组件强制执行属性命名样式
},
}
eslintignore 忽略文件
dist
node_modules
运行脚本
package.json新增两个运行脚本
"scripts": {
"lint": "eslint src",
"fix": "eslint src --fix",
}
2.项目配置
有了eslint,为什么还要有prettier?eslint针对的是javascript,他是一个检测工具,包含js语法以及少部分格式问题,在eslint看来,语法对了就能保证代码正常运行,格式问题属于其次;
而prettier属于格式化工具,它看不惯格式不统一,所以它就把eslint没干好的事接着干,另外,prettier支持
包含js在内的多种语言。
总结起来,eslint和prettier这俩兄弟一个保证js代码质量,一个保证代码美观。
安装依赖包
pnpm install -D eslint-plugin-prettier prettier eslint-config-prettier
prettierrc.json添加规则
{
"singleQuote": true, //字符串全为单引号
"semi": false,//无需分号
"bracketSpacing": true,
"htmlWhitespaceSensitivity": "ignore",
"endOfLine": "auto",
"trailingComma": "all",
"tabWidth": 2
}
.prettierignore忽略文件
/dist/*
/html/*
.local
/node_modules/**
**/*.svg
**/*.sh
/public/*
通过 pnpm run lint 去检测语法,如果出现不规范格式,通过 pnpm run fix 修改
3.配置stylelint
stylelint 为 css的lint工具。可格式化css代码,检查css语法错误与不合理的写法,指定css书写顺序等。
项目中使用scss作为预处理器,安装以下依赖:
pnpm add sass sass-loader stylelint postcss postcss-scss postcss-html stylelint-config-prettier stylelint-config-recess-order stylelint-config-recommended-scss stylelint-config-standard stylelint-config-standard-vue stylelint-scss stylelint-order stylelint-config-standard-scss -D
.stylelintrc.cjs配置文件
// @see https://stylelint.bootcss.com/
module.exports = {
extends: [
'stylelint-config-standard', // 配置stylelint拓展插件
'stylelint-config-html/vue', // 配置 vue 中 template 样式格式化
'stylelint-config-standard-scss', // 配置stylelint scss插件
'stylelint-config-recommended-vue/scss', // 配置 vue 中 scss 样式格式化
'stylelint-config-recess-order', // 配置stylelint css属性书写顺序插件,
'stylelint-config-prettier', // 配置stylelint和prettier兼容
],
overrides: [
{
files: ['**/*.(scss|css|vue|html)'],
customSyntax: 'postcss-scss',
},
{
files: ['**/*.(html|vue)'],
customSyntax: 'postcss-html',
},
],
ignoreFiles: [
'**/*.js',
'**/*.jsx',
'**/*.tsx',
'**/*.ts',
'**/*.json',
'**/*.md',
'**/*.yaml',
],
/**
* null => 关闭该规则
* always => 必须
*/
rules: {
'value-keyword-case': null, // 在 css 中使用 v-bind,不报错
'no-descending-specificity': null, // 禁止在具有较高优先级的选择器后出现被其覆盖的较低优先级的选择器
'function-url-quotes': 'always', // 要求或禁止 URL 的引号 "always(必须加上引号)"|"never(没有引号)"
'no-empty-source': null, // 关闭禁止空源码
'selector-class-pattern': null, // 关闭强制选择器类名的格式
'property-no-unknown': null, // 禁止未知的属性(true 为不允许)
'block-opening-brace-space-before': 'always', //大括号之前必须有一个空格或不能有空白符
'value-no-vendor-prefix': null, // 关闭 属性值前缀 --webkit-box
'property-no-vendor-prefix': null, // 关闭 属性前缀 -webkit-mask
'selector-pseudo-class-no-unknown': [
// 不允许未知的选择器
true,
{
ignorePseudoClasses: ['global', 'v-deep', 'deep'], // 忽略属性,修改element默认样式的时候能使用到
},
],
},
}
.stylelintignore忽略文件
/node_modules/*
/dist/*
/html/*
/public/*
添加运行脚本
"scripts": {
"lint:style": "stylelint src/**/*.{css,scss,vue} --cache --fix"
}
最后配置统一的prettier来格式化我们的js和css,html代码
"scripts": {
"dev": "vite --open",
"build": "vue-tsc && vite build",
"preview": "vite preview",
"lint": "eslint src",
"fix": "eslint src --fix",
"format": "prettier --write \"./**/*.{html,vue,ts,js,json,md}\"",
"lint:eslint": "eslint src/**/*.{ts,vue} --cache --fix",
"lint:style": "stylelint src/**/*.{css,scss,vue} --cache --fix"
},
当我们运行 pnpm run format 的时候,会把代码直接格式化。
4.配置husky
在上面我们已经集成好了我们代码校验工具,但是需要每次手动的去执行命令才会格式化我们的代码。如果有人没有格式化就提交了远程仓库中,那这个规范就没什么用。所以我们需要强制让开发人员按照代码规范来提交。
要做到这件事情,就需要利用husky在代码提交之前触发 git hook (git在客户端的钩子),然后执行pnpm run format 来自动的格式化我们的代码。
安装husky
pnpm install -D husky
会在根目录下生成个一个.husky目录,在这个目录下面会有一个pre-commit文件,这个文件里面的命令在我们执行commit的时候就会执行
在.husky/pre-commit文件添加如下命令:
npx husky-init
会在根目录下生成个一个.husky目录,在这个目录下面会有一个pre-commit文件,这个文件里面的命令在我们执行commit的时候就会执行
在.husky/pre-commit文件添加如下命令:
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
pnpm run format
git语句:
创建了仓库之后,先初始化git init
关联,git remote add origin [仓库地址]
git show remote 命令查看关联情况 :git remote show origin
添加当前目录所有文件到本地git仓:git add .
git commit 提交本地文件到本地git仓:git commit -m '提交代码'
推送:git push / git push -u origin master
三、项目集成
3.1 集成element-plus
硅谷甄选运营平台,UI组件库采用的element-plus,因此需要集成element-plus插件!!!
官网地址:一个 Vue 3 UI 框架 | Element Plus
pnpm install element-plus @element-plus/icons-vue
入口文件main.ts全局安装element-plus,element-plus默认支持语言英语设置为中文
import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css'
//@ts-ignore忽略当前文件ts类型的检测否则有红色提示(打包会失败)
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
app.use(ElementPlus, {
locale: zhCn
})
Element Plus全局组件类型声明
// tsconfig.json
{
"compilerOptions": {
// ...
"types": ["element-plus/global"]
}
}
配置完毕可以测试element-plus组件与图标的使用。
3.2 src别名的配置
在开发项目的时候文件与文件关系可能很复杂,因此我们需要给src文件夹配置一个别名!!!
// vite.config.ts
import {defineConfig} from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
"@": path.resolve("./src") // 相对路径别名配置,使用 @ 代替 src
}
}
})
TypeScript 编译配置
// tsconfig.json
{
"compilerOptions": {
"baseUrl": "./", // 解析非相对模块的基地址,默认是当前目录
"paths": { //路径映射,相对于baseUrl
"@/*": ["src/*"]
}
}
}
3.3环境变量的配置
项目开发过程中,至少会经历开发环境、测试环境和生产环境(即正式环境)三个阶段。不同阶段请求的状态(如接口地址等)不尽相同,若手动切换接口地址是相当繁琐且易出错的。于是环境变量配置的需求就应运而生,我们只需做简单的配置,把环境状态切换的工作交给代码。
开发环境(development)
顾名思义,开发使用的环境,每位开发人员在自己的dev分支上干活,开发到一定程度,同事会合并代码,进行联调。
测试环境(testing)
测试同事干活的环境啦,一般会由测试同事自己来部署,然后在此环境进行测试
生产环境(production)
生产环境是指正式提供对外服务的,一般会关掉错误报告,打开错误日志。(正式提供给客户使用的环境。)
注意:一般情况下,一个环境对应一台服务器,也有的公司开发与测试环境是一台服务器!!!
项目根目录分别添加 开发、生产和测试环境的文件!
.env.development
.env.production
.env.test
创建一个test测试环境,development和production同理
# 标量必须以VITE_ 为前缀才能暴漏给外部读取
NODE_ENV = 'test'
VITE_APP_TITLE = '叶叶运营平台'
VITE_APP_BASE_API = '/dev-production'
VITE_SERVE=''
可通过console.log(import.meta.env);去检查环境内容
3.4 SVG图标配置
在开发项目的时候经常会用到svg矢量图,而且我们使用SVG以后,页面上加载的不再是图片资源,
这对页面性能来说是个很大的提升,而且我们SVG文件比img要小的很多,放在项目中几乎不占用资源。
pnpm install vite-plugin-svg-icons -D
测试SVG图标使用
svg:图标外层容器节点, 内部需要与use标签结合使用
<svg>
<!-- 测试SVG图标使用
svg:图标外层容器节点,内部需要与use标签结合使用 -->
<!-- xlink:href执行用哪一个图标,属性值务必#icon-图标名字 -->
<!-- use标签fill属性可以设置图标的颜色 -->
<use xlink:href="#icon-icon" fill="red"></use>
</svg>
在vite.config.ts中配置插件
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import path from 'path'
export default () => {
return {
plugins: [
createSvgIconsPlugin({
// Specify the icon folder to be cached
iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
// Specify symbolId format
symbolId: 'icon-[dir]-[name]',
}),
],
}
}
入口文件main.ts导入
import 'virtual:svg-icons-register'
svg封装为全局组件
因为项目很多模块需要使用图标,因此把它封装为全局组件!!!
在src/components目录下创建一个SvgIcon组件:代表如下
<template>
<div>
<svg :style="{ width: width, height: height }">
<use :xlink:href="prefix + name" :fill="color"></use>
</svg>
</div>
</template>
<script setup lang="ts">
defineProps({
//xlink:href属性值的前缀
prefix: {
type: String,
default: '#icon-'
},
//svg矢量图的名字
name: String,
//svg图标的颜色
color: {
type: String,
default: ""
},
//svg宽度
width: {
type: String,
default: '16px'
},
//svg高度
height: {
type: String,
default: '16px'
}
})
</script>
<style scoped></style>
在components文件夹目录下创建一个index.ts文件:用于注册components文件夹内部全部全局组件!!
//引入项目钟全部的全局组件
import SvgIcon from './SvgIcon/index.vue'
import Pagination from './Pagination/index.vue'
//全局对象
const allGloablComponent = {SvgIcon,Pagination };
console.log(allGloablComponent,'123')
//对外暴漏插件对象
export default {
//务必叫做install方法
install(app){
console.log(app)
//注册项目全部的全局组件
Object.keys(allGloablComponent).forEach(key =>{
//注册为全局组件
console.log(key,'22')
app.component(key,allGloablComponent[key])
})
}
}
通过循环吧components里面组件直接注册成全局,不需要引用就可以直接使用
<template>
<div class="box">
<h1>测试svg</h1>
<Pagination></Pagination>
</div>
</template>
在main.ts引入,打开的时候自动引用上去
import gloablComponent from './components/index';
app.use(gloablComponent);
3.5 集成sass
我们目前在组件内部已经可以使用scss样式,因为在配置styleLint工具的时候,项目当中已经安装过sass sass-loader,因此我们再组件内可以使用scss语法!!!需要加上lang="scss"
<style scoped lang="scss"></style>
接下来我们为项目添加一些全局的样式
在src/styles目录下创建一个index.scss文件,当然项目中需要用到清除默认样式,因此在index.scss引入reset.scss
@import reset.scss
在入口文件引入
import '@/styles'
但是你会发现在src/styles/index.scss全局样式文件中没有办法使用$变量.因此需要给项目中引入全局变量$.
在style/variable.scss创建一个variable.scss文件!
在vite.config.ts文件配置如下:
export default defineConfig((config) => {
css: {
preprocessorOptions: {
scss: {
javascriptEnabled: true,
additionalData: '@import "./src/styles/variable.scss";',
},
},
},
}
}
@import "./src/styles/variable.less";后面的;不要忘记,不然会报错!
配置完毕你会发现scss提供这些全局变量可以在组件样式中使用了!!
3.6 mock数据
安装依赖: https://www.npmjs.com/package/vite-plugin-mock
pnpm install -D vite-plugin-mock mockjs
mock中配置文件会报错,是因为vite-plugin-mock版本匹配导致的,可以 pnpm i vite-plugin-mock@2.0.0
还有pinia最新版也不匹配 可以安装时候 pnpm i pinia@2.0.34
createUserList:次函数执行会返回一个数组,数组里面包含两个用户信息。
对外暴露一个数组:数组里面包含两个接口
登录假的接口
获取用户信息的假的接口
在 vite.config.js 配置文件启用插件。
mport { UserConfigExport, ConfigEnv } from 'vite'
import { viteMockServe } from 'vite-plugin-mock'
import vue from '@vitejs/plugin-vue'
export default ({ command })=> {
return {
plugins: [
vue(),
viteMockServe({
localEnabled: command === 'serve',
}),
],
}
}
在根目录创建mock文件夹:去创建我们需要mock数据与接口!!!
在mock文件夹内部创建一个user.ts文件
//用户信息数据
function createUserList() {
return [
{
userId: 1,
avatar:
'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
username: 'admin',
password: '111111',
desc: '平台管理员',
roles: ['平台管理员'],
buttons: ['cuser.detail'],
routes: ['home'],
token: 'Admin Token',
},
{
userId: 2,
avatar:
'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
username: 'system',
password: '111111',
desc: '系统管理员',
roles: ['系统管理员'],
buttons: ['cuser.detail', 'cuser.user'],
routes: ['home'],
token: 'System Token',
},
]
}
export default [
// 用户登录接口
{
url: '/api/user/login',//请求地址
method: 'post',//请求方式
response: ({ body }) => {
//获取请求体携带过来的用户名与密码
const { username, password } = body;
//调用获取用户信息函数,用于判断是否有此用户
const checkUser = createUserList().find(
(item) => item.username === username && item.password === password,
)
//没有用户返回失败信息
if (!checkUser) {
return { code: 201, data: { message: '账号或者密码不正确' } }
}
//如果有返回成功信息
const { token } = checkUser
return { code: 200, data: { token } }
},
},
// 获取用户信息
{
url: '/api/user/info',
method: 'get',
response: (request) => {
//获取请求头携带token
const token = request.headers.token;
//查看用户信息是否包含有次token用户
const checkUser = createUserList().find((item) => item.token === token)
//没有返回失败的信息
if (!checkUser) {
return { code: 201, data: { message: '获取用户信息失败' } }
}
//如果有返回成功信息
return { code: 200, data: {checkUser} }
},
},
]
安装axios
pnpm install axios
最后通过axios测试接口!!
3.7 axios二次封装
在开发项目的时候避免不了与后端进行交互,因此我们需要使用axios插件实现发送网络请求。在开发项目的时候
我们经常会把axios进行二次封装。
目的:
1:使用请求拦截器,可以在请求拦截器中处理一些业务(开始进度条、请求头携带公共参数)
2:使用响应拦截器,可以在响应拦截器中处理一些业务(进度条结束、简化服务器返回的数据、处理http网络错误)
在根目录下创建utils/request.ts
import axios from "axios";
import { ElMessage } from "element-plus";
//创建axios实例
let request = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_API,
timeout: 5000
})
//请求拦截器
request.interceptors.request.use(config => {
return config;
});
//响应拦截器
request.interceptors.response.use((response) => {
return response.data;
}, (error) => {
//处理网络错误
let msg = '';
let status = error.response.status;
switch (status) {
case 401:
msg = "token过期";
break;
case 403:
msg = '无权访问';
break;
case 404:
msg = "请求地址错误";
break;
case 500:
msg = "服务器出现问题";
break;
default:
msg = "无网络";
}
ElMessage({
type: 'error',
message: msg
})
return Promise.reject(error);
});
export default request;
3.8API接口统一管理
在开发项目的时候,接口可能很多需要统一管理。在src目录下去创建api文件夹去统一管理项目的接口;
比如:下面方式
//统一管理咱们项目用户相关的接口
import request from '@/utils/request'
import type {
loginFormData,
loginResponseData,
userInfoReponseData,
} from './type'
//项目用户相关的请求地址
enum API {
LOGIN_URL = '/admin/acl/index/login',
USERINFO_URL = '/admin/acl/index/info',
LOGOUT_URL = '/admin/acl/index/logout',
}
//登录接口
export const reqLogin = (data: loginFormData) =>
request.post<any, loginResponseData>(API.LOGIN_URL, data)
//获取用户信息
export const reqUserInfo = () =>
request.get<any, userInfoReponseData>(API.USERINFO_URL)
//退出登录
export const reqLogout = () => request.post<any, any>(API.LOGOUT_URL)
四、项目基本搭建
4.1、路由配置
需要至少四个一级路由:主页、登入页、404页、任意路由(指向404)
安装路由插件:4版本(可以去看官网)
pnpm i vue-router
src下新建文件夹views
分别创建404、login、home路由组件
src下新建router文件夹---包含index.ts和routes.ts
//对外暴露配置路由(常量路由)
export const constantRoute = [
{//登录
path: '/login',//路径
component: () => import('@/views/login/index.vue'),//组件
name: 'login',//命名路由
},
{
// 登录成功以后展示数据的路由
path: '/',
component: () => import('@/views/home/index.vue'),
name: 'layout',
},
{
path: '/404',
component: () => import('@/views/404/index.vue'),
name: '404',
},
{
//任意路由
path:'/:pathMatch(.*)*',//上面都没有匹配上的话,就重回到404页面
redirect:'/404',//重定向到404页面
name:'any',
}
]
// 通过 vue-router 插件实现模板路由配置
import { createRouter, createWebHashHistory } from "vue-router";
import { constantRoute } from "./routes";
// 创建路由器
let router = createRouter({
// 路由模式hash
history: createWebHashHistory(),//用hash模式创建
routes: constantRoute,//注意这里是routes
// 滚动行为
scrollBehavior() {
return {
left: 0,
top: 0
}
}
})
export default router;
在入口文件去调用router
//引入路由
import router from './router'
//引入路由
app.use(router);
// 记得不要放在app.mount后面,不然无法调用
// 讲应用挂载到挂载点
app.mount('#app')
4.2、登入界面搭建
使用element布局搭建
栅格布局:span是占的分数,xs是屏幕小于760时占的分数,栅格一共是24份
使用icon图表组件:User、Lock(前缀图标:prefix-icon、后缀图标、切换密码图标show-password)
登入的静态模板组件 src/views/login/index.vue
<template>
<div class="login_container">
<el-row>
<el-col :span="12" :xs="0">左侧占位的</el-col>
<el-col :span="12" :xs="24">
<el-form class="login_form">
<h1>Hello!</h1>
<h2>Welcome to Y2K甄选</h2>
<el-form-item>
<el-input :prefix-icon="User" v-model="loginForm.username"></el-input>
</el-form-item>
<el-form-item>
<el-input type="password" :prefix-icon="Lock" v-model="loginForm.password" show-password></el-input>
</el-form-item>
<el-form-item>
<el-button :loading="loading" type="primary" class="login_btn">登入</el-button>
</el-form-item>
</el-form>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
// 输入框前置的图标
import { User, Lock } from '@element-plus/icons-vue'
import { reactive } from 'vue';
//收集账号密码
let loginForm = reactive({ username: 'admin', password: '111111' })
</script>
<style scoped lang="scss">
.login_container {
width: 100%;
height: 100vh;
background: url('@/assets/images/background.jpg') no-repeat;
background-size: cover;
.login_form {
position: relative;
width: 80%;
top: 30vh;
background: url('@/assets/images/login_form.png') no-repeat;
background-size: cover;
padding: 40px;
h1 {
color: white;
font-size: 40px;
}
h2 {
color: white;
font-size: 20px;
margin: 20px 0px;
}
.login_btn {
width: 100%;
}
}
}
</style>
4.3模板封装的业务,pinia存储
点击登入之后需要在回调函数里做的事
- 通知仓库发送登入请求
- 请求成功:首页展示数据
- 请求失败:弹出错误信息
首先安装pinia仓库
pnpm i pinia@2.0.34//用低版本做到匹配
新建大仓库文件 src/store/index.ts
//仓库大仓库
import { createPinia } from 'pinia';
//创建大仓库
const pinia = createPinia();
//对外暴露:入口文件需要安装仓库
export default pinia;
入口文件main.ts:
import pinia from '@/store'
app.use(pinia)
创建小仓库 src/store/modules/user.ts
存储永久性的token
//创建用户相关的小仓库
import {defineStore} from 'pinia'
// 引入接口
import {reqLogin} from '@/api/user'
//import {SET_TOKEN} from '@/utils/token'
//引入数据类型
import type {loginForm} from '@/api/user/type.ts'
//创建用户小仓库
const useUserStore = defineStore('User', {
// 小仓库存储数据
state: () => {
return {
token: localStorage.getItem('TOKEN'),
}
},
// 异步|逻辑 地方
actions: {
//用户登录的方法
async userLogin(data: loginForm) {
//登录请求
let result:any = await reqLogin(data)
console.log(result, '123')
//登录请求:成功200->token
//登录请求:失败201->登录失败错误的信息
if (result.code == 200) {
// //pinia仓库存储一下token
// //由于pinia|vuex存储数据其实利用js对象
this.token = result.data.token;
// //本地存储持久化存储一份
localStorage.setItem("TOKEN",result.data.token)
// SET_TOKEN(result.data as string)
// //能保证当前async函数返回一个成功的promise
return Promise.resolve('ok')
} else {
return Promise.reject(new Error(result.data.message));
}
},
},
getters: {},
})
//对外暴露获取小仓库方法
export default useUserStore
登入页面中引入小仓库,点击登入时,通知user小仓库发请求,存数据
路径:project\src\views\login\index.vue
userLogin这个函数会返回一个promise结果,根据结果来进行下一步(try或者点than写法都可)
<template>
<div class="login_container">
<el-row>
<el-col :span="12" :xs="0">左侧占位的</el-col>
<el-col :span="12" :xs="24">
<el-form class="login_form">
<h1>Hello!</h1>
<h2>Welcome to Y2K甄选</h2>
<el-form-item>
<el-input :prefix-icon="User" v-model="loginForm.username"></el-input>
</el-form-item>
<el-form-item>
<el-input type="password" :prefix-icon="Lock" v-model="loginForm.password" show-password></el-input>
</el-form-item>
<el-form-item>
<el-button :loading="loading" type="primary" class="login_btn" @click="login">登入</el-button>
</el-form-item>
</el-form>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
// 输入框前置的图标
import {User, Lock} from '@element-plus/icons-vue'
import {reactive, ref} from 'vue';
import {useRouter} from 'vue-router';
import {ElNotification} from "element-plus";
//引入用户相关的小仓库
import useUserStore from "@/store/modules/user"
//定义变量加载控制按钮效果
let loading = ref(false);
let useStore = useUserStore();
//获取路由器
let $router = useRouter();
//收集账号密码
let loginForm = reactive({username: 'admin', password: '111111'})
const login = async () => {
//点击登录按钮
//开始加载效果
loading.value = true;
// useStore.userLogin(loginForm);
//通知仓库发送登录请求
//请求成功-》跳转到首页展示数据的地方
//请求失败-》提示错误信息
try {
await useStore.userLogin(loginForm);
//编程式导航跳转到展示数据首页
$router.push('/');
// 登录成功提示信息
ElNotification({
type: 'success',
message: '欢迎回来',
})
// 登录成功,加载效果也消失
loading.value = false;
} catch (error) {
//登录失败,加载效果消失
loading.value = false;
console.log('请求失败');
//登录失败信息
ElNotification({
type: 'error',
message: (error as Error).message,
})
}
console.log(loginForm, '123');
}
</script>
<style scoped lang="scss">
.login_container {
width: 100%;
height: 100vh;
background: url('@/assets/images/background.jpg') no-repeat;
background-size: cover;
.login_form {
position: relative;
width: 80%;
top: 30vh;
background: url('@/assets/images/login_form.png') no-repeat;
background-size: cover;
padding: 40px;
h1 {
color: white;
font-size: 40px;
}
h2 {
color: white;
font-size: 20px;
margin: 20px 0px;
}
.login_btn {
width: 100%;
}
}
}
</style>
这里遇到的问题:
//引入用户相关的小仓库
import useUserStore from "@/store/modules/user" 不能写成
import {useUserStore} from "@/store/modules/user" 否则找不到模块
用户仓库数据的ts类型定义
project\src\store\modules\types\type.ts
// 定义小仓库数据state类型
export interface UserState {
token: null | string
}
4.4用户仓库数据的ts类型定义
// 定义小仓库数据state类型
// 定义小仓库数据state类型
export interface UserState {
token: null | string
}
登入接口返回的数据类型
interface dataType{
token?:string,
message?:string,
}
//登录接口返回数据类型
export interface loginResponseData{
code:number,
data:dataType
}
封装本地存储方法以及读取数据方法
// 封装本地存储存储数据和读取数据的方法
//存储数据
export const SET_TOKEN = (token: string) =>{
localStorage.setItem('TOKEN',token);
}
//本地获取数据
export const GET_TOKEN = () =>{
return localStorage.getItem('TOKEN');
}
4.5登入时间的判断与封装
export const getTime = () => {
let message = '';
// 通过内置构造函数 Date 获取当前时间
let hours = new Date().getHours();
console.log(hours)
if(hours <= 6){
message = '凌晨'
}else if (hours <= 9){
message = '早上'
}else if (hours <= 12){
message = '上午'
}else if (hours <= 18){
message = '下午'
}else{
message = '晚上'
}
// console.log(message)
return message;
}
登录界面使用封装的时间方法
//引入获取当前时间的函数
import {getTime} from "@/utils/time";
try {
await useStore.userLogin(loginForm);
//编程式导航跳转到展示数据首页
$router.push('/');
// 登录成功提示信息
ElNotification({
type: 'success',
title: `HI,${getTime()}好`,//封装出来的数据放在这里体现
message: '欢迎回来',
})
// 登录成功,加载效果也消失
loading.value = false;
} catch (error) {
//登录失败,加载效果消失
loading.value = false;
console.log('请求失败');
//登录失败信息
ElNotification({
type: 'error',
message: (error as Error).message,
})
}
4.6登录模块表单校验
利用element-plus的表单认证
在 <el-form>添加 :rules="rules" ref="loginForms",loginForms需要引用
el-form-item文本框加入prop="验证”,自定义规则
<el-col :span="12" :xs="24">
<el-form class="login_form" :model="loginForm" :rules="rules" ref="loginForms">
<h1>Hello!</h1>
<h2>Welcome to Y2K甄选</h2>
<el-form-item prop="username">
<el-input :prefix-icon="User" v-model="loginForm.username"></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input type="password" :prefix-icon="Lock" v-model="loginForm.password" show-password></el-input>
</el-form-item>
<el-form-item>
<el-button :loading="loading" type="primary" class="login_btn" @click="login">登入</el-button>
</el-form-item>
</el-form>
</el-col>
//获取el-form组件
let loginForms = ref();
表单规则
//定义表单验证规则
const rules = {
//required代表该字段必填
//message代表错误提示信息
//trigger代表触发方式/校验表单时机,change->文本发生改变出发,blur->文本失去焦点触发
username: [
{required: true,min:6,max:12,message: '用户名长度为6-12位',trigger: 'change'}
],
password: [
{required: true,min:6,max:15,message: '密码长度6-15位', trigger: 'change'}
]
}
再点击登录的时候,会全部表单校验通过在发送请求
const login = async () => {
//async用于声明一个函数是异步的
//保证全部表单校验通过在发送请求
// console.log(loginForms.value.validate(),'asndioasjdn')
//await用于等待一个Promise对象的解析结果,并将其赋值给一个变量
//通过使用async和await,可以让代码以同步的方式进行异步操作
await loginForms.value.validate();
4.7自定义校验登录表单
用validator自定义表单规则,同样是element-plus带来的
const rules = {
//required代表该字段必填
//message代表错误提示信息
//trigger代表触发方式/校验表单时机,change->文本发生改变出发,blur->文本失去焦点触发
username: [
{trigger: 'change', validator: validateUsername}
// {required: true,min:6,max:12,message: '用户名长度为6-12位',trigger: 'change'}
],
password: [
// {required: true,min:6,max:15,message: '密码长度6-15位', trigger: 'change'}
{trigger: 'change', validator: validatePasssword}
]
}
同样引用:rules="rules",prop="username",prop="password"
<el-col :span="12" :xs="24">
<el-form class="login_form" :model="loginForm" :rules="rules" ref="loginForms">
<h1>Hello!</h1>
<h2>Welcome to Y2K甄选</h2>
<el-form-item prop="username">
<el-input :prefix-icon="User" v-model="loginForm.username"></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input type="password" :prefix-icon="Lock" v-model="loginForm.password" show-password></el-input>
</el-form-item>
<el-form-item>
<el-button :loading="loading" type="primary" class="login_btn" @click="login">登入</el-button>
</el-form-item>
</el-form>
</el-col>
方法:
//rule即为校验规则对象
//value即为表单数据
//通过就callback返回一个函数,如果不符合条件也是callback方法,注入错误提示信息
//自定义校验规则函数
const validateUsername = (rule: any, value: string, callback: any) => {
//rule即为校验规则对象
//value即为表单数据
//通过就callback返回一个函数,如果不符合条件也是callback方法,注入错误提示信息
if(value.length>=5){
callback();
}else{
callback(new Error('用户名至少5位'));
}
}
const validatePasssword = (rule: any, value: string, callback: any) => {
//rule即为校验规则对象
//value即为表单数据
//通过就callback返回一个函数,如果不符合条件也是callback方法,注入错误提示信息
if(value.length>=6){
callback();
}else{
callback(new Error('密码长度至少6位'));
}
}
五:layout组件搭建
5.1layout组件搭建
<template>
<div class="layout_container">
<!-- 左侧菜单展示 -->
<div class="layout_slider">
111
</div>
<!-- 顶部导航 -->
<div class="layout_tabbar">
222
</div>
<!-- 内容展示区域 -->
<div class="layout_main">
<p style="height: 1000px;background: #e0c7e3" >111</p>
</div>
</div>
</template>
<script setup lang="ts">
</script>
<style scoped lang="scss">
.layout_container {
width: 100%;
height: 100vh;
background: blue;
.layout_slider {
width: $base-menu-width;//260px
height: 100vh;
background: $base-menu-background;
}
.layout_tabbar {
position: fixed;
//top: 0;
//right: 0;
left:$base-menu-width;
width: calc(100% - $base-menu-width);
height: $base-tabbar-height;//50x
background: $base-tabbar-background;
}
.layout_main {
position: absolute;
right: 0;
top: $base-tabbar-height;
width: calc(100% - $base-menu-width);
height: calc(100vh - $base-tabbar-height);
background: yellowgreen;
padding: 20px;
overflow: auto;//滚动条
}
}
</style>
//引入清楚默认样式
//用于清除默认样式。这可以帮助开发者自定义滚动条样式而不受浏览器默认样式的影响。
@import "./reset.scss";
//CSS伪元素选择器"::-webkit-scrollbar"来选择滚动条本身
//::-webkit-scrollbar"是一个Webkit私有的选择器,用于设置Webkit内核浏览器的滚动条样式。
// 这个选择器只能在Webkit内核的浏览器中生效,如Chrome、Safari等。
// 滚动条外观
::-webkit-scrollbar {
width: 10px;
}
::-webkit-scrollbar-track {
background: $base-menu-background;
}
::-webkit-scrollbar-thumb {
width: 10px;
background-color: yellowgreen;
border-radius: 10px;
}
//项目提供scss全局变量
$color: red;
$base-color: blue;
// layout组件
// 左侧菜单的宽度
$base-menu-width: 260px;
// 左侧菜单的颜色
$base-menu-background: #846e89;
// 顶部导航的高度
$base-tabbar-height: 50px;
// 顶部导航的颜色
$base-tabbar-background: #e0c7e3;
// 内容展示区域颜色
$base-main-background: #c6d182;
5.2Logo组件封装
引入图片logo在public/logo.jpg
src/layout/logo/index.vue 左侧的logo的子组件
引入setting
<template>
<div class="logo" v-if="setting.logoHidden">
<img :src="setting.logo" alt="">
<p>{{ setting.title }}</p>
</div>
</template>
<script setup lang="ts">
// 引入设置标题与图片的配置文件
import setting from '@/settings'
</script>
<style scoped lang="scss">
.logo {
display: flex;
width: 100%;
height: $base-menu-logo-height;
align-items: center;
color: wheat;
padding: 20px;
img {
width: 40px;
height: 40px;
}
p {
font-size: $base-logo-title-fontSize;
margin-left: 30px;
}
}
</style>
父组件引入子组件
<div class="layout_slider">
<logo></logo>
111
</div>
<script setup lang="ts">
//引入左侧菜单logo子组件
import Logo from './logo/index.vue';
添加全局的样式
//左侧logo高度设置
$base-menu-logo-height: 50px;
// 左侧菜单字体大小
$base-logo-title-fontSize: 20px;
Logo组件封装
设置图片与标题的配置文件(方便别入修改)
project\src\settings.ts
//用于项目logo|标题配置
export default {
title: 'y2k', //项目的标题
logo: '/public/logo.jpg', //项目logo设置
logoHidden: true, //logo组件是否隐藏设置
}
5.3左侧菜单的静态搭建
都是用element-plus的scrollbar组件以及menu菜单组件构造出静态
<!-- 左侧菜单展示 -->
<div class="layout_slider">
<logo></logo>
<!-- 展示菜单-->
<!-- 滚动组件-->
<el-scrollbar height="400px" class="scrollbar">
<el-menu background-color="#001529" text-color="white">
<!--菜单组件-->
<el-menu-item index="1">首页</el-menu-item>
<el-menu-item index="2">数据大屏</el-menu-item>
<!-- <el-menu-item index="3">数据统计</el-menu-item>-->
<!--折叠菜单-->
<el-sub-menu index="3">
<template #title>
<span>权限管理</span>
</template>
<el-menu-item index="2-1">用户管理</el-menu-item>\
<el-menu-item index="2-2">角色管理</el-menu-item>
<el-menu-item index="2-3">菜单管理</el-menu-item>
</el-sub-menu>
</el-menu>
</el-scrollbar>
</div>
5.4递归组件生成动态菜单
新建 menu 组件,并在layout中引入和使用<Menu />
<div class="layout_container">
<!-- 左侧菜单展示 -->
<div class="layout_slider">
<logo></logo>
<!-- 展示菜单-->
<!-- 滚动组件-->
<el-scrollbar height="400px" class="scrollbar">
<el-menu background-color="#001529" text-color="white">
<!--菜单组件-->
<Menu></Menu>
</el-menu>
</el-scrollbar>
</div>
<!-- 顶部导航 -->
<div class="layout_tabbar">
222
</div>
<!-- 内容展示区域 -->
<div class="layout_main">
<p style="height: 1000px;background: #e0c7e3" >111</p>
</div>
</div>
//引入左侧菜单子组件
import Menu from './menu/index.vue';
添加二级路由配置
project\src\router\routes.ts
//对外暴露配置路由(常量路由)
export const constantRoute = [
{//登录
path: '/login',//路径
component: () => import('@/views/login/index.vue'),//组件
name: 'login',//命名路由
meta:{
title:'登录',//菜单标题
hidden:true,//代表路由标题是否在菜单中隐藏
}
},
{
// 登录成功以后展示数据的路由
path: '/',
component: () => import('@/layout/index.vue'),
name: 'layout',
meta:{
title:'layout',//菜单标题
hidden:false,//代表路由标题是否在菜单中隐藏
},
children:[
{
path:'/home',
component: () => import('@/views/home/index.vue'),
meta:{
title:'首页',//菜单标题
hidden:true,//代表路由标题是否在菜单中隐藏
},
},
{
path:'/ceshi',
component: () => import('@/views/home/index.vue'),
meta:{
title:'测试',//菜单标题
hidden:false,//代表路由标题是否在菜单中隐藏
},
}
],
},
{
path: '/404',
component: () => import('@/views/404/index.vue'),
name: '404',
meta:{
title:'404',//菜单标题
hidden:true,//代表路由标题是否在菜单中隐藏
},
},
{
//任意路由
path:'/:pathMatch(.*)*',//上面都没有匹配上的话,就重回到404页面
redirect:'/404',//重定向到404页面
name:'any',
meta:{
title:'任意路由',//菜单标题
hidden:true,//代表路由标题是否在菜单中隐藏
},
}
]
将路由数组放进仓库,后面可以遍历拿去数据
//引入路由(常量路由)
import { constantRoute } from "@/router/routes";
//创建用户小仓库
const useUserStore = defineStore('User', {
// 小仓库存储数据
state: ():UserState => {
return {
token: GET_TOKEN(),//用户唯一标识token
menuRoutes: constantRoute,//仓库存储生成菜单需要数组(路由)
}
},
menuRoutes类型定义
project\src\store\modules\types\type.ts
import type {RouterRecordRaw} from "vue-router";
//定义小仓库数据state类型
export interface UserState {
token:string|null;
menuRoutes:RouterRecordRaw[];
}
其他组件就可以使用路由配置数组了
layout组件就可以使用(给Menu组件传过去)
<div class="layout_container">
<!-- 左侧菜单展示 -->
<div class="layout_slider">
<logo></logo>
<!-- 展示菜单-->
<!-- 滚动组件-->
<el-scrollbar height="400px" class="scrollbar">
<el-menu background-color="#001529" text-color="white">
<!--菜单组件-->
<Menu :menuList="userStore.menuRoutes"></Menu>
</el-menu>
</el-scrollbar>
</div>
<!-- 顶部导航 -->
<div class="layout_tabbar">
222
</div>
<!-- 内容展示区域 -->
<div class="layout_main">
<p style="height: 1000px;background: #e0c7e3" >111</p>
</div>
</div>
//引入左侧菜单子组件
import Menu from './menu/index.vue';
//获取用户相关的小仓库
import useUserStore from '@/store/modules/user';
let userStore = useUserStore();
menu组件:使用menuList生成动态菜单,(折叠就是去判断有没有子路由,如果有且大于1就递归自己,让子路由重新去判断,然后生成两个没有子路由。)
注意递归组件,注意判断条件,注意index要写,注意script可以写两次但ls要一样(递归组件需要名字)
点击菜单进行路由跳转element提供了两种方法,menu-item的属性或者事件,注意路由重定向----@click="goRoute"
记得递归文件要起名字,否则调用不成功
export default {
name: "Menu",
}
<template>
<div>
<!-- <h1>{{menuList}}</h1>-->
<template v-for="(item,index) in menuList" :key="item.path">
<!-- 没有子路由-->
<template v-if="!item.children">
<el-menu-item :index="item.path" v-if="!item.meta.hidden">
<template #title>
<span>标 </span>
<span>{{item.meta.title}}</span>
</template>
</el-menu-item>
</template>
<!--有子路由但是只要一个子路由 -->
<template v-if="item.children && item.children.length==1">
<el-menu-item :index="item.children[0].path" v-if="!item.children[0].meta.title">
<template #title>
<span>{{item.children[0].meta.title}}</span>
</template>
</el-menu-item>
</template>
<!-- 有子路由且个数大于1-->
<el-sub-menu :index="item.path" v-if="item.children && item.children.length>1">
<template #title>
<span>{{item.meta.title}}</span>
</template>
<!-- 递归自己,进入到这里在循环一次,子children在生成两个'没有子路由'-->
<Menu :menuList="item.children"></Menu>
</el-sub-menu>
</template>
</div>
<!-- </el-sub-menu>-->
</template>
<script setup lang="ts">
//获取父组件传递过来的全部路由数据
defineProps(['menuList'])
</script>
<script lang="ts">
//递归文件要起名字,否则调用不成功
export default {
name: "Menu",
}
</script>
<style scoped>
</style>
5.5菜单图标
(使用elememnt):动态展示,将图标注册成全局组件,然后放在路由元信息中
project\src\components\index.ts
// 引入element-plus提供全部图标组件
import * as ElementPlusIconVue from '@element-plus/icons-vue';
//全局对象
const allGloablComponent = {SvgIcon,Pagination };
console.log(allGloablComponent,'123')
//对外暴漏插件对象
export default {
//务必叫做install方法
install(app){
console.log(app)
//注册项目全部的全局组件
Object.keys(allGloablComponent).forEach(key =>{
//注册为全局组件
console.log(key,'22')
app.component(key,allGloablComponent[key])
});
// 将element-plus提供图标注册为全局组件
// console.log(Object.entries(ElementPlusIconVue));
for (const [key, component] of Object.entries(ElementPlusIconVue)){
app.component(key, component);
}
}
通过icon直接使用,前端
{
path: '/product',
component: () => import('@/layout/index.vue'),
name: 'Product',
meta: {
title: '商品管理',
hidden: false,
icon: 'Goods',
},
前端使用,
<!-- 没有子路由-->
<component :is="item.meta.icon"></component>
<!--有子路由但是只要一个子路由 -->
<el-icon>
<component :is="item.children[0].meta.icon"></component>
</el-icon>
<!-- 有子路由且个数大于1-->
<el-icon>
<component :is="item.meta.icon"></component>
</el-icon>
5.6配置全部路由配置
一级路由:数据大屏、权限管理
(二级组件:用户、角色、菜单管理)、商品管理(二级路由:sku、spu、品牌、属性)
权限和商品的一级路由用的还是组件 layout
首页直接重定向到home
//对外暴露配置路由(常量路由)
export const constantRoute = [
{//登录
path: '/login',//路径
component: () => import('@/views/login/index.vue'),//组件
name: 'login',//命名路由
meta: {
title: '登录',//菜单标题
hidden: true,//代表路由标题是否在菜单中隐藏
icon: 'Promotion',//左侧图标,支持element-plus所有图标
},
},
{
// 登录成功以后展示数据的路由
path: '/',
component: () => import('@/layout/index.vue'),
name: 'layout',
meta: {
title: 'layout',//菜单标题
hidden: false,//代表路由标题是否在菜单中隐藏
icon: 'Avatar',
},
redirect: '/home',
children: [
{
path: '/home',
component: () => import('@/views/home/index.vue'),
meta: {
title: '首页',//菜单标题
hidden: false,//代表路由标题是否在菜单中隐藏
icon: 'HomeFilled',
},
},
{
path: '/ceshi',
component: () => import('@/views/home/index.vue'),
meta: {
title: '测试',//菜单标题
hidden: false,//代表路由标题是否在菜单中隐藏
icon: 'HomeFilled',
},
}
],
},
{
path: '/404',
component: () => import('@/views/404/index.vue'),
name: '404',
meta: {
title: '404',//菜单标题
hidden: true,//代表路由标题是否在菜单中隐藏
icon: 'HomeFilled',
},
},
{
path: '/screen',
component: () => import('@/views/screen/index.vue'),
name: 'Screen',
meta: {
hidden: false,
title: '数据大屏',
icon: 'Platform'
}
},
{
path: '/acl',
component: () => import('@/layout/index.vue'),
name: 'Acl',
meta: {
title: '权限管理',
hidden: false,
icon: 'Lock',
},
children: [
{
path: '/acl/role',
component: () => import('@/views/acl/role/index.vue'),
name: 'Role',
meta: {
title: '权限管理',
hidden: false,
icon: 'Lock',
}
},
{
path: '/acl/user',
component: () => import('@/views/acl/user/index.vue'),
name: 'Acl',
meta: {
title: '角色管理',
hidden: false,
icon: 'UserFilled',
}
},
{
path: '/acl/permisssion',
component: () => import('@/views/acl/permisssion/index.vue'),
name: 'Permisssion',
meta: {
title: '菜单管理',
hidden: false,
icon: 'Monitor',
}
}
],
},
{
path: '/product',
component: () => import('@/layout/index.vue'),
name: 'Product',
meta: {
title: '商品管理',
hidden: false,
icon: 'Goods',
},
children: [
{
path: '/product/trademark',
component: () => import('@/views/product/trademark/index.vue'),
name: 'Trademark',
meta: {
title: '平台管理',
hidden: false,
icon: 'ShoppingCartFull',
}
},
{
path: '/product/attr',
component: () => import('@/views/product/attr/index.vue'),
name: 'Attr',
meta: {
title: '属性管理',
hidden: false,
icon: 'ChromeFilled',
}
},
{
path: '/product/spu',
component: () => import('@/views/product/spu/index.vue'),
name: 'Spu',
meta: {
title: 'Spu管理',
hidden: false,
icon: 'Calendar',
}
},
{
path: '/product/sku',
component: () => import('@/views/product/sku/index.vue'),
name: 'Sku',
meta: {
title: 'Sku管理',
hidden: false,
icon: 'Orange',
}
}
],
},
{
//任意路由
path: '/:pathMatch(.*)*',//上面都没有匹配上的话,就重回到404页面
redirect: '/404',//重定向到404页面
name: 'any',
meta: {
title: '任意路由',//菜单标题
hidden: true,//代表路由标题是否在菜单中隐藏
icon: 'HomeFilled',
},
}
]
layout右侧展示区域封装成一个组件 main,想做一点过度动画
src/layout/index.vue
<!-- 内容展示区域 -->
<div class="layout_main">
<Main></Main>
<!-- <p style="height: 1000px;background: #e0c7e3" >111</p>-->
</div>
// 引入右侧内容展示区域
import Main from './main/index.vue';
main文件可以做一下过渡动画,这里用router-view,调用的时候回自动注入组件,切换的话就自动销毁
src/layout/main/index.vue
<template>
<!--路由组件出口的位置-->
<!-- 点击组件的时候,插槽会把组件注入进来-->
<router-view v-slot="{Component}">
<transition name="fade">
<!-- 渲染layout一级路由组件的子路由-->
<!-- 切换的时候回被销毁-->
<component :is="Component" />
</transition>
</router-view>
</template>
<script setup lang="ts">
</script>
<style lang="scss" scoped>
.fade-enter-from{
opacity: 0;
transform: scale(0);
}
.fade-enter-active{
transition: all 1s;
}
.fade-enter-to{
opacity: 1;
transform: scale(1);
}
</style>
5.7顶部组件搭建
顶部左侧折叠和面包屑实现
左侧菜单刷新折叠的问题解决---属性default-active
折叠之后图标不见:icon放在插槽外面----element的menu属性:collapse
/menu/index.vue
/layout/index.vue
顶部tabbar静态组件封装:拆分为左侧面包屑、右侧设置区域
面包屑组件:project\src\layout\tabbar\breadcrumb\index.vue
<template>
<!-- 面包屑-->
<el-breadcrumb separator-icon="ArrowRight">
<el-breadcrumb-item>权限管理</el-breadcrumb-item>
<el-breadcrumb-item>用户管理</el-breadcrumb-item>
</el-breadcrumb>
</template>
<script setup lang="ts">
</script>
<style scoped>
</style>
src/layout/tabbar/setting/index.vue
<template>
<el-button size="small" icon="Refresh" circle></el-button>
<el-button size="small" icon="FullScreen" circle></el-button>
<el-button size="small" icon="Setting" circle></el-button>
<img src="../../../../public/logo.jpg" style="width: 24px;height: 24px;margin: 0px 10px">
<!-- 下拉菜单-->
<el-dropdown>
<span class="el-dropdown-link">
admin
<el-icon class="el-icon-right">
<arrow-down/>
</el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<script setup lang="ts">
</script>
<style scoped>
</style>
src/layout/tabbar/index.vue
<template>
<div class="tabbar">
<div class="tabbar_left">
<!-- 顶部左侧静态-->
<el-icon style="margin-right: 10px">
<Expand/>
</el-icon>
<!-- 左侧面包屑-->
<Breadcrumb></Breadcrumb>
</div>
<div class="tabbar_right">
<Setting></Setting>
</div>
</div>
</template>
<script setup lang="ts">
//引用左侧面包屑组件
import Breadcrumb from "./breadcrumb/index.vue"
// 引用右侧组组件
import Setting from "./setting/index.vue"
</script>
<style lang="scss" scoped>
.tabbar {
width: 100%;
height: 100%;
display: flex;
justify-content: space-between;
background-image: linear-gradient(to right, rgb(232,223,223),rgb(201,178,178), rgb(234, 181, 181));
.tabbar_left {
display: flex;
align-items: center;
margin-left: 20px;
}
.tabbar_right{
display: flex;
align-items: center;
//background: skyblue;
}
}
</style>
5.8菜单折叠的实现
定义控制折叠展开的变量 fold,放在仓库比较合适,因为其他组件也要根据fold进行变化,放在面包屑组件的传递给其他组件比较麻烦(把fold设置成全局组件)
src/store/modules/setting.ts
记得暴露出来
// 小仓库:layout组件相关配置仓库
import {defineStore} from "pinia";
let useLayOutSettingStore = defineStore('SettingStore', {
state: () => {
return {
fold: false,//用户控制菜单折叠还是收起
}
}
})
export default useLayOutSettingStore;
面包屑组件:project\src\layout\tabbar\breadcrumb\index.vue(通过点击图标切换仓库中fold的状态,其他组件读取时获取最新的)
<!-- 面包屑-->
<el-icon style="margin-right: 10px" @click="changIcon">
<!-- <Expand/>-->
<component :is="LayoutSettingStore.fold?'Fold':'Expand'"></component>
</el-icon>
import useLayOutSettingStore from "@/store/modules/setting.ts";
//获取layout配置相关的仓库
let LayoutSettingStore = useLayOutSettingStore()
const changIcon = () => {
// console.log('chang')
// 进行一个切换
LayoutSettingStore.fold = !LayoutSettingStore.fold
}
layout组件读取 fold 变量,同时添加样式的时候也要去修改left属性让折叠自然
src/layout/index.vue
<div class="layout_slider" :class="{fold:LayOutSettingStore.fold?true:false}">
<logo></logo>
<!-- 展示菜单-->
<!-- 滚动组件-->
<el-scrollbar height="1000px" class="scrollbar">
<el-menu :collapse="LayOutSettingStore.fold?true:false" :default-active="$route.path" background-color="#001529" text-color="white" active-text-color="blue">
<!--菜单组件-->
<Menu :menuList="userStore.menuRoutes"></Menu>
</el-menu>
</el-scrollbar>
</div>
<!-- 顶部导航 -->
<div class="layout_tabbar" :class="{fold:LayOutSettingStore.fold?true:false}">
<!-- layout组件的顶部导航tabbar-->
<Tabbar></Tabbar>
</div>
<!-- 内容展示区域 -->
<div class="layout_main" :class="{fold:LayOutSettingStore.fold?true:false}">
<Main></Main>
<!-- <p style="height: 1000px;background: #e0c7e3" >111</p>-->
</div>
import useLayOutSettingStore from "@/store/modules/setting.ts";
// 获取layout仓库
let LayOutSettingStore = useLayOutSettingStore();
.layout_slider {
width: $base-menu-width; //260px
height: 100vh;
background: $base-menu-background;
transition: all 0.3s;
.scrollbar {
width: 100%;
height: calc(100vh - $base-menu-logo-height);
.el-menu {
board-right: none;
}
}
&.fold {
width: calc(100vw - $base-menu-min-width);
left: $base-menu-min-width;
}
}
.layout_tabbar {
position: fixed;
top: 0;
right: 0;
left: $base-menu-width;
width: calc(100% - $base-menu-width);
height: $base-tabbar-height; //50x
//background: $base-tabbar-background;
&.fold {
width: calc(100vw - $base-menu-min-width);
left: $base-menu-min-width;
}
}
5.9顶部面包屑动态展示
通过获取路由来动态展示标题和图标内容
layout组件不需要展示到面包屑,直接展示首页,所以去掉layout路由中的元信息title和icon,然后再面包屑渲染时进行v-show判断
点击面包屑也可以进行路由跳转--element可以实现---to
src/layout/tabbar/breadcrumb/index.vue
<template>
<!-- 面包屑-->
<el-icon style="margin-right: 10px" @click="changIcon">
<!-- <Expand/>-->
<component :is="LayoutSettingStore.fold?'Fold':'Expand'"></component>
</el-icon>
<el-breadcrumb separator-icon="ArrowRight">
<!-- 面包动态展示路由的名字和标题-->
<el-breadcrumb-item v-for="(item,index) in $route.matched" :ked="index" v-show="item.meta.title" :to="item.path">
<!--面包屑动态展示图标-->
<el-icon>
<component :is="item.meta.icon"></component>
</el-icon>
<!-- 面包蟹展示匹配路由的标题-->
<span style="margin: 0px 0px 5px">{{item.meta.title}}</span>
</el-breadcrumb-item>
<el-breadcrumb-item>用户管理</el-breadcrumb-item>
</el-breadcrumb>
</template>
<script setup lang="ts">
import {ref} from 'vue';
import {useRoute} from 'vue-router';
import useLayOutSettingStore from "@/store/modules/setting.ts";
//获取layout配置相关的仓库
let LayoutSettingStore = useLayOutSettingStore()
let $route = useRoute();
const changIcon = () => {
// console.log('chang')
// 进行一个切换
LayoutSettingStore.fold = !LayoutSettingStore.fold
}
</script>
<script lang="ts">
export default {
name:"Breadcrumb",
}
</script>
<style scoped>
</style>
路由第一级需要重定向到路由第二级的第一个,方便显示
下面同级以此类推
5.10刷新功能实现
简单来说就是路由组件销毁和重建,重新发请求获取数据
涉及顶部导航与内容展示区域间通信---刷新的变量放在小仓库中
小仓库:src/store/modules/setting.ts
refresh: false,//控制刷新效果
Main组件监听refresh变化,变化就重新发请求,<script setup lang="ts">要放到这个里面,<script lang="ts">注意这个
src/layout/tabbar/setting/index.vue
// 获取仓库中的 刷新变量 refresh
import useLayOutSettingStore from '@/store/modules/setting'
let layOutSettingStore = useLayOutSettingStore()
// 点击刷新的回调
const refreshChange = () => {
layOutSettingStore.refresh = !layOutSettingStore.refresh
}
//刷新按钮
<el-button size="small" icon="Refresh" circle @click="refreshChange" />
src/layout/main/index.vue
import { watch, ref, nextTick } from 'vue'
// 获取仓库中的 刷新变量 refresh
import useLayOutSettingStore from '@/store/modules/setting'
let layOutSettingStore = useLayOutSettingStore()
// 控制组件销毁与创建
let flag = ref(true)
// 监听仓库中的refresh,如果发生变化说明用户点击刷新按钮
watch(() => layOutSettingStore.refresh, () => {
//refresh变化,则销毁组件
flag.value = false
//等待组件完毕重新加载
nextTick(() => {
flag.value = true
})
})
<component :is="Component" v-if="flag" />
5.11全屏功能实现:dom操作
//按钮
<el-button size="small" icon="FullScreen" circle @click="fullScreen"></el-button>
const fullScreen = ()=>{
// DOM对象的一个属性:可以判断当前是不是全屏模式【全屏:true,不是全屏:false】
let full = document.fullscreenElement;
// 切换全屏模式
if (!full){
// 文档根节点的方法requestFullscreen,实现全屏模式
document.documentElement.requestFullscreen()
}else{
// 变为不是全屏模式,退出全屏
document.exitFullscreen()
}
}
5.12退出登入业务
登入成功,首页组件挂在完毕,通知用户小仓库拿着token去找服务器那用户的相关数据,并存在小仓库中
src/views/login/index.vue
const login = async () => {
//保证全部表单校验通过在发送请求
// console.log(loginForms.value.validate(),'asndioasjdn')
//await用于等待一个Promise对象的解析结果,并将其赋值给一个变量
//通过使用async和await,可以让代码以同步的方式进行异步操作
await loginForms.value.validate();
//点击登录按钮
//开始加载效果
loading.value = true;
// useStore.userLogin(loginForm);
//通知仓库发送登录请求
//请求成功-》跳转到首页展示数据的地方
//请求失败-》提示错误信息
try {
await useStore.userLogin(loginForm);
//编程式导航跳转到展示数据首页
// 判断登录的时候,路由路径当中是否有query参数,如果有就往query参数跳转,没有就往首页
let redirect:any = $route.query.redirect;
$router.push({path:redirect||'/'})
$router.push('/');
// 登录成功提示信息
ElNotification({
type: 'success',
title: `HI,${getTime()}好`,
message: '欢迎回来',
})
// 登录成功,加载效果也消失
loading.value = false;
} catch (error) {
//登录失败,加载效果消失
loading.value = false;
console.log('请求失败');
//登录失败信息
ElNotification({
type: 'error',
message: (error as Error).message,
})
}
仓库中的方法
// 异步|逻辑 地方
actions: {
//用户登录的方法
async userLogin(data: loginForm) {
//登录请求
let result:loginResponseData = await reqLogin(data)
console.log(result, '123')
//登录请求:成功200->token
//登录请求:失败201->登录失败错误的信息
if (result.code == 200) {
// //pinia仓库存储一下token
// //由于pinia|vuex存储数据其实利用js对象
this.token = (result.data.token as string);
// //本地存储持久化存储一份
localStorage.setItem("TOKEN",(result.data.token as string))
SET_TOKEN(result.data as string)
// //能保证当前async函数返回一个成功的promise
return Promise.resolve('ok')
} else {
return Promise.reject(new Error(result.data.message));
}
},
// 退出登录
userLogout(){
// 目前没有mock接口:退出登录接口(通知服务器本地当前用户失效)
this.token = '';
this.username = '';
this.avatar = '';
REMOVE_TOKEN();
}
},
src/store/modules/types/type.ts记得定义token的类型
import type {RouterRecordRaw} from "vue-router";
//定义小仓库数据state类型
export interface UserState {
token:string|null;
username:string|null;
avatar:string|null;
menuRoutes:RouterRecordRaw[];
}
用户有n个请求,那怎么携带token呢,放在请求拦截器里面
project\src\utils\request.ts
// 引入用户相关的仓库
import useUserStore from '@/store/modules/user'
// 第二步:request实例添加请求和响应拦截器
request.interceptors.request.use((config) => {
// 获取用户小仓库,拿到登入成功的token数据携带给服务器
let userStore = useUserStore()
if (userStore.token) {
config.headers.token = userStore.token
}
// config 配置对象,有headers属性请求头,经常给服务器端携带公共参数
// 返回配置对象
return config;
})
获取到用户信息之后,在tabbar组件的setting组件中展示一下
获取仓库,展示即可
退出登入功能实现:点击之后退到登入页,将用户信息和token清除掉
退出登入需要发送请求,告诉服务器token失效,下一次登入服务器重新返回新的token
project\src\layout\tabbar\setting\index.vue
// 退出登入需要进行路由跳转
import { useRouter } from 'vue-router';
// 获取路由器对象
let $router = useRouter()
//退出登录点击回调
const logout = async () => {
//第一件事情:需要向服务器发请求[退出登录接口]******暂时没有
//第二件事情:仓库当中关于用于相关的数据清空[token|username|avatar]
//第三件事情:跳转到登录页面
await userStore.userLogout();
//跳转到登录页面
$router.push({ path: '/login' });
}
<el-dropdown-item @click="logout">退出登入</el-dropdown-item>
用户小仓库:project\src\store\modules\user.ts
//引入操作本地存储的工具方法
import {SET_TOKEN,GET_TOKEN,REMOVE_TOKEN} from '@/utils/token'
// 退出登录
userLogout(){
// 目前没有mock接口:退出登录接口(通知服务器本地当前用户失效)
this.token = '';
this.username = '';
this.avatar = '';
REMOVE_TOKEN();
}
还有一些问题需要解决:登入成功之后不允许在跳转到login、用户信息需要持久化存储
路由鉴权与进度条实现
进度条是可以用全局路由守卫实现(前置,后置)
路由组件挂在完毕我们去请求获取用户信息进行展示,但是不适用大量组件,可以通过全局路由守卫进行实现,路由跳转的时候发请求就可以
创建路由鉴权文件
注意:在组件的外部使用小仓库会报错(同步的语句获取仓库不可以),需要获取小仓库的数据必须先有大仓库
发请求:本来我们是在各个组件中发请求获取用户信息,现在我们可以在前置路由守卫中判断有没有用户名,有的话可以放行到其他路由组件,没有的话先获取用户信息再放行
放在路由守卫中也可以解决我们当时在首页中发请求获取用户信息时,我们跳转其他路由组件数据丢失问题,因为路由守卫就算没有用户信息,也会发请求拿到
5.13进度条
进度条是可以用全局路由守卫实现(前置,后置)
引入nprogress:
pnpm i nprogress
// 路由鉴权,鉴权,项目当中路由能不能被的权限的设置(某一个路由什么条件可以访问,什么条件下不能访问)
import router from "@/router";
import nprogress from 'nprogress';
// 引入进度条样式
import 'nprogress/nprogress.css';
// 全局守卫,项目当中任意路由切换都会触发的钩子
//全局的前置守卫
router.beforeEach((to:any,from:any,next:any)=>{
// to:你将要访问哪个路由
// from:你从来个路由而来
// next:路由的放行函数
nprogress.start();
next();
})
//全局的后置守卫
router.beforeEach((to:any,from:any)=>{
console.log('222')
nprogress.done();
})
5.14路由鉴权(有bug)
src/store/modules/user.ts
actions: {
//用户登录的方法
async userLogin(data: loginForm) {
//登录请求
let result:loginResponseData = await reqLogin(data)
console.log(result, '123')
//登录请求:成功200->token
//登录请求:失败201->登录失败错误的信息
if (result.code == 200) {
console.log('登录成功')
// //pinia仓库存储一下token
// //由于pinia|vuex存储数据其实利用js对象
this.token = (result.data.token as string);
// //本地存储持久化存储一份
localStorage.setItem("TOKEN",(result.data.token as string))
SET_TOKEN(result.data as string)
// //能保证当前async函数返回一个成功的promise
return Promise.resolve('ok')
} else {
return Promise.reject(new Error(result.data.message));
}
},
// 获取用户信息方法
// 拿token向服务器请求用户数据
async userInfo(){
// 获取用户信息进行存储【头像,用户名】
let result = await reqUserInfo();
console.log(result, 'result123')
if (result.code == 200) {
this.username = result.data.checkUser.username
console.log(this.username,'this.username')
this.avatar = result.data.checkUser.avatar
console.log(this.avatar,'this.avatar')
return 'ok';
} else {
return Promise.reject('获取用户信息失败');
}
},
// 退出登录
userLogout(){
// 目前没有mock接口:退出登录接口(通知服务器本地当前用户失效)
this.token = '';
this.username = '';
this.avatar = '';
REMOVE_TOKEN();
console.log('退出登录')
}
},
src/permisstion.ts
//路由鉴权:鉴权,项目当中路由能不能被的权限的设置(某一个路由什么条件下可以访问、什么条件下不可以访问)
import router from '@/router';
import setting from './settings';
import nprogress from 'nprogress';
//引入进度条样式
import "nprogress/nprogress.css";
nprogress.configure({ showSpinner: false });
//获取用户相关的小仓库内部token数据,去判断用户是否登录成功
import useUserStore from './store/modules/user';
import pinia from './store';
let userStore = useUserStore(pinia);
console.log(userStore,'sass')
//全局守卫:项目当中任意路由切换都会触发的钩子
//全局前置守卫
router.beforeEach(async (to: any, from: any, next: any) => {
document.title = `${setting.title} - ${to.meta.title}`
//to:你将要访问那个路由
//from:你从来个路由而来
//next:路由的放行函数
nprogress.start();
//获取token,去判断用户登录、还是未登录
let token = userStore.token;
console.log(token,'token')
//获取用户名字
let username = userStore.username;
console.log(username,'username')
//用户登录判断
if (token) {
//登录成功,访问login,不能访问,指向首页
if (to.path == '/login') {
console.log('登录成功,访问login,不能访问,指向首页')
next({ path: '/' })
} else {
//登录成功访问其余六个路由(登录排除)
//有用户信息
if (username) {
//放行
next();
} else {
//如果没有用户信息,在守卫这里发请求获取到了用户信息再放行
try {
console.log('获取用户信息userInfo')
//获取用户信息
await userStore.userInfo();
//放行
//万一:刷新的时候是异步路由,有可能获取到用户信息、异步路由还没有加载完毕,出现空白的效果
next({...to});
} catch (error) {
//token过期:获取不到用户信息了
//用户手动修改本地存储token
//退出登录->用户相关的数据清空
await userStore.userLogout();
console.log('有问题')
next({ path: '/login', query: { redirect: to.path } })
}
}
}
} else {
//用户未登录判断
if (to.path == '/login') {
next();
} else {
next({ path: '/login', query: { redirect: to.path } });
}
}
})
//全局后置守卫
router.afterEach((to: any, from: any) => {
nprogress.done();
});
//第一个问题:任意路由切换实现进度条业务 ---nprogress
//第二个问题:路由鉴权(路由组件访问权限的设置)
//全部路由组件:登录|404|任意路由|首页|数据大屏|权限管理(三个子路由)|商品管理(四个子路由)
//用户未登录:可以访问login,其余六个路由不能访问(指向login)
//用户登录成功:不可以访问login[指向首页],其余的路由可以访问
在main中引入
//引入路由鉴权文件
import './permisstion'
6.
6.1真实接口替换mock接口(登录)
真实接口地址:http://sph-api.atguigu.cn
添加到三台服务器中:开发、测试、上线(添加变量,我们不用手动去变化)
.env.development
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV = 'development'
VITE_APP_TITLE = 'y2k平台'
VITE_APP_BASE_API = '/api'
VITE_SERVE="http://sph-api.atguigu.cn"
剩下两个如同
vite官网的配置
loadEnv:加载接口变量,调用该函数会返回当前的环境变量
mode告诉加载哪个环境的文件,process告诉文件在哪里,这样就会获取对应的一个环境对象
project\vite.config.ts
import { defineConfig, loadEnv } from 'vite'
export default defineConfig(({command, mode}) => {
// 获取各种环境下的变量,在根目录
let env = loadEnv(mode, process.cwd());
console.log(env,'获取用户环境变量')
return {
// 代理跨域
server: {
proxy: {
[env.VITE_APP_BASE_API]: {
//获取数据的服务器地址设置
target: env.VITE_SERVE,
//需要代理跨域
changeOrigin: true,
//路径重写
rewrite: (path) => path.replace(/^\/api/, ''),
}
}
真实接口替换api中mock的接口,类型也要重新定义一下
src/api/user/index.ts
// 统一管理用户相关的接口
// 发请求就需要 request
import request from '@/utils/request'
// 用户相关的接口
enum API {
LOGIN_URL = '/admin/acl/index/login',
USERINFO_URL = '/admin/acl/index/info',
LOGOUT_URL = '/admin/acl/index/logout'
}
// 暴露请求函数
// 登入的接口方法
export const reqLogin = (data: any) => request.post<any, any>(API.LOGIN_URL, data)
// 获取用户信息的接口方法
export const reqUserInfo = () => request.get<any, any>(API.USERINFO_URL)
//退出登入的接口
export const reqLogout = () => request.post<any, any>(API.LOGOUT_URL)
去用户小仓库中修改三个发请求的函数
体验账号:admin,atguigu123
login路由组件中的初始密码记得修改一下,方便登入
project\src\store\modules\user.ts
//创建用户相关的小仓库
import {defineStore} from 'pinia'
// 引入接口
import {reqLogin, reqLogout, reqUserInfo} from '@/api/user'
//引入操作本地存储的工具方法
import {SET_TOKEN,GET_TOKEN,REMOVE_TOKEN} from '@/utils/token'
//引入路由(常量路由)
import { constantRoute } from "@/router/routes";
//引入数据类型
// import type {loginForm, loginResponseData, userInfoReponseData} from '@/api/user/type.ts'
import type {UserState} from "./types/type";
//创建用户小仓库
const useUserStore = defineStore('User', {
// 小仓库存储数据
state: ():UserState => {
return {
token: GET_TOKEN(),//用户唯一标识token
menuRoutes: constantRoute,//仓库存储生成菜单需要数组(路由)
username:'',
avatar:'',
}
},
// 异步|逻辑 地方
actions: {
//用户登录的方法
async userLogin(data: any) {
//登录请求
let result:any = await reqLogin(data)
console.log(result, '123')
//登录请求:成功200->token
//登录请求:失败201->登录失败错误的信息
if (result.code == 200) {
console.log('登录成功')
// //pinia仓库存储一下token
// //由于pinia|vuex存储数据其实利用js对象
this.token = (result.data as string);
// //本地存储持久化存储一份
localStorage.setItem("TOKEN",(result.data.token as string))
SET_TOKEN(result.data as string)
// //能保证当前async函数返回一个成功的promise
return Promise.resolve('ok')
} else {
return Promise.reject(new Error(result.data));
}
},
// 这里不知道为什么无法获取token信息
// 获取用户信息方法
// 拿token向服务器请求用户数据
async userInfo(){
// 获取用户信息进行存储【头像,用户名】
let result = await reqUserInfo();
console.log(result, 'result123')
if (result.code == 200) {
this.username = result.data.checkUser.username
console.log(this.username,'this.username')
this.avatar = result.data.checkUser.avatar
console.log(this.avatar,'this.avatar')
return 'ok';
} else {
return Promise.reject(new Error(result.message));
}
},
// 退出登录
async userLogout(){
let result = await reqLogout();
console.log(result,'asasas')
if (result.code == 200) {
this.token = '';
this.username = '';
this.avatar = '';
REMOVE_TOKEN();
console.log('退出登录')
return 'ok';
}else{
return Promise.reject(new Error(result.message));
}
}
},
getters: {},
})
//对外暴露获取小仓库方法
export default useUserStore
路由鉴权,由于给的服务器地址的info获取token有问题,所以先直接放行,但逻辑是没有问题 。
src/permisstion.ts
//路由鉴权:鉴权,项目当中路由能不能被的权限的设置(某一个路由什么条件下可以访问、什么条件下不可以访问)
import router from '@/router';
import setting from './settings';
import nprogress from 'nprogress';
//引入进度条样式
import "nprogress/nprogress.css";
nprogress.configure({ showSpinner: false });
//获取用户相关的小仓库内部token数据,去判断用户是否登录成功
import useUserStore from './store/modules/user';
import pinia from './store';
let userStore = useUserStore(pinia);
console.log(userStore,'sass')
//全局守卫:项目当中任意路由切换都会触发的钩子
//全局前置守卫
router.beforeEach(async (to: any, from: any, next: any) => {
document.title = `${setting.title} - ${to.meta.title}`
//to:你将要访问那个路由
//from:你从来个路由而来
//next:路由的放行函数
nprogress.start();
//获取token,去判断用户登录、还是未登录
let token = userStore.token;
console.log(token,'token')
//获取用户名字
let username = userStore.username;
console.log(username,'username')
//用户登录判断
if (token) {
//登录成功,访问login,不能访问,指向首页
if (to.path == '/login') {
console.log('登录成功,访问login,不能访问,指向首页')
next({ path: '/' })
} else {
//登录成功访问其余六个路由(登录排除)
//有用户信息
if (username) {
//放行
next();
} else {
//如果没有用户信息,在守卫这里发请求获取到了用户信息再放行
try {
console.log('获取用户信息userInfo')
//获取用户信息
await userStore.userInfo();
//放行
//万一:刷新的时候是异步路由,有可能获取到用户信息、异步路由还没有加载完毕,出现空白的效果
next({...to});
} catch (error) {
//token过期:获取不到用户信息了
//用户手动修改本地存储token
//退出登录->用户相关的数据清空
console.log('有问题,先直接放行')
// 这里获取不到接口信息,服务器问题,所以直接放行
// await userStore.userLogout();
// next({ path: '/login', query: { redirect: to.path } })
next();
}
}
}
} else {
//用户未登录判断
if (to.path == '/login') {
next();
} else {
next({ path: '/login', query: { redirect: to.path } });
}
}
})
//全局后置守卫
router.afterEach((to: any, from: any) => {
nprogress.done();
});
//第一个问题:任意路由切换实现进度条业务 ---nprogress
//第二个问题:路由鉴权(路由组件访问权限的设置)
//全部路由组件:登录|404|任意路由|首页|数据大屏|权限管理(三个子路由)|商品管理(四个子路由)
//用户未登录:可以访问login,其余六个路由不能访问(指向login)
//用户登录成功:不可以访问login[指向首页],其余的路由可以访问
到tabbar右侧组件中,根据退出登入返回的promise结果进行操作,推出登入成功则跳转到login
路由鉴权中,token过期也需要等待结果,保证退出登入成功之后再跳转login(加上await)
project\src\layout\tabbar\setting\index.vue
const logout = async()=>{
// 需要想服务器发请求[退出登录请求,] 没服务器就做不到
// 仓库当中关于相关的数据清空[token|username|avatar]
await userStore.userLogout();
// 跳转到登录页面
$router.push({ path:'/login',query:{redirect:$route.path}});
}
6.2品牌管理模块静态
<template>
<el-card class="box-card">
<!-- 卡片顶部添加品牌按钮 -->
<el-button color="#c6d182" size="default" icon="Plus">添加品牌</el-button>
<!-- 表格组件:用于展示已有得平台数据 -->
<!-- table:---border:可以设置表格纵向是否有边框
table-column:---label:某一个列表 ---width:设置这列宽度 ---align:设置这一列对齐方式
-->
<el-table style="margin:10px 0px" border>
<el-table-column label="序号" width="80px" align="center" type="index"></el-table-column>
<!-- table-column:默认展示数据用div -->
<el-table-column label="品牌名称"></el-table-column>
<el-table-column label="品牌LOGO"></el-table-column>
<el-table-column label="品牌操作"></el-table-column>
</el-table>
<!-- 分页器组件
pagination
v-model:current-page:设置分页器当前页码
v-model:page-size:设置每一个展示数据条数
page-sizes:用于设置下拉菜单数据
background:设置分页器按钮的背景颜色
layout:可以设置分页器六个子组件布局调整
-->
<el-pagination :pager-count="9" v-model:current-page="pageNo" v-model:page-size="limit" :page-sizes="[3, 5, 7, 9]"
:background="true" layout="prev, pager, next, jumper,->,sizes,total" :total="400" />
</el-card>
</template>
<script setup lang="ts">
import { ref } from 'vue'
//当前页码
let pageNo = ref<number>(1);
//每一页展示多少条数据
let limit = ref<number>(3);
</script>
<style scoped lang="scss"></style>
6.3品牌管理模块动态展示
接口中数据类型定义好之后,仓库中的数据类型也要改
定义接口的数据类型
src/api/user/type.ts
//定义用户相关数据的ts类型
//用户登录接口携带参数的ts类型
export interface loginFormData {
username: string
password: string
}
//定义全部接口返回数据都拥有ts类型
export interface ResponseData {
code: number
message: string
ok: boolean
}
//定义登录接口返回数据类型
export interface loginResponseData extends ResponseData {
data: string
}
//定义获取用户信息返回数据类型
export interface userInfoReponseData extends ResponseData {
data: {
routes: string[]
buttons: string[]
roles: string[]
name: string
avatar: string
}
}
修该user的登录以及获取用户信息的类型约束
src/store/modules/user.ts
import type {loginFormData,loginResponseData,userInfoResponseData} from "@/api/user/type";
async userLogin(data: loginFormData) {
//登录请求
let result:loginResponseData = await reqLogin(data)
async userInfo(){
// 获取用户信息进行存储【头像,用户名】
let result:userInfoResponseData = await reqUserInfo();
发请求,渲染数据
project\src\views\product\trademark\index.vue
prop就是简单的用div展示数据
这里有使用到插槽, 如果div满足不了需求(图片、按钮等),就得使用插槽传需要的值
<template>
<el-card class="box-card">
<!-- 卡片顶部添加品牌按钮 -->
<el-button color="#c6d182" size="default" icon="Plus">添加品牌</el-button>
<!-- 表格组件:用于展示已有得平台数据 -->
<!-- table:---border:可以设置表格纵向是否有边框
table-column:---label:某一个列表 ---width:设置这列宽度 ---align:设置这一列对齐方式
-->
<el-table style="margin:10px 0px" border :data="trademarkArr">
<el-table-column label="序号" width="80px" align="center" type="index">z</el-table-column>
<!-- table-column:默认展示数据用div -->
<!-- 可以直接使用prop展示-->
<!-- 如果div满足不了需求(图片、按钮等),就得使用插槽传需要的值-->
<el-table-column label="品牌名称" prop="tmName">
<!-- 使用插槽使用-->
<template #="{row,$index}">
<pre style="color: #e0c7e3">{{row.tmName}}</pre>
</template>
</el-table-column>
<!--品牌Logo-->
<el-table-column label="品牌Logo">
<template #="{row,$index}">
<img :src="row.logoUrl" alt="图片不存在" style="width: 100px;height: 100px"/>
</template>
</el-table-column>
<!-- 品牌操作-->
<el-table-column label="品牌操作">
<template #="{row,$index}">
<el-button type="primary" size="small" icon="Edit"></el-button>
<el-button type="primary" size="small" icon="Delete"></el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页器组件
pagination
v-model:current-page:设置分页器当前页码
v-model:page-size:设置每一个展示数据条数
page-sizes:用于设置下拉菜单数据,设置一页可以有多少条数据
background:设置分页器按钮的背景颜色
layout:可以设置分页器六个子组件布局调整
-->
<el-pagination :pager-count="9" v-model:current-page="pageNo" v-model:page-size="limit" :page-sizes="[3, 5, 7, 9]"
:background="true" layout="prev, pager, next, jumper,->,sizes,total" :total="total"/>
</el-card>
</template>
每次点击分页器都发一次请求,哪一页,要几条
在组件中发请求封装成一个函数,需要请求就调用。
组件挂载完毕发一次,点击分页器发一次
<script setup lang="ts">
import {ref, onMounted} from 'vue'
// 引入
import {reqHasTrademark} from "@/api/product/trademark";
//当前页码
let pageNo = ref<number>(1);
//每一页展示多少条数据
let limit = ref<number>(3);
// 存储已有品牌数据总数
let total = ref<number>(0)
// 存储已有品牌的数据
let trademarkArr = ref<any>([]);
// 获取已有品牌的接口封装作为一个函数:在任何情况下获取数据,调用此函数即可
const getHasTrademark = async () => {
let result = await reqHasTrademark(pageNo.value, limit.value);
// console.log(result,'12222')
if (result.code == 200) {
// 存储已有品牌的总个数
total.value ==result.data.total;
trademarkArr.value ==result.data.records;
}
}
// 组件挂在完毕的钩子,--发一次请求,获取第一页、一页三个已有品牌数据
onMounted(() => {
getHasTrademark()
})
</script>
6.4定义品牌ts类型
project\src\api\product\trademark\type.ts
export interface ResponseData{
code:number,
message:string,
ok:boolean
}
//已有品牌的ts类型
export interface TradeMark{
id?:number,//没有?是已有的品牌,加了?是代表新增的id(数据库新添加的数据id自增)就代表可有可无
tmName:string,
logoUrl:string
}
// 包含全部品牌数据的ts类型
export type Records = TradeMark[];
// 获取已有全部品牌的数据ts类型
export interface TradeMarkResponseData extends ResponseData{
data:{
records:Records,
total:number,
size:number,
current:number,
searchCount:boolean,
pages:number
}
}
对应的index文件的类型需要修改,还有组件中发请求的时候类型一同修改。
src/api/product/trademark/index.ts
// 引入ts类型
import type {TradeMarkResponseData} from "@/api/product/trademark/type.ts";
export const reqHasTrademark = (page:number,limit:number)=>request.get<any,TradeMarkResponseData>(API.TRADEMARK_URL+`${page}/${limit}`);
src/views/product/trademark/index.vue
// 引入ts类型
import type {Records,TradeMarkResponseData} from "@/api/product/trademark/type.ts";
// 存储已有品牌的数据
let trademarkArr = ref<Records>([]);
// 获取已有品牌的接口封装作为一个函数:在任何情况下获取数据,调用此函数即可
const getHasTrademark = async () => {
let result:TradeMarkResponseData = await reqHasTrademark(pageNo.value, limit.value);
6.5品牌管理分页展示数据
src/views/product/trademark/index.vue
设置页码,使用了v-model可以动态绑定每一页的条数以及当前的页码,切换页码的时候直接在调用getHasTrademark方法,切换每一页有多少个条数的时候,默认跳转到页面为1的页面,同样是采用 @size-change=getHasTrademark方法。或者使用 @size-change="sizeChange"sizeChange方法触发getHasTrademark
</el-table>
<!-- 分页器组件
pagination
v-model:current-page:设置分页器当前页码
v-model:page-size:设置每一个展示数据条数
page-sizes:用于设置下拉菜单数据,设置一页可以有多少条数据
background:设置分页器按钮的背景颜色
layout:可以设置分页器六个子组件布局调整
-->
<el-pagination
@size-change="sizeChange"
:pager-count="9"
@current-change="getHasTrademark"
v-model:current-page="pageNo"
v-model:page-size="limit" :page-sizes="[3, 5, 7, 9]"
:background="true" layout="prev, pager, next, jumper,->,sizes,total" :total="total"/>
</el-card>
// 获取已有品牌的接口封装作为一个函数:在任何情况下获取数据,调用此函数即可
const getHasTrademark = async (pager = 1) => {
// 当前页码为1
pageNo.value=pager
let result: TradeMarkResponseData = await reqHasTrademark(pageNo.value, limit.value);
console.log(result, '12222')
if (result.code == 200) {
// 存储已有品牌的总个数
total.value = result.data.total;
trademarkArr.value = result.data.records;
}
}
// 当下拉菜单发生变化的时候会触发此方法
// 这个自定义时间,分页器组件会将下拉菜单选中数据返回
const sizeChange=()=>{
// 当前每一页的数据量变化的时候,让他回归第一页
// pageNo.value =1
// console.log(a)
getHasTrademark()
// console.log(123)
}
// 分页器当前页码发生变化的时候触发
// 对于当前页码发生变化自定义事件,组件pagination父组件回传了数据(当前页码),组件用了v-model的,可以动态获取到当前的页码
// const changePageNo=()=>{
// // 当前页码发生变化的时候再次发请i去获取对应已有的数据显示
// getHasTrademark();
// }
6.6对话框dialog静态展示
使用element的dialog组件以及upload组件
<!-- 对话框组件:在添加品牌与修改已有品牌的业务时候使用结构-->
<!-- v-model属性用户控制对话框的显示与隐藏 true显示 false隐藏
title:设置对话框左上角标题
label-width:标签宽度-->
src/views/product/trademark/index.vue
<!-- 对话框组件:在添加品牌与修改已有品牌的业务时候使用结构-->
<!-- v-model属性用户控制对话框的显示与隐藏 true显示 false隐藏
title:设置对话框左上角标题
label-width:标签宽度-->
<el-dialog v-model="dialogFormVisible" title="添加品牌">
<el-form style="width: 80%">
<el-form-item label="品牌名称" label-width="100px">
<el-input placeholder="输入品牌名称" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="品牌LOGO" label-width="100px">
<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>
<!-- 具名插槽:footer-->
<template #footer>
<el-button type="primary" size="default" @click="cancel">取消</el-button>
<el-button type="primary" size="default" @click="confirm">确认</el-button>
</template>
</el-dialog>
//修改已有品牌按钮的回调
const updateTrademark = () => {
// 对话框显示
dialogFormVisible.value = true
}
// 对话框底部取消按钮
const cancel = () => {
// 对话框隐藏
dialogFormVisible.value = false
}
const confirm = () => {
// 对话框隐藏
dialogFormVisible.value = false
}
6.6收集新增品牌数据(添加品牌)
src/api/product/trademark/index.ts
enum API {
// 获取新增品牌接口
ADDTRADEMARK_URL ="/admin/product/baseTrademark/save/",
// 修改已有品牌接口
UPDATETRADEMARK_URL = "/admin/product/baseTrademark/update/"
}
//添加与修改已有品牌的接口方法
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)
}
}
import {ref, onMounted, reactive} from 'vue'
// 引入
import {reqHasTrademark} from "@/api/product/trademark";
// 引入ts类型
import type {Records, TradeMarkResponseData, TradeMark} from "@/api/product/trademark/type.ts";
import logo from "@/layout/logo/index.vue";
import type {UploadProps} from 'element-plus'
import {ElMessage} from "element-plus";
<!-- upload组件属性:action图片上传路径书写/api,代理服务器不发送这次post请求
:before-upload限制上传的类型或者大小-->
<el-upload
class="avatar-uploader"
action="/api/admin/product/fileUpload"
:show-file-list="false"
:on-success="handleAvatarSuccess"
:before-upload="beforeAvatarUpload"
>
<img v-if="trademarkParams.logoUrl" :src="trademarkParams.logoUrl" class="avatar"/>
<el-icon v-else class="avatar-uploader-icon">
<Plus/>
</el-icon>
</el-upload>
// 上传图片组件->上传图片之前触发的钩子函数
//
const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => {
// 钩子是在图片上传成功之前触发,上传文件之前可以约束文件的类型或者大小
// 要求:上传文件格式png|jpg|gif 4m
if (rawFile.type == 'image/png' || rawFile.type == 'image/jpeg' || rawFile.type == 'image/gif') {
if (rawFile.size / 1024 / 1024 < 4) {
return true
} else {
ElMessage({
type: 'error',
message: "上传文件大小应该小于4m"
})
return false
}
} else {
ElMessage({
type: 'error',
message: "上传文件务必是png,gif,jpg"
})
return false
}
console.log('rawFile', rawFile)
}
// 图片上传成功的钩子
const handleAvatarSuccess: UploadProps['onSuccess'] = (response, uploadFile) => {
//response:即为当前这次上传图片post请求服务器的返回数据,
// uploadFile:即为当前上传的图片文件
// 收集上传图片的地址,添加一个新的品牌的时候带给服务器
trademarkParams.logoUrl = response.data;
}
6.7添加新的品牌
确认添加的时候调用添加的接口,并关闭对话框,给出提示信息
点击添加按钮的时候,清空收集的信息,防止会有信息残留
// 引入
import {reqHasTrademark,reqAddOrUpdateTrademark} from "@/api/product/trademark";
const confirm = async () => {
let result:any = await reqAddOrUpdateTrademark(trademarkParams)
console.log(result,'1123sa')
// 添加品牌成功
if (result.code == 200){
// 关闭对话框
dialogFormVisible.value = false
// 弹出提示信息
ElMessage({
type:'success',
message:'添加品牌成功'
})
// 再次发请求获取已有的数据
getHasTrademark();
}else {
// 添加失败
// 弹出提示框
ElMessage({
type:'error',
message:'添加品牌失败'
})
// 关闭对话框
dialogFormVisible.value = false
}
}
// 添加品牌按钮的回调
const addTrademark = () => {
// 对话框显示
dialogFormVisible.value = true
// 清空收集的数据
trademarkParams.tmName =''
trademarkParams.logoUrl=''
}
6.8修改已添加品牌数据
通过row拿到现有的品牌信息展示在对话框中,就是赋值给trademarkParams数据
修改品牌需要id
通过判断有没有id区分新增还是修改,对应对话框标题
根据id去判断对应的添加或者修改。
<el-dialog v-model="dialogFormVisible" :title="trademarkParams.id?'修改品牌':'添加品牌'">
// 添加品牌按钮的回调
const addTrademark = () => {
// 对话框显示
dialogFormVisible.value = true
// 清空收集的数据
trademarkParams.tmName =''
trademarkParams.logoUrl=''
trademarkParams.id=0;
}
const updateTrademark = (row:TradeMark) => {
// 对话框显示
// row:修改之后的数据
dialogFormVisible.value = true
// ES6语法合并对象
Object.assign(trademarkParams,row);
// trademarkParams.id = row.id
// // 收集已有品牌的数据
// trademarkParams.tmName = row.tmName
// trademarkParams.logoUrl = row.logoUrl
}
const confirm = async () => {
let result:any = await reqAddOrUpdateTrademark(trademarkParams)
console.log(result,'1123sa')
// 添加|修改品牌成功 用id来判断
if (result.code == 200){
// 关闭对话框
dialogFormVisible.value = false
// 弹出提示信息
ElMessage({
type:'success',
message:trademarkParams.id?'修改品牌成功':'添加品牌成功'
})
// 再次发请求获取已有的数据
getHasTrademark(trademarkParams.id?pageNo.value:1);
}else {
// 添加失败
// 弹出提示框
ElMessage({
type:'error',
message:trademarkParams.id?'修改品牌失败':'添加品牌失败'
})
// 关闭对话框
dialogFormVisible.value = false
}
}
6.9品牌管理表单校验
表单校验,用element的el-form的:rules="rules"去校验
根据rules去定义logoUrl和tmName,validator是自定义校验规则
await formRef.value.validate();按确定的时候,进行校验,
根据ref="formRef"获取组件的实例,在第二次点开表单的时候都去清除formRef.value数据
<el-form style="width: 80%" :model="trademarkParams" :rules="rules" ref="formRef">
import {ref, onMounted, reactive, nextTick} from 'vue'
// 获取el-form组件实例
let formRef = ref();
// 添加品牌按钮的回调
const addTrademark = () => {
// 对话框显示
dialogFormVisible.value = true
// 清空收集的数据
trademarkParams.tmName = ''
trademarkParams.logoUrl = ''
trademarkParams.id = 0;
// 第一次打开表单的时候是不会有 formRef.value数据,所以不需要清理,第一次打开之后会保留 formRef.value数据,所以需要清理掉
// 第一种写法:ts|es6的问号语法
// formRef.value?.clearValidate('tmName')
// formRef.value?.clearValidate('logoUrl')
// 第二种写法,第二次打开的时候才去清理
nextTick(() => {
formRef.value.clearValidate('tmName')
formRef.value.clearValidate('logoUrl')
})
}
const updateTrademark = (row: TradeMark) => {
// 清空校验规则错误的校验信息
nextTick(() => {
formRef.value.clearValidate('tmName')
formRef.value.clearValidate('logoUrl')
})
}
const confirm = async () => {
// 在发请求前,对整个表单进行检验
// 调用这个方法进行全部表单校验,如果全部通过,才执行后面的
await formRef.value.validate();
}
// 品牌校验自定义校验规则方法
const validatorTmName = (rule: any, value: any, callback: any) => {
// 当表单元素触发blur时候,会触发方法
// 自定义校验规则
if (value.trim().length >= 2) {
callback()
} else {
// 校验未通过返回的信息
callback(new Error('品牌名称不能少于2位'))
}
}
// 图片logo自定义校验方法
const validatorLogoUrl = (rule: any, value: any, callback: any) => {
console.log(value)
// 当表单元素触发blur时候,会触发方法
// 自定义校验规则
if (value.trim().length >= 2) {
callback()
} else {
// 校验未通过返回的信息
callback(new Error('添加logo图片'))
}
}
// 表单校验规则
const rules = {
tmName: [
// 这个字段必须要填写,表单项前面出来五角星
// trigger:代表触发校验规则时机[blue、change]
{required: true, trigger: 'blur', validator: validatorTmName}
],
logoUrl: [
{required: true, validator: validatorLogoUrl}
]
}
6.10品牌管理模块删除
src/api/product/trademark/index.ts
品牌模块管理地址
enum API {
// 删除已有的品牌
DELETE_URL = '/admin/product/baseTrademark/remove/'
}
//删除某一个已有品牌的数据
export const reqDeleteTrademark =(id:number)=>request.delete<any,any>(API.DELETE_URL+id)
这里使用emement的气泡确认组件,确认删除发送删除请求,删除成功则回到当前页,删除了当前页的长度为0,则返回到上一页。
<!-- 品牌操作-->
<el-table-column label="品牌操作">
<template #="{row,$index}">
<el-button type="primary" size="small" icon="Edit" @click="updateTrademark(row)"></el-button>
<el-popconfirm :title="`你确定删除${row.tmName}?`" width="250px" icon="Delete" @confirm="removeTradeMark(row.id)">
<template #reference>
<el-button type="primary" size="small" icon="Delete"></el-button>-->
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
import {reqHasTrademark, reqAddOrUpdateTrademark,reqDeleteTrademark} from "@/api/product/trademark";
//气泡确认框确认按钮的回调
const removeTradeMark = async(id:number) => {
// 点击确认按钮删除已有的品牌请求
console.log(id,'id')
let result = await reqDeleteTrademark(id);
console.log(result,'res')
if (result.code == 200){
ElMessage({
type: 'success',
message: '删除成功'
})
// 再次获取已有的品牌数据
getHasTrademark(trademarkArr.value.length>1?pageNo.value:pageNo.value-1)
}else{
ElMessage({
type: 'error',
message: '删除失败'
})
}
}
7品牌模块搭建
7.1平台属性管理模块静态搭建
src/views/product/attr/index.vue
引用全局组件Category
<template>
<div>
<div>
<!-- 三级分类全局组件-->
<Category/>
<el-card style="margin: 10px 0">
<el-button type="primary" size="default" icon="Plus">添加图标</el-button>
<el-table border style="margin: 10px 0px">
<el-table-column type="index" label="序号" width="100px" align="center"></el-table-column>
<el-table-column label="属性名称" width="170px"></el-table-column>
<el-table-column label="属性值名称"></el-table-column>
<el-table-column label="操作" width="170px"></el-table-column>
</el-table>
</el-card>
</div>
</div>
</template>
<Category/>定义在全局组件
src/components/index.ts
import Category from './Category/index.vue'
const allGloablComponent = {SvgIcon,Pagination,Category};
src/components/Category/index.vue
三级联合搜索
<template>
<el-card>
<el-form :inline="true">
<el-form-item label="一级分类">
<el-select>
<el-option label="北京"></el-option>
<el-option label="广州"></el-option>
<el-option label="深圳"></el-option>
</el-select>
</el-form-item>
<el-form-item label="二级分类">
<el-select>
<el-option label="北京"></el-option>
<el-option label="广州"></el-option>
<el-option label="深圳"></el-option>
</el-select>
</el-form-item>
<el-form-item label="三级分类">
<el-select>
<el-option label="北京"></el-option>
<el-option label="广州"></el-option>
<el-option label="深圳"></el-option>
</el-select>
</el-form-item>
</el-form>
</el-card>
</template>
<script setup lang="ts">
</script>
<style scoped>
</style>
7.2属性管理模块一级分类数据与收集
这种情况需要用仓库去收集请求之后的信息,因为子组件获取到信息又得传到父组件,又得涉及组件通信,不如直接仓库去获取
src/components/Category/index.vue
<el-form-item label="一级分类">
<el-select v-model="categoryStore.c1Id">
<!-- option:label即为显示文字, value属性即为select下拉菜单收集的数据-->
<el-option v-for="(c1,index) in categoryStore.c1Arr" :key="c1.id" :label="c1.name" :value="c1.id"></el-option>
</el-select>
</el-form-item>
script setup lang="ts">
//引入组件挂载完毕方法
import {onMounted} from "vue";
// 引入分类相关的仓库
import useCategoryStore from "@/store/modules/category";
let categoryStore = useCategoryStore();
// 分类全局组件挂载完毕,通知仓库发请求获取一级分类的数据
onMounted(()=>{
getC1();
})
// 通知仓库获取一级分类的方法
const getC1 = () => {
categoryStore.getC1()
}
</script>
src/store/modules/category.ts
建立一个商品分类的仓库
//商品分类全局组件的小仓库
import {defineStore} from "pinia";
import {reqC1} from "@/api/product/attr";
let useCategoryStore = defineStore('Category', {
state: () => {
return {
c1Arr: [],
// 存储一级分类的Id
c1Id:'',
}
},
actions: {
// 获取一级分类的方法
async getC1() {
let result = await reqC1();
if (result.code == 200) {
this.c1Arr = result.data;
}
}
},
getters: {}
})
export default useCategoryStore;
7.3分类数据的ts类型
根据接口返回的数据去定义ts类型
src/api/product/attr/type.ts
//分类相关的数据ts类型
export interface ResponseData {
code:number,
message:string,
ok:boolean
}
//分类ts类型
export interface CategoryObj{
id:number|string,
name:string,
category1Id?:number,//可有可无
category2Id?:number,//可有可无
}
//响应的分类接口返回数据的类型
export interface CategoryResponseData extends ResponseData {
data:CategoryObj[],
}
src/store/modules/types/type.ts 定义c1Id和c1Arr类型
import type {RouterRecordRaw} from "vue-router";
import type {CategoryObj} from "@/api/product/attr/type.ts";
//定义小仓库数据state类型
export interface UserState {
token: string | null;
username: string | null;
avatar: string | null;
menuRoutes: RouterRecordRaw[];
}
//定义分类仓库state对象的ts类型
export interface CategoryState {
c1Id: string | number,
c1Arr: CategoryObj[],
}
src/api/product/attr/index.ts 接口返回的ts类型
import type {CategoryResponseData} from "@/api/product/attr/type.ts";
export const reqC1 =()=>request.get<any,CategoryResponseData>(API.C1_URL);
//获取二级分类的接口方法
export const reqC2 =(category1Id:number)=>request.get<any,CategoryResponseData>(API.C2_URL+category1Id);
//获取三级分类的接口方法
export const reqC3 =(category2Id:number)=>request.get<any,CategoryResponseData>(API.C3_URL+category2Id);
src/store/modules/category.ts 仓库也要定义返回的类型
import type {CategoryResponseData} from "@/api/product/attr/type.ts";
import type {CategoryState} from "@/store/modules/types/type.ts";
actions: {
// 获取一级分类的方法
async getC1() {
let result:CategoryResponseData = await reqC1();
if (result.code == 200) {
this.c1Arr = result.data;
}
}
},
7.4分类组件业务
大概就是全局挂载完毕之后,调用一级分类的数据,每次点击一级分类的时候调用了element的@chang方法,获取到c1Id,在通过:value获取到c1Id,在发送请求获取第二分类的数据,第三级数据同理。但是点击了第一二三级后,再去修改第一级就要去清除第二级的id以及第三级的数据,如果第一级没被修改,修改了第二级则要清楚第三级的id,
<el-form :inline="true">
<el-form-item label="一级分类">
<el-select v-model="categoryStore.c1Id" @change="handler">
<!-- option:label即为显示文字, value属性即为select下拉菜单收集的数据-->
<el-option v-for="(c1,index) in categoryStore.c1Arr" :key="c1.id" :label="c1.name" :value="c1.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="二级分类">
<el-select v-model="categoryStore.c2Id" @change="handler1">
<el-option v-for="(c2,index) in categoryStore.c2Arr" :key="c2.id" :label="c2.name" :value="c2.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="三级分类">
<el-select v-model="categoryStore.c3Id">
<el-option v-for="(c3,index) in categoryStore.c3Arr" :key="c3.id" :label="c3.name" :value="c3.id"></el-option>
</el-select>
</el-form-item>
</el-form>
<script setup lang="ts">
//引入组件挂载完毕方法
import {onMounted} from "vue";
// 引入分类相关的仓库
import useCategoryStore from "@/store/modules/category";
let categoryStore = useCategoryStore();
// 分类全局组件挂载完毕,通知仓库发请求获取一级分类的数据
onMounted(()=>{
getC1();
})
// 通知仓库获取一级分类的方法
const getC1 = () => {
categoryStore.getC1()
}
// 此方法即为一级分类下拉菜单的change事件(选中值的时候触发,保证一级菜单id有)
const handler=()=>{
// 需要将二级和三级都清空
categoryStore.c2Id='';
categoryStore.c3Arr=[];
categoryStore.c3Id='';
// 通知仓库去获取耳机分类数据
categoryStore.getC2();
}
const handler1=()=>{
// 需要三级id分类清空
categoryStore.c3Id='';
// 通知仓库去获取二级分类数据
categoryStore.getC3();
}
</script>
src/views/product/attr/index.vue
添加一个禁用属性:disabled,根据categoryStore.c3Id去判断是否禁用这个添加按钮
<el-button type="primary" size="default" icon="Plus" :disabled="categoryStore.c3Id ? false:true">添加属性</el-button>
import useCategoryStore from "@/store/modules/category.ts";
let categoryStore = useCategoryStore()
//分类相关的数据ts类型
export interface ResponseData {
code:number,
message:string,
ok:boolean
}
//分类ts类型
export interface CategoryObj{
id:number|string,
name:string,
category1Id?:number,//可有可无
category2Id?:number,//可有可无
category3Id?:number,//可有可无
}
//响应的分类接口返回数据的类型
export interface CategoryResponseData extends ResponseData {
data:CategoryObj[],
}
仓库的方法
src/store/modules/category.ts
//商品分类全局组件的小仓库
import {defineStore} from "pinia";
import {reqC1,reqC3,reqC2} from "@/api/product/attr";
import type {CategoryResponseData} from "@/api/product/attr/type.ts";
import type {CategoryState} from "@/store/modules/types/type.ts";
let useCategoryStore = defineStore('Category', {
state: ():CategoryState => {
return {
c1Arr: [],
// 存储一级分类的Id
c1Id:'',
// 存储对应一级分类下的二级分类的数据
c2Arr:[],
// 收集二级分类的id
c2Id:'',
// 存储对应二级分类下的三级分类的数据
c3Arr:[],
// 收集三级分类的id
c3Id:'',
}
},
actions: {
// 获取一级分类的方法
async getC1() {
let result:CategoryResponseData = await reqC1();
if (result.code == 200) {
this.c1Arr = result.data;
}
},
// 获取二级分类的数据
async getC2(){
// 获取对应一级分类的下二级分类的数据
let result:CategoryResponseData =await reqC2(this.c1Id)
console.log(result,'asbndoa')
if (result.code == 200) {
this.c2Arr = result.data;
}
},
// 获取三级分类的数据
async getC3(){
// 获取对应一级分类的下二级分类的数据
let result:CategoryResponseData =await reqC3(this.c2Id)
console.log(result,'bzxjicb')
if (result.code == 200) {
this.c3Arr = result.data;
}
}
},
getters: {}
})
export default useCategoryStore;
ts类型定义
src/store/modules/types/type.ts
//定义小仓库数据state类型
export interface UserState {
token: string | null;
username: string | null;
avatar: string | null;
menuRoutes: RouterRecordRaw[];
}
//定义分类仓库state对象的ts类型
export interface CategoryState {
c1Id: string | number,
c1Arr: CategoryObj[],
c2Arr: CategoryObj[],
c2Id: string | number,
c3Arr: CategoryObj[],
c3Id: string | number,
}
7.5已有属性与属性展示业务
src/views/product/attr/index.vue
el-table获取 :data="attrArr"数据 <el-table-column prop="attrName"获取数据
<el-table-column要用插槽加两个button
<el-card style="margin: 10px 0">
<el-button type="primary" size="default" icon="Plus" :disabled="categoryStore.c3Id ? false:true">添加属性</el-button>
<el-table border style="margin: 10px 0px" :data="attrArr">
<el-table-column type="index" label="序号" width="100px" align="center"></el-table-column>
<el-table-column label="属性名称" width="170px" prop="attrName"></el-table-column>
<el-table-column label="属性值名称">
<template #="{row,$index}">
<el-tag v-for="(item,index) in row.attrValueList" :key="item.id">{{item.valueName}}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="170px">
<!-- row已有的属性对象-->
<template #="{row,$index}">
<el-button type="primary" size="small" icon="Edit"></el-button>
<el-button type="primary" size="small" icon="Delete"></el-button>
</template>
</el-table-column>
</el-table>
</el-card>
src/api/product/attr/index.ts
定义接口
enum API {
// 获取分类下已有的属性与属性值
ATTR_URL = '/admin/product/attrInfoList/'
}
/// 获取对应分类下已有的属性与属性值
export const reqAttr = (category1Id: number | string, category2Id: number | string, category3Id: number | string) => request.get<any, AttrResponseData>(API.ATTR_URL + `${category1Id} / ${category2Id} / ${category3Id}`);
定义ts类型
export type AttrValueList = AttrValue[];
//属性对象
export interface Attr{
id:number,
attrName:string,
categoryId:number,
categoryLevel: number,
attrValueList:AttrValueList
}
7.6添加与修改属性静态搭建
src/views/product/attr/index.vue
根据scene去切换属性的结构
点击修改或者取消都可以去切换
另外三级分类全局组件传入scene的值,子组件调用 :disabled="scene==0?false:true"去禁用
<!-- 三级分类全局组件-->
<Category :scene="scene"/>
<div v-show="scene==0">
<el-button @click="addAttr" type="primary" size="default" icon="Plus"
:disabled="categoryStore.c3Id ? false:true">添加属性
</el-button>
<el-table border style="margin: 10px 0px" :data="attrArr">
<el-table-column type="index" label="序号" width="100px" align="center"></el-table-column>
<el-table-column label="属性名称" width="170px" prop="attrName"></el-table-column>
<el-table-column label="属性值名称">
<template #="{row,$index}">
<el-tag v-for="(item,index) in row.attrValueList" :key="item.id">{{ item.valueName }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="170px">
<!-- row已有的属性对象-->
<template #="{row,$index}">
<el-button type="primary" size="small" icon="Edit" @click="updateAttr"></el-button>
<el-button type="primary" size="small" icon="Delete"></el-button>
</template>
</el-table-column>
</el-table>
</div>
<div v-show="scene==1">
<!-- 展示添加属性与修改数据的结构-->
<el-form :inline="true">
<el-form-item label="属性名称">
<el-input placeholder="输入名称"></el-input>
</el-form-item>
</el-form>
<el-button type="primary" size="default" icon="Plus">添加属性值</el-button>
<el-button type="primary" size="default" @click="cancel">取消</el-button>
<el-table border style="margin: 10px 0px">
<el-table-column label="序号" width="80px" type="index" align="center"></el-table-column>
<el-table-column label="属性值名称" prop="attrName"></el-table-column>
<el-table-column label="属性名称" prop="attrName"></el-table-column>
</el-table>
<el-button type="primary" size="default">保存</el-button>
<el-button type="primary" size="default" @click="cancel">取消</el-button>
</div>
// 定义card组件内容切换变量
let scene = ref<number>(0);//如果scene为0 显示table,scene为1,展示添加与修改属性结构
// 添加属性按钮的回调
const addAttr = () => {
// 切换为添加与修改的属性的结构
scene.value = 1;
}
// table表格修改已有属性按钮的回调
const updateAttr = () => {
// 切换为添加与修改的属性的结构
scene.value = 1;
}
// 取消按钮的回调
const cancel = () => {
scene.value = 0;
}
<el-select :disabled="scene==0?false:true" v-model="categoryStore.c1Id" @change="handler">
<el-select :disabled="scene==0?false:true" v-model="categoryStore.c2Id" @change="handler1">
<el-select :disabled="scene==0?false:true" v-model="categoryStore.c3Id">
// 接受父组件传递过来的scene
defineProps(['scene']);
7.7属性接口的理解
添加一个新增或者修改已有属性的接口
src/api/product/attr/index.ts
enum API {
// 添加或者修改已有的属性的接口
ADDORUPDATEATTR = '/admin/product/saveAttrInfo'
}
// 新增或者修改已有的1属性接口
export const reqAddOrUpdateAttr = (data: Attr) => request.post<any, any>(API.ADDORUPDATEATTR, data);
src/views/product/attr/index.vue
定义一个attrParams并用reactive去约束
//组合式API函数watch
import {watch, ref,reactive} from "vue";
let attrParams = reactive<Attr>({
attrName:"",//新增的属性的名字
attrValueList: [//新增的属性值数组
// {
// valueName:""
// }
],
categoryId:'',//三级分类的ID
categoryLevel:3,//代表的是三级分类
})
id为可选值,如果没id就是添加功能,有id就是修改
src/api/product/attr/type.ts
//属性与属性值的ts类型
// 属性值对象的ts类型
export interface AttrValue{
id?:number,
valueName:string,
attrId?:number
}
export interface Attr{
id?:number,
attrName:string,
categoryId:number|string,
categoryLevel: number,
attrValueList:AttrValueList
}
7.7添加新的属性业务
<div v-show="scene==0">
<el-button @click="addAttr" type="primary" size="default" icon="Plus"
:disabled="categoryStore.c3Id ? false:true">添加属性
</el-button>
<div v-show="scene==1">
<!-- 展示添加属性与修改数据的结构-->
<el-form :inline="true">
<el-form-item label="属性名称">
<el-input placeholder="输入名称" v-model="attrParams.attrName"></el-input>
</el-form-item>
</el-form>
<el-button @click="addAttrValue" :disabled="attrParams.attrName?false:true" type="primary" size="default"
icon="Plus">添加属性值
</el-button>
<el-button type="primary" size="default" @click="cancel">取消</el-button>
<el-table border style="margin: 10px 0px" :data="attrParams.attrValueList">
<el-table-column label="序号" width="80px" type="index" align="center"></el-table-column>
<el-table-column label="属性值名称" prop="attrName">
<!-- row即为当前属性值对象-->
<template #="{row,$index}">
<el-input placeholder="输入属性值名称" v-model="row.valueName"></el-input>
</template>
</el-table-column>
<el-table-column label="属性名称" prop="attrName"></el-table-column>
</el-table>
<el-button type="primary" size="default" @click="save">保存</el-button>
<el-button type="primary" size="default" @click="cancel">取消</el-button>
</div>
import {reqAttr, reqAddOrUpdateAttr} from "@/api/product/attr";
// 添加属性按钮的回调
const addAttr = () => {
// 每一次点击的时候,先清空一下数组在收集
Object.assign(attrParams, {
attrName: "",//新增的属性的名字
attrValueList: [//新增的属性值数组
],
categoryId: categoryStore.c3Id,//三级分类的ID
categoryLevel: 3,//代表的是三级分类
})
// 切换为添加与修改的属性的结构
scene.value = 1;
}
// 添加属性值按钮的回调
const addAttrValue = () => {
// 点击添加属性值按钮的时候,想数组添加一个属性值对象
attrParams.attrValueList.push({
// 数组添加一个属性值对象
valueName: ''
})
}
}
// 保存按钮的回调
const save = async () => {
// 发送请求
let result = await reqAddOrUpdateAttr(attrParams)
// console.log(result,'reqadd')
if (result.code == 200) {
scene.value = 0;//切换场景
// 提示信息
ElMessage({
type: 'success',
message: attrParams.id ? '修改成功' : '添加成功'
})
//重新获取全部已有属性与属性值
getAttr()
} else {
// 提示信息
ElMessage({
type: 'error',
message: attrParams.id ? '修改失败' : '添加失败'
})
}
}
7.8属性值编辑模式与查看模式的切换
失去焦点的时候,触发tolook方法,如果属性值名称为空,删除调用对应属性值为空的值,并提示信息,如果是重复的话,也会删除当前的数组,并提示信息
<div v-show="scene==1">
<!-- 展示添加属性与修改数据的结构-->
<el-form :inline="true">
<el-form-item label="属性名称">
<el-input placeholder="输入名称" v-model="attrParams.attrName"></el-input>
</el-form-item>
</el-form>
<el-button @click="addAttrValue" :disabled="attrParams.attrName?false:true" type="primary" size="default"
icon="Plus">添加属性值
</el-button>
<el-button type="primary" size="default" @click="cancel">取消</el-button>
<el-table border style="margin: 10px 0px" :data="attrParams.attrValueList">
<el-table-column label="序号" width="80px" type="index" align="center"></el-table-column>
<el-table-column label="属性值名称" prop="attrName">
<!-- row即为当前属性值对象-->
<template #="{row,$index}">
<el-input v-if="row.flag" @blur="toLook(row,$index)" size="small" placeholder="输入属性值名称" v-model="row.valueName"></el-input>
<div v-else @blur="toEdit(row)" >{{row.valueName}}</div>
</template>
</el-table-column>
<el-table-column label="属性名称" prop="attrName"></el-table-column>
</el-table>
<el-button type="primary" size="default" @click="save" :disabled="attrParams.attrValueList.lengthh>0?false:true">保存</el-button>
<el-button type="primary" size="default" @click="cancel">取消</el-button>
</div>
// 添加属性值按钮的回调
const addAttrValue = () => {
// 点击添加属性值按钮的时候,想数组添加一个属性值对象
attrParams.attrValueList.push({
// 数组添加一个属性值对象
valueName: '',
flag:true,//控制每一个属性值编辑模式与切换模式的切换
})
}
// 属性值表单元素失去焦点的回调
const toLook = (row:AttrValue,$index:number) => {
// 非法清空判断
if (row.valueName.trim() == ''){
// 删除调用对应属性值为空的值
attrParams.attrValueList.splice($index,1)
//提示信息
ElMessage({
type:"error",
message:'属性值不能为空',
})
return
}
// 非法情况2
let repeat = attrParams.attrValueList.find((item)=>{
// 切记把当前失却焦点属性值对象从当前数组扣除判断
if (item!=row){
return item.valueName === row.valueName;
}
})
if(repeat){
// 将重复的属性值从数组中剔除
attrParams.attrValueList.splice($index,1)//$index是要删除第几个位置,1是代表删除一个
console.log($index,'index')
ElMessage({
type:'error',
message:'属性值不能重复',
})
return;
}
// 相应的属性值对象flag,响应
row.flag = false
}
// 属性值div点击事件
const toEdit = (row:AttrValue) => {
// 相应的属性值对象flag,变为true,展示input
row.flag = true
}
src/api/product/attr/type.ts
添加flag属性
//属性与属性值的ts类型
// 属性值对象的ts类型
export interface AttrValue{
id?:number,
valueName:string,
attrId?:number,
flag?:boolean,
}
7.9表单聚焦业务
ref获取到整个对象
点击添加的时候,下一个为聚焦状态
<el-table-column label="属性值名称" prop="attrName">
<!-- row即为当前属性值对象-->
<template #="{row,$index}">
<el-input :ref="(vc:any) => inputArr[$index] = vc" v-if="row.flag"
@blur="toLook(row,$index)" size="small" placeholder="输入属性值名称" v-model="row.valueName"></el-input>
<div v-else @click="toEdit(row,$index)">{{row.valueName}}</div>
</template>
</el-table-column>
// 添加属性值按钮的回调
const addAttrValue = () => {
// 点击添加属性值按钮的时候,想数组添加一个属性值对象
attrParams.attrValueList.push({
// 数组添加一个属性值对象
valueName: '',
flag:true,//控制每一个属性值编辑模式与切换模式的切换
})
// 获取最后el-input组件聚焦
nextTick(()=>{
inputArr.value[attrParams.attrValueList.length-1].focus();
})
}
// 属性值div点击事件
const toEdit = (row:AttrValue,$index:number) => {
// 相应的属性值对象flag,变为true,展示input
row.flag = true
console.log(inputArr.value[$index],'bhhh')
// nexttick:响应式数据发生变化,获取对应更新(组件实例)
nextTick(()=>{
inputArr.value[$index].focus();
})
}
7.10删除业务
根据attrID去删除对应的属性元素
src/views/product/attr/index.vue
<el-popconfirm :title="`你确定删除${row.attrName}?`" width="200px" @confirm="deleteAttr(row.id)">
// 引入获取已有的属性与属性值接口
import {reqAttr, reqAddOrUpdateAttr, reqRemoveAttr} from "@/api/product/attr";
// 删除某一个已有的属性回调
const deleteAttr = async (attrId: number) => {
// 发响应的删除已有的属性请求
let result: any = await reqRemoveAttr(attrId)
// 删除成功
if (result.code == 200) {
ElMessage({
type: 'success',
message: '删除成功'
})
// 获取一次已有的属性与属性值
getAttr();
} else {
ElMessage({
type: 'error',
message: '删除失败'
})
}
}
src/api/product/attr/index.ts
enum API {
// 删除某一个已有的属性
DELETEATTR_URL='/admin/product/deleteAttr/',
}
删除某一个已有的属性
export const reqRemoveAttr = (attrId: number) => request.delete<any, any>(API.DELETEATTR_URL + attrId);
8spu
8.1SPU静态页面
src/views/product/spu/index.vue
这里要传参:scene="scene"让他为0,不然是禁用状态
<template>
<div>
<!-- 三级分类-->
<Category :scene="scene"></Category>
<el-card style="margin: 10px 0px">
<el-button type="primary" size="default" icon="Plus">添加SPU</el-button>
<el-table style="margin: 10px 0px" border>
<el-table-column type="index" label="序号" width="100" align="center"></el-table-column>
<el-table-column label="SPU名称" align="center"></el-table-column>
<el-table-column label="SPU描述" align="center"></el-table-column>
<el-table-column label="SPU操作" align="center"></el-table-column>
</el-table>
<!-- 分页器-->
<el-pagination
v-model:current-page="pageNo" v-model:page-size="pageSize" :page-sizes="[3,5,7,9]"
:background="true" layout="prev,pager,next,jumper,->,sizes,total" :total="400">
</el-pagination>
</el-card>
</div>
</template>
<script setup lang="ts">
import {ref} from 'vue';
//场景的数据
let scene = ref<number>(0);
// 分页器默认页码
let pageNo = ref<number>(1);
// 每一页展示几条数据
let pageSize = ref<number>(3);
</script>
<style>
</style>
8.2spu模块展示已有的数据
//SPU管理模块的接口
src/api/product/spu/index.ts
//SPU管理模块的接口
import request from '@/utils/request';
import type {HasSpuResponseData} from "./type";
enum API{
HASSPU_URL='/admin/product/'
}
//获取某一个三级分类下的已有的SPU数据
export const reqHasSPU = (page:number,limit:number,category3Id:string|number)=>request.get<any,HasSpuResponseData>(API.HASSPU_URL+`${page}/${limit}?category3Id=${category3Id}`);
ts定义,根据后端返回的数据去定义
//服务器全部接口返回的数据类型
export interface ResponseData{
code:number,
msg:string,
ok:boolean
}
//Spu数据的ts类型,需要修改
export interface SpuData{
id?:number,
spuName?:string,
description:string,
category3Id:string|number,
tmId:number,
spuSaleAttrList:null,
spuImageList:null,
}
// 数组:元素都是已有Spu数据类型
export type Records=SpuData[];
//定义获取已有的SPU接口返回的数据ts类型
export interface HasSpuResponseData extends ResponseData{
data:{
records:Records,
total:number,
size:number,
current:number,
searchCount:boolean,
pages:number
}
}
动态展示出来,三级id变化的时候,watch就会执行getHasSpu,如果没变就不执行
分页器也有一些逻辑@current-change="getHasSpu"
@size-change="changeSize" 页码变化的时候,以及条数变化的时候都会触发获取spu数据的接口方法
默认都是page=1;
<template>
<div>
<!-- 三级分类-->
<Category :scene="scene"></Category>
<el-card style="margin: 10px 0px">
<el-button type="primary" size="default" icon="Plus" :disabled="categoryStore.c3Id? false : true">添加SPU</el-button>
<!-- 展示已有的SPU数据-->
<el-table style="margin: 10px 0px" border :data="records">
<el-table-column type="index" label="序号" width="100" align="center"></el-table-column>
<el-table-column label="SPU名称" align="center" prop="spuName"></el-table-column>
<el-table-column label="SPU描述" align="center" prop="description" show-overflow-tooltip></el-table-column>
<el-table-column label="SPU操作" align="center">
<template #="{row,$index}">
<el-button type="primary" size="small" icon="Plus" title="添加SKU"></el-button>
<el-button type="primary" size="small" icon="Edit" title="修改SKU"></el-button>
<el-button type="primary" size="small" icon="View" title="查看SKU"></el-button>
<el-button type="primary" size="small" icon="Delete" title="删除SKU"></el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页器-->
<el-pagination
v-model:current-page="pageNo" v-model:page-size="pageSize" :page-sizes="[3,5,7,9]"
:background="true" layout="prev,pager,next,jumper,->,sizes,total" :total="total" @current-change="getHasSpu"
@size-change="changeSize">
</el-pagination>
</el-card>
</div>
</template>
<script setup lang="ts">
import {ref, watch} from 'vue';
// 接口类型ts
import type {HasSpuResponseData, Records} from "@/api/product/spu/type.ts";
//获取数据接口
import {reqHasSPU} from "@/api/product/spu";
// 引入分类的仓库
import useCategoryStore from "@/store/modules/category.ts";
let categoryStore = useCategoryStore()
//场景的数据
let scene = ref<number>(0);
// 分页器默认页码
let pageNo = ref<number>(1);
// 每一页展示几条数据
let pageSize = ref<number>(3);
// 存储已有的SPU数据
let records = ref<Records>([])
// /存储已有Spu个数
let total = ref<number>(0);
// 监视三级分类Id变化
watch(() => categoryStore.c3Id, () => {
// 务必保证又三级分类Id
if (!categoryStore.c3Id) return;
getHasSpu();
});
// 此方法执行,可以获取某一个三级分类下全部的已有的Spu
const getHasSpu = async (pager = 1) => {
// 修改当前页码
pageNo.value = pager;
let result: HasSpuResponseData = await reqHasSPU(pageNo.value, pageSize.value, categoryStore.c3Id)
if (result.code == 200) {
records.value = result.data.records;
total.value = result.data.total;
}
}
// 分页器下拉菜单发生变化的时候触发
const changeSize = () => {
getHasSpu();
}
</script>
<style>
</style>
8.3spu模块场景的切换
新建src/api/product/spu/spuForm.vue
该子组件添加Spu|修改spu子组件
通过defineEmits,点击取消的时候通知父组件切换场景为0
<template>
<el-form label-width="auto">
<el-form-item label="SPU名称">
<el-input placeholder="请你输入SPU名称"></el-input>
</el-form-item>
<el-form-item label="SPU品牌">
<el-select>
<el-option label="华为"></el-option>
</el-select>
</el-form-item>
<el-form-item label="SPU描述">
<el-input type="textarea" placeholder="请你输入SPU描述"></el-input>
</el-form-item>
<el-form-item label="SPU图片">
<el-upload
v-model:file-list="fileList"
action="https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15"
list-type="picture-card"
:on-preview="handlePictureCardPreview"
:on-remove="handleRemove"
>
<el-icon><Plus /></el-icon>
</el-upload>
</el-form-item>
<!-- 展示销售属性的销售菜单 -->
<el-form-item label="spu的销售属性">
<el-select>
<el-option label="华为"></el-option>
</el-select>
<el-button style="margin-left: 10px" type="primary" size="default" icon="Plus">添加属性值</el-button>
<!--table展示销售属性与属性值的地方-->
<el-table border style="margin: 10px 0px">
<el-table-column label="序号" type="index" align="center" width="100px"></el-table-column>
<el-table-column label="属性名" width="100px" align="center"></el-table-column>
<el-table-column label="属性值" align="center"></el-table-column>
<el-table-column label="操作" width="120px" align="center"></el-table-column>
</el-table>
</el-form-item>
<el-form-item>
<el-button type="primary" size="default" icon="Plus">保存</el-button>
<el-button type="primary" size="default" icon="Close">取消</el-button>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
let $emit = defineEmits(['changeScene']);
// 点击取消按钮,通知父组件切换场景为0 ,展示已有的spu的数据
const cancel = () =>{
$emit('changeScene',0);
}
</script>
<style scoped>
</style>
src/views/product/spu/index.vue
默认场景scene为0,点击添加或者修改按钮的时候,修改scene为1,
<template>
<div>
<!-- 三级分类-->
<Category :scene="scene"></Category>
<el-card style="margin: 10px 0px">
<!-- v-if|v-show:都可以实现显示与隐藏,但v-if要销毁,损耗性能,v-show挂载了就不用了-->
<div v-show="scene===0">
<el-button @click="addSpu" type="primary" size="default" icon="Plus" :disabled="categoryStore.c3Id? false : true">添加SPU</el-button>
<!-- 展示已有的SPU数据-->
<el-table style="margin: 10px 0px" border :data="records">
<el-table-column type="index" label="序号" width="100" align="center"></el-table-column>
<el-table-column label="SPU名称" align="center" prop="spuName"></el-table-column>
<el-table-column label="SPU描述" align="center" prop="description" show-overflow-tooltip></el-table-column>
<el-table-column label="SPU操作" align="center">
<template #="{row,$index}">
<el-button type="primary" size="small" icon="Plus" title="添加SKU"></el-button>
<el-button type="primary" size="small" icon="Edit" title="修改SKU" @click="updateSpu"></el-button>
<el-button type="primary" size="small" icon="View" title="查看SKU"></el-button>
<el-button type="primary" size="small" icon="Delete" title="删除SKU"></el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页器-->
<el-pagination
v-model:current-page="pageNo" v-model:page-size="pageSize" :page-sizes="[3,5,7,9]"
:background="true" layout="prev,pager,next,jumper,->,sizes,total" :total="total" @current-change="getHasSpu"
@size-change="changeSize">
</el-pagination>
</div>
<!-- 添加Spu|修改spu子组件-->
<SpuForm v-show="scene===1" @changeScene="changeScene"></SpuForm>
<!-- 添加sku的子组件-->
<SkuForm v-show="scene===2"></SkuForm>
</el-card>
</div>
</template>
<script setup lang="ts">
import {ref, watch} from 'vue';
// 接口类型ts
import type {HasSpuResponseData, Records} from "@/api/product/spu/type.ts";
//获取数据接口
import {reqHasSPU} from "@/api/product/spu";
// 引入分类的仓库
import useCategoryStore from "@/store/modules/category.ts";
// 引入子组件
import SpuForm from "@/api/product/spu/spuForm.vue";
import SkuForm from "@/api/product/spu/skuForm.vue";
let categoryStore = useCategoryStore()
//场景的数据
let scene = ref<number>(0);//0现实已有SPU, 1:添加或者修改已有SPU 2:添加sku的结构
// 分页器默认页码
let pageNo = ref<number>(1);
// 每一页展示几条数据
let pageSize = ref<number>(3);
// 存储已有的SPU数据
let records = ref<Records>([])
// /存储已有Spu个数
let total = ref<number>(0);
// 监视三级分类Id变化
watch(() => categoryStore.c3Id, () => {
// 务必保证又三级分类Id
if (!categoryStore.c3Id) return;
getHasSpu();
});
// 此方法执行,可以获取某一个三级分类下全部的已有的Spu
const getHasSpu = async (pager = 1) => {
// 修改当前页码
pageNo.value = pager;
let result: HasSpuResponseData = await reqHasSPU(pageNo.value, pageSize.value, categoryStore.c3Id)
if (result.code == 200) {
records.value = result.data.records;
total.value = result.data.total;
}
}
// 分页器下拉菜单发生变化的时候触发
const changeSize = () => {
getHasSpu();
}
// 添加新的spu按钮的回调
const addSpu = () => {
// 点击切换为场景1:修改与添加已有的SPU结构
scene.value = 1;
}
// 子组件spuform绑定自定义事件:目前事子组件通知父组件切换场景为0
const changeScene = (num:number) => {
// 子组件Spuform点击取消变为场景0,展示已有的spu属性
scene.value = num;
}
// 点击修改已有的spu按钮的回调,
const updateSpu = () => {
// 点击切换为场景1:修改与添加已有的SPU结构
scene.value = 1;
}
</script>
<style>
</style>
8.4spu模块api的书写以及ts定义
根据接口返回的数据去进行定义
src/api/product/spu/type.ts
//商品图片的ts类型
export interface SpuImg{
id:number,
creatTime:string,
updateTime:string,
spuId:number,
imgName:string,
imgUrl:string
}
//已有的spu的照片墙数据的类型
export interface SpuHasImg extends ResponseData{
data:SpuImg[],
}
//已有的销售属性值对象ts类型
export interface SaleAttrValue{
id?:number,
creatTime:null,
updateTime:null,
spuId:number,
baseSaleAttrId:number,
saleAttrValueName:string,
saleAttrName:string,
isChecked:null
}
//存储已有的销售属性值数组类型
export type SaleAttrValueList=SaleAttrValue[];
//销售属性对象ts类型
export interface SaleAttr{
id?:number,
creatTime:null,
updateTime:null,
spuId:number,
baseSaleAttrId:number,
saleAttrName:string,
spuSaleAttrValueList:SaleAttrValueList,
}
//spu已有的销售属性接口返回数据ts类型
export interface SaleAttrResponseData extends ResponseData{
data:SaleAttr[]
}
//已有的全部spu的返回数据ts类型
export interface HasSaleAttr{
id:number,
name:string
}
export interface HasSaleAttrResponseData extends ResponseData{
data:HasSaleAttr[]
}
api接口
src/api/product/spu/index.ts
enum API{
// 获取已有的spu数据
HASSPU_URL='/admin/product/',
// 获取全部品牌的数据
ALLTRADEMARK_URL='/admin/product/baseTrademark/getTrademarkList',
//获取某个spu下的全部的售卖商品的图片数据
IMAGE_URL = '/admin/product/spuImageList/',
// 获取某一个SPU全部的已有的销售属性接口地址
SPUHASSALEATTR_URL = '/admin/product/spuSaleAttrList/',
// 获取整个项目全部的销售属性[颜色、版本、尺码]
ALLSALEATTR_URL = '/admin/product/baseSaleAttrList'
}
//获取某一个三级分类下的已有的SPU数据
export const reqHasSPU = (page:number,limit:number,category3Id:string|number)=>request.get<any,HasSpuResponseData>(API.HASSPU_URL+`${page}/${limit}?category3Id=${category3Id}`);
// 获取全部的spu的品牌的数据
export const reqAllTrademark = ()=>request.get<any,AllTradeMark[]>(API.ALLTRADEMARK_URL);
// 获取某一个已有的spi下全部商品的图片地址
export const reqSpuImageList = (spuId:number)=>request.get<any,SpuHasImg[]>(API.IMAGE_URL+spuId);
// 获取某一个已有的SPU拥有多少个销售属性
export const reqSpuHasSaleAttrList = (spuId:number)=>request.get<any,SaleAttrResponseData>(API.SPUHASSALEATTR_URL+spuId);
// 获取整个项目全部的销售属性TS类型[颜色、版本、尺码]
export const reqAllSaleAttr= ()=>request.get<any,HasSaleAttrResponseData>(API.ALLSALEATTR_URL);
8.4获取已有的spu数据
<SpuForm ref="spu",父组件可以获取到SpuForm的实例对象,父组件点击编辑修改的时候,可以把实例传给子组件,让子组件触发initHasSpuData方法接受到信息
src/views/product/spu/index.vue
<!-- 添加Spu|修改spu子组件 子组件传值过来changeScene-->
<SpuForm ref="spu" v-show="scene===1" @changeScene="changeScene"></SpuForm>
<template #="{row,$index}">
<el-button type="primary" size="small" icon="Edit" title="修改SKU" @click="updateSpu(row)"></el-button>
// 点击修改已有的spu按钮的回调,
const updateSpu = (row:SpuData) => {
// 点击切换为场景1:修改与添加已有的SPU结构
scene.value = 1;
// 调用子组件实例的方法获取完整已有的spu数组
spu.value.initHasSpuData(row);
console.log(spu.value)
}
子组件
组件获取数据前提需要暴露,因为setup的语法默认是不暴露的
子组件获取到了到了spu,根据spu.id去发送请求,获取对应的接口数据,其中ts需要定义好,获取到的数据存储起来。
src/api/product/spu/spuForm.vue
// 引入接口
import {reqAllTrademark, reqSpuImageList, reqSpuHasSaleAttrList, reqAllSaleAttr} from "@/api/product/spu/index.ts";
import type {
HasSaleAttr,
SaleAttr,
SpuImg,
Trademark,
AllTradeMark,
SpuHasImg,
SaleAttrResponseData,
HasSaleAttrResponseData
} from "@/api/product/spu/type";
// 点击取消按钮,通知父组件切换场景为1 ,展示已有的spu的数据
const cancel = () => {
$emit('changeScene', 0);
}
//
// 存储已有的spu数组‘
let AllTradeMark = ref<Trademark[]>([]);
// 商品的图片
let imgList = ref<SpuImg[]>([]);
// 已有的spu销售属性
let saleAttr = ref<SaleAttr>([]);
// 全部的销售属性
let allSaleAttr = ref<HasSaleAttr[]>([]);
// 子组件书写一个方法
const initHasSpuData = async (spu: SpuData) => {
// spu即为父组件传递过来的已有的spu对象[不完整]
// 获取全部品牌的数据
let result: AllTradeMark[] = await reqAllTrademark()
// 获取某一个品牌旗下全部售卖商品的图片
let result1: SpuHasImg = await reqSpuImageList(spu.id as number);
// 获取已有的spu销售属性的数据
let result2: SaleAttrResponseData = await reqSpuHasSaleAttrList(spu.id as number);
// 获取整个项目全部spu的销售属性
let result3: HasSaleAttrResponseData = await reqAllSaleAttr()
// 存储全部品牌的数据
AllTradeMark.value = result.data
// SPU对应商品图片
imgList.value = result1.data
// 存储已有的SPU销售属性
saleAttr.value = result2.data
// 存储全部的销售属性
allSaleAttr.value = result3.data
}
//对外暴露
defineExpose({initHasSpuData})
8.5修改spu的定义
src/api/product/spu/type.ts
//服务器全部接口返回的数据类型
export interface ResponseData {
code: number,
msg: string,
ok: boolean
}
//Spu数据的ts类型,需要修改
export interface SpuData {
id?: number,
spuName?: string,
description: string,
category3Id: string | number,
tmId: number,
spuSaleAttrList: null|SaleAttr[],
spuImageList: null | SpuImg[],
}
// 数组:元素都是已有Spu数据类型
export type Records = SpuData[];
//定义获取已有的SPU接口返回的数据ts类型
export interface HasSpuResponseData extends ResponseData {
data: {
records: Records,
total: number,
size: number,
current: number,
searchCount: boolean,
pages: number
}
}
//品牌数据的ts类型
export interface Brand {
export interface Trademark {
id: number,
tmName: string,
logoUrl: string
}
//品牌接口返回的数据ts类型
export interface AllTradeMark extends ResponseData {
data: Trademark[]
}
//商品图片的ts类型
export interface SpuImg {
id?: number,
creatTime?: string,
updateTime?: string,
spuId?: number,
imgName: string,
imgUrl: string
}
//已有的spu的照片墙数据的类型
export interface SpuHasImg extends ResponseData {
data: SpuImg[],
}
//已有的销售属性值对象ts类型
export interface SaleAttrValue {
id?: number,
creatTime?: null,
updateTime?: null,
spuId?: number,
baseSaleAttrId: number,
saleAttrValueName: string,
saleAttrName?: string,
isChecked?: null
}
//存储已有的销售属性值数组类型
export type SaleAttrValueList = SaleAttrValue[];
//销售属性对象ts类型
export interface SaleAttr {
id?: number,
creatTime?: null,
updateTime?: null,
spuId?: number,
baseSaleAttrId: number,
saleAttrName: string,
spuSaleAttrValueList: SaleAttrValueList,
}
//spu已有的销售属性接口返回数据ts类型
export interface SaleAttrResponseData extends ResponseData {
data: SaleAttr[]
}
//已有的全部spu的返回数据ts类型
export interface HasSaleAttr {
id: number,
name: string
}
export interface HasSaleAttrResponseData extends ResponseData {
data: HasSaleAttr[]
}
src/api/product/spu/index.ts
// 获取整个项目全部的销售属性[颜色、版本、尺码]
ALLSALEATTR_URL = '/admin/product/baseSaleAttrList',
// 追加一个新的spu
ADDSPU_URL = '/admin/product/saveSpuInfo',
// 而更新一个新的spu
UPDATESPU_URL = '/admin/product/saveSpuInfo'
export const reqAddOrUpdateSpu = (data:SpuData)=>{
// 如果SPU对象拥有id,更新已有的spu
if (data.id){
return request.post<any,any>(API.UPDATESPU_URL,data);
}else{
return request.post<any,any>(API.ADDSPU_URL,data);
}
}
8.6展示和收集已有的spu数据
src/api/product/spu/spuForm.vue
获取到 SpuParams.value = spu;数据后,将他展示出来
<el-form-item label="SPU名称">
<el-input placeholder="请你输入SPU名称" v-model="SpuParams.spuName"></el-input>
</el-form-item>
<el-form-item label="SPU品牌">
<el-select v-model="SpuParams.tmId">
<el-option v-for="(item,index) in AllTradeMark" :key="item.id" :label="item.tmName" :value="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="SPU描述">
<el-input type="textarea" placeholder="请你输入SPU描述" v-model="SpuParams.description"></el-input>
</el-form-item>
// 存储已有的spu对象
let SpuParams = ref<SpuData>({
category3Id:"",//收集三级分类的id
spuName:"",//Spu的名字
description:"",//spu的描述
tmId:'',//品牌的id
spuImageList:[
],
spuSaleAttrList:[],
});
//
// 子组件书写一个方法
const initHasSpuData = async (spu: SpuData) => {
// 存储已有的spu对象,将来在模板中展示
SpuParams.value = spu;
8.7已有照片墙数据的收集
<!-- v-modle:fileName 默认展示图片
action:上传图片的接口
list-type="picture-card":文件列表的类型 (照片墙)
:on-preview=:点击文件上传的预览
-->
<el-upload
v-model:file-list="imgList"
action="/api/admin/product/fileUpload"
list-type="picture-card"
:on-preview="handlePictureCardPreview"
:on-remove="handleRemove"
:before-upload="handleUpload"
>
<el-icon>
// 照片墙点击预览按钮的时候触发钩子
const handlePictureCardPreview = (fill:any) => {
dialogImageUrl.value = fill.url;
// 对话框弹出来
dialogVisible.value = true;
}
// 照片墙删除事件
const handleRemove = () => {
}
// 照片墙上传成功之前的钩子约束文件的大小与类型
const handleUpload = (file:any) => {
if (file.type=='image/png'||file.type == 'image/jpeg'||file.type == 'image/gif'){
if (file.size/1024/1024<3){
return true;
}else{
ElMessage({
type:'error',
message:'上传文件小于3m'
})
}
}else{
return false;
}
}
8.8展示已有销售属性与属性值
:data为saleAttr,后面用prop获取,满足不了用插槽(添加按钮)
<!--table展示销售属性与属性值的地方-->
<el-table border style="margin: 10px 0px" :data="saleAttr">
<el-table-column label="序号" type="index" align="center" width="100px"></el-table-column>
<el-table-column label="属性名" width="100px" align="center" prop="saleAttrName"></el-table-column>
<el-table-column label="属性值" align="center">
<!-- row:当前spu销售属性对象-->
<template #="{row,$index}">
<el-tag style="margin: 0px 5px" v-for="(item,index) in row.spuSaleAttrValueList" :key="row.id" class="mx-1" closable>
{{item.saleAttrValueName}}
</el-tag>
<el-button type="primary" size="small" icon="Plus"></el-button>
</template>
</el-table-column>
<el-table-column label="操作" width="120px" align="center">
<template #="{row,$index}">
<el-button type="primary" size="small" icon="Delete" @click="saleAttr.splice($index,1)"></el-button>
</template>
</el-table-column>
</el-table>
</el-form-item>
<el-form-item>
<el-button type="primary" size="default" icon="Plus">保存</el-button>
<el-button type="primary" size="default" icon="Close" @click="cancel">取消</el-button>
</el-form-item>
8.9收集新增销售属性
src/api/product/spu/spuForm.vue
<!-- 展示销售属性的销售菜单 -->
<el-form-item label="spu的销售属性">
<!-- :placeholder="unSelectSaleAttr.length ? `还未选择${unSelectSaleAttr.length}个` : '无'"-->
<el-select v-model="saleAttrIdAndValueName"
:placeholder="unSelectSaleAttr.length ? `还未选择${unSelectSaleAttr.length}个` : '无'">
<el-option :value="`${item.id}:${item.name}`" v-for="(item,index) in unSelectSaleAttr" :key="item.id" :label="item.name"></el-option>
</el-select>
<el-button @click="addSaleAttr" :disabled="saleAttrIdAndValueName?false:true" style="margin-left: 10px" type="primary" size="default" icon="Plus">添加属性</el-button>
//计算出当前SPU还未拥有的销售属性
let unSelectSaleAttr = computed(() => {
//全部销售属性:颜色、版本、尺码
//已有的销售属性:颜色、版本
let unSelectArr = allSaleAttr.value.filter(item => {
return saleAttr.value.every(item1 => {
return item.name != item1.saleAttrName;
});
})
return unSelectArr;
})
//添加销售属性的方法
const addSaleAttr = () => {
/*
"baseSaleAttrId": number,
"saleAttrName": string,
"spuSaleAttrValueList": SpuSaleAttrValueList
*/
const [baseSaleAttrId, saleAttrName] = saleAttrIdAndValueName.value.split(':');
//准备一个新的销售属性对象:将来带给服务器即可
let newSaleAttr: SaleAttr = {
baseSaleAttrId,
saleAttrName,
spuSaleAttrValueList: []
}
//追加到数组当中
saleAttr.value.push(newSaleAttr);
//清空收集的数据
saleAttrIdAndValueName.value = '';
}
8.10展示与收集已有spu业务
src/api/product/spu/spuForm.vue
通过flag去判断input和button什么时候展开,
失去焦点的时候触发tolook方法,
input框同时用v-model动态绑定row.saleAttrValue,好收集起数据
整理成服务器需要的属性值的形式
let newSaleAttrValue:SaleAttrValue = {
baseSaleAttrId,
saleAttrValueName: (saleAttrValue as string)
}
<el-table-column label="属性值" align="center">
<!-- row:当前spu销售属性对象-->
<template #="{row,$index}">
<el-tag style="margin: 0px 5px" @close="row.spuSaleAttrValueList.splice(index,1)" v-for="(item,index) in row.spuSaleAttrValueList" :key="row.id" class="mx-1" closable>
{{item.saleAttrValueName}}
</el-tag>
<el-input @blur="toLook(row)" v-model="row.saleAttrValue" v-if="row.flag==true" placeholder="请你输入属性值" size="small" style="width:100px"></el-input>
<el-button @click="toEdit(row)" v-else type="primary" size="small" icon="Plus"></el-button>
</template>
</el-table-column>
// 属性值按钮的点击事件
const toEdit = (row:SaleAttr) => {
// 点击按钮的时候,input组件不就不出来,编辑模式
row.flag = true;
row.saleAttrValue = ''
}
// 表单元素失却焦点事件回调
const toLook = (row:SaleAttr) => {
// 整理收集的属性的id与属性值的名字
const {baseSaleAttrId,saleAttrValue} = row;
// 整理成服务器需要的属性值的形式
let newSaleAttrValue:SaleAttrValue = {
baseSaleAttrId,
saleAttrValueName: (saleAttrValue as string)
}
// 非法情况的判断
if ((saleAttrValue as string).trim() == ''){
ElMessage({
type:'error',
message: '属性值不能为空'
})
return;
}
// 判断属性值是否在数组中存在
let repeat = row.spuSaleAttrValueList.find(item=>{
return item.saleAttrValueName == saleAttrValue;
})
if (repeat){
ElMessage({
type:'error',
message: '属性值不能重复'
})
return;
}
// 追加新的属性值对象
row.spuSaleAttrValueList.push(newSaleAttrValue);
// 失去焦点的时候,input组件出来,查看模式
row.flag = false;
}
8.11添加spu业务
src/api/product/spu/spuForm.vue
暴漏个index父组件
修改的时候清空数据,
// 添加一个新的spu初始化请求方法
const initAddSpu = async (c3Id:number|string) => {
// 清空数据
Object.assign(SpuParams.value,{
category3Id:"",//收集三级分类的id
spuName:"",//Spu的名字
description:"",//spu的描述
tmId:'',//品牌的id
spuImageList:[],
spuSaleAttrList:[],
})
// 清空照片墙
imgList.value = [];
// 清空销售属性
saleAttr.value = [];
// 存储三级分类的id
SpuParams.value.category3Id = c3Id;
saleAttrIdAndValueName.value = '';
// 获取全部品牌的数据
let result: AllTradeMark = await reqAllTrademark();
let result1: HasSaleAttrResponseData = await reqAllSaleAttr()
// 存储数组
AllTradeMark.value = result.data;
allSaleAttr.value = result1.data;
}
//对外暴露
defineExpose({initHasSpuData,initAddSpu})
src/views/product/spu/index.vue
添加或者修改的时候,根据params是否是update来判断存留在第几页
const addSpu = () => {
// 点击切换为场景1:修改与添加已有的SPU结构
scene.value = 1;
// 点击添加spu按钮,调用子组件的方法初始化数
spu.value.initAddSpu(categoryStore.c3Id);
}
// 子组件spuform绑定自定义事件:目前事子组件通知父组件切换场景为0
const changeScene = (obj:any) => {
// 子组件Spuform点击取消变为场景0,展示已有的spu属性
scene.value = obj.flag;
if (obj.params=='update'){
// 再次获取全部的已有spu
getHasSpu(pageNo.value);
}else {
// 添加的话留在第一页
getHasSpu();
}
}
8.12完成spu的添加sku所有业务
src/api/product/spu/skuForm.vue
<template>
<el-form label-width="100px">
<el-form-item label="SKU名称">
<el-input placeholder="SKU名称" v-model="skuParams.skuName"></el-input>
</el-form-item>
<el-form-item label="价格(元)">
<el-input placeholder="价格(元)" type="number" v-model="skuParams.price"></el-input>
</el-form-item>
<el-form-item label="重量(g)">
<el-input placeholder="重量(g)" type="number" v-model="skuParams.weight"></el-input>
</el-form-item>
<el-form-item label="SKU描述">
<el-input placeholder="SKU描述" type="textarea" v-model="skuParams.skuDesc"></el-input>
</el-form-item>
<el-form-item label="平台属性">
<el-form :inline="true">
<el-form-item v-for="(item, index) in attrArr" :key="item.id" :label="item.attrName">
<el-select v-model="item.attrIdAndValueId">
<el-option :value="`${item.id}:${attrValue.id}`" v-for="(attrValue, index) in item.attrValueList"
:key="attrValue.id" :label="attrValue.valueName"></el-option>
</el-select>
</el-form-item>
</el-form>
</el-form-item>
<el-form-item label="销售属性">
<el-form :inline="true">
<el-form-item :label="item.saleAttrName" v-for="(item, index) in saleArr" :key="item.id">
<el-select v-model="item.saleIdAndValueId">
<el-option :value="`${item.id}:${saleAttrValue.id}`"
v-for="(saleAttrValue, index) in item.spuSaleAttrValueList" :key="saleAttrValue.id"
:label="saleAttrValue.saleAttrValueName"></el-option>
</el-select>
</el-form-item>
</el-form>
</el-form-item>
<el-form-item label="图片名称">
<el-table border :data="imgArr" ref="table">
<el-table-column type="selection" width="80px" align="center"></el-table-column>
<el-table-column label="图片">
<template #="{ row, $index }">
<img :src="row.imgUrl" alt="" style="width:100px;height: 100px;">
</template>
</el-table-column>
<el-table-column label="名称" prop="imgName"></el-table-column>
<el-table-column label="操作">
<template #="{ row, $index }">
<el-button type="primary" size="small" @click="handler(row)">设置默认</el-button>
</template>
</el-table-column>
</el-table>
</el-form-item>
<el-form-item>
<el-button type="primary" size="default" @click="save">保存</el-button>
<el-button type="primary" size="default" @click="cancel">取消</el-button>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
//引入请求API
import { reqAttr } from '@/api/product/attr';
import {reqAddSku, reqSpuImageList,reqSpuHasSaleAttrList } from '@/api/product/spu';
import type { SkuData } from '@/api/product/spu/type'
import { ElMessage } from 'element-plus';
import { ref, reactive } from 'vue';
//平台属性
let attrArr = ref<any>([]);
//销售属性
let saleArr = ref<any>([]);
//照片的数据
let imgArr = ref<any>([]);
//获取table组件实例
let table = ref<any>();
//收集SKU的参数
let skuParams = reactive<SkuData>({
//父组件传递过来的数据
"category3Id": "",//三级分类的ID
"spuId": "",//已有的SPU的ID
"tmId": "",//SPU品牌的ID
//v-model收集
"skuName": "",//sku名字
"price": "",//sku价格
"weight": "",//sku重量
"skuDesc": "",//sku的描述
"skuAttrValueList": [//平台属性的收集
],
"skuSaleAttrValueList": [//销售属性
],
"skuDefaultImg": "",//sku图片地址
})
//当前子组件的方法对外暴露
const initSkuData = async (c1Id: number | string, c2Id: number | string, spu: any) => {
//收集数据
skuParams.category3Id = spu.category3Id;
skuParams.spuId = spu.id;
skuParams.tmId = spu.tmId;
//获取平台属性
let result: any = await reqAttr(c1Id, c2Id, spu.category3Id);
//获取对应的销售属性
let result1: any = await reqSpuHasSaleAttrList(spu.id);
//获取照片墙的数据
let result2: any = await reqSpuImageList(spu.id);
//平台属性
attrArr.value = result.data;
//销售属性
saleArr.value = result1.data;
//图片
imgArr.value = result2.data;
}
//取消按钮的回调
const cancel = () => {
$emit('changeScene', { flag: 0, params: '' });
}
//设置默认图片的方法回调
const handler = (row: any) => {
//点击的时候,全部图片的的复选框不勾选
imgArr.value.forEach((item: any) => {
table.value.toggleRowSelection(item, false);
});
table.value.toggleRowSelection(row, true);
//收集图片地址
skuParams.skuDefaultImg = row.imgUrl;
}
//对外暴露方法
defineExpose({
initSkuData
});
//保存按钮的方法
const save = async () => {
//选中的图片才勾选 //整理参数
//平台属性
skuParams.skuAttrValueList = attrArr.value.reduce((prev: any, next: any) => {
if (next.attrIdAndValueId) {
let [attrId, valueId] = next.attrIdAndValueId.split(':');
prev.push({
attrId,
valueId
})
}
return prev;
}, []);
//销售属性
skuParams.skuSaleAttrValueList = saleArr.value.reduce((prev: any, next: any) => {
if (next.saleIdAndValueId) {
let [saleAttrId, saleAttrValueId] = next.saleIdAndValueId.split(':');
prev.push({
saleAttrId, saleAttrValueId
})
}
return prev;
}, []);
//添加SKU的请求
let result: any = await reqAddSku(skuParams);
if (result.code == 200) {
ElMessage({
type: 'success',
message: '添加SKU成功'
});
//通知父组件切换场景为零
$emit('changeScene',{flag:0,params:''})
} else {
ElMessage({
type: 'error',
message: '添加SKU失败'
})
}
}
//自定义事件的方法
let $emit = defineEmits(['changeScene']);
</script>
<style scoped></style>
enum API{
// 追加一个新的sku地址
ADDSKU_URL = '/admin/product/saveSkuInfo',
//查看某一个已有的SPU下全部售卖的商品
SKUINFO_URL = '/admin/product/findBySpuId/',
// 删除某一个已有的SPU下的商品
REMOVESKU_URL = '/admin/product/deleteSpu/',
}
// 添加sku的请求方法
export const reqAddSku = (data:SkuData)=>request.post<any,any>(API.ADDSKU_URL,data);
// 获取sku数据
export const reqSkuList = (spuId: number | string) => request.get<any, SkuInfoData>(API.SKUINFO_URL + spuId);
//删除已有的SPU
export const reqRemoveSqu= (spuId:number|string)=>request.delete<any,any>(API.REMOVESKU_URL+spuId)
删除sku:
el-popconfirm:气泡组件
删除后重新获取页面
路由组件销毁前,清空关于仓库分类的数据
src/views/product/spu/index.vue
<el-popconfirm :title="`你确定删除吗${row.spuName}`" width="200px" @confirm="deleteSpu(row)">
<template #reference>
<el-button type="primary" size="small" icon="Delete" title="删除SKU"></el-button>
</template>
</el-popconfirm>
// 删除某个spu按钮的回调
const deleteSpu = async(row:SpuData) => {
let result:any = await reqRemoveSqu((row.id as number))
if (result.code == 200) {
ElMessage({
type:"success",
message:"删除成功"
})
// 获取剩余的
getHasSpu(records.value.length>1?pageNo.value:pageNo.value-1);
}else{
ElMessage({
type:"error",
message:"删除失败"
})
}
}
// 路由组件销魂前,清空关于仓库分类的数据
onBeforeUnmount(()=>{
categoryStore.$reset();
})
9sku静态搭建、展示、上、下架以及删除
//SKU模块接口管理
import request from "@/utils/request";
import type { SkuResponseData, SkuInfoData } from './type'
//枚举地址
enum API {
//获取已有的商品的数据-SKU
SKU_URL = '/admin/product/list/',
//上架
SALE_URL = '/admin/product/onSale/',
//下架的接口
CANCELSALE_URL = '/admin/product/cancelSale/',
//获取商品详情的接口
SKUINFO_URL = '/admin/product/getSkuInfo/',
//删除已有的商品
DELETESKU_URL = '/admin/product/deleteSku/'
}
//获取商品SKU的接口
export const reqSkuList = (page: number, limit: number) => request.get<any, SkuResponseData>(API.SKU_URL + `${page}/${limit}`)
//已有商品上架的请求
export const reqSaleSku = (skuId: number) => request.get<any, any>(API.SALE_URL + skuId);
//下架的请求
export const reqCancelSale = (skuId: number) => request.get<any, any>(API.CANCELSALE_URL + skuId);
//获取商品详情的接口
export const reqSkuInfo = (skuId: number) => request.get<any, SkuInfoData>(API.SKUINFO_URL + skuId);
//删除某一个已有的商品
export const reqRemoveSku = (skuId: number) => request.delete<any, any>(API.DELETESKU_URL + skuId)
src/api/product/sku/type.ts
export interface ResponseData {
code: number,
message: string,
ok: boolean
}
//定义SKU对象的ts类型
export interface Attr {
id?:number
"attrId": number | string,//平台属性的ID
"valueId": number | string,//属性值的ID
}
export interface saleArr {
id?:number,
"saleAttrId": number | string,//属性ID
"saleAttrValueId": number | string,//属性值的ID
}
export interface SkuData {
"category3Id"?: string | number,//三级分类的ID
"spuId"?: string | number,//已有的SPU的ID
"tmId"?: string | number,//SPU品牌的ID
"skuName"?: string,//sku名字
"price"?: string | number,//sku价格
"weight"?: string | number,//sku重量
"skuDesc"?: string,//sku的描述
"skuAttrValueList"?: Attr[],
"skuSaleAttrValueList"?: saleArr[]
"skuDefaultImg"?: string,//sku图片地址
isSale?: number,//控制商品的上架与下架
id?: number
}
//获取SKU接口返回的数据ts类型
export interface SkuResponseData extends ResponseData {
data: {
records: SkuData[],
"total": number,
"size": number,
"current": number,
"orders": [],
"optimizeCountSql": boolean,
"hitCount": boolean,
"countId": null,
"maxLimit": null,
"searchCount": boolean,
"pages": number
}
}
//获取SKU商品详情接口的ts类型
export interface SkuInfoData extends ResponseData {
data: SkuData
}
src/views/product/sku/index.vue
逻辑和spu的差不多,就是多了歌预览的,但同样都是传参上去,弄一个轮播图
<template>
<el-card>
<el-table border style="margin: 10px 0px;" :data="skuArr">
<el-table-column label="序号" type="index" align="center" width="80px"></el-table-column>
<el-table-column label="名称" show-overflow-tooltip width="150px" prop="skuName"></el-table-column>
<el-table-column label="描述" show-overflow-tooltip width="150px" prop="skuDesc"></el-table-column>
<el-table-column label="图片" width="150px">
<template #="{ row, $index }">
<img :src="row.skuDefaultImg" alt="" style="width: 100px;height: 100px;">
</template>
</el-table-column>
<el-table-column label="重量" width="150px" prop="weight"></el-table-column>
<el-table-column label="价格" width="150px" prop="price"></el-table-column>
<el-table-column label="操作" width="250px" fixed="right">
<template #="{ row, $index }">
<el-button type="primary" size="small" :icon="row.isSale == 1 ? 'Bottom' : 'Top'"
@click="updateSale(row)"></el-button>
<el-button type="primary" size="small" icon="Edit" @click="updateSku"></el-button>
<el-button type="primary" size="small" icon="InfoFilled" @click="findSku(row)"></el-button>
<el-popconfirm :title="`你确定要删除${row.skuName}?`" width="200px" @confirm="removeSku(row.id)">
<template #reference>
<el-button type="primary" size="small" icon="Delete"></el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<el-pagination v-model:current-page="pageNo" v-model:page-size="pageSize" :page-sizes="[10, 20, 30, 40]"
:background="true" layout="prev, pager, next, jumper,->,sizes,total" :total="total" @current-change="getHasSku"
@size-change="handler" />
<!-- 抽屉组件:展示商品详情 -->
<el-drawer v-model="drawer">
<!-- 标题部分 -->
<template #header>
<h4>查看商品的详情</h4>
</template>
<template #default>
<el-row style="margin:10px 0px;">
<el-col :span="6">名称</el-col>
<el-col :span="18">{{ skuInfo.skuName }}</el-col>
</el-row>
<el-row style="margin:10px 0px;">
<el-col :span="6">描述</el-col>
<el-col :span="18">{{ skuInfo.skuDesc }}</el-col>
</el-row>
<el-row style="margin:10px 0px;">
<el-col :span="6">价格</el-col>
<el-col :span="18">{{ skuInfo.price }}</el-col>
</el-row>
<el-row style="margin:10px 0px;">
<el-col :span="6">平台属性</el-col>
<el-col :span="18">
<el-tag style="margin:5px;" v-for="item in skuInfo.skuAttrValueList" :key="item.id">{{
item.valueName }}</el-tag>
</el-col>
</el-row>
<el-row style="margin:10px 0px;">
<el-col :span="6">销售属性</el-col>
<el-col :span="18">
<el-tag style="margin:5px;" v-for="item in skuInfo.skuSaleAttrValueList" :key="item.id">{{
item.saleAttrValueName }}</el-tag>
</el-col>
</el-row>
<el-row style="margin:10px 0px;">
<el-col :span="6">商品图片</el-col>
<el-col :span="18">
<el-carousel :interval="4000" type="card" height="200px">
<el-carousel-item v-for="item in skuInfo.skuImageList" :key="item.id">
<img :src="item.imgUrl" alt="" style="width:100%;height: 100%;">
</el-carousel-item>
</el-carousel>
</el-col>
</el-row>
</template>
</el-drawer>
</el-card>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
//引入请求
import { reqSkuList, reqSaleSku, reqCancelSale, reqSkuInfo, reqRemoveSku } from '@/api/product/sku'
//引入ts类型
import type { SkuResponseData, SkuData, SkuInfoData } from '@/api/product/sku/type';
import { ElMessage } from 'element-plus';
//分页器当前页码
let pageNo = ref<number>(1);
//每一页展示几条数据
let pageSize = ref<number>(10);
let total = ref<number>(0);
let skuArr = ref<SkuData[]>([]);
//控制抽屉显示与隐藏的字段
let drawer = ref<boolean>(false);
let skuInfo = ref<any>({});
//组件挂载完毕
onMounted(() => {
getHasSku();
});
const getHasSku = async (pager = 1) => {
//当前分页器的页码
pageNo.value = pager;
let result: SkuResponseData = await reqSkuList(pageNo.value, pageSize.value);
if (result.code == 200) {
total.value = result.data.total;
skuArr.value = result.data.records;
}
}
//分页器下拉菜单发生变化触发
const handler = (pageSizes: number) => {
getHasSku();
}
//商品的上架与下架的操作
const updateSale = async (row: SkuData) => {
//如果当前商品的isSale==1,说明当前商品是上架的额状态->更新为下架
//否则else情况与上面情况相反
if (row.isSale == 1) {
//下架操作
await reqCancelSale((row.id as number));
//提示信息
ElMessage({ type: 'success', message: '下架成功' });
//发请求获取当前更新完毕的全部已有的SKU
getHasSku(pageNo.value);
} else {
//下架操作
await reqSaleSku((row.id as number));
//提示信息
ElMessage({ type: 'success', message: '上架成功' });
//发请求获取当前更新完毕的全部已有的SKU
getHasSku(pageNo.value);
}
}
//更新已有的SKU
const updateSku = () => {
ElMessage({ type: 'success', message: '程序员在努力的更新中....' })
}
//查看商品详情按钮的回调
const findSku = async (row: SkuData) => {
//抽屉展示出来
drawer.value = true;
//获取已有商品详情数据
let result: SkuInfoData = await reqSkuInfo((row.id as number));
//存储已有的SKU
skuInfo.value = result.data;
}
//删除某一个已有的商品
const removeSku = async (id: number) => {
//删除某一个已有商品的情况
let result: any = await reqRemoveSku(id);
if (result.code == 200) {
//提示信息
ElMessage({ type: 'success', message: '删除成功' });
//获取已有全部商品
getHasSku(skuArr.value.length > 1 ? pageNo.value : pageNo.value - 1);
} else {
//删除失败
ElMessage({ type: 'error', message: '系统数据不能删除' });
}
}
</script>
<style scoped>
.el-carousel__item h3 {
color: #475669;
opacity: 0.75;
line-height: 200px;
margin: 0;
text-align: center;
}
.el-carousel__item:nth-child(2n) {
background-color: #99a9bf;
}
.el-carousel__item:nth-child(2n + 1) {
background-color: #d3dce6;
}
</style>