【前端】项目中遇到的问题汇总(长期更新)

一、联调交互类

1、出现一个数据在当前页面进行了修改,另外一个页面的同一数据并未同步更改

当前的数据经过调用接口修改更新以后,if(code == 200) 将当前数据存入store.dispatch, 然后另一个地方获取该数据,直接获取存入的数据,这样就可以同步显示了

let XXX=computed(() => { return store.getters.***}        //直接XXX.数据拿到就是同步的

2、给菜单项加权限,不显示管理员独有的菜单项

利用filter来传,但是不能直接在菜单的数组使用,这样会出现管理员打开页面没有该菜单项,需要二次刷新页面才出现,后也尝试在onMounted里写,结果仍需要二次刷新才出现

最后的解决方法1:

//template里传MenuList
let managerMenu= computed(() => {
  return store.getters.managerMenu;
});

//过滤掉管理员的菜单项,if里的条件看接口怎么定义、怎么存数据的
const menu = [{……}];
const menuList = computed(() => {
    if(managerMenu.value.length == 0) {
        return menu.fliter((item) => {
            return item.path != "XXX";
        });
    }
    else return menu
});

方法2:

//menu里不放管理员的菜单项
const menu = [{……}];

const menuList = computed(() => {
    if(managerMenu.value?.length > 0) {
        return [...menu, ...[{menuName: XXX, menPath:XXX,……}] ];
    }
    else return menu
});

3、路由跳转到某个页面,并显示某个标签

// 1、先设置对应的路由进行跳转
router.push("/" + item?.menuPath);

// 2、跳转点击的菜单项XXX
if(item.menuName == XXX) {
    store.dispatch("app/menu", "***");
}

//push是跳转当前页面,resolve+window.open则是跳转新页面

//push支持query和params,resolve只支持query,若没用query传参,也可以用localStorage
const routePath = router.resolve({path:XXX, query:XXX,});
window.open(routePath.href, "_blank");
localStorage.setItem('a', 1);

4、为简化代码,调多个接口可用三目运算符写入一个函数

const xxParams = ref({……})
const getList = async () => {
const { code, data} = await API.XXX[xxId ? "methodAPI1" : "methodAPI2"](xxParams.value);
if(code == 200) {……}

5、使用ElementUI的无线滚动加载 v-infinite-scroll,出现一直可以滚动的情况

默认情况下,infinite-scroll-disabled是false

方法1:可以在v-infinite-scroll="loadMore"的loadMore方法中限制达到列表长度就return;

方法2:

// 标签里写infinite-scroll-disabled="disabled"
<p v-if="loading">加载中...</p>
<p v-if="noMore">没有更多了</p>

const loading= ref(false)
const loadMore = () => {loading.value = true; ……}
const noMore= computed(() => return list.length >=20)    //eg:表长度限制
const disabled = computed(() => return loading || noMore

6、使用ElementPlus组件的tab标签用法

以标签tab为一级栏目切换,获取一级tab下的二级表利用一级tab的id来找,v-for需要指定key来进行排序,label显示当前一级的标题,name标识tab-pane,点击切换tab传入当前的一级item来获取二级表

<el-tabs v-model="firstTabId">
    <el-tab-pane
        v-for="(item, index) in firstList"
        :key="item?.id" 
        :label="item?.name" 
        :name="item?.id"
        @click="handleTabChange(item)"
    >
</el-tabs>

//先调用接口,获取firstList,再进行以下:
const handleTabChange = (item) => {
  firstTabId.value = item.id;
  currentfirstTab.valuue = item;
  getSecondListData(item.id);    //调用获取二级表的接口,传入一级id
}

7、编辑当前表格行时,弹出抽屉修改表单数据,此时,修改表单数据与表格里的行数据变成了同步修改,点击关闭抽屉取消修改,但行数据还是发生了变化

该问题主要是因为数据是浅拷贝的,所以在表单里修改了,表格中已经自动更新了,所以需要在显示抽屉表单时,传入表格行数据进行深拷贝传入,这样就不会出现该问题

import _ from "loadsh";
//以下的param是表格传入的行数据,判断id主要是确认是否存在当前行数据,是否开启的抽屉编辑
const showDrawer = (param) => {
    if (param?.id) {
        formData.value = _.cloneDeep(param);
        ……
    }
}

8、对当前列表进行增改后台,在抽屉中怎么刷新父组件的列表

        每次增删改查都需要刷新当前列表,删查在父组件直接进行,查的方法getTableList中获取当前列表接口,删的时候调用接口后在if(code==200)里最后再调用getTableList

        而增改则需要弹出抽屉进行数据修改,修改后调用查的方法进行刷新列表,这种情况就需要在父组件定义时,传入 @refreshTable="getTableList"暴露给子组件,子组件通过defineEmits拿到

const emit = defineEmits(["refreshTable"]);

并在增改方法调用接口成功以后,最后emit该方法进行刷新列表:

emit("refreshTable", currentParentId.value);        //传入当前列表所在的上一层级id

9、列表行拖拽排序-- 通过拖拽行拖拽图标进行重排序,序号、排序号均重排

利用Sortable方法,在onEnd调用排序方法,排序方法中先将当前表格数据备份,再用备份数据进行重排序,把原数据置空,再在nextTick中将备份重排序后的数据交给原数据,最后遍历表格数据数组,将sort利用index+1重排,最后调用接口刷新表格。

// HTML:
<el-table-column prop="sortNo" label=" ">
    <img class="handle" :src="drag-icon"/>
</el-table-column>
              
     
// CSS:
.handle {
  cursor: move;
  display: flex;
  vertical-align: middle;
  margin: auto;
}
.sortable-format {
  opacity: 0.5;
  pointer-events: none;
}

// JS:
//导入sortablejs---没下载依赖包,需进行下载npm i sortablejs @types/sortablejs
import Sortable from "sortablejs/Sortable.js"; //可能会与个人按照依赖包有关系

const sortTable= () => {
  const el = document.querySelector(".el-table__body-wrapper tbody");
  Sortable.create(el, {
    animation: 150,
    ghostClass: 'sortable-format', //拖拽样式
    handle: ".handle"  //指定拖拽手柄的类名,如果需点击某个图标拖拽[需要把图标的class写在这里]
    // 结束拖动事件
    onEnd: ({ newIndex, oldIndex }) => {
      dragSort(newIndex, oldIndex);
    },
  })
}

const dragSort = (newIndex, oldIndex) => {
  // 使用arr复制一份当前表格数组数据
  const arr = tableData.value;
  const currentRow = arr.splice(oldIndex, 1)[0];
  arr.splice(newIndex, 0, currentRow );
  // 将原数据数组置空
  tableData.value= [];
  nextTick(async () => {
    // 将拖拽排序后的数据赋值给原数据数组
     tableData.value= arr;
    // 提交后后台数据进行排序--遍历数组拿到index更新sort重排序
    let params = [];
    tableData.value.forEach((item, index) => {
      params.push({
        id: item.id,
        sort: index + 1,
      });
    });
    // 调用后台接口传参刷新列表
    const { code} = api.dragRowSort(params);
    if (code == 200) {
        getTableData();    //调用查询列表方法,刷新当前列表
    }
  });
}

10、使用ELementPlus组件的upload上传文件方法及图片回显

 el-upload的使用:

        需要传入请求头获取token,如果采用单独调用接口,则可以使用http-request,直接传入后端接口则用action
action放图片上传调用的后台方法,headers请求头携带token,file-list存放成功上传图片列表,用于修改功能时页面已有图片显示,before-upload图片上传前进行格式校验判断,accept指定文件类型,limit限制文件数目

以上传单张图片为例:

<el-upload
    :headers="getHerders()"
    action
    accept="image/jpeg,image/png"
    :file-list="imgFileList"
    :limit="1"
    :before-upload="handleImgBeforeUpload"
    :http-request="uploadImg"
>
    <img :src="uploadImgUrl" />
</el-upload>

// 上传登录背景图
const uploadImg= async ({ file }) => {
  let formData = new FormData();
  formData.append("file", file);
  //调用接口传入当前图片
  try {
    const { code, data } = await api.getImg(formData, {
        headers: {"Content-Type": "multipart/form-data", },
    });
    if(code == 200) {
        //获取当前上传的图片的路径
        currentFile.value.filePath = data;    
        // lookImg是查看图片的接口方法,用于回显图片
        uploadImgUrl.value = lookImg(data);    
    }
  } catch (error) {
    return Promise.reject(error);
  }
}

11、 组件间通信方式

(1)ref + emit:

打开弹窗/抽屉之类的在父组件调用方法,弹窗/抽屉组件的展示方法要暴露给父组件,父组件在调用子组件时,通过ref获取:

<!-- 子组件 -->
<Dialog v-model="dialogVisible" />
//script
const dialogVisible= ref(false);
const showDialog = () => {    //打开弹窗
  dialogVisible.value = true;
};
defineExpose({ showDialog , });

<!-- 父组件 -->
<CustomTable @addCustom="addCustom"/>
<CustomDialog
	ref="dialogRef"
	@refreshList="refresh"
/>
//script
const dialogRef = ref(null);
const addCustom = () => {    // 新增弹窗
  dialogRef.value.showDialog();
};

其他子组件(要调用子组件的兄弟组件,emit获取):

<!-- 兄弟组件 -->
<el-button @click="addProcess()"> + 新增流程 </el-button>

// 打开新增弹窗和展示详情抽屉
const emits = defineEmits(["addCustom"]);
const addProcess= () => {    // 新增弹窗
  emits("addCustom");
}
(2)v-model+props:

子组件需要使用父组件获取的数据,父组件调用子组件通过v-model传值

<!-- 父组件 -->
<CustomTable :customList="customList" />
// 调接口获取数据
const customList = ref([]);
const getAllList = async () => {
    // ……
    customList.value = data;
}

<!-- 子组件 -->
<el-option
	v-for="item in customList"
    //……
></el-option>
const props = defineProps({
  customList: {
    type: Array,
    default: () => [],
  },
});
(3)动态组件传值
<component
	:is="checkedMenu?.componentName"
	:currentId="userInfo?.id"
></component>

// 组件中写:
const props = defineProps(["currentId"]);
// props.currentId调用
(4)路由传参

点击菜单项跳转路由,需要传递当前菜单项id(当前组件id传到另外一个组件),通过路由传参:(query路径地址会显示参数)

const enterMenu = (menuItem) => {
  router.push({
    path: "/***",
    query: {
      fartherId: menuItem?.id,
    },
  });
};

另一个组件调用:route?.query?.fartherId

 12、el-popover 按钮的拖拽,拖拽时pop弹窗消失,停止是出现,鼠标移出消失

实现效果:el-popover移入显示弹窗,鼠标抓取可以拖拽,拖拽移动时弹窗不显示,拖拽完后弹窗显示,鼠标移出弹窗不显示,变成正常的hover。

        对el-popover的菜单拖拽,利用mousedown--mousemove--mouseup来写,需要写在reference的插槽内,该插槽对应卡片弹窗的按钮。

<template #reference>
	<div ref="dragDiv" @mousedown="startDrag" @mouseup="endDrag"></div>
</template>

实现:需要记录拖拽的初始xy,拖拽状态,针对鼠标按下、移动、抬起后方法各自定义,设置弹窗的hover状态变化(正常hover弹出,点击拖拽时隐藏,移动松开鼠标后显示,移出鼠标隐藏变成正常hover)

// 拖拽--菜单按钮标识
const dragDiv = ref(null);
// 初始化偏移值和拖拽状态
let offsetX, offsetY;
let dragging = false;
// 是否隐藏弹框标志
const isVisiblePopover = ref(null)
// 鼠标按下开始拖拽
const startDrag = (event)  => {
  isVisiblePopover.value = false;
  dragging = true;
  // 计算初始偏移值
  offsetX = event.clientX - dragDiv.value.getBoundingClientRect().left;
  offsetY = event.clientY - dragDiv.value.getBoundingClientRect().top;
  // 监听移动和松开事件
  document.addEventListener("mousemove", moveDrag);
};
// 鼠标移动拖拽
const moveDrag = (event) => {
  if(!dragging) return;
  // 计算新的位置
  const x = event.clientX - offsetX;
  const y = event.clientY - offsetY;
  // 检查新位置是否在视口内
  const viewWidth  = window.innerWidth;
  const viewHeight = window.innerHeight;
  if (x >= 0 && x + dragDiv.value.clientWidth <= viewWidth) {
    dragDiv.value.style.left = `${x}px`;
  }
  if (y >= 0 && y + dragDiv.value.clientHeight <= viewHeight) {
    dragDiv.value.style.top = `${y}px`;
  }
  document.ondragstart = function (event) {
    event.preventDefault();
  };
  document.ondragend = function (event) {
    event.preventDefault();
  };
};
// 鼠标松开结束拖拽--先true是为了松开鼠标立即显示,设置定时器又恢复原来的hover
const endDrag = () => {
  dragging = false;
  isVisiblePopover.value = true;
  setTimeout(() => {
    isVisiblePopover.value = null;
    }, 5);
};

13、同时展开/折叠不同菜单项的el-collapse-transition折叠面板

可以给每个选项加一个flag,每次点击详情展开时判断当前flag是否存在,不存在则添加,存在则判断true或false取反

// 打开关闭折叠面板
const openItem = (item) => {
  // 动态添加事件的折叠展开flag
  if (!item?.flag) item.flag = false;
  if (item?.flag == false) {
    item.flag = true;
  } else item.flag = false;
};

14、 富文本内容显示、默认样式及图片点击放大

内容显示使用v-html

<div class="rich-text-content" 
    v-html="item?.content" 	 
    @click="showImg($event, item)">
</div>
<el-image-viewer
	v-if="item?.imgShow"
	:url-list="item?.pictureList"
	@close="item.imgShow = false"
/>

富文本框添加的表格无文本框,图片默认移入是箭头,可添加:

:deep(img) {
	cursor: pointer;
}
:deep(table) {
	border: 1px solid #ccc;
	border-collapse: collapse;    //不设置单元格和边框线分离
	align-content: center;
    th {
        border-right: 1px solid #ccc;
	}
	td {
		border: 1px solid #ccc;
	}
 }

图片点击放大预览,使用方法showImg,分为两种情况,单图和多图可预览切换。

(1)单图:直接抓取富文本中的img标签,拿到src:
const showImg = (e, item) => {
  if (e?.target?.tagName == "IMG") {
    picView = e?.target?.src;
    item.imgShow = true;
  }
};
(2)多图:调接口时,获取每个item的图片列表:
if (code == 200) {
    data.value?.map((dataItem) => {
      dataItem.pictureList = [];
      let imgList= dataItem?.content?.split("img");
      imgList?.map((item) => {
        if (item?.indexOf("src") != -1) {
          dataItem?.pictureList?.push(
            item.split('" alt')[0].replace(/ src=/g, "").replace('"', "")
          );
        }
     });
  });
}

再在点击预览遍历到当前图片路径,并把前后图片数组拼接:

const showImg = (e, item) => {
  if (e?.target?.tagName == "IMG") {
    let tempBefore = [];
    let tempAfter = [];
    if (item?.pictureList?.length) {
      for (let i = 0; i < item?.pictureList?.length; i++) {
         if (item?.pictureList?.[i] == e?.target?.src) {
           tempBefore = item?.pictureList?.slice(0, i);
           tempAfter = item?.pictureList?.slice(i, item?.pictureList?.length);
           break;
         }
      }
    }
    item.pictureList = [...tempBefore];
    item.imgShow = true;
  }
};

15、动态路由跳转菜单项,接口获取可选菜单再匹配

使用动态路由,但菜单项需获取当前可选菜单:需要先await获取菜单列表,再用获取的菜单标识去匹配全部菜单项,拿到可选菜单显示,用checkMenu唯一标识

<div
v-for="(item, key) in currentDepartMenu"
	:key="key"
	@click="handleClickMenu(menus?.[item?.menuId])"
>{{ item?.moduleName }}</div>
<component :is="checkedMenu?.componentName"></component>

// 全部菜单
export const menus = {
 [menuId]: {
    moduleName: "***",
    componentName: shallowRef(组件名),
  },
  ……
};

// 点击菜单跳转
const handleClickMenu = (item) => {
  checkMenu.value = item;
};
// 获取当前菜单
const getCurrentMenu = async (id) => {
  const { code, data } =
    await api.getCurrentMenu(id);
  if (code == 200) {
    currentMenu.value = data;
    checkMenu.value = menus?.[data?.[0]?.menuId];
  }
};

onMounted(async () => {
  await getCurrentMenu();
  // 需先获取菜单列表,否则影响预览
  if (route.query?.menuId) {
    checkMenu.value = menus?.[route.query?.menuId];
  }
});

16、如果在详情页里有多个tab,tab想要互相跳转可用provide/inject

//在点击切换tabs的详情页定义provide:
import { ref, provide } from "vue";
provide("checkedMenu", (activeTab) => {
  handleClickMenu(activeTab);
}

//在需要点击切换至其他同级tab的页面inject:
import { ref, inject } from "vue";
const checkedMenu = inject("checkedMenu");
//---在点击方法里调用:
checkedMenu("要跳转的tab标识,可以是name,都可以看自己写");

 

二、CSS样式类

1、兄弟div盒子实现并排

1、设置其父容器盒子的样式为 display: flex; (常用)

2、两个盒子都用float:left,再用定位调整。(脱离文档流)

2、多个兄弟盒子设置样式简单方式

如果使用v-for遍历,可以利用index和个数n,用三目运算符来写样式:

        :style="marginRight: (index+1) % n == 0 ? '0' : '5px' "

3、用el-input写登录输入框出现下边框消失

方法1:

        先看样式里面有没有设置相关样式,比如我遇到的问题就是设置了.el-input__wrapper的padding,这个时候按照浏览器的像素,eg:Edge1440宽来调整padding就可以调好。

方法2:

.el-input__wrapper {
  @media screen and (max-width: 1440px) {
    padding:***;
  }
}

4、el-popover卡片存在默认样式

<el-popover
    placement="bottom"
    trigger="hover"

    :width="250"
    popper-class="xxx-popper"//给该类设置以下样式(需在非scoped设置)
>

.xxx-popper {
    border: none !important;
    padding: 0 !important;
}

5、 el-popover卡片如果设置了圆角,内部头div不设置圆角,又会变成直角覆盖

border-radius: 8px !important;    //.xxx-popper
border-radius: 8px 8px 0px 0px;    //.xxx-popper-header

6、常见居中的样式设置

(1)父div里的子div(子div居中显示):设置父div即可
.father {
    height: 60px;
    display: flex;
    justify-content: center;
    align-content: center;
}
(2)div或span中的文字居中:
line-height: 30px;		//设盒子高度则垂直居中
text-align: center;
(3)如果兄弟标签宽度各不相同,但需设置div的文本居中(此时填充padding失效,水平是默认的文字宽度):可以将div文本再放入内部span标签内设置
div {
    line-height: 30px;		//div设置行高垂直居中
    span {
        padding: 0 16px;	//内部span设置左右padding或者margin水平居中
    }
}
(4)grid内容居中
display: grid;
place-items: center;

7、多个div的flex布局常用

display: flex;                  //可让内部子元素div横向排列
flex-wrap: wrap;            //可以换行,属性值wrap-reverse是行内有序,行逆序
align-items: flex-end;    //若只有一行可这样设置置于容器底部(需确认高度设置)

8、 v-for遍历的多个div菜单项,单个菜单项的hover样式设置

鼠标移入效果,可以通过mouseenter和mouseleave获取当前菜单项的index来作为条件,获取以后通过判断index,拿到当前菜单项,设置样式

:class="showIndex == index ? 'hoverMenuItem': ''"

 9、一些ElementPlus的底层样式(都可以F12仔细看源码找到)

el-input 输入框的焦点::deep(.el-input__wrapper.is-focus)

el-timeline 时间线:

        <template #dot></template>    //可设置时间线上图标
:deep(.el-timeline-item__tail)    // 修改时间线的竖线样式
:deep(.el-timeline-item__dot)        // 修改时间线节点icon的位置
:deep(.el-timeline-item__content)// 修改时间线item间间距
:deep(.el-timeline-item__timestamp.is-bottom) // 修改时间线item间边距

10、 盒子内的文本 超长数字/单词超出区域,多个空格合并成1个,换行符未生效,首行缩进

word-wrap: break-word;        //单词截断

white-space: pre-wrap;        //空格不合并,换行符正常换行

text-indent: 2em;                //首行缩进

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值