feat(declare): 申报记录支持撤回/作废,导入支持跳过与失败清单

- 申报记录详情弹窗新增撤回、作废(强制放行)入口,覆盖 9 个模块
- 申报管理支持配置允许重复提交与最多同时条数
- 批量导入逐行容错,命中重复时跳过并给出失败清单导出
This commit is contained in:
2026-11-02 19:28:56 +08:00
parent 42fddc5c85
commit 0444846422
17 changed files with 266 additions and 22 deletions
+34
View File
@@ -0,0 +1,34 @@
import request from '@/utils/request';
import type { ApiResult } from '@/api';
/**
* 撤回申报:待审核 -> 草稿
*/
export async function withdrawDeclareRecord(module: string, id: number) {
const res = await request.post<ApiResult<unknown>>(
'/gxmu/declare-record/withdraw',
{ module, id }
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 作废申报(等同强制放行,作废后申报人可重新申报)
*/
export async function voidDeclareRecord(
module: string,
id: number,
reason?: string
) {
const res = await request.post<ApiResult<unknown>>(
'/gxmu/declare-record/void',
{ module, id, reason }
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
+13 -1
View File
@@ -36,6 +36,18 @@ export interface Declare {
createTime?: string; createTime?: string;
// 更新时间 // 更新时间
updateTime?: string; updateTime?: string;
// 是否允许重复提交:0否 1是
allowRepeat?: number;
// 最多同时进行中的申报条数
maxPending?: number;
// 当前用户在该项目下的最新申报状态:0草稿 1待审核 2通过 3驳回 4作废
myStatus?: number;
// 当前用户在该项目下的最新申报记录id
myRecordId?: number;
// 当前用户在该项目下的最新申报记录提交时间
mySubmitTime?: string;
// 当前用户在该项目下的最新申报记录状态备注
myStatusRemark?: string;
// 删除标记:0未删 1已删 // 删除标记:0未删 1已删
deleted?: number; deleted?: number;
// 是否可申报 // 是否可申报
@@ -55,4 +67,4 @@ export interface DeclareParam extends PageParam {
formProjectGroup?: string; formProjectGroup?: string;
publicProjectType?: string; publicProjectType?: string;
publicProjectGroup?: string; publicProjectGroup?: string;
} }
+2 -1
View File
@@ -99,6 +99,7 @@
:module="REVIEW_MODULE" :module="REVIEW_MODULE"
module-name="青马工程" module-name="青马工程"
:pk="detailCurrent?.id" :pk="detailCurrent?.id"
@done="reload"
/> />
<ReviewHistoryModal <ReviewHistoryModal
v-model:visible="showReviewHistory" v-model:visible="showReviewHistory"
@@ -363,4 +364,4 @@
<style lang="less" scoped> <style lang="less" scoped>
@import '../toolbar.less'; @import '../toolbar.less';
</style> </style>
+2 -1
View File
@@ -89,6 +89,7 @@
:module="REVIEW_MODULE" :module="REVIEW_MODULE"
module-name="十佳青年岗位能手" module-name="十佳青年岗位能手"
:pk="detailCurrent?.id" :pk="detailCurrent?.id"
@done="reload"
/> />
<ReviewHistoryModal <ReviewHistoryModal
v-model:visible="showReviewHistory" v-model:visible="showReviewHistory"
@@ -386,4 +387,4 @@
<style lang="less" scoped> <style lang="less" scoped>
@import '../toolbar.less'; @import '../toolbar.less';
</style> </style>
+2 -1
View File
@@ -89,6 +89,7 @@
:module="REVIEW_MODULE" :module="REVIEW_MODULE"
module-name="十佳团支部书记" module-name="十佳团支部书记"
:pk="detailCurrent?.id" :pk="detailCurrent?.id"
@done="reload"
/> />
<ReviewHistoryModal <ReviewHistoryModal
v-model:visible="showReviewHistory" v-model:visible="showReviewHistory"
@@ -376,4 +377,4 @@
<style lang="less" scoped> <style lang="less" scoped>
@import '../toolbar.less'; @import '../toolbar.less';
</style> </style>
+2 -1
View File
@@ -114,6 +114,7 @@
module="gxmu_tygl_form" module="gxmu_tygl_form"
module-name="团员管理" module-name="团员管理"
:pk="detailCurrent?.id" :pk="detailCurrent?.id"
@done="reload"
/> />
<ReviewHistoryModal <ReviewHistoryModal
v-model:visible="showReviewHistory" v-model:visible="showReviewHistory"
@@ -510,4 +511,4 @@
gap: 8px 12px; gap: 8px 12px;
line-height: 1.5; line-height: 1.5;
} }
</style> </style>
+2 -1
View File
@@ -84,6 +84,7 @@
:module="REVIEW_MODULE" :module="REVIEW_MODULE"
module-name="挑战杯" module-name="挑战杯"
:pk="detailCurrent?.id" :pk="detailCurrent?.id"
@done="reload"
/> />
<ReviewHistoryModal <ReviewHistoryModal
v-model:visible="showReviewHistory" v-model:visible="showReviewHistory"
@@ -289,4 +290,4 @@
<style lang="less" scoped> <style lang="less" scoped>
@import '../toolbar.less'; @import '../toolbar.less';
</style> </style>
+2 -1
View File
@@ -89,6 +89,7 @@
:module="REVIEW_MODULE" :module="REVIEW_MODULE"
module-name="五四红旗团委" module-name="五四红旗团委"
:pk="detailCurrent?.id" :pk="detailCurrent?.id"
@done="reload"
/> />
<ReviewHistoryModal <ReviewHistoryModal
v-model:visible="showReviewHistory" v-model:visible="showReviewHistory"
@@ -444,4 +445,4 @@
<style lang="less" scoped> <style lang="less" scoped>
@import '../toolbar.less'; @import '../toolbar.less';
</style> </style>
+2 -1
View File
@@ -89,6 +89,7 @@
:module="REVIEW_MODULE" :module="REVIEW_MODULE"
module-name="五四红旗团支部" module-name="五四红旗团支部"
:pk="detailCurrent?.id" :pk="detailCurrent?.id"
@done="reload"
/> />
<ReviewHistoryModal <ReviewHistoryModal
v-model:visible="showReviewHistory" v-model:visible="showReviewHistory"
@@ -478,4 +479,4 @@
<style lang="less" scoped> <style lang="less" scoped>
@import '../toolbar.less'; @import '../toolbar.less';
</style> </style>
+2 -1
View File
@@ -105,6 +105,7 @@
:module="REVIEW_MODULE" :module="REVIEW_MODULE"
module-name="未来学术之星" module-name="未来学术之星"
:pk="detailCurrent?.id" :pk="detailCurrent?.id"
@done="reload"
/> />
<ReviewHistoryModal <ReviewHistoryModal
v-model:visible="showReviewHistory" v-model:visible="showReviewHistory"
@@ -375,4 +376,4 @@
gap: 8px 12px; gap: 8px 12px;
line-height: 1.5; line-height: 1.5;
} }
</style> </style>
+2 -1
View File
@@ -89,6 +89,7 @@
:module="REVIEW_MODULE" :module="REVIEW_MODULE"
module-name="优秀共青团干部" module-name="优秀共青团干部"
:pk="detailCurrent?.id" :pk="detailCurrent?.id"
@done="reload"
/> />
<ReviewHistoryModal <ReviewHistoryModal
v-model:visible="showReviewHistory" v-model:visible="showReviewHistory"
@@ -409,4 +410,4 @@
<style lang="less" scoped> <style lang="less" scoped>
@import '../toolbar.less'; @import '../toolbar.less';
</style> </style>
+2 -1
View File
@@ -333,6 +333,7 @@
:module="REVIEW_MODULE" :module="REVIEW_MODULE"
module-name="优秀共青团员" module-name="优秀共青团员"
:pk="detailCurrent?.id" :pk="detailCurrent?.id"
@done="reload"
/> />
<ReviewHistoryModal <ReviewHistoryModal
v-model:visible="showReviewHistory" v-model:visible="showReviewHistory"
@@ -867,4 +868,4 @@
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
} }
</style> </style>
+70 -6
View File
@@ -26,6 +26,21 @@
</p> </p>
<p class="ant-upload-hint">将 Excel 文件拖到此处,或点击上传</p> <p class="ant-upload-hint">将 Excel 文件拖到此处,或点击上传</p>
</a-upload-dragger> </a-upload-dragger>
<a-alert
v-if="failures.length"
type="warning"
show-icon
:message="`${successCount} 条导入成功,${failures.length} 条被跳过`"
>
<template #description>
<div class="import-failures">
<div v-for="failure in failures" :key="failure.row">
第 {{ failure.row }} 行:{{ failure.reason }}
</div>
</div>
<a-button size="small" @click="exportFailures">下载失败清单</a-button>
</template>
</a-alert>
</a-space> </a-space>
</a-spin> </a-spin>
</ele-modal> </ele-modal>
@@ -63,6 +78,8 @@
}>(); }>();
const loading = ref(false); const loading = ref(false);
const failures = ref<{ row: number; reason: string }[]>([]);
const successCount = ref(0);
const description = computed( const description = computed(
() => () =>
@@ -141,13 +158,17 @@
if (!rows.length) { if (!rows.length) {
throw new Error('导入文件内容为空'); throw new Error('导入文件内容为空');
} }
const data = rows const entries = rows
.map((row, index) => props.createPayload(row, index + 2)) .map((row, index) => ({
.filter((item) => rowNo: index + 2,
item: props.createPayload(row, index + 2)
}))
.filter(({ item }) =>
props.templateFields.some((field) => props.templateFields.some((field) =>
!!String(item[field.key] ?? '').trim() !!String(item[field.key] ?? '').trim()
) )
); );
const data = entries.map((entry) => entry.item);
if (!data.length) { if (!data.length) {
throw new Error('未识别到有效数据'); throw new Error('未识别到有效数据');
} }
@@ -155,10 +176,28 @@
if (errors.length) { if (errors.length) {
throw new Error(errors.slice(0, 5).join(';')); throw new Error(errors.slice(0, 5).join(';'));
} }
for (const item of data) { failures.value = [];
await props.saveItem(item); successCount.value = 0;
for (const entry of entries) {
try {
await props.saveItem(entry.item);
successCount.value += 1;
} catch (e: any) {
failures.value.push({
row: entry.rowNo,
reason: e?.message || '保存失败'
});
}
} }
message.success(`成功导入 ${data.length} 条${props.moduleName}数据`); if (failures.value.length) {
// 被跳过的是「同一申报项目已有进行中/已通过/被驳回」的记录,保留在清单里供人工处理
message.warning(
`成功 ${successCount.value} 条,跳过 ${failures.value.length} 条`
);
emit('done');
return false;
}
message.success(`成功导入 ${successCount.value} 条${props.moduleName}数据`);
updateVisible(false); updateVisible(false);
emit('done'); emit('done');
} catch (e: any) { } catch (e: any) {
@@ -169,7 +208,32 @@
return false; return false;
}; };
const exportFailures = () => {
if (!failures.value.length) {
return;
}
const sheet = utils.aoa_to_sheet([
['行号', '跳过原因'],
...failures.value.map((item) => [item.row, item.reason])
]);
sheet['!cols'] = [{ wch: 10 }, { wch: 64 }];
writeFile(
{
SheetNames: ['失败清单'],
Sheets: { 失败清单: sheet }
},
`${props.moduleName}导入失败清单.xlsx`
);
};
const updateVisible = (value: boolean) => { const updateVisible = (value: boolean) => {
emit('update:visible', value); emit('update:visible', value);
}; };
</script> </script>
<style lang="less" scoped>
.import-failures {
max-height: 180px;
overflow: auto;
margin-bottom: 8px;
}
</style>
@@ -149,6 +149,19 @@
</div> </div>
</a-form-item> </a-form-item>
</template> </template>
<a-form-item
label="允许重复提交"
name="allowRepeat"
extra="关闭时,同一用户在同一申报项目下同时只能有一条待审核申报;开启后可配置最多同时几条"
>
<a-switch
:checked="form.allowRepeat === 1"
@change="(checked: boolean) => (form.allowRepeat = checked ? 1 : 0)"
/>
</a-form-item>
<a-form-item v-if="form.allowRepeat === 1" label="最多同时几条" name="maxPending">
<a-input-number :min="1" :max="20" class="ele-fluid" v-model:value="form.maxPending" />
</a-form-item>
<a-form-item label="开始时间" name="startTime"> <a-form-item label="开始时间" name="startTime">
<a-date-picker <a-date-picker
allow-clear allow-clear
@@ -248,6 +261,8 @@
publicProjectGroup: [''], publicProjectGroup: [''],
startTime: undefined, startTime: undefined,
endTime: undefined, endTime: undefined,
allowRepeat: 0,
maxPending: 1,
userId: undefined, userId: undefined,
tenantId: undefined, tenantId: undefined,
createTime: undefined, createTime: undefined,
@@ -517,4 +532,4 @@
gap: 8px; gap: 8px;
margin-bottom: 8px; margin-bottom: 8px;
} }
</style> </style>
+2 -1
View File
@@ -92,6 +92,7 @@
:module="detailCurrent?.module" :module="detailCurrent?.module"
:module-name="getDeclareModuleLabel(detailCurrent?.module)" :module-name="getDeclareModuleLabel(detailCurrent?.module)"
:pk="detailCurrent?.id" :pk="detailCurrent?.id"
@done="loadHistoryData"
/> />
<ReviewFlowModal <ReviewFlowModal
v-model:visible="showReviewFlow" v-model:visible="showReviewFlow"
@@ -499,4 +500,4 @@ export default {
line-height: 1.5; line-height: 1.5;
} }
} }
</style> </style>
@@ -11,6 +11,24 @@
@ok="updateVisible(false)" @ok="updateVisible(false)"
> >
<a-spin :spinning="loading"> <a-spin :spinning="loading">
<div v-if="canOperateDeclare" class="declare-actions">
<a-space>
<a-button size="small" :loading="actionLoading" @click="handleWithdraw">
撤回申报
</a-button>
<a-button
size="small"
danger
:loading="actionLoading"
@click="handleVoid"
>
作废申报
</a-button>
</a-space>
<span class="declare-actions-tip">
作废后该申报人可重新申报这个项目;撤回后记录回到草稿,本人可继续修改。
</span>
</div>
<a-empty v-if="!loading && showEmpty" :description="emptyDescription" /> <a-empty v-if="!loading && showEmpty" :description="emptyDescription" />
<a-tabs v-else v-model:activeKey="activeTab" class="detail-tabs"> <a-tabs v-else v-model:activeKey="activeTab" class="detail-tabs">
<a-tab-pane key="sheet" :tab="sheetTabLabel"> <a-tab-pane key="sheet" :tab="sheetTabLabel">
@@ -193,7 +211,12 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import { message } from 'ant-design-vue'; import { Input, Modal, message } from 'ant-design-vue';
import { h } from 'vue';
import {
voidDeclareRecord,
withdrawDeclareRecord
} from '@/api/gxmu/declare-record';
import { toDateString } from 'ele-admin-pro'; import { toDateString } from 'ele-admin-pro';
import 'github-markdown-css/github-markdown-light.css'; import 'github-markdown-css/github-markdown-light.css';
import AttachmentList from '@/views/gxmu/components/attachmentList.vue'; import AttachmentList from '@/views/gxmu/components/attachmentList.vue';
@@ -381,10 +404,81 @@
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:visible', visible: boolean): void; (e: 'update:visible', visible: boolean): void;
(e: 'done'): void;
}>(); }>();
const loading = ref(false); const loading = ref(false);
const maxable = ref(true); const maxable = ref(true);
const actionLoading = ref(false);
/** 走申报项目判重的模块,才显示撤回 / 作废 */
const DECLARE_MODULES = [
'gxmu_sjqn',
'gxmu_sjtbzbsj',
'gxmu_wshqtw',
'gxmu_wshqtzb',
'gxmu_yxgqtdgb',
'gxmu_yxgqty',
'gxmu_tzbcy_form',
'gxmu_qmgc_form',
'gxmu_wxxzx_project'
];
const canOperateDeclare = computed(
() => !!props.module && !!props.pk && DECLARE_MODULES.includes(props.module)
);
const handleWithdraw = () => {
Modal.confirm({
title: '撤回这条申报?',
content:
'撤回后记录回到草稿,申报人可继续修改后重新提交;已经有一级审核通过的记录不能撤回。',
onOk: async () => {
actionLoading.value = true;
try {
await withdrawDeclareRecord(props.module as string, props.pk as number);
message.success('已撤回,申报人可继续修改');
emit('done');
updateVisible(false);
} catch (e: any) {
message.error(e.message || '撤回失败');
} finally {
actionLoading.value = false;
}
}
});
};
const handleVoid = () => {
const reason = ref('');
Modal.confirm({
title: '作废这条申报?',
content: h('div', [
h('p', '作废后该申报人可以重新申报这个项目;作废记录对他仍然可见。'),
h(Input, {
value: reason.value,
placeholder: '作废原因(会展示给申报人,可不填)',
'onUpdate:value': (value: string) => (reason.value = value)
})
]),
onOk: async () => {
actionLoading.value = true;
try {
await voidDeclareRecord(
props.module as string,
props.pk as number,
reason.value || '管理员作废'
);
message.success('已作废,该申报人可重新申报');
emit('done');
updateVisible(false);
} catch (e: any) {
message.error(e.message || '作废失败');
} finally {
actionLoading.value = false;
}
}
});
};
const detail = ref<Record<string, unknown>>({}); const detail = ref<Record<string, unknown>>({});
const loadFailed = ref(false); const loadFailed = ref(false);
const activeTab = ref('sheet'); const activeTab = ref('sheet');
@@ -979,4 +1073,17 @@
.material-empty { .material-empty {
margin: 12px 0; margin: 12px 0;
} }
</style> .declare-actions {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 12px;
}
.declare-actions-tip {
font-size: 12px;
color: #8c8c8c;
}
</style>
+2 -1
View File
@@ -46,6 +46,7 @@
:module="dataCurrent?.module" :module="dataCurrent?.module"
:module-name="dataCurrent?.moduleName" :module-name="dataCurrent?.moduleName"
:pk="dataCurrent?.pk" :pk="dataCurrent?.pk"
@done="reload"
/> />
<ReviewFlowModal <ReviewFlowModal
v-model:visible="showFlowVisible" v-model:visible="showFlowVisible"
@@ -232,4 +233,4 @@
}; };
</script> </script>
<style lang="less" scoped></style> <style lang="less" scoped></style>