|
|
|
|
@ -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>
|
|
|
|
|
|