feat(declare): 申报记录支持撤回/作废,导入支持跳过与失败清单
- 申报记录详情弹窗新增撤回、作废(强制放行)入口,覆盖 9 个模块 - 申报管理支持配置允许重复提交与最多同时条数 - 批量导入逐行容错,命中重复时跳过并给出失败清单导出
This commit is contained in:
@@ -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));
|
||||
}
|
||||
@@ -36,6 +36,18 @@ export interface Declare {
|
||||
createTime?: 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已删
|
||||
deleted?: number;
|
||||
// 是否可申报
|
||||
@@ -55,4 +67,4 @@ export interface DeclareParam extends PageParam {
|
||||
formProjectGroup?: string;
|
||||
publicProjectType?: string;
|
||||
publicProjectGroup?: string;
|
||||
}
|
||||
}
|
||||
@@ -99,6 +99,7 @@
|
||||
:module="REVIEW_MODULE"
|
||||
module-name="青马工程"
|
||||
:pk="detailCurrent?.id"
|
||||
@done="reload"
|
||||
/>
|
||||
<ReviewHistoryModal
|
||||
v-model:visible="showReviewHistory"
|
||||
@@ -363,4 +364,4 @@
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import '../toolbar.less';
|
||||
</style>
|
||||
</style>
|
||||
@@ -89,6 +89,7 @@
|
||||
:module="REVIEW_MODULE"
|
||||
module-name="十佳青年岗位能手"
|
||||
:pk="detailCurrent?.id"
|
||||
@done="reload"
|
||||
/>
|
||||
<ReviewHistoryModal
|
||||
v-model:visible="showReviewHistory"
|
||||
@@ -386,4 +387,4 @@
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import '../toolbar.less';
|
||||
</style>
|
||||
</style>
|
||||
@@ -89,6 +89,7 @@
|
||||
:module="REVIEW_MODULE"
|
||||
module-name="十佳团支部书记"
|
||||
:pk="detailCurrent?.id"
|
||||
@done="reload"
|
||||
/>
|
||||
<ReviewHistoryModal
|
||||
v-model:visible="showReviewHistory"
|
||||
@@ -376,4 +377,4 @@
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import '../toolbar.less';
|
||||
</style>
|
||||
</style>
|
||||
@@ -114,6 +114,7 @@
|
||||
module="gxmu_tygl_form"
|
||||
module-name="团员管理"
|
||||
:pk="detailCurrent?.id"
|
||||
@done="reload"
|
||||
/>
|
||||
<ReviewHistoryModal
|
||||
v-model:visible="showReviewHistory"
|
||||
@@ -510,4 +511,4 @@
|
||||
gap: 8px 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -84,6 +84,7 @@
|
||||
:module="REVIEW_MODULE"
|
||||
module-name="挑战杯"
|
||||
:pk="detailCurrent?.id"
|
||||
@done="reload"
|
||||
/>
|
||||
<ReviewHistoryModal
|
||||
v-model:visible="showReviewHistory"
|
||||
@@ -289,4 +290,4 @@
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import '../toolbar.less';
|
||||
</style>
|
||||
</style>
|
||||
@@ -89,6 +89,7 @@
|
||||
:module="REVIEW_MODULE"
|
||||
module-name="五四红旗团委"
|
||||
:pk="detailCurrent?.id"
|
||||
@done="reload"
|
||||
/>
|
||||
<ReviewHistoryModal
|
||||
v-model:visible="showReviewHistory"
|
||||
@@ -444,4 +445,4 @@
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import '../toolbar.less';
|
||||
</style>
|
||||
</style>
|
||||
@@ -89,6 +89,7 @@
|
||||
:module="REVIEW_MODULE"
|
||||
module-name="五四红旗团支部"
|
||||
:pk="detailCurrent?.id"
|
||||
@done="reload"
|
||||
/>
|
||||
<ReviewHistoryModal
|
||||
v-model:visible="showReviewHistory"
|
||||
@@ -478,4 +479,4 @@
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import '../toolbar.less';
|
||||
</style>
|
||||
</style>
|
||||
@@ -105,6 +105,7 @@
|
||||
:module="REVIEW_MODULE"
|
||||
module-name="未来学术之星"
|
||||
:pk="detailCurrent?.id"
|
||||
@done="reload"
|
||||
/>
|
||||
<ReviewHistoryModal
|
||||
v-model:visible="showReviewHistory"
|
||||
@@ -375,4 +376,4 @@
|
||||
gap: 8px 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -89,6 +89,7 @@
|
||||
:module="REVIEW_MODULE"
|
||||
module-name="优秀共青团干部"
|
||||
:pk="detailCurrent?.id"
|
||||
@done="reload"
|
||||
/>
|
||||
<ReviewHistoryModal
|
||||
v-model:visible="showReviewHistory"
|
||||
@@ -409,4 +410,4 @@
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import '../toolbar.less';
|
||||
</style>
|
||||
</style>
|
||||
@@ -333,6 +333,7 @@
|
||||
:module="REVIEW_MODULE"
|
||||
module-name="优秀共青团员"
|
||||
:pk="detailCurrent?.id"
|
||||
@done="reload"
|
||||
/>
|
||||
<ReviewHistoryModal
|
||||
v-model:visible="showReviewHistory"
|
||||
@@ -867,4 +868,4 @@
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -26,6 +26,21 @@
|
||||
</p>
|
||||
<p class="ant-upload-hint">将 Excel 文件拖到此处,或点击上传</p>
|
||||
</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-spin>
|
||||
</ele-modal>
|
||||
@@ -63,6 +78,8 @@
|
||||
}>();
|
||||
|
||||
const loading = ref(false);
|
||||
const failures = ref<{ row: number; reason: string }[]>([]);
|
||||
const successCount = ref(0);
|
||||
|
||||
const description = computed(
|
||||
() =>
|
||||
@@ -141,13 +158,17 @@
|
||||
if (!rows.length) {
|
||||
throw new Error('导入文件内容为空');
|
||||
}
|
||||
const data = rows
|
||||
.map((row, index) => props.createPayload(row, index + 2))
|
||||
.filter((item) =>
|
||||
const entries = rows
|
||||
.map((row, index) => ({
|
||||
rowNo: index + 2,
|
||||
item: props.createPayload(row, index + 2)
|
||||
}))
|
||||
.filter(({ item }) =>
|
||||
props.templateFields.some((field) =>
|
||||
!!String(item[field.key] ?? '').trim()
|
||||
)
|
||||
);
|
||||
const data = entries.map((entry) => entry.item);
|
||||
if (!data.length) {
|
||||
throw new Error('未识别到有效数据');
|
||||
}
|
||||
@@ -155,10 +176,28 @@
|
||||
if (errors.length) {
|
||||
throw new Error(errors.slice(0, 5).join(';'));
|
||||
}
|
||||
for (const item of data) {
|
||||
await props.saveItem(item);
|
||||
failures.value = [];
|
||||
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);
|
||||
emit('done');
|
||||
} catch (e: any) {
|
||||
@@ -169,7 +208,32 @@
|
||||
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) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.import-failures {
|
||||
max-height: 180px;
|
||||
overflow: auto;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -149,6 +149,19 @@
|
||||
</div>
|
||||
</a-form-item>
|
||||
</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-date-picker
|
||||
allow-clear
|
||||
@@ -248,6 +261,8 @@
|
||||
publicProjectGroup: [''],
|
||||
startTime: undefined,
|
||||
endTime: undefined,
|
||||
allowRepeat: 0,
|
||||
maxPending: 1,
|
||||
userId: undefined,
|
||||
tenantId: undefined,
|
||||
createTime: undefined,
|
||||
@@ -517,4 +532,4 @@
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -92,6 +92,7 @@
|
||||
:module="detailCurrent?.module"
|
||||
:module-name="getDeclareModuleLabel(detailCurrent?.module)"
|
||||
:pk="detailCurrent?.id"
|
||||
@done="loadHistoryData"
|
||||
/>
|
||||
<ReviewFlowModal
|
||||
v-model:visible="showReviewFlow"
|
||||
@@ -499,4 +500,4 @@ export default {
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -11,6 +11,24 @@
|
||||
@ok="updateVisible(false)"
|
||||
>
|
||||
<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-tabs v-else v-model:activeKey="activeTab" class="detail-tabs">
|
||||
<a-tab-pane key="sheet" :tab="sheetTabLabel">
|
||||
@@ -193,7 +211,12 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
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 'github-markdown-css/github-markdown-light.css';
|
||||
import AttachmentList from '@/views/gxmu/components/attachmentList.vue';
|
||||
@@ -381,10 +404,81 @@
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
(e: 'done'): void;
|
||||
}>();
|
||||
|
||||
const loading = ref(false);
|
||||
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 loadFailed = ref(false);
|
||||
const activeTab = ref('sheet');
|
||||
@@ -979,4 +1073,17 @@
|
||||
.material-empty {
|
||||
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>
|
||||
@@ -46,6 +46,7 @@
|
||||
:module="dataCurrent?.module"
|
||||
:module-name="dataCurrent?.moduleName"
|
||||
:pk="dataCurrent?.pk"
|
||||
@done="reload"
|
||||
/>
|
||||
<ReviewFlowModal
|
||||
v-model:visible="showFlowVisible"
|
||||
@@ -232,4 +233,4 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
<style lang="less" scoped></style>
|
||||
Reference in New Issue
Block a user