Files
one-pipe-system/src/components/business/ImportDialog.vue
sexygoat 2b3119c549
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m51s
fix(operator): fix operator edit issue
2026-04-10 14:00:21 +08:00

383 lines
9.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<el-dialog
v-model="visible"
:title="title"
:width="width"
:close-on-click-modal="false"
:close-on-press-escape="!uploading"
@closed="handleClosed"
>
<div class="import-dialog-content">
<!-- 下载模板 -->
<div v-if="templateUrl" class="template-section">
<el-alert
title="请先下载导入模板,按照模板格式填写数据"
type="info"
:closable="false"
show-icon
>
<template #default>
<el-button type="primary" link @click="handleDownloadTemplate">
<el-icon><Download /></el-icon>
下载模板
</el-button>
</template>
</el-alert>
</div>
<!-- 文件上传 -->
<el-upload
ref="uploadRef"
class="upload-area"
drag
:action="uploadAction"
:before-upload="handleBeforeUpload"
:on-success="handleUploadSuccess"
:on-error="handleUploadError"
:on-progress="handleUploadProgress"
:file-list="fileList"
:limit="1"
:accept="accept"
:auto-upload="false"
:disabled="uploading"
>
<el-icon class="upload-icon"><UploadFilled /></el-icon>
<div class="upload-text">
<div>将文件拖到此处<em>点击上传</em></div>
<div class="upload-tip">只能上传 {{ acceptText }} 文件且不超过 {{ maxSizeMB }}MB</div>
</div>
</el-upload>
<!-- 上传进度 -->
<div v-if="uploading" class="upload-progress">
<el-progress :percentage="uploadProgress" />
<p>正在上传中请稍候...</p>
</div>
<!-- 导入结果 -->
<div v-if="importResult" class="import-result">
<el-alert
:title="importResult.message"
:type="importResult.success ? 'success' : 'error'"
:closable="false"
show-icon
>
<template v-if="importResult.detail" #default>
<div class="result-detail">
<p v-if="importResult.detail.total">总计{{ importResult.detail.total }} </p>
<p v-if="importResult.detail.success">成功{{ importResult.detail.success }} </p>
<p v-if="importResult.detail.failed">失败{{ importResult.detail.failed }} </p>
</div>
</template>
</el-alert>
<!-- 失败详情 -->
<div v-if="importResult.errors && importResult.errors.length > 0" class="error-list">
<el-divider content-position="left">失败详情</el-divider>
<el-scrollbar max-height="200px">
<div v-for="(error, index) in importResult.errors" :key="index" class="error-item">
<span class="error-row"> {{ error.row }} </span>
<span class="error-msg">{{ error.message }}</span>
</div>
</el-scrollbar>
</div>
</div>
<!-- 额外表单字段 -->
<el-form
v-if="$slots.form"
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
class="import-form"
>
<slot name="form" :form-data="formData" />
</el-form>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleCancel" :disabled="uploading">
{{ importResult ? '关闭' : '取消' }}
</el-button>
<el-button v-if="!importResult" type="primary" :loading="uploading" @click="handleConfirm">
开始导入
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import type {
FormInstance,
FormRules,
UploadInstance,
UploadUserFile,
UploadProgressEvent
} from 'element-plus'
import { ElMessage } from 'element-plus'
import { Download, UploadFilled } from '@element-plus/icons-vue'
interface ImportResult {
success: boolean
message: string
detail?: {
total?: number
success?: number
failed?: number
}
errors?: Array<{
row: number
message: string
}>
}
interface Props {
modelValue: boolean
title?: string
width?: string | number
// 模板下载地址
templateUrl?: string
// 文件上传地址(如果使用 action 方式)
uploadAction?: string
// 接受的文件类型
accept?: string
// 最大文件大小MB
maxSize?: number
// 表单验证规则
formRules?: FormRules
}
interface Emits {
(e: 'update:modelValue', value: boolean): void
(e: 'confirm', file: File, formData?: Record<string, any>): Promise<ImportResult> | void
(e: 'cancel'): void
}
const props = withDefaults(defineProps<Props>(), {
title: '导入数据',
width: '600px',
uploadAction: '#',
accept: '.xlsx,.xls',
maxSize: 10
})
const emit = defineEmits<Emits>()
const uploadRef = ref<UploadInstance>()
const formRef = ref<FormInstance>()
const fileList = ref<UploadUserFile[]>([])
const formData = ref<Record<string, any>>({})
const uploading = ref(false)
const uploadProgress = ref(0)
const importResult = ref<ImportResult>()
const visible = computed({
get: () => props.modelValue,
set: (val) => {
emit('update:modelValue', val)
}
})
const maxSizeMB = computed(() => props.maxSize)
const acceptText = computed(() => props.accept.replace(/\./g, '').toUpperCase())
const handleDownloadTemplate = () => {
if (props.templateUrl) {
window.open(props.templateUrl, '_blank')
}
}
const handleBeforeUpload = (file: File) => {
const isValidType = props.accept.split(',').some((type) => {
const ext = type.trim()
return file.name.toLowerCase().endsWith(ext)
})
if (!isValidType) {
console.log(`只能上传 ${acceptText.value} 格式的文件`)
return false
}
const isLtSize = file.size / 1024 / 1024 < props.maxSize
if (!isLtSize) {
console.log(`文件大小不能超过 ${props.maxSize}MB`)
return false
}
return true
}
const handleUploadProgress = (event: UploadProgressEvent) => {
uploadProgress.value = Math.round(event.percent)
}
const handleUploadSuccess = () => {
uploading.value = false
ElMessage.success('导入成功')
}
const handleUploadError = () => {
uploading.value = false
console.log('导入失败')
}
const handleConfirm = async () => {
if (fileList.value.length === 0) {
ElMessage.warning('请选择要导入的文件')
return
}
// 验证表单(如果有)
if (formRef.value) {
try {
await formRef.value.validate()
} catch {
ElMessage.warning('请检查表单填写')
return
}
}
const file = fileList.value[0].raw
if (!file) return
uploading.value = true
uploadProgress.value = 0
try {
const result = await emit('confirm', file, formData.value)
if (result) {
importResult.value = result
}
} catch (error: any) {
importResult.value = {
success: false,
message: error.message || '导入失败'
}
} finally {
uploading.value = false
}
}
const handleCancel = () => {
visible.value = false
emit('cancel')
}
const handleClosed = () => {
fileList.value = []
formRef.value?.resetFields()
formData.value = {}
uploadProgress.value = 0
importResult.value = undefined
}
defineExpose({
formData
})
</script>
<style scoped lang="scss">
.import-dialog-content {
display: flex;
flex-direction: column;
gap: 16px;
.template-section {
margin-bottom: 8px;
}
.upload-area {
:deep(.el-upload) {
width: 100%;
}
:deep(.el-upload-dragger) {
padding: 40px 20px;
}
.upload-icon {
margin-bottom: 16px;
font-size: 48px;
color: var(--el-color-primary);
}
.upload-text {
font-size: 14px;
color: var(--el-text-color-regular);
em {
font-style: normal;
color: var(--el-color-primary);
}
.upload-tip {
margin-top: 8px;
font-size: 12px;
color: var(--el-text-color-secondary);
}
}
}
.upload-progress {
padding: 20px 0;
text-align: center;
p {
margin-top: 12px;
font-size: 14px;
color: var(--el-text-color-secondary);
}
}
.import-result {
.result-detail {
margin-top: 8px;
font-size: 14px;
line-height: 1.6;
p {
margin: 4px 0;
}
}
.error-list {
margin-top: 16px;
.error-item {
padding: 8px 12px;
margin-bottom: 8px;
font-size: 13px;
line-height: 1.6;
background-color: var(--el-fill-color-light);
border-radius: 4px;
.error-row {
font-weight: 600;
color: var(--el-color-danger);
}
.error-msg {
color: var(--el-text-color-regular);
}
}
}
}
.import-form {
padding-top: 16px;
margin-top: 16px;
border-top: 1px solid var(--el-border-color-lighter);
}
}
.dialog-footer {
display: flex;
gap: 12px;
justify-content: flex-end;
}
</style>