master
lion 3 weeks ago
parent 095318aa6f
commit 36d3319065

@ -4,4 +4,5 @@ ENV = 'development'
# base api
#VUE_APP_BASE_API = 'http://192.168.60.99:8001'
VUE_APP_OUT_URL = 'http://192.168.60.24'
VUE_APP_BASE_API = http://192.168.60.99:9001
# 本地开发使用 localhost远程可使用 http://192.168.60.99:9001
VUE_APP_BASE_API = http://localhost:9001

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

@ -19,6 +19,8 @@
<Button type="primary" @click="getList"></Button>
<Button type="primary" style="margin-left: 10px;" @click="editorOutline('','add')"></Button>
<Button type="primary" style="margin-left: 10px;" @click="exportTable"></Button>
<Button type="primary" style="margin-left: 10px;" @click="showImport"></Button>
</div>
</slot>
</lx-header>
@ -62,6 +64,7 @@
<addOutline ref="addOutline" @refresh="getList"></addOutline>
<addPlan ref="addPlan" @refresh="getList"></addPlan>
<addUnit ref="addUnit" @toUnit="getList" @refresh="getList"></addUnit>
<importOutline ref="importOutline" @refresh="getList"></importOutline>
</div>
</template>
@ -80,12 +83,16 @@
import addOutline from '../list/components/addOutline.vue'
import addPlan from '../list/components/addPlan.vue'
import addUnit from '../list/components/addUnit.vue'
import importOutline from '../list/components/importOutline.vue'
import state from '@/store/modules/user.js'
import * as XLSX from 'xlsx'
import { saveAs } from 'file-saver'
export default {
components: {
addOutline,
addPlan,
addUnit,
importOutline,
},
data() {
return {
@ -173,6 +180,24 @@
width: 240,
align: 'left'
}],
// id
exportColumns: [
{ label: 'ID', prop: 'id' },
{ label: '年份', prop: 'year' },
{ label: '工作项目', prop: 'menu' },
{ label: '性质级别', prop: 'level' },
{ label: '工作内容', prop: 'work_content' },
{ label: '要求', prop: 'work_require' },
{ label: '主要工作', prop: 'content' },
{ label: '工作周期', prop: 'period' },
{ label: '工作步骤', prop: 'require' },
{ label: '完成时间', prop: 'end_time' },
{ label: '责任科室', prop: 'duty_dep_id' },
{ label: '参与科室', prop: 'join_dep_id' },
{ label: '具体流程概述', prop: 'flow' },
{ label: '所需资源概述', prop: 'resource' },
{ label: '是否已创建计划或专项任务', prop: 'has_mission_status' }
]
}
},
computed: {
@ -263,6 +288,47 @@
toUrlUnit(e) {
this.$router.push('/task/list/unit_4')
},
//
exportTable() {
if (!this.mission_log || this.mission_log.length === 0) {
this.$Message.warning('暂无数据可导出')
return
}
const headers = this.exportColumns.map(i => ({
key: i.prop,
title: i.label
}))
const exportData = this.mission_log.map(row => {
const item = { ...row, year: row.year || this.select.year }
//
if (row.mission_plans && row.mission_plans.length > 0) {
item.has_mission_status = '已创建计划'
} else if (row.missions && row.missions.length > 0) {
item.has_mission_status = '已创建专项任务'
} else {
item.has_mission_status = ''
}
return item
})
const data = exportData.map(row => headers.map(header => row[header.key] ?? ''))
data.unshift(headers.map(header => header.title))
const wb = XLSX.utils.book_new()
const ws = XLSX.utils.aoa_to_sheet(data)
const sheetName = `${this.select.year}年任务大纲`
XLSX.utils.book_append_sheet(wb, ws, sheetName)
const wbout = XLSX.write(wb, {
bookType: 'xlsx',
bookSST: true,
type: 'array'
})
saveAs(new Blob([wbout], {
type: 'application/octet-stream'
}), `任务大纲_${this.select.year}.xlsx`)
this.$Message.success('导出成功')
},
showImport() {
this.$refs.importOutline.show()
}
},
watch: {}

Loading…
Cancel
Save