1:什么是后台管理系统项目?
注意:前端领域当中,开发后台管理系统项目,并非是java、php等后台语言项目。
build
mock
node_modules
public
src - 程序员源代码存储的地方
------api文件夹:涉及请求相关的
------assets文件夹:里面放置一些静态资源(一般共享的),放在aseets文件夹里面静态资源,在webpack打包的时候,会进行编译
------components文件夹:一般放置非路由组件获取全局组件
------icons这个文件夹的里面放置了一些svg矢量图
------layout文件夹:他里面放置一些组件与混入
------router文件夹:与路由相关的
-----store文件夹:一定是与vuex先关的
-----style文件夹:与样式先关的
------utils文件夹:request.js是axios二次封装文件****
------views文件夹:里面放置的是路由组件
-------------------------------------------
App.vue:根组件
main.js:入口文件
permission.js:与导航守卫先关、
settings:项目配置项文件
.env.development
.env.producation
background: url(~@/assets/deadlift.jpg);
background-size: 100% 100%;
书写API (store -> modules -> user.js)
一般是先在vuex中书写相应的api如:actions,commit等 然后在具体想用的组件中进行dispatch操作
axios二次封装
二次封装的时候,有些内容我们也要进行修改,如服务器返回的接口,有时候返回真实的结果可能是200,也可能是20000
if (res.code !== 20000 && res.code != 200) { ... }
// 引入axios(axios进行了二次封装操作)
import request from '@/utils/request'
// 对外暴露接口接口函数
export function login(data) {
return request({
url: '/admin/acl/index/login',
method: 'post',
data
})
}
// 对外暴露用户信息的函数
export function getInfo(token) {
return request({
url: '/admin/acl/index/info',
method: 'get',
params: { token }
})
}
export function logout() {
return request({
url: '/admin/acl/index/logout',
method: 'post'
})
}
换成真实的url接口,以及method方法也是需要进行更改操作
在换入接口以后还需要解决跨域问题 – 在vue.config.js文件中进行配置
devServer: {
port: port,
open: true,
overlay: {
warnings: false,
errors: true
},
proxy: {
'/dev-api': {
// 数据来自于哪一台代理服务器 就使用哪一台代理服务器即可
target: 'http://39.98.123.211:8170',
pathRewrite: { '^/dev-api': '' }
},
}
// before: require('./mock/mock-server.js')
// 配置代理跨域
},
proxy: {
'/dev-api': {
// 数据来自于哪一台代理服务器 就使用哪一台代理服务器即可
target: 'http://39.98.123.211:8170',
pathRewrite: { '^/dev-api': '' }
},
}
整个路由的项目框架都是依赖于Layout外部框架
/* 整个项目的框架 */
import Layout from '@/layout'
路由组件的配置是通过懒加载的形式进行的 - component:箭头函数的形式
component: () => import('@/views/login/index'),
需要注意的是,二级路由是放在Children属性下面的
export const constantRoutes = [
{
path: '/login',
component: () => import('@/views/login/index'),
hidden: true
},
{
path: '/404',
component: () => import('@/views/404'),
hidden: true
},
{
path: '/',
component: Layout,
// 访问 / 会进入到整个大的一个结构,但是它会立马重定向到它的二级路由
redirect: '/dashboard',
children: [{
path: 'dashboard',
name: 'Dashboard',
component: () => import('@/views/dashboard/index'),
meta: { title: 'Dashboard', icon: 'dashboard' }
}]
},
/*
在搭建路由的时候,在总体框架 component: Layout,中书写完全以后,
组件的内容就在其中了
*/
{
path: '/product',
// 我们在进行搭建组件的时候都是在layout组件下面进行的
component: Layout,
name: 'Product',
meta: { title: '商品管理', icon: 'el-icon-goods' },
children: [
{
path: 'trademark',
name: 'Trademark',
component: () => import('@/views/product/tradeMark'),
meta: { title: '品牌管理' }
},
{
path: 'attr',
name: 'Attr',
component: () => import('@/views/product/Attr'),
meta: { title: '平台属性管理' }
},
{
path: 'spu',
name: 'Spu',
component: () => import('@/views/product/Spu'),
meta: { title: 'Spu管理' }
},
{
path: 'sku',
name: 'Sku',
component: () => import('@/views/product/Sku'),
meta: { title: 'Sku管理' }
}
]
},
// 404 page must be placed at the end !!!
{ path: '*', redirect: '/404', hidden: true }
]
然后对外暴露一个箭头函数,而且这个箭头函数一致性会返回一个路由器对象
const createRouter = () => new Router({ // mode: 'history', // require service support scrollBehavior: () => ({ y: 0 }), routes: constantRoutes })'运行
重置路由操作,了解
// 重置路由操作
export function resetRouter() {
const newRouter = createRouter()
router.matcher = newRouter.matcher // reset router
}
export default router // 对外暴露
首先项目图,product下应该有很多的二级路由 先创建二级路由组件

<!-- 像element-ui组件库里面的内容可以直接书写 -->
<el-button type="primary" icon="el-icon-plus" style="margin: 10px 0px"
>添加</el-button
>
其中,任何标签中还可以书写相应的样式 style标签,形式如下
style="margin: 10px 0px"'运行
<!--
表格组件
- data:表格组件将来要展示的数据 --- 数组类型
- border:表格的边框
- el-table-column :代表一列
- label:显示的标题
- width:对应列的宽度
- align:标题的对齐方式
-->
<el-table :data="data" style="width: 100%" border>
<el-table-column prop="prop" label="序号" width="80px" align="center">
</el-table-column>
<el-table-column prop="prop" label="品牌名称" width="width">
</el-table-column>
<el-table-column prop="prop" label="品牌Logo" width="width">
</el-table-column>
<el-table-column prop="prop" label="操作" width="width">
</el-table-column>
</el-table>
<!--
分页器
当前第几页、数据总条数、每一页展示的条数、连续页码数
@size-change="handleSizeChange"
@current-change="6"
- current-page 当前第几页
- total 代表分页器一共需要展示数据条数
- page-size 代表每一页需要展示多少条数据
- page-sizes 可以设置每一页展示多少条数据
- layout 可以实现分页器的布局
- pager-count 按钮的数量 如果9 连续页码数是7
- 都是el-pagination里面的属性内容 是包裹在el-pagination标签内部的,注意结束位置
-->
<el-pagination
style="margin-top:20px;textAlign:center"
:current-page="6"
:total="99"
:pager-count='9'
:page-size="3"
:page-sizes="[3, 5, 10]"
layout="prev, pager, next, jumper, ->, sizes, total">
</el-pagination>
书写相关的API接口

/*
将四个模块请求的接口统一对外暴露
*/
import * as trademark from './product/tradeMark'
import * as attr from './product/attr'
import * as spu from './product/spu'
import * as sku from './product/sku'
// 对外暴露 kv一致
export default {
trademark,
attr,
spu,
sku
}
/*
引入相关API请求接口
组件实例的原型的原型指向的是Vue.prototype
任意组件可以使用API相关的接口
*/
import API from '@/api'
Vue.prototype.$API = API
proxy: {
/* '/dev-api': {
// 数据来自于哪一台代理服务器 就使用哪一台代理服务器即可
target: 'http://39.98.123.211:8170',
pathRewrite: { '^/dev-api': '' }
}, */
'/dev-api/admin/acl': {
// 数据来自于哪一台代理服务器 就使用哪一台代理服务器即可
target: 'http://39.98.123.211:8170',
pathRewrite: { '^/dev-api': '' }
},
'/dev-api/admin/product': {
// 数据来自于哪一台代理服务器 就使用哪一台代理服务器即可
// 配置了多个跨域问题
target: 'http://39.98.123.211:8510',
pathRewrite: { '^/dev-api': '' }
},
}
获取数据在组件内部静态展示
data() {
return {
// 分页器第几页
page: 1,
// 当前页数展示数据条数
limit: 3,
// 总共数据条数
total: 0,
// 列表展示的数据
list: [],
};
},
// 解构出参数
const { page, limit } = this;
async getPageList(pager = 1) {
this.page = pager
// 解构出参数
const { page, limit } = this;
// 获取品牌列表的接口,需要带两个参数(在data中声明)
let result = await this.$API.trademark.reqTradeMarkList(page, limit);
// console.log(result);
if(result.code == 200) {
this.total = result.data.total
this.list = result.data.records
}
},
handleCurrentChange(pager) {
// 修改参数
this.page = pager
this.getPageList()
}
// 当分页器某一页需要展示数据条数发生变化的时候出现
handleSizeChange(limit) {
// console.log(limit)
// 整理参数,再次发送请求
this.limit = limit
this.getPageList()
}
// 点击添加品牌的按钮
showDialog() {
// 显示对话框
this.dialogFormVisible = true;
// 为了防止用于点击取消,然后点进去又有可以每次进去都清楚数据
this.tmForm = {
tmName: "",
logoUrl: "",
};
},
this.dialogFormVisible = true;
async addOrUpdateTradeMark() {
this.dialogFormVisible = false
// 发请求(添加品牌|修改品牌)
let result = await this.$API.trademark.reqAddOrUpdateTradeMark(this.tmForm)
if(result.code == 200) {
// 弹出一个信息框:添加品牌成功、修改品牌成功
this.$message(this.tmForm.id ? '修改品牌成功' : '添加品牌成功')
// 成功以后,将数据进行展示即可
this.getPageList()
}
}
this.$message()
@click="updateTradeMark(row)"
updateTradeMark(row) {
// console.log(row) row是整个品牌的对象 - 用于选中的品牌的信息
// 显示对话框
this.dialogFormVisible = true;
/*
将已有的品牌信息赋值给tmForm进行展示
将服务器返回的品牌信息,直接赋值给了tmForm进行展示
也就是说tmForm存储的即为服务器返回的品牌信息
获取到的对象,进行浅拷贝操作 {...row}
*/
// this.tmForm = row;
this.tmForm = { ...row };
},
需要注意的是: 有时候获取到的对象是和v-model的属性绑定了的 需要用到浅拷贝,这样修改此处不会影响到别的地方
// 表单验证规则
rules: {
/*
required 必须要验证字段
message 提示信息
trigger 用户行为设置(事件的设置blur change)
min max 字段长度
*/
// 品牌名称的验证规则
tmName: [
{ required: true, message: "请输入品牌名称", trigger: "blur" },
{
min: 2,
max: 10,
message: "长度在 2 到 10 个字符",
trigger: "change",
},
],
// 品牌logo的验证规则
logoUrl: [
{
required: true,
message: "请选择品牌的图片区域",
trigger: "change",
},
],
},
<!-- form表单
:model属性 - 这个属性的作用是 把表单的数据收集到那个对象身上,将来表单验证,也需要这个属性
-->
<el-form style="width: 80%" :model="tmForm" :rules="rules" ref="ruleForm">
addOrUpdateTradeMark() {
// 当全部验证字段通过 再去书写业务逻辑
this.$refs.ruleForm.validate(async (success) => {
if (success) {
// 如果全部字段符合条件
this.dialogFormVisible = false;
// 发请求(添加品牌|修改品牌)
let result = await this.$API.trademark.reqAddOrUpdateTradeMark(
this.tmForm
);
if (result.code == 200) {
// 弹出一个信息框:添加品牌成功、修改品牌成功
// this.$message(this.tmForm.id ? "修改品牌成功" : "添加品牌成功");
this.$message({
type: "success",
message: this.tmForm.id ? "修改品牌成功" : "添加品牌成功",
});
// 成功以后,将数据进行展示即可
// 如果添加品牌,停留在第一页,修改品牌应该停留在当前页面
this.getPageList(this.tmForm.id ? this.page : 1);
}
} else {
console.log("Submit Error!!!");
return false;
}
});
},
/*
删除品牌的信息
删除品牌 /admin/product/baseTrademark/remove/{id} delete
在书写方法的时候 中间是有参数的
*/
export const reqDeleteTradeMark = (id) => request({
url: `/admin/product/baseTrademark/remove/${id} `,
method: 'delete'
})
@click="deleteTradeMark(row)"
// 删除品牌的操作
deleteTradeMark(row) {
this.$confirm(`你确定删除${row.tmName}?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async () => {
// 当用户点击确定按钮的时候会触发
// 向服务器发请求
let result = await this.$API.trademark.reqDeleteTradeMark(row.id);
if (result.code == 200) {
this.$message({
type: "success",
message: "删除成功!",
});
// 再次获取列表数据 但是展示数据的页码数需要发生变化
this.getPageList(this.list.length>1 ? this.page : this.page-1)
}
})
.catch(() => {
// 当用户点击取消按钮的时候会触发
this.$message({
type: "info",
message: "已取消删除",
});
});
},
this.getPageList(this.list.length>1 ? this.page : this.page-1)
import CategorySelect from '@/components/CategorySelect'
// 注册全局组件 因为一开始名字中用到了,所以直接用名字来进行注册操作
Vue.component(CategorySelect.name, CategorySelect)
注意: 子给父传数据我们用的是自定义事件

/*
平台属性管理模块请求文件
*/
import request from "@/utils/request"
/*
获取一级分类数据接口
/admin/product/getCategory1 get
*/
export const reqCategory1List = () =>request({
url:'/admin/product/getCategory1',
method:'get',
})
/*
获取二级分类数据接口
/admin/product/getCategory2/{category1Id} get
*/
export const reqCategory2List = (category1Id) =>request({
url:`/admin/product/getCategory2/${category1Id}`,
method:'get',
})
/*
获取三级分类数据接口
/admin/product/getCategory2/{category1Id} get
*/
export const reqCategory3List = (category2Id) =>request({
url:`/admin/product/getCategory3/${category2Id}`,
method:'get',
})
因为组件内部需要收集到相应的数据进行展示 - 且数据收集时候的初始值都是[] 分类的id数量有多个可以放在一个对象中
data() {
return {
// 一级分类的数据
list1: [],
// 二级分类的数据
list2: [],
// 三级分类的数据
list3: [],
// 收集相应的一级二级三级分类的id
cForm: {
category1Id: "",
category2Id: "",
category3Id: "",
},
};
},
// 组件挂载完毕,向服务器发请求,获取相应的一级分类的数据
mounted() {
// 获取一级分类的数据的方法
this.getCategory1List();
},
methods: {
// 获取一级分类数据的方法
async getCategory1List() {
// 获取一级分类的请求,不需要携带参数
let result = await this.$API.attr.reqCategory1List();
// console.log(result)
// 不使用vuex来操作数据的话,就需要通过在data属性中设置相应的数据来进行·保存
if (result.code == 200) {
this.list1 = result.data;
}
},
/*
一级分类的select事件的回调
- 当一级分类的option发生变化的时候获取相应二级分类的数据
*/
async handler1() {
this.list2 = [];
this.list3 = [];
this.cForm.category2Id = "";
this.cForm.category3Id = "";
// console.log(111)
// 解构出一级分类的id
const { category1Id } = this.cForm;
/* 为了区分开传的到底是几id 可以进行相应的打标第操作 */
// this.$emit("getCategoryId", category1Id);
this.$emit("getCategoryId", { categoryId: category1Id, level: 1 });
// 通过一级分类的id 获取二级分类的数据
let result = await this.$API.attr.reqCategory2List(category1Id);
// console.log(result);
if (result.code == 200) {
this.list2 = result.data;
}
},
/*
二级分类的select事件的回调
- 当二级分类的option发生变化的时候获取相应三级分类的数据
*/
async handler2() {
this.list2 = [];
this.cForm.category3Id = "";
const { category2Id } = this.cForm;
this.$emit("getCategoryId", { categoryId: category2Id, level: 2 });
let result = await this.$API.attr.reqCategory3List(category2Id);
if (result.code == 200) {
this.list3 = result.data;
}
},
// 三级分类的事件回调
handler3() {
const { category3Id } = this.cForm;
this.$emit("getCategoryId", { categoryId: category3Id, level: 3 });
},
},
父组件需要用到子组件中的数据来进行相应展示 - 父组件发送请求,主要是需要用到子组件传递过来的三级分类的id
因为书写API接口方法需要用到,因为在子组件中触发自定义函数用到了level来区分具体是哪一个级别分类的id
父组件Attr
methods: {
/*
自定义事件的回调
*/
getCategoryId({ categoryId, level }) {
// 通过level来区别是几级id
if (level == 1) {
this.category1Id = categoryId;
this.category2Id = "";
this.category3Id = "";
} else if (level == 2) {
this.category2Id = categoryId;
this.category3Id = "";
} else {
// 代表三级分类已经有了
this.category3Id = categoryId;
// 发请求获取品牌属性
this.getAttrList()
}
},
// 获取平台属性的数据
getAttrList() {
console.log('发请求')
}
},
/*
获取平台属性的接口
/admin/product/attrInfoList/{category1Id}/{category2Id}/{category3Id}
*/
export const reqAttrList = (category1Id, category2Id, category3Id) => request({
url: `/admin/product/attrInfoList/${category1Id}/${category2Id}/${category3Id}`,
method: 'get'
})
获取数据来进行展示
async getAttrList() {
// console.log('发请求')
// 获取分类的id
const { category1Id, category2Id, category3Id } = this;
let result = await this.$API.attr.reqAttrList(
category1Id,
category2Id,
category3Id
);
// console.log(result)
if (result.code == 200) {
this.attrList = result.data;
}
},
// 接收平台属性的字段
attrList: [],
将上述获取到的属性 展示到 el-card组件当中即可
添加属性
{{ attrValue.valueName }}
添加属性值
取消
保存
取消
**el-table中是以列属性来进行展示的,**其中el-table-column是用来展示每一列的属性的
**点击添加或者修改按钮以后,**会跳转展示到另一个界面当中, 使用 v-show来完成即可 且通过isShowTable来控制完成
同样的有内部有内容框 每一个el-table-column中有el-input时候就需要用到作用域插槽来实现该效果
收集属性名的操作
收集属性名首先也是一样的,需要先书写接口函数 - 当有多个接口的时候,我们可以用一个data对象来进行表示
/*
添加属性与属性值接口
/admin/product/saveAttrInfo post
写data,用一个对象来表示数据即可
data:
"attrName":"" 属性名
"attrValueList":[ 属性名中属性值,因为属性值可以是多个,因此需要的是数组
{
"attrId":0, // 属性的ID
"valueName":'string' // 相应的属性值
}
],
"categoryId":0, category3Id
"categoryLevel":3
*/
export const reqAddAttr = (data) => request({
url: "/admin/product/saveAttrInfo",
method: 'post',
data
})
在组件中进行调用的时候,不用vuex以后就需要在data中设置相应的数据来进行接收
attrInfo: {
attrName: "", // 属性名
attrValueList: [
// 属性值
/* {
attrId: 0, // 相应的属性名的id
valueName: "",
}, */
],
categoryId: 0, // 三级分类的id
categoryLevel: 3, // 因为服务器也需要区分是几级Id
},
一开始是不应该有内容的,只有每次点击了才会加上相应的内容,所以在data中,一开始attrValueList中是不应该有内容的
添加属性值
// 添加属性值的按钮
addAttrValue() {
// 向属性值的数组里面添加元素
this.attrInfo.attrValueList.push({
attrId:undefined,
valueName:'',
})
}
解决返回按钮数据回显问题
**点击取消以后,我们需要对数据进行取消操作 ** 清除数据,this.attrInfo直接进行赋值操作即可,里面相应的内容设为空即可
但是我们在添加商品以后,需要设置商品的id即可
// 清除数据
this.attrInfo = {
attrName: "", // 属性名
attrValueList: [
// 属性值
/* {
attrId: 0, // 相应的属性名的id
valueName: "",
}, */
],
// categoryId: 0, // 三级分类的id 点击的那一刻 3级分类的id是可以拿到的
categoryId: this.category3Id,
categoryLevel: 3, // 因为服务器也需要区分是几级Id
};
修改属性操作
**修改某一个属性的时候,先切换页面 在将attrInfo的数据进行相应的替换操作
- 但是在数据里面的结构当中,当存在对象里面套数组,数组里面套对象等操作,结构相对复杂一点的可以使用深拷贝来解决
// 修改某一个属性
updateAttr(row) {
// console.log(row)
this.isShowTable = false;
// 将选中的属性,赋值给attrInfo
// 由于数据结构当中存在对象里面套数组 数组里面套对象 因此需要使用深拷贝解决这类问题
// 深拷贝 浅拷贝在面试的时候出现的频率很高,切记达到手写深拷贝和浅拷贝
this.attrInfo = cloneDeep(row);
},
查看模式与修改模式切换
- 为了控制每一个输入框的里面是查看还是修改模式的状态,可以在添加属性的时候,就添加一个flag属性来控制这一点
// 添加属性值的按钮
addAttrValue() {
// 向属性值的数组里面添加元素
this.attrInfo.attrValueList.push({
// attrId: undefined,
/*
对于修改某一个属性的时候
可以在已有的属性值基础之上新增新的属性值
- 新增属性值的时候,需要把已有的属性的id带上
此处直接解决了两个地方,添加的时候没有id就undefined
修改的时候有id 就直接添加上即可
*/
attrId: this.attrInfo.id,
valueName: "",
/*
flag属性
- 给每一个属性值添加一个标记flag
- 用户切换查看模式与编辑模式
- 好处:每一个属性值可以控制自己的模式切换
- 当前的flag属性是响应式数据(数据变化视图跟着变化)
- 因为flag本质上是attrValueList上面的,在data中有定义过
*/
flag: true,
});
},
el-input与span之间的显示与隐藏通过 v-if 以及 v-else来解决处理即可
{{ row.valueName }}
失去焦点的事件
// 失去焦点的事件,切换为查看模式 显示span
toLook(row) {
row.flag = false
}
查看模式与编辑模式注意事项
- 失去焦点的事件,切换为查看模式,显示span
- 但是在这样进行相应切换的时候,我们有自己需要注意的点:尤其是用户在input框里面进行输入的时候
- 有时候用户在进行输入的时候,输入的内容会有相应的偏差,比如说用户输入为空,可以发生一个提示信息
// 如果属性值为空 不能作为新的属性值 需要给用户提示 让他输入一个其他属性值
if (row.valueName.trim() == "") {
this.$message("请输入一个正常的属性值:");
return;
}
- 新增的属性值,不能与已有的属性值进行重复,重复的话直接进行return操作即可
// 失去焦点的事件,切换为查看模式 显示span
toLook(row) {
// 如果属性值为空 不能作为新的属性值 需要给用户提示 让他输入一个其他属性值
if (row.valueName.trim() == "") {
this.$message("请输入一个正常的属性值:");
return;
}
// 新增的属性值,不能与已有的属性值重复
let isRepat = this.attrInfo.attrValueList.some((item) => {
// 需要将row从数组里面判断的时候去除
/*
row是最新的属性值【数组的最后一项元素】
判断的时候,需要把已有的数组当中新增的这个属性值去除
*/
if (row !== item) {
return row.valueName == item.valueName;
}
});
// console.log(isRepat)
if (isRepat) return;
// row是当前用户添加的最新的属性值
// console.log(row)
// 当前的编辑模式变为查看模式【让input消失,显示span】
row.flag = false;
},
修改属性的查看与编辑模式切换
- 我们在进行修改的时候,点击修改的时候,因为是一开始没有定义flag属性,所以没有办法切换查看模式和编辑模式
- 遍历每一个this.attrInfo.attrValueList.forEach((item) => {} 将里面的元素的flag属性都设置为 false的话,在内部是探测不到的
- **原因:**因为vue无法探测普通的新增property,这样书写的属性并非响应式属性
- 我们可以使用 Vue.set 来添加响应式属性
this.attrInfo.attrValueList.forEach((item) => {
// 这样书写也可以给属性值添加flag属性,但是会发现视图不会跟着变化(因为flag不是响应式数据)
// item.flag = false 之前用put是可以探测到的,但是这里是给一个对象进行操作
/*
上述:因为vue无法探测普通的新增property,这样书写的属性并非响应式属性
响应式属性(数据变化视图跟着变化)
Vue.set 向响应式
参数一:对象
参数二:添加新的响应式属性
参数三:添加新的响应式属性的属性值
*/
this.$set(item, "flag", false);
});
表单元素自动聚焦的实现
点击span自动变成编辑模式
@click="toEdit(row, $index)"
// 点击span的回调,变为编辑模式
toEdit(row, index) {
row.flag = true;
// 获取input节点 实现自动聚焦
// console.log(this.$refs[index])
/*
- 获取input节点,实现自动聚焦
- 需要注意:点击span的时候,切换为input变为编辑模式
但是需要注意:对于浏览器而言页面的重绘会会消耗时间
- 点击span的时候,重绘一个input是需要耗费时间的,不可能一点击span就立马获取到input
*/
this.$nextTick(() => {
// 获取相应的input表单元素实现聚焦
this.$refs[index].focus();
});
},
- 当然在我们每一次点击一个添加按钮的时候,同样需要在输入框中立马出现自动聚焦的操作,
- 因为每次点击添加操作应该都是在 列表 的最后一个元素当中
// 添加添加按钮以后 自动聚焦操作
this.$nextTick(() => {
this.$refs[this.attrInfo.attrValueList.length - 1].focus();
});
- 需要用到ref,所以我们需要在 el-input中进行相应的定义操作,定义ref并将上述的$index进行传入即可
{{ row.valueName }}
删除属性的操作
其中删除属性我们用的是 el-popconfirm 标签
其中确认删除也是有固定的 点击事件 onConfirm – 因为我们确认删除是需要知道具体删除的是哪一个事件
书写相应的点击事件 – 需要传入相应的参数,即为点击属性的相应索引值
deleteAttrValue(index) {
// alert(111)
// 当前删除属性值的操作是不需要发请求的
this.attrInfo.attrValueList.splice(index, 1);
},
保存操作
当用户在输入框中输入完毕以后,我们需要将数据进行保存的操作
- 过滤掉的属性值不能是空的
- 删除掉flag属性
- 通过调用·方法向服务器中保存相应的属性,因为需要知道返回成功和返回失败等信息,可以使用try…catch
// 保存按钮 进行添加属性或修改属性的操作
async addOrUpdateAttr() {
// alert(111)
/*
整理参数
- 1、如果用户添加很多属性值,且属性值为空的不应该提交给服务器
- 2、提交给服务器数据中不应该出现flag字段
*/
this.attrInfo.attrValueList = this.attrInfo.attrValueList.filter(
(item) => {
// 过滤掉属性值不是空的
if (item.valueName != "") {
// 删除掉flag属性
delete item.flag;
return true;
}
}
);
// 向服务器发送请求来保存数据
// 成功干什么 失败干什么 可以使用try...catch
try {
// 发请求
await this.$API.attr.reqAddOrUpdateAttr(this.attrInfo);
// 展示平台属性的信号量进行切换
this.isShowTable = true;
// 提示消息
this.$message({
type: "success",
message: "保存成功",
});
// 再次调用服务器全新的数据来进行调用展示全新的数据
this.getAttrList();
} catch (error) {
/*
有很多数据是服务器提供的数据,是不支持进行修改的
系统数据进行修改会发生报错
*/
this.$message("保存失败");
}
},
按钮与三级联动的可操作性
父亲给儿子传递事件直接使用props进行操作即可
通过下面属性值的输入,来控制上面三级联动的效果, 只需传入isShowTable属性即可
子组件CategorySelect直接进行接受即可 然后在每一个下拉框中加上disabled属性即可
Spu管理模块(静态)
- 最上面是一样的也是三级联动组件,直接调用全局组件即可,放在el-card当中
- 但是和Attr组件不同的是,这下面区域有三种状态,所以我们必须使用数值来代替情况,在data中定义scene字段,表示三种状态
/*
0代表展示SPU列表数据,1
1添加SPU|修改SPU
2添加SKU
*/
scene: 2,
动态展示Spu列表
- 根据上面所说的Spu列表的展示其实也算是一种状态的展示,其状态可以用 scene == 0 来进行表示
- 书写获取Spu列表数据的方法 getSpuList
书写获取Spu列表的API接口方法
参数 需要三个,其中如果参数个数在路径中没有体现出来的话,可以通过params参数来进行携带操作
// 1、先引入我们二次封装的axios
import request from '@/utils/request'
// 2、获取SPU列表数据的接口
/*
/admin/product/{page}/{limit} get操作
参数:page、limit、category3Id
*/
export const reqSpuList = (page, limit, category3Id) => request({
url: `/admin/product/${page}/${limit}`,
method: 'get',
// 还有一个参数在路径中没有体现出来,可以通过params参数进行携带
params: { category3Id }
})
书写获取Spu列表数据的方法 getSpuList
async getSpuList() {
// 解构出函数需要的三个参数
const { page, limit, category3Id } = this;
/*
携带三个参数
1、page 第几页
2、limit 每一页需要展示多少条数据
3、三级分类id
*/
let result = await this.$API.spu.reqSpuList(page, limit, category3Id);
// console.log(result);
if (result.code == 200) {
this.total = result.data.total;
this.records = result.data.records;
}
},
在从服务器获取数据的时候,像Spu列表数据,总条数等都需要在data中预先定义出来
data() {
return {
// 分类的id
category1Id: "",
category2Id: "",
category3Id: "",
// 控制三级联动的可是测试性
show: true,
page: 1, // 分页器当前第几页
limit: 3, // 分页器每一页需要展示多少条数据
records: [], // spu列表的数据
total: 0, // 分页器一共需要展示数据的条数
/*
0代表展示SPU列表数据,1
1添加SPU|修改SPU
2添加SKU
*/
scene: 2,
};
},
分页器两个方法的书写
- 点击分页器第几页按钮的回调,其实就是将函数的参数进行了相应的修改,然后再次调用函数来返回相应的参数即可
handleCurrentChange(page) {
this.page = page;
this.getSpuList();
},
- 当分页器某一个展示数据条数发生变化的回调
handleSizeChange(limit) {
// 修改参数
this.limit = limit;
// 再次发送请求
this.getSpuList();
},
Spu管理内容的切换
因为此处有多个状态需要进行展示,为了方便出来可以拆分为子组件

然后在父组件中,通过v-show里面的scene值来决定是否进行展示该组件状态
自己封装button组件
有很多按钮的时候,我们可以封装一个hint-button全局组件
- 在main.js入口文件中,进行全局注册即可
import HintButton from '@/components/HintButton'
Vue.component(HintButton.name, HintButton)
然后在Spu组件中进行使用即可
想要鼠标放上去显示相应的内容,传入自己想要的title值即可
SpuForm静态组件完成
- SpuForm中由一个一个的el-form-item来进行组成,
- 在el-form-item中也可以自己选择el-input 、 el-option 等内容来供自己选择操作 非常关键
添加销售属性
保存
取消
spuForm请求业务的分析
- v-show只是控制spuForm子组件的显示与隐藏,子组件并没有卸载
- 我们在一个父组件中想要获取到子组件中的内容 比如说子组件中的数据和方法,可以使用ref来进行父子组件之间的通信
- 然后我们在父组件中点击修改按钮,就可以调用子组件中的方法,我们发4个请求是在子组件中发请求的
// 修改某一个Spu
updateSpu(row) {
this.scene = 1
// 在修改某一个子组件的时候,完全可以获取一个子组件的相关内容
// console.log(this.$refs.spu) // VueComponent
// 在父组件中可以获取到子组件,那么对于子组件的数据和方法都可以拿到
// 在父组件中可以通过$ref获取子组件等等
this.$refs.spu.initSpuData(row)
},
四大请求的书写
/*
3、获取Spu信息
/admin/product/getSpuById/{SpuId} get
*/
export const reqSpu = (spuId) => request({
url: `/admin/product/getSpuById/${spuId}`,
method: 'get'
})
/*
4、获取品牌的信息
/admin/product/baseTrademark/getTrademarkList
*/
export const reqTradeMarkList = () => request({
url: "/admin/product/baseTrademark/getTrademarkList",
method: 'get',
})
/*
5、获取Spu图片的请求
/admin/product/spuImageList/{spuId} get
*/
export const reqSpuImageList = (spuId) => request({
url: `/admin/product/spuImageList/${spuId}`,
method: 'get'
})
/*
6、获取平台全部的销售属性 整个平台销售属性一共就三个
/admin/product/baseSaleAttrList
*/
export const reqBaseSaleAttrList = () => request({
url:"/admin/product/baseSaleAttrList",
method:"get",
})
在子组件spuForm中获取数据
- 先书写相应的API接口方法
/*
3、获取Spu信息
/admin/product/getSpuById/{SpuId} get
*/
export const reqSpu = (spuId) => request({
url: `/admin/product/getSpuById/${spuId}`,
method: 'get'
})
/*
4、获取品牌的信息
/admin/product/baseTrademark/getTrademarkList
*/
export const reqTradeMarkList = () => request({
url: "/admin/product/baseTrademark/getTrademarkList",
method: 'get',
})
/*
5、获取Spu图片的请求
/admin/product/spuImageList/{spuId} get
*/
export const reqSpuImageList = (spuId) => request({
url: `/admin/product/spuImageList/${spuId}`,
method: 'get'
})
/*
6、获取平台全部的销售属性 整个平台销售属性一共就三个
/admin/product/baseSaleAttrList
*/
export const reqBaseSaleAttrList = () => request({
url:"/admin/product/baseSaleAttrList",
method:"get",
})
- 初始化数据,获取需要在子组件中展示的数据
// 初始化SpuForm数据
async initSpuData(spu) {
// console.log('发数据', spu)
// 获取Spu信息的数据
let spuResult = await this.$API.spu.reqSpu(spu.id);
// console.log(spuResult);
if(spuResult.code == 200) {
this.spu = spuResult.data
}
// 获取品牌的信息
let tradeMarkResult = await this.$API.spu.reqTradeMarkList()
// console.log(tradeMarkResult)
if(tradeMarkResult.code == 200) {
this.tradeMarkList = tradeMarkResult.data
}
// 获取spu图片的数据
let spuImageResult = await this.$API.spu.reqSpuImageList(spu.id)
// console.log(spuImageResult)
if(spuImageResult.code == 200) {
this.spuImageList = spuImageResult.data
}
// 获取平台全部的销售属性
let saleResult = await this.$API.spu.reqBaseSaleAttrList()
if(saleResult.code == 200) {
this.saleAttrList = saleResult.data
}
},
- 在data中需要配置相应的默认初始值进行接收
data() {
return {
dialogImageUrl: "",
dialogVisible: false,
spu: {}, // 存储SPU信息属性
tradeMarkList:[], // 存储品牌信息
spuImageList:[], // 存储Spu图片的数据
saleAttrList:[], // 销售属性的数据
};
},
SpuForm销售属性的数据展示
我们在展示数据的时候,还需要将数据同步收集
- v-model不仅可以展示数据,也可以用于收集数据,是双向的
- spuForm组件中用到了照片墙技术,使用到了el-upload 进行上传,需要注意的是照片墙技术是有样式的,在引入的时候需要将样式进行引入,
- 需要注意的一点是:很多elementui提供的组件在进行使用的时候,里面的属性都有自己特定的字段。比如里面规定是name、url字段
- 我们就需要将自己获取到的名字和url字段赋值到一个全新的数组,在进行相应的赋值操作
// 获取spu图片的数据
let spuImageResult = await this.$API.spu.reqSpuImageList(spu.id);
// console.log(spuImageResult)
if (spuImageResult.code == 200) {
// this.spuImageList = spuImageResult.data;
// 可以将数据进行处理,然后在返回给数组进行接收操作
let listArr = spuImageResult.data;
/*
由于照片墙显示图片的数据需要数组,
数组里面的元素需要有name和url字段
需要把服务器返回的数据进行修改再赋予新的值
*/
listArr.forEach((item) => {
item.name = item.imgName;
item.url = item.imgUrl;
});
// 把整理好的数据赋值给一开始设置好的初始数据
this.spuImageList = listArr;
}
- 尤其是有多个属性进行展示的,比如说下拉框等,我们有多个数据的时候,需要使用到v-for来进行遍历
- 比如说在子组件当中的,使用el-option,很多数据需要展示的时候,我们就需要用到v-for来进行遍历操作
el-tag数据进行展示

这一类用到el-tag组件,这一类内容都用到作用域插槽来进行
{{ tag.saleAttrValueName }}
+ New Tag
同时我们还需要很注意,我们在进行收集用户输入的数据的时候,需要在data中创建初始数据的形态
- 我们在spu对象内容的时候 在初始化的时候是一个空对象 在修改spu的时候 会向服务器发请求,返回SPU信息(对象)
- 在修改的时候可以利用服务器返回的这个对象收集最新的数据提交给服务器, 添加Spu 如果是添加Spu的时候,并没有向服务器发请求,数据收集到哪里呀【SPU】收集数据的时候有哪些属性字段,需要看文档
spu: {
//三级分类的id
category3Id: 0,
//描述
description: "",
//spu名称
spuName: "",
//平台的id
tmId: 0,
//收集SPU图片的信息
spuImageList: [],
//平台属性与属性值收集
spuSaleAttrList: [],
},
销售属性的计算

/*
整个平台的销售属性就三个
- 颜色、尺寸、版本 saleAttrList
- 当前Spu拥有的属于自己的销售属性 Spu.spuSaleAttrList -- 颜色
- 数组过滤的方法,可以从已有的数据当中过滤出用户需要的元素,并返回一个新的数据
- 对于filter而言,我们需要返回的是boolean值
*/
复习JavaScript中的语法
filter - 返回真表示 过滤出来的元素,我们是需要的
every- 数组的过滤方法::可以从已有的数组当中过滤出用户需要的元素,并返回一个新的数据
过滤出来的结果需要用一个全新的字段来进行接收
- 计算出来的结果供给我们使用 可以用到vue2中的计算属性
// 计算出还未选择的销售属性
unSelectSaleAttr() {
/*
整个平台的销售属性就三个
- 颜色、尺寸、版本 saleAttrList
- 当前Spu拥有的属于自己的销售属性 Spu.spuSaleAttrList -- 颜色
- 数组过滤的方法,可以从已有的数据当中过滤出用户需要的元素,并返回一个新的数据
- 对于filter而言,我们需要返回的是boolean值
*/
let result = this.saleAttrList.filter((item) => {
// filter 返回真表示 过滤出来的元素,我们是需要的
// every数组的过滤方法:可以从已有的数组当中过滤出用户需要的元素,并返回一个新的数据
// 过滤出来的结果就是一个数组
return this.spu.spuSaleAttrList.every(item1 => {
return item.name != item1.saleAttrName
})
})
// return '二哈'
// 对于一个计算属性肯定要有一个返回值
return result
}
完成spuForm照片墙图片的收集
照片墙何时收集数据
- 预览照片墙的时候,显示大的图片的时候,需要收集数据吗? —不需要收集的【数据已经有了】
- 照片墙在删除图片的时候,需要收集数据 || 照片墙在添加图片的时候,需要收集数据的。
照片墙进行上传的时候,需要用到我们特定的地址
action="/dev-api/admin/product/fileUpload"
'运行
file-list属性是用于存放照片墙需要展示的数据 照片墙需要展示的数据有name和url属性
:file-list="spuImageList"
同时对于照片墙我们有一个照片预览和移除的功能,我们需要写上相应的回调
:on-preview="handlePictureCardPreview"
:on-remove="handleRemove"
书写回调的时候,有三个方法,其中还有一个是照片墙上传成功的回调
- 照片墙删除的时候会触发
handleRemove(file, fileList) {
/*
file:代表删除的那张图片
fileList:照片墙删除某一张图片以后,剩余其他的图片
*/
// console.log(file, fileList);
/*
收集照片墙图片的数据
对于已有的图片【照片墙中显示的图片,有name、url字段】
因为照片墙显示数据务必要有这两个属性
而对服务器而言,不需要name和url字段
将来对于有的图片的数据在提交给服务器的时候,需要进行处理
*/
this.spuImageList = fileList;
},
- 照片墙图片预览的回调
handlePictureCardPreview(file) {
// 将图片地址赋值给这个属性
this.dialogImageUrl = file.url;
// 对话框显示
this.dialogVisible = true;
},
- 照片墙图片上传成功的回调(将收集文件的属性fileList赋值给我们一开始就定义好的属性)
handlerSuccess(response, file, fileList) {
this.spuImageList = fileList;
},
销售属性添加的操作
- 我们在收集销售属性的时候,不仅需要收集销售属性的名字,还有收集其所对应的id
:value="`${unselect.id}:${unselect.name}`"
- 收集到了在书写方法的时候,分割出来 split,而且数据进行存储的话,需要存储到spu中,添加完全以后还需要将数据进行清空
// 添加新的销售属性
addSaleAttr() {
// 已经收集到了需要添加的销售属性的信息
// 把收集到的销售属性的数据进行分割
const [baseSaleAttrId, saleAttrName] = this.attrIdAndAttrName.split(":");
// 向SPU对象的spuSaleAttrList属性里面添加新的销售属性
let newSaleAttr = { baseSaleAttrId, saleAttrName, spuSaleAttrList: [] };
// 添加新的销售属性
/*
收集数据可以收集到很多位置,但是收集到spu可以直接进行展示
*/
this.spu.spuSaleAttrList.push(newSaleAttr);
// 将数据进行清空操作
this.attrIdAndAttrName = "";
},
- 在el-input和button添加框中书写相应的回调,通过inputVisible来控制显示与隐藏,但是该数据不能放在data中,放在data中大家公用不能单独控制每一行里面的元素,
- 应该在点击添加按钮的时候,通过响应式数据来添加一个inputVisible属性
// 添加按钮的回调,其中row是当前点击的这一行销售属性
addSaleAttrValue(row) {
// 点击销售属性中的添加按钮,需要将button变为input,通过当前销售属性的inputVisible控制
// 挂载在销售属性身上的响应式数据inputVisible控制这button与input切换
this.$set(row, "inputVisible", true);
// 通过响应式数据inputValue字段收集新增的销售属性值
this.$set(row, "inputValue", "");
},
- 失去焦点的事件,在失去焦点的时候,我们应该将相应的数据收集起来,并到时候进行相应的展示操作
在进行添加或者过滤方法的时候,总是报错说push方法或者啥filter未undefined 一般都是前面的调用者不是一个数组,
我们可以这样书写
let arr = row.spuSaleAttrValueList || []
// 因为可能就一开始没有获取到相应的内容
let result = arr.every((item) => item.saleAttrValueName != inputValue)
if(!result) return
- 不过后续我们需要将arr赋值给我们项目定义的属性内容,要不然不能进行匹配添加
row.spuSaleAttrValueList = arr
- 失去焦点事件书写
// el-input 失去焦点的事件
handleInputConfirm(row) {
console.log(row)
/*
新增的销售属性值需要收集的字段
baseSaleAttrId
saleAttrValueName
应该往当前的销售属性的属性值添加新的属性才行
需要解构出销售属性当中收集数据
*/
const { baseSaleAttrId, inputValue } = row;
// console.log(baseSaleAttrId, inputValue)
if(inputValue.trim() == "") {
this.$message('属性值不能为空')
return
}
let arr = row.spuSaleAttrValueList || []
// 直接书写下面
// let result = row.spuSaleAttrValueList.every((item) => item.saleAttrValueName != inputValue)
let result = arr.every((item) => item.saleAttrValueName != inputValue)
if(!result) return
// 新增的销售属性值
let newSaleAttrValue = { baseSaleAttrId, saleAttrValueName: inputValue };
row.spuSaleAttrValueList = arr
arr.push(newSaleAttrValue);
// 因为上面添加的inputVisible为响应式的,修改其值为false就显示button了
row.inputVisible = false;
},
删除销售属性与属性值的操作
删除销售属性值的话,其实在删除的时候,是将一行整个属性进行删除 - 用到数组里面的splice,索引在这个时候就很有作用了,我们就是通过索引来进行删除操作,删除往往是获取到数据以后跟根据数组的索引从数组当中删除数据
@click="spu.spuSaleAttrList.splice($index, 1)"
完成修改Spu的保存操作
- 先书写保存操作的API数据操作方法,其中修改Spu和添加Spu的方法都是一样的,差别在于修改Spu的操作需要携带相关的Id,两个方法类似,都需要在放松请求的时候携带相应的spuInfo信息,我们可以根据是否携带有相应的id来区分是修改Spu还是添加Spu
/*
7、修改Spu || 添加Spu
对于修改或者添加,携带给服务器参数大致是一样的,
唯一的区别就是携带的参数是否携带id
*/
export const reqAddOrUpdateSpu = (spuInfo) => {
// 携带的参数带有id -- 修改spu
if (spuInfo.id) {
return request({
url: "/admin/product/updateSpuInfo",
method: 'post',
data: spuInfo
})
} else {
// 携带的参数不带有id --- 添加Spu
return request({
url: "/admin/product/saveSpuInfo",
method: 'post',
data: spuInfo
})
}
}
- 在外形框架中书写相应点击事件的名字,点击该按钮就会触发addOrUpdateSpu 保存或者修改的按钮,进行操作
<el-button type="primary" @click="addOrUpdateSpu">保存</el-button>
保存按钮相应的回调
- 整理参数,需要整理照片墙的数据,对于对于图片,需要携带imageName和imageUrl字段
- 之前为了展示将图片设置为 name和url字段,但是在数据当中需要设置为imageName和imageUrl字段才行
- 在通知父组件回到场景0的时候,可以通过flag字段来标记是修改还是进行添加操作
- 最后在进行清除数据的时候,有一个常用技巧
// 保存按钮的回调
async addOrUpdateSpu() {
/*
整理参数,需要整理照片墙的数据
携带参数,对于图片,需要携带imageName和imageUrl字段
map方法会返回一个全新的对象
*/
this.spu.spuImageList = this.spuImageList.map((item) => {
return {
imageName: item.name,
// 新图放在response当中,老图放在url当中
// 数组的map会返回一个新数组,我们可以返回给我们spuImageList字段
imageUrl: (item.response && item.response.data) || item.url,
};
});
// 发请求
// 在进行修改保存的时候,有可能会失败,可能是修改到了官方的数据
let result = await this.$API.spu.reqAddOrUpdateSpu(this.spu);
// console.log(result)
if (result.code == 200) {
// 提示
this.$message({
type: "success",
message: "保存成功",
});
// 也是一样的,通知父组件回到场景0那里即可
// this.$emit("changeScene", 0);
this.$emit("changeScene", {
scene: 0,
flag: this.spu.id ? "修改" : "添加",
});
}
// 清除数据
Object.assign(this._data, this.$options.data());
},
其中 this._data 可以操作data中的响应式数据,this.$options可以获取配置对象,配置对象的data函数执行,进行将响应式中的数据进行清零操作
取消按钮的操作
- 点击取消按钮,通知父亲来进行切换场景的操作,同样也需要进行清除数据的操作
cancel() {
// 取消按钮的回调,通知父亲切换场景
this.$emit("changeScene", { scene: 0, flag: "" });
/*
清理数据
Object.assign:es6中新增的方法 可以合并对象
组件实例 this._data 可以操作data当中的响应式数据
this.$options可以获取配置对象,配置对象的data函数执行,
返回的响应式数据为空的
*/
Object.assign(this._data, this.$options.data());
},
完成添加Spu的操作
- 需要注意的一点是,在子组件中是获取不到category3Id的, 但是在父组件通过refs与子组件通信的时候,可以将category3Id传递给子组件
- 父组件
// 添加Spu按钮的回调
addSpu() {
// 切换场景为1
this.scene = 1
// 通知子组件spuForm发请求 -- 两个
// 因为在子组件当中是不能有这几个数据的,在传入的时候,可以通过父组件传送过去,因为父组件这个时候是有的
this.$refs.spu.addSpuData(this.category3Id)
},
- 同样的,相关的调用接口的方法都可以放在子组件当中
- 点击添加Spu的操作需要调用两个方法:获取到品牌和销售属性
// 点击添加SPU按钮的时候,发请求的函数
async addSpuData(category3Id) {
// 添加Spu的时候收集三级分类的id 收集到spu的category3Id大当中
this.category3Id = category3Id;
// console.log("tianjia")
// 获取品牌的信息
let tradeMarkResult = await this.$API.spu.reqTradeMarkList();
// console.log(tradeMarkResult)
if (tradeMarkResult.code == 200) {
this.tradeMarkList = tradeMarkResult.data;
}
// 获取平台全部的销售属性
let saleResult = await this.$API.spu.reqBaseSaleAttrList();
if (saleResult.code == 200) {
this.saleAttrList = saleResult.data;
}
},
- 而后自己进行相关输入,点击保存按钮可以将数据进行保存
// 点击添加SPU按钮的时候,发请求的函数
async addSpuData(category3Id) {
// 添加Spu的时候收集三级分类的id 收集到spu的category3Id大当中
this.category3Id = category3Id;
// console.log("tianjia")
// 获取品牌的信息
let tradeMarkResult = await this.$API.spu.reqTradeMarkList();
// console.log(tradeMarkResult)
if (tradeMarkResult.code == 200) {
this.tradeMarkList = tradeMarkResult.data;
}
// 获取平台全部的销售属性
let saleResult = await this.$API.spu.reqBaseSaleAttrList();
if (saleResult.code == 200) {
this.saleAttrList = saleResult.data;
}
},
- 但是进行修改操作和添加操作成功以后,返回父组件展示的时候,定位到之前的页面不一样,这个时候就需要用到page属性和flag属性
// 自定义事件回调(Spu)修改场景值
changeScene({scene, flag}) {
// flag 是为了区分保存按钮是添加还是修改
// console.log(scene)
this.scene = scene
if(flag == "修改") {
this.getSpuList(this.page)
} else {
this.getSpuList()
}
// 修改了具体哪一页,在后面进行返回的时候,需要停留在当前页才行
// this.getSpuList(this.page)
}
- 在进行调试的时候,如果数据对不太上,可以使用google调试工具,看看数据是否收集到了,只有用户收集到了数据才能
删除Spu的操作
- 在进行删除的时候,为了美观引用了pop插件,其可以弹出一个提示框,用于提示
- 在进行删除的时候,需要将删除的对象,直接传入进去,比如这一块删除的对象就是row ,
- 其中组件pop中的确定删除事件是固定的,固定为onConfirm 事件
<el-popconfirm title="这是一段内容确定删除吗?" @onConfirm="deleteSpu(row)">
删除Spu的API的书写
- 既然要删除某个内容,最好的办法就是传入id
/*
8、删除Spu
/admin/product/deleteSpu/{spuId}
*/
export const reqDeleteSpu = (spuId) => request({
url: `/admin/product/deleteSpu/${spuId}`,
method: 'delete',
})
- 同样的在删除成功以后,需要重新调用数据进行展示,但是展示的页数还是有讲究的,当页数大于1的时候,展示当前页,否则展示前一页
// 删除spu按钮的回调
async deleteSpu(row) {
// 删除Spu肯定是要去发请求的
// alert(111)
let result = await this.$API.spu.reqDeleteSpu(row.id)
if(result.code == 200) {
this.$message({
type:'success',
message:"删除成功"
})
// 提示成功,删除成功以后还需要回调数据
this.getSpuList(this.records.length>1 ? this.page : this.page-1)
}
},
完成SKU静态组件
- Sku组件其实和Spu组件是很相似的,大致都是使用el-form-item组件标签来进行操作使用
- 在el-table当中,每一列进行展示的时候,使用el-table-column,其中列当中勾选按钮使用 type=“selection” 来进行表示
<div>
<el-form ref="form" label-width="80px">
<el-form-item label="SPU名称"> 海绵宝宝 </el-form-item>
<el-form-item label="SKU名称">
<el-input placeholder="SKU名称"></el-input>
</el-form-item>
<el-form-item label="价格(元)">
<el-input placeholder="价格(元素)"></el-input>
</el-form-item>
<el-form-item label="重量(千克)">
<el-input placeholder="重量(千克)"></el-input>
</el-form-item>
<el-form-item label="规格描述">
<el-input type="textarea" rows="4"></el-input>
</el-form-item>
<el-form-item label="平台属性">
<el-form :inline="true" ref="form" label-width="80px">
<el-form-item label="屏幕尺寸">
<el-select placeholder="请选择" value="">
<el-option label="label" value="value"></el-option>
</el-select>
</el-form-item>
</el-form>
</el-form-item>
<el-form-item label="销售属性">
<el-form :inline="true" ref="form" label-width="80px">
<el-form-item label="屏幕尺寸">
<el-select placeholder="请选择" value="">
<el-option label="label" value="value"></el-option>
</el-select>
</el-form-item>
</el-form>
</el-form-item>
<el-form-item label="图片列表">
<el-table style="width: 100%" border>
<el-table-column
prop="prop"
label="label"
width="width"
type="selection"
></el-table-column>
<el-table-column prop="prop" label="图片" width="width">
</el-table-column>
<el-table-column prop="prop" label="名称" width="width">
</el-table-column>
<el-table-column prop="prop" label="操作" width="width">
</el-table-column>
</el-table>
</el-form-item>
<el-form-item>
<el-button type="primary">保存</el-button>
<el-button>取消</el-button>
</el-form-item>
</el-form>
</div>
获取SkuForm数据进行展示
- 在获取SkuForm数据进行展示的时候,点击添加的那一刻需要发送3个请求操作,
- 书写三个发送请求的API方法,获取图片数据、获取销售属性数据、获取平台属性的数据
// 1、先引入我们二次封装的axios
import request from '@/utils/request'
/*
1、获取图片的数据
/admin/product/spuImageList/{spuId} get
*/
export const reqSpuImageList = (spuId) => request({
url: `/admin/product/spuImageList/${spuId}`,
method: 'get'
})
/*
2、获取销售属性的数据
/admin/product/spuSaleAttrList/{spuId} get
*/
export const reqSpuSaleAttrList = (spuId) => request({
url: `/admin/product/spuSaleAttrList/${spuId}`,
method: 'get'
})
/*
3、获取平台属性的数据
/admin/product/attrInfoList/{category1Id}/{category2Id}/{category3Id} get
*/
export const reqAttrInfoList = (category1Id, category2Id, category3Id) => request({
url: `/admin/product/attrInfoList/${category1Id}/${category2Id}/${category3Id}`,
method: 'get'
})
- 在父组件中书写点击添加sku按钮回调,父组件中可以收集到相关的分类的id,可以传送到子组件当中
// 添加Sku按钮的回调
addSku(row) {
// 切换场景为2
this.scene = 2
// 通过父组件调用子组件的方法,让子组件发请求 -- 三个请求
// 父组件身上有的参数,可以直接传送过去
this.$refs.sku.getData(this.category1Id, this.category2Id, row)
}
- 在SkuForm子组件当中书写相应的回调操作
- 没有使用到vuex所以相关的数据都需要在data当中进行初始化
data() {
return {
// 有需要获取到数据的时候,设置一个初始属性进行相应接收才行
// 存储图片的信息
spuImageList: [],
// 存储销售的属性
spuSaleAttrList: [],
// 存储平台属性的数据
reqAttrInfoList: [],
};
},
- 书写相应的获取数据的方法
methods: {
// 获取SkuForm数据
async getData(category1Id, category2Id, spu) {
// console.log("获取数据")
// 获取图片的数据
let result = await this.$API.sku.reqSpuImageList(spu.id);
// console.log(result);
if (result.code == 200) {
this.spuImageList = result.data;
}
// 获取销售属性的数据
let result1 = await this.$API.sku.reqSpuSaleAttrList(spu.id);
// console.log(result1)
if (result1.code == 200) {
this.spuSaleAttrList = result1.data;
}
// 获取平台属性的数据
let result2 = await this.$API.sku.reqAttrInfoList(
category1Id,
category2Id,
spu.category3Id
);
// console.log(result2);
if(result2.code == 200) {
this.reqAttrInfoList = result2.data
}
},
},
展示Sku数据与收集Sku数据
- 前面我们通过发送3个请求来获取Sku相关的数据,存储在下面当中
// 存储图片的信息
spuImageList: [],
// 存储销售的属性
spuSaleAttrList: [],
// 存储平台属性的数据
attrInfoList: [],
- 但是我们还需要将数据收集到skuInfo当中,用于向服务器发送请求的操作,用一个大的对象来进行表示
skuInfo: {
// 我们是添加sku,已有的才有id
// id: 0,
// isSale: 0,
// 第一类收集的数据:父组件给的数据,
category3Id: 0,
spuId: 0,
tmId: 0,
// 第二类:需要通过数据双向绑定v-model收集
skuName: "",
price: 0,
weight: "",
skuDesc: "",
// 第三类:需要自己书写代码
// 平台属性
skuAttrValueList: [
{
attrId: 0,
valueId: 0,
// attrName: "string",
// id: 0,
// skuId: 0,
// valueName: "string",
},
],
// 设置默认图片
skuDefaultImg: "",
// 收集图片的字段,默认为一个空数组
skuImageList: [
// {
// id: 0,
// imgName: "string",
// imgUrl: "string",
// isDefault: "string",
// skuId: 0,
// spuImgId: 0,
// },
],
// 销售属性
skuSaleAttrValueList: [
// {
// id: 0,
// saleAttrId: 0,
// saleAttrName: "string",
// saleAttrValueId: 0,
// saleAttrValueName: "string",
// skuId: 0,
// spuId: 0,
// },
],
},
- skuInfo中的字段,我们在获取SkuForm中的数据的时候,也可以先进行收集存储
// 收集父组件给予的数据
this.skuInfo.category3Id = spu.category3Id;
this.skuInfo.spuId = spu.id;
this.skuInfo.tmId = spu.tmId;
this.spu = spu;
- 尤其是对于图片的操作,我们需要对图片列表,加上相应的id,在最初的获取操作,加上我们想要的默认属性字段
- 图片显示区域有一个默认字段和显示两种状态的互显示,这种排他的数据,只能在数据内容中单个显示,不可在data中设置显示
// 将列表中的每个对象加上一个默认属性字段
list.forEach((item) => {
// 给每一个图片的信息,加上这个需要的字段即可
// 0代表显示设置默认,1代表默认
item.isDefault = 0;
});
// 这种往默认属性字段中添加了相应的属性,乃至属性值,其实就是对原有数组进行了改变
// 将数组赋值到原有数组进行存储
this.spuImageList = list;
图片列表显示的业务
- 选中第一列当中的复选框, 这是elementui提供的包装方法,选中的参数就是该张图片的信息
// table表格复选框按钮的事件
handleSelectionChange(params) {
/*
获取到用户选中图片的信息数据
但是需要注意,当前收集的数据当中,缺少isDefault字段
现在收集到的数据是不完整的,所以不能存储在skuImageList当中,
因为这一块将来是需要提交给服务器的
*/
this.imageList = params;
},
- 图片的显示设置默认和默认的操作, 即排他的操作, 即两种状态只能显示其中一种
- 结构样式:el-table-column中,通过作用域插槽,传入两个按钮,通过v-if和v-else来实现互斥操作,
- 通过 @click=“changeDefault(row)” 来改变default属性值
排他的操作
- 先将图片字段中的所有的isDefault字段变为0,然后将你点击的那个字段变为1,收集一下默认图片的地址
// 排他的操作
changeDefault(row) {
// 图片列表的isDefault字段变为0,只有你点击的那个变为1
this.spuImageList.forEach((item) => {
// 所有的isDefault字段变为0
item.isDefault = 0;
});
// 点击的那个图片的数据变为1
row.isDefault = 1;
// 收集一下默认图片的地址
this.skuInfo.skuDefaultImg = row.imgUrl;
},
其他的内容数据收集操作都是通过v-model来进行收集信息 - 实现数据的交互操作
完成添加Sku保存操作
先完成取消操作
- 点击取消之后,我们需要通知父组件改变scene的值,来进行切换场景操作
- 触发切换场景以后,还需要将数据进行清除操作
cancel() {
// 自定义事件,让父组件切换场景为0
this.$emit("changeScenes", 0);
// 清除数据
Object.assign(this._data, this.$options.data());
},
保存按钮的操作
- 1、整理平台属性
- 2、整理好数据以后,进行发请求的操作
- 3、发送成功以后,给予提示信息,并提示父组件进行切换场景的操作
整理平台属性的数据方式一(forEach)
// 整理平台属性的数据方式一
// 新建一个数组
let arr = []
// 把收集到的数据整理一下
attrInfoList.forEach(item => {
// 当前平台属性用户进行了选择
if(item.attrIdAndValueId) {
const [attrId, valueId] = item.attrIdAndValueId.split(":")
// 携带给服务器的参数,应该是一个对象
let obj = {attrId, valueId}
arr.push(obj)
}
})
// 将整理好的参数字段赋值给skuInfo.skuAttrValueList
skuInfo.skuAttrValueList = arr
整理平台属性的数据方式二(reduce)
/*
prev初始值为[] 初始值为一个空数组
item为遍历到的每一个元素
常用于:求数组累加和、最大值、当前情况
*/
// 返回的是最后一次执行的结果
skuInfo.skuAttrValueList = attrInfoList.reduce((prev, item) => {
// 用户已经选择了
if (item.attrIdAndValueId) {
const [attrId, valueId] = item.attrIdAndValueId.split(":");
prev.push({ attrId, valueId });
}
// 最后一次返回执行的结果 reduce需要将下一次的结果返回,当做下一次的
return prev;
}, []);
整理销售属性
// 整理销售属性 spuSaleAttrList
skuInfo.skuSaleAttrValueList = spuSaleAttrList.reduce((prev, item) => {
if (item.attrIdAndValueId) {
const [saleAttrId, saleAttrValueId] =
item.attrIdAndValueId.split(":");
prev.push({ saleAttrId, saleAttrValueId });
}
return prev;
}, []);
整理图片的数据 - 利用已有的图片数据来映射出一个新的数据出来
// 整理图片的数据 利用已有的图片数据来映射出一个新的数据出来
// map是映射出一个新的数组,赋值给它即可
skuInfo.skuImageList = imageList.map((item) => {
return {
imgName: item.imgName,
imgUrl: item.imgUrl,
isDefault: item.isDefault,
spuImgId: item.id,
};
});
整理好的数据都放在了skuInfo当中 通过发送请求,成功以后弹出提示信息,然后提示父组件进行场景调换即可
// 整理好了数据以后,发送请求即可
let result4 = await this.$API.sku.reqAddSku(skuInfo)
// console.log(result4)
if(result4.code == 200) {
this.$message({
type:"success",
message:"添加Sku成功"
})
this.$emit('changeScenes', 0)
}
对于map的测试
- 若arr的形式是这种
let arr = [
// 对象里面肯定是放键值对冒号的形式出来
{ imgName: 1 },
{ imgUrl: 2 },
{ isDefault: 3 },
{ id: 4 }
]
- 则使用map对里面arr中的每个对象进行赋值操作得到的结果是
let arr = [
// 对象里面肯定是放键值对冒号的形式出来
{ imgName: 1 },
{ imgUrl: 2 },
{ isDefault: 3 },
{ id: 4 }
]
let skuInfo = arr.map(item => {
return {
imgName: item.imgName,
imgUrl: item.imgUrl,
isDefault: item.isDefault,
spuImgId: item.id,
}
})
console.log(skuInfo)
// 结果
[
{
imgName: 1,
imgUrl: undefined,
isDefault: undefined,
spuImgId: undefined
},
{
imgName: undefined,
imgUrl: 2,
isDefault: undefined,
spuImgId: undefined
},
{
imgName: undefined,
imgUrl: undefined,
isDefault: 3,
spuImgId: undefined
},
{
imgName: undefined,
imgUrl: undefined,
isDefault: undefined,
spuImgId: 4
}
]
let arr = [
// 对象里面肯定是放键值对冒号的形式出来
{
imgName: 1,
imgUrl: 2,
isDefault: 3,
id: 4
}
]
// 对数组里面的每个对象进行操作
let skuInfo = arr.map(item => {
return {
imgName: item.imgName,
imgUrl: item.imgUrl,
isDefault: item.isDefault,
spuImgId: item.id,
}
})
console.log(skuInfo)
// 结果 非常关键
[ { imgName: 1, imgUrl: 2, isDefault: 3, spuImgId: 4 } ]
对于reduce的测试
- reduce接收两个参数:① 执行归并操作的函数,任务:将两个值归并或组合为一个值并返回这个值 ② 可选的,是传给归并函数的初始值
let a = [1, 2, 3, 4, 5]
console.log(a.reduce((x, y) => x + y, 0)) // 15
console.log(a.reduce((x, y) => x * y, 1)) // 120
console.log(a.reduce((x, y) => (x > y) ? x : y)) // 5
let a = [1, 2, 3, 4, 5]
b = a.reduce((x, y) => {
// 完整的写法,工作中用的多
x = x + y
return x // 每次要将x前一个参数进行返回,作为下一次运算的参数
}, 0)
console.log(b) // 15
Sku列表的展示
- 想要查看具体内容的话,像本文是用到了el-dialog插件来进行的
- 像这种样式,随便放在哪一个位置,因为它有专门的属性来控制对话框的显示与隐藏
- 里面也是用el-table来进行展示操作的
<!-- 像这种点击一下就进行显示的操作,放在随便哪一个位置都可以,弄成动态的才行 -->
<el-dialog
:title="`${spu.spuName}的sku列表`"
:visible.sync="dialogTableVisible"
:before-close="close"
>
<el-table :data="skuList" style="width: 100%" border v-loading="loading">
<!-- prop当中展示相应的字段操作 -->
<el-table-column prop="skuName" label="名称" width="width">
</el-table-column>
<el-table-column prop="price" label="价格" width="width">
</el-table-column>
<el-table-column prop="weight" label="重量" width="width">
</el-table-column>
<el-table-column label="默认图片" width="width">
<template slot-scope="{ row, $index }">
<img
:src="row.skuDefaultImg"
alt=""
style="width: 100px; height: 100px"
/>
</template>
</el-table-column>
</el-table>
</el-dialog>
- 上方点击按钮的操作的回调
@click="handler(row)"
- 书写相应的回调操作
async handler(spu) {
// 点击按钮的时候,对话框应该是可见的
this.dialogTableVisible = true;
// 保存spu的信息,因为我们想要在别的地方进行展示,保存到data当中
this.spu = spu;
// 刚才写了相应的参数,我们需要获取sku列表的数据进行展示
let result = await this.$API.sku.reqSkuList(spu.id);
// console.log(result);
if (result.code == 200) {
this.skuList = result.data;
// 将loading进行隐藏
this.loading = false
}
},
- 在对话框中,:before-close=“close” 常在这类函数中书写相关的逻辑,比如加载等
// 关闭对话框的回调
close(done) {
// loading属性再次变为真
this.loading = true
// 清除sku列表的数据,每次展示前将数据进行清除操作
this.skuList = []
// 关闭对话框
done()
}
Sku模块数据展示与分页功能
- 同样的套路,没有了vuex,要进行数据展示,必须在data中一开始就对数据进行声明,到时候获取到了在进行存储
- 对于分页器有几个常用的属性,① 当前第几页 ② 当前页面显示的数据的条数 ③ 总共需要展示多少条数据
- 对于别的数据的展示,我们也需要弄出来, 直接弄一个初始状态即可
data() {
return {
// 代表当前第几页
page: 1,
// 代表当前页面有几条数据
limit: 10,
// 存储Sku列表的数据
records: [],
// 存储分页器一共展示的数据
total: 0,
// 存储sku的信息的
skuInfo: {},
show: false,
};
},
- 因为发请求需要在多个地方请求,所以发送数据的回调方法一般不放在mounted当中,而是放在methods中,到时候进行调用的时候比较方便
获取sku列表数据的接口
/*
6、sku列表的接口
/admin/product/list/{page}/{limit}
*/
export const reqSkuList2 = (page, limit) => request({
url: `/admin/product/list/${page}/${limit}`,
method: 'get'
})
获取sku列表数据的方法
async getSkuList(pages = 1) {
this.page = pages;
// 需要用到哪些参数,我们后续需要解构出来
const { page, limit } = this;
let result = await this.$API.sku.reqSkuList2(page, limit);
// console.log(result)
if (result.code == 200) {
this.total = result.data.total;
this.records = result.data.records;
}
},
在结构进行展示的时候 尤其是在el-table中展示的时候,直接将要展示的数据,放入到:data中,
而el-table-column中的数据进行展示的时候,直接使用:data 的字段即可
<!-- 表格 -->
<el-table style="width: 100%" border :data="records">
<el-table-column type="index" label="序号" width="80" align="center">
</el-table-column>
<!-- 里面在进行展示的时候,直接进行展示即可,没有多余的操作 -->
<el-table-column prop="skuName" label="名称" width="width">
</el-table-column>
<el-table-column prop="skuDesc" label="描述" width="width">
</el-table-column>
<el-table-column prop="prop" label="默认图片" width="110">
<template slot-scope="{ row, $index }">
<img
:src="row.skuDefaultImg"
alt=""
style="width: 80px; height: 80px"
/>
</template>
</el-table-column>
<el-table-column prop="weight" label="重量" width="80"> </el-table-column>
<el-table-column prop="price" label="价格" width="80"> </el-table-column>
<el-table-column prop="prop" label="操作" width="width">
<template slot-scope="{ row, $index }">
<el-button
type="success"
icon="el-icon-sort-down"
size="mini"
v-if="row.isSale == 0"
@click="sale(row)"
></el-button>
<el-button
type="success"
icon="el-icon-sort-up"
size="mini"
v-else
@click="cancel(row)"
></el-button>
<el-button
type="primary"
icon="el-icon-edit"
size="mini"
@click="edit"
></el-button>
<el-button
type="info"
icon="el-icon-info"
size="mini"
@click="getSkuInfo(row)"
></el-button>
<el-button
type="danger"
icon="el-icon-delete"
size="mini"
></el-button>
</template>
</el-table-column>
</el-table>
分页的操作
<el-pagination
style="text-align: center"
:current-page="page"
:page-sizes="[3, 5, 10]"
:page-size="limit"
layout="prev, pager, next, jumper, ->, sizes, total"
:total="total"
@current-change="getSkuList"
@size-change="handleSizeChange"
>
两个回调方法
@current-change="getSkuList"
// 可以复用上述的getSkuList的方法,可以都是获取pages页进行数据展示,不过之前的固定是page = 1
// 获取sku列表数据的方法
async getSkuList(pages = 1) {
this.page = pages;
// 需要用到哪些参数,我们后续需要解构出来
const { page, limit } = this;
let result = await this.$API.sku.reqSkuList2(page, limit);
// console.log(result)
if (result.code == 200) {
this.total = result.data.total;
this.records = result.data.records;
}
},
@size-change="handleSizeChange"
// 修改携带的参数然后再进行展示
handleSizeChange(limit) {
// 修改参数,然后发送请求即可
this.limit = limit;
this.getSkuList();
},
sku上架与下架的操作
上架与下架操作 这两个按钮为互斥操作,即这两个按钮也只能展示其中一个
- 只能展示其中一个使用v-if v-else 来进行操作,需要控制某一个属性来控制
- 注意某一行数据当中是有isSale属性,来控制sku产品的上架与下架操作
<el-button
type="success"
icon="el-icon-sort-down"
size="mini"
v-if="row.isSale == 0"
@click="sale(row)"
></el-button>
<el-button
type="success"
icon="el-icon-sort-up"
size="mini"
v-else
@click="cancel(row)"
></el-button>
- 书写上架与下架的API
/*
7、商品的上架操作
/admin/product/onSale/{skuId}
*/
export const reqSale = (skuId) => request({
url: `/admin/product/onSale/${skuId}`,
method: 'get',
})
/*
8、商品的下架操作
/admin/product/cancelSale/{skuId}
*/
export const reqCancel = (skuId) => request({
url: `/admin/product/cancelSale/${skuId}`,
method: 'get',
})
书写上架操作和下架操作的方法的回调
- 上下架操作其实就是让后台进行展示,我们只需要切换页面,然后弹出成功的提示信息即可
// 上架的业务
async sale(row) {
let result = await this.$API.sku.reqSale(row.id);
if (result.code == 200) {
// 上架以后,将isSale字段进行相应的修改操作
row.isSale = 1;
this.$message({
type: "success",
message: "上架成功",
});
}
},
// 下架的业务
async cancel(row) {
let result = await this.$API.sku.reqCancel(row.id);
if (result.code == 200) {
row.isSale = 0;
this.$message({
type: "success",
message: "下架成功",
});
}
},
// edit
edit() {
this.$message("正在开发中");
},
sku详情查看完成
获取详情数据的api书写
/*
9、获取sku详情的接口
/admin/product/getSkuById/{skuId} get
*/
export const reqSkuById = (skuId) => request({
url: `/admin/product/getSkuById/${skuId}`,
method: 'get'
})
// 获取sku详情的方法
async getSkuInfo(sku) {
// 展示抽屉
this.show = true;
// 获取Sku的数据
let result = await this.$API.sku.reqSkuById(sku.id);
if (result.code == 200) {
this.skuInfo = result.data;
}
},
- 将数据存储在skuInfo属性字段中,这一点很关键
- 通过抽屉插件来进行sku数据的展示
- 在进行数据的样式的布局,我们还可以通过el-row || el-col来进行布局
- 轮播图使用el-carousel标签来使用
<el-drawer
title="我是标题"
:visible.sync="show"
:before-close="handleClose"
:show-close="false"
size="50%"
>
<el-row>
<el-col :span="5">名称</el-col>
<el-col :span="16">{{ skuInfo.skuName }}</el-col>
</el-row>
<el-row>
<el-col :span="5">描述</el-col>
<el-col :span="16">{{ skuInfo.skuDesc }}</el-col>
</el-row>
<el-row>
<el-col :span="5">价格</el-col>
<el-col :span="16">{{ skuInfo.price }}元</el-col>
</el-row>
<el-row>
<el-col :span="5">平台属性</el-col>
<el-col :span="16">
<template>
<el-tag
type="success"
v-for="(attr, index) in skuInfo.skuAttrValueList"
:key="attr.id"
style="margin-right: 10px"
>
// 展示的话,还是使用{{}}进行展示操作
{{ attr.attrId }}-{{ attr.valueId }}
</el-tag>
</template>
</el-col>
</el-row>
<el-row>
<el-col :span="5">商品图片</el-col>
<el-col :span="16">
<el-carousel height="150px">
// data中定义的数据是可以直接获取展示的
<el-carousel-item
v-for="item in skuInfo.skuImageList"
:key="item.id"
>
<img :src="item.imgUrl" alt="" />
</el-carousel-item>
</el-carousel>
</el-col>
</el-row>
</el-drawer>
深度选择器
- 为了让父组件中style中的样式书写了scoped属性以后还能影响到子组件我们可以使用深度选择器来实现
>>> 一般用于原生CSS
/deep/ 一般用于less
::v-deep 一般用户scss
- 一般的我们在父组件中书写了样式,并在style中添加了scoped属性,我们只能在子组件中的div那一层拥有和父组件一样的样式
数据可视化
数据可视化简介
- 就是服务器返回的数据,是以视图的形式进行展示【饼图、折线图,K线图】
echarts:vue、react
v-chart:vue
d3.js:vue、react
hightchart:vue、react
echarts:基本使用
canvas绘制线段
canvas画布
- 是HTML5中新增的一个特性,双闭合标签
- 该标签默认具有宽度与高度 300 * 150
- 浏览器认为canvas标签是一张图片,可以另存为
- 给canvas画布添加文本内容没有任何意义
- 给canvas添加子节点也是没有任何意义
- 你想操作canvas画布:画布当中绘制图形,显示一个文字,都必须通过JS完成
- canvas标签的w|h务必通过canvas标签属性width||height设置
- 切记不能通过样式去设置画布的宽度与高度
注意点:
/*
- canvas标签任何操作务必通过JS完成
- 通过“JS” 当中的“笔”去完成
- 该图形是由像素点组成的像素群
*/
'运行
使用canvas绘制线段
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>canvas的基本使用</title>
<style>
* {
/* 书写的时候,每次结束用分号隔开 */
margin: 0;
padding: 0;
}
canvas {
border: 1px solid black;
}
</style>
</head>
<body>
<!--
canvas画布
- 是HTML5中新增的一个特性,双闭合标签
- 该标签默认具有宽度与高度 300 * 150
- 浏览器认为canvas标签是一张图片,可以另存为
- 给canvas画布添加文本内容没有任何意义
- 给canvas添加子节点也是没有任何意义
- 你想操作canvas画布:画布当中绘制图形,显示一个文字,都必须通过JS完成
- canvas标签的w|h务必通过canvas标签属性width||height设置
- 切记不能通过样式去设置画布的宽度与高度
-->
<canvas width="600" height="400"></canvas>
</body>
<script>
/*
- canvas标签任何操作务必通过JS完成
- 通过“JS” 当中的“笔”去完成
- 该图形是由像素点组成的像素群
*/
let canvas = document.querySelector('canvas')
// 获取画布的笔【上下文】
let ctx = canvas.getContext('2d')
// console.log(ctx)
// 绘制线段:绘制线段的起点的设置
ctx.moveTo(100, 100)
// 其他点的设置(可以有多个)
ctx.lineTo(100, 200)
ctx.lineTo(200, 100)
// 设置图形填充的颜色
ctx.fillStyle = "red"
ctx.fill()
// 设置图型的线段的颜色与宽度
ctx.strokeStyle = "purple"
ctx.lineWidth = "20"
// 可以设置起点与最终的结束点连接在一起
ctx.closePath()
// 调用stroke方法去绘制线段
ctx.stroke()
</script>
使用canvas绘制矩形
- 绘制矩形使用:strokeRect() 其有四个参数,分别代表左上角距离x的宽度和y的宽度,以及所要绘制矩形的宽高
Document
使用canvas绘制圆形
- 绘制圆形使用的方式是arc() 方法,其有六个参数,分别代表: x, y, r, 起始的弧度, 结束的弧度, 是否逆时针绘制(x, y 是圆心距离水平轴和y轴)
Document
画布清除与绘制文字
**清除画布使用clearRect()**其有四个参数
**绘制文字使用fillText()**其有三个参数
Document
绘制柱状图
所要实现效果图

实现代码
Document
- 其是通过一条条线来进行绘制完成的,然后在响应的位置加上字,最后绘制矩形,填充颜色即可
svg的基本使用
- svg双闭合标签
- 默认宽度与高度为300 * 150
- svg绘制图形务必在svg标签内部使用绘制图形
- svg标签内部有很多的属性,可以通过这些内部标签属性来绘制图形
Document
echarts的基本使用
- 首先echarts的使用要先准备一个容器
- 然后在获取配置项与数据,根据配置项和数据来进行绘制表格的操作
Document
echarts展示多个表格
既然要展示多个表格,就需要准备多个容器,然后创建多个实例 - 然后根据配置项和数据来创建相应的表格
Document
echarts数据集dataset的使用
- 即可以不用在series中声明各种类型图的时候把对应的数据传入进去,而是在外面用配置项data进行声明,而在图表的配置项与数据当中使用数据源进行引入,series中用encode进行声明
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<!-- 引入echarts依赖包 -->
<script src="https://cdn.bootcdn.net/ajax/libs/echarts/5.3.3/echarts.common.js"></script>
<style>
* {
margin: 0px;
padding: 0px;
}
div {
width: 800px;
height: 400px;
}
</style>
</head>
<body>
<!-- 准备一个容器:容器就是显示图标的区域 -->
<div></div>
</body>
<script>
// 基于准备好的dom初始化一个echart实例
let dom = document.querySelector('div')
// 创建echarts实例
let mycharts = echarts.init(dom)
let data = [
["衣服", 10, 22, 'x', 10],
["直播", 12, 55, 'y', 60],
["游戏", 16, 44, 'z', 50],
["电影", 19, 32, 't', 70],
]
// 准备指定图表的配置项与数据
mycharts.setOption({
// 设置字符集
dataset: {
// 数据源
source: data,
},
// 图标的标题
title: {
// 主标题的设置
text: "数据可视化",
// 子标题
subtext: "echarts的基本使用",
// 主标题的颜色
textStyle: {
color: "cyan"
},
// 设置标题位置
left: "center"
},
// x轴的配置项
xAxis: {
// 数据
data: ["衣服", "直播", "游戏", "电影"]
},
// y轴的配置项
yAxis: {
// 显示Y轴的线条
axisLine: {
show: true,
},
// 显示Y轴的刻度
axisTick: {
show: true,
}
},
// 系列的设置
series: [
// 柱状图
{
// 图表类型的设置
type: "bar",
// 图表的数据
// data: [10, 20, 30, 40],
// 颜色
color: "red",
encode: {
y: 1,
}
},
// 折线图
{
// 图表类型的设置
type: "line",
// 图表的数据
// data: [10, 20, 30, 40],
color: "pink",
encode: {
y: 2,
}
},
// 饼图
{
type: "pie",
// 想要展示文字和数字可以data里面配置对象
// 饼图你可以显示文字,data写法如下
/* data: [
{ name: 'x', value: 10 },
{ name: 'y', value: 20 },
{ name: 'z', value: 30 },
{ name: 't', value: 40 },
], */
// 饼图的宽度与高度
width: 250,
height: 250,
// 饼图的位置
left: 150,
top: 100,
// 饼图的半径
radius: 25,
encode: {
// 饼图旁边的文字,
itemName: 3,
value: 4,
}
}
]
})
</script>
echarts内置组件的使用
- echarts内部有很多的组件,比如提示组件、内置切换组件等,需要使用的话,可以进行引入
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://cdn.bootcdn.net/ajax/libs/echarts/5.3.3/echarts.common.js"></script>
<style>
* {
margin: 0px;
padding: 0px;
}
.box {
width: 100%;
height: 400px;
/* border: 1px solid black; */
}
</style>
</head>
<body>
<!-- 准备容器 -->
<div class="box"></div>
</body>
<script>
// 初始化echarts实例
// 获取容器
let dom = document.querySelector('.box')
let myCharts = echarts.init(dom)
// 配置数据
myCharts.setOption({
dataZoom: {},
// 标题
title: {
text: "echarts组件",
},
xAxis: {
data: ['游戏', '电影', '直播', '娱乐'],
},
yAxis: {},
series: [
{
name: "柱状图",
type: "bar",
data: [10, 20, 30, 40]
},
{
name: "折线图",
type: "line",
data: [30, 40, 50, 60]
}
],
// 提示组件
tooltip: {
// 提示框文字的颜色
textStyle: {
color: 'red',
}
},
// 系列切换组件
legend: {
data: ['柱状图', '折线图']
},
toolbox: {
show: true,
feature: {
dataZoom: {
yAxisIndex: 'none'
},
dataView: { readOnly: false },
magicType: { type: ['line', 'bar'] },
restore: {},
saveAsImage: {}
}
},
// 调整图表的布局
grid: {
left: 30,
right: 0,
}
})
</script>
echarts坐标体系
## echarts坐标体系之一个坐标体系
例子:散点图
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<!-- 引入echarts依赖包 -->
<script src="https://cdn.bootcdn.net/ajax/libs/echarts/5.3.3/echarts.common.js"></script>
<style>
* {
margin: 0px;
padding: 0px;
}
div {
width: 100%;
height: 400px;
}
</style>
</head>
<body>
<!-- 准备一个容器:容器就是显示图标的区域 -->
<div></div>
</body>
<script>
// 基于准备好的dom初始化一个echart实例
let dom = document.querySelector('div')
// 创建echarts实例
let mycharts = echarts.init(dom)
// 准备指定图表的配置项与数据
mycharts.setOption({
// 标题
title: {
text: "一个坐标系",
},
// X轴和Y轴 的使用
xAxis: {
type: "category"
},
yAxis: {},
// 散点图
series: [
{
type: "scatter",
// 散点图的数据 是一个二维数组
data: [
[10, 20],
[13, 66],
[50, 9],
[44, 22],
[15, 10]
]
}
]
})
</script>
echarts坐标体系之多个坐标体系
- series中在每个对象中设置自己的属性即可(yAxisIndex)
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<!-- 引入echarts依赖包 -->
<script src="https://cdn.bootcdn.net/ajax/libs/echarts/5.3.3/echarts.common.js"></script>
<style>
* {
margin: 0px;
padding: 0px;
}
div {
width: 100%;
height: 400px;
}
</style>
</head>
<body>
<!-- 准备一个容器:容器就是显示图标的区域 -->
<div></div>
</body>
<script>
// 基于准备好的dom初始化一个echart实例
let dom = document.querySelector('div')
// 创建echarts实例
let mycharts = echarts.init(dom)
// 准备指定图表的配置项与数据
mycharts.setOption({
// 标题
title: {
text: "双坐标",
},
// X轴和Y轴 的使用
xAxis: {
data: ['游戏', '直播', '经济', '娱乐'],
},
yAxis: [
{
// 显示Y轴的线条
axisLine: {
show: true,
},
// 显示Y轴的刻度
axisTick: {
show: true,
},
},
{
// 显示Y轴的线条
axisLine: {
show: true,
},
// 显示Y轴的刻度
axisTick: {
show: true,
},
}
],
// 散点图
series: [
{
type: "line",
data: [10, 20, 30, 40],
yAxisIndex: 0
},
{
type: "bar",
data: [6, 10, 80, 20],
yAxisIndex: 1
}
]
})
</script>
Home首页Card静态组件
- 页面布局当中有一个静态布局,就是el-row和el-col 其中el-row占的总数是24,要进行均分的话,el-col占6份即可
- 然后在每一个el-col中放一个el-card即可,因为想要阴影效果,所以使用card
- 为了更好操纵,我们在card组件下面又定义了4个子组件

- 在子组件中书写相应的样式即可,
- 其中子组件在进行书写样式的时候,把相应的结构,弄出来,父组件给子组件传递数据的时候使用props进行传递即可
- 对于每一个子组件中,有的部分需用到插槽的内容,在子组件中占位置,父组件中使用template书写结构
- 比如Detail子组件
<template>
<div>
<div class="card-header">
<span>{{ title }}</span>
<svg
t="1663466055240"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="2421"
width="20"
height="20"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64z m0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z"
p-id="2422"
></path>
<path
d="M512 336m-48 0a48 48 0 1 0 96 0 48 48 0 1 0-96 0Z"
p-id="2423"
></path>
<path
d="M536 448h-48c-4.4 0-8 3.6-8 8v272c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V456c0-4.4-3.6-8-8-8z"
p-id="2424"
></path>
</svg>
</div>
<div class="card-content">{{ count }}</div>
<div class="card-charts">
<slot name="charts"> </slot>
</div>
<div class="card-footer">
<slot name="footer"> </slot>
</div>
</div>
</template>
- 父组件传递相应的数据,并使用template定义相应的结构
<el-col :span="6">
<!-- 想要有阴影的效果,可以外面放上el-card -->
<el-card>
<!-- 父组件进行传递,子组件进行接收的时候,不需要动态进行传递 -->
<Detail title="总销售额" count="¥126560">
<template slot="charts">
<span>周同比 56.67%</span
><svg
t="1663467150019"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="3501"
width="16"
height="16"
>
<path
d="M786.295467 485.000533l-6.126934 2.082134v-18.295467h6.024534l0.1024 16.213333zM221.5424 485.239467h-8.9088l-0.085333-16.247467 8.994133-0.512v16.759467z"
fill="#996E28"
p-id="3502"
></path>
<path
d="M784.469333 480.290133L504.456533 178.978133a6.826667 6.826667 0 0 0-10.001066 0L214.459733 480.290133a6.826667 6.826667 0 0 0 5.922134 11.4176l158.1056-21.435733L358.4 853.333333v16.162134a6.826667 6.826667 0 0 0 6.826667 7.168h268.356266a6.826667 6.826667 0 0 0 6.826667-7.168l-0.119467-16.3328-19.848533-382.8736 158.122667 21.418666a6.826667 6.826667 0 0 0 5.922133-11.4176z"
fill="#996E28"
p-id="3503"
></path>
<path
d="M191.3344 734.6176l103.031467 72.789333 103.048533-72.789333a127.197867 127.197867 0 0 0-206.097067 0z"
fill="#D5382E"
p-id="3504"
></path>
<path
d="M294.382933 681.984a127.010133 127.010133 0 0 1 103.048534 52.6336l-103.048534 72.789333-103.031466-72.789333a127.010133 127.010133 0 0 1 103.048533-52.6336m0-10.24a137.915733 137.915733 0 0 0-111.342933 56.849067 10.24 10.24 0 0 0 2.269866 14.2848l0.136534 0.1024L288.426667 815.786667a10.24 10.24 0 0 0 11.810133 0l103.031467-72.772267a10.24 10.24 0 0 0 2.4064-14.3872 137.915733 137.915733 0 0 0-111.342934-56.849067z"
fill="#E8D4AB"
p-id="3505"
></path>
<path
d="M384.1536 444.142933h1.5872l-20.48 409.6h268.2368l-20.48-409.6"
fill="#D5382E"
p-id="3506"
></path>
<path
d="M633.4976 860.5696H365.226667a6.826667 6.826667 0 0 1-6.826667-7.168l20.48-409.6 5.239467 0.256v-6.826667h1.5872a6.826667 6.826667 0 0 1 6.826666 7.168l-20.1216 402.432H626.346667l-20.1216-402.432 13.653333-0.682666 20.48 409.6a6.826667 6.826667 0 0 1-6.826667 7.168z"
fill="#E8D4AB"
p-id="3507"
></path>
<path
d="M613.0176 446.276267l166.365867 22.545066L499.387733 167.509333 219.374933 468.821333l164.778667-22.545066"
fill="#D5382E"
p-id="3508"
></path>
<path
d="M633.4976 860.5696H365.226667a6.826667 6.826667 0 0 1-6.826667-7.168l19.968-399.240533-158.122667 21.435733a6.826667 6.826667 0 0 1-5.922133-11.4176L494.370133 162.8672a6.826667 6.826667 0 0 1 10.001067 0L784.384 464.213333a6.826667 6.826667 0 0 1-5.922133 11.4176l-158.122667-21.418666L640.3072 853.333333a6.826667 6.826667 0 0 1-6.826667 7.168z m-261.12-13.653333H626.346667L606.3104 446.634667a6.826667 6.826667 0 0 1 7.7312-7.099734l147.3536 19.950934L499.370667 177.493333 237.3632 459.485867l147.3536-19.968a6.826667 6.826667 0 0 1 7.7312 7.099733z"
fill="#E8D4AB"
p-id="3509"
></path>
<path
d="M557.3632 617.5744l149.5552 105.659733 149.572267-105.659733a184.644267 184.644267 0 0 0-299.1616 0z"
fill="#D5382E"
p-id="3510"
></path>
<path
d="M706.935467 541.184a184.32 184.32 0 0 1 149.572266 76.3904l-149.572266 105.659733-149.5552-105.659733a184.32 184.32 0 0 1 149.572266-76.3904m0-10.24a194.56 194.56 0 0 0-157.866666 80.622933 10.24 10.24 0 0 0 2.184533 14.2336l0.221867 0.1536 149.5552 105.659734a10.24 10.24 0 0 0 11.810133 0l149.5552-105.642667a10.24 10.24 0 0 0 2.4064-14.3872 194.56 194.56 0 0 0-157.866667-80.622933z"
fill="#E8D4AB"
p-id="3511"
></path>
</svg>
<span>日同比 19.96%</span
><svg
t="1663467232683"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="4715"
width="16"
height="16"
>
<path
d="M252.484267 566.749867h11.776v16.6912h-11.776zM815.684267 566.8352h10.461866v16.9984h-10.461866z"
fill="#996E28"
p-id="4716"
></path>
<path
d="M825.361067 580.642133a6.826667 6.826667 0 0 0-6.980267-3.618133l-158.0032 21.418667L680.2432 201.386667a6.826667 6.826667 0 0 0 0-1.058134v-1.109333a6.826667 6.826667 0 0 0-6.826667-7.168H405.1968a6.826667 6.826667 0 0 0-6.826667 7.168v1.109333a6.826667 6.826667 0 0 0 0 1.058134l19.848534 397.038933-158.0032-21.4016a6.826667 6.826667 0 0 0-5.922134 11.4176l280.0128 301.277867a6.826667 6.826667 0 0 0 10.001067 0L824.32 588.424533a6.826667 6.826667 0 0 0 1.041067-7.7824z"
fill="#996E28"
p-id="4717"
></path>
<path
d="M339.780267 523.7248H103.6288a17.066667 17.066667 0 1 1 0-34.133333h236.151467a13.380267 13.380267 0 1 0 0-26.760534h-33.467734a44.782933 44.782933 0 1 1 0.631467-89.565866h238.250667a17.066667 17.066667 0 0 1 0 34.133333h-238.250667a11.093333 11.093333 0 0 0-11.229867 9.5744 10.717867 10.717867 0 0 0 10.5984 11.7248h33.467734a47.5136 47.5136 0 1 1 0 95.0272z"
fill="#D5382E"
p-id="4718"
></path>
<path
d="M545.194667 373.282133a17.066667 17.066667 0 0 1 0 34.133334h-238.250667a11.093333 11.093333 0 0 0-11.229867 9.5744 10.717867 10.717867 0 0 0 10.5984 11.7248h33.467734a47.5136 47.5136 0 1 1 0 95.0272H103.6288a17.066667 17.066667 0 1 1 0-34.133334h236.151467a13.380267 13.380267 0 1 0 0-26.760533h-33.467734a44.782933 44.782933 0 1 1 0.631467-89.565867h238.250667m0-10.24h-238.250667a55.022933 55.022933 0 1 0-0.631467 110.045867h33.467734a3.140267 3.140267 0 1 1 0 6.280533H103.6288a27.306667 27.306667 0 1 0 0 54.613334h236.151467a57.7536 57.7536 0 1 0 0-115.5072h-33.467734a0.512 0.512 0 0 1-0.290133-0.170667 0.3584 0.3584 0 0 1-0.119467-0.221867 1.416533 1.416533 0 0 1 1.041067-0.426666h238.250667a27.306667 27.306667 0 0 0 0-54.613334z"
fill="#E8D4AB"
p-id="4719"
></path>
<path
d="M654.523733 591.854933h-1.5872l20.48-409.6H405.1968l20.48 409.6"
fill="#AF3131"
p-id="4720"
></path>
<path
d="M654.523733 593.5616h-1.5872l20.48-409.6H405.1968l20.48 409.6"
fill="#D5382E"
p-id="4721"
></path>
<path
d="M654.523733 600.388267h-1.5872a6.826667 6.826667 0 0 1-6.826666-7.168l20.1216-402.432H412.3648l20.1216 402.432-13.653333 0.682666-20.48-409.6a6.826667 6.826667 0 0 1 6.826666-7.168h268.253867a6.826667 6.826667 0 0 1 6.826667 7.168l-20.48 409.6-5.239467-0.256z"
fill="#E8D4AB"
p-id="4722"
></path>
<path
d="M425.6768 589.704533l-166.365867-22.528L539.306667 868.471467l279.995733-301.294934-164.778667 22.528"
fill="#D5382E"
p-id="4723"
></path>
<path
d="M425.6768 591.4112l-166.365867-22.528L539.306667 870.178133l279.995733-301.294933-164.778667 22.528"
fill="#D5382E"
p-id="4724"
></path>
<path
d="M539.306667 874.837333a6.826667 6.826667 0 0 1-5.000534-2.184533L254.293333 571.357867a6.826667 6.826667 0 0 1 5.922134-11.4176l158.122666 21.418666-19.968-399.223466a6.826667 6.826667 0 0 1 6.826667-7.168h268.2368a6.826667 6.826667 0 0 1 6.826667 7.168l-19.968 399.240533 158.122666-21.435733a6.826667 6.826667 0 0 1 5.922134 11.4176L544.3072 872.6528a6.826667 6.826667 0 0 1-5.000533 2.184533zM277.2992 576.034133L539.306667 857.975467l262.007466-281.941334-147.3536 19.968a6.826667 6.826667 0 0 1-7.7312-7.099733l20.0192-400.2816H412.3648l20.0192 400.264533a6.826667 6.826667 0 0 1-7.7312 7.099734z"
fill="#E8D4AB"
p-id="4725"
></path>
<path
d="M780.7488 226.338133h105.540267a17.066667 17.066667 0 0 1 0 34.133334h-105.540267a13.380267 13.380267 0 1 0 0 26.760533h8.0384a44.782933 44.782933 0 1 1 0.631467 89.565867H503.7056a17.066667 17.066667 0 0 1 0-34.133334h285.730133a10.717867 10.717867 0 0 0 10.5984-11.7248 11.076267 11.076267 0 0 0-11.229866-9.5744h-8.0384a47.5136 47.5136 0 1 1 0-95.0272z"
fill="#D5382E"
p-id="4726"
></path>
<path
d="M886.289067 226.338133a17.066667 17.066667 0 0 1 0 34.133334h-105.540267a13.380267 13.380267 0 1 0 0 26.760533h8.0384a44.782933 44.782933 0 1 1 0.631467 89.565867H503.7056a17.066667 17.066667 0 0 1 0-34.133334h285.730133a10.717867 10.717867 0 0 0 10.5984-11.7248 11.076267 11.076267 0 0 0-11.229866-9.5744h-8.0384a47.5136 47.5136 0 1 1 0-95.0272h105.540266m0-10.24h-105.557333a57.7536 57.7536 0 1 0 0 115.5072h8.0384a1.416533 1.416533 0 0 1 1.041067 0.426667 0.341333 0.341333 0 0 1-0.119467 0.221867 0.802133 0.802133 0 0 1-0.221867 0.170666H503.7056a27.306667 27.306667 0 0 0 0 54.613334h285.730133a55.022933 55.022933 0 1 0-0.631466-110.045867h-8.0384a3.140267 3.140267 0 1 1 0-6.280533h105.540266a27.306667 27.306667 0 0 0 0-54.613334z"
fill="#E8D4AB"
p-id="4727"
></path>
</svg>
</template>
<template slot="footer">
<span>日销售额¥12423</span>
</template>
</Detail>
</el-card>
</el-col>
折线图的绘制
- 同样是定义出来一个子组件然后在父组件中相应的位置放置即可
柱状图和进度条完成