饿了么:模板介绍
简洁版: https://github.com/PanJiaChen/vue-admin-template
加强版: https://github.com/PanJiaChen/vue-element-admin
好笔记:https://blog.csdn.net/weixin_44337386/article/details/124228661?spm=1001.2014.3001.5502
模板的文件与文件夹认知【简洁版】
build
----index.js webpack配置文件【很少修改这个文件】
mock
----mock数据的文件夹【模拟一些假的数据mockjs实现的】,因为咱们实际开发的时候,利用的是真是接口
node_modules
------项目依赖的模块
public
------ico图标,静态页面,publick文件夹里面经常放置一些静态资源,而且在项目打包的时候webpack不会编译这个文件夹,原封不动的打包到dist文件夹里面
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
静态页面
书写API
axios二次封装
换成真实接口之后需要解决代理跨域问题(解决代理跨域问题)
后台管理系统API接口在线文档:
http://39.98.123.211:8170/swagger-ui.html
http://39.98.123.211:8216/swagger-ui.html
从哪里退出,登录就从哪里进入
async logout() {
await this.$store.dispatch('user/logout')
this.$router.push(`/login?redirect=${this.$route.fullPath}`)
}
/* 引入最外层骨架的一级路由组件*/
import Layout from '@/layout'
{
path: '/',
component: Layout,
redirect: '/dashboard',
children: [{
path: 'dashboard',
name: 'Dashboard',
component: () => import('@/views/dashboard/index'),
meta: { title: '首页', icon: 'dashboard' }
}]
},
{
path: '/product',
component: Layout,
meta: { title: '商品管理', icon: 'dashboard' },
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管理' }
},
]
},
module.exports = {
lintOnSave: false,//关闭语法检查
}
全局颜色配置
新建css文件夹
文件夹中的内容
/* 改变主题色变量 */
$--color-primary: teal;
/* 改变 icon 字体路径变量,必需 */
$--font-path: '~element-ui/lib/theme-chalk/fonts';
@import "~element-ui/packages/theme-chalk/src/index";
在mian.js中引入
最后在文件mian.js入口引入上面的文件。
文件格式
import * as tradeMark from './product/tradeMark'
import * as attr from './product/attr'
import * as sku from './product/sku'
import * as spu from './product/spu'
export default{
tradeMark,
attr,
sku,
spu
}
//获取品牌列表的数据
async getPageList(pager = 1) {
this.page = pager;
//解构出参数
const { page, limit } = this;
//获取品牌列表的接口
//当你向服务器发请求的时候,这个函数需要带参数,因此老师在data当中初始化两个字段,代表给服务器传递参数
let result = await this.$API.trademark.reqTradeMarkList(page, limit);
if (result.code == 200) {
//分别是展示数据总条数与列表展示的数据
this.total = result.data.total;
this.list = result.data.records;
}
},
<el-pagination
style="margin-top: 20px; text-align: center"
:current-page="page"
:total="total"
:page-size="limit"
:pager-count="7"
:page-sizes="[3, 5, 10]"
@current-change="getPageList"
@size-change="handleSizeChange"
layout="prev, pager, next, jumper,->,sizes,total"
>
el-pagination>
如图
在浏览器地址栏中添加 平台的地址+接口
封装asiox文件:res.code!=200
// 响应头
response => {
const res = response.data
// if the custom code is not 20000, it is judged as an error.
if (res.code !== 20000 && res.code!=200) {
Message({
message: res.message || 'Error',
type: 'error',
duration: 5 * 1000
})
// 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired;
if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
// to re-login
MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', {
confirmButtonText: 'Re-Login',
cancelButtonText: 'Cancel',
type: 'warning'
}).then(() => {
store.dispatch('user/resetToken').then(() => {
location.reload()
})
})
}
return Promise.reject(new Error(res.message || 'Error'))
} else {
return res
}
},
以下情况是在你本地跑项目的时候的地址。
let x = location.origin
console.log(x) //'http://localhost:8080'
报错接口
1
新增修改
用一个函数只需要判断一下是否有id
存在
2 对于新增的品牌,给服务器传递数据,不需要传递ID,ID是由服务器生成
3 对于修改某一个品牌的操作,前端携带的参数需要带上id,你需要告诉服务器修改的是哪一个品牌
//处理添加品牌
//新增品牌: /admin/product/baseTrademark/save post 携带两个参数:品牌名称、品牌logo
//切记:对于新增的品牌,给服务器传递数据,不需要传递ID,ID是由服务器生成
//修改品牌的
//修改品牌 /admin/product/baseTrademark/update put 携带三个参数:id、品牌名称、品牌logo
//切记:对于修改某一个品牌的操作,前端携带的参数需要带上id,你需要告诉服务器修改的是哪一个品牌
export const reqAddOrUpdateTradeMark = (tradeMark) => {
//带给服务器数据携带ID---修改
if (tradeMark.id) {
return request({ url: '/admin/product/baseTrademark/update', method: 'put', data: tradeMark });
} else {
//新增品牌
return request({ url: '/admin/product/baseTrademark/save', method: 'post', data: tradeMark });
}
}
点击确定:
新增
可以是返回第一页
,修改
该留在当前页中
。
//添加按钮(添加品牌|修改品牌)
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({
type: "success",
message: this.tmForm.id ? "修改品牌成功" : "添加品牌成功",
});
//添加或者修改品牌成功以后,需要再次获取品牌列表进行展示
//如果添加品牌: 停留在第一页,修改品牌应该留在当前页面
this.getPageList(this.tmForm.id ? this.page : 1);
}
} else {
console.log("error submit!!");
return false;
}
});
},
删除
:需要留在上一条数据的页数
this.getList(this.list.length > 1this.page: this.page-1)
deleteTradeMark(row){
this.$confirm(`此操作将永久删除${row.tmName}?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async() => {
let res = await reqDeleteTradeMark(row.id)
if(res.code == 200){
this.$message({
type: 'success',
message: '删除成功!'
});
this.getList(this.list.length > 1? this.page: this.page-1)
}
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
})
})
},
点击取消的情况下验证但不重置表单
close(formName){
this.dialogFormVisible = false;this.$refs[formName].clearValidate()
},
要修改的代码
1 var array = [
2 {
3 id:1,
4 name:"小明"
5 },
6 {
7 id:2,
8 name:"小红"
9 }
10 ];
修改后代码
var array = [
{
value:1,
label:"小明"
},
{
value:2,
label:"小红"
}
];
修改步骤
// 原生js
var array = [
{
id:1,
name:"小明"
},
{
id:2,
name:"小红"
}
];
//旧key到新key的映射
var keyMap = {
"id" : "value",
"name" : "label"
};
for(var i = 0;i < array.length;i++){
var obj = array[i];
for(var key in obj){
var newKey = keyMap[key];
if(newKey){
obj[newKey] = obj[key];
delete obj[key];
}
}
}
console.log(array);
// ES6 写法
var result = array.map(o =>{
return{
value:o.id,
label:o.name
}
});
console.log(result);
<el-button type="text" size="small" @click="downloadFile">点击下载导入模板el-button>
<el-upload
action=""
class="upload-demo"
ref="upload"
:headers="headers"
drag
:accept="acceptType"
:http-request="readExcel"
name="file"
:limit="1"
>
<i class="el-icon-upload">i>
<div class="el-upload__text">
将文件拖到此处,或<em>点击上传em>
div>
<div class="el-upload__tip" slot="tip">
温馨提示:只能上传xls/xlsx文件
div>
el-upload>
<el-button @click="onSubmit" >确定el-button>
引入XLSX
import XLSX from "xlsx";
tabJson的数据样式
转换数据:将上传的表格数据转换成json数据
readExcel(params) {
// 获取上传的文件对象
const file = params.file;
// 通过FileReader对象读取文件
this.file2Xce(file).then((tabJson) => {
let xlsxJson = tabJson;
this.fileList = [...xlsxJson[0].sheet];
// 将表格数据转化成后端需要的格式
var result = this.fileList.map((key) => {
return {
name: key.姓名,
phone: key.电话,
address : key.地址,
sex : key.性别,
birthday : key.出生日期,
grade : key.年级,
school : key.学校,
classes : key.班级,
remark : key.备注,
starLevel : key.星级,
};
});
this.guestBat = result
});
},
// 提取表格数据
file2Xce(file) {
return new Promise(function (resolve, reject) {
// 通过FileReader对象读取文件
const reader = new FileReader();
reader.onload = function (e) {
const data = e.target.result;
// 以二进制流方式读取得到整份excel表格对象
this.wb = XLSX.read(data, {
type: "binary",
});
const result = [];
// 遍历每张工作表进行读取(这里默认只读取第一张表)
this.wb.SheetNames.forEach((sheetName) => {
result.push({
sheetName: sheetName,
sheet: XLSX.utils.sheet_to_json(this.wb.Sheets[sheetName]),
// 利用 sheet_to_json 方法将 excel 转成 json 数据
});
});
resolve(result);
};
reader.readAsBinaryString(file);
});
},
点击确定获取表格的字段
onSubmit() {
this.$refs["form"].validate(async (valid) => {
if (valid) {
if (this.guestBat.length === 0) {
$msg("请先选择导入的文件", 2);
return false;
}
if (!this.uploadnot) {
let object = {
customerList: this.guestBat,
};
const res = await addCustomerBat(object);
if (res.state === "success") {
window.$msg("批量导入成功");
}
} else {
$msg("上传文件中有必填的数据项为空,请检查后再上传", 1);
}
}
});
},
下载模板
downloadFile() {
let link = document.createElement("a"); // 创建a标签
link.style.display = "none";
link.href =
"https://coduckfile.oss-cn-hangzhou.aliyuncs.com/kdy/批量导入客户_1.xlsx"; // 设置下载地址
link.setAttribute("download", ""); // 添加downLoad属性
document.body.appendChild(link);
link.click();
},
=====================分割线,下面是点击按钮上传表格数据
点击按钮上传文件不用组件上传
readExcel (e) {
const files = e.target.files
if (files.length < 1) {
return false
} else if (!/\.(xls|xlsx)$/.test(files[0].name.toLowerCase())) {
$msg('上传文件格式不正确,请上传xls或者xlsx格式')
return false
}
this.arrfile = []
const fileReader = new FileReader()
fileReader.onload = (ev) => {
try {
const data = ev.target.result
const workbook = XLSX.read(data, {
type: 'binary',
}) // 读取数据
const wsname = workbook.SheetNames[0] // 取第一张表
const sheet2JSONOpts = {
/** Default value for null/undefined values */
defval: ''//给defval赋值为空的字符串
}
const ws = XLSX.utils.sheet_to_json(workbook.Sheets[wsname],sheet2JSONOpts) // 生成json表格内容
ws.map((v,index) => {
let obj = {}
obj.name = v.姓名
obj.phone = v.电话
obj.address = v.地址
obj.sex = v.性别
obj.birthday = v.出生日期
obj.grade = v.年级
obj.school = v.学校
obj.classes = v.班级
obj.remark = v.备注
obj.starLevel = v.星级
if(obj.tel === "") {
this.emptyntel = true
}
if(obj.name === "" || obj.phone === "") {
$msg("第"+index+"行有必填的数据项为空,请检查后再上传",1)
this.uploadnot = true
return false
}
this.arrfile.push(obj)
})
console.log(JSON.stringify(this.arrfile))
if(!this.uploadnot) {
this.$refs.upload.value = null
$msg("文件添加成功")
}
} catch (e) {
console.log(e)
return false
}
}
fileReader.readAsBinaryString(files[0])
},
初始值展位现象
data(){
return{
attrInfo: {
attrName: "", //属性名
attrValueList: [
//属性值,因为属性值可以有多个因此用数组,每一个属性值都是一个对象需要attrId,valueName
],
categoryId: 0, //三级分类的id
categoryLevel: 3, //因为服务器也需要区分几级id
},
}
}
点击取消按钮清空数据
addAttr() {
// 1 打开属性值页面
this.isShowTable = false
// 将属性页能够读取到的值传给属性值页面
this.attrInfo = {
attrName: "", //属性名
attrValueList: [
],
categoryId: this.category3Id, //三级分类的id
categoryLevel: 3, //因为服务器也需要区分几级id
}
},
赋值 浅复制 深复制
直接赋值 浅复制 深复制
//按需引入lodash当中的深拷贝
import cloneDeep from "lodash/cloneDeep";
this.attrInfo = row // 直接赋值
this.attrInfo = {...row} //浅复制
//由于数据结构当中存在对象里面套数组,数组里面有套对象,因此需要使用深拷贝解决这类问题
this.attrInfo = cloneDeep(row)
1、直接赋值更改数据点击取消会将所有的值更改
2、浅复制更改数据点击取消会将所有的值更改
解决;由于数据结构当中存在对象里面套数组,数组里面有套对象,因此需要使用深拷贝解决这类问题
<el-form-item >
<span slot="label">
<el-tooltip
content="图片"
placement="top"
>
<i class="el-icon-question">i>
el-tooltip>
图片
span>
<el-row v-for="(item,index) in topicList" :key="index">
<el-col :span="4">
<el-upload
class="avatar-uploader"
accept='image/*'
:action="url"
:show-file-list="false"
:on-success="(res,file)=>handleAvatarSuccess(res, file,index)"
:before-upload="beforeAvatarUpload"
:headers="headers"
>
<img v-if="item.icon" :src="origin1 + item.icon" class="avatar" />
<i v-else class="el-icon-plus avatar-uploader-icon">i>
el-upload>
el-col>
<!-- :span="9">
<el-select
:key="index"
filterable
clearable
v-model="item.layerId"
placeholder="请选择"
style="width: 85%"
>
<el-option
v-for="item1 in sensorData"
:key="item1.value"
:label="item1.label"
:value="item1.layerId"
>
el-option>
el-select>
el-col>
<el-col :span="9">
<el-select
:key="index"
v-model="item.vertical"
filterable
clearable
placeholder="请输入"
style="width: 85%"
>
<el-option
v-for="dict in dict.eqp_layer_vertical"
:key="dict.value"
:label="dict.label"
:value="dict.value"
>
el-option>
el-select>
el-col>
<el-col :span="2">
<i class="el-icon-plus el-icon-circle-plus-outline" @click="pushClick(item)" v-if="item.icon && item.layerId && item.vertical"> i>
<i class="el-icon-plus el-icon-remove-outline" @click="removeClick(index)" v-if="topicList.length > 1">i>
el-col>
el-row>
data(){
return{
topicList: [
{icon:'',layerId:'',vertical:''},
],
}
}
添加、删除
pushClick(item) {
this.topicList.push(this.initItem? JSON.parse(JSON.stringify(this.initItem)) :{})
},
removeClick(index) {
this.topicList.splice(index,1)
},
验证不为空
if(!this.topicList[this.topicList.length-1].icon){
this.$message.info('图片不能为空,不容许添加')
return ;
}
if(!this.topicList[this.topicList.length-1].layerId){
this.$message.info('层位不能为空,不容许添加')
return ;
}
if(!this.topicList[this.topicList.length-1].vertical){
this.$message.info('位置不能为空,不容许添加')
return;
}
添加、修改属性(包括lodash当中的深拷贝)
//按需引入lodash当中的深拷贝
import cloneDeep from "lodash/cloneDeep";
//添加属性值回调
addAttrValue() {
//向属性值的数组里面添加元素
//attrId:是你相应的属性的id,目前而言我们是添加属性的操作,还没有相应的属性的id,目前而言带给服务器的id为undefined
//valueName:相应的属性值的名称
this.attrInfo.attrValueList.push({
attrId: this.attrInfo.id, //对于修改某一个属性的时候,可以在已有的属性值基础之上新增新的属性值(新增属性值的时候,需要把已有的属性的id带上)
valueName: "",
flag:true
//flag属性:给每一个属性值添加一个标记flag,用户切换查看模式与编辑模式,好处,每一个属性值可以控制自己的模式切换
//当前flag属性,响应式数据(数据变化视图跟着变化
})
this.$nextTick(()=>{ // 新增获取焦点
this.$refs[this.attrInfo.attrValueList.length-1].focus()
})
},
//修改某一个属性
updateAttr(row) {
//isShowTable变为false
this.isShowTable = false;
//将选中的属性赋值给attrInfo
//由于数据结构当中存在对象里面套数组,数组里面有套对象,因此需要使用深拷贝解决这类问题
//深拷贝,浅拷贝在面试的时候出现频率很高,切记达到手写深拷贝与浅拷贝
this.attrInfo = cloneDeep(row);
//在修改某一个属性的时候,将相应的属性值元素添加上flag这个标记
this.attrInfo.attrValueList.forEach((item) => {
//这样书写也可以给属性值添加flag自动,但是会发现视图不会跟着变化(因为flag不是响应式数据)
//因为Vue无法探测普通的新增 property,这样书写的属性并非响应式属性(数据变化视图跟这边)
//第一个参数:对象 第二个参数:添加新的响应式属性 第三参数:新的属性的属性值
this.$set(item, "flag", false);
});
},
属性值修改
<el-button
type="primary"
icon="el-icon-plus"
@click="addAttrValue"
:disabled="!attrInfo.attrName"
>添加属性值el-button
>
<el-button @click="isShowTable = true">取消el-button>
<el-table
style="width: 100%; margin: 20px 0px"
border
:data="attrInfo.attrValueList"
>
<el-table-column align="center" type="index" label="序号" width="80">
el-table-column>
<el-table-column width="width" prop="prop" label="属性值名称">
<template slot-scope="{ row, $index }">
<el-input
v-model="row.valueName"
placeholder="请输入属性值名称"
size="mini"
v-if="row.flag"
@blur="toLook(row)"
@keyup.native.enter="toLook(row)"
:ref="$index"
>el-input>
<span
v-else
@click="toEdit(row, $index)"
style="display: block"
>{{ row.valueName }}span
>
template>
el-table-column>
<el-table-column width="width" prop="prop" label="操作">
<template slot-scope="{ row, $index }">
<el-popconfirm :title="`确定删除${row.valueName}?`" @onConfirm="deleteAttrValue($index)">
<el-button
type="danger"
icon="el-icon-delete"
size="mini"
slot="reference"
>el-button>
el-popconfirm>
template>
el-table-column>
el-table>
//点击span的回调,变为编辑模式
toEdit(row, index) {
row.flag = true;
this.$nextTick(() => {
//获取相应的input表单元素实现聚焦
this.$refs[index].focus();
});
},
//失却焦点的事件---切换为查看模式,展示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;
}
});
if(isRepat) return;
// row:形参是当前用户添加的最新的属性值
// 当前编辑模式变为查看模式【让input消失,显示span】
row.flag = false;
},
保存 1.delete item.flag;2.属性值不能为空
//气泡确认框确定按钮的回调
//最新版本的ElementUI----2.15.6,目前模板中的版本号2.13.x
deleteAttrValue(index){
//当前删除属性值的操作是不需要发请求的
this.attrInfo.attrValueList.splice(index,1);
},
//保存按钮:进行添加属性或者修改属性的操作
async addOrUpdateAttr(){
//整理参数:1,如果用户添加很多属性值,且属性值为空的不应该提交给服务器
//提交给服务器数据当中不应该出现flag字段
this.attrInfo.attrValueList = this.attrInfo.attrValueList.filter(item=>{
//过滤掉属性值不是空的
if(item.valueName!=''){
//删除掉flag属性
delete item.flag;
return true;
}
})
//发请求
let res = await this.$API.attr.reqAddOrUpdateAttr(this.attrInfo);
if(res.code== 200){
//展示平台属性的信号量进行切换
this.isShowTable = true;
//提示消失
this.$message({type:'success',message:'保存成功'});
//保存成功以后需要再次获取平台属性进行展示
this.getAttrList();
}
}
import request from '@/utils/request';
// 获取SPU列表数据的接口
// /admin/product/{page}/{limit} get page limit category3Id
export const reqSpuList = (page,limit,category3Id)=>request({
url:`/admin/product/${page}/${limit}`,
method:'get',
params:{category3Id}
})
选择一级,其他两级置空
getCategoryId({ categoryId, level }) {
if (level == 1) {
this.category1Id = categoryId;
this.category2Id = "";
this.category3Id = "";
} else if (level == 2) {
this.category2Id = categoryId;
this.category3Id = "";
} else {
this.category3Id = categoryId;
this.getSpuList();
}
},
async getSpuList(pages = 1) {
this.page = pages;
const { page, limit, category3Id } = this;
let res = await this.$API.spu3.reqSpuList(page, limit, category3Id);
if (res.code == 200) {
this.total = res.data.total;
this.records = res.data.records;
}
},
封装三级联动组件
<template>
<div>
<!-- inline:代表的是行内表单,代表一行可以放置多个表单元素 -->
<el-form :inline="true" class="demo-form-inline" :model="cForm">
<el-form-item label="一级分类">
<el-select
placeholder="请选择"
v-model="cForm.category1Id"
@change="handler1"
:disabled="show"
>
<el-option
:label="c1.name"
:value="c1.id"
v-for="(c1, index) in list1"
:key="c1.id"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="二级分类">
<el-select
placeholder="请选择"
v-model="cForm.category2Id"
@change="handler2"
:disabled="show"
>
<el-option
:label="c2.name"
:value="c2.id"
v-for="(c2, index) in list2"
:key="c2.id"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="三级分类">
<el-select
placeholder="请选择"
v-model="cForm.category3Id"
@change="handler3"
:disabled="show"
>
<el-option
:label="c3.name"
:value="c3.id"
v-for="(c3, index) in list3"
:key="c3.id"
></el-option>
</el-select>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
name: "CategorySelect",
props: ["show"],
data() {
return {
//一级分类的数据
list1: [],
//二级分类的数据
list2: [],
//三级分类的数据
list3: [],
//收集相应的一级二级三级分类的id
cForm: {
category1Id: "",
category2Id: "",
category3Id: "",
},
};
},
//组件挂载完毕:向服务器发请求,获取相应的一级分类的数据
mounted() {
//获取一级分类的数据的方法
this.getCategory1List();
},
methods: {
//获取一级分类数据的方法
async getCategory1List() {
//获取一级分类的请求:不需要携带参数
let result = await this.$API.attr.reqCategory1List();
if (result.code == 200) {
this.list1 = result.data;
}
},
//一级分类的select事件回调(当一级分类的option发生变化的时候获取相应二级分类的数据)
async handler1() {
//清除数据
this.list2 = [];
this.list3 = [];
this.cForm.category2Id = "";
this.cForm.category3Id = "";
//解构出一级分类的id
const { category1Id } = this.cForm;
this.$emit("getCategoryId", { categoryId: category1Id, level: 1 });
//通过一级分类的id,获取二级分类的数据
let result = await this.$API.attr.reqCategory2List(category1Id);
if (result.code == 200) {
this.list2 = result.data;
}
},
//二级分类的select事件回调(当二级分类的option发生变化的时候获取相应三级分类的数据)
async handler2() {
//清除数据
this.list3 = [];
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() {
//获取三级分类的id
const { category3Id } = this.cForm;
this.$emit("getCategoryId", { categoryId: category3Id, level: 3 });
},
},
};
</script>
<style scoped>
</style>
来源:https://juejin.cn/post/7009114318750875685
按钮组件(element -ui自带提示功能,这里为了演示跨组件通信)
必须使用v-bind,不能够简写为:
必须使用v-on,不能简写为@
props接收title属性时,$attrs就接收不到title属性,只能接收剩下的属性值
$listeners监听事件
<template>
<a :title="title" style="margin:10px">
<el-button v-bind="$attrs" v-on="$listeners">el-button>
a>
template>
<script>
export default {
name: "HintButton",
props: ["title"]
};
script>
使用
<hint-button
type="success"
icon="el-icon-plus"
size="mini"
title="添加sku"
@click="addSku"
></hint-button>
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
this.st = this.formatDate(start, "yyyy-MM-dd");
this.et = this.formatDate(end, "yyyy-MM-dd");
formatDate(date, fmt) {
if (/(y+)/.test(fmt)) {
fmt = fmt.replace(
RegExp.$1,
(date.getFullYear() + "").substr(4 - RegExp.$1.length)
);
}
let o = {
"M+": date.getMonth() + 1,
"d+": date.getDate(),
"h+": date.getHours(),
"m+": date.getMinutes(),
"s+": date.getSeconds(),
};
for (let k in o) {
if (new RegExp(`(${k})`).test(fmt)) {
let str = o[k] + "";
fmt = fmt.replace(
RegExp.$1,
RegExp.$1.length === 1 ? str : this.padLeftZero(str)
);
}
}
return fmt;
},
padLeftZero(str) {
return ("00" + str).substr(str.length);
},
两种方法
使用: child child2 组件一样
注意使用update:money
money为变量
npm install vue-clipboard2 --save
mian.js引入注册
import VueClipboard from 'vue-clipboard2'
Vue.use(VueClipboard)
示例使用
<el-table-column width="300" property="authCode" :show-overflow-tooltip="true" label="鉴权码">el-table-column>
<el-table-column label="操作" width="150">
<template slot-scope="scope">
<el-button v-clipboard:copy="scope.row.authCode" v-clipboard:success="onCopy" v-clipboard:error="onError" type="text" size="small">复制el-button>
<el-button @click="copy(scope.row,scope.$index)" type="text" size="small">复制一行el-button>
template>
el-table-column>
onCopy(e) {
if(e.text){
this.$message({
message: `复制${e.text}成功`,
type: 'success'
});
}
},
onError(e) {
this.$message({
message: '复制失败',
type: 'error'
});
},
// 单个复制一行(会出现一模一样的一行数据,tabledata:表格数据,)
copy(val, index) {
this.tabledata.splice(index, 0,JSON.parse(JSON.stringify(val)))
},
action
:图片上传的地址
list-type
: 文件列表的类型
on-preview
:图片预览的时候会触发
on-remove
:当删除图片的时候会触发
file-list
:照片墙需要展示的数据【数组:数组里面的元素务必要有name
(如果没有name可以用uid
代替)、url
属性。】
on-preview
:图片预览功能
on-remove
:删除图片的时候会触发
<el-upload
action="/dev-api/admin/product/fileUpload"
list-type="picture-card"
:on-preview="handlePictureCardPreview"
:on-remove="handleRemove"
:on-success="handlerSuccess"
:file-list="spuImageList">
<i class="el-icon-plus">i>
el-upload>
<el-dialog :visible.sync="dialogVisible">
<img width="100%" :src="dialogImageUrl" alt="">
el-dialog>
<script>
export default {
data() {
return {
dialogImageUrl: '',
dialogVisible: false,
spuImageList:[]
};
},
methods: {}
}
script>
// 删除
handleRemove(file, fileList) {
// file参数:代表的是删除的那个张图片
// fileList:照片墙删除某一张图片以后,剩余的其他的图片
// 收集照片墙图片的数据
this.spuImageList = fileList;
// `注意` 对于已有的图片【照片钱中显示的图片:有name、url字段的】,因为照片墙显示数据务必要有这两个属性
// 对于服务器而言,不需要name、url字段,将来对于有的图片的数据在提交给服务器的时候,需要处理的
},
// 预览
handlePictureCardPreview(file) {
// 将图片地址赋值给这个属性
this.dialogImageUrl = file.url;
//对话框显示
this.dialogVisible = true;
},
handlerSuccess(response, file, fileList){
// 收集图片的信息
this.spuImageList = fileList;
},
修改回显图片: 处理数据方式1
//获取spu图片的数据
let spuImageResult = await this.$API.spu.reqSpuImageList(spu.id);
if (spuImageResult.code == 200) {
let listArr = spuImageResult.data;
//由于照片墙显示图片的数据需要数组,数组里面的元素需要有name与url字段
//需要把服务器返回的数据进行修改
listArr.forEach((item) => {
item.name = item.imgName;
item.url = item.imgUrl;
});
//把整理好的数据赋值给
this.spuImageList = listArr;
}
处理数据方式2
//由于照片墙显示图片的数据需要数组,数组里面的元素需要有name与url字段
//需要把服务器返回的数据进行修改
this.spuImageList = spuImageResult.data.map(i=>{
return{
...i,
name :i.imgName,
url :i.imgUrl,
}
});
地址:https://toolgg.com/excel-to-json.html
https://blog.csdn.net/GongWei_/article/details/109670992
datas.Sheet.forEach((element,index) => {
this.dataList.push( Object.values(element))
if(index==0){
this.header = Object.keys(element)
}
});
console.log(this.header);
console.log(JSON.stringify(this.dataList));
已知:全部属性列表 (this.saleAttrList) 、表格属性列表 (this.spu.spuSaleAttrList)
未知:未选择属性列表 (下拉框的列表)
computed: {
//计算出还未选择的销售属性
unSelectSaleAttr() {
//整个平台的销售属性一共三个:尺寸、颜色、版本 ----saleAttrList
//当前SPU拥有的属于自己的销售属性SPU.spuSaleAttrList ----颜色
//数组的过滤方法,可以从已有的数组当中过滤出用户需要的元素,并未返回一个新的数据
let result = this.saleAttrList.filter((item) => {
//every数组的方法,会返回一个布尔值【真,假的】
return this.spu.spuSaleAttrList.every((i) => {
return item.name != i.saleAttrName;
});
});
return result;
},
},
v-model
:收集数据【:value
中的数据】
:label
:input框显示的数据
<el-select
:placeholder="`还有${unSelectSaleAttr.length}未选择`"
v-model="attrIdAndAttrName"
>
<el-option
:label="unselect.name"
:value="`${unselect.id}:${unselect.name}`"
v-for="(unselect, index) in unSelectSaleAttr"
:key="unselect.id"
></el-option>
</el-select>
select选择添加一条销售属性,对应的表格中多一条销售属性数据。添加之后,清空select值
:value=“
${unselect.id}:${unselect.name}
” 可以使用拼接的形式收集想要的数据
<el-select
:placeholder="`还有${unSelectSaleAttr.length}未选择`"
v-model="attrIdAndAttrName"
>
<el-option
:label="unselect.name"
:value="`${unselect.id}:${unselect.name}`"
v-for="(unselect, index) in unSelectSaleAttr"
:key="unselect.id"
>el-option>
el-select>
<el-button
type="primary"
icon="el-icon-plus"
:disabled="!attrIdAndAttrName"
@click="addSaleAttr"
>添加销售属性el-button
>
通过将收集的数据分割、解构得到对应的属性
//添加新的销售属性
addSaleAttr() {
//已经收集需要添加的销售属性的信息
//把收集到的销售属性数据进行分割
const [baseSaleAttrId, saleAttrName] = this.attrIdAndAttrName.split(":");
//向SPU对象的spuSaleAttrList属性里面添加新的销售属性
let newSaleAttr = {
baseSaleAttrId,
saleAttrName,
spuSaleAttrValueList: [],
};
//添加新的销售属性
this.spu.spuSaleAttrList.push(newSaleAttr);
//清空数据
this.attrIdAndAttrName = "";
},
tag inpt button
<el-table-column prop="prop" label="属性值名称列表" width="width">
<template slot-scope="{ row, $index }">
<el-tag
:key="tag.id"
v-for="(tag, index) in row.spuSaleAttrValueList"
closable
:disable-transitions="false"
@close="row.spuSaleAttrValueList.splice(index, 1)"
>{{ tag.saleAttrValueName }}el-tag
>
<el-input
class="input-new-tag"
v-if="row.inputVisible"
v-model="row.inputValue"
ref="saveTagInput"
size="small"
@blur="handleInputConfirm(row)"
>
el-input>
<el-button
v-else
class="button-new-tag"
size="small"
@click="addSaleAttrValue(row)"
>添加el-button
>
template>
el-table-column>
v-if="row.inputVisible"
用来显示隐藏input。表格中本没有inputVisible这个字段,需要动态添加,在给后台发送数据的时候记得去掉
注意:不能在data中单独定义,表格中有多条属性时,点击其中一个添加按钮显示所有input切换现象。
@close="row.spuSaleAttrValueList.splice(index, 1)"
删除逻辑处理。
//添加按钮的回调
addSaleAttrValue(row) {
//点击销售属性值当中添加按钮的时候,需要有button变为input,通过当前销售属性的inputVisible控制
//挂载在销售属性身上的响应式数据inputVisible,控制button与input切换
this.$set(row, "inputVisible", true);
//通过响应式数据inputValue字段收集新增的销售属性值
this.$set(row, "inputValue", "");
},
//el-input失却焦点的事件
handleInputConfirm(row) {
//解构出销售属性当中收集数据
const { baseSaleAttrId, inputValue } = row;
//新增的销售属性值的名称不能为空
if (inputValue.trim() == "") {
this.$message("属性值不能为空");
return;
}
//属性值不能重复,这里也可以用some
// let result = row.spuSaleAttrValueList.every(
// (item) => item.saleAttrValueName != inputValue
// );
// if (!result) return;
// 或者
let res = row.spuSaleAttrValueList.some(i=>{
return i.saleAttrName == inputValue
})
if(res) return
//新增的销售属性值
let newSaleAttrValue = { baseSaleAttrId, saleAttrValueName: inputValue };
//新增
row.spuSaleAttrValueList.push(newSaleAttrValue);
//修改inputVisible为false,不就显示button
row.inputVisible = false;
},
保存(子组件)
flag: this.spu.id ? "修改" : "添加",
点击添加返回第一页,修改返回修改数据的那一页
//保存按钮的回调
async addOrUpdateSpu() {
//整理参数:需要整理照片墙的数据
//携带参数:对于图片,需要携带imageName与imageUrl字段
this.spu.spuImageList = this.spuImageList.map((item) => {
return {
imageName: item.name,
imageUrl: (item.response && item.response.data) || item.url,
};
});
//发请求
let result = await this.$API.spu.reqAddOrUpdateSpu(this.spu);
if (result.code == 200) {
//提示
this.$message({ type: "success", message: "保存成功" });
//通知父组件回到场景0
this.$emit("changeScene", {
scene: 0,
flag: this.spu.id ? "修改" : "添加",
});
}
//清除数据
Object.assign(this._data, this.$options.data());
},
//取消按钮
cancel() {
//取消按钮的回调,通知父亲切换场景为0
this.$emit("changeScene", { scene: 0, flag: "" });
//清理数据
//Object.assign:es6中新增的方法可以合并对象
//组件实例this._data,可以操作data当中响应式数据
//this.$options可以获取配置对象,配置对象的data函数执行,返回的响应式数据为空的
Object.assign(this._data, this.$options.data());
},
父组件
changeScene({ scene, flag }) {
//flag这个形参为了区分保存按钮是添加还是修改
//切换结构(场景)
this.scene = scene;
//子组件通知父组件切换场景,需要再次获取SPU列表的数据进行展示
if (flag == "修改") {
this.getSpuList(this.page);
} else {
this.getSpuList();
}
},
可以用来清空数据:当你保存或取消的时候可以清空表单数据,保证点击新增的时候,表单数据是空的
新增、修改接口
//修改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 });
}
}
let arr = [1,2,3]
let arr1 = [4,5,6]
let allArr = [...arr,...arr1]
// allArr [1,2,3,4,5,6]
后端需要收集的是默认的图片地址
获取到用户选中图片的信息数据,需要注意,当前收集的数据当中,缺少isDefault字段,也就是控制显示默认还是设置默认的字段,需要前端动态添加。
<el-table style="width: 100%" border :data="spuImageList" @selection-change="handleSelectionChange">
<el-table-column type="selection" prop="prop" width="80">
el-table-column>
<el-table-column prop="prop" label="图片" width="width">
<template slot-scope="{row,$index}">
<img :src="row.imgUrl" style="width:100px;height:100px">
template>
el-table-column>
<el-table-column prop="imgName" label="名称" width="width">
el-table-column>
<el-table-column prop="prop" label="操作" width="width">
<template slot-scope="{row,$index}">
<el-button type="primary" v-if="!row.isDefault" @click="changeDefault(row)">设置默认el-button>
<el-button v-else>默认el-button>
template>
el-table-column>
el-table>
获取数据的时候,添加字段
let result = await this.$API.spu.reqSpuImageLIst(spu.id);
if (result.code == 200) {
let list = result.data;
list.forEach(item=>{
item.isDefault = false;
});
this.spuImageList = list;
}
收集默认的图片
//排他操作
changeDefault(row){
//图片列表的数据的isDefault字段变为 0
this.spuImageList.forEach(item=>{
item.isDefault = 0;
});
//点击的那个图片的数据变为1
row.isDefault = 1;
//收集默认图片的地址
this.skuInfo.skuDefaultImg = row.imgUrl;
},
销售属性、平台属性两个代码一致,这里只显示一个
<el-form-item label="销售属性">
<el-form :inline="true" ref="form" label-width="80px">
<el-form-item :label="saleAttr.saleAttrName" v-for="(saleAttr,index) in spuSaleAttrList" :key="saleAttr.id">
<el-select placeholder="请选择" v-model="saleAttr.attrIdAndValueId">
<el-option :label="saleAttrValue.saleAttrValueName" :value="`${saleAttr.id}:${saleAttrValue.id}`" v-for="(saleAttrValue,index) in saleAttr.spuSaleAttrValueList" :key="saleAttrValue.id">el-option>
el-select>
el-form-item>
el-form>
el-form-item>
目前数据被收集在skuSaleAttrValueList
,最后保存的时候需要整理收集数据在skuInfo.skuSaleAttrValueList
中
reduce累加器:其中prev为初始值或者计算后的返回值(必须)、currentValue为当前元素(必须)、currentIndex为当前元素索引(可选)、arr为当前元素所属的对象(可选)、initialValue为传递给函数的初始值
不更改原数组,返回一个新的数组
array.reduce(function(prev, currentValue, currentIndex, arr){return prev}, initialValue)
案例1:数组去重
// 数组去重
var arr = [12, 34, 34, 342, 345, 34, 123, 345, 45, 12]
var newArr = arr.reduce(function (prev, next) {
prev.indexOf(next) == -1 && prev.push(next)
return prev
}, [])
console.log(arr) // [12, 34, 34, 342, 345, 34, 123, 345, 45, 12]
console.log(newArr) // [12, 34, 342, 345, 123, 45]
//初始化一个空数组,判断下一个元素是否在当前数组中,不存在则添加到当前数组中。
案例2:数组中元素求和
// 数组求和
var arr = [1, 2, 3, 4, 5]
var total = arr.reduce(function (prev, next) {
return prev + next
}, 0)
console.log(total) // 15
将0当做reduce回调函数中的初始值,然后依次累加
案例3:求数组中最大值或最小值:
// 获取数组中最大值
var arr = [1347, 34773, 12, 345, 355, 425, 356, 34566, 79088]
var max = arr.reduce(function (prev, next) {
return Math.max(prev, next) // Math.min(prev, next)
}, 0)
console.log(max)
tips:initialValue为传递给函数的初始值,假如该值不存在时,则回调函数的初始值为数组中的第一项,即回调函数中的prev值
spu保存,处理数据
async save() {
//整理参数
//整理平台属性
const { attrInfoList, skuInfo, spuSaleAttrList, imageList } = this;
//整理平台属的数据
skuInfo.skuAttrValueList = attrInfoList.reduce((prev, item) => {
if (item.attrIdAndValueId) {
const [attrId, valueId] = item.attrIdAndValueId.split(":");
prev.push({ attrId, valueId });
}
return prev;
}, []);
//整理销售属性
skuInfo.skuSaleAttrValueList = spuSaleAttrList.reduce((prev, item) => {
if (item.attrIdAndValueId) {
const [saleAttrId, saleAttrValueId] =
item.attrIdAndValueId.split(":");
prev.push({ saleAttrId, saleAttrValueId });
}
return prev;
}, []);
//整理图片的数据
skuInfo.skuImageList = imageList.map((item) => {
return {
imgName: item.imgName,
imgUrl: item.imgUrl,
isDefault: item.isDefault,
spuImgId: item.id,
};
});
//发请求
let result = await this.$API.spu.reqAddSku(skuInfo);
if (result.code == 200) {
this.$message({ type: "success", message: "添加SKU成功" });
this.$emit("changeScenes", 0);
}
},
release/20220523-090840174456201_release_1262456_91 这是远程合并产生的冲突分支,直接拉取解决。
拉取远程分支
git fetch origin
将远程分支复制一份到本地
git checkout -b release/20220523-090840174456201_release_1262456_91 origin/release/20220523-090840174456201_release_1262456_91
获取有冲突的文件
git merge --no-ff a174bff17ef338f89a088a329bc348814dd1ce35
点击info按钮,发送请求获取数据,弹出数据。加Loading 加载效果。
问题:
1、点击第一个info按钮会出现loading 效果,关闭点击其他的不在出现。
2、快速切换info按钮,会先回显之前的数据然后清空,再回显数据。
解决:v-loading=“loading” 指令
1、:before-close=“close” 点击X
之前的事件更改属性( 解决问题1、2)
或者2、点击info直接将loading属性变为真( 解决问题1)
<el-dialog :before-close="close"/>
close(done) {
//loading属性再次变为真
this.loading = true;
//清除sku列表的数据
this.skuList = [];
//管理对话框
done();
},
深度选择器
原生的css 使用>>>
less 使用/deep/
scss 使用::v-deep
子组件的根节点div拥有父组件h3的css样式,但无法影响子组件div中h3的样式
子组件的根节点h3拥有父组件h3的css样式。
如果父组件(scoped)要影响子組件中的样式,要使用深度选择器
:show-close=“false” 不显示
x
<el-drawer :visible.sync="show" :show-close="false" size="50%">
<el-row>
<el-col :span="5">商品图片el-col>
<el-col :span="16">
<div style="width: 450px; height: 450px; border: 1px #eee solid">
<el-carousel height="450px">
<el-carousel-item
v-for="item in skuInfo.skuImageList"
:key="item.id"
>
<img :src="item.imgUrl" style="width: 100%; height: 100%" />
el-carousel-item>
el-carousel>
div>
el-col>
el-row>
el-drawer>
直接在style scoped 修改样式是不能实现的
方法1、直接在没有scoped 的style 中修改
方法2、使用深度选择器
https://gitee.com/jch1011/projects
git clone https://gitee.com/jch1011/data-visualization-code.git
npm i
npm run dev
运行即可
点击登录按钮获取token信息
// 登录页
<el-button :loading="loading" type="primary" @click.native.prevent="handleLogin">登录el-button>
派发一个action:user/login
//登录业务:发请求,带着用户名与密码给服务器(成功与失败)
handleLogin() {
//这里是在验证表单元素(用户名与密码)的是否符合规则
this.$refs.loginForm.validate(valid => {
//如果符合验证规则
if (valid) {
//按钮会有一个loading效果
this.loading = true;
//派发一个action:user/login,带着用户名与密码的载荷
this.$store.dispatch('user/login', this.loginForm).then(() => {
//登录成功进行路由的跳转
this.$router.push({ path: this.redirect || '/' });
//loading效果结束
this.loading = false
}).catch(() => {
this.loading = false
})
} else {
console.log('error submit!!')
return false
}
})
}
getInfo:通过token,获取用户信息。
use.js
//引入登录|退出登录|获取用户信息的接口函数
import { login, logout, getInfo } from '@/api/user'
// 获取token|设置token|删除token的函数
import { getToken, setToken, removeToken } from '@/utils/auth'
//路由模块当中重置路由的方法
import { anyRoutes, resetRouter, asyncRoutes, constantRoutes } from '@/router';
import router from '@/router';
import cloneDeep from 'lodash/cloneDeep'
//箭头函数
const getDefaultState = () => {
return {
//获取token
token: getToken(),
//存储用户名
name: '',
//存储用户头像
avatar: '',
//服务器返回的菜单信息【根据不同的角色:返回的标记信息,数组里面的元素是字符串】
routes: [],
//角色信息
roles: [],
//按钮权限的信息
buttons: [],
//对比之后【项目中已有的异步路由,与服务器返回的标记信息进行对比最终需要展示的理由】
resultAsyncRoutes: [],
//用户最终需要展示全部路由
resultAllRputes: []
}
}
const state = getDefaultState()
//唯一修改state的地方
const mutations = {
//重置state
RESET_STATE: (state) => {
Object.assign(state, getDefaultState())
},
//存储token
SET_TOKEN: (state, token) => {
state.token = token
},
//存储用户信息
SET_USERINFO: (state, userInfo) => {
//用户名
state.name = userInfo.name;
//用户头像
state.avatar = userInfo.avatar;
//菜单权限标记
state.routes = userInfo.routes;
//按钮权限标记
state.buttons = userInfo.buttons;
//角色
state.roles = userInfo.roles;
},
//最终计算出的异步路由
SET_RESULTASYNCROUTES: (state, asyncRoutes) => {
//vuex保存当前用户的异步路由,注意,一个用户需要展示完成路由:常量、异步、任意路由
state.resultAsyncRoutes = asyncRoutes;
//计算出当前用户需要展示所有路由
state.resultAllRputes = constantRoutes.concat(state.resultAsyncRoutes, anyRoutes);
//给路由器添加新的路由
router.addRoutes(state.resultAllRputes)
}
}
//定义一个函数:两个数组进行对比,对比出当前用户到底显示哪些异步路由
const computedAsyncRoutes = (asyncRoutes, routes) => {
//过滤出当前用户【超级管理|普通员工】需要展示的异步路由
return asyncRoutes.filter(item => {
//数组当中没有这个元素返回索引值-1,如果有这个元素返回的索引值一定不是-1
if (routes.indexOf(item.name) != -1) {
//递归:别忘记还有2、3、4、5、6级路由
if (item.children && item.children.length) {
item.children = computedAsyncRoutes(item.children, routes);
}
return true;
}
})
}
//actions
const actions = {
//这里在处理登录业务
async login({ commit }, userInfo) {
//解构出用户名与密码
const { username, password } = userInfo;
let result = await login({ username: username.trim(), password: password });
//注意:当前登录请求现在使用mock数据,mock数据code是20000
if (result.code == 20000) {
//vuex存储token
commit('SET_TOKEN', result.data.token);
//本地持久化存储token
setToken(result.data.token);
return 'ok';
} else {
return Promise.reject(new Error('faile'));
}
},
//获取用户信息
getInfo({ commit, state }) {
return new Promise((resolve, reject) => {
getInfo(state.token).then(response => {
//获取用户信息:返回数据包含:
// 用户名name、用户头像avatar、routes[返回的标志:不同的用户应该展示哪些菜单的标记]、roles(用户角色信息)、buttons【按钮的信息:按钮权限用的标记】
const { data } = response;
//vuex存储用户全部的信息
commit('SET_USERINFO', data);
commit('SET_RESULTASYNCROUTES', computedAsyncRoutes(cloneDeep(asyncRoutes), data.routes));
resolve(data)
}).catch(error => {
reject(error)
})
})
},
// user logout
logout({ commit, state }) {
return new Promise((resolve, reject) => {
logout(state.token).then(() => {
removeToken() // must remove token first
resetRouter()
commit('RESET_STATE')
resolve()
}).catch(error => {
reject(error)
})
})
},
// remove token
resetToken({ commit }) {
return new Promise(resolve => {
removeToken() // must remove token first
commit('RESET_STATE')
resolve()
})
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
拆分use.js功能
state
//箭头函数
const getDefaultState = () => {
return {
//获取token
token: getToken(),
//存储用户名
name: '',
//存储用户头像
avatar: '',
//服务器返回的菜单信息【根据不同的角色:返回的标记信息,数组里面的元素是字符串】
routes: [],
//角色信息
roles: [],
//按钮权限的信息
buttons: [],
//对比之后【项目中已有的异步路由,与服务器返回的标记信息进行对比最终需要展示的理由】
resultAsyncRoutes: [],
//用户最终需要展示全部路由
resultAllRputes: []
}
}
const state = getDefaultState()
获取用户信息
getInfo({commit,state}){
return new Promise(resolve, reject)=>{
getInfo(state.token).then(res=>{
//获取用户信息:返回数据包含:
// 用户名name、用户头像avatar、routes[返回的标志:不同的用户应该展示哪些菜单的标记]、roles(用户角色信息)、buttons【按钮的信息:按钮权限用的标记】
const { data } = response;
//vuex存储用户全部的信息
commit('SET_USERINFO', data);
commit('SET_RESULTASYNCROUTES', computedAsyncRoutes(cloneDeep(asyncRoutes), data.routes));
resolve(data)
}).catch(error => {
reject(error)
})
}
}
将
异步路由
与后台返回的路由
进行对比出当前用户到底显示哪些异步路由
//定义一个函数:两个数组进行对比,对比出当前用户到底显示哪些异步路由
const computedAsyncRoutes = (asyncRoutes, routes) => {
//过滤出当前用户【超级管理|普通员工】需要展示的异步路由
return asyncRoutes.filter(item => {
//数组当中没有这个元素返回索引值-1,如果有这个元素返回的索引值一定不是-1
if (routes.indexOf(item.name) != -1) {
//递归:别忘记还有2、3、4、5、6级路由
if (item.children && item.children.length) {
item.children = computedAsyncRoutes(item.children, routes);
}
return true;
}
})
}
getInfo 的获取的用户信息
vuex保存当前用户的异步路由,注意,一个用户需要展示完成路由:常量、异步、任意路由
router.addRoutes(state.resultAllRputes) 动态添加路由
mutations
//唯一修改state的地方
const mutations = {
//重置state
RESET_STATE: (state) => {
Object.assign(state, getDefaultState())
},
//存储token
SET_TOKEN: (state, token) => {
state.token = token
},
//存储用户信息
SET_USERINFO: (state, userInfo) => {
//用户名
state.name = userInfo.name;
//用户头像
state.avatar = userInfo.avatar;
//菜单权限标记
state.routes = userInfo.routes;
//按钮权限标记
state.buttons = userInfo.buttons;
//角色
state.roles = userInfo.roles;
},
//最终计算出的异步路由
SET_RESULTASYNCROUTES: (state, asyncRoutes) => {
//vuex保存当前用户的异步路由,注意,一个用户需要展示完成路由:常量constantRoutes、异步resultAsyncRoutes、任意路由anyRoutes
state.resultAsyncRoutes = asyncRoutes;
//计算出当前用户需要展示所有路由(用数组进行拼接)
state.resultAllRputes = constantRoutes.concat(state.resultAsyncRoutes, anyRoutes);
//动态给路由器添加新的路由
router.addRoutes(state.resultAllRputes)
}
}
router=>index.js
//引入Vue|Vue-router
import Vue from 'vue'
import Router from 'vue-router'
//使用路由插件
Vue.use(Router)
/* 引入最外层骨架的一级路由组件*/
import Layout from '@/layout'
//路由的配置:为什么不同用户登录我们的项目,菜单(路由)都是一样的?
//因为咱们的路由‘死的’,不管你是谁,你能看见的,操作的菜单都是一样的
//需要把项目中的路由进行拆分
//常量路由:就是不关用户是什么角色,都可以看见的路由
//什么角色(超级管理员,普通员工):登录、404、首页
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: '首页', icon: 'dashboard' }
}]
},
]
//异步理由:不同的用户(角色),需要过滤筛选出的路由,称之为异步路由
//有的用户可以看见测试管理、有的看不见
export const asyncRoutes = [
{
name: 'Acl',
path: '/acl',
component: Layout,
redirect: '/acl/user/list',
meta: {
title: '权限管理',
icon: 'el-icon-lock'
},
children: [
{
name: 'User',
path: 'user/list',
component: () => import('@/views/acl/user/list'),
meta: {
title: '用户管理',
},
},
{
name: 'Role',
path: 'role/list',
component: () => import('@/views/acl/role/list'),
meta: {
title: '角色管理',
},
},
{
name: 'RoleAuth',
path: 'role/auth/:id',
component: () => import('@/views/acl/role/roleAuth'),
meta: {
activeMenu: '/acl/role/list',
title: '角色授权',
},
hidden: true,
},
{
name: 'Permission',
path: 'permission/list',
component: () => import('@/views/acl/permission/list'),
meta: {
title: '菜单管理',
},
},
]
},
];
//任意路由:当路径出现错误的时候重定向404
export const anyRoutes ={ path: '*', redirect: '/404', hidden: true };
const createRouter = () => new Router({
// mode: 'history', // require service support
scrollBehavior: () => ({ y: 0 }),
//因为注册的路由是‘死的’,‘活的’路由如果根据不同用户(角色)可以展示不同菜单
routes: constantRoutes
})
const router = createRouter()
// Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
export function resetRouter() {
const newRouter = createRouter()
router.matcher = newRouter.matcher // reset router
}
export default router
修改左侧菜单的路由数组
computed: {
//应该替换为仓库中已经计算好的需要展示的全部路由
routes() {
//sliderbar:需要遍历的应该是仓库计算完毕的全部路由
return this.$store.state.user.resultAllRputes;
},
}
tree树形控件获取父节点ID的实例: 修改源码就可,一般不要轻易修改,会造成其他页面破坏
vue elementUI tree树形控件获取父节点ID的实例
修改源码:
情况1: element-ui没有实现按需引入打包
node_modules\element-ui\lib\element-ui.common.js 25382行修改源码 去掉 'includeHalfChecked &&'
// if ((child.checked || includeHalfChecked && child.indeterminate) && (!leafOnly || leafOnly && child.isLeaf)) {
if ((child.checked || child.indeterminate) && (!leafOnly || leafOnly && child.isLeaf)) {
情况2: element-ui实现了按需引入打包
node_modules\element-ui\lib\tree.js 1051行修改源码 去掉 'includeHalfChecked &&'
// if ((child.checked || includeHalfChecked && child.indeterminate) && (!leafOnly || leafOnly && child.isLeaf)) {
if ((child.checked || child.indeterminate) && (!leafOnly || leafOnly && child.isLeaf)) {
将子节点以及角色id传过去
<template>
<div>
<el-input disabled :value="$route.query.roleName"></el-input>
<el-tree
style="margin: 20px 0"
ref="tree"
:data="allPermissions"
node-key="id"
show-checkbox
default-expand-all
:props="defaultProps"
/>
<el-button :loading="loading" type="primary" @click="save">保存</el-button>
<el-button @click="$router.replace({ name: 'Role' })">取消</el-button>
</div>
</template>
<script>
export default {
name: "roleAuth",
data() {
return {
loading: false, // 用来标识是否正在保存请求中的标识, 防止重复提交
allPermissions: [], // 所有
defaultProps: {
children: "children",
label: "name",
},
};
},
created() {
this.init();
},
methods: {
/*
初始化
*/
init() {
const roleId = this.$route.params.id;
this.getPermissions(roleId);
},
/*
获取指定角色的权限列表
*/
getPermissions(roleId) {
this.$API.permission.toAssign(roleId).then((result) => {
const allPermissions = result.data.children;
this.allPermissions = allPermissions;
const checkedIds = this.getCheckedIds(allPermissions);
this.$refs.tree.setCheckedKeys(checkedIds);
});
},
/*
得到所有选中的id列表,只有选中的子节点id
*/
getCheckedIds(auths, initArr = []) {
return auths.reduce((pre, item) => {
if (item.select && item.level === 4) {
console.log();
pre.push(item.id);
} else if (item.children) {
this.getCheckedIds(item.children, initArr);
}
return pre;
}, initArr);
},
/*
保存权限列表
*/
save() {
var ids = this.$refs.tree.getCheckedKeys().join(",");
this.loading = true;
this.$API.permission
.doAssign(this.$route.params.id, ids)
.then((result) => {
this.loading = false;
this.$message.success(result.$message || "分配权限成功");
// 必须在跳转前获取(跳转后通过this获取不到正确的数据了)
const roleName = this.$route.query.roleName;
const roles = this.$store.getters.roles;
this.$router.replace("/acl/role/list", () => {
console.log("replace onComplete");
// 跳转成功后, 判断如果更新的是当前用户对应角色的权限, 重新加载页面以获得最新的数据
if (roles.includes(roleName)) {
window.location.reload();
}
});
});
},
},
};
</script>
// 从课程中拉取id为51c9a74f-b688-4f2f-8470-7ee95882e0a8的课程
select * from course where id = '51c9a74f-b688-4f2f-8470-7ee95882e0a8'
// 从video_play_log 表中查询 课程id和时间大于5月26的
SELECT
*
FROM
video_play_log
WHERE
course_id = '51c9a74f-b688-4f2f-8470-7ee95882e0a8'
AND created_time > '2022-5-26'
// 从课节中找课程
select * from course_unit where course_id = '51c9a74f-b688-4f2f-8470-7ee95882e0a8'
具名插槽
父组件slot="charts"
<el-card>
<Detail title="访问量" count="88460">
<template slot="charts">
<lineCharts>lineCharts>
template>
<template slot="footer">
<span>日访问量 1234span>
template>
Detail>
el-card>
子组件
<div>
<div class="card-header">
<span>{{title}}span>
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>
<el-card class="box-card">
<div slot="header" class="clearfix">
<el-tabs v-model="activeName" class="all">
<el-tab-pane label="销售额" name="first"> el-tab-pane>
<el-tab-pane label="明细额" name="second"> el-tab-pane>
el-tabs>
<div class="right">
<span @click="setDay">今日span>
<span @click="setWeek">本周span>
<span @click="setMonth">本月span>
<span @click="setYear">本年span>
<el-date-picker
v-model="date"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="yyyy-MM-dd"
size="mini"
>
el-date-picker>
div>
div>
<div>
<el-row :gutter="10">
<el-col :span="18">
<div class="charts" ref="charts">div>
el-col>
<el-col :span="6" class="right">
<h3>门店{{ title }}排名h3>
<ul>
<li>
<span class="rindex">0span>
<span>肯德基span>
<span class="rvalue">123456span>
li>
<li>
<span class="rindex">1span>
<span>肯德基span>
<span class="rvalue">123456span>
li>
<li>
<span class="rindex">3span>
<span>肯德基span>
<span class="rvalue">123456span>
li>
<li>
<span>4span>
<span>肯德基span>
<span class="rvalue">123456span>
li>
ul>
el-col>
el-row>
div>
el-card>
template>
dayjs 时间插件
echarts 本身无高度,给一个高度
mounted只执行一次,切换数据,echarts不在渲染,通过watch监听重新渲染数据
<script>
import dayjs from "dayjs";
import * as echarts from "echarts";
export default {
data() {
return {
activeName: "first",
date: [],
charts: null,
option: {},
};
},
watch: {
//切换数据视图
title() {
this.option.title.text = this.title + "数据";
this.option.xAxis = {
data:
this.title == "销售"
? ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
: ["1", "2", "3", "4", "5", "6", "7"],
};
this.option.series = {
data:
this.title == "销售"
? [120, 200, 150, 80, 70, 110, 130]
: [220, 20, 150, 80, 170, 10, 130],
};
this.charts.setOption(this.option);
},
// 初始数据
listState() {
this.mycharts.setOption({
title: {
text: this.title + "趋势",
},
tooltip: {
trigger: "axis",
axisPointer: {
type: "shadow",
},
},
grid: {
left: "3%",
right: "4%",
bottom: "3%",
containLabel: true,
},
xAxis: [
{
type: "category",
data: this.listState.orderFullYearAxis,
axisTick: {
alignWithLabel: true,
},
},
],
yAxis: [
{
type: "value",
},
],
series: [
{
name: "Direct",
type: "bar",
barWidth: "60%",
data: this.listState.orderFullYear,
color: "yellowgreen",
},
],
});
},
},
// 只执行一次,切换数据,echarts不在渲染
mounted() {
this.initData();
},
computed: {
title() {
return this.activeName == "first" ? "销售" : "明细";
},
...mapState({
listState: (state) => state.home.list,
}),
},
methods: {
// 时间获取
setDay() {
const day = dayjs().format("YYYY-MM-DD");
this.date = [day, day];
},
setWeek() {
const start = dayjs().day(1).format("YYYY-MM-DD");
const end = dayjs().day(7).format("YYYY-MM-DD");
this.date = [start, end];
},
setMonth() {
const start = dayjs().startOf("month").format("YYYY-MM-DD");
const end = dayjs().endOf("month").format("YYYY-MM-DD");
this.date = [start, end];
},
setYear() {
const start = dayjs().startOf("year").format("YYYY-MM-DD");
const end = dayjs().endOf("year").format("YYYY-MM-DD");
this.date = [start, end];
},
// 初始化echarts
initData() {
this.charts = echarts.init(this.$refs.charts);
this.option = {
title: {
text: this.title + "数据",
subtext: "Living Expenses in Shenzhen",
},
xAxis: {
type: "category",
data: [],
},
yAxis: {
type: "value",
},
series: [
{
data: [],
type: "bar",
showBackground: true,
backgroundStyle: {
color: "rgba(180, 180, 180, 0.2)",
},
barWidth: "50%",
color: "purple",
},
],
};
this.option && this.charts.setOption(this.option);
},
},
};
</script>
<div class="charts" ref="charts">div>
<script>
import echarts from "echarts";
export default {
name: "",
mounted() {
//饼图
let mychart = echarts.init(this.$refs.charts);
mychart.setOption({
title: {
text: "视频",
subtext: 1048,
left: "center",
top: "center",
},
tooltip: {
trigger: "item",
},
series: [
{
name: "Access From",
type: "pie",
radius: ["40%", "70%"],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: "#fff",
borderWidth: 2,
},
label: {
show: true,
position: "outsize",
},
labelLine: {
show: true,
},
data: [
{ value: 1048, name: "视频" },
{ value: 735, name: "Direct" },
{ value: 580, name: "Email" },
{ value: 484, name: "Union Ads" },
{ value: 300, name: "Video Ads" },
],
},
],
});
//绑定事件,更改饼图中间的数据以及title
mychart.on("mouseover", (params) => {
//获取鼠标移上去那条数据
const { name, value } = params.data;
//重新设置标题
mychart.setOption({
title: {
text: name,
subtext: value,
},
});
});
},
};
</script>
iframe 父子交互方式
子
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<link rel="icon" href="">
<title>Document</title>
</head>
<body>
<div>孩子</div>
<div id='box'> </div>
<button id="btn" onclick="say()">点击</button>
<script>
window.onload = function(){
window.addEventListener('message', function(e) {
if (!e || !e.data) { return; }
switch (e.data.type) {
case 'boundFileKeys':
document.getElementById("box").innerHTML = e.data.data;
break;
}
}, false);
}
function say() {
//window.parent 用于iframe嵌套情况下寻找上级window对象
//跳转打开新页面的情况下,获取父页面,即打开新页面的原始页面
let parentWindow = window.parent.postMessage({ type: 'check', data: '有,enen..你有米吗?' }, '*'); // 获取打开此页面的window对象
}
function getParentParam(){
window.parent.postMessage({ type: 'hit', data: '打你,就知道哭哭' }, '*');
}
</script>
</body>
</html>
父
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<link rel="icon" href="">
<title>Document</title>
</head>
<body>
<div>父</div>
<div id='parentHello'></div><span id='box1'></span>
<input type="button" onclick="parentHello()" value='sendMessage' >
<button onclick="hit()">暴揍小孩</button>
<span id='id2'></span>
<iframe id='childIframe' src='./child.html' name='Iam' fr
ameborder='1' style="width: 100%; height:100%; overflow: hidden;" scrolling='yes'></iframe>
</body>
<script>
// window.onload = function(){
// iframe id
// iframe name
// let nameIam = window.frames['Iam'].window
// innerText
// innerHTML
function parentHello(){
let childIframe = document.getElementById('childIframe')
childIframe.contentWindow.postMessage({ type: 'boundFileKeys', data: "你好,小孩你有糖吗?" }, '*')
}
function hit(){
let childIframe = document.getElementById('childIframe')
childIframe.contentWindow.getParentParam();
}
window.addEventListener('message', function(e) {
console.log(e);
if (!e || !e.data) { return; }
switch(e.data.type){
case 'check':
document.getElementById("box1").innerHTML = e.data.data;
break;
case 'hit':
document.getElementById("id2").innerHTML = e.data.data;
break;
}
}, false);
// }
</script>
</html>
npm install file-saver --save
https://blog.csdn.net/qq_41067835/article/details/88798771
https://www.jb51.net/article/238106.htm#_label0
<div>
<input type="file" id="myFile" value="上传">
div>
<button onclick='export2()'>下载button>
// 本地文件上传
var upload = document.querySelector("#myFile");
upload.addEventListener("change", function (event) {
// readAsText() 读取文本文件,(可以使用Txt打开的文件)
// readAsBinaryString(): 读取任意类型的文件,返回二进制字符串
// readAsDataURL: 方法可以将读取到的文件编码成DataURL ,可以将资料(例如图片、excel文件)内嵌在网页之中,不用放到外部文件
let reader = new FileReader() // 读取文件
reader.readAsText(event.target.files[0]) //把目标文件转地址,文件来自于上传组件。
// ....读取中
reader.onload = function () {
let py = document.getElementById('pyId').contentWindow
py.importFile(reader.result);
}
}),
// 本地文件下载
export2 = function(){
let aTag = document.createElement('a');
let py = document.getElementById('pyId').contentWindow
let value = py.exportFile()
const content = value.toString();
let blob = new Blob([content], { type: "text/plain;charset=utf-8" });
saveAs(blob, '11' + ".py"); // 插件使用
// 直接下载
// aTag.download = "文件名" + ".py";
// aTag.href = URL.createObjectURL(blob);
// aTag.click();
// URL.revokeObjectURL(blob);
}
解析远程文件,通过解析url获取编辑器文本
function draw(py){
fetch('./textpy/'+ py).then((res)=>{
return res.text()
}).then((text)=>{
let py = document.getElementById('pyId').contentWindow
py.importFile(text);
})
}
// 获取编辑器文本
const value = this.iframeWin.exportFile();
// 转成字符串
const content = value.toString();
// 将编辑器文本转成文件
var file = new File([content], "py");
let params = new FormData();
params.append("file", file);
// 上传给后端,获取编辑器文本对应的url: res.body.fileUrl,进行编辑或者是新增
uploadFile(params)
.then((res) => {
// 上传成功后提交作品信息
this.loading = false;
let props = {
...this.formInfo,
sourceCodeFile: res.body.fileUrl,
};
props.id ? this.editWorkInfo(props) : this.saveProject(props);
})
.catch((err) => {
this.loading = false;
this.$message.warning("提交失败");
});
安装
npm install http-server --save
控制台运行
http-server