工作总结之开发篇
复盘开发时遇到的一些问题
前言
就把自己遇到的,认为比较重要的提出来讲一讲
后端
- 修改两个SpringBoot项目各自的路径
因为这个涉及到映射容器内的日志到宿主机上,配合loki使用,而两个项目的日志路径,原本是有问题的,路径没有做区分,甚至还有undefined目录,所以如何改路径需要好好的斟酌。
- 计算工作时长
涉及到计算工作时长,排除午休和周末,暂时还没有要求排出节假日,如果要求的话,可能到时候需要将节假日维护进数据库。
大致思路是遍历开始时间和结束时间的每一天,遇到周六加两天,遇到周日加一天,将期间的工作时长(排除掉午休)累加起来,得到期间的工作时长。具体实现如下:
package com.workplat.administrate.util;
import com.workplat.administrate.constant.NumberConstants;
import java.time.*;
import java.util.Date;
/**
* @author stupi
*/
public class WorkTimeCalculateUtil {
// 上班时间
private static final LocalTime WORKING_START_TIME = LocalTime.of(9, 00);
// 下班时间
private static final LocalTime WORKING_END_TIME = LocalTime.of(17, 00);
// 午休开始时间
private static final LocalTime NOON_BREAK_START_TIME = LocalTime.of(11, 30);
// 午休结束时间
private static final LocalTime NOON_BREAK_END_TIME = LocalTime.of(13, 0);
public static Double getWorkTime(LocalDateTime startDateTime, LocalDateTime endDateTime, Boolean isSkip) {
Asserts.testFail(startDateTime.compareTo(endDateTime) > 0,"时间参数错误");
int diff = 0;
while (true) {
// TODO: 调休日期处理
// 是否需要跳过周六周日
if(isSkip){
if (startDateTime.getDayOfWeek() == DayOfWeek.SATURDAY) {
startDateTime = LocalDateTime.of(startDateTime.toLocalDate(), WORKING_START_TIME).plusDays(2);
}
if (startDateTime.getDayOfWeek() == DayOfWeek.SUNDAY) {
startDateTime = LocalDateTime.of(startDateTime.toLocalDate(), WORKING_START_TIME).plusDays(1);
}
if (endDateTime.getDayOfWeek() == DayOfWeek.SATURDAY) {
endDateTime = LocalDateTime.of(endDateTime.toLocalDate(), WORKING_START_TIME).plusDays(2);
}
if (endDateTime.getDayOfWeek() == DayOfWeek.SUNDAY) {
endDateTime = LocalDateTime.of(endDateTime.toLocalDate(), WORKING_START_TIME).plusDays(1);
}
}
// 跨天处理
if (startDateTime.getDayOfYear() == endDateTime.getDayOfYear()) {
int diffSecond = getDiffSecond(startDateTime, endDateTime);
diff = diffSecond + diff;
break;
}
int diffSecond = getDiffSecond(startDateTime, LocalDateTime.of(startDateTime.toLocalDate(), WORKING_END_TIME));
diff = diffSecond + diff;
startDateTime = LocalDateTime.of(startDateTime.toLocalDate(), WORKING_START_TIME).plusDays(1);
}
//总的小时数
double hours = Double.valueOf(diff) / 3600;
//保留1位小数
double finalHours = (Math.floor(hours * 10)) / 10;
int intPart = (int) finalHours;
double decimalPart = finalHours - intPart;
if(decimalPart >= NumberConstants.ZERO_POINT_FIVE){
finalHours = intPart + NumberConstants.ZERO_POINT_FIVE;
}else {
finalHours = intPart;
}
System.out.println("finalHours: " + finalHours);
return finalHours;
}
private static int getDiffSecond(LocalDateTime startDateTime, LocalDateTime endDateTime) {
LocalTime startTime = startDateTime.toLocalTime();
LocalTime endTime = endDateTime.toLocalTime();
// diff单位:秒
int diff = 0;
// 开始时间切移
if (startTime.isBefore(WORKING_START_TIME)) {
startTime = WORKING_START_TIME;
} else if (startTime.isAfter(NOON_BREAK_START_TIME) && startTime.isBefore(NOON_BREAK_END_TIME)) {
startTime = NOON_BREAK_START_TIME;
} else if (startTime.isAfter(WORKING_END_TIME)) {
startTime = WORKING_END_TIME;
}
// 结束时间切移
if (endTime.isBefore(WORKING_START_TIME)) {
endTime = WORKING_START_TIME;
} else if (endTime.isAfter(NOON_BREAK_START_TIME) && endTime.isBefore(NOON_BREAK_END_TIME)) {
endTime = NOON_BREAK_START_TIME;
} else if (endTime.isAfter(WORKING_END_TIME)) {
endTime = WORKING_END_TIME;
}
// 午休时间判断处理
if (startTime.compareTo(NOON_BREAK_START_TIME) <= 0 && endTime.compareTo(NOON_BREAK_END_TIME) >= 0) {
diff = diff + 60 * 60 + 60 * 60 / 2;
}
diff = endTime.toSecondOfDay() - startTime.toSecondOfDay() - diff;
return diff;
}
public static LocalDateTime DateToLocalDateTime(Date date){
Instant instant = date.toInstant();
ZoneId zoneId = ZoneId.systemDefault();
LocalDateTime localDateTime = instant.atZone(zoneId).toLocalDateTime();
return localDateTime;
}
public static void main(String[] args) {
WorkTimeCalculateUtil.getWorkTime(LocalDateTime.of(2022, 10, 23, 20, 30, 0),
LocalDateTime.of(2022, 10, 27, 13, 50, 0),Boolean.TRUE);
}
}
- mybatis中的if标签的条件不生效
原因:mybatis会自动将本是String类型而填充的是数字识别为Integer;对于单引号的内容会认为是字符(前者理解好像有一丢丢不对,因为交换双单引号对前者也用)
解决方法:前者将数字的引号去掉即可;后者要么将双引号和单引号的位置换一下,要么像这样'Y'.toString()
转换一下再比较
参考文章:
https://blog.csdn.net/qq_31594647/article/details/89429265
https://www.yht7.com/news/124481
https://blog.csdn.net/weixin_45237517/article/details/101530117 - mybatis-plus的逻辑删除
@TableLogic,该注解对应mybatis-plus的删除方法会改为逻辑删除 - EasyExcel
封装EasyExcel的公共导出方法;写一个Handler设置表格的自动宽度,大概就是要根据单元格的字符长度来设置单元格的宽度;写一些Converter将实体字段从标识转换为对应的文字含义。大致代码如下:
公共方法:
package com.workplat.administrate.util;
import com.alibaba.excel.EasyExcel;
import com.workplat.administrate.entity.SysConvenienceUser;
import com.workplat.administrate.excel.handler.AutoColumnWidthHandler;
import org.springframework.http.MediaType;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.List;
/**
* @author stupi
*/
public class EasyExcelUtil {
public static void export(HttpServletResponse response, Class clazz, String sheetName, String fileName, List dataList){
//生成EXCEL
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
response.setCharacterEncoding("utf-8");
//设置文件名
String fileNameURL = "";
try {
fileNameURL = URLEncoder.encode(fileName+".xlsx", "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
response.setHeader("Content-Disposition", "attachment;filename="+fileNameURL+";"+"filename*=utf-8''"+fileNameURL);
try {
EasyExcel.write(response.getOutputStream(), clazz).registerWriteHandler(new AutoColumnWidthHandler()).sheet(sheetName).doWrite(dataList);
} catch (IOException e) {
e.printStackTrace();
}
}
}
自动宽度
package com.workplat.administrate.excel.handler;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.metadata.data.CellData;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.style.column.AbstractColumnWidthStyleStrategy;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Sheet;
import org.springframework.util.CollectionUtils;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author stupi
*/
public class AutoColumnWidthHandler extends AbstractColumnWidthStyleStrategy {
private Map<Integer, Map<Integer, Integer>> CACHE = new HashMap<>();
@Override
protected void setColumnWidth(WriteSheetHolder writeSheetHolder, List<WriteCellData<?>> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
boolean needSetWidth = isHead || !CollectionUtils.isEmpty(cellDataList);
if (needSetWidth) {
Map<Integer, Integer> maxColumnWidthMap = CACHE.get(writeSheetHolder.getSheetNo());
if (maxColumnWidthMap == null) {
maxColumnWidthMap = new HashMap<>();
CACHE.put(writeSheetHolder.getSheetNo(), maxColumnWidthMap);
}
Integer columnWidth = this.dataLength(cellDataList, cell, isHead);
if (columnWidth >= 0) {
if (columnWidth > 255) {
columnWidth = 255;
}
Integer maxColumnWidth = maxColumnWidthMap.get(cell.getColumnIndex());
if (maxColumnWidth == null || columnWidth > maxColumnWidth) {
maxColumnWidthMap.put(cell.getColumnIndex(), columnWidth);
writeSheetHolder.getSheet().setColumnWidth(cell.getColumnIndex(), columnWidth * 256);
}
}
}
}
private Integer dataLength(List<WriteCellData<?>> cellDataList, Cell cell, Boolean isHead) {
if (isHead) {
return cell.getStringCellValue().getBytes().length;
} else {
CellData cellData = cellDataList.get(0);
CellDataTypeEnum type = cellData.getType();
if (type == null) {
return -1;
} else {
switch (type) {
case STRING:
return cellData.getStringValue().getBytes().length + 1;
case BOOLEAN:
return cellData.getBooleanValue().toString().getBytes().length;
case NUMBER:
return cellData.getNumberValue().toString().getBytes().length;
default:
return -1;
}
}
}
}
}
字段标识转换(在实体注解引用即可)
package com.workplat.administrate.excel.converter;
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.data.ReadCellData;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.metadata.property.ExcelContentProperty;
import com.alibaba.excel.util.StringUtils;
import com.google.common.collect.ImmutableMap;
import com.workplat.administrate.enums.WorkStatus;
import java.text.ParseException;
import java.util.Map;
import java.util.Objects;
/**
* @author stupi
*/
public class WorkStatusConverter implements Converter<Integer> {
public static final Map<String,Integer> WORK_STATUS_MAP = ImmutableMap.<String,Integer>builder()
.put("工作中",0)
.put("请假中",1)
.build();
@Override
public Class supportJavaTypeKey() {
return Integer.class;
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return CellDataTypeEnum.STRING;
}
@Override
public Integer convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws ParseException {
if (StringUtils.isBlank(cellData.getStringValue())){
return 0;
}
return WORK_STATUS_MAP.getOrDefault(cellData.getStringValue(),0);
}
@Override
public WriteCellData<?> convertToExcelData(Integer value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
if (Objects.nonNull(value)){
return new WriteCellData(WorkStatus.fromKey(value).getDesc());
}
return null;
}
}
- 表格的导入
使用poi对表格逐行读取,封装到实体类,批量保存到库里。 - 减少魔法值
平时在代码的编写中,对于一些状态值,都尽量用枚举去表示,减少魔法值的出现 - 优化文件的上传与下载
特别是在下载是要正确显示文件的名称,思路是在响应头中正确设置utf8的编码,期间还遇到swagger无法将调用的二进制流转为文件的问题(要正确设置swagger识别的响应媒体类型),还有postman只能下载不超过50M的文件的问题(一直报与远程服务器断开连接,大冤种一直找不到错误的原因),代码如下:
package com.workplat.administrate.controller;
import com.mongodb.client.MongoClient;
import com.mongodb.client.gridfs.GridFSBucket;
import com.mongodb.client.gridfs.model.GridFSFile;
import com.workplat.administrate.common.CommonResult;
import com.workplat.administrate.dto.FileEntity;
import com.workplat.administrate.file.service.FileEntityService;
import com.workplat.administrate.file.service.impl.FileEntityConverter;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.bson.types.ObjectId;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.gridfs.GridFsResource;
import org.springframework.data.mongodb.gridfs.GridFsTemplate;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.util.*;
@Controller
@RequestMapping("/api/attachment")
@Slf4j
public class AttachmentApi {
@Autowired
private FileEntityConverter fileEntityConverter;
@Autowired
private FileEntityService fileEntityService;
@Autowired
GridFsTemplate gridFsTemplate;
private MongoClient mongoClient;
private GridFSBucket gridFSBucket;
/**
* 文件上传
* @param file
* @return
*/
@PostMapping(value = "/upload")
@ResponseBody
public CommonResult upload(@RequestParam MultipartFile file) throws Exception {
// 获得提交的文件名
String fileName = file.getOriginalFilename();
// 获得文件输入流
InputStream ins = file.getInputStream();
// 获得文件类型
String contentType = file.getContentType();
//保存文件信息
FileEntity fileEntity = new FileEntity();
try {
//如果文件大小大于15M则采用GridFS 方式存储
if(file.getSize() > 15*1024*1024){
// 将文件存储到mongodb中,mongodb 将会返回这个文件的具体信息
long startTime = System.currentTimeMillis();
ObjectId fileInfo = gridFsTemplate.store(ins, fileName, contentType);
long endTime = System.currentTimeMillis();
log.info("gridFs上传时间:{}s",(endTime - startTime)/1000);
fileEntity.setId(fileInfo.toString());
}else{
fileEntity.setContent(file.getBytes());
}
fileEntity.setFileType(contentType);
fileEntity.setFileName(fileName);
fileEntity.setCreateTime(new Date());
fileEntity.setFileSize(file.getSize());
//存入md5值
fileEntity.setMd5(DigestUtils.md5Hex(file.getBytes()));
if(Objects.nonNull(fileName)){
String[] splitArr = fileName.split("\\.");
if (splitArr.length != 0 && Objects.nonNull(splitArr[splitArr.length - 1])){
fileEntity.setExt(splitArr[splitArr.length - 1]);
}
}
fileEntity = fileEntityService.create(fileEntity);
}catch (Exception e){
log.error("上传失败,文件名:{}",fileName);
return CommonResult.failed("上传失败!");
}
return CommonResult.success(fileEntity);
}
/**
* 文件下载
* @param fileId
* @param response
*/
@ApiOperation(value = "下载文件",produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
@GetMapping(value = "/download")
public CommonResult download(String fileId, HttpServletResponse response,HttpServletRequest request) throws Exception{
FileEntity fileEntity = fileEntityService.findById(fileId);
if(fileEntity == null){
return CommonResult.failed("文件不存在");
}else{
OutputStream os = response.getOutputStream();
String fileName = fileEntity.getFileName();
String fileNameURL = URLEncoder.encode(fileName, "UTF-8");
//multipart/form-data:指定传输数据为二进制数据,例如图片、mp3、文件也可以是表单键值对
response.setContentType("multipart/form-data");
response.setCharacterEncoding("utf-8");
//2.设置文件头:最后一个参数是设置下载文件名(假如我们叫a.pdf) 告诉浏览器以附件形式下载文件
//response.setHeader("Content-Disposition", "attachment;filename="
// + URLEncoder.encode(fileName, "UTF-8"));该方式使用postman调用不能还原为中文,浏览器可以,下面的方式两种都是正确的中文
response.setHeader("Content-Disposition", "attachment;filename="+fileNameURL+";"+"filename*=utf-8''"+fileNameURL);
if(null != fileEntity.getContent()){
os.write(fileEntity.getContent());
}else{
//_id为主键
GridFSFile fs = gridFsTemplate.findOne(Query.query(Criteria.where("_id").is(fileId)));
GridFsResource fsdb = gridFsTemplate.getResource(fs);
byte bs[] = new byte[1024];
long total = 0;
long times = 0;
long len = 0;
try {
InputStream inputStream = fsdb.getInputStream();
while ((len = inputStream.read(bs))>0){
total += len;
times++;
log.info("fileEntity:{},total:{},times:{}",
fileEntity.getFileSize(),total,times);
os.write(bs);
}
inputStream.close();
}catch (IOException e){
log.error("e:{}",e);
}
}
os.flush();
os.close();
return CommonResult.success();
}
}
/**
* 删除证照
* @param id
* @return
*/
@RequestMapping(value = "/delete/{id}")
@ResponseBody
public CommonResult delFile(@PathVariable("id") String id){
fileEntityService.delete(id);
return CommonResult.success();
}
}
- 新增business微服务且日常负责将测试环境的代码合并到正式环境还有负责测试环境与正式环境的构建等的排错(经常代码被冲掉啊)
- 延时服务时长抵消请假时长加事务加抛出自定义报错提示用户
- 拦截器放掉一些swagger的资源路径
前端
- 模态框在关闭之前采用手动清除值时会闪现一下初始状态
研究了一下antd vue的官网,决定使用模态框的after-close回调一个clearAll方法来来做清除动作,不会出现闪现一下初始模态框的情况,之前使用destroyOnClose="true"属性并不能成功。
关键代码:
<Modal :visible="props.show" title="修改" @ok="onComplate" @cancel="onCancel" width="1000px" :after-close="clearAll">
...
const clearAll = () => {
reduceDuration.value = undefined;
remark.value = undefined;
}
- 为组件的事件传入自定义参数
关键代码:
@change="(val1,val2) => onChange(val1,val2,0)"
...
const onChange = async(val1,val2,which) => {
console.log("===val1、val2和which",val1,val2,which);
let newBt = leaveDetail.value.sysLeave.beginTime;
let newEt = leaveDetail.value.sysLeave.endTime;
console.log("===开始时间和结束时间:",newBt,newEt);
if (newBt && newEt) {
let flag = startEndJudgment(newBt, newEt);
if (!flag) {
let {data:durationUnitHour} = await workTimeCalc({
beginTime: newBt,
endTime: newEt,
});
leaveDetail.value.totalDurationUnitHour = durationUnitHour + "小时";
} else {
return;
}
}
};
- vue3中如何写监听器,监听多个字段(包含结束时间不能早于开始时间)
关键代码:
watch(
[() => leaveDetail.value.sysLeave.beginTime, () => leaveDetail.value.sysLeave.endTime],
(newValue, oldValue) => {
console.log("====newValue:", newValue);
console.log("====oldValue:", oldValue);
let oldBT = oldValue[0];
let oldET = oldValue[1];
let newBt = newValue[0];
let newET = newValue[1];
let bTime = null;
let eTime = null;
//年月日时分秒由时间戳转换为秒
if (oldET) {
eTime = new Date(oldET).getTime() / 1000;
}
if (oldBT) {
bTime = new Date(oldBT).getTime() / 1000;
}
if (newET) {
eTime = new Date(newET).getTime() / 1000;
}
if (newBt) {
bTime = new Date(newBt).getTime() / 1000;
}
if (bTime && eTime) {
let duration = parseInt((eTime - bTime) / 60 / 60);
let durationUnitDay = (duration / 24).toFixed(2);
leaveDetail.value.sysLeave.totalDuration = durationUnitDay + "天";
leaveDetail.value.totalDurationUnitDay = durationUnitDay + "天";
console.log("====duration:", duration, durationUnitDay);
}
},
{ deep: true,immediate:true }
);
- 子组件向父组件传值
子组件:
<template>
<div>
<Modal :visible="props.show" title="审批通过确认" @ok="onComplate" @cancel="onCancel" :after-close="clearAll">
<template #footer>
<a-button @click="onCancel" key="back">取消</a-button>
<a-button @click="onComplate" key="submit" type="primary" :loading="loading">审批通过</a-button>
</template>
<div>
<div class="popup-row">
<div class="popup-key">是否需要下级审批:</div>
<a-space class="popup-value" direction="vertical">
<a-radio-group v-model:value="need" :options="[
{
value: 1,
label: '是',
},
{
value: 0,
label: '否',
},
]" />
</a-space>
</div>
<div class="popup-row" v-if="need">
<div class="popup-key">选择下级审批人:</div>
<a-select v-model:value="applier" class="popup-value" :options="usersOptions" />
</div>
<div class="popup-row" v-if="need">
<div class="popup-key">添加抄送人:</div>
<a-select v-model:value="CC" class="popup-value" :options="usersOptions" />
</div>
</div>
</Modal>
</div>
</template>
<script setup>
import { getUserList } from "@/api/property";
import { Modal } from "ant-design-vue";
import { defineProps, defineEmits, ref } from "vue";
import "@/assets/css/popup-form.css";
const props = defineProps(["show"]);
const emits = defineEmits(["complate", "cancel"]);
const applier = ref();
const CC = ref();
const need = ref(1);
const onComplate = () => {
emits("complate", {
need: need.value,
cc: CC.value,
applier: applier.value,
applierName: getName(applier.value),
CCName: getName(CC.value),
});
// need.value = 1;
// applier.value = undefined;
// CC.value = undefined;
};
const getName = (id) => {
const itemObject = usersOptions.value.find((x) => x.value === id);
console.log("itemObject", usersOptions.value);
if (itemObject) {
return itemObject.label;
} else {
return "";
}
};
const onCancel = () => {
emits("cancel");
// need.value = 1;
// applier.value = undefined;
// CC.value = undefined;
};
const usersOptions = ref([]);
(async () => {
const { data: result } = await getUserList();
usersOptions.value = result.map((item) => {
return {
value: item.id,
label: item.fullName,
};
});
})();
const clearAll = () => {
need.value = 1;
applier.value = undefined;
CC.value = undefined;
}
</script>
<style scoped>
.popup-key {
width: 150px !important;
}
</style>
父组件引用的地方
//该button打开模态框
<a-button
type="link" size="small"
ghost
@click="approveLeave(record.id, record.detailId)"
>通过审批
</a-button>
// 通过isAgree使得模态框 展示出来
const approveLeave = async (id, detailId) => {
console.log("通过时的id和detailId:", id, detailId);
agreeData.value.id = id;
agreeData.value.detailId = detailId;
isAgree.value = true;
};
//父组件引用子组件的地方
<Approval @cancel="isAgree = false" @complate="onApproval" :show="isAgree" />
//绑定的通过方法
const onApproval = async (data) => {
console.log("通过时的data", data);
//立马关闭弹窗,防止弹窗重置值的时候,误以为是新弹窗。这个应该没有用了,被前面的after-close取代了
isAgree.value = false;
if (sourceOne.value == "leave") {
const {
code,
data: result,
message: messageStr,
} = await agreeLeave({
id: agreeData.value.detailId,
sysLeaveId: agreeData.value.id,
carbonCopyUserid: data.cc,
carbonCopyUser: data.CCName,
lowerLevelUser: data.applierName,
lowerLevelUserid: data.applier,
isLowerLevelApproval: data.need,
status: 2,
});
if (code == 200) {
message.success("操作成功");
onHandle();
} else {
message.error(messageStr);
}
} else if (sourceOne.value == "delay") {
const {
code,
data: result,
message: messageStr,
} = await agreeDelay({
id: agreeData.value.detailId,
sysDelayServiceId: agreeData.value.id,
carbonCopyUserid: data.cc,
carbonCopyUser: data.CCName,
lowerLevelUser: data.applierName,
lowerLevelUserid: data.applier,
isLowerLevelApproval: data.need,
status: 2,
});
if (code == 200) {
message.success("操作成功");
onHandle();
} else {
message.error(messageStr);
}
}
};
讲一下,是如何子组件是如何向父组件传的值:
子组件通过const emits = defineEmits(["complate", "cancel"]);
定义了一个自定义事件complate,然后给模态框的确定绑定了一个方法@ok="onComplate"
,在点击确定的时候就会触发onComplate,而在该方法里面调用了自定义事件
emits("complate", {
need: need.value,
cc: CC.value,
applier: applier.value,
applierName: getName(applier.value),
CCName: getName(CC.value),
});
去传送数据,而在父组件值该complate绑定了onApproval,onApproval的data参数就是complate从子组件传出来的,这样就达到了子向父传值的目的了。
5. vue的路由使用params和query传值的区别
如果使用params传值,且在路由中没有进行配置,这样就不会体现在地址栏上,类似于http的post请求,但是当你刷新页面的时候,这些值会丢失,引起页面报错,又一个但是,在路由中对params进行了配置,类似于
const onLeaveDetail = async (id) => {
router.push({
name: "LeaveDetail",
params: {
id,
authority: 1,
},
query: {
//query+param
activeTabKey: dealState.value,
},
});
};
...
{
path: "/achievement/leave/detail/:id/:authority",
name: "LeaveDetail",
meta: {
titles: ["行政管理", "绩效管理", "我的绩效", "请销假"],
},
component: () => import("../views/Achievement/LeaveDetail/LeaveDetail.vue"),
}
再去刷新页面,这些params的值并不会丢,会体现在地址栏一级一级的路径中;如果使用query传值,是一定会体现在地址栏的?后面,并且刷新页面也不会丢值报错,还有一个问题,nginx会丢失params传的值,query的就不会,因此推荐query进行传值。
6. 接口异步
在众多图表的页面请求数据的时候,并发请求,而不是一个请求完了才是下一个,起关键作用的是async。
关键代码:
const onlyFlush = () => {
doNaturalTotal();
doGetEvaluateNum();
doGetHandlingList();
doGetHandlingTotalList();
doGetChannel();
doGetRankingList();
};
onst doNaturalTotal = async () => {
// 取号数/办件数
const { data: result } = await naturalTotal({
areaCode: deptId.value,
type: '1',
year: year.value,
});
nowQhNum.value = result.nowQhNum == null ? '0' : result.nowQhNum;
nowTotal.value = result.nowTotal == null ? '0' : result.nowTotal;
qhNum.value = result.qhNum == null ? '0' : result.qhNum;
total.value = result.total;
}
const doGetEvaluateNum = async () => {
// 评价数
const { data: result2 } = await getEvaluateNum({
areaCode: deptId.value,
type: '1',
year: year.value,
});
nowEvaluate.value = result2.nowEvaluate == null ? '0' : result2.nowEvaluate;
evaluateTotal.value = result2.total;
}
const doGetHandlingList = async () => {
// 区级办件总量趋势分析
const myChart = echarts.init(trendEchartsBox.value);
myChart.setOption(trendOption.value);
const { data: result4 } = await getHandlingList({
areaCode: deptId.value,
type: '1',
year: year.value,
});
myChart.setOption({
series: [
{
type: 'line',
data: [
result4[1].handleNum,
result4[3].handleNum,
result4[5].handleNum,
result4[7].handleNum,
result4[9].handleNum,
result4[11].handleNum,
],
},
],
});
}
const doGetHandlingTotalList = async () => {
// 政务服务大厅办件总量
const handlingMyChart = echarts.init(handlingEchartsBox.value);
handlingMyChart.setOption(handlingOption.value);
const { data: result3 } = await getHandlingTotalList({
areaCode: deptId.value,
type: '1',
year: year.value,
});
zwTotal.value = 0;
for (var i = 0; i < result3.length; i++) {
zwTotal.value += Number(result3[i].num);
}
handlingMyChart.setOption({
series: [
{
type: 'bar',
data: [result3[3].num, result3[0].num, result3[1].num, result3[2].num],
},
],
});
}
const doGetChannel = async () => {
// 区级办件渠道统计分析
const channelMyChart = echarts.init(channelEchartsBox.value);
channelMyChart.setOption(channelOption.value);
const { data: result5 } = await getChannel({
areaCode: deptId.value,
type: '1',
year: year.value,
});
channelMyChart.setOption({
series: [
{
name: '旗舰店',
type: 'bar',
stack: 'Ad',
color: 'yellow',
emphasis: {
focus: 'series',
},
data: [
result5[1].flagShip,
result5[3].flagShip,
result5[5].flagShip,
result5[7].flagShip,
result5[9].flagShip,
result5[11].flagShip,
],
},
{
name: '大厅接件',
type: 'bar',
stack: 'Ad',
emphasis: {
focus: 'series',
},
data: [
result5[1].hall,
result5[3].hall,
result5[5].hall,
result5[7].hall,
result5[9].hall,
result5[11].hall,
],
},
{
name: 'i相伴',
type: 'bar',
// stack: 'Ad',
emphasis: {
focus: 'series',
},
data: [
result5[1].program,
result5[3].program,
result5[5].program,
result5[7].program,
result5[9].program,
result5[11].program,
],
},
{
name: '自助机',
type: 'bar',
// stack: 'Ad',
emphasis: {
focus: 'series',
},
data: [
result5[1].selfHelp,
result5[3].selfHelp,
result5[5].selfHelp,
result5[7].selfHelp,
result5[9].selfHelp,
result5[11].selfHelp,
],
},
{
name: '集聚柜',
type: 'bar',
stack: 'Ad',
emphasis: {
focus: 'series',
},
data: [
result5[1].outData,
result5[3].outData,
result5[5].outData,
result5[7].outData,
result5[9].outData,
result5[11].outData,
],
},
],
});
}
const doGetRankingList = async () => {
// 高频事件TOP10
const { data: result6 } = await getRankingList({
areaCode: deptId.value,
type: '1',
year: year.value,
});
busList.value = result6;
}
- onMounted与nextTick
onMounted中一般写对接口的调用,nextTick在本项目中的作用不是很明显,这里贴一下它的作用:
https://blog.csdn.net/m0_54850604/article/details/123304129
https://blog.csdn.net/a654540233/article/details/107245152/
https://zhuanlan.zhihu.com/p/387645539 - 弹性布局
现在仍然记得,弹性布局分为主轴和侧轴,默认水平主轴起点在左边,垂直侧轴(起点默认在上边),可以改,这是替代浮动的利器,主要用过居左、居右和间距相等这些用法,贴一下资料:
基本的了解:
https://www.cnblogs.com/yszr/p/9339809.html
flex按比例分配空间:
https://www.w3cschool.cn/cssref/css3-pr-flex.html
https://www.cnblogs.com/guangzan/p/10291885.html
justify-content和align-items(在线测试网址:https://www.runoob.com/try/playit.php?f=playcss_justify-content&preval=space-between):
https://www.runoob.com/cssref/css3-pr-justify-content.html
https://zhuanlan.zhihu.com/p/404686707
https://blog.csdn.net/qq_41809113/article/details/121869338
https://www.runoob.com/cssref/css3-pr-align-items.html
https://zhuanlan.zhihu.com/p/376077719
https://www.jianshu.com/p/20355ae92bda
flex-direction和flex-wrap:
http://t.zoukankan.com/slovey-p-9243755.html
https://www.cnblogs.com/leviAckman/p/16334034.html - 各种居中
传统的css属性;text-align和vertical-align;弹性布局的两个方向上的居中;利用行高居中。
https://www.zhangshilong.cn/work/266060.html
https://www.php.cn/css-tutorial-411747.html
https://www.jianshu.com/p/4c39d633a843
实在不行就用相对定位和绝对定位来假居中:
https://www.runoob.com/w3cnote/css-position-static-relative-absolute-fixed.html - v-model与v-bind的区别
两者对于数据来讲都是响应式的,但是前者叫做双向绑定,一般用于一些表单组件,例如输入框 - ant vue日期组件
<a-date-picker
踩坑
有两个格式化属性:format和value-format,一个负责显示日期的格式化,一个负责实际值,比如传给后台的格式化 - 下载文件时$event的妙用
<a
v-for="(item, index) in fileList"
@click="downLoad($event)"
:data-file-id="item.uid"
:date-file-name="item.name"
>
{{ item.name }}
</a>
...
const downLoad = (e) => {
console.log("eee", e);
let fileName = e.currentTarget.getAttribute("date-file-name");
let fileId = e.currentTarget.getAttribute("data-file-id");
axios
.get("/api/attachment/download?fileId=" + fileId, { responseType: "blob" })
.then((res) => {
downLoadFile(res, fileName);
});
};
绑定for循环每个组件的特殊值。
13. 下载方法增加兼容判断
因为antd vue的下载组件,对后台读取的文件id和刚刚上传的文件id的处理方式不一样,所以要兼容处理.
关键代码:
<a-upload
:disabled="!isNew"
name="file"
:multiple="true"
v-model:fileList="fileList"
:customRequest="customRequest"
@change="handleUpload"
@download="handleDownload"
:show-upload-list="{ showDownloadIcon: true, showRemoveIcon: true }"
>
<div class="clickUpload">点击上传</div>
</a-upload>
...
const handleDownload = (file) => {
debugger
console.log("===file:",file);
let id = '';
if (file.response) {
id = file.response.data.id;
}else{
id = file.uid;
}
axios.get(`/api/attachment/download?fileId=${id}`, { responseType: "blob" }).then((res) => {
downLoadFile(res, file.name)
});
}
- antd vue的表格的宽度调整(钉死)
因为这个表格的表头宽度之前总是有各种各样的显示问题,老是自动换行,或者宽度不够,显示起来非常难看,最终选择将每列宽度钉死,超出显示点点点,做法如下:
关键代码:
<a-table
size="small"
:columns="columns"
:data-source="dataSource"
:pagination="pagination"
v-show="sourceOne == 'leave'"
:scroll="{ x: '100%'}"//这里一定要设置为100%
>
...
{
title: '单据编号',
dataIndex: 'billNumber',
width: 115,//宽度后面直接跟数字,表示像素,不要加任何引号
align: 'center',
ellipsis: true,//不加此行显示点点点,会直接换行
},
最后,留一些列不设置宽度,宽度弹性渲染
15. antd vue的栅格行标签有垂直居中属性
关键代码:
<a-row justify="start" align="middle">
- 控制文件上传按钮的宽度
当文件名过长时,会显示点点点。
关键代码:
//拒绝组件文件名过长显示点点点 ps:加上my-popup-row,不会污染全局
.my-popup-row .ant-upload-span .ant-upload-list-item-name{
width: 100px;
}
.my-popup-row .ant-space-item>span{
width: 150px;
display: inline-block;
}
- 月份组件不显示头部的年份
月份组件的选择幕布发现是动态生成的,最终发现可以利用全局样式去隐藏年份区域
关键代码:
main.less
//月份组件去掉头部的年份显示
.ant-picker-month-panel .ant-picker-header{
display: none;
}
main.js
import "@/assets/css/main.less";
- js的splice用法
略 - 一个规律,对于antd vue的组件,给他写css样式,会在该组件的某层div上生效,还有TreeSelect组件的id不能重复
- 在请求后台接口时,如果有字段的值是undefined,那么这个字段不会传给后台,连null都不会给
- 上传文件时,针对文件id的兼容性写法(因为读取的文件和刚刚上传的文件对id的处理不一样),以及校验只上传某些格式的文件
关键代码:
<a-upload
name="imgFile"
:multiple="true"
v-model:fileList="imgFileList"
:customRequest="customRequest"
@change="handleUploadImg"
:before-upload="beforeUpload"
>
<div class="clickUpload">点击上传</div>
</a-upload>
...
const handleUploadImg = (info) => {
imgFileIds.value = [];
console.log("===handleUploadImg的参数info:",info);
console.log("===imgFileList:",imgFileList.value);
if (info.file.status !== "uploading") {
info.fileList.forEach((item) => {
if(item.response){
imgFileIds.value.push(item.response.data.id);
}else{
imgFileIds.value.push(item.uid);
}
});
}
};
const beforeUpload = file => {
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png' || file.type === 'image/jpg';
if (!isJpgOrPng) {
message.error('请上传图片!');
}
return isJpgOrPng || Upload.LIST_IGNORE;
};
- vue几种种刷新当前页面的高级方法(含不白屏)
https://www.jianshu.com/p/037a06ec20fb
https://blog.csdn.net/yaxuan88521/article/details/123307992
https://www.cnblogs.com/panwudi/p/16699277.html - 导航菜单的默认路由(可不管)
- 为实现在同一页面,根据不同类型显示不同的面包屑,采用了两种方法在进入路由之前做一些事情
面包屑核心是用meta.titles作为显示的值
第一种:
//路由代码的定义
{
path: "/decision/ew/comprehensive-list/:type",
name: "EventWarningComprehensiveList",
meta: {
titles: ["决策分析", "风险预警", "事项预警","xx"],
},
beforeEnter: (to, from, next) => {
let listType = to.params.type;
if(listType == 0){
to.meta.titles = ["决策分析", "风险预警", "事项预警","事项办理压缩率"];
}else if(listType == 1){
to.meta.titles = ["决策分析", "风险预警", "事项预警","即办件事项"];
}else if(listType == 2){
to.meta.titles = ["决策分析", "风险预警", "事项预警","不见面情况"];
}else if(listType == 3){
to.meta.titles = ["决策分析", "风险预警", "事项预警","最多跑一次事项情况"];
}
next();
},
component: () => import("../views/Decision/EventComprehensiveList/EventComprehensiveList.vue"),
}
第二种:
//在<script setup>的语法糖的情况下,只能这样子在进入路由之前曲线救国
<script >
import { defineComponent } from 'vue';
export default defineComponent({
beforeRouteEnter(to, from, next) {
let listType = to.params.type;
if(listType == 0){
to.meta.titles = ["决策分析", "风险预警", "办件预警","差评列表"];
}else if(listType == 1){
to.meta.titles = ["决策分析", "风险预警", "办件预警","超期列表"];
}else if(listType == 2){
to.meta.titles = ["决策分析", "风险预警", "办件预警","即将超期列表"];
}
next();
}
})
</script>
- 污染分页的样式
.filter-value,
.filter-card ::v-deep(.ant-select-single:not(.ant-select-customize-input)//原代码不含.filter-card会污染分页
.ant-select-selector) {
width: 310px;
height: 40px;
}
- antd vue的分页组件的踩坑
//onShowSizeChange和onChange在改变每页条数的时候都会触发,导致调用两遍接口,最终结果显示不正确
const pagination = ref({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
pageSizeOptions: ['5', '10', '20', '30', '40', '100'],
showTotal: (total) => `共 ${total} 条数据`,
// onShowSizeChange: pageSizeChange,
onChange: pageChange,
});
const pageSizeChange = (current, pageSize) => {
debugger
pagination.value.pageSize = pageSize;
pagination.value.current = 1;
// const params = {
// pageSize: pagination.value.pageSize,
// pageNum: pagination.value.current,
// areaCode: deptId.value,
// }
// getPageData(params);
};
const pageChange = (page, pageSize) => {
debugger
if(pagination.value.pageSize != pageSize){
pagination.value.current = 1;
pagination.value.pageSize = pageSize;
}else{
pagination.value.current = page;
}
const params = {
pageSize: pagination.value.pageSize,
pageNum: pagination.value.current,
areaCode: deptId.value,
}
getPageData(params); //获取列表数据
};
const getPageData = async (params) => {
const { data: result } = await getHandingWarnList({
...params
});
dataSource.value = result.dataList.list;
pagination.value.total = result.dataList.total;
badNum.value = result.badNum;
overNum.value = result.overNum;
overSoonNum.value = result.overSoonNum;
}
- tab组件结合父子路由使用
//父页面
<template>
<div class="container">
<Tab :tabs="routes" value="key" label="name" :modelValue="route.name" @change="goNext"/>
<div class="sub">
<router-view ></router-view>
</div>
</div>
</template>
<script setup>
import router from "@/router";
import { useRoute } from "vue-router";
import Tab from "@/components/Tab.vue";
const route = useRoute();
const routes = [
{
key: "HandlingWarning",
name: "办件预警",
},
{
key: "EventWarning",
name: "事项预警",
},
{
key: "PeopleFlowWarning",
name: "人流预警",
},
];
const goNext = (name) => {
router.push({
name: name,
});
};
</script>
<style scoped>
.sub {
margin-top: 24px;
}
</style>
//tab组件
<template>
<div class="tabs">
<div
:class="{
tab: true,
active: (props.value ? item[props.value] : index) === props.modelValue,
}"
v-for="(item, index) in props.tabs"
@click="onChange(props.value ? item[props.value] : index)"
:key="index"
>
{{ props.label ? item[props.label] : item }}
</div>
</div>
</template>
<script setup>
import { defineProps, defineEmits } from "vue";
const props = defineProps(["tabs", "value", "modelValue", "label"]);
const emits = defineEmits(["update:modelValue", "change"]);
const onChange = (key) => {
console.log("tab组件对应的key", key);
emits("update:modelValue", key);
emits("change", key);
};
</script>
<style lang="less" scoped>
.tabs {
display: flex;
border-radius: 4px;
overflow: hidden;
width: fit-content;
}
.tab {
width: 133px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background-color: white;
cursor: pointer;
color:#b0afb2
}
.tab:hover,
.tab.active {
background-color: #1890ff;
color:#fff;
}
// .ant-badge {
// left: 265px;
// bottom: 70px;
// }
</style>
//父子路由,默认重定向到/decision/handling-warning
{
path: "/decision",
name: "Decision",
meta: {
titles: ["决策分析", "风险预警"],
},
redirect: "/decision/handling-warning",
component: () => import("../views/Decision/Decision.vue"),
children: [{
path: "/decision/handling-warning",
name: "HandlingWarning",
meta: {
titles: ["决策分析", "风险预警","办件预警"],
},
component: () => import("../views/Decision/HandlingWarning/HandlingWarning.vue"),
}, {
path: "/decision/event-warning",
name: "EventWarning",
meta: {
titles: ["决策分析", "风险预警","事项预警"],
},
component: () => import("../views/Decision/EventWarning/EventWarning.vue"),
}, {
path: "/decision/peopleflow-warning",
name: "PeopleFlowWarning",
meta: {
titles: ["决策分析", "风险预警","人流预警"],
},
component: () => import("../views/Decision/PeopleFlowWarning/PeopleFlowWarning.vue"),
},
],
}
- 最初曾遇到windows下对于.vue文件不识别大小写的问题
虽然重命名了文件的大小写,但是git并不能正确识别,导致无法提交,需要用特殊的git命令 - 定制表格
效果:
关键代码:
//表格的列,前三行是为了显示的特殊形状
const columns = [
{
title: '',
dataIndex: '111',
},{
title: '',
dataIndex: '222',
},{
title: '',
dataIndex: '333',
},
{
title: "单据编号",
dataIndex: "billNumber",
width: 200,
align: "center",
ellipsis: true, //不加此行显示点点点,会直接换行
},
{
title: "申请时间",
dataIndex: "applyTime",
width: 200,
align: "center",
ellipsis: true,
},
{
title: "申请人",
dataIndex: "applier",
width: 150,
align: "center",
ellipsis: true,
},
{
title: "所属部门",
dataIndex: "deptName",
width: 200,
align: "center",
ellipsis: true,
},
{
title: "申请事项",
dataIndex: "applyItem",
width: 200,
align: "center",
ellipsis: true,
},
{
title: "申请内容", //请假事由
dataIndex: "reason",
align: "center",
width: 300,
ellipsis: true,
},
{
title: "审批状态",
dataIndex: "status",
width: 150,
align: "center",
ellipsis: true,
},
{
title: "待审批人",
dataIndex: "approver",
width: 150,
align: "center",
ellipsis: true,
},
{
title: "操作",
dataIndex: "action",
width: 172,
align: "center",
// width: '25%',
},
];
//样式,deep是为了穿透(vue有个默认data属性啥的),使得样式生效,border-spacing:0px 18px;是为了调整表格行之间的间距
:deep(.ant-table-thead > tr > th){
border-bottom: 3px solid #fff;
padding-top: 0;
}
:deep(.ant-table-tbody > tr > td) {
border-top: 3px solid #fff;//#f0f0f0
border-bottom: 3px solid #fff;
background-color: #fff;
}
::v-deep.ant-table table {
border-spacing:0px 18px;
background-color:#F9F9F9;
}
//表头
:deep(th:nth-child(1)){
padding: 0;
width: 20px !important;//此项生效了,100%和固定宽度,功不可没
}
:deep(th:nth-child(2)){
padding: 0;
width: 1px !important;//此项生效了,100%和固定宽度,功不可没
}
:deep(th:nth-child(3)){
padding: 0;
width: 20px !important;//此项生效了,100%和固定宽度,功不可没
}
:deep(th:nth-child(3)::before){
display: none;
}
:deep(th:nth-child(2)::before){
display: none;
}
:deep(th:nth-child(1)::before){
display: none;
}
//td
:deep(td:nth-child(2)){
padding: 0;
width: 1px !important;
}
:deep(td:nth-child(2) .line){
position: absolute;
top:0;
width: 3px;
height: 85px;
background-color:#fff;
z-index: 1;
}
:deep(td:nth-child(2) .circle){
position: absolute;
left: -6px;
top: 22%;
width: 15px;
height: 15px;
background-color: #FFFFFF;
border: 2px solid #DBDBDB;
border-radius: 50%;
z-index: 1;
}
:deep(td:nth-child(2) .tri){
position: absolute;
left: 14px;
bottom: 45%;
width: 0px;
height: 0px;
// background-color: #FFFFFF;
border-top:7px solid #F9F9F9;
border-bottom:7px solid #F9F9F9;
border-right:14px solid #FFFFFF;
width:0;
height:0;
z-index: 1;
}
:deep(td:nth-child(1)){
background-color: #F9F9F9 !important;
border: 0 !important;
}
:deep(td:nth-child(3)){
background-color: #F9F9F9 !important;
border: 0 !important;
}
//最后一个line
:deep(.ant-table-row:last-child .line){
height: 100%;
}
//第一个line
:deep(.ant-table-row:first-child .line){
height: 98px;
top: -18px;
}
总结
现在是半夜1点多,先休息啦,安