头像批量上传

master
lion 5 days ago
parent b1cd19d2a3
commit 8ff1eb1a83

@ -4,76 +4,92 @@
title="批量上传头像"
width="900px"
top="5vh"
custom-class="batch-headimg-dialog"
@close="handleClose"
>
<div class="tip-box">
<p>1. 请先选择目标课程仅会匹配该课程下的学员</p>
<p>2. 图片文件名需与学员姓名完全一致 <code>张三.jpg</code></p>
<p>3. 可更新状态的记录会被导入未匹配重名无效文件会自动跳过</p>
</div>
<div class="dialog-body-inner">
<div class="tip-box">
<p>1. 请先选择目标课程仅会匹配该课程下的学员</p>
<p>2. 图片文件名需与学员姓名完全一致 <code>张三.jpg</code></p>
<p>3. 预览后可勾选需要导入的学员未匹配重名无效文件不可选</p>
</div>
<el-form label-width="90px" size="small">
<el-form-item label="目标课程" required>
<el-select
v-model="courseId"
filterable
clearable
placeholder="请选择课程"
style="width: 100%;"
@change="handleCourseChange"
>
<el-option
v-for="item in courseOptions"
:key="item.id"
:label="formatCourseLabel(item)"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-form>
<el-form label-width="90px" size="small">
<el-form-item label="目标课程" required>
<el-select
v-model="courseId"
filterable
clearable
placeholder="请选择课程"
style="width: 100%;"
@change="handleCourseChange"
<div class="upload-section">
<div class="upload-grid-box">
<el-upload
ref="uploadRef"
class="headimg-upload"
action="#"
multiple
list-type="picture-card"
accept=".png,.jpg,.jpeg,.bmp,.svg,.webp"
:auto-upload="false"
:file-list="fileList"
:on-change="handleFileChange"
:on-remove="handleFileRemove"
>
<i class="el-icon-plus"></i>
</el-upload>
</div>
<div class="el-upload__tip">支持 jpg/png 等图片单张不超过 1MB</div>
</div>
<div class="action-row">
<el-button
type="primary"
size="small"
:loading="previewLoading"
:disabled="!courseId || rawFiles.length === 0"
@click="handlePreview"
>
<el-option
v-for="item in courseOptions"
:key="item.id"
:label="formatCourseLabel(item)"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-form>
<el-upload
ref="uploadRef"
drag
action="#"
multiple
accept=".png,.jpg,.jpeg,.bmp,.svg,.webp"
:auto-upload="false"
:file-list="fileList"
:on-change="handleFileChange"
:on-remove="handleFileRemove"
>
<i class="el-icon-upload"></i>
<div class="el-upload__text">将图片拖到此处<em>点击选择</em></div>
<div slot="tip" class="el-upload__tip">支持 jpg/png 等图片单张不超过 500KB</div>
</el-upload>
<div class="action-row">
<el-button
type="primary"
预览匹配
</el-button>
<span v-if="previewList.length" class="stats-text">
可更新 {{ stats.ready }}
未匹配 {{ stats.unmatched }}
重名 {{ stats.duplicate }}
无效 {{ stats.invalid }}
已选 {{ selectedReadyItems.length }}
</span>
</div>
<el-table
v-if="previewList.length"
ref="previewTable"
:data="previewList"
border
size="small"
:loading="previewLoading"
:disabled="!courseId || rawFiles.length === 0"
@click="handlePreview"
height="220"
style="width: 100%; margin-top: 12px;"
:row-key="getRowKey"
@selection-change="handleSelectionChange"
>
预览匹配
</el-button>
<span v-if="stats.ready" class="stats-text">
可更新 {{ stats.ready }}
未匹配 {{ stats.unmatched }}
重名 {{ stats.duplicate }}
无效 {{ stats.invalid }}
</span>
</div>
<el-table
v-if="previewList.length"
:data="previewList"
border
size="small"
max-height="360"
style="width: 100%; margin-top: 12px;"
>
<el-table-column
type="selection"
width="50"
align="center"
:selectable="rowSelectable"
/>
<el-table-column prop="filename" label="文件名" min-width="140" />
<el-table-column prop="name" label="匹配姓名" width="100" />
<el-table-column prop="username" label="学员姓名" width="100" />
@ -108,16 +124,17 @@
</template>
</el-table-column>
</el-table>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button
type="primary"
:loading="importLoading"
:disabled="readyItems.length === 0"
:disabled="selectedReadyItems.length === 0"
@click="handleImport"
>
确认导入{{ readyItems.length }}
确认导入{{ selectedReadyItems.length }}
</el-button>
</span>
</el-dialog>
@ -139,6 +156,7 @@ export default {
fileList: [],
rawFiles: [],
previewList: [],
selectedRows: [],
stats: {
ready: 0,
unmatched: 0,
@ -150,8 +168,8 @@ export default {
}
},
computed: {
readyItems() {
return this.previewList.filter(item => item.status === 'ready')
selectedReadyItems() {
return this.selectedRows.filter(item => item.status === 'ready')
}
},
methods: {
@ -169,6 +187,7 @@ export default {
this.fileList = []
this.rawFiles = []
this.previewList = []
this.selectedRows = []
this.stats = { ready: 0, unmatched: 0, duplicate: 0, invalid: 0 }
this.previewLoading = false
this.importLoading = false
@ -191,20 +210,56 @@ export default {
},
handleCourseChange() {
this.previewList = []
this.selectedRows = []
this.stats = { ready: 0, unmatched: 0, duplicate: 0, invalid: 0 }
},
handleFileChange(file, fileList) {
this.fileList = fileList
this.rawFiles = fileList.map(item => item.raw).filter(Boolean)
this.previewList = []
this.selectedRows = []
this.stats = { ready: 0, unmatched: 0, duplicate: 0, invalid: 0 }
},
handleFileRemove(file, fileList) {
this.fileList = fileList
this.rawFiles = fileList.map(item => item.raw).filter(Boolean)
this.previewList = []
this.selectedRows = []
this.stats = { ready: 0, unmatched: 0, duplicate: 0, invalid: 0 }
},
getRowKey(row) {
return `${row.index}_${row.filename}`
},
rowSelectable(row) {
return row.status === 'ready'
},
handleSelectionChange(selection) {
this.selectedRows = selection
},
getSortWeight(item) {
if (item.status === 'unmatched') return 0
if (item.current_headimgurl) return 1
if (item.status === 'ready') return 2
if (item.status === 'duplicate') return 3
return 4
},
sortPreviewList(list) {
return [...list].sort((a, b) => {
const weightDiff = this.getSortWeight(a) - this.getSortWeight(b)
if (weightDiff !== 0) return weightDiff
return String(a.filename).localeCompare(String(b.filename), 'zh-CN')
})
},
selectDefaultRows() {
const table = this.$refs.previewTable
if (!table) return
table.clearSelection()
this.previewList.forEach(row => {
if (this.rowSelectable(row)) {
table.toggleRowSelection(row, true)
}
})
},
statusTagType(status) {
const map = {
ready: 'success',
@ -224,16 +279,17 @@ export default {
return
}
const formData = new FormData()
formData.append('course_id', this.courseId)
this.rawFiles.forEach(file => {
formData.append('files[]', file)
})
this.previewLoading = true
try {
const res = await batchHeadimgPreview(formData)
this.previewList = res.list || []
const res = await batchHeadimgPreview({
course_id: this.courseId,
files: this.rawFiles.map(file => ({
filename: file.name,
size: file.size
}))
})
this.previewList = this.sortPreviewList(res.list || [])
this.selectedRows = []
this.stats = Object.assign({
ready: 0,
unmatched: 0,
@ -246,7 +302,12 @@ export default {
return
}
this.$message.success(`预览完成,可更新 ${this.stats.ready}`)
this.$nextTick(() => {
this.selectDefaultRows()
})
const total = res.file_total || this.previewList.length
this.$message.success(`预览完成,共 ${total} 张,可更新 ${this.stats.ready} 条,已默认全选可导入项`)
} catch (error) {
console.error(error)
} finally {
@ -275,8 +336,8 @@ export default {
return data.url
},
async handleImport() {
if (!this.readyItems.length) {
this.$message.warning('没有可导入的数据')
if (!this.selectedReadyItems.length) {
this.$message.warning('请先勾选需要导入的学员')
return
}
@ -285,7 +346,7 @@ export default {
const failedUploads = []
try {
for (const item of this.readyItems) {
for (const item of this.selectedReadyItems) {
const file = this.findRawFile(item.filename)
if (!file) {
failedUploads.push(`${item.filename}:找不到本地文件`)
@ -334,8 +395,14 @@ export default {
</script>
<style scoped lang="scss">
.dialog-body-inner {
height: 100%;
overflow-y: auto;
padding-right: 4px;
}
.tip-box {
margin-bottom: 16px;
margin-bottom: 12px;
padding: 10px 12px;
background: #f4f8ff;
border-radius: 4px;
@ -348,10 +415,59 @@ export default {
}
}
.upload-section {
margin-bottom: 8px;
}
.upload-grid-box {
height: 212px;
padding: 8px;
border: 1px dashed #dcdfe6;
border-radius: 4px;
background: #fafafa;
overflow-y: auto;
}
.headimg-upload {
display: flex;
flex-wrap: wrap;
align-content: flex-start;
::v-deep > .el-upload--picture-card {
order: -1;
width: calc(20% - 8px);
height: 96px;
line-height: 94px;
margin: 0 8px 8px 0;
}
::v-deep > .el-upload-list--picture-card {
display: contents;
margin: 0;
}
::v-deep .el-upload-list--picture-card .el-upload-list__item {
width: calc(20% - 8px);
height: 96px;
line-height: 94px;
margin: 0 8px 8px 0;
}
::v-deep .el-upload-list--picture-card .el-upload-list__item-thumbnail {
object-fit: cover;
}
}
.el-upload__tip {
margin-top: 4px;
font-size: 12px;
color: #909399;
}
.action-row {
display: flex;
align-items: center;
margin-top: 12px;
margin-top: 8px;
gap: 12px;
}
@ -366,3 +482,14 @@ export default {
color: #909399;
}
</style>
<style lang="scss">
.batch-headimg-dialog {
.el-dialog__body {
height: 580px;
overflow: hidden;
padding-top: 10px;
padding-bottom: 10px;
}
}
</style>

@ -33,7 +33,7 @@
:on-remove="uploadHeadimgRemove">
<i class="el-icon-plus"></i>
</el-upload>
<div class="el-upload__tip">支持 jpg/png 格式大小不超过 500KB</div>
<div class="el-upload__tip">支持 jpg/png 格式大小不超过 1MB</div>
</div>
</div>
</div>
@ -446,14 +446,14 @@
methods: {
beforeHeadimgUpload(file) {
const isImage = file.type.includes('image')
const isLt500K = file.size / 1024 <= 500
const isLt1M = file.size / 1024 / 1024 <= 1
if (!isImage) {
this.$message.error('请上传正确的图片格式文件')
}
if (!isLt500K) {
this.$message.error('上传文件大小不能超过 500KB')
if (!isLt1M) {
this.$message.error('上传文件大小不能超过 1MB')
}
return isImage && isLt500K
return isImage && isLt1M
},
onHeadimgExceed() {
this.$Message.warning('头像只能上传一张')

Loading…
Cancel
Save