|
|
|
|
@ -0,0 +1,341 @@
|
|
|
|
|
<template>
|
|
|
|
|
<el-dialog
|
|
|
|
|
:visible.sync="dialogVisible"
|
|
|
|
|
title="任务大纲导入"
|
|
|
|
|
width="800px"
|
|
|
|
|
:close-on-click-modal="false"
|
|
|
|
|
@close="handleClose"
|
|
|
|
|
>
|
|
|
|
|
<div class="import-tip">
|
|
|
|
|
<p class="tip-title">
|
|
|
|
|
<i class="el-icon-warning"></i> 导入说明
|
|
|
|
|
</p>
|
|
|
|
|
<p>请先导出当前年份大纲,若需要增加下一年份大纲,请删除ID列,并填写归属年份。</p>
|
|
|
|
|
<p><strong>有ID:</strong>覆盖更新已有记录</p>
|
|
|
|
|
<p><strong>无ID:</strong>新增记录,必须包含「年份」列</p>
|
|
|
|
|
<p><strong>外键校验:</strong>工作项目、性质级别、责任科室必须在系统中存在,否则导入失败</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<el-upload
|
|
|
|
|
ref="upload"
|
|
|
|
|
class="upload-demo"
|
|
|
|
|
action="#"
|
|
|
|
|
drag
|
|
|
|
|
:auto-upload="false"
|
|
|
|
|
:limit="1"
|
|
|
|
|
:on-change="handleFileChange"
|
|
|
|
|
:on-exceed="handleExceed"
|
|
|
|
|
accept=".xls,.xlsx"
|
|
|
|
|
>
|
|
|
|
|
<i class="el-icon-upload"></i>
|
|
|
|
|
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
|
|
|
|
<div class="el-upload__tip" slot="tip">只能上传 xls/xlsx 文件,表头需与导出一致</div>
|
|
|
|
|
</el-upload>
|
|
|
|
|
|
|
|
|
|
<div v-if="parseErrors.length > 0" class="parse-errors">
|
|
|
|
|
<p class="error-title">数据校验失败,无法导入:</p>
|
|
|
|
|
<ul>
|
|
|
|
|
<li v-for="(err, idx) in parseErrors" :key="idx">{{ err }}</li>
|
|
|
|
|
</ul>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div v-if="tableList.length > 0 && parseErrors.length === 0" class="preview-section">
|
|
|
|
|
<div class="title">数据预览(共 {{ tableList.length }} 条)</div>
|
|
|
|
|
<div class="table-wrapper">
|
|
|
|
|
<el-table :data="tableList" border size="small" height="320">
|
|
|
|
|
<el-table-column prop="id" label="ID" width="70" />
|
|
|
|
|
<el-table-column prop="year" label="年份" width="80" />
|
|
|
|
|
<el-table-column prop="menu" label="工作项目" width="100" />
|
|
|
|
|
<el-table-column prop="level" label="性质级别" width="100" />
|
|
|
|
|
<el-table-column prop="work_content" label="工作内容" min-width="120" show-overflow-tooltip />
|
|
|
|
|
<el-table-column prop="work_require" label="要求" min-width="120" show-overflow-tooltip />
|
|
|
|
|
<el-table-column prop="content" label="主要工作" min-width="120" show-overflow-tooltip />
|
|
|
|
|
<el-table-column prop="period" label="工作周期" width="90" />
|
|
|
|
|
<el-table-column prop="require" label="工作步骤" min-width="120" show-overflow-tooltip />
|
|
|
|
|
<el-table-column prop="end_time" label="完成时间" width="90" />
|
|
|
|
|
<el-table-column prop="duty_dep_id" label="责任科室" width="100" show-overflow-tooltip />
|
|
|
|
|
<el-table-column prop="join_dep_id" label="参与科室" width="100" show-overflow-tooltip />
|
|
|
|
|
<el-table-column prop="flow" label="具体流程概述" min-width="120" show-overflow-tooltip />
|
|
|
|
|
<el-table-column prop="resource" label="所需资源概述" min-width="120" show-overflow-tooltip />
|
|
|
|
|
</el-table>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<span slot="footer" class="dialog-footer">
|
|
|
|
|
<el-button @click="handleClose">取 消</el-button>
|
|
|
|
|
<el-button type="primary" :loading="importing" :disabled="tableList.length === 0 || parseErrors.length > 0" @click="doImport">
|
|
|
|
|
确认导入
|
|
|
|
|
</el-button>
|
|
|
|
|
</span>
|
|
|
|
|
</el-dialog>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
import * as XLSX from 'xlsx'
|
|
|
|
|
import { imports } from '@/api/task/newplan.js'
|
|
|
|
|
import { listregulation } from '@/api/lawsfile/regulation.js'
|
|
|
|
|
import { listdept } from '@/api/system/department.js'
|
|
|
|
|
|
|
|
|
|
const COLUMN_MAP = {
|
|
|
|
|
'ID': 'id',
|
|
|
|
|
'年份': 'year',
|
|
|
|
|
'工作项目': 'menu',
|
|
|
|
|
'性质级别': 'level',
|
|
|
|
|
'工作内容': 'work_content',
|
|
|
|
|
'要求': 'work_require',
|
|
|
|
|
'主要工作': 'content',
|
|
|
|
|
'工作周期': 'period',
|
|
|
|
|
'工作步骤': 'require',
|
|
|
|
|
'完成时间': 'end_time',
|
|
|
|
|
'责任科室': 'duty_dep_id',
|
|
|
|
|
'参与科室': 'join_dep_id',
|
|
|
|
|
'具体流程概述': 'flow',
|
|
|
|
|
'所需资源概述': 'resource'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
data() {
|
|
|
|
|
return {
|
|
|
|
|
dialogVisible: false,
|
|
|
|
|
tableList: [],
|
|
|
|
|
parseErrors: [],
|
|
|
|
|
importing: false,
|
|
|
|
|
menuList: [],
|
|
|
|
|
depList: []
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
methods: {
|
|
|
|
|
show() {
|
|
|
|
|
this.dialogVisible = true
|
|
|
|
|
this.tableList = []
|
|
|
|
|
this.parseErrors = []
|
|
|
|
|
this.loadRefData()
|
|
|
|
|
},
|
|
|
|
|
handleClose() {
|
|
|
|
|
this.dialogVisible = false
|
|
|
|
|
this.tableList = []
|
|
|
|
|
this.parseErrors = []
|
|
|
|
|
this.$refs.upload && this.$refs.upload.clearFiles()
|
|
|
|
|
},
|
|
|
|
|
async loadRefData() {
|
|
|
|
|
try {
|
|
|
|
|
const [menuRes, depRes] = await Promise.all([
|
|
|
|
|
listregulation(1, 20),
|
|
|
|
|
listdept()
|
|
|
|
|
])
|
|
|
|
|
this.menuList = Array.isArray(menuRes) ? menuRes : (menuRes?.data || [])
|
|
|
|
|
this.depList = Array.isArray(depRes) ? depRes : (depRes?.data || [])
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error(e)
|
|
|
|
|
this.$message.error('加载工作项目/科室数据失败')
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
handleExceed() {
|
|
|
|
|
this.$message.warning('只能上传一个文件')
|
|
|
|
|
},
|
|
|
|
|
handleFileChange(file) {
|
|
|
|
|
this.parseErrors = []
|
|
|
|
|
this.tableList = []
|
|
|
|
|
if (!file.raw) return
|
|
|
|
|
const reader = new FileReader()
|
|
|
|
|
reader.onload = (e) => {
|
|
|
|
|
try {
|
|
|
|
|
const data = new Uint8Array(e.target.result)
|
|
|
|
|
const workbook = XLSX.read(data, { type: 'array' })
|
|
|
|
|
const firstSheet = workbook.Sheets[workbook.SheetNames[0]]
|
|
|
|
|
const raw = XLSX.utils.sheet_to_json(firstSheet, { header: 1, defval: '' })
|
|
|
|
|
if (!raw || raw.length < 2) {
|
|
|
|
|
this.parseErrors.push('文件无有效数据,至少需要表头和一行数据')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
const headers = raw[0].map(h => String(h || '').trim())
|
|
|
|
|
const rows = raw.slice(1)
|
|
|
|
|
const errors = []
|
|
|
|
|
const list = []
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < rows.length; i++) {
|
|
|
|
|
const row = rows[i]
|
|
|
|
|
const obj = {}
|
|
|
|
|
headers.forEach((h, colIdx) => {
|
|
|
|
|
const key = COLUMN_MAP[h]
|
|
|
|
|
if (key) {
|
|
|
|
|
let val = row[colIdx]
|
|
|
|
|
if (val !== undefined && val !== null) val = String(val).trim()
|
|
|
|
|
else val = ''
|
|
|
|
|
obj[key] = val
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 跳过全空行
|
|
|
|
|
const hasContent = Object.values(obj).some(v => v)
|
|
|
|
|
if (!hasContent) return
|
|
|
|
|
|
|
|
|
|
const rowNo = i + 2 // Excel行号
|
|
|
|
|
|
|
|
|
|
// 无ID时必须要有年份
|
|
|
|
|
const hasId = obj.id && !isNaN(Number(obj.id)) && Number(obj.id) > 0
|
|
|
|
|
if (!hasId) {
|
|
|
|
|
if (!obj.year) {
|
|
|
|
|
errors.push(`第${rowNo}行:新增记录必须填写「年份」`)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 必填项
|
|
|
|
|
if (!obj.menu) errors.push(`第${rowNo}行:工作项目不能为空`)
|
|
|
|
|
if (!obj.level) errors.push(`第${rowNo}行:性质级别不能为空`)
|
|
|
|
|
if (!obj.period) errors.push(`第${rowNo}行:工作周期不能为空`)
|
|
|
|
|
if (!obj.end_time) errors.push(`第${rowNo}行:完成时间不能为空`)
|
|
|
|
|
|
|
|
|
|
// 工作项目、性质级别(regulation)校验
|
|
|
|
|
const menuItem = this.menuList.find(m => m.name === obj.menu)
|
|
|
|
|
if (obj.menu && !menuItem) {
|
|
|
|
|
errors.push(`第${rowNo}行:工作项目「${obj.menu}」在系统中不存在`)
|
|
|
|
|
}
|
|
|
|
|
if (menuItem && obj.level) {
|
|
|
|
|
const children = menuItem.children || []
|
|
|
|
|
const levelItem = children.find(c => c.name === obj.level)
|
|
|
|
|
if (!levelItem) {
|
|
|
|
|
errors.push(`第${rowNo}行:性质级别「${obj.level}」在工作项目「${obj.menu}」下不存在`)
|
|
|
|
|
} else {
|
|
|
|
|
obj.menu_id = menuItem.id
|
|
|
|
|
obj.level_id = levelItem.id
|
|
|
|
|
obj.sort = String(menuItem.sort || 0) + String(levelItem.sort || 0)
|
|
|
|
|
}
|
|
|
|
|
} else if (menuItem) {
|
|
|
|
|
obj.menu_id = menuItem.id
|
|
|
|
|
obj.sort = String(menuItem.sort || 0)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 责任科室校验(存储为名称,需存在于科室列表)
|
|
|
|
|
if (obj.duty_dep_id) {
|
|
|
|
|
const depExists = this.depList.some(d => d.name === obj.duty_dep_id)
|
|
|
|
|
if (!depExists) {
|
|
|
|
|
errors.push(`第${rowNo}行:责任科室「${obj.duty_dep_id}」在系统中不存在`)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
obj.name = `${obj.year || new Date().getFullYear()}任务大纲`
|
|
|
|
|
if (hasId) obj.id = Number(obj.id)
|
|
|
|
|
else delete obj.id
|
|
|
|
|
|
|
|
|
|
list.push(obj)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (errors.length > 0) {
|
|
|
|
|
this.parseErrors = errors
|
|
|
|
|
this.tableList = []
|
|
|
|
|
} else {
|
|
|
|
|
this.parseErrors = []
|
|
|
|
|
this.tableList = list
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error(err)
|
|
|
|
|
this.parseErrors.push('文件解析失败:' + (err.message || '未知错误'))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
reader.readAsArrayBuffer(file.raw)
|
|
|
|
|
},
|
|
|
|
|
async doImport() {
|
|
|
|
|
if (this.tableList.length === 0 || this.parseErrors.length > 0) return
|
|
|
|
|
this.importing = true
|
|
|
|
|
try {
|
|
|
|
|
const res = await imports({
|
|
|
|
|
table_name: 'new_plans',
|
|
|
|
|
data: this.tableList
|
|
|
|
|
})
|
|
|
|
|
const total = res.total || this.tableList.length
|
|
|
|
|
const fail = res.fail || 0
|
|
|
|
|
const success = total - fail
|
|
|
|
|
if (fail > 0) {
|
|
|
|
|
this.$message({
|
|
|
|
|
type: 'warning',
|
|
|
|
|
message: `导入完成:成功 ${success} 条,失败 ${fail} 条`,
|
|
|
|
|
duration: 3000
|
|
|
|
|
})
|
|
|
|
|
const errList = res.err || []
|
|
|
|
|
if (errList.length > 0 && errList.length <= 5) {
|
|
|
|
|
errList.forEach(m => this.$message.error(m))
|
|
|
|
|
} else if (errList.length > 5) {
|
|
|
|
|
this.$message.error(errList.slice(0, 3).join(';') + ' ...')
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
this.$message.success(`成功导入 ${total} 条`)
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
this.$message.error(e.message || e.msg || '导入失败')
|
|
|
|
|
} finally {
|
|
|
|
|
this.importing = false
|
|
|
|
|
}
|
|
|
|
|
this.handleClose()
|
|
|
|
|
this.$emit('refresh')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
|
|
.import-tip {
|
|
|
|
|
background: #fef9e7;
|
|
|
|
|
border: 1px solid #f5d76e;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
color: #666;
|
|
|
|
|
line-height: 1.8;
|
|
|
|
|
|
|
|
|
|
.tip-title {
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
color: #e6a23c;
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
|
|
|
|
i {
|
|
|
|
|
margin-right: 4px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.upload-demo {
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.parse-errors {
|
|
|
|
|
padding: 12px;
|
|
|
|
|
background: #fef0f0;
|
|
|
|
|
border: 1px solid #fbc4c4;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
max-height: 160px;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
|
|
|
|
.error-title {
|
|
|
|
|
color: #f56c6c;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ul {
|
|
|
|
|
margin: 0;
|
|
|
|
|
padding-left: 20px;
|
|
|
|
|
color: #666;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.preview-section {
|
|
|
|
|
.title {
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.table-wrapper {
|
|
|
|
|
overflow: auto;
|
|
|
|
|
max-height: 360px;
|
|
|
|
|
|
|
|
|
|
.el-table {
|
|
|
|
|
min-width: 100%;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|