element plus地点选择器 参考链接:https://blog.csdn.net/m0_63209237/article/details/134030737
value为string数组
第一步,安装中国全省市区的数据
1 sudo npm install element-china-area-data -S
第二步,在要使用地址选择器的页面导入数据
1 2 3 4 5 6 7 import { provinceAndCityData, pcTextArr, regionData, pcaTextArr, codeToText, } from "element-china-area-data" ;
导入数据说明:
1 2 3 4 5 provinceAndCityData:省市二级联动数据,汉字+code regionData:省市区三级联动数据 pcTextArr:省市联动数据,纯汉字 pcaTextArr:省市区联动数据,纯汉字 codeToText:是个大对象,属性是区域码,属性值是汉字 用法例如:codeToText['110000' ]输出北京市
html 部分
1 2 3 4 5 6 7 8 9 10 <template> <div id="app"> <el-cascader size="large" :options="pcaTextArr" v-model="area" placeholder="请点击选择区域"> </el-cascader> </div> </template>
js 部分
1 2 import { pcaTextArr } from 'element-china-area-data' const area = ref ('' )
MP 分页实现 访问:http://localhost:8080/activity?page=1&size=1
1 2 3 4 5 6 7 8 9 10 11 12 @GetMapping public R<Page<Activity>> getAll (@RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size) { Page<Activity> activityPage = new Page <>(page, size); Page<Activity> result = activityService.page(activityPage); return R.ok(result); }
注意需要加配置项
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor () { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor (); interceptor.addInnerInterceptor(new PaginationInnerInterceptor ()); return interceptor; } }
日历实现 功能描述
在日历中展示未来的活动,使用变深颜色标记有活动的日期。当点击该日期时,显示活动详情,包括活动名称、时间和地点。
实现步骤
1. 活动日期的标记:
• 在每个日期单元格中,判断该日期是否有活动。
• 如果该日期有活动,将日期单元格的背景颜色变深(如#BFDFFF),设置成圆形,并将文字变为白色,以示区分。
• 当日期处于当前月,透明度为1;否则,透明度减半(0.5)。
2. 日期点击触发活动详情展示:
• 点击某一日期后,检查是否有活动数据。
• 如果该日期有活动数据,切换 Popover 的显示状态,以展示活动信息;如果无活动数据,则隐藏 Popover。
3. Popover 组件用于显示活动详细信息:
• 弹出活动详情,包含:
• 活动名称 :以粗体显示。
• 活动时间 :以本地时间格式显示(格式:年-月-日 时:分:秒)。
• 活动地点 :显示活动举办地点。
• Popover 弹出位置为下方(placement=”bottom”),宽度设置为200px,并且不使用teleport,以避免重定位。
4. 数据加载和格式化:
• 在组件挂载时调用getAllActivity接口,加载所有活动数据。
• 将活动按日期(startTime字段的日期部分)分组存储到activityByDate中,便于按日期查询活动。
5. 关键变量
• activityByDate:按日期组织的活动数据对象,结构为 { [key: string]: Activity[] }。
• popoverVisible:记录每个日期是否显示活动详情 Popover 的布尔状态。
代码说明
• <el-calendar>
组件展示日历,并通过 #date-cell 插槽自定义日期单元格。
• handleDateClick 函数切换 Popover 的显示状态。
• getAllActivity 异步获取活动数据,并处理数据格式。
• Activity 接口定义了活动对象的基本信息,包括 id、name、startTime、place 等字段。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 <template> <div class="pt-10 px-10"> <el-calendar> <!-- 自定义日期单元格 --> <template #date-cell="{ data }"> <div :style="{ backgroundColor: activityByDate[data.day] ? '#BFDFFF' : '', color: activityByDate[data.day] ? 'white' : '', opacity: data.type === 'current-month' ? 1 : 0.5, width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', position: 'relative' }" @click="handleDateClick(data.day)"> {{ data.day.split('-')[2] }} <!-- 显示日期的日部分 --> <!-- 动态 Popover 展示活动信息 --> <el-popover v-if="popoverVisible[data.day]" v-model:visible="popoverVisible[data.day]" width="100%" :teleported="false" > <div v-for="activity in activityByDate[data.day]" :key="activity.id"> <p><strong>{{ activity.name }}</strong></p> <p>时间: {{ new Date(activity.startTime).toLocaleString() }}</p> <p>地点: {{ activity.place }}</p> </div> </el-popover> </div> </template> </el-calendar> </div> </template> <script setup lang="ts"> import { ref, onMounted } from 'vue'; import { ElMessage } from 'element-plus'; import { Activity } from '../interfaces/Activity'; import { getAllActivity } from '../api/activity'; const loading = ref(false); const allData = ref<Activity[]>([]); const activityByDate = ref<{ [key: string]: Activity[] }>({}); const popoverVisible = ref<{ [key: string]: boolean }>({}); onMounted(async () => { loading.value = true; try { const res = await getAllActivity(); allData.value = res.data.records; // 按日期字符串组织活动 allData.value.forEach((activity) => { const date = activity.startTime.split('T')[0]; if (!activityByDate.value[date]) { activityByDate.value[date] = []; } activityByDate.value[date].push(activity); }); } catch (error) { ElMessage.error('加载活动数据失败'); } finally { loading.value = false; } }); // 点击日期显示活动详情的 Popover const handleDateClick = (day: string) => { if (activityByDate.value[day]) { // 切换 Popover 可见状态 popoverVisible.value[day] = !popoverVisible.value[day]; } else { popoverVisible.value[day] = false; } }; </script>
日期不显示
一开始将date作为Date类型使用,准备用解析日期的方式获取yyyy-MM-dd
格式的数据,但是实际上EP已经准备了对应的插槽数据,date底下就有一个.day
属性,格式好了所有的日期
date-cell插槽中数据的是固定的,就你说使用data,而不是date,否则会获取不到数据(类型:{ data: { type: ‘prev-month’ | ‘current-month’ | ‘next-month’, isSelected: boolean, day: string, date: Date } })
el-popover位置错位问题 需要将需要定位的元素作为relative,并将el-popover移动至元素下面(其实一般是使用插槽去实现,但是会与日历组件冲突)、开启teleported,最大的踩坑点在于需要是:teleported
,冒号是不能忘记的
上传获取图片 首先代码
将文件目录作为静态资源路径,以便可以通过 URL 访问到上传的图片。可以在 Spring Boot 的配置文件中指定静态资源路径。
在 Spring Boot 的配置文件中定义上传目录的路径,方便管理和更改。
1 2 3 4 5 6 7 spring: web: resources: static-locations: file:/Users/tec/Desktop/ file: upload-dir: /Users/tec/Desktop
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 package com.tec.campuscareerbackend.controller;import com.baomidou.mybatisplus.extension.plugins.pagination.Page;import com.tec.campuscareerbackend.common.R;import com.tec.campuscareerbackend.entity.Activity;import com.tec.campuscareerbackend.service.IActivityService;import jakarta.annotation.Resource;import org.springframework.beans.factory.annotation.Value;import org.springframework.web.bind.annotation.*;import org.springframework.web.multipart.MultipartFile;import java.io.File;import java.io.IOException;import java.util.List;@RestController @RequestMapping("/activity") public class ActivityController { @Value("${file.upload-dir}") private String uploadDir; @Resource private IActivityService activityService; @PostMapping("/file") public R<String> uploadFile (@RequestParam("file") MultipartFile file) { if (file.isEmpty()) { return R.error("文件不能为空" ); } try { File dir = new File (uploadDir); if (!dir.exists()) { dir.mkdirs(); } File uploadFile = new File (dir, file.getOriginalFilename()); file.transferTo(uploadFile); return R.ok("文件上传成功:" + uploadFile.getName()); } catch (IOException e) { e.printStackTrace(); return R.error("文件上传失败:" + e.getMessage()); } } }
接着安全性考虑
1 2 3 4 5 6 7 8 9 10 11 12 13 @Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addCorsMappings (CorsRegistry registry) { registry.addMapping("/**" ) .allowedOrigins("http://localhost:5173" ) .allowedMethods("GET" , "POST" , "PUT" , "DELETE" ) .allowedHeaders("*" ) .allowCredentials(true ) .maxAge(3600 ); } }
关联表 踩坑点在于必须使用@TableField(exist = false)
禁止将该属性映射到数据库表中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Data @EqualsAndHashCode(callSuper = false) @Accessors(chain = true) @TableName("activity") public class Activity implements Serializable { private static final long serialVersionUID = 1L ; @TableId(value = "id", type = IdType.AUTO) private Integer id; private String activityImage; @TableField(exist = false) private List<String> imagePaths; }
下载并打包为压缩包 文件名乱码问题 不使用默认java提供的ZipArchiveOutputStream
1 2 3 4 5 <dependency > <groupId > org.apache.commons</groupId > <artifactId > commons-compress</artifactId > <version > 1.21</version > </dependency >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 @PostMapping("/download") public ResponseEntity<ByteArrayResource> downloadAttachmentsZip (@RequestBody EmploymentDatabase employmentDatabase) { List<EmploymentDatabaseAttachment> urls = employmentDatabase.getAttachment(); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream (); try (ZipArchiveOutputStream zipOutputStream = new ZipArchiveOutputStream (byteArrayOutputStream)) { zipOutputStream.setEncoding("UTF-8" ); for (EmploymentDatabaseAttachment url : urls) { URL fileUrl = new URL (url.getFilePath()); try (InputStream inputStream = fileUrl.openStream()) { ZipArchiveEntry entry = new ZipArchiveEntry (url.getFileName()); zipOutputStream.putArchiveEntry(entry); byte [] buffer = new byte [1024 ]; int len; while ((len = inputStream.read(buffer)) > 0 ) { zipOutputStream.write(buffer, 0 , len); } zipOutputStream.closeArchiveEntry(); } } } catch (Exception e) { e.printStackTrace(); } ByteArrayResource byteArrayResource = new ByteArrayResource (byteArrayOutputStream.toByteArray()); HttpHeaders headers = new HttpHeaders (); headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=attachments.zip" ); return ResponseEntity.ok() .headers(headers) .contentLength(byteArrayResource.contentLength()) .contentType(org.springframework.http.MediaType.APPLICATION_OCTET_STREAM) .body(byteArrayResource); }
el-tree-select 注意使用multiple可以多选,并且可以通过其他命令调整大标题是否可选
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <!-- 第六行 --> <div class="flex flex-1 justify-between items-center gap-10"> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">网申链接:</p> <el-input v-model="applicationLink" placeholder="请输入链接" /> </div> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">发送人群:</p> <el-tree-select v-model="targetAudience" :data="treeData" placeholder="请点击选择发送人群" size="large" clearable :props="defaultProps" multiple show-checkbox collapse-tags class="dynamic-height-select" /> </div> </div>
配置树状选择栏部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 export const targetAudience = ref<any >([]);export const treeData = ref ([ { label : '21级本科' , children : [ { label : '电子信息2101' , value : '电子信息2101' }, { label : '电子信息2102' , value : '电子信息2102' }, { label : '电子信息2103' , value : '电子信息2103' }, { label : '电子信息2104' , value : '电子信息2104' }, { label : '软件工程2101' , value : '软件工程2101' }, { label : '软件工程2102' , value : '软件工程2102' } ] }, { label : '22级本科' , children : [ { label : '电子信息2201' , value : '电子信息2201' }, { label : '电子信息2202' , value : '电子信息2202' }, { label : '电子信息2203' , value : '电子信息2203' }, { label : '电子信息2204' , value : '电子信息2204' }, { label : '软件工程2201' , value : '软件工程2201' }, { label : '软件工程2202' , value : '软件工程2202' } ] }, { label : '23级本科' , children : [ { label : '电子信息2301' , value : '电子信息2301' }, { label : '电子信息2302' , value : '电子信息2302' }, { label : '电子信息2303' , value : '电子信息2303' }, { label : '电子信息2304' , value : '电子信息2304' }, { label : '软件工程2301' , value : '软件工程2301' }, { label : '软件工程2302' , value : '软件工程2302' } ] }, { label : '24级本科' , children : [ { label : '电子信息2401' , value : '电子信息2401' }, { label : '电子信息2402' , value : '电子信息2402' }, { label : '电子信息2403' , value : '电子信息2403' }, { label : '电子信息2404' , value : '电子信息2404' }, { label : '软件工程2401' , value : '软件工程2401' }, { label : '软件工程2402' , value : '软件工程2402' } ] } ]); export const defaultProps = { children : 'children' , label : 'label' , value : 'value' };
通过key的方式对子组件强制渲染 参考:https://www.cnblogs.com/zhangycun/p/13268577.html
Vue 3 提供的响应式 API 可以通过事件触发和 ref 变量的方式更简洁地实现刷新操作,无需直接操作子组件的内部方法。你可以在父组件中使用状态(如refreshKey)来驱动子组件刷新。以下是这种更符合 Vue 3 哲学的实现方法:
在父组件中 ,定义一个 refreshKey 的 ref,并在上传成功后更改 refreshKey 的值,子组件会自动响应这个变化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import { ref } from 'vue' ;const fileInput = ref<HTMLInputElement | null >(null );const refreshKey = ref (0 ); const handleFileUpload = ( ) => { fileInput.value ?.click (); }; const onFileChange = async (event: Event ) => { const target = event.target as HTMLInputElement ; const file = target.files ?.[0 ]; if (!file) return ; const formData = new FormData (); formData.append ('file' , file); try { const response = await fetch ('http://localhost:5173/api/user-detail/importExcel' , { method : 'POST' , body : formData, }); if (response.ok ) { ElMessage .success ('文件上传成功!' ); refreshKey.value += 1 ; } else { ElMessage .error ('文件上传失败,请重试!' ); } } catch (error) { ElMessage .error ('上传过程中出现错误!' ); } };
在模板中 ,将 refreshKey 传递给 <UserDetailTable>
组件:
1 2 <UserDetailTable :key="refreshKey" :dateOrder="dateOrder" :typeOrder="typeOrder" /> <input type="file" ref="fileInput" @change="onFileChange" accept=".xls, .xlsx" style="display: none" />
在 <UserDetailTable>
子组件中 ,每次接收到新的 key 值时会自动重新加载,因此你只需要在 onMounted 钩子中调用 fetchTableData:
1 2 3 4 5 6 7 8 9 10 11 const fetchTableData = async ( ) => { loading.value = true ; try { const response = await getAllUserDetails (); tableData.value = response.data ; } finally { loading.value = false ; } }; onMounted (fetchTableData);
通过更改 key 值,Vue 会自动重新渲染 <UserDetailTable>
组件,这样可以避免直接调用子组件方法,保持代码更加简洁。
手机适配 参考链接:https://juejin.cn/post/7265129339195424827
taliwincss 常用样式 踩坑点主要有需要加上!
增加其样式的优先级,以及 elementui 部分的内含样式太多,不好轻易改,所以通过媒体查询+v-if的方式去进行手机的适配
手机存在,大屏幕不存在
1 class="!hidden md:!block"
字体调整
1 class="md:text-4xl font-extrabold text-3xl"
IndexView.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 <template> <div class="IndexView flex p-0 md:p-5"> <!-- 仅在大屏幕上显示 sidebar (大于md) --> <div class="min-w-52 hidden md:block"> <Sidebar device="pc"/> </div> <div class="md:w-[86%] w-full h-full"> <div class=""> <Header /> </div> <div class="h-[93%]"> <router-view></router-view> </div> </div> </div> </template> <script setup lang="ts"> import Sidebar from '../components/Sidebar.vue' // 引入Header import Header from '../components/Header.vue' </script> <style lang="scss" scoped> .IndexView { height: 100vh; background: #F2F8FC; } </style>
slider转为点击图标出现菜单
1 2 3 4 5 6 7 8 9 10 <template> <div class="Header rounded-none md:rounded-2xl md:mx-6 md:mb-6 m-0 px-5"> <el-icon size="20" class="!block md:!hidden mr-2" @click="showSlider"> <Menu/> </el-icon> <Sidebar device="phone" v-if="ifShowSidebar"/> <MaskLayer :ifShow="ifShowSidebar" @click="showSlider"/> </div> </template>
媒体查询 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 <template> <el-scrollbar height="100%"> <el-table :data="tableData" class="tableBox" table-layout="fixed" @selection-change="handleSelectionChange" v-loading="loading" :row-style="{ height: '80px' }"> <!-- v-if实现隐藏和显示的逻辑 --> <el-table-column v-if="isMediumScreen" type="selection" width="40" :class="{ 'hidden-md': !isMediumScreen }" /> </el-table> </el-scrollbar> </template> <script setup lang="ts"> import { ref, onMounted, onBeforeUnmount } from "vue"; // 定义是否处于中等屏幕以上的状态 const isMediumScreen = ref(false); // 更新屏幕宽度的响应式逻辑 const updateScreenSize = () => { isMediumScreen.value = window.innerWidth >= 768; }; onMounted(() => { updateScreenSize(); // 初始化时检查屏幕大小 window.addEventListener("resize", updateScreenSize); // 监听窗口变化 }); onBeforeUnmount(() => { window.removeEventListener("resize", updateScreenSize); // 组件卸载时移除监听器 }); const handleSelectionChange = (selection: any) => { console.log("Selection changed:", selection); }; </script>
Vue 3 router addroute no match found 参考链接:https://www.jianshu.com/p/95569fd0b20a
原因:当 name
名相同时,后面的路由会覆盖前端的路由。
easyexcel 报错ExceptionInInitializerError 参考链接:https://github.com/alibaba/easyexcel/issues/2040、https://github.com/alibaba/easyexcel/issues/2240
官网:https://easyexcel.opensource.alibaba.com/docs/current/quickstart/read
原因:easyexcel 版本设定低了,应该按照下面版本,修复了在JDK17中的适配问题
1 2 3 4 5 6 <dependency > <groupId > com.alibaba</groupId > <artifactId > easyexcel</artifactId > <version > 3.1.0</version > </dependency >
java自带ZipOutputStream压缩文件名为中文的乱码问题 参考链接:https://blog.csdn.net/cqstart116/article/details/44728821
原因:java自带的工具就没有设定编码的功能,所以改用apache的工具
1 2 3 4 5 6 <dependency > <groupId > org.apache.commons</groupId > <artifactId > commons-compress</artifactId > <version > 1.21</version > </dependency >
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 @RestController @RequestMapping("/employment-database") public class EmploymentDatabaseController { @Resource private IEmploymentDatabaseService employmentDatabaseService; @Resource private IEmploymentDatabaseAttachmentService employmentDatabaseAttachmentService; @PostMapping("/download") public ResponseEntity<ByteArrayResource> downloadAttachmentsZip (@RequestBody EmploymentDatabase employmentDatabase) { List<EmploymentDatabaseAttachment> urls = employmentDatabase.getAttachment(); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream (); try (ZipArchiveOutputStream zipOutputStream = new ZipArchiveOutputStream (byteArrayOutputStream)) { zipOutputStream.setEncoding("UTF-8" ); for (EmploymentDatabaseAttachment url : urls) { URL fileUrl = new URL (url.getFilePath()); try (InputStream inputStream = fileUrl.openStream()) { ZipArchiveEntry entry = new ZipArchiveEntry (url.getFileName()); zipOutputStream.putArchiveEntry(entry); byte [] buffer = new byte [1024 ]; int len; while ((len = inputStream.read(buffer)) > 0 ) { zipOutputStream.write(buffer, 0 , len); } zipOutputStream.closeArchiveEntry(); } } } catch (Exception e) { e.printStackTrace(); } ByteArrayResource byteArrayResource = new ByteArrayResource (byteArrayOutputStream.toByteArray()); HttpHeaders headers = new HttpHeaders (); headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=attachments.zip" ); return ResponseEntity.ok() .headers(headers) .contentLength(byteArrayResource.contentLength()) .contentType(org.springframework.http.MediaType.APPLICATION_OCTET_STREAM) .body(byteArrayResource); } }
ElementPlus upload fileList为空 参考链接:https://www.sunzhongwei.com/vue-element-ui-upload-file-upload-component-after-file-list-is-empty-array
不使用 action 可以避免用户上传了图片,但是没保存表单,导致图片冗余的问题,但是实际上不使用 action fileList又为空,导致极难获取到上传到图片,所以这个还没有实现
使用 action 上传
带属性搜索 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 @RestController @RequestMapping("/employment-database") public class EmploymentDatabaseController { @Resource private IEmploymentDatabaseService employmentDatabaseService; @Resource private IEmploymentDatabaseAttachmentService employmentDatabaseAttachmentService; @GetMapping("/search") public R<Page<EmploymentDatabase>> searchEmploymentDatabase ( @RequestParam(required = false) String filterField, @RequestParam(required = false) String filterValue, @RequestParam int page, @RequestParam int size) { Page<EmploymentDatabase> employmentDatabasePage = new Page <>(page, size); QueryWrapper<EmploymentDatabase> queryWrapper = new QueryWrapper <>(); if (filterField != null && filterValue != null ) { switch (filterField) { case "category" : queryWrapper.like("category" , filterValue); break ; case "title" : queryWrapper.like("title" , filterValue); break ; case "attachment" : queryWrapper.like("attachment" , filterValue); break ; case "details" : queryWrapper.like("details" , filterValue); break ; case "createdAt" : queryWrapper.like("created_at" , filterValue); break ; default : return R.error("无效的筛选字段" ); } } Page<EmploymentDatabase> result = employmentDatabaseService.page(employmentDatabasePage, queryWrapper); return R.ok(result); } }
智能匹配岗位 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @RestController @RequestMapping("/job-search") public class JobSearchController { @Resource private IJobSearchService jobSearchService; @GetMapping("/match") public R<Page<JobSearch>> matchJobsByStudentId (@RequestParam String studentId, @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size) { Page<JobSearch> result = jobSearchService.matchJobsByStudentId(studentId, page, size); return R.ok(result); } }
导入excel 关键实现步骤
1. 上传文件
• 使用 @RequestParam(“file”) MultipartFile file 接收用户上传的文件。
• 通过 file.getInputStream() 获取文件的输入流。
2. 读取 Excel 数据
• 使用 EasyExcel 的 read 方法读取文件内容:
1 2 3 4 List<UserDetailExcelDto> userList = EasyExcel.read(file.getInputStream()) .head(UserDetailExcelDto.class) .sheet() .doReadSync();
• head(UserDetailExcelDto.class):指定 Excel 数据对应的实体类 UserDetailExcelDto,表示每一行数据将被映射到该对象。
• sheet():默认读取第一个工作表。
• doReadSync():同步读取数据,结果存储为一个 List。
3. 数据校验和保存
• 遍历 userList,对每条数据进行处理:
1 2 3 4 5 for (UserDetailExcelDto dto : userList) { if (dto.getName() == null || dto.getName().isEmpty()) { return R.ok("导入成功" ); } }
• 检查关键字段(如 dto.getName())是否为空,跳过空白行。
4. 保存到数据库
• 保存到 user_detail 表 :
1 2 3 4 5 6 7 8 9 UserDetail userDetail = new UserDetail ();userDetail.setName(dto.getName()); userDetail.setGender(dto.getGender()); userDetail.setClassName(dto.getClassName()); userDetail.setStudentId(dto.getStudentId()); userDetail.setContactNumber(dto.getContactNumber()); userDetail.setClassTeacher(dto.getClassTeacher()); userDetail.setGraduationTutor(dto.getGraduationTutor()); userDetailService.save(userDetail);
将 Excel 数据映射到 UserDetail 实体类,并调用 userDetailService.save 保存到数据库。
• 保存到 users 表 :
1 2 3 4 5 6 7 8 9 10 11 Users user = new Users ();user.setStudentId(dto.getStudentId()); user.setUsername(dto.getName()); String initialPassword = dto.getStudentId().substring(dto.getStudentId().length() - 6 );String salt = generateSalt();String passwordHash = encryptHv(initialPassword, salt);user.setPasswordHash(passwordHash); user.setSalt(salt); user.setUserType("student" ); user.setPhone(dto.getContactNumber()); usersService.save(user);
初始化用户账号信息:
• 生成默认密码:学号后 6 位。
• 调用加密方法 encryptHv 进行密码加密。
• 调用 usersService.save 保存到数据库。
完整代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 @RestController @RequestMapping("/user-detail") public class UserDetailController { @Resource private IUserDetailService userDetailService; @Resource private IUsersService usersService; @PostMapping("/importExcel") public R<String> importExcel (@RequestParam("file") MultipartFile file) { try { List<UserDetailExcelDto> userList = EasyExcel.read(file.getInputStream()) .head(UserDetailExcelDto.class) .sheet() .doReadSync(); for (UserDetailExcelDto dto : userList) { if (dto.getName() == null || dto.getName().isEmpty()) { return R.ok("导入成功" ); } UserDetail userDetail = new UserDetail (); userDetail.setName(dto.getName()); userDetail.setGender(dto.getGender()); userDetail.setClassName(dto.getClassName()); userDetail.setStudentId(dto.getStudentId()); userDetail.setContactNumber(dto.getContactNumber()); userDetail.setClassTeacher(dto.getClassTeacher()); userDetail.setGraduationTutor(dto.getGraduationTutor()); userDetailService.save(userDetail); Users user = new Users (); user.setStudentId(dto.getStudentId()); user.setUsername(dto.getName()); String initialPassword = dto.getStudentId().substring(dto.getStudentId().length() - 6 ); String salt = generateSalt(); String passwordHash = encryptHv(initialPassword, salt); user.setPasswordHash(passwordHash); user.setSalt(salt); user.setUserType("student" ); user.setPhone(dto.getContactNumber()); usersService.save(user); } return R.ok("导入成功" ); } catch (Exception e) { e.printStackTrace(); return R.error("导入失败: " + e.getMessage()); } } }
GPT新建表惯例
请根据下面的表单,修改其表单字段名称,多余的属性删去,缺少的属性补上:
根据上面的字段为我创建一个名为conversation_records的数据表,请你把sql语句给我
请给我对应的rowjson,方便我测试添加接口,注意驼峰
请你参考下面的Dto,根据上面的字段编写一份ConversationRecordsExcelDto,多余的属性删去,缺少的属性补上:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Data public class UserDetailExcelDto { @ExcelProperty("姓名") private String name; @ExcelProperty("性别") private String gender; @ExcelProperty("所在班级") private String className; @ExcelProperty("学号") private String studentId; @ExcelProperty("手机号码") private String contactNumber; @ExcelProperty("班主任") private String classTeacher; @ExcelProperty("导师") private String graduationTutor; }
请你参考下面的importExcel接口,根据上面的字段编写一份新的接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 @PostMapping("/importExcel") public R<String> importActivityTargetAudienceExcel (@RequestParam("file") MultipartFile file) { try { List<ActivityTargetAudienceExcelDto> audienceList = EasyExcel.read(file.getInputStream()) .head(ActivityTargetAudienceExcelDto.class) .sheet() .doReadSync(); List<ActivityTargetAudienceExcelDto> validAudienceList = audienceList.stream() .filter(dto -> dto.getAudienceLabel() != null && !dto.getAudienceLabel().isEmpty()) .collect(Collectors.toList()); List<ActivityTargetAudience> audienceEntities = validAudienceList.stream().map(dto -> { ActivityTargetAudience entity = new ActivityTargetAudience (); entity.setId(dto.getId()); entity.setMajor(dto.getMajor()); entity.setAudienceLabel(dto.getAudienceLabel()); entity.setAudienceValue(dto.getAudienceValue()); return entity; }).collect(Collectors.toList()); if (!audienceEntities.isEmpty()) { activityTargetAudienceService.saveOrUpdateBatch(audienceEntities); } return R.ok("导入成功" ); } catch (Exception e) { e.printStackTrace(); return R.error("导入失败: " + e.getMessage()); } }
GPT新建表格导入规范惯例
我有一个excel导入数据库的功能,现在需要我为每个excel的每个标题定一个存入标准,防止存入时报错,请你根据字段以及对应sql语句,给我相关的文字标准描述:
序号 展位号 招聘企业 招聘岗位 HR名称 联系电话 所需专业 招聘人数 薪资待遇 地区 网申链接 其他要求 企业简介
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 DROP TABLE IF EXISTS job_search;CREATE TABLE job_search ( id int NOT NULL AUTO_INCREMENT COMMENT '主键ID' , company_name varchar (255 ) NOT NULL COMMENT '企业名称' , position_name varchar (255 ) NOT NULL COMMENT '岗位名称' , hr_name varchar (100 ) DEFAULT NULL COMMENT 'HR名称' , hr_phone varchar (20 ) DEFAULT NULL COMMENT '联系电话' , major_requirement varchar (255 ) DEFAULT NULL COMMENT '专业要求' , participant_count int DEFAULT '0' COMMENT '招聘人数' , money varchar (50 ) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '薪资待遇' , area varchar (255 ) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '工作地点' , application_link text COMMENT '网申链接' , additional_requirements text COMMENT '其他要求' , company_description text COMMENT '企业简介' , created_at timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' , updated_at timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间' , PRIMARY KEY (id) ) ENGINE= InnoDB AUTO_INCREMENT= 19 DEFAULT CHARSET= utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT= '岗位发布详情表' ;
参考JobSearchExcellmportListener,为我编写一个ActivityTargetAudienceImportListener,错误消息要求逻辑为第2列班级必填、第3列专业名称必填且不允许出现“专升本”这三个字、第4列年级必填,且只能为数字,由于我的audienceValue存入时是string,具体的逻辑,可以是先尝试转化为Integer,如果失败则抛出然后存入错误:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 public class JobSearchExcelImportListener extends AnalysisEventListener <JobSearchExcelDto> { private final List<JobSearchExcelDto> jobList; private final List<Map<Integer, String>> errorDataList; public JobSearchExcelImportListener (List<JobSearchExcelDto> jobList, List<Map<Integer, String>> errorDataList) { this .jobList = jobList; this .errorDataList = errorDataList; } @Override public void invoke (JobSearchExcelDto dto, AnalysisContext context) { Map<Integer, String> errors = new HashMap <>(); if (dto.getCompanyName() == null || dto.getCompanyName().isEmpty()) { errors.put(2 , "招聘企业不能为空" ); } if (dto.getPositionName() == null || dto.getPositionName().isEmpty()) { errors.put(3 , "招聘岗位不能为空" ); } if (dto.getMajorRequirement() == null || dto.getMajorRequirement().isEmpty()) { errors.put(6 , "所需专业不能为空" ); } else if (!Pattern.matches("^[\\w/\\u4e00-\\u9fa5]+$" , dto.getMajorRequirement())) { errors.put(6 , "所需专业必须使用“/”分割" ); } if (dto.getParticipantCount() == null ) { errors.put(7 , "招聘人数不能为空" ); }else if (dto.getParticipantCount() == 0 ) { errors.put(7 , "招聘人数必须为数字整数" ); } if (dto.getMoney() == null || dto.getMoney().isEmpty()) { errors.put(8 , "薪资待遇不能为空" ); } else { List<String> validSalaries = Arrays.asList("2000-5000" , "5000-8000" , "8000-15000" , "15000以上" ,"面议" ); if (!validSalaries.contains(dto.getMoney())) { errors.put(8 , "薪资待遇格式错误,仅支持以下格式: 2000-5000, 5000-8000, 8000-15000, 15000以上, 面议" ); } } if (dto.getArea() == null || dto.getArea().isEmpty()) { errors.put(9 , "地区不能为空" ); } else if (!Pattern.matches("^[\\w,\\u4e00-\\u9fa5]+$" , dto.getArea())) { errors.put(9 , "地区必须使用英文逗号分割" ); } dto.setErrorMessages(errors); jobList.add(dto); errorDataList.add(errors.isEmpty() ? new HashMap <>() : errors); } @Override public void doAfterAllAnalysed (AnalysisContext context) { } }
ActivityTargetAudienceExcelDto:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Data public class ActivityTargetAudienceExcelDto { @ExcelProperty("序号") private Integer id; @ExcelProperty("年级") private String audienceLabel; @ExcelProperty("班级") private String audienceValue; @ExcelProperty("专业名称") private String major; @ExcelIgnore private Map<Integer, String> errorMessages = new HashMap <>(); }
参考JobSearchController的importExcel修改ActivityTargetAudienceController的importExcel
JobSearchController的importExcel:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 @PostMapping("/importExcel") public void importJobSearchExcel (@RequestParam("file") MultipartFile file, HttpServletResponse response) { try { List<JobSearchExcelDto> jobList = new ArrayList <>(); List<Map<Integer, String>> errorDataList = new ArrayList <>(); JobSearchExcelImportListener listener = new JobSearchExcelImportListener (jobList, errorDataList); EasyExcel.read(file.getInputStream(), JobSearchExcelDto.class, listener) .sheet() .doReadSync(); if (jobList.isEmpty()) { response.setContentType("application/json" ); response.setCharacterEncoding("utf-8" ); response.getWriter().write("{\"message\":\"导入数据为空\"}" ); return ; } boolean hasErrors = jobList.stream() .anyMatch(dto -> dto.getErrorMessages() != null && !dto.getErrorMessages().isEmpty()); if (hasErrors) { response.setContentType("application/vnd.ms-excel" ); response.setCharacterEncoding("utf-8" ); response.setHeader("Content-Disposition" , "attachment;filename=error_data.xlsx" ); EasyExcel.write(response.getOutputStream(), JobSearchExcelDto.class) .registerWriteHandler(new ErrorCellStyleHandler (errorDataList)) .sheet("错误数据" ) .doWrite(jobList); return ; } List<JobSearch> jobEntities = jobList.stream() .map(this ::mapToJobSearch) .collect(Collectors.toList()); if (!jobEntities.isEmpty()) { jobSearchService.saveOrUpdateBatch(jobEntities); } response.setContentType("application/json" ); response.setCharacterEncoding("utf-8" ); response.getWriter().write("{\"message\":\"导入成功\"}" ); } catch (Exception e) { e.printStackTrace(); try { response.setContentType("application/json" ); response.setCharacterEncoding("utf-8" ); response.getWriter().write("{\"message\":\"导入失败: " + e.getMessage() + "\"}" ); } catch (IOException ioException) { ioException.printStackTrace(); } } }
利用 CASE 语句,将 money 转换为数值后按从高到低排序 在代码中,CASE 语句用于将文字形式的薪资范围(如 “2000-5000”)转换为对应的数值,以便进行条件过滤和排序操作。以下是具体解析:
CASE 语句基本结构
CASE 是 SQL 中的一种条件表达式,类似于编程语言中的 if-else 或 switch-case,用于根据条件返回不同的值。
语法如下:
CASE
WHEN condition1 THEN result1
WHEN condition2 THEN result2
…
ELSE resultN
END
• WHEN condition1 THEN result1:如果 condition1 为真,返回 result1。
• ELSE resultN:当没有任何条件为真时,返回 resultN(可以省略)。
在我们的代码中,CASE 被用在 QueryWrapper.apply 和 orderByDesc 中。
用法 1:映射薪资范围到数值
queryWrapper.lambda()
.apply(“CASE “ +
“WHEN money = ‘2000-5000’ THEN 2000 “ +
“WHEN money = ‘5000-8000’ THEN 5000 “ +
“WHEN money = ‘8000-15000’ THEN 8000 “ +
“WHEN money = ‘15000以上’ THEN 15000 “ +
“ELSE 0 END >= {0}”, lowSalaryValue);
解析:
1. money = ‘2000-5000’:
• 如果 JobSearch.money 为 “2000-5000”,返回 2000。
2. money = ‘5000-8000’:
• 如果 JobSearch.money 为 “5000-8000”,返回 5000。
3. ELSE 0:
• 如果 JobSearch.money 的值不在已知范围内,返回 0。
最终,这段 CASE 的结果是一个整数值,可以用来和用户的最低薪资需求(lowSalaryValue)进行数值比较。
例如:如果用户的 lowSalaryValue 是 8000,那么查询条件会变成:
CASE
WHEN money = ‘2000-5000’ THEN 2000
WHEN money = ‘5000-8000’ THEN 5000
WHEN money = ‘8000-15000’ THEN 8000
WHEN money = ‘15000以上’ THEN 15000
ELSE 0
END >= 8000
这将筛选出薪资范围为 “8000-15000” 和 “15000以上” 的数据。
用法 2:排序
queryWrapper.orderByDesc(“CASE “ +
“WHEN money = ‘2000-5000’ THEN 2000 “ +
“WHEN money = ‘5000-8000’ THEN 5000 “ +
“WHEN money = ‘8000-15000’ THEN 8000 “ +
“WHEN money = ‘15000以上’ THEN 15000 “ +
“ELSE 0 END”);
解析:
1. CASE 的逻辑同上,将文字形式的 money 映射为可比较的数值。
2. orderByDesc 按照这些数值从大到小排序。
例如:
• “15000以上” -> 15000
• “8000-15000” -> 8000
• “5000-8000” -> 5000
• “2000-5000” -> 2000
最终,结果将按这些映射值从高到低排序。
SQL 执行逻辑示例
假设表 JobSearch 中有如下数据:
money
2000-5000
5000-8000
8000-15000
15000以上
SQL 查询语句:
SELECT *
FROM JobSearch
WHERE
CASE
WHEN money = ‘2000-5000’ THEN 2000
WHEN money = ‘5000-8000’ THEN 5000
WHEN money = ‘8000-15000’ THEN 8000
WHEN money = ‘15000以上’ THEN 15000
ELSE 0
END >= 8000
ORDER BY
CASE
WHEN money = ‘2000-5000’ THEN 2000
WHEN money = ‘5000-8000’ THEN 5000
WHEN money = ‘8000-15000’ THEN 8000
WHEN money = ‘15000以上’ THEN 15000
ELSE 0
END DESC;
执行结果:
• WHERE 过滤出 money >= 8000 的数据,即:”8000-15000” 和 “15000以上”。
• ORDER BY 按映射值排序,最终结果:
1. “15000以上”
2. “8000-15000”
总结
• CASE 用于将无法直接比较的文字形式数据(薪资范围)转换为数值。
• 筛选通过 CASE 结合 WHERE 实现。
• 排序通过 CASE 映射值结合 ORDER BY 实现。
• 这种方法兼容文字范围的动态映射,同时保留了高效的 SQL 查询能力。
如何让item.title部分的p标签保持只有一行,多处一行的部分用…代替 Tailwind 的 truncate 类作用
truncate 类的实际效果等同于以下 CSS:
1 2 3 overflow : hidden; text-overflow : ellipsis; white-space : nowrap;
效果
1. 文本长度超过容器宽度时,会自动加上 …。
2. 文本强制保持单行,不会因为内容过多而换行。
自定义宽度容器
如果需要限制 title 的最大显示宽度以便触发省略号,可以添加 max-w-* 类。例如:
1 2 3 <p class ="text-lg text-blue-400 cursor-pointer hover:text-black truncate max-w-[200px]" > {{ item.title }} </p >
其中:
• max-w-[200px]:限制最大宽度为 200px(可以根据设计需求调整)。
• 这将确保在宽度超过 200px 时文本溢出,触发省略号效果。
esayexcel esayexcel导入失败 参考链接:https://segmentfault.com/q/1010000018526018
1 导入失败: Text '2024-11-23' could not be parsed: Unable to obtain LocalDateTime from TemporalAccessor: {},ISO resolved to 2024-11-23 of type java.time.format.Parsed
导入失败原因:LocalDateTime
不能转换 yyyy-MM-dd
这种格式的字符串,LocalDateTime 是 LocalDate + LocalTime 两部分都得有,也就是说上面的格式字符串缺少了小时分钟以及秒,将类似的类型改为LocalDate就行
esayexcel导出 后端接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 @GetMapping("/exportJobSearchExcel") public void exportJobSearchExcel (HttpServletResponse response) { try { List<JobSearch> jobSearchList = jobSearchService.list(); if (jobSearchList.isEmpty()) { throw new RuntimeException ("无数据可导出" ); } List<JobSearchExcelDto> jobSearchDtoList = jobSearchList.stream() .map(this ::mapToJobSearchExcelDto) .collect(Collectors.toList()); response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ); response.setCharacterEncoding("utf-8" ); String fileName = URLEncoder.encode("求职信息" , "UTF-8" ).replaceAll("\\+" , "%20" ); response.setHeader("Content-Disposition" , "attachment; filename*=UTF-8''" + fileName + ".xlsx" ); EasyExcel.write(response.getOutputStream(), JobSearchExcelDto.class) .sheet("求职信息" ) .doWrite(jobSearchDtoList); } catch (Exception e) { e.printStackTrace(); try { response.setContentType("application/json" ); response.setCharacterEncoding("utf-8" ); response.getWriter().write("{\"message\":\"导出失败: " + e.getMessage() + "\"}" ); } catch (IOException ioException) { ioException.printStackTrace(); } } } private JobSearchExcelDto mapToJobSearchExcelDto (JobSearch jobSearch) { JobSearchExcelDto dto = new JobSearchExcelDto (); dto.setId(jobSearch.getId()); dto.setCompanyName(jobSearch.getCompanyName()); dto.setPositionName(jobSearch.getPositionName()); dto.setHrName(jobSearch.getHrName()); dto.setHrPhone(jobSearch.getHrPhone()); dto.setMajorRequirement(jobSearch.getMajorRequirement()); dto.setParticipantCount(jobSearch.getParticipantCount()); dto.setMoney(jobSearch.getMoney()); dto.setArea(jobSearch.getArea()); dto.setApplicationLink(jobSearch.getApplicationLink()); dto.setAdditionalRequirements(jobSearch.getAdditionalRequirements()); dto.setCompanyDescription(jobSearch.getCompanyDescription()); return dto; }
前端请求:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 const handleExport = async ( ) => { try { const response = await fetch ("http://localhost:5173/api/user-info/exportExcel" , { method : "GET" , }); if (response.ok ) { const blob = await response.blob (); const url = window .URL .createObjectURL (blob); const link = document .createElement ("a" ); link.href = url; link.setAttribute ("download" , "用户信息.xlsx" ); document .body .appendChild (link); link.click (); document .body .removeChild (link); window .URL .revokeObjectURL (url); ElMessage .success ("文件导出成功!" ); } else { ElMessage .error ("文件导出失败,请重试!" ); } } catch (error) { console .error ("导出过程中出现错误:" , error); ElMessage .error ("导出过程中出现错误!" ); } };
esayexcel标红错误部分数据的单元格 注意别忘了ExcelDto里加上errorMessages
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Data public class ActivityTargetAudienceExcelDto { @ExcelProperty("序号") private Integer id; @ExcelProperty("年级") private String audienceLabel; @ExcelProperty("班级") private String audienceValue; @ExcelProperty("专业名称") private String major; @ExcelIgnore private Map<Integer, String> errorMessages = new HashMap <>(); }
整体步骤
数据读取与校验 :
通过 EasyExcel
读取用户上传的 Excel 文件。
使用自定义监听器(JobSearchExcelImportListener
)对每一行数据进行校验,记录错误信息。
标注错误单元格 :
如果检测到错误,通过自定义的 ErrorCellStyleHandler
在输出的 Excel 中将错误单元格标红,并在最后一列记录对应的错误信息。
导出错误文件 :
如果存在错误数据,将处理后的 Excel 数据(包含标注)作为错误文件导出。
如果没有错误,则将数据保存到数据库。
ErrorCellStyleHandler
作用 :
对包含错误数据的单元格进行样式修改(标红)。
在最后一列追加错误描述。
关键逻辑 :
afterCellDispose
:
检测当前单元格是否包含错误信息。
如果有错误,则设置背景色为红色。
afterRowDispose
:
如果是标题行,在最后一列添加“错误信息”标题。
如果是数据行,在最后一列写入当前行的错误描述。
注意事项 :
样式设置时,确保未覆盖已有样式。
行索引与列索引从 0 开始,容易混淆。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 public class ErrorCellStyleHandler implements CellWriteHandler , RowWriteHandler { private final List<Map<Integer, String>> errorDataList; public ErrorCellStyleHandler (List<Map<Integer, String>> errorDataList) { this .errorDataList = errorDataList; System.out.println("ErrorCellStyleHandler initialized" +errorDataList.size()); } @Override public void afterCellDispose (CellWriteHandlerContext context) { if (Boolean.TRUE.equals(context.getHead())) { return ; } int rowIndex = context.getRowIndex(); int columnIndex = context.getColumnIndex(); if (rowIndex > 0 && rowIndex - 1 < errorDataList.size()) { Map<Integer, String> errorMap = errorDataList.get(rowIndex - 1 ); if (errorMap.containsKey(columnIndex)) { WriteCellData<?> cellData = context.getFirstCellData(); WriteCellStyle writeCellStyle = cellData.getOrCreateStyle(); writeCellStyle.setFillPatternType(FillPatternType.SOLID_FOREGROUND); writeCellStyle.setFillForegroundColor(IndexedColors.RED.getIndex()); System.out.println("标记错误: 行 " + (rowIndex + 1 ) + ", 列 " + (columnIndex + 1 ) + ", 错误信息: " + errorMap.get(columnIndex)); } } } @Override public void afterRowDispose (RowWriteHandlerContext context) { if (BooleanUtils.isTrue(context.getHead())) { int lastColumnIndex = context.getRow().getLastCellNum(); Cell cell = context.getRow().createCell(lastColumnIndex, CellType.STRING); cell.setCellValue("错误信息" ); Workbook workbook = context.getWriteWorkbookHolder().getWorkbook(); CellStyle headerStyle = workbook.createCellStyle(); Font font = workbook.createFont(); font.setBold(true ); headerStyle.setFont(font); cell.setCellStyle(headerStyle); return ; } if (context.getRelativeRowIndex() != null ) { int rowIndex = context.getRelativeRowIndex(); if (rowIndex < errorDataList.size()) { Map<Integer, String> errorMap = errorDataList.get(rowIndex); if (errorMap != null && !errorMap.isEmpty()) { int lastColumnIndex = context.getRow().getLastCellNum(); String errorMessage = String.join("; " , errorMap.values()); Cell cell = context.getRow().createCell(lastColumnIndex, CellType.STRING); cell.setCellValue(errorMessage); Workbook workbook = context.getWriteWorkbookHolder().getWorkbook(); CellStyle errorStyle = workbook.createCellStyle(); Font font = workbook.createFont(); font.setColor(IndexedColors.RED.getIndex()); errorStyle.setFont(font); cell.setCellStyle(errorStyle); } } } } }
JobSearchExcellmportListener
作用 :
负责监听 Excel 的数据解析过程。
对每一行的关键字段(如招聘企业、岗位、薪资等)进行校验,记录错误信息。
关键逻辑 :
利用 Map<Integer, String>
存储每一行错误信息,其中键为列索引,值为错误描述。
errorDataList
保存所有行的错误信息,用于后续处理。
注意事项 :
检查逻辑需要与业务需求保持一致,例如字段是否为必填、格式是否正确等。
注意之前确保每行都有一个 Map部分的代码有误,现在是修正版本,否则只是储存了有错误的数据,实际上每行都要存入,即使该行没错误
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 public class JobSearchExcelImportListener extends AnalysisEventListener <JobSearchExcelDto> { private final List<JobSearchExcelDto> jobList; private final List<Map<Integer, String>> errorDataList; public JobSearchExcelImportListener (List<JobSearchExcelDto> jobList, List<Map<Integer, String>> errorDataList) { this .jobList = jobList; this .errorDataList = errorDataList; } @Override public void invoke (JobSearchExcelDto dto, AnalysisContext context) { Map<Integer, String> errors = new HashMap <>(); if (dto.getCompanyName() == null || dto.getCompanyName().isEmpty()) { errors.put(2 , "招聘企业不能为空" ); } if (dto.getPositionName() == null || dto.getPositionName().isEmpty()) { errors.put(3 , "招聘岗位不能为空" ); } if (dto.getMajorRequirement() == null || dto.getMajorRequirement().isEmpty()) { errors.put(6 , "所需专业不能为空" ); } else if (!Pattern.matches("^[\\w/\\u4e00-\\u9fa5]+$" , dto.getMajorRequirement())) { errors.put(6 , "所需专业必须使用“/”分割" ); } if (dto.getParticipantCount() == null ) { errors.put(7 , "招聘人数不能为空" ); } if (dto.getMoney() == null || dto.getMoney().isEmpty()) { errors.put(8 , "薪资待遇不能为空" ); } else { List<String> validSalaries = Arrays.asList("2000-5000" , "5000-8000" , "8000-15000" , "15000以上" ,"面议" ); if (!validSalaries.contains(dto.getMoney())) { errors.put(8 , "薪资待遇格式错误,仅支持以下格式: 2000-5000, 5000-8000, 8000-15000, 15000以上, 面议" ); } } if (dto.getArea() == null || dto.getArea().isEmpty()) { errors.put(9 , "地区不能为空" ); } else if (!Pattern.matches("^[\\w,\\u4e00-\\u9fa5]+$" , dto.getArea())) { errors.put(9 , "地区必须使用英文逗号分割" ); } dto.setErrorMessages(errors); jobList.add(dto); errorDataList.add(errors.isEmpty() ? new HashMap <>() : errors); } @Override public void doAfterAllAnalysed (AnalysisContext context) { } }
JobSearchController
作用 :
读取用户上传的文件,并根据错误情况决定后续操作:
如果没有错误,直接保存数据。
如果有错误,生成包含错误标注的文件并返回给用户。
关键逻辑 :
判断是否存在错误:jobList.stream().anyMatch(...)
。
使用 EasyExcel
写入时,注册 ErrorCellStyleHandler
以实现错误标注。
注意事项 :
返回 Excel 文件时需要正确设置 Content-Type
和 Content-Disposition
。
处理异常时需要保证响应的输出流关闭。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 @PostMapping("/importExcel") public void importJobSearchExcel (@RequestParam("file") MultipartFile file, HttpServletResponse response) { try { List<JobSearchExcelDto> jobList = new ArrayList <>(); List<Map<Integer, String>> errorDataList = new ArrayList <>(); JobSearchExcelImportListener listener = new JobSearchExcelImportListener (jobList, errorDataList); EasyExcel.read(file.getInputStream(), JobSearchExcelDto.class, listener) .sheet() .doReadSync(); if (jobList.isEmpty()) { response.setContentType("application/json" ); response.setCharacterEncoding("utf-8" ); response.getWriter().write("{\"message\":\"导入数据为空\"}" ); return ; } boolean hasErrors = jobList.stream() .anyMatch(dto -> dto.getErrorMessages() != null && !dto.getErrorMessages().isEmpty()); if (hasErrors) { response.setContentType("application/vnd.ms-excel" ); response.setCharacterEncoding("utf-8" ); response.setHeader("Content-Disposition" , "attachment;filename=error_data.xlsx" ); EasyExcel.write(response.getOutputStream(), JobSearchExcelDto.class) .registerWriteHandler(new ErrorCellStyleHandler (errorDataList)) .sheet("错误数据" ) .doWrite(jobList); return ; } List<JobSearch> jobEntities = jobList.stream() .map(this ::mapToJobSearch) .collect(Collectors.toList()); if (!jobEntities.isEmpty()) { jobSearchService.saveOrUpdateBatch(jobEntities); } response.setContentType("application/json" ); response.setCharacterEncoding("utf-8" ); response.getWriter().write("{\"message\":\"导入成功\"}" ); } catch (Exception e) { e.printStackTrace(); try { response.setContentType("application/json" ); response.setCharacterEncoding("utf-8" ); response.getWriter().write("{\"message\":\"导入失败: " + e.getMessage() + "\"}" ); } catch (IOException ioException) { ioException.printStackTrace(); } } }
CustomlntegerConverter
自定义类型转换:CustomIntegerConverter
作用:
解决 Excel 数据中整数字段可能为空、格式错误的问题。
在读取或写入过程中,将错误数据转换为默认值(如 0),但保留错误信息。
关键逻辑:
**convertToJavaData
**:解析单元格数据,确保其为合法的整数格式,否则返回默认值。
**convertToExcelData
**:在写入 Excel 时,优先输出原始错误数据。
注意事项:
注意字符串转换为数字时可能抛出的 NumberFormatException
。
确保 errorData
在多线程情况下不会被共享导致数据错误。
原始错误信息保留这块通过解析单元格数据时存入,在写入 Excel 时通过变量改变实现的,否则是无法存入不同类型的数据的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public class CustomIntegerConverter implements Converter <Integer> { String errorData = "" ; @Override public Integer convertToJavaData (ReadCellData cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { String cellValue = cellData.getStringValue(); if (cellValue == null || cellValue.trim().isEmpty()) { if (cellData.getNumberValue() != null ) { return cellData.getNumberValue().intValue(); } return 0 ; } try { return Integer.parseInt(cellValue); } catch (NumberFormatException e) { errorData = cellValue; return 0 ; } } @Override public WriteCellData<?> convertToExcelData(Integer value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception { System.out.println("convertToExcelData-errorData: " + errorData); if (value == null || value == 0 ) { return new WriteCellData <>(errorData); } return new WriteCellData <>(String.valueOf(value)); } }
学生信息表备份:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 <template> <div class="h-full"> <div class="flex justify-center items-center px-8"> <div class="h-full"> <p class="md:text-4xl font-extrabold text-2xl" v-if="userInfo.user?.userType == 'student'">个人信息登记详情</p> <p class="md:text-4xl font-extrabold text-2xl text-center" v-if="userInfo.user?.userType == 'teacher'">{{ userDetail.name }}学生信息表</p> </div> </div> <el-scrollbar height="90%"> <div class="main flex flex-col justify-center gap-10 p-10"> <!-- 第一行 --> <div class="flex flex-1 justify-between items-center gap-10"> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">学生照片:</p> <img :src="userDetail.imageUrl" class="avatar" /> </div> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">姓名:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.name }}</p> </div> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">性别:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.gender }}</p> </div> </div> <!-- 第二行 --> <div class="md:flex md:flex-1 justify-between items-center gap-10"> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">手机号:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.phone }}</p> </div> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">学号:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.studentId }}</p> </div> <div class="flex flex-1 justify-start items-center mt-4 md:mt-0"> <p class="text-xl font-bold whitespace-nowrap">身份证号:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.idCard }}</p> </div> </div> <!-- 第三行 --> <div class="md:flex md:flex-1 justify-between items-center gap-10"> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">年级:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.grade }}</p> </div> <div class="flex flex-1 justify-start items-center mt-4 md:mt-0"> <p class="text-xl font-bold whitespace-nowrap">专业:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.major }}</p> </div> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">班级:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.className }}</p> </div> </div> <!-- 出生日期相关 --> <div class="md:flex md:flex-1 justify-between items-center gap-10"> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">出生日期:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.birthDate }}</p> </div> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">入学日期:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.admissionDate }}</p> </div> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">预计毕业时间:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.expectedGraduation }}</p> </div> </div> <!-- 其他信息 --> <div class="md:flex md:flex-1 justify-between items-center gap-10"> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">籍贯:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.nativePlace }}</p> </div> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">生源地:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.sourcePlace }}</p> </div> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">民族:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.ethnicity }}</p> </div> </div> <!-- 第四行 --> <div class="flex flex-1 justify-between items-center gap-10"> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">班级职务:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.classRole }}</p> </div> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">专业方向:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.specialization }}</p> </div> </div> <div class="md:flex md:flex-1 justify-between items-center gap-10"> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">户口所在地:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.residence }}</p> </div> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">家庭住址:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.homeAddress }}</p> </div> </div> <div class="md:flex md:flex-1 justify-between items-center gap-10"> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">辅导员:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.counselor }}</p> </div> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">辅导员手机号:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.counselorPhone }}</p> </div> </div> <div class="md:flex md:flex-1 justify-between items-center gap-10"> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">班主任:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.classTeacher }}</p> </div> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">班主任手机号:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.classTeacherPhone }}</p> </div> </div> <div class="md:flex md:flex-1 justify-between items-center gap-10"> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">毕设导师:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.graduationTutor }}</p> </div> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">毕设导师手机号:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.graduationTutorPhone }}</p> </div> </div> <div class="md:flex md:flex-1 justify-between items-center gap-10"> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">寝室号:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.dormitoryNumber }}</p> </div> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">红旗网络:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.networkStatus }}</p> </div> </div> <div class="md:flex md:flex-1 justify-between items-center gap-10"> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">寝室成员名单:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.dormitoryMembers }}</p> </div> </div> <div class="md:flex md:flex-1 justify-between items-center gap-10"> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">政治面貌:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.politicalStatus }}</p> </div> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">入党进度:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.partyProgress }}</p> </div> </div> <div class="md:flex md:flex-1 justify-between items-center gap-10"> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">入党培训进度:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.partyTrainingProgress }}</p> </div> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">所在支部:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.branchName }}</p> </div> </div> <div class="md:flex md:flex-1 justify-between items-center gap-10"> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">入党申请时间:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.applicationDate }}</p> </div> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">入党积极分子时间:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.activistDate }}</p> </div> </div> <div class="md:flex md:flex-1 justify-between items-center gap-10"> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">发展对象时间:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.developmentDate }}</p> </div> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">预备党员时间:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.probationaryDate }}</p> </div> </div> <div class="md:flex md:flex-1 justify-between items-center gap-10"> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">发展转正时间:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.fullMemberDate }}</p> </div> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">党建工时:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.partyHours }}</p> </div> </div> <div class="md:flex md:flex-1 justify-between items-center gap-10"> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">党支部书记姓名:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.branchSecretary }}</p> </div> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">党支部副书记姓名:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.branchDeputySecretary }}</p> </div> </div> <div class="md:flex md:flex-1 justify-between items-center gap-10"> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">电子邮箱:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.email }}</p> </div> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">QQ号码:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.qqNumber }}</p> </div> </div> <div class="md:flex md:flex-1 justify-between items-center gap-10"> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">微信号码:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.wechatId }}</p> </div> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">抖音账号:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.douyinId }}</p> </div> </div> <div class="md:flex md:flex-1 justify-between items-center gap-10"> <!-- 家长1信息 --> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">家长1姓名:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.parent1Name }}</p> </div> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">家长1手机号:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.parent1Phone }}</p> </div> </div> <div class="md:flex md:flex-1 justify-between items-center gap-10"> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">家长1工作单位:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.parent1Company }}</p> </div> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">家长1职业:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.parent1Job }}</p> </div> </div> <div class="md:flex md:flex-1 justify-between items-center gap-10"> <!-- 家长2信息 --> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">家长2姓名:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.parent2Name }}</p> </div> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">家长2手机号:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.parent2Phone }}</p> </div> </div> <div class="md:flex md:flex-1 justify-between items-center gap-10"> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">家长2工作单位:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.parent2Company }}</p> </div> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">家长2职业:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.parent2Job }}</p> </div> </div> <div class="md:flex md:flex-1 justify-between items-center gap-10"> <!-- 紧急联系人信息 --> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">紧急联系人姓名:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.emergencyContactName }}</p> </div> <div class="flex flex-1 justify-start items-center"> <p class="text-xl font-bold whitespace-nowrap">紧急联系人手机号:</p> <p class="text-xl font-bold whitespace-nowrap">{{ userDetail.emergencyContactPhone }}</p> </div> </div> </div> </el-scrollbar> </div> </template> <script setup lang="ts"> import { ref, onMounted } from "vue"; import { Delete, Download, Plus, ZoomIn } from '@element-plus/icons-vue'; import router from '../router/index'; import { useRoute } from 'vue-router'; import { ElMessage } from 'element-plus'; import { pcaTextArr } from "element-china-area-data"; import type { UploadProps } from 'element-plus' // 引入接口方法 import { addUserInfo, getUserInfoById, editUserInfo } from '../api/userInfo'; import type { UploadFile } from 'element-plus'; import { userInfoStore } from "../stores/UserInfoStore"; const userInfo = userInfoStore(); // 是否为修改模式 const route = useRoute(); const isEdit = ref(false); const loading = ref(false); // 定义表单字段 const id = ref(''); const name = ref(''); const gender = ref<'男' | '女'>('男'); const className = ref(''); const studentId = ref(''); const contactNumber = ref(''); const classTeacher = ref(''); const graduationTutor = ref(''); const futurePlan = ref(''); const salary = ref(''); const companyNature = ref(''); const workLocation = ref([]); const employmentStatus = ref('实习'); const companyName = ref(''); const userDetail = ref<any>({ id: '', // 用户ID name: '', // 姓名 gender: '男', // 性别 studentId: '', // 学号 idCard: '', // 身份证号 grade: '', // 年级 major: '', // 专业 className: '', // 班级 classRole: '', // 班级职务 specialization: '', // 专业方向 birthDate: '', // 出生日期 admissionDate: '', // 入学日期 expectedGraduation: '', // 预计毕业时间 nativePlace: '', // 籍贯 sourcePlace: '', // 生源地 ethnicity: '', // 民族 residence: '', // 户口所在地 homeAddress: '', // 家庭住址 counselor: '', // 辅导员姓名 counselorPhone: '', // 辅导员手机号 classTeacher: '', // 班主任姓名 classTeacherPhone: '', // 班主任手机号 graduationTutor: '', // 毕设导师姓名 graduationTutorPhone: '', // 毕设导师手机号 dormitoryNumber: '', // 寝室号 networkStatus: '', // 红旗网络 dormitoryMembers: '', // 寝室成员名单 politicalStatus: '', // 政治面貌 partyProgress: '', // 入党进度 partyTrainingProgress: '', // 入党培训进度 branchName: '', // 所在支部 applicationDate: '', // 入党申请时间 activistDate: '', // 入党积极分子时间 developmentDate: '', // 发展对象时间 probationaryDate: '', // 预备党员时间 fullMemberDate: '', // 发展转正时间 partyHours: '', // 党建工时 branchSecretary: '', // 党支部书记姓名 branchDeputySecretary: '', // 党支部副书记姓名 email: '', qqNumber: '', wechatId: '', douyinId: '', parent1Name: '', parent1Phone: '', parent1Company: '', parent1Job: '', parent2Name: '', parent2Phone: '', parent2Company: '', parent2Job: '', emergencyContactName: '', emergencyContactPhone: '', }); // 定义上传文件列表 const fileList = ref<UploadFile[]>([]); // 初始化 onMounted(async () => { await getUserInfoById(route.params.id as string).then((res) => { const data = res.data; console.log("userDetail.value", userDetail.value); // 如果不为空则填充表单字段,如果为空则重置表单字段 if (data) { populateFormFields(data); isEdit.value = true; loading.value = true; } else { isEdit.value = false; resetFormFields(); } loading.value = false; }).catch((err) => { console.log(err); }); }); // 重置表单字段 const resetFormFields = () => { name.value = ''; gender.value = '男'; className.value = ''; studentId.value = ''; contactNumber.value = ''; classTeacher.value = ''; graduationTutor.value = ''; futurePlan.value = ''; salary.value = ''; companyNature.value = ''; workLocation.value = []; employmentStatus.value = '实习'; companyName.value = ''; fileList.value = []; }; const populateFormFields = (data: any) => { Object.assign(userDetail.value, data); }; </script> <style lang="scss" scoped> .fade-enter-active, .fade-leave-active { transition: opacity 0.3s ease; } .fade-enter-from, .fade-leave-to { opacity: 0; } // 下面为el-select部分 @mixin select_radius { border-radius: 12px; } // 控制el-select的长度以及圆角 :deep(.el-select__wrapper) { height: 50px; @include select_radius; } // 控制el-select中文字的样式 :deep(.el-select__placeholder) { font-size: 16px; font-weight: bold; } // 控制点击后的边框颜色 :deep(.el-select__wrapper.is-focused) { box-shadow: 0 0 0 1px var(--accent-100); } // 下面为下拉框部分 // 下面用于控制整体的下拉框圆角 :deep(.el-select__popper.el-popper) { @include select_radius; } //下拉框的文本未选中的样式 // .el-select-dropdown__item { // } //下拉框的文本颜色选中之后的样式 .el-select-dropdown__item.is-selected { color: var(--accent-200); } .el-input { height: 50px; border-radius: 12px; border: 0.5px solid var(--text-200); border: 0; background-color: var(--bg-200); font-size: 16px; font-weight: bold; :deep(.el-input__wrapper) { border-radius: 12px; } :deep(.is-focus) { box-shadow: 0 0 0 1px var(--accent-100) } } // 下面是日期选择组件的自定义样式 :deep(.el-date-editor.el-input, .el-date-editor.el-input__wrapper) { width: 100%; height: 50px; border-radius: 12px; } .el-date-editor-style { --el-input-border-radius: 12px; } // 下面是数字选择组件的自定义样式 .el-input-number { width: 100%; height: 50px; } :deep(.el-input) { box-shadow: 0 0 0 1px var(--accent-200, var(--accent-100)) inset; border-radius: 12px; } :deep(.el-input-number__increase:hover~.el-input:not(.is-disabled) .el-input__wrapper) { box-shadow: 0 0 0 1px var(--accent-200, var(--accent-100)) inset; } :deep(.el-input-number__decrease:hover~.el-input:not(.is-disabled) .el-input__wrapper) { box-shadow: 0 0 0 1px var(--accent-200, var(--accent-100)) inset; } :deep(.el-input-number__increase:hover) { color: var(--accent-200); } :deep(.el-input-number__decrease:hover) { color: var(--accent-200); } :deep(.el-input__wrapper.is-focus) { box-shadow: 0 0 0 1px var(--accent-200, var(--accent-100)) inset !important; } // 下面是textarea组件的自定义样式 .el-textarea { font-size: 16px; font-weight: bold; --el-input-focus-border-color: var(--accent-200); } // 下面是地区选择组件的自定义样式 :deep(.el-cascader--large) { width: 100%; height: 50px; } :deep(.el-cascader .el-input) { height: 50px; } :deep(.el-input--large .el-input__wrapper) { border-radius: 12px; } :deep(.el-cascader .el-input .el-input__inner) { font-size: 16px; font-weight: bold; } .avatar-uploader .avatar { width: 80px; height: 112px; /* 80px * (35/25) */ display: block; } </style> <style> .avatar-uploader .el-upload { border: 1px dashed var(--el-border-color); border-radius: 6px; cursor: pointer; position: relative; overflow: hidden; transition: var(--el-transition-duration-fast); } .avatar-uploader .el-upload:hover { border-color: var(--el-color-primary); } .el-icon.avatar-uploader-icon { font-size: 28px; color: #8c939d; width: 80px; height: 112px; /* 80px * (35/25) */ text-align: center; } </style>
Vue3 打印实现 Vue3 中使用 html2canvas + jspdf + print-js 实现打印的详细方案
这套方案适合复杂页面的打印需求,尤其是在需要高精度 PDF 输出、支持跨域图片加载以及良好页面布局控制的情况下。
核心功能简介
1. html2canvas:
• 将指定的 DOM 元素渲染为 canvas 图像。
• 支持动态内容(如样式、图片)。
• 配合 useCORS 参数解决图片跨域问题。
2. jspdf:
• 将 canvas 转换为 PDF 文件。
• 支持多页 PDF 输出。
3. print-js:
• 支持直接打印 HTML 或 PDF 文件。
• 提供弹窗式的打印预览,体验更友好。
实现步骤
1. 安装依赖
运行以下命令安装相关库:
1 npm install html2canvas jspdf print-js
2. 代码实现
(1) 基础模板代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 <template> <div> <!-- 打印区域 --> <div id="printArea" class="p-5 bg-white"> <h1 class="text-xl font-bold">学生信息表</h1> <div class="flex items-center gap-4"> <div> <p>姓名: {{ userDetail.name }}</p> <p>学号: {{ userDetail.studentId }}</p> <p>专业: {{ userDetail.major }}</p> </div> <img :src="userDetail.imageUrl" class="w-32 h-32 border" /> </div> </div> <!-- 按钮操作 --> <div class="mt-5"> <button @click="exportToPDF" class="px-4 py-2 bg-blue-500 text-white rounded">导出 PDF</button> <button @click="printPDF" class="px-4 py-2 bg-green-500 text-white rounded">打印</button> </div> </div> </template> <script setup lang="ts"> import { ref } from 'vue'; import html2canvas from 'html2canvas'; import jsPDF from 'jspdf'; import printJS from 'print-js'; // 导出为 PDF 并打印 const handleExportToPDF = async () => { const element = document.getElementById('printArea'); if (!element) { console.error('打印区域未找到'); return; } try { // 将 DOM 转为图片 const canvas = await html2canvas(element, { scale: 2, useCORS: true, // 启用跨域支持 }); const imgData = canvas.toDataURL('image/jpeg', 1.0); // 创建 PDF const pdf = new jsPDF('p', 'mm', 'a4'); const pdfWidth = pdf.internal.pageSize.getWidth(); const pdfHeight = (canvas.height * pdfWidth) / canvas.width; pdf.addImage(imgData, 'JPEG', 0, 0, pdfWidth, pdfHeight); // 导出 PDF 文件 Blob URL const pdfBlob = pdf.output('blob'); const pdfUrl = URL.createObjectURL(pdfBlob); // 使用 print-js 打印 PDF printJS({ printable: pdfUrl, type: 'pdf', showModal: true }); } catch (error) { console.error('生成 PDF 失败:', error); } }; </script>
常见问题与解决方法
1. 图片跨域问题
• 原因:html2canvas 对跨域资源有限制。
• 解决方案:
1. 使用 useCORS: true 参数。
2. 确保图片服务器启用了 CORS 并设置 Access-Control-Allow-Origin: *。
2. 样式丢失
• 原因:部分样式未被内联,或者浏览器限制外部样式加载。
• 解决方案:
• 不要直接使用printjs打印,而是转成pdf再打印
最终建议
• 简单需求 :直接使用 print-js 打印。
• 复杂需求 :结合 html2canvas + jspdf 输出 PDF,再通过 print-js 打印,适配性更强。
• 样式控制 :确保打印专用样式定义清晰,避免内容超出页面或影响用户体验。