1. 基本知识
对于下述Demo涉及以下知识点:
- Vue 组件基础
使用 defineComponent 定义一个 Vue 组件
script setup 是一种新的 <script>
语法糖,用于简化组件的定义
Dialog 组件用于创建一个弹出对话框,包含 v-model 双向绑定来控制对话框的显示和隐藏
el-upload 组件用于文件上传,提供多种属性和事件来处理文件上传逻辑template #footer
是一个具名插槽,用于定义对话框底部的按钮
- Vue 3 Composition API
ref 和 reactive
ref 用于创建响应式的数据对象,如 dialogVisible、formLoading、fileList 等
reactive 可以创建一个响应式对象,用于更复杂的状态管理
方法和事件
通过 ref 和 defineExpose 提供方法给父组件调用,如 open 方法
详细分析Vue3中的defineExpose(附Demo)
使用 defineEmits 定义组件触发的事件,如 success 事件
详细分析Vue3中的defineEmits基本知识(子传父)
- Element Plus 组件库
Dialog 对话框
Dialog 组件用于创建弹出对话框,使用 v-model 绑定显示状态
width 属性设置对话框的宽度,title 属性设置对话框的标题
el-upload 文件上传el-upload
组件用于文件上传,支持拖拽上传和点击上传action
属性指定上传文件的服务器地址headers
属性用于设置上传请求的头部信息
- 异步操作和 API 调用
API 调用
使用 axios 或其他 HTTP 客户端库进行 API 调用,如 AppointmentCommissionApi.importUserTemplate()
处理文件下载时,使用 download 工具函数下载模板文件
4.2. 异步函数
使用 async/await 语法处理异步操作,确保操作按预期顺序执行
2. Demo
基本的语法知识只是点睛一下
如下Demo:
AppointmentImportForm(负责渲染一个对话框,允许用户上传文件以导入预约委托)
<template>
<Dialog v-model="dialogVisible" title="危品预约委托导入" width="400">
<!-- 文件上传组件 -->
<el-upload
ref="uploadRef"
v-model:file-list="fileList"
:action="importUrl + '?updateSupport=' + updateSupport"
:auto-upload="false"
:disabled="formLoading"
:headers="uploadHeaders"
:limit="1"
:on-error="submitFormError"
:on-exceed="handleExceed"
:on-success="submitFormSuccess"
accept=".xlsx, .xls"
drag
>
<Icon icon="ep:upload" />
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
<!-- 上传提示信息 -->
<template #tip>
<div class="el-upload__tip text-center">
<div class="el-upload__tip">
<el-checkbox v-model="updateSupport" />
是否更新已经存在的委托数据
</div>
<span>仅允许导入 xls、xlsx 格式文件。</span>
<el-link
:underline="false"
style="font-size: 12px; vertical-align: baseline"
type="primary"
@click="importTemplate"
>
下载模板
</el-link>
</div>
</template>
</el-upload>
<!-- 对话框底部的按钮 -->
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
<el-button @click="dialogVisible = false">取 消</el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import * as AppointmentCommissionApi from '@/api/dangerous/appointmentcommission'
import { getAccessToken, getTenantId } from '@/utils/auth'
import download from '@/utils/download'
defineOptions({ name: 'AppointmentImportForm' })
// 消息弹窗
const message = useMessage()
// 对话框的显示控制
const dialogVisible = ref(false)
const formLoading = ref(false)
const uploadRef = ref()
const importUrl = import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL + '/dangerous/appointment-commission/import'
const uploadHeaders = ref()
const fileList = ref([])
const updateSupport = ref(0)
// 打开对话框
const open = () => {
dialogVisible.value = true
updateSupport.value = 0
fileList.value = []
resetForm()
}
defineExpose({ open })
// 提交表单
const submitForm = async () => {
if (fileList.value.length == 0) {
message.error('请上传文件')
return
}
// 设置上传请求头
uploadHeaders.value = {
Authorization: 'Bearer ' + getAccessToken(),
'tenant-id': getTenantId()
}
formLoading.value = true
uploadRef.value!.submit()
}
// 文件上传成功的处理
const emits = defineEmits(['success'])
const submitFormSuccess = (response: any) => {
if (response.code !== 0) {
message.error(response.msg)
formLoading.value = false
return
}
// 构建成功提示信息
const data = response.data
let text = '上传成功数量:' + data.createUsernames.length + ';'
for (let username of data.createUsernames) {
text += '< ' + username + ' >'
}
text += '更新成功数量:' + data.updateUsernames.length + ';'
for (const username of data.updateUsernames) {
text += '< ' + username + ' >'
}
text += '更新失败数量:' + Object.keys(data.failureUsernames).length + ';'
for (const username in data.failureUsernames) {
text += '< ' + username + ': ' + data.failureUsernames[username] + ' >'
}
message.alert(text)
formLoading.value = false
dialogVisible.value = false
emits('success')
}
// 文件上传失败的处理
const submitFormError = (): void => {
message.error('上传失败,请您重新上传!')
formLoading.value = false
}
// 重置表单
const resetForm = async (): Promise<void> => {
formLoading.value = false
uploadRef.value?.clearFiles()
}
// 文件超出数量限制的处理
const handleExceed = (): void => {
message.error('最多只能上传一个文件!')
}
// 下载模板
const importTemplate = async () => {
const res = await AppointmentCommissionApi.importUserTemplate()
download.excel(res, '危品预约委托模版.xls')
}
</script>
按钮触发对话框:(以下为抽取实战中的Demo)
<template>
<el-button
type="warning"
plain
@click="handleImport"
v-hasPermi="['dangerous:appointment-commission:import']"
>
<Icon icon="ep:upload" class="mr-5px" /> 导入
</el-button>
<!-- 用户导入对话框 -->
<AppointmentImportForm ref="importFormRef" @success="getList" />
</template>
<script lang="ts" setup>
import AppointmentImportForm from '@/components/AppointmentImportForm.vue'
import { ref } from 'vue'
import * as AppointmentCommissionApi from '@/api/dangerous/appointmentcommission'
// 引用导入表单组件
const importFormRef = ref()
// 触发导入对话框
const handleImport = () => {
importFormRef.value.open()
}
// 获取列表数据
const getList = async () => {
loading.value = true
try {
const data = await AppointmentCommissionApi.getAppointmentCommissionPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
</script>
以及接口文件:
// 下载危品导入模板
export const importUserTemplate = () => {
return request.download({ url: '/dangerous/appointment-commission/get-import-template' })
}
Demo如下:
3. 彩蛋
由于是Java作为后端,此处补充Java的基本接口:(只提供思路,后端代码给的不全)
对应的上传下载推荐阅读:【Java项目】实战CRUD的功能整理(持续更新)
3.1 下载
其模版接口如下:
@GetMapping("/get-import-template")
@Operation(summary = "获得导入危品委托管理的模板")
public void importTemplate(HttpServletResponse response) throws IOException {
// 手动创建导出 demo
List<AppointmentCommissionImportExcelVO> list = Arrays.asList(
AppointmentCommissionImportExcelVO.builder().chineseShipName("xx").shipVoyage("xx").appointmentCompany("xx").appointmentType("装船")
.appointmentEntryTime(LocalDate.parse("2024-05-29").atStartOfDay()).build(),
AppointmentCommissionImportExcelVO.builder().chineseShipName("xx").shipVoyage("xx").appointmentCompany("xx").appointmentType("卸船")
.appointmentEntryTime(LocalDate.parse("2024-05-29").atStartOfDay()).build()
);
// 输出
ExcelUtils.write(response, "危品预约委托管理导入模板.xls", "危品预约", AppointmentCommissionImportExcelVO.class, list);
}
对应的导入类如下:
package cn.iocoder.yudao.module.dangerous.controller.admin.appointmentcommission.vo;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.time.LocalDateTime;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = false) // 设置 chain = false,避免用户导入有问题
public class AppointmentCommissionImportExcelVO {
@ExcelProperty("中文船名")
private String chineseShipName;
@ExcelProperty("船舶航次")
private String shipVoyage;
@ExcelProperty("预约公司")
private String appointmentCompany;
@ExcelProperty("预约类型")
private String appointmentType;
@ExcelProperty("预约进场时间")
private LocalDateTime appointmentEntryTime;
}
3.2 上传
@PostMapping("/import")
@Operation(summary = "导入危品预约委托")
@Parameters({
@Parameter(name = "file", description = "Excel 文件", required = true),
@Parameter(name = "updateSupport", description = "是否支持更新,默认为 false", example = "true")
})
@PreAuthorize("@ss.hasPermission('dangerous:appointment-commission:import')")
public CommonResult<AppointmentCommissionImportResqVO> importExcel(@RequestParam("file") MultipartFile file,
@RequestParam(value = "updateSupport", required = false, defaultValue = "false") Boolean updateSupport) throws Exception {
List<AppointmentCommissionImportExcelVO> list = ExcelUtils.read(file, AppointmentCommissionImportExcelVO.class);
return success(appointmentCommissionService.importAppointmentCommissionList(list, updateSupport));
}
对应的实现类如下:
@Override
@Transactional(rollbackFor = Exception.class) // 添加事务,异常则回滚所有导入
public AppointmentCommissionImportResqVO importAppointmentCommissionList(List<AppointmentCommissionImportExcelVO> importAppointmentCommissionDatas, boolean isUpdateSupport) {
if(CollUtil.isEmpty(importAppointmentCommissionDatas)){
throw exception(APPOINTMENT_COMMISSION_IMPORT_LIST_IS_EMPTY);
}
AppointmentCommissionImportResqVO respVO = AppointmentCommissionImportResqVO.builder()
.createChineseShipName(new ArrayList<>())
.updateChineseShipName(new ArrayList<>())
.failureChineseShipName(new LinkedHashMap<>())
.build();
importAppointmentCommissionDatas.forEach(importAppointmentCommissionData ->{
try {
AppointmentCommissionDO insertDo = BeanUtils.toBean(importAppointmentCommissionData, AppointmentCommissionDO.class);
String orderNo = redisIdGeneratorService.generatorOrderNo("SQ");
insertDo.setAppointmentId(orderNo);
insertDo.setAppointmentStatus("未提交");
appointmentCommissionMapper.insert(insertDo);
respVO.getCreateChineseShipName().add(importAppointmentCommissionData.getChineseShipName());
} catch (Exception e) {
respVO.getFailureChineseShipName().put(importAppointmentCommissionData.getChineseShipName(), e.getMessage());
}
});
return respVO;
}
对应的类如下:
@Schema(description = "危品导入 Response VO")
@Data
@Builder
public class AppointmentCommissionImportResqVO {
@Schema(description = "创建成功中文船名", requiredMode = Schema.RequiredMode.REQUIRED)
private List<String> createChineseShipName;
@Schema(description = "更新成功的中文船名", requiredMode = Schema.RequiredMode.REQUIRED)
private List<String> updateChineseShipName;
@Schema(description = "导入失败的中文船名,key 为用户名,value 为失败原因", requiredMode = Schema.RequiredMode.REQUIRED)
private Map<String, String> failureChineseShipName;
}