From 6d876ec7dd53aff3a59a61995c1d86d67aa85b7e Mon Sep 17 00:00:00 2001 From: dk <1260500659@qq.com> Date: Mon, 20 Jul 2026 16:09:05 +0800 Subject: [PATCH] =?UTF-8?q?feat(testReport):=20=E6=96=B0=E5=A2=9E=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E6=8A=A5=E5=91=8A=E5=88=86=E7=BB=84=E5=92=8C=E6=8A=A5?= =?UTF-8?q?=E5=91=8A=E7=94=9F=E6=88=90=E5=8A=9F=E8=83=BD=20-=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0ReportTaskState=E5=92=8CReportTaskGenerateState?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E5=AE=9A=E4=B9=89=20-=20=E6=89=A9=E5=B1=95Re?= =?UTF-8?q?portTaskLedgerImportStage=E6=9E=9A=E4=B8=BE=EF=BC=8C=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E6=96=87=E4=BB=B6=E5=AF=BC=E5=85=A5=E9=98=B6=E6=AE=B5?= =?UTF-8?q?=20-=20=E6=96=B0=E5=A2=9EReportTaskLedgerImportRequest=E3=80=81?= =?UTF-8?q?ReportTaskLedgerValidateAttachmentRecord=E7=AD=89=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E5=AE=9A=E4=B9=89=20-=20=E6=B7=BB=E5=8A=A0validateRep?= =?UTF-8?q?ortTaskLedgerApi=E3=80=81importReportTaskLedgerApi=E7=AD=89API?= =?UTF-8?q?=E6=96=B9=E6=B3=95=20-=20=E5=9C=A8=E6=93=8D=E4=BD=9C=E5=88=97?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=88=86=E7=BB=84=E5=92=8C=E7=94=9F=E6=88=90?= =?UTF-8?q?=E6=8A=A5=E5=91=8A=E6=8C=89=E9=92=AE=20-=20=E5=AE=9E=E7=8E=B0Re?= =?UTF-8?q?portTaskGroupDialog=E7=BB=84=E4=BB=B6=E7=94=A8=E4=BA=8E?= =?UTF-8?q?=E7=9B=91=E6=B5=8B=E7=82=B9=E5=88=86=E7=BB=84=E7=AE=A1=E7=90=86?= =?UTF-8?q?=20-=20=E6=B7=BB=E5=8A=A0=E5=88=86=E7=BB=84=E6=8A=A5=E5=91=8A?= =?UTF-8?q?=E4=B8=8B=E8=BD=BD=E5=8A=9F=E8=83=BD=E5=92=8C=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E4=BA=8B=E4=BB=B6=E5=A4=84=E7=90=86=20-=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E6=8A=A5=E5=91=8A=E4=BB=BB=E5=8A=A1=E8=AF=A6=E6=83=85=E5=AF=B9?= =?UTF-8?q?=E8=AF=9D=E6=A1=86=E4=B8=AD=E7=9A=84=E5=88=86=E7=BB=84=E6=8A=A5?= =?UTF-8?q?=E5=91=8A=E6=98=BE=E7=A4=BA=20-=20=E6=9B=B4=E6=96=B0createEmpty?= =?UTF-8?q?LedgerPreparedState=E5=87=BD=E6=95=B0=E5=88=9D=E5=A7=8B?= =?UTF-8?q?=E5=8C=96=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/api/aireport/testReport/index.ts | 23 +- .../aireport/testReport/interface/index.ts | 57 +++++ .../components/ReportTaskDetailDialog.vue | 16 ++ .../components/ReportTaskFormDialog.vue | 18 +- .../components/ReportTaskGroupDialog.vue | 234 ++++++++++++++++++ .../src/views/aireport/testReport/index.vue | 173 ++++++++++++- .../testReport/utils/reportTaskFlow.ts | 2 + .../aireport/testReport/utils/testReport.ts | 10 +- 8 files changed, 518 insertions(+), 15 deletions(-) create mode 100644 frontend/src/views/aireport/testReport/components/ReportTaskGroupDialog.vue diff --git a/frontend/src/api/aireport/testReport/index.ts b/frontend/src/api/aireport/testReport/index.ts index 64da730..eddcecd 100644 --- a/frontend/src/api/aireport/testReport/index.ts +++ b/frontend/src/api/aireport/testReport/index.ts @@ -37,11 +37,32 @@ export const downloadReportTaskLedgerTemplateApi = (): Promise> export const validateReportTaskLedgerApi = (id: string, formData: FormData) => { - return http.post(`${REPORT_TASK_BASE_URL}/${id}/ledger/validate`, formData, { + return http.post(`${REPORT_TASK_BASE_URL}/${id}/ledger/validate`, formData, { headers: { 'Content-Type': 'multipart/form-data' } }) } +export const importReportTaskLedgerApi = (id: string, params: ReportTask.ReportTaskLedgerImportRequest) => { + return http.post(`${REPORT_TASK_BASE_URL}/${id}/ledger/import`, params) +} + +export const listReportTaskPointsApi = (id: string) => { + return http.get(`${REPORT_TASK_BASE_URL}/${id}/point/list`) +} + +export const saveReportTaskGroupsApi = (id: string, params: ReportTask.ReportTaskGroupSaveRequest) => { + return http.post(`${REPORT_TASK_BASE_URL}/${id}/group/save`, params) +} + export const listReportTaskGroupReportsApi = (id: string) => { return http.get(`${REPORT_TASK_BASE_URL}/${id}/group-report/list`) } + +export const generateReportTaskGroupReportsApi = (id: string) => { + return http.post(`${REPORT_TASK_BASE_URL}/${id}/generate`) +} + +export const downloadReportTaskGroupReportApi = (groupReportId: string): Promise> => + http.get(`${REPORT_TASK_BASE_URL}/group-report/${groupReportId}/download`, undefined, { + responseType: 'blob' + }) as unknown as Promise> diff --git a/frontend/src/api/aireport/testReport/interface/index.ts b/frontend/src/api/aireport/testReport/interface/index.ts index 3ce2e4f..7edb995 100644 --- a/frontend/src/api/aireport/testReport/interface/index.ts +++ b/frontend/src/api/aireport/testReport/interface/index.ts @@ -1,9 +1,12 @@ export namespace ReportTask { + export type ReportTaskState = '01' | '02' | '03' | '04' | '05' + export type ReportTaskGenerateState = 0 | 1 | 2 export type ReportTaskGroupReportGenerateState = 0 | 1 | 2 | 3 export type ReportTaskLedgerImportStage = | '选择文件' | '校验文件' | '文件上传' + | '文件导入' | '基础资料维护' | '完成' @@ -41,6 +44,7 @@ export namespace ReportTask { searchBeginTime?: string searchEndTime?: string timeRange?: string[] + state?: ReportTaskState | string pageNum?: number pageSize?: number } @@ -65,12 +69,59 @@ export namespace ReportTask { checkResult?: string } + export interface ReportTaskLedgerImportRequest { + batchId: string + } + export interface ReportTaskLedgerImportStageRecord { stage?: ReportTaskLedgerImportStage | string | null success?: boolean | null message?: string | null } + export interface ReportTaskLedgerValidateAttachmentRecord { + attachmentName?: string | null + attachmentType?: string | null + referenced?: boolean | null + uploaded?: boolean | null + missing?: boolean | null + reuploadAllowed?: boolean | null + message?: string | null + } + + export interface ReportTaskLedgerValidatePointRecord { + rowNo?: number | null + pointKey?: string | null + substationName?: string | null + pointName?: string | null + complete?: boolean | null + missingAttachmentCount?: number | null + testDeviceNoValid?: boolean | null + testDeviceNoMessage?: string | null + clientUnitNameValid?: boolean | null + clientUnitNameMessage?: string | null + attachments?: ReportTaskLedgerValidateAttachmentRecord[] | null + } + + export interface ReportTaskLedgerValidateResult { + sessionId?: string | null + batchId?: string | null + success?: boolean | null + summaryFileName?: string | null + summaryFilePresent?: boolean | null + tempStoragePath?: string | null + uploadedFileCount?: number | null + pointCount?: number | null + missingAttachmentCount?: number | null + canContinueUpload?: boolean | null + requiredSheets?: string[] | null + existingSheets?: string[] | null + missingSheets?: string[] | null + orphanAttachmentNames?: string[] | null + failReasons?: string[] | null + points?: ReportTaskLedgerValidatePointRecord[] | null + } + export interface ReportTaskLedgerImportSnapshotGroupRecord { groupNo?: number | null count?: number | null @@ -100,7 +151,9 @@ export namespace ReportTask { currentStage?: ReportTaskLedgerImportStage | string | null success?: boolean | null summaryFileName?: string | null + tempStoragePath?: string | null totalFileCount?: number + uploadedFileCount?: number pointCount?: number excelAttachmentCount?: number wordAttachmentCount?: number @@ -153,9 +206,13 @@ export namespace ReportTask { export interface ReportTaskLedgerPreparedState { draftId: string batchId: string + validateSessionId: string + validateResult: ReportTaskLedgerValidateResult | null latestImportResult: ReportTaskLedgerImportResult | null snapshot: ReportTaskLedgerImportSnapshot | null hasImported: boolean + validatedBatchId: string pendingReimport: boolean + pendingRevalidate: boolean } } diff --git a/frontend/src/views/aireport/testReport/components/ReportTaskDetailDialog.vue b/frontend/src/views/aireport/testReport/components/ReportTaskDetailDialog.vue index 6fb91fa..8094b87 100644 --- a/frontend/src/views/aireport/testReport/components/ReportTaskDetailDialog.vue +++ b/frontend/src/views/aireport/testReport/components/ReportTaskDetailDialog.vue @@ -44,6 +44,19 @@ + + + @@ -75,10 +88,12 @@ const props = defineProps<{ groupReports: ReportTask.ReportTaskGroupReportRecord[] companyLabelMap: Record standardLabelMap: Record + downloadingGroupReportId?: string | null }>() const emit = defineEmits<{ (event: 'update:visible', value: boolean): void + (event: 'download-group-report', value: ReportTask.ReportTaskGroupReportRecord): void }>() const visibleProxy = computed({ @@ -89,6 +104,7 @@ const visibleProxy = computed({ const companyLabelMap = computed(() => props.companyLabelMap) const standardLabelMap = computed(() => props.standardLabelMap) const groupReports = computed(() => props.groupReports || []) +const downloadingGroupReportId = computed(() => props.downloadingGroupReportId || '') diff --git a/frontend/src/views/aireport/testReport/index.vue b/frontend/src/views/aireport/testReport/index.vue index ed1e62f..9d90b40 100644 --- a/frontend/src/views/aireport/testReport/index.vue +++ b/frontend/src/views/aireport/testReport/index.vue @@ -24,6 +24,17 @@ @@ -71,10 +93,14 @@ import type { ColumnProps, ProTableInstance } from '@/components/ProTable/interf import { createReportTaskApi, deleteReportTasksApi, + downloadReportTaskGroupReportApi, exportReportTasksApi, + generateReportTaskGroupReportsApi, getReportTaskDetailApi, listReportTaskGroupReportsApi, + listReportTaskPointsApi, listReportTasksApi, + saveReportTaskGroupsApi, submitReportTaskAuditApi, updateReportTaskApi } from '@/api/aireport/testReport' @@ -88,6 +114,7 @@ import { useDictStore } from '@/stores/modules/dict' import ReportTaskAuditDialog from './components/ReportTaskAuditDialog.vue' import ReportTaskDetailDialog from './components/ReportTaskDetailDialog.vue' import ReportTaskFormDialog from './components/ReportTaskFormDialog.vue' +import ReportTaskGroupDialog from './components/ReportTaskGroupDialog.vue' import { buildDictSelectOptions, buildOptionLabelMap, @@ -95,6 +122,7 @@ import { normalizeReportTaskGroupReportList, normalizeReportTaskListParams, normalizeReportTaskPage, + normalizeReportTaskPointList, resolveReportTaskCreateUnitText, resolveReportTaskStandardText, resolveReportTaskText, @@ -110,17 +138,23 @@ const proTable = ref() const formDialogVisible = ref(false) const detailDialogVisible = ref(false) const auditDialogVisible = ref(false) +const groupDialogVisible = ref(false) const formSaving = ref(false) const formSaveSuccessVersion = ref(0) const detailLoading = ref(false) const auditSaving = ref(false) +const groupDialogLoading = ref(false) +const groupDialogSaving = ref(false) const formMode = ref<'create' | 'edit'>('create') const currentRecord = ref(null) const detailRecord = ref(null) const detailGroupReports = ref([]) const currentAuditRecord = ref(null) +const currentGroupRecord = ref(null) +const groupPointList = ref([]) const clientUnitOptions = ref>([]) const reportModelOptions = ref>([]) +const downloadingGroupReportId = ref('') const companyOptions = computed(() => buildDictSelectOptions(dictStore.getDictData(DICT_CODES.TEST_REPORT_COMPANY))) const standardOptions = computed(() => buildDictSelectOptions(dictStore.getDictData(DICT_CODES.TEST_REPORT_STANDARD))) @@ -130,6 +164,7 @@ const requiredDictCodes = [DICT_CODES.TEST_REPORT_COMPANY, DICT_CODES.TEST_REPOR const currentSearchParams = computed(() => normalizeReportTaskListParams((proTable.value?.searchParam || {}) as ReportTask.ReportTaskListParams) ) +const isReportTaskGeneratePending = (row: ReportTask.ReportTaskRecord) => Number(row.reportGenerateState ?? 0) === 0 const columns = reactive[]>([ { type: 'selection', fixed: 'left', width: 70 }, @@ -191,7 +226,7 @@ const columns = reactive[]>([ { prop: 'createTime', label: '创建时间', minWidth: 170, render: ({ row }) => resolveReportTaskText(row.createTime) }, { prop: 'updateByName', label: '更新人', minWidth: 120, isShow: false, render: ({ row }) => resolveReportTaskText(row.updateByName || row.updateBy) }, { prop: 'updateTime', label: '更新时间', minWidth: 170, isShow: false, render: ({ row }) => resolveReportTaskText(row.updateTime) }, - { prop: 'operation', label: '操作', fixed: 'right', width: 280 } + { prop: 'operation', label: '操作', fixed: 'right', width: 360 } ]) const ensureReportTaskDictOptionsReady = async () => { @@ -297,6 +332,27 @@ const openEditDialog = async (row: ReportTask.ReportTaskRecord) => { } } +const loadDetailDialogData = async (id: string) => { + const [detailResponse, groupReportsResponse] = await Promise.all([getReportTaskDetailApi(id), listReportTaskGroupReportsApi(id)]) + + detailRecord.value = unwrapReportTaskPayload(detailResponse) + detailGroupReports.value = normalizeReportTaskGroupReportList(groupReportsResponse) +} + +const refreshDetailDialogDataIfMatched = async (id: string) => { + if (!detailDialogVisible.value) return + + const currentDetailId = String(detailRecord.value?.id || '').trim() + if (currentDetailId !== id) return + + detailLoading.value = true + try { + await loadDetailDialogData(id) + } finally { + detailLoading.value = false + } +} + const openDetailDialog = async (row: ReportTask.ReportTaskRecord) => { if (!row.id) { ElMessage.warning('当前报告任务缺少 ID,无法查看详情') @@ -309,13 +365,7 @@ const openDetailDialog = async (row: ReportTask.ReportTaskRecord) => { detailLoading.value = true try { - const [detailResponse, groupReportsResponse] = await Promise.all([ - getReportTaskDetailApi(row.id), - listReportTaskGroupReportsApi(row.id) - ]) - - detailRecord.value = unwrapReportTaskPayload(detailResponse) - detailGroupReports.value = normalizeReportTaskGroupReportList(groupReportsResponse) + await loadDetailDialogData(row.id) } catch (error) { ElMessage.error(getReportTaskErrorMessage(error, '报告任务详情查询失败')) } finally { @@ -323,6 +373,33 @@ const openDetailDialog = async (row: ReportTask.ReportTaskRecord) => { } } +const openGroupDialog = async (row: ReportTask.ReportTaskRecord) => { + if (!row.id) { + ElMessage.warning('当前报告任务缺少 ID,无法维护分组') + return + } + + if (!isReportTaskGeneratePending(row)) { + ElMessage.warning('当前报告任务已生成过报告,不能再修改分组') + return + } + + currentGroupRecord.value = row + groupPointList.value = [] + groupDialogVisible.value = true + groupDialogLoading.value = true + + try { + const response = await listReportTaskPointsApi(row.id) + groupPointList.value = normalizeReportTaskPointList(response) + } catch (error) { + groupDialogVisible.value = false + ElMessage.error(getReportTaskErrorMessage(error, '监测点列表查询失败')) + } finally { + groupDialogLoading.value = false + } +} + const openAuditDialog = (row: ReportTask.ReportTaskRecord) => { if (!row.id) { ElMessage.warning('当前报告任务缺少 ID,无法提交审核') @@ -368,6 +445,86 @@ const handleSubmitAudit = async (params: ReportTask.ReportTaskAuditRequest) => { } } +const handleSaveGroups = async (params: ReportTask.ReportTaskGroupSaveRequest) => { + const reportId = String(currentGroupRecord.value?.id || '').trim() + if (!reportId) { + ElMessage.warning('当前报告任务缺少 ID,无法保存分组') + return + } + + groupDialogSaving.value = true + try { + await saveReportTaskGroupsApi(reportId, params) + ElMessage.success('监测点分组保存成功') + groupDialogVisible.value = false + refreshReportTasks() + await refreshDetailDialogDataIfMatched(reportId) + } catch (error) { + ElMessage.error(getReportTaskErrorMessage(error, '监测点分组保存失败')) + } finally { + groupDialogSaving.value = false + } +} + +const handleGenerateGroupReports = async (row: ReportTask.ReportTaskRecord) => { + if (!row.id) { + ElMessage.warning('当前报告任务缺少 ID,无法生成报告') + return + } + + if (!isReportTaskGeneratePending(row)) { + ElMessage.warning('当前报告任务不是待生成状态,不能重复生成') + return + } + + if (!row.groupCount) { + ElMessage.warning('请先完成监测点分组后再生成报告') + return + } + + try { + await ElMessageBox.confirm('将按当前分组生成所有报告,是否继续?', '生成报告', { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: 'warning' + }) + } catch { + return + } + + try { + await generateReportTaskGroupReportsApi(row.id) + ElMessage.success('报告生成任务已执行完成') + refreshReportTasks() + await refreshDetailDialogDataIfMatched(row.id) + } catch (error) { + ElMessage.error(getReportTaskErrorMessage(error, '报告生成失败')) + } +} + +const handleDownloadGroupReport = async (row: ReportTask.ReportTaskGroupReportRecord) => { + const groupReportId = String(row.id || '').trim() + if (!groupReportId) { + ElMessage.warning('当前分组报告缺少 ID,无法下载') + return + } + + downloadingGroupReportId.value = groupReportId + try { + await useDownloadWithServerFileName( + () => downloadReportTaskGroupReportApi(groupReportId), + row.reportFileName || row.reportName || `分组报告_${row.groupNo || ''}`, + undefined, + false, + '.docx' + ) + } catch (error) { + ElMessage.error(getReportTaskErrorMessage(error, '分组报告下载失败')) + } finally { + downloadingGroupReportId.value = '' + } +} + const deleteReportTasks = async (ids: string[], successMessage: string) => { if (!ids.length) { ElMessage.warning('请选择要删除的报告任务') diff --git a/frontend/src/views/aireport/testReport/utils/reportTaskFlow.ts b/frontend/src/views/aireport/testReport/utils/reportTaskFlow.ts index bd12021..2a1f38c 100644 --- a/frontend/src/views/aireport/testReport/utils/reportTaskFlow.ts +++ b/frontend/src/views/aireport/testReport/utils/reportTaskFlow.ts @@ -39,12 +39,14 @@ export const REPORT_TASK_FLOW_STEPS: ReportTaskFlowStepOption[] = [ export const createEmptyLedgerPreparedState = (): ReportTask.ReportTaskLedgerPreparedState => ({ draftId: '', + batchId: '', validateSessionId: '', validateResult: null, latestImportResult: null, snapshot: null, hasImported: false, validatedBatchId: '', + pendingReimport: false, pendingRevalidate: false }) diff --git a/frontend/src/views/aireport/testReport/utils/testReport.ts b/frontend/src/views/aireport/testReport/utils/testReport.ts index 155ee5e..49435ad 100644 --- a/frontend/src/views/aireport/testReport/utils/testReport.ts +++ b/frontend/src/views/aireport/testReport/utils/testReport.ts @@ -145,9 +145,13 @@ export const normalizeReportTaskPointList = ( export const normalizeReportTaskLedgerImportResult = ( response: ResultData | ReportTask.ReportTaskLedgerImportResult | null | undefined + | ResultData + | ReportTask.ReportTaskLedgerValidateResult ): ReportTask.ReportTaskLedgerImportResult => { - const payload = unwrapReportTaskPayload( - response as ResultData + const payload = unwrapReportTaskPayload< + Partial | null | undefined + >( + response as ResultData> ) const stageList = Array.isArray(payload?.stages) ? payload.stages : [] @@ -169,7 +173,7 @@ export const normalizeReportTaskLedgerImportResult = ( clientReuseCount: payload?.clientReuseCount ?? 0, groupCount: payload?.groupCount ?? 0, failReasons: Array.isArray(payload?.failReasons) ? payload.failReasons.filter(Boolean) : [], - stages: stageList.map(stage => ({ + stages: stageList.map((stage: ReportTask.ReportTaskLedgerImportStageRecord) => ({ stage: stage?.stage || undefined, success: stage?.success ?? false, message: stage?.message || ''