fetch(add): 账户管理
This commit is contained in:
@@ -17,11 +17,7 @@
|
||||
show-icon
|
||||
>
|
||||
<template #default>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
@click="handleDownloadTemplate"
|
||||
>
|
||||
<el-button type="primary" link @click="handleDownloadTemplate">
|
||||
<el-icon><Download /></el-icon>
|
||||
下载模板
|
||||
</el-button>
|
||||
@@ -79,11 +75,7 @@
|
||||
<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"
|
||||
>
|
||||
<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>
|
||||
@@ -109,12 +101,7 @@
|
||||
<el-button @click="handleCancel" :disabled="uploading">
|
||||
{{ importResult ? '关闭' : '取消' }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="!importResult"
|
||||
type="primary"
|
||||
:loading="uploading"
|
||||
@click="handleConfirm"
|
||||
>
|
||||
<el-button v-if="!importResult" type="primary" :loading="uploading" @click="handleConfirm">
|
||||
开始导入
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -123,267 +110,273 @@
|
||||
</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'
|
||||
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
|
||||
interface ImportResult {
|
||||
success: boolean
|
||||
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)
|
||||
detail?: {
|
||||
total?: number
|
||||
success?: number
|
||||
failed?: number
|
||||
}
|
||||
errors?: Array<{
|
||||
row: number
|
||||
message: string
|
||||
}>
|
||||
}
|
||||
})
|
||||
|
||||
const maxSizeMB = computed(() => props.maxSize)
|
||||
const acceptText = computed(() => props.accept.replace(/\./g, '').toUpperCase())
|
||||
|
||||
const handleDownloadTemplate = () => {
|
||||
if (props.templateUrl) {
|
||||
window.open(props.templateUrl, '_blank')
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
title?: string
|
||||
width?: string | number
|
||||
// 模板下载地址
|
||||
templateUrl?: string
|
||||
// 文件上传地址(如果使用 action 方式)
|
||||
uploadAction?: string
|
||||
// 接受的文件类型
|
||||
accept?: string
|
||||
// 最大文件大小(MB)
|
||||
maxSize?: number
|
||||
// 表单验证规则
|
||||
formRules?: FormRules
|
||||
}
|
||||
}
|
||||
|
||||
const handleBeforeUpload = (file: File) => {
|
||||
const isValidType = props.accept.split(',').some(type => {
|
||||
const ext = type.trim()
|
||||
return file.name.toLowerCase().endsWith(ext)
|
||||
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
|
||||
})
|
||||
|
||||
if (!isValidType) {
|
||||
ElMessage.error(`只能上传 ${acceptText.value} 格式的文件`)
|
||||
return false
|
||||
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 isLtSize = file.size / 1024 / 1024 < props.maxSize
|
||||
if (!isLtSize) {
|
||||
ElMessage.error(`文件大小不能超过 ${props.maxSize}MB`)
|
||||
return false
|
||||
const handleBeforeUpload = (file: File) => {
|
||||
const isValidType = props.accept.split(',').some((type) => {
|
||||
const ext = type.trim()
|
||||
return file.name.toLowerCase().endsWith(ext)
|
||||
})
|
||||
|
||||
if (!isValidType) {
|
||||
ElMessage.error(`只能上传 ${acceptText.value} 格式的文件`)
|
||||
return false
|
||||
}
|
||||
|
||||
const isLtSize = file.size / 1024 / 1024 < props.maxSize
|
||||
if (!isLtSize) {
|
||||
ElMessage.error(`文件大小不能超过 ${props.maxSize}MB`)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const handleUploadProgress = (event: UploadProgressEvent) => {
|
||||
uploadProgress.value = Math.round(event.percent)
|
||||
}
|
||||
|
||||
const handleUploadSuccess = () => {
|
||||
uploading.value = false
|
||||
ElMessage.success('导入成功')
|
||||
}
|
||||
|
||||
const handleUploadError = () => {
|
||||
uploading.value = false
|
||||
ElMessage.error('导入失败')
|
||||
}
|
||||
|
||||
const handleConfirm = async () => {
|
||||
if (fileList.value.length === 0) {
|
||||
ElMessage.warning('请选择要导入的文件')
|
||||
return
|
||||
const handleUploadProgress = (event: UploadProgressEvent) => {
|
||||
uploadProgress.value = Math.round(event.percent)
|
||||
}
|
||||
|
||||
// 验证表单(如果有)
|
||||
if (formRef.value) {
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
} catch {
|
||||
ElMessage.warning('请检查表单填写')
|
||||
const handleUploadSuccess = () => {
|
||||
uploading.value = false
|
||||
ElMessage.success('导入成功')
|
||||
}
|
||||
|
||||
const handleUploadError = () => {
|
||||
uploading.value = false
|
||||
ElMessage.error('导入失败')
|
||||
}
|
||||
|
||||
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 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 handleCancel = () => {
|
||||
visible.value = false
|
||||
emit('cancel')
|
||||
}
|
||||
const handleClosed = () => {
|
||||
fileList.value = []
|
||||
formRef.value?.resetFields()
|
||||
formData.value = {}
|
||||
uploadProgress.value = 0
|
||||
importResult.value = undefined
|
||||
}
|
||||
|
||||
const handleClosed = () => {
|
||||
fileList.value = []
|
||||
formRef.value?.resetFields()
|
||||
formData.value = {}
|
||||
uploadProgress.value = 0
|
||||
importResult.value = undefined
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
formData
|
||||
})
|
||||
defineExpose({
|
||||
formData
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.import-dialog-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
.import-dialog-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.template-section {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.upload-area {
|
||||
:deep(.el-upload) {
|
||||
width: 100%;
|
||||
.template-section {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
:deep(.el-upload-dragger) {
|
||||
padding: 40px 20px;
|
||||
}
|
||||
.upload-area {
|
||||
:deep(.el-upload) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.upload-icon {
|
||||
font-size: 48px;
|
||||
color: var(--el-color-primary);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
:deep(.el-upload-dragger) {
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-regular);
|
||||
|
||||
em {
|
||||
.upload-icon {
|
||||
margin-bottom: 16px;
|
||||
font-size: 48px;
|
||||
color: var(--el-color-primary);
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.upload-tip {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-top: 8px;
|
||||
.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 {
|
||||
text-align: center;
|
||||
padding: 20px 0;
|
||||
|
||||
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;
|
||||
.upload-progress {
|
||||
padding: 20px 0;
|
||||
text-align: center;
|
||||
|
||||
p {
|
||||
margin: 4px 0;
|
||||
margin-top: 12px;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.error-list {
|
||||
margin-top: 16px;
|
||||
|
||||
.error-item {
|
||||
padding: 8px 12px;
|
||||
font-size: 13px;
|
||||
.import-result {
|
||||
.result-detail {
|
||||
margin-top: 8px;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
background-color: var(--el-fill-color-light);
|
||||
border-radius: 4px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.error-row {
|
||||
font-weight: 600;
|
||||
color: var(--el-color-danger);
|
||||
p {
|
||||
margin: 4px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.error-msg {
|
||||
color: var(--el-text-color-regular);
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
||||
.import-form {
|
||||
margin-top: 16px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--el-border-color-lighter);
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user