feat: 俱乐部增加导入社团

This commit is contained in:
2026-10-23 16:58:42 +08:00
parent bb4c44443d
commit 01e2443db8
9 changed files with 936 additions and 2 deletions
+1
View File
@@ -97,6 +97,7 @@ xsssql:
urls: /platform/cms/content/article/addDo,/platform/sys/app/conf/addDo,/platform/sys/app/conf/editDo
beetl:
RESOURCE_LOADER: com.budwk.app.web.commons.ext.beetl.BeetlCustomResourceLoader
RESOURCE:
#本地路径,开发时设置本地路径,便于调试
rootLocal: '${framework.beetl.RESOURCE.rootLocal}'
@@ -8,6 +8,7 @@ const branchUnionManage = {
<el-button type="primary" class="ml5" size="small" icon="el-icon-search" @click="doSearch"></el-button>
<el-button @click="openAudit" icon="el-icon-s-check" type="primary" style="margin-left: auto" size="small" v-if="$auth.hasRoleOr(['SYSADMIN','SCHOOL_UNION_ADMIN'])">基层干部审核</el-button>
<el-button @click="openAdd" icon="ti-plus" type="primary" size="small" v-if="$auth.hasPermission('sys.manager.union.add')">新建分工会</el-button>
<el-button @click="openCadreImport" icon="el-icon-upload2" type="primary" size="small" v-if="$auth.hasRole('SYSADMIN')">批量添加分工会干部</el-button>
</el-row>
</el-card>
<el-card class="branch-union-table-card" shadow="never" style="border: 1px solid var(--border-color-lighter); margin-top: 10px">
@@ -39,6 +40,75 @@ const branchUnionManage = {
></el-pagination>
</el-row>
</el-card>
<el-dialog title="批量添加分工会干部" :visible.sync="cadreImportVisible" width="940px" top="5vh"
append-to-body custom-class="union-cadre-import-dialog"
:close-on-click-modal="false" :close-on-press-escape="!cadreImportLoading" :show-close="!cadreImportLoading">
<el-alert title="已存在的记录自动跳过,不重复添加、不覆盖原记录。"
description="已有角色(含禁用关系)、已有在任或待审核干部记录均会跳过,其他有效数据继续导入。"
type="info" :closable="false" show-icon></el-alert>
<div class="union-cadre-import-grid">
<section class="union-cadre-import-panel">
<div class="union-cadre-import-heading"><span class="union-cadre-import-number">1</span>下载并填写模板</div>
<p class="union-cadre-import-muted">模板内附当前工会、角色、届次及所属单位对照表。</p>
<el-button size="small" type="primary" plain icon="el-icon-download" @click="downloadCadreTemplate"
:disabled="cadreImportLoading">下载模板及参数对照表</el-button>
<dl class="union-cadre-import-fields">
<dt>必填参数</dt><dd>工会编码、工号、角色编码</dd>
<dt>选填参数</dt><dd>届次编码,可留空</dd>
<dt>书记角色</dt><dd>所属单位编码必填,多个二级单位用逗号分隔;其他角色留空</dd>
</dl>
<div class="union-cadre-import-note">人员须属于对应分工会;工号与编码按文本填写,请保留模板表头。</div>
</section>
<section class="union-cadre-import-panel">
<div class="union-cadre-import-heading"><span class="union-cadre-import-number">2</span>上传填写好的文件</div>
<p class="union-cadre-import-muted">支持 .xls / .xlsx,每次一个文件,最多1000行有效数据。</p>
<el-upload ref="cadreImportUpload" action="/platform/sys/unionCadreImport/doImport" drag
accept=".xls,.xlsx" :auto-upload="false" :limit="1" :file-list="cadreImportFiles"
:disabled="cadreImportLoading" :headers="{ 'X-Requested-With': 'XMLHttpRequest' }"
:on-change="cadreImportChange" :on-remove="cadreImportRemove" :on-exceed="cadreImportExceed"
:before-upload="beforeCadreImport" :on-success="cadreImportSuccess" :on-error="cadreImportError">
<i :class="cadreImportLoading ? 'el-icon-loading' : 'el-icon-upload'"></i>
<div class="el-upload__text" v-if="cadreImportLoading">正在导入,请稍候…</div>
<div class="el-upload__text" v-else>将文件拖到此处,或<em>点击选择文件</em></div>
</el-upload>
<div class="union-cadre-import-note">选择文件后点击“开始导入”。逐行保存,一行失败不影响其他行。</div>
</section>
</div>
<section v-if="cadreImportResult" class="union-cadre-import-results">
<div class="union-cadre-import-heading">导入结果</div>
<div class="union-cadre-import-summary">
<div class="union-cadre-import-stat is-success"><span>成功添加</span><strong>{{ cadreImportResult.success }}<small>行</small></strong></div>
<div class="union-cadre-import-stat is-skipped"><span>已跳过</span><strong>{{ cadreImportResult.skipped }}<small>行</small></strong></div>
<div class="union-cadre-import-stat is-failed"><span>导入失败</span><strong>{{ cadreImportResult.failed }}<small>行</small></strong></div>
</div>
<el-radio-group v-model="cadreImportFilter" size="small" class="union-cadre-import-filters">
<el-radio-button label="全部">全部</el-radio-button>
<el-radio-button label="成功">成功</el-radio-button>
<el-radio-button label="跳过">跳过</el-radio-button>
<el-radio-button label="失败">失败</el-radio-button>
</el-radio-group>
<el-table :data="cadreImportRows" border stripe size="small" max-height="280" empty-text="暂无此类导入记录">
<el-table-column prop="rowNumber" label="Excel行号" width="90"></el-table-column>
<el-table-column prop="unionCode" label="工会编码" width="100" show-overflow-tooltip></el-table-column>
<el-table-column prop="loginname" label="工号" width="100" show-overflow-tooltip></el-table-column>
<el-table-column prop="roleCode" label="角色编码" min-width="160" show-overflow-tooltip></el-table-column>
<el-table-column label="结果" width="85" align="center">
<template slot-scope="{row}">
<el-tag size="mini" :type="row.status === '成功' ? 'success' : row.status === '跳过' ? 'info' : 'danger'">{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="message" label="处理说明" min-width="240"></el-table-column>
</el-table>
</section>
<div slot="footer" class="union-cadre-import-footer">
<span class="union-cadre-import-muted">{{ cadreImportLoading ? '正在处理,请勿关闭或刷新页面' : '仅系统管理员可操作 · 已存在的记录自动跳过' }}</span>
<div>
<el-button size="small" @click="cadreImportVisible = false" :disabled="cadreImportLoading">关闭</el-button>
<el-button size="small" type="primary" icon="el-icon-upload2" @click="submitCadreImport" :loading="cadreImportLoading"
:disabled="!cadreImportFiles.length">{{ cadreImportLoading ? '正在导入' : '开始导入' }}</el-button>
</div>
</div>
</el-dialog>
<el-dialog :title="formData.id ? '编辑分工会' : '新增分工会'" :visible.sync="dialogFormVisible" width="600px">
<el-form :model="formData" ref="formRef" :rules="formRules" size="small" label-width="80px">
<el-form-item label="工会名称" prop="name">
@@ -61,6 +131,11 @@ const branchUnionManage = {
mixins: [initTableMixins],
data() {
return {
cadreImportVisible: false,
cadreImportLoading: false,
cadreImportFiles: [],
cadreImportResult: null,
cadreImportFilter: "全部",
dialogFormVisible: false,
formData: {},
formRules: {
@@ -70,6 +145,12 @@ const branchUnionManage = {
}
}
},
computed: {
cadreImportRows() {
const rows = this.cadreImportResult ? this.cadreImportResult.rows : []
return this.cadreImportFilter === "全部" ? rows : rows.filter(row => row.status === this.cadreImportFilter)
}
},
methods: {
pageData() {
$.get("/platform/sys/union/pageData", this.pageForm).then((res) => {
@@ -77,6 +158,67 @@ const branchUnionManage = {
this.pageForm.totalCount = res.data.totalCount
})
},
openCadreImport() {
this.cadreImportFiles = []
this.cadreImportResult = null
this.cadreImportVisible = true
this.$nextTick(() => this.$refs.cadreImportUpload.clearFiles())
},
downloadCadreTemplate() {
commonUtil.downLoadService("/platform/sys/unionCadreImport/downloadTemplate", {})
},
cadreImportChange(file, fileList) {
if (file.status === "ready") {
this.cadreImportFiles = fileList
this.cadreImportResult = null
}
},
cadreImportRemove() {
this.cadreImportFiles = []
},
cadreImportExceed() {
this.$message.warning("每次只能导入一个文件,请先移除已选文件")
},
beforeCadreImport(file) {
if (!/\.(xls|xlsx)$/i.test(file.name)) {
this.$message.error("请选择xls或xlsx文件")
return false
}
this.cadreImportLoading = true
return true
},
submitCadreImport() {
if (this.cadreImportLoading || !this.cadreImportFiles.length) {
return
}
this.$refs.cadreImportUpload.submit()
},
cadreImportSuccess(res) {
this.cadreImportLoading = false
this.$refs.cadreImportUpload.clearFiles()
this.cadreImportFiles = []
if (!res || res.code !== 0 || !res.data) {
this.$message.error((res && res.msg) || "导入失败,请重新选择文件")
return
}
this.cadreImportFilter = "全部"
this.cadreImportResult = res.data
if (res.data.failed) {
this.$message.warning("导入完成,部分行失败,请查看结果说明")
} else {
this.$message.success("导入完成,请查看结果")
}
if (res.data.success) {
this.pageData()
this.$emit("refresh", null)
}
},
cadreImportError() {
this.cadreImportLoading = false
this.$refs.cadreImportUpload.clearFiles()
this.cadreImportFiles = []
this.$message.error("导入请求失败,可重新选择文件重试,已分配的角色将自动跳过")
},
// 基层干部审核
openAudit() {
this.$emit('union-cadre')
@@ -49,6 +49,37 @@ layout("/layouts/platform.html"){
.union-branch-list-panel .el-pagination-container {
flex-shrink: 0;
}
.union-cadre-import-dialog { max-width: calc(100vw - 32px); }
.union-cadre-import-dialog .el-dialog__body { padding: 16px 24px; max-height: 72vh; overflow-y: auto; }
.union-cadre-import-dialog .el-dialog__header { padding: 20px 24px 16px; border-bottom: 1px solid #ebeef5; }
.union-cadre-import-dialog .el-dialog__footer { padding: 16px 24px; border-top: 1px solid #ebeef5; }
.union-cadre-import-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 20px; }
.union-cadre-import-panel { min-width: 0; padding: 20px; border: 1px solid #e4e7ed; border-radius: 8px; }
.union-cadre-import-heading { display: flex; align-items: center; gap: 10px; font-size: 15px; font-weight: 600; color: #303133; }
.union-cadre-import-number { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: 50%; background: #ecf5ff; color: #409eff; font-size: 13px; }
.union-cadre-import-muted { color: #606266; font-size: 12px; line-height: 1.7; }
.union-cadre-import-fields { display: grid; grid-template-columns: 68px 1fr; gap: 10px; margin: 20px 0 14px; font-size: 12px; line-height: 1.7; }
.union-cadre-import-fields dt { color: #909399; }
.union-cadre-import-fields dd { margin: 0; color: #303133; }
.union-cadre-import-note { margin-top: 14px; color: #606266; font-size: 12px; line-height: 1.8; }
.union-cadre-import-panel .el-upload { display: block; width: 100%; }
.union-cadre-import-panel .el-upload-dragger { width: 100%; height: 180px; background: #fafcff; }
.union-cadre-import-panel .el-upload-dragger .el-icon-loading { display: block; margin: 40px auto 20px; font-size: 40px; color: #409eff; }
.union-cadre-import-results { margin-top: 24px; }
.union-cadre-import-summary { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin: 14px 0; }
.union-cadre-import-stat { display: flex; align-items: center; justify-content: space-between; padding: 14px 18px; border-radius: 6px; font-size: 13px; }
.union-cadre-import-stat strong { font-size: 24px; }
.union-cadre-import-stat small { margin-left: 6px; font-size: 12px; font-weight: normal; }
.union-cadre-import-stat.is-success { color: #529b2e; background: #f0f9eb; }
.union-cadre-import-stat.is-skipped { color: #606266; background: #f4f4f5; }
.union-cadre-import-stat.is-failed { color: #c45656; background: #fef0f0; }
.union-cadre-import-filters { margin-bottom: 12px; }
.union-cadre-import-footer { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; text-align: left; }
@media (max-width: 720px) {
.union-cadre-import-grid { grid-template-columns: 1fr; }
.union-cadre-import-dialog .el-dialog__body { padding: 16px; }
.union-cadre-import-stat { padding: 12px 8px; flex-direction: column; gap: 6px; }
}
</style>
<div id="app" v-cloak>
@@ -0,0 +1,118 @@
const CLUB_REGISTER_IMPORT_TEMPLATE = {
template: `
<el-dialog title="批量导入社团" :visible.sync="visible" width="920px" top="5vh" append-to-body
custom-class="club-register-import-dialog" :close-on-click-modal="false"
:close-on-press-escape="!loading" :show-close="!loading">
<el-alert title="导入后直接通过,无需审批" type="info" :closable="false" show-icon
description="已存在的社团名称或编码自动跳过,不覆盖原有数据。每行独立处理,失败行不影响其他社团。"></el-alert>
<div class="club-register-import-grid">
<section class="club-register-import-panel">
<h4>1. 下载并填写模板</h4>
<el-button type="primary" plain size="small" icon="el-icon-download" @click="download" :disabled="loading">下载模板及参数说明</el-button>
<p><strong>必填:</strong>名称、类型编码、成立日期、会长和秘书长工号、至少3名发起人工号。</p>
<p><strong>人员:</strong>会长和秘书长各1人,副职选填。多人工号用逗号分隔,理事人员不能重复。</p>
<p><strong>选填:</strong>社团编码留空自动生成;联系人、会费、介绍、缩略图及附件等详见模板。</p>
<p class="club-register-import-muted">社团类型来自系统字典,模板内附当前类型对照表。</p>
</section>
<section class="club-register-import-panel">
<h4>2. 上传Excel文件</h4>
<el-upload ref="upload" drag action="/platform/club/infoManage/registerImport/doImport"
accept=".xls,.xlsx" :auto-upload="false" :limit="1" :disabled="loading"
:file-list="files" :headers="{ 'X-Requested-With': 'XMLHttpRequest' }"
:on-change="change" :on-remove="remove" :on-exceed="exceed" :before-upload="beforeUpload"
:on-success="success" :on-error="error">
<i :class="loading ? 'el-icon-loading' : 'el-icon-upload'"></i>
<div class="el-upload__text">{{ loading ? '正在导入,请稍候…' : '拖拽文件到此处,或点击选择文件' }}</div>
</el-upload>
<p class="club-register-import-muted">支持xls、xlsx,每次最多1000行有效数据。保留完整表头,工号与编码按文本填写。</p>
<p class="club-register-import-muted">附件列可留空;填写时使用已上传文件的ID,模板本身不上传附件。</p>
</section>
</div>
<section v-if="result">
<div class="club-register-import-summary">
<el-tag type="success">成功 {{ result.success }} 行</el-tag>
<el-tag type="info">跳过 {{ result.skipped }} 行</el-tag>
<el-tag type="danger">失败 {{ result.failed }} 行</el-tag>
</div>
<el-radio-group v-model="filter" size="small" style="margin-bottom: 12px">
<el-radio-button v-for="item in ['全部', '成功', '跳过', '失败']" :key="item" :label="item">{{ item }}</el-radio-button>
</el-radio-group>
<el-table :data="rows" border stripe size="small" max-height="280" empty-text="暂无此类记录">
<el-table-column prop="rowNumber" label="Excel行号" width="90"></el-table-column>
<el-table-column prop="clubName" label="社团名称" min-width="150" show-overflow-tooltip></el-table-column>
<el-table-column prop="clubCode" label="社团编码" width="120" show-overflow-tooltip></el-table-column>
<el-table-column label="结果" width="80" align="center">
<template slot-scope="{row}"><el-tag size="mini" :type="row.status === '成功' ? 'success' : row.status === '跳过' ? 'info' : 'danger'">{{ row.status }}</el-tag></template>
</el-table-column>
<el-table-column prop="message" label="处理说明" min-width="260"></el-table-column>
</el-table>
</section>
<div slot="footer" class="club-register-import-footer">
<span class="club-register-import-muted">{{ loading ? '正在处理,请勿关闭或刷新页面' : '仅系统管理员可导入 · 已存在自动跳过' }}</span>
<div>
<el-button size="small" @click="visible = false" :disabled="loading">关闭</el-button>
<el-button size="small" type="primary" icon="el-icon-upload2" :loading="loading" :disabled="!files.length" @click="submit">开始导入</el-button>
</div>
</div>
</el-dialog>
`,
data() {
return { visible: false, loading: false, files: [], result: null, filter: "全部" }
},
computed: {
rows() {
const rows = this.result ? this.result.rows : []
return this.filter === "全部" ? rows : rows.filter(row => row.status === this.filter)
}
},
methods: {
open() {
this.files = []
this.result = null
this.filter = "全部"
this.visible = true
this.$nextTick(() => this.$refs.upload.clearFiles())
},
download() {
commonUtil.downLoadService("/platform/club/infoManage/registerImport/downloadTemplate", {})
},
change(file, files) {
if (file.status === "ready") {
this.files = files
this.result = null
}
},
remove() { this.files = [] },
exceed() { this.$message.warning("每次选择一个文件,请先移除已选文件") },
beforeUpload(file) {
if (!/\.(xls|xlsx)$/i.test(file.name)) {
this.$message.error("请选择xls或xlsx文件")
return false
}
this.loading = true
return true
},
submit() {
if (!this.loading && this.files.length) this.$refs.upload.submit()
},
success(res) {
this.loading = false
this.files = []
this.$refs.upload.clearFiles()
if (!res || res.code !== 0 || !res.data) {
this.$message.error((res && res.msg) || "导入失败,请重新选择文件")
return
}
this.result = res.data
this.filter = "全部"
this.$message[res.data.failed ? 'warning' : 'success']("导入完成,请查看逐行处理结果")
if (res.data.success) this.$emit("completed")
},
error() {
this.loading = false
this.files = []
this.$refs.upload.clearFiles()
this.$message.error("导入请求失败,可重新选择文件重试,已存在的社团会自动跳过")
}
}
}
@@ -69,6 +69,24 @@ layout("/layouts/platform.html"){
#app .club-manage-list-card .el-pagination-container {
flex: 0 0 auto;
}
.club-register-import-dialog { max-width: calc(100vw - 32px); }
.club-register-import-dialog .el-dialog__body { padding: 16px 24px; max-height: 72vh; overflow-y: auto; }
.club-register-import-dialog .el-dialog__header { padding: 20px 24px 16px; border-bottom: 1px solid #ebeef5; }
.club-register-import-dialog .el-dialog__footer { padding: 16px 24px; border-top: 1px solid #ebeef5; }
.club-register-import-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin: 20px 0; }
.club-register-import-panel { min-width: 0; padding: 18px; border: 1px solid #e4e7ed; border-radius: 8px; }
.club-register-import-panel h4 { margin: 0 0 16px; color: #303133; }
.club-register-import-panel p { font-size: 12px; line-height: 1.8; }
.club-register-import-muted { color: #606266; font-size: 12px; line-height: 1.7; }
.club-register-import-panel .el-upload, .club-register-import-panel .el-upload-dragger { width: 100%; }
.club-register-import-panel .el-upload-dragger { background: #fafcff; }
.club-register-import-panel .el-icon-loading { display: block; margin: 40px auto 20px; font-size: 40px; color: #409eff; }
.club-register-import-summary { display: flex; gap: 12px; margin-bottom: 16px; }
.club-register-import-footer { display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap; }
@media (max-width: 720px) {
.club-register-import-grid { grid-template-columns: 1fr; }
.club-register-import-dialog .el-dialog__body { padding: 16px; }
}
</style>
<div id="app" v-cloak>
@@ -192,6 +210,7 @@ layout("/layouts/platform.html"){
<!--#include("../../register/apply/clubRegisterForm.js"){}#-->
<!--#include("../../common/clubInfoComponent.js"){}#-->
<!--#include("../../common/clubRoleConstant.js"){}#-->
<!--#include("clubRegisterImport.js"){}#-->
<!--#include("schoolClubManage.js"){}#-->
<!--#include("clubInfoManage.js"){}#-->
new Vue({
@@ -22,6 +22,8 @@ const SCHOOL_CLUB_MANAGE_TEMPLATE = {
<el-card shadow="never" class="mt10 club-manage-list-card">
<table-tool label="社团列表">
<template slot="content">
<el-button v-if="$auth.hasRole('SYSADMIN')" type="primary" size="small" icon="el-icon-upload2"
@click="openImport">批量导入社团</el-button>
<!-- 一级节点展示社团总览时,引导用户从左侧树选择社团查看详细信息。 -->
<span v-if="parentNode.currentTreeNode && parentNode.currentTreeNode.level === 1"
style="display: block; text-align: center; color: #f56c6c;">
@@ -71,10 +73,14 @@ const SCHOOL_CLUB_MANAGE_TEMPLATE = {
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</el-card>
<club-register-import ref="registerImport" @completed="importCompleted"></club-register-import>
</div>
`,
mixins: [initTableMixins, CLUB_FIXED_TABLE_MIXIN],
components: {
"club-register-import": CLUB_REGISTER_IMPORT_TEMPLATE
},
props: {
parentNode: {
type: Object,
@@ -122,7 +128,13 @@ const SCHOOL_CLUB_MANAGE_TEMPLATE = {
this.parentNode.getTreeData()
}
},
openImport() {},
openImport() {
this.$refs.registerImport.open()
},
importCompleted() {
this.doSearch()
this.parentNode.getTreeData()
},
pageData() {
// 查询期间清空旧页并显示加载状态,快速连续操作时只接收最后一次响应。
const requestSequence = ++this.pageRequestSequence