<template>
|
<TableLayout :permissions="['business:goodsImportTask:query']">
|
<el-form ref="searchForm" slot="search-form" :model="searchForm" label-width="100px" inline>
|
<el-form-item label="任务状态" prop="status">
|
<el-select v-model="searchForm.status" clearable placeholder="全部">
|
<el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value" />
|
</el-select>
|
</el-form-item>
|
<section>
|
<el-button type="primary" @click="search">搜索</el-button>
|
<el-button @click="reset">重置</el-button>
|
</section>
|
</el-form>
|
<template v-slot:table-wrap>
|
<ul class="toolbar">
|
<li>
|
<el-button type="primary" :loading="uploading" @click="pickFile" v-permissions="['business:goodsImportTask:create']">提交异步导入</el-button>
|
</li>
|
<li>
|
<el-button :loading="downloading" @click="downloadTemplate" v-permissions="['business:goods:exportExcel']">下载异步导入模板</el-button>
|
</li>
|
</ul>
|
<input ref="upload" type="file" accept=".xlsx,.xls" style="display:none" @change="uploadFile" />
|
<el-table v-loading="isWorking.search" :data="tableData.list" stripe border>
|
<el-table-column prop="id" label="任务ID" width="90" />
|
<el-table-column prop="fileName" label="文件名" min-width="180" show-overflow-tooltip />
|
<el-table-column label="状态" width="100">
|
<template slot-scope="{ row }">
|
<el-tag :type="statusTagType(row.status)" size="small">{{ row.statusText }}</el-tag>
|
</template>
|
</el-table-column>
|
<el-table-column label="进度" width="160">
|
<template slot-scope="{ row }">
|
<el-progress :percentage="row.progress || 0" :status="progressStatus(row.status)" />
|
</template>
|
</el-table-column>
|
<el-table-column prop="totalRows" label="总行数" width="90" />
|
<el-table-column prop="successRows" label="成功" width="80" />
|
<el-table-column prop="failedRows" label="失败" width="80" />
|
<el-table-column label="创建时间" min-width="160">
|
<template slot-scope="{ row }">{{ formatDateTime(row.createDate) }}</template>
|
</el-table-column>
|
<el-table-column label="操作" width="140" fixed="right">
|
<template slot-scope="{ row }">
|
<el-button type="text" @click="showDetail(row)">详情</el-button>
|
<el-button
|
type="text"
|
class="btn-delete"
|
icon="el-icon-delete"
|
:disabled="row.status === 0 || row.status === 1"
|
@click="deleteRow(row)"
|
v-permissions="['business:goodsImportTask:delete']"
|
>删除</el-button>
|
</template>
|
</el-table-column>
|
</el-table>
|
<pagination @size-change="handleSizeChange" @current-change="handlePageChange" :pagination="tableData.pagination" />
|
</template>
|
<el-dialog title="导入任务详情" :visible.sync="detailVisible" width="720px" append-to-body>
|
<template v-if="detail">
|
<el-descriptions :column="2" border size="small">
|
<el-descriptions-item label="任务ID">{{ detail.id }}</el-descriptions-item>
|
<el-descriptions-item label="状态">{{ detail.statusText }}</el-descriptions-item>
|
<el-descriptions-item label="文件名" :span="2">{{ detail.fileName }}</el-descriptions-item>
|
<el-descriptions-item label="进度">{{ detail.progress }}%</el-descriptions-item>
|
<el-descriptions-item label="总行数">{{ detail.totalRows }}</el-descriptions-item>
|
<el-descriptions-item label="成功">{{ detail.successRows }}</el-descriptions-item>
|
<el-descriptions-item label="失败">{{ detail.failedRows }}</el-descriptions-item>
|
<el-descriptions-item label="摘要" :span="2">{{ detail.errorMessage || '—' }}</el-descriptions-item>
|
</el-descriptions>
|
<div v-if="errorLines.length" class="error_detail">
|
<div class="error_detail_title">失败明细</div>
|
<div v-for="(line, idx) in errorLines" :key="idx" class="error_detail_line">{{ line }}</div>
|
</div>
|
</template>
|
</el-dialog>
|
</TableLayout>
|
</template>
|
|
<script>
|
import BaseTable from '@/components/base/BaseTable'
|
import TableLayout from '@/layouts/TableLayout'
|
import Pagination from '@/components/common/Pagination'
|
import {
|
exportImportTemplate,
|
importExcelAsync,
|
fetchTaskById,
|
hasUnfinishedTask,
|
deleteById
|
} from '@/api/business/goodsImportTask'
|
|
export default {
|
name: 'GoodsImportTask',
|
extends: BaseTable,
|
components: { TableLayout, Pagination },
|
data () {
|
return {
|
searchForm: { status: '' },
|
statusOptions: [
|
{ label: '待处理', value: 0 },
|
{ label: '处理中', value: 1 },
|
{ label: '成功', value: 2 },
|
{ label: '失败', value: 3 }
|
],
|
uploading: false,
|
downloading: false,
|
detailVisible: false,
|
detail: null,
|
errorLines: [],
|
pollTimer: null
|
}
|
},
|
created () {
|
this.config({
|
module: '商品导入任务',
|
api: '/business/goodsImportTask',
|
'field.id': 'id',
|
'field.main': 'fileName'
|
})
|
this.search()
|
this.startPolling()
|
},
|
beforeDestroy () {
|
this.stopPolling()
|
},
|
methods: {
|
formatDateTime (val) {
|
if (!val) return '—'
|
const d = new Date(val)
|
if (Number.isNaN(d.getTime())) return val
|
const p = n => String(n).padStart(2, '0')
|
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`
|
},
|
statusTagType (status) {
|
if (status === 2) return 'success'
|
if (status === 3) return 'danger'
|
if (status === 1) return 'warning'
|
return 'info'
|
},
|
progressStatus (status) {
|
if (status === 2) return 'success'
|
if (status === 3) return 'exception'
|
return null
|
},
|
downloadTemplate () {
|
this.downloading = true
|
exportImportTemplate({})
|
.then(res => this.download(res))
|
.catch(e => this.$tip.apiFailed(e))
|
.finally(() => { this.downloading = false })
|
},
|
pickFile () {
|
hasUnfinishedTask()
|
.then(res => {
|
if (res) {
|
this.$message.warning('当前存在未完成的导入任务,请等待完成后再提交')
|
return
|
}
|
this.$refs.upload.click()
|
})
|
.catch(e => this.$tip.apiFailed(e))
|
},
|
uploadFile (e) {
|
const file = e.target.files && e.target.files[0]
|
this.$refs.upload.value = null
|
if (!file) return
|
const formData = new FormData()
|
formData.append('file', file)
|
this.uploading = true
|
importExcelAsync(formData)
|
.then(() => {
|
this.$message.success('导入任务已提交,请在列表中查看进度')
|
this.search()
|
this.startPolling()
|
})
|
.catch(err => this.$message.error(err.message || '提交失败'))
|
.finally(() => { this.uploading = false })
|
},
|
showDetail (row) {
|
fetchTaskById(row.id)
|
.then(res => {
|
this.detail = res.data || res
|
this.errorLines = []
|
if (this.detail.errorDetail) {
|
try {
|
const parsed = JSON.parse(this.detail.errorDetail)
|
this.errorLines = Array.isArray(parsed) ? parsed : [this.detail.errorDetail]
|
} catch (err) {
|
this.errorLines = [this.detail.errorDetail]
|
}
|
}
|
this.detailVisible = true
|
})
|
.catch(e => this.$tip.apiFailed(e))
|
},
|
deleteRow (row) {
|
if (row.status === 0 || row.status === 1) {
|
this.$message.warning('待处理或处理中的任务无法删除')
|
return
|
}
|
this.$confirm(`确认删除导入任务【${row.fileName || row.id}】?`, '提示', { type: 'warning' })
|
.then(() => deleteById(row.id))
|
.then(() => {
|
this.$message.success('删除成功')
|
this.search()
|
})
|
.catch(err => {
|
if (err !== 'cancel') {
|
this.$tip.apiFailed(err)
|
}
|
})
|
},
|
hasRunningTask () {
|
return (this.tableData.list || []).some(item => item.status === 0 || item.status === 1)
|
},
|
startPolling () {
|
this.stopPolling()
|
this.pollTimer = setInterval(() => {
|
if (this.hasRunningTask()) {
|
this.handlePageChange()
|
}
|
}, 3000)
|
},
|
stopPolling () {
|
if (this.pollTimer) {
|
clearInterval(this.pollTimer)
|
this.pollTimer = null
|
}
|
}
|
}
|
}
|
</script>
|
|
<style scoped>
|
.error_detail {
|
margin-top: 16px;
|
max-height: 280px;
|
overflow-y: auto;
|
padding: 12px;
|
background: #fef0f0;
|
border-radius: 6px;
|
border: 1px solid #fde2e2;
|
}
|
.error_detail_title {
|
font-weight: 600;
|
margin-bottom: 8px;
|
color: #f56c6c;
|
}
|
.error_detail_line {
|
font-size: 13px;
|
line-height: 1.6;
|
color: #606266;
|
}
|
</style>
|