feat(aireport): 新增多模板批任务功能

- 在动态路由中添加 reportTaskMulti 和 testReportMulti 的路径映射
- 新增 TestReportMulti API 接口定义,包括批任务记录、列表参数、模板目录等接口类型
- 实现批任务多模板相关 API 方法,包含列表查询、压缩包验证、创建、详情获取等功能
- 创建 testReportMulti 页面组件,实现批任务管理界面和交互逻辑
- 开发批任务创建对话框组件,支持压缩包上传和子报告配置功能
This commit is contained in:
dk
2026-07-21 15:52:59 +08:00
parent 48ab2567c6
commit ed81e58583
7 changed files with 1297 additions and 1 deletions

View File

@@ -0,0 +1,40 @@
import http from '@/api'
import type { AxiosResponse } from 'axios'
import type { ReportTaskMulti } from './interface'
const REPORT_TASK_MULTI_BASE_URL = '/api/test-report-multi'
export const listReportTaskMultisApi = (params: ReportTaskMulti.ReportTaskMultiListParams) => {
return http.post(`${REPORT_TASK_MULTI_BASE_URL}/list`, params)
}
export const validateReportTaskMultiZipApi = (formData: FormData) => {
return http.post<ReportTaskMulti.ReportTaskMultiValidateResult>(`${REPORT_TASK_MULTI_BASE_URL}/zip/validate`, formData, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
export const createReportTaskMultiApi = (params: ReportTaskMulti.ReportTaskMultiCreateParam) => {
return http.post<boolean>(`${REPORT_TASK_MULTI_BASE_URL}/create`, params)
}
export const getReportTaskMultiDetailApi = (id: string) => {
return http.get<ReportTaskMulti.ReportTaskMultiRecord>(`${REPORT_TASK_MULTI_BASE_URL}/${id}`)
}
export const listReportTaskMultiChildReportsApi = (id: string) => {
return http.get(`${REPORT_TASK_MULTI_BASE_URL}/${id}/child-report/list`)
}
export const downloadReportTaskMultiApi = (id: string): Promise<AxiosResponse<Blob>> =>
http.get(`${REPORT_TASK_MULTI_BASE_URL}/${id}/download`, undefined, {
responseType: 'blob'
}) as unknown as Promise<AxiosResponse<Blob>>
export const deleteReportTaskMultisApi = (ids: string[]) => {
return http.post<boolean>(`${REPORT_TASK_MULTI_BASE_URL}/delete`, ids)
}
export const generateReportTaskMultiApi = (id: string) => {
return http.post<boolean>(`${REPORT_TASK_MULTI_BASE_URL}/${id}/generate`)
}

View File

@@ -0,0 +1,72 @@
export namespace ReportTaskMulti {
export interface ReportTaskMultiRecord {
id?: string
no?: string | null
zipFileName?: string | null
summaryFileName?: string | null
dirCount?: number | null
childReportCount?: number | null
pointCount?: number | null
importState?: number | null
generateState?: number | null
status?: number | null
createBy?: string | null
createByName?: string | null
createTime?: string | null
updateBy?: string | null
updateByName?: string | null
updateTime?: string | null
}
export interface ReportTaskMultiListParams {
keyword?: string
searchBeginTime?: string
searchEndTime?: string
timeRange?: string[]
generateState?: number
pageNum?: number
pageSize?: number
}
export interface ReportTaskMultiTemplateDirRecord {
dirName?: string | null
dirPath?: string | null
pointCount?: number | null
groupCount?: number | null
excelAttachmentCount?: number | null
wordAttachmentCount?: number | null
}
export interface ReportTaskMultiValidateResult {
success?: boolean | null
sessionId?: string | null
zipFileName?: string | null
summaryFileName?: string | null
dirCount?: number | null
childReportCount?: number | null
pointCount?: number | null
excelAttachmentCount?: number | null
wordAttachmentCount?: number | null
orphanAttachmentPaths?: string[] | null
failReasons?: string[] | null
templateDirs?: ReportTaskMultiTemplateDirRecord[] | null
}
export interface ReportTaskMultiChildConfig {
templateDirName: string
templateDirPath: string
no: string
clientUnitId: string
reportModelId: string
createUnit: string
contractNumber: string
standard: string[]
phonenumber?: string
}
export interface ReportTaskMultiCreateParam {
no: string
sessionId: string
childReports: ReportTaskMultiChildConfig[]
}
}

View File

@@ -44,8 +44,12 @@ const COMPONENT_PATH_ALIASES: Record<string, string> = {
// 报告菜单后端使用 aiReport前端目录沿用既有 aireport 命名。
'/aiReport/reportTask': '/aireport/testReport',
'/aiReport/reportTask/index': '/aireport/testReport/index',
'/aiReport/reportTaskMulti': '/aireport/testReportMulti',
'/aiReport/reportTaskMulti/index': '/aireport/testReportMulti/index',
'/aiReport/testReport': '/aireport/testReport',
'/aiReport/testReport/index': '/aireport/testReport/index',
'/aiReport/testReportMulti': '/aireport/testReportMulti',
'/aiReport/testReportMulti/index': '/aireport/testReportMulti/index',
'/aiReport/reportModel': '/aireport/reportModel',
'/aiReport/reportModel/index': '/aireport/reportModel/index',
'/aiReport/clientUnit': '/aireport/clientUnit',

View File

@@ -0,0 +1,438 @@
<template>
<el-dialog
v-model="visibleProxy"
title="新增多模板批任务"
width="1240px"
append-to-body
destroy-on-close
@closed="handleClosed"
>
<div class="create-dialog-body">
<el-form :model="createForm" label-width="110px" class="create-base-form">
<el-row :gutter="16">
<el-col :xs="24" :sm="12">
<el-form-item label="批任务编号" required>
<el-input v-model="createForm.no" maxlength="64" clearable placeholder="请输入批任务编号" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12">
<el-form-item label="压缩包文件" required>
<el-upload
:auto-upload="false"
:limit="1"
:file-list="zipFileList"
accept=".zip"
@change="handleZipChange"
@remove="handleZipRemove"
@exceed="handleZipExceed"
>
<el-button>选择 zip 压缩包</el-button>
</el-upload>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div class="validate-toolbar">
<el-button type="primary" :loading="zipValidating" @click="handleValidateZip">校验压缩包</el-button>
<div v-if="validateResult" class="validate-summary">
<span>模板目录{{ validateResult.dirCount || 0 }}</span>
<span>子报告{{ validateResult.childReportCount || 0 }}</span>
<span>监测点{{ validateResult.pointCount || 0 }}</span>
<span>Excel{{ validateResult.excelAttachmentCount || 0 }}</span>
<span>Word{{ validateResult.wordAttachmentCount || 0 }}</span>
</div>
</div>
<el-alert
v-if="validateResult?.success"
type="success"
show-icon
:closable="false"
title="压缩包校验通过,可继续填写各模板目录对应的子报告信息。"
/>
<el-alert
v-else-if="validateResult"
type="warning"
show-icon
:closable="false"
title="压缩包校验未通过,请按下方原因修正后重新上传。"
/>
<el-card v-if="validateResult?.failReasons?.length" shadow="never" class="result-card">
<template #header>校验问题</template>
<ul class="result-list">
<li v-for="item in validateResult.failReasons" :key="item">{{ item }}</li>
</ul>
</el-card>
<el-card v-if="validateResult?.orphanAttachmentPaths?.length" shadow="never" class="result-card">
<template #header>未引用附件</template>
<ul class="result-list">
<li v-for="item in validateResult.orphanAttachmentPaths" :key="item">{{ item }}</li>
</ul>
</el-card>
<el-card v-if="createForm.childReports.length" shadow="never" class="result-card">
<template #header>模板目录与子报告配置</template>
<div class="dir-config-list">
<div v-for="item in createForm.childReports" :key="item.templateDirPath" class="dir-config-item">
<div class="dir-config-head">
<div>
<div class="dir-title">{{ item.templateDirName }}</div>
<div class="dir-path">{{ item.templateDirPath }}</div>
</div>
<div class="dir-metrics">
<span>监测点 {{ item.pointCount }}</span>
<span>分组 {{ item.groupCount }}</span>
<span>Excel {{ item.excelAttachmentCount }}</span>
<span>Word {{ item.wordAttachmentCount }}</span>
</div>
</div>
<el-form label-width="96px">
<el-row :gutter="16">
<el-col :xs="24" :sm="12" :md="8">
<el-form-item label="报告编号" required>
<el-input v-model="item.no" maxlength="32" clearable placeholder="请输入报告编号" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-form-item label="委托单位" required>
<el-select v-model="item.clientUnitId" filterable clearable placeholder="请选择委托单位">
<el-option
v-for="option in clientUnitOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-form-item label="报告模板" required>
<el-select v-model="item.reportModelId" filterable clearable placeholder="请选择报告模板">
<el-option
v-for="option in reportModelOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-form-item label="检测公司" required>
<el-select v-model="item.createUnit" filterable clearable placeholder="请选择检测公司">
<el-option
v-for="option in companyOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-form-item label="合同编号" required>
<el-input v-model="item.contractNumber" maxlength="32" clearable placeholder="请输入合同编号" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<el-form-item label="联系电话">
<el-input v-model="item.phonenumber" maxlength="32" clearable placeholder="请输入联系电话" />
</el-form-item>
</el-col>
<el-col :xs="24">
<el-form-item label="检测依据" required>
<el-select
v-model="item.standard"
multiple
filterable
clearable
collapse-tags
placeholder="请选择检测依据"
>
<el-option
v-for="option in standardOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
</el-form>
</div>
</div>
</el-card>
</div>
<template #footer>
<el-button :disabled="createSaving" @click="visibleProxy = false">取消</el-button>
<el-button type="primary" :loading="createSaving" @click="handleCreate">创建批任务</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ElMessage, type UploadFile, type UploadFiles, type UploadUserFile } from 'element-plus'
import { computed, reactive, ref, watch } from 'vue'
import type { ReportTaskMulti } from '@/api/aireport/testReportMulti/interface'
defineOptions({
name: 'TestReportMultiCreateDialog'
})
type SelectOption = {
label: string
value: string
}
type ChildConfigFormItem = ReportTaskMulti.ReportTaskMultiChildConfig &
ReportTaskMulti.ReportTaskMultiTemplateDirRecord & {
pointCount: number
groupCount: number
excelAttachmentCount: number
wordAttachmentCount: number
}
const props = defineProps<{
visible: boolean
zipValidating: boolean
createSaving: boolean
validateResult: ReportTaskMulti.ReportTaskMultiValidateResult | null
clientUnitOptions: SelectOption[]
reportModelOptions: SelectOption[]
companyOptions: SelectOption[]
standardOptions: SelectOption[]
}>()
const emit = defineEmits<{
(event: 'update:visible', value: boolean): void
(event: 'validate-zip', file: File): void
(event: 'create', payload: ReportTaskMulti.ReportTaskMultiCreateParam): void
(event: 'reset'): void
}>()
const selectedZipFile = ref<File | null>(null)
const zipFileList = ref<UploadUserFile[]>([])
const createForm = reactive<{
no: string
sessionId: string
childReports: ChildConfigFormItem[]
}>({
no: '',
sessionId: '',
childReports: []
})
const visibleProxy = computed({
get: () => props.visible,
set: value => emit('update:visible', value)
})
const clearValidatedResult = (notify = true) => {
createForm.sessionId = ''
createForm.childReports = []
if (notify) {
emit('reset')
}
}
const resetLocalState = (notify = true) => {
createForm.no = ''
selectedZipFile.value = null
zipFileList.value = []
clearValidatedResult(notify)
}
const buildChildConfigForms = (templateDirs: ReportTaskMulti.ReportTaskMultiTemplateDirRecord[] = []) =>
templateDirs.map(item => ({
templateDirName: String(item.dirName || ''),
templateDirPath: String(item.dirPath || ''),
no: '',
clientUnitId: '',
reportModelId: '',
createUnit: '',
contractNumber: '',
standard: [],
phonenumber: '',
pointCount: Number(item.pointCount || 0),
groupCount: Number(item.groupCount || 0),
excelAttachmentCount: Number(item.excelAttachmentCount || 0),
wordAttachmentCount: Number(item.wordAttachmentCount || 0)
}))
watch(
() => props.validateResult,
value => {
createForm.sessionId = String(value?.sessionId || '')
createForm.childReports = buildChildConfigForms(value?.templateDirs || [])
},
{ immediate: true }
)
const handleZipChange = (uploadFile: UploadFile, uploadFiles: UploadFiles) => {
selectedZipFile.value = uploadFile.raw || null
zipFileList.value = uploadFiles.slice(-1).map(item => ({
name: item.name,
url: item.url,
status: item.status,
uid: item.uid
}))
clearValidatedResult()
}
const handleZipRemove = () => {
selectedZipFile.value = null
zipFileList.value = []
clearValidatedResult()
}
const handleZipExceed = (files: File[]) => {
const file = files[0]
if (!file) return
selectedZipFile.value = file
zipFileList.value = [{ name: file.name, uid: Date.now(), status: 'ready' }]
clearValidatedResult()
}
const handleValidateZip = () => {
if (!selectedZipFile.value) {
ElMessage.warning('请先选择 zip 压缩包')
return
}
emit('validate-zip', selectedZipFile.value)
}
const validateCreateForm = () => {
if (!createForm.no.trim()) return '请输入批任务编号'
if (!createForm.sessionId) return '请先校验通过压缩包'
if (!createForm.childReports.length) return '当前没有可创建的子报告配置'
for (const item of createForm.childReports) {
if (!item.no.trim()) return `模板目录 ${item.templateDirName} 缺少报告编号`
if (!item.clientUnitId) return `模板目录 ${item.templateDirName} 缺少委托单位`
if (!item.reportModelId) return `模板目录 ${item.templateDirName} 缺少报告模板`
if (!item.createUnit) return `模板目录 ${item.templateDirName} 缺少检测公司`
if (!item.contractNumber.trim()) return `模板目录 ${item.templateDirName} 缺少合同编号`
if (!item.standard.length) return `模板目录 ${item.templateDirName} 缺少检测依据`
}
return ''
}
const handleCreate = () => {
const errorMessage = validateCreateForm()
if (errorMessage) {
ElMessage.warning(errorMessage)
return
}
emit('create', {
no: createForm.no.trim(),
sessionId: createForm.sessionId,
childReports: createForm.childReports.map(item => ({
templateDirName: item.templateDirName,
templateDirPath: item.templateDirPath,
no: item.no.trim(),
clientUnitId: item.clientUnitId,
reportModelId: item.reportModelId,
createUnit: item.createUnit,
contractNumber: item.contractNumber.trim(),
standard: item.standard,
phonenumber: item.phonenumber?.trim() || undefined
}))
})
}
const handleClosed = () => {
resetLocalState()
}
</script>
<style scoped lang="scss">
.create-dialog-body {
display: flex;
flex-direction: column;
gap: 16px;
}
.create-base-form,
.result-card {
width: 100%;
}
.validate-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
}
.validate-summary {
display: flex;
gap: 16px;
flex-wrap: wrap;
color: var(--el-text-color-regular);
font-size: 13px;
}
.result-list {
margin: 0;
padding-left: 18px;
line-height: 1.8;
}
.dir-config-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.dir-config-item {
border: 1px solid var(--el-border-color-light);
border-radius: 8px;
padding: 16px 16px 0;
background: var(--el-fill-color-blank);
}
.dir-config-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.dir-title {
font-size: 15px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.dir-path {
margin-top: 4px;
font-size: 12px;
color: var(--el-text-color-secondary);
}
.dir-metrics {
display: flex;
gap: 12px;
flex-wrap: wrap;
color: var(--el-text-color-secondary);
font-size: 12px;
}
@media (max-width: 900px) {
.validate-toolbar,
.dir-config-head {
align-items: flex-start;
flex-direction: column;
}
}
</style>

View File

@@ -0,0 +1,133 @@
<template>
<ProTable
ref="proTable"
:columns="columns"
:request-api="props.requestApi"
:request-error="props.requestError"
:search-col="{ xs: 1, sm: 2, md: 2, lg: 4, xl: 4 }"
:tool-button="['refresh', 'setting']"
row-key="id"
>
<template #tableHeader="scope">
<el-button type="primary" :icon="Plus" @click="emit('create')">新增批任务</el-button>
<el-button
type="danger"
plain
:icon="Delete"
:disabled="!scope.isSelected"
@click="emit('batch-delete', scope.selectedListIds)"
>
批量删除
</el-button>
</template>
<template #operation="{ row }">
<el-button link type="primary" :icon="View" @click="emit('detail', row)">详情</el-button>
<el-button link type="primary" :icon="Download" @click="emit('download', row)">下载</el-button>
<el-button link type="primary" @click="emit('generate', row)">执行生成</el-button>
<el-button link type="danger" :icon="Delete" @click="emit('delete', row)">删除</el-button>
</template>
</ProTable>
</template>
<script setup lang="tsx">
import { Delete, Download, Plus, View } from '@element-plus/icons-vue'
import { ElTag } from 'element-plus'
import { ref } from 'vue'
import type { ResPage } from '@/api/interface'
import ProTable from '@/components/ProTable/index.vue'
import type { ColumnProps, ProTableInstance } from '@/components/ProTable/interface'
import type { ReportTaskMulti } from '@/api/aireport/testReportMulti/interface'
import { resolveReportTaskText } from '../../testReport/utils/testReport'
defineOptions({
name: 'TestReportMultiTable'
})
const props = defineProps<{
requestApi: (
params: ReportTaskMulti.ReportTaskMultiListParams
) => Promise<{ data: ResPage<ReportTaskMulti.ReportTaskMultiRecord> }>
requestError?: (error: unknown) => void
}>()
const emit = defineEmits<{
(event: 'create'): void
(event: 'detail', row: ReportTaskMulti.ReportTaskMultiRecord): void
(event: 'download', row: ReportTaskMulti.ReportTaskMultiRecord): void
(event: 'generate', row: ReportTaskMulti.ReportTaskMultiRecord): void
(event: 'delete', row: ReportTaskMulti.ReportTaskMultiRecord): void
(event: 'batch-delete', ids: string[]): void
}>()
const MULTI_GENERATE_STATE_MAP: Record<number, { text: string; tagType: 'info' | 'warning' | 'success' | 'danger' }> = {
0: { text: '未生成', tagType: 'info' },
1: { text: '部分成功', tagType: 'warning' },
2: { text: '全部成功', tagType: 'success' },
3: { text: '生成中', tagType: 'warning' },
4: { text: '生成失败', tagType: 'danger' }
}
const proTable = ref<ProTableInstance>()
const columns: ColumnProps[] = [
{ type: 'selection', fixed: 'left', width: 70 },
{ type: 'index', fixed: 'left', width: 70, label: '序号' },
{
prop: 'keyword',
label: '关键字',
minWidth: 180,
isShow: false,
search: {
el: 'input',
order: 1,
props: {
placeholder: '请输入批任务编号、压缩包或台账汇总文件名'
}
}
},
{
prop: 'timeRange',
label: '创建时间',
minWidth: 220,
isShow: false,
search: {
el: 'date-picker',
order: 2,
key: 'timeRange',
props: {
type: 'datetimerange',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
startPlaceholder: '开始创建时间',
endPlaceholder: '结束创建时间'
}
}
},
{ prop: 'no', label: '批任务编号', minWidth: 180, fixed: 'left', render: ({ row }) => resolveReportTaskText(row.no) },
{ prop: 'zipFileName', label: '压缩包文件', minWidth: 200, render: ({ row }) => resolveReportTaskText(row.zipFileName) },
{ prop: 'summaryFileName', label: '台账汇总文件', minWidth: 200, render: ({ row }) => resolveReportTaskText(row.summaryFileName) },
{ prop: 'dirCount', label: '模板目录数', minWidth: 110 },
{ prop: 'childReportCount', label: '子报告数', minWidth: 100 },
{ prop: 'pointCount', label: '监测点总数', minWidth: 110 },
{
prop: 'generateState',
label: '整体生成状态',
minWidth: 130,
render: ({ row }) => (
<ElTag type={MULTI_GENERATE_STATE_MAP[Number(row.generateState)]?.tagType || 'info'}>
{MULTI_GENERATE_STATE_MAP[Number(row.generateState)]?.text || '未知状态'}
</ElTag>
)
},
{ prop: 'createTime', label: '创建时间', minWidth: 170, render: ({ row }) => resolveReportTaskText(row.createTime) },
{ prop: 'operation', label: '操作', fixed: 'right', width: 280 }
]
const refreshTable = () => {
proTable.value?.clearSelection()
proTable.value?.getTableList()
}
defineExpose({
refreshTable
})
</script>

View File

@@ -0,0 +1,607 @@
<template>
<div class="table-box report-task-multi-page">
<TestReportMultiTable
ref="tableRef"
:request-api="getTableList"
:request-error="handleTableRequestError"
@create="openCreateDialog"
@detail="openDetailDialog"
@download="handleDownloadMulti"
@generate="handleGenerateMulti"
@delete="handleDelete"
@batch-delete="handleBatchDelete"
/>
<TestReportMultiCreateDialog
v-model:visible="createDialogVisible"
:zip-validating="zipValidating"
:create-saving="createSaving"
:validate-result="zipValidateResult"
:client-unit-options="clientUnitOptions"
:report-model-options="reportModelOptions"
:company-options="companyOptions"
:standard-options="standardOptions"
@validate-zip="handleValidateZip"
@create="handleCreateMulti"
@reset="resetCreateDialogState"
/>
<el-dialog v-model="detailDialogVisible" title="多模板批任务详情" width="1200px" append-to-body destroy-on-close>
<div v-loading="detailLoading" class="detail-dialog-body">
<el-descriptions v-if="detailRecord" :column="2" border>
<el-descriptions-item label="批任务编号">{{ resolveReportTaskText(detailRecord.no) }}</el-descriptions-item>
<el-descriptions-item label="压缩包文件">{{ resolveReportTaskText(detailRecord.zipFileName) }}</el-descriptions-item>
<el-descriptions-item label="台账汇总文件">{{ resolveReportTaskText(detailRecord.summaryFileName) }}</el-descriptions-item>
<el-descriptions-item label="整体生成状态">
<el-tag :type="getMultiGenerateStateTagType(detailRecord.generateState)">
{{ getMultiGenerateStateText(detailRecord.generateState) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="模板目录数">{{ detailRecord.dirCount ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="子报告数">{{ detailRecord.childReportCount ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="监测点总数">{{ detailRecord.pointCount ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ resolveReportTaskText(detailRecord.createTime) }}</el-descriptions-item>
</el-descriptions>
<div class="detail-toolbar">
<div class="section-title">子报告列表</div>
<el-button
:loading="downloadingMultiId === detailRecord?.id"
:disabled="!detailRecord?.id"
@click="handleDownloadMulti(detailRecord)"
>
下载批任务
</el-button>
<el-button
type="primary"
:loading="multiGenerating"
:disabled="!detailRecord?.id"
@click="handleGenerateMulti(detailRecord)"
>
执行整批生成
</el-button>
</div>
<el-table :data="detailChildReports" border class="child-report-table">
<el-table-column prop="templateDirName" label="模板目录" min-width="150" />
<el-table-column prop="no" label="报告编号" min-width="160" />
<el-table-column prop="reportModelName" label="报告模板" min-width="140" />
<el-table-column prop="state" label="报告状态" min-width="120">
<template #default="{ row }">
<el-tag :type="getReportTaskStateTagType(row.state)">{{ getReportTaskStateText(row.state) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="reportGenerateState" label="生成状态" min-width="120">
<template #default="{ row }">
<el-tag :type="getReportTaskGenerateStateTagType(row.reportGenerateState)">
{{ getReportTaskGenerateStateText(row.reportGenerateState) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="pointCount" label="监测点数" min-width="100" />
<el-table-column prop="groupCount" label="分组数" min-width="100" />
<el-table-column label="操作" min-width="220" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="openChildDetailDialog(row)">详情</el-button>
<el-button link type="primary" @click="openChildGroupDialog(row)">分组</el-button>
<el-button link type="primary" @click="handleGenerateChildReport(row)">生成报告</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-dialog>
<ReportTaskDetailDialog
v-model:visible="childDetailDialogVisible"
:loading="childDetailLoading"
:detail="childDetailRecord"
:group-reports="childDetailGroupReports"
:company-label-map="companyLabelMap"
:standard-label-map="standardLabelMap"
:downloading-group-report-id="downloadingGroupReportId"
@download-group-report="handleDownloadGroupReport"
/>
<ReportTaskGroupDialog
v-model:visible="childGroupDialogVisible"
:loading="childGroupLoading"
:saving="childGroupSaving"
:record="currentChildGroupRecord"
:point-list="childGroupPointList"
@submit="handleSaveChildGroups"
/>
</div>
</template>
<script setup lang="ts">
import { ElMessage, ElMessageBox } from 'element-plus'
import { computed, onMounted, ref } from 'vue'
import type { Dict, ResPage, ResultData } from '@/api/interface'
import { getDictList } from '@/api/user/login'
import { DICT_CODES } from '@/constants/dictCodes'
import { listClientUnitsApi } from '@/api/aireport/clientUnit'
import { listReportModelsApi } from '@/api/aireport/reportModel'
import {
deleteReportTaskMultisApi,
downloadReportTaskMultiApi,
generateReportTaskMultiApi,
getReportTaskMultiDetailApi,
listReportTaskMultiChildReportsApi,
listReportTaskMultisApi,
validateReportTaskMultiZipApi,
createReportTaskMultiApi
} from '@/api/aireport/testReportMulti'
import type { ReportTaskMulti } from '@/api/aireport/testReportMulti/interface'
import {
downloadReportTaskGroupReportApi,
generateReportTaskGroupReportsApi,
getReportTaskDetailApi,
listReportTaskGroupReportsApi,
listReportTaskPointsApi,
saveReportTaskGroupsApi
} from '@/api/aireport/testReport'
import type { ReportTask } from '@/api/aireport/testReport/interface'
import { useDownloadWithServerFileName } from '@/hooks/useDownload'
import { useDictStore } from '@/stores/modules/dict'
import TestReportMultiCreateDialog from './components/TestReportMultiCreateDialog.vue'
import TestReportMultiTable from './components/TestReportMultiTable.vue'
import ReportTaskDetailDialog from '../testReport/components/ReportTaskDetailDialog.vue'
import ReportTaskGroupDialog from '../testReport/components/ReportTaskGroupDialog.vue'
import {
buildDictSelectOptions,
buildOptionLabelMap,
getReportTaskErrorMessage,
getReportTaskGenerateStateTagType,
getReportTaskGenerateStateText,
getReportTaskStateTagType,
getReportTaskStateText,
normalizeReportTaskGroupReportList,
normalizeReportTaskPage,
normalizeReportTaskPointList,
resolveReportTaskText,
unwrapReportTaskPayload
} from '../testReport/utils/testReport'
defineOptions({
name: 'ReportTaskMultiPage'
})
const MULTI_GENERATE_STATE_MAP: Record<number, { text: string; tagType: 'info' | 'warning' | 'success' | 'danger' }> = {
0: { text: '未生成', tagType: 'info' },
1: { text: '部分成功', tagType: 'warning' },
2: { text: '全部成功', tagType: 'success' },
3: { text: '生成中', tagType: 'warning' },
4: { text: '生成失败', tagType: 'danger' }
}
const dictStore = useDictStore()
const tableRef = ref<{ refreshTable: () => void } | null>(null)
const createDialogVisible = ref(false)
const detailDialogVisible = ref(false)
const childDetailDialogVisible = ref(false)
const childGroupDialogVisible = ref(false)
const zipValidating = ref(false)
const createSaving = ref(false)
const detailLoading = ref(false)
const childDetailLoading = ref(false)
const childGroupLoading = ref(false)
const childGroupSaving = ref(false)
const multiGenerating = ref(false)
const zipValidateResult = ref<ReportTaskMulti.ReportTaskMultiValidateResult | null>(null)
const detailRecord = ref<ReportTaskMulti.ReportTaskMultiRecord | null>(null)
const detailChildReports = ref<ReportTask.ReportTaskRecord[]>([])
const childDetailRecord = ref<ReportTask.ReportTaskRecord | null>(null)
const childDetailGroupReports = ref<ReportTask.ReportTaskGroupReportRecord[]>([])
const downloadingMultiId = ref('')
const downloadingGroupReportId = ref('')
const currentChildGroupRecord = ref<ReportTask.ReportTaskRecord | null>(null)
const childGroupPointList = ref<ReportTask.ReportTaskPointRecord[]>([])
const clientUnitOptions = ref<Array<{ label: string; value: string }>>([])
const reportModelOptions = ref<Array<{ label: string; value: string }>>([])
const companyOptions = computed(() => buildDictSelectOptions(dictStore.getDictData(DICT_CODES.TEST_REPORT_COMPANY)))
const standardOptions = computed(() => buildDictSelectOptions(dictStore.getDictData(DICT_CODES.TEST_REPORT_STANDARD)))
const companyLabelMap = computed(() => buildOptionLabelMap(companyOptions.value))
const standardLabelMap = computed(() => buildOptionLabelMap(standardOptions.value))
const requiredDictCodes = [DICT_CODES.TEST_REPORT_COMPANY, DICT_CODES.TEST_REPORT_STANDARD]
const normalizeMultiListParams = (params: ReportTaskMulti.ReportTaskMultiListParams = {}) => {
const timeRange = Array.isArray(params.timeRange) ? params.timeRange : []
const [searchBeginTime = '', searchEndTime = ''] = timeRange
return {
keyword: String(params.keyword || '').trim() || undefined,
searchBeginTime: searchBeginTime || params.searchBeginTime || undefined,
searchEndTime: searchEndTime || params.searchEndTime || undefined,
generateState: params.generateState,
pageNum: params.pageNum || 1,
pageSize: params.pageSize || 10
}
}
const getMultiGenerateStateText = (value?: number | null) => MULTI_GENERATE_STATE_MAP[Number(value)]?.text || '未知状态'
const getMultiGenerateStateTagType = (value?: number | null) => MULTI_GENERATE_STATE_MAP[Number(value)]?.tagType || 'info'
const ensureDictOptionsReady = async () => {
const missingCodes = requiredDictCodes.filter(code => !dictStore.getDictData(code).length)
if (!missingCodes.length) return
const response = await getDictList()
const dictData = (response.data as unknown as Dict[]) || []
const nextDictMap = new Map((dictStore.dictData || []).map(item => [item.code, item]))
dictData.forEach(item => {
if (item?.code) nextDictMap.set(item.code, item)
})
dictStore.setDictData(Array.from(nextDictMap.values()))
}
const loadClientUnitOptions = async () => {
const response = await listClientUnitsApi({ pageNum: 1, pageSize: 200 })
const pageData = normalizeReportTaskPage<{ id?: string; name?: string }>(response as unknown as ResPage<{ id?: string; name?: string }>)
clientUnitOptions.value = (pageData.records || [])
.filter(item => item.id && item.name)
.map(item => ({ label: item.name!, value: item.id! }))
}
const loadReportModelOptions = async () => {
const response = await listReportModelsApi({ pageNum: 1, pageSize: 200 })
const pageData = normalizeReportTaskPage<{ id?: string; name?: string }>(response as unknown as ResPage<{ id?: string; name?: string }>)
reportModelOptions.value = (pageData.records || [])
.filter(item => item.id && item.name)
.map(item => ({ label: item.name!, value: item.id! }))
}
const ensureCreateOptionsReady = async () => {
await Promise.all([ensureDictOptionsReady(), loadClientUnitOptions(), loadReportModelOptions()])
}
const getTableList = async (params: ReportTaskMulti.ReportTaskMultiListParams = {}) => {
const response = await listReportTaskMultisApi(normalizeMultiListParams(params))
const pageData = normalizeReportTaskPage<ReportTaskMulti.ReportTaskMultiRecord>(
response as unknown as ResPage<ReportTaskMulti.ReportTaskMultiRecord>
)
return {
data: pageData
}
}
const refreshTable = () => {
tableRef.value?.refreshTable()
}
const handleTableRequestError = (error: unknown) => {
ElMessage.error(getReportTaskErrorMessage(error, '多模板批任务列表查询失败'))
}
const resetCreateDialogState = () => {
zipValidateResult.value = null
}
const openCreateDialog = async () => {
try {
await ensureCreateOptionsReady()
resetCreateDialogState()
createDialogVisible.value = true
} catch (error) {
ElMessage.error(getReportTaskErrorMessage(error, '批任务关联选项加载失败'))
}
}
const handleValidateZip = async (file: File) => {
const formData = new FormData()
formData.append('file', file)
zipValidating.value = true
try {
const response = await validateReportTaskMultiZipApi(formData)
const result = unwrapReportTaskPayload<ReportTaskMulti.ReportTaskMultiValidateResult>(
response as ResultData<ReportTaskMulti.ReportTaskMultiValidateResult>
)
zipValidateResult.value = result
ElMessage.success(result?.success ? '压缩包校验通过' : '压缩包校验未通过')
} catch (error) {
ElMessage.error(getReportTaskErrorMessage(error, '压缩包校验失败'))
} finally {
zipValidating.value = false
}
}
const handleCreateMulti = async (payload: ReportTaskMulti.ReportTaskMultiCreateParam) => {
createSaving.value = true
try {
await createReportTaskMultiApi(payload)
ElMessage.success('多模板批任务创建成功')
createDialogVisible.value = false
refreshTable()
} catch (error) {
ElMessage.error(getReportTaskErrorMessage(error, '多模板批任务创建失败'))
} finally {
createSaving.value = false
}
}
const loadDetailData = async (id: string) => {
const [detailResponse, childReportsResponse] = await Promise.all([
getReportTaskMultiDetailApi(id),
listReportTaskMultiChildReportsApi(id)
])
detailRecord.value = unwrapReportTaskPayload<ReportTaskMulti.ReportTaskMultiRecord>(
detailResponse as ResultData<ReportTaskMulti.ReportTaskMultiRecord>
)
detailChildReports.value = (unwrapReportTaskPayload<ReportTask.ReportTaskRecord[]>(
childReportsResponse as ResultData<ReportTask.ReportTaskRecord[]>
) || []) as ReportTask.ReportTaskRecord[]
}
const openDetailDialog = async (row: ReportTaskMulti.ReportTaskMultiRecord) => {
const id = String(row.id || '').trim()
if (!id) {
ElMessage.warning('当前批任务缺少 ID无法查看详情')
return
}
detailDialogVisible.value = true
detailLoading.value = true
try {
await loadDetailData(id)
} catch (error) {
detailDialogVisible.value = false
ElMessage.error(getReportTaskErrorMessage(error, '多模板批任务详情查询失败'))
} finally {
detailLoading.value = false
}
}
const refreshDetailIfMatched = async (id: string) => {
if (!detailDialogVisible.value) return
if (String(detailRecord.value?.id || '').trim() !== id) return
await loadDetailData(id)
}
const handleDownloadMulti = async (row: ReportTaskMulti.ReportTaskMultiRecord | null) => {
const id = String(row?.id || '').trim()
if (!id) {
ElMessage.warning('当前批任务缺少 ID无法下载')
return
}
downloadingMultiId.value = id
try {
await useDownloadWithServerFileName(
() => downloadReportTaskMultiApi(id),
row?.no || '批任务结果包',
undefined,
false,
'.zip'
)
} catch (error) {
ElMessage.error(getReportTaskErrorMessage(error, '批任务下载失败'))
} finally {
downloadingMultiId.value = ''
}
}
const handleGenerateMulti = async (row: ReportTaskMulti.ReportTaskMultiRecord | null) => {
const id = String(row?.id || '').trim()
if (!id) {
ElMessage.warning('当前批任务缺少 ID无法执行生成')
return
}
try {
await ElMessageBox.confirm('将依次执行当前批任务下全部子报告的按分组生成,是否继续?', '执行整批生成', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
} catch {
return
}
multiGenerating.value = true
try {
await generateReportTaskMultiApi(id)
ElMessage.success('整批生成执行完成')
refreshTable()
await refreshDetailIfMatched(id)
} catch (error) {
ElMessage.error(getReportTaskErrorMessage(error, '整批生成失败'))
await refreshDetailIfMatched(id)
refreshTable()
} finally {
multiGenerating.value = false
}
}
const openChildDetailDialog = async (row: ReportTask.ReportTaskRecord) => {
const reportId = String(row.id || '').trim()
if (!reportId) {
ElMessage.warning('当前子报告缺少 ID无法查看详情')
return
}
childDetailDialogVisible.value = true
childDetailLoading.value = true
try {
const [detailResponse, groupReportsResponse] = await Promise.all([
getReportTaskDetailApi(reportId),
listReportTaskGroupReportsApi(reportId)
])
childDetailRecord.value = unwrapReportTaskPayload<ReportTask.ReportTaskRecord>(
detailResponse as ResultData<ReportTask.ReportTaskRecord>
)
childDetailGroupReports.value = normalizeReportTaskGroupReportList(groupReportsResponse)
} catch (error) {
childDetailDialogVisible.value = false
ElMessage.error(getReportTaskErrorMessage(error, '子报告详情查询失败'))
} finally {
childDetailLoading.value = false
}
}
const openChildGroupDialog = async (row: ReportTask.ReportTaskRecord) => {
const reportId = String(row.id || '').trim()
if (!reportId) {
ElMessage.warning('当前子报告缺少 ID无法维护分组')
return
}
currentChildGroupRecord.value = row
childGroupPointList.value = []
childGroupDialogVisible.value = true
childGroupLoading.value = true
try {
const response = await listReportTaskPointsApi(reportId)
childGroupPointList.value = normalizeReportTaskPointList(response)
} catch (error) {
childGroupDialogVisible.value = false
ElMessage.error(getReportTaskErrorMessage(error, '子报告监测点查询失败'))
} finally {
childGroupLoading.value = false
}
}
const handleSaveChildGroups = async (params: ReportTask.ReportTaskGroupSaveRequest) => {
const reportId = String(currentChildGroupRecord.value?.id || '').trim()
if (!reportId) {
ElMessage.warning('当前子报告缺少 ID无法保存分组')
return
}
childGroupSaving.value = true
try {
await saveReportTaskGroupsApi(reportId, params)
ElMessage.success('子报告分组保存成功')
childGroupDialogVisible.value = false
refreshTable()
await refreshDetailIfMatched(String(detailRecord.value?.id || ''))
} catch (error) {
ElMessage.error(getReportTaskErrorMessage(error, '子报告分组保存失败'))
} finally {
childGroupSaving.value = false
}
}
const handleGenerateChildReport = async (row: ReportTask.ReportTaskRecord) => {
const reportId = String(row.id || '').trim()
if (!reportId) {
ElMessage.warning('当前子报告缺少 ID无法生成报告')
return
}
try {
await ElMessageBox.confirm('将按当前子报告的分组生成全部报告,是否继续?', '生成子报告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
} catch {
return
}
try {
await generateReportTaskGroupReportsApi(reportId)
ElMessage.success('子报告生成完成')
refreshTable()
await refreshDetailIfMatched(String(detailRecord.value?.id || ''))
if (childDetailDialogVisible.value && String(childDetailRecord.value?.id || '') === reportId) {
await openChildDetailDialog(row)
}
} 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 deleteRows = async (ids: string[], successMessage: string) => {
if (!ids.length) {
ElMessage.warning('请选择要删除的批任务')
return
}
try {
await ElMessageBox.confirm('删除后该批任务及其下全部子报告都会被删除,是否继续?', '删除批任务', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
} catch {
return
}
try {
await deleteReportTaskMultisApi(ids)
ElMessage.success(successMessage)
refreshTable()
} catch (error) {
ElMessage.error(getReportTaskErrorMessage(error, '批任务删除失败'))
}
}
const handleDelete = async (row: ReportTaskMulti.ReportTaskMultiRecord) => {
const id = String(row.id || '').trim()
if (!id) {
ElMessage.warning('当前批任务缺少 ID无法删除')
return
}
await deleteRows([id], '批任务删除成功')
}
const handleBatchDelete = async (ids: string[]) => {
await deleteRows(ids, '批任务批量删除成功')
}
onMounted(async () => {
try {
await ensureDictOptionsReady()
} catch (error) {
ElMessage.error(getReportTaskErrorMessage(error, '字典加载失败'))
}
})
</script>
<style scoped lang="scss">
.report-task-multi-page {
min-height: 0;
padding: 0;
overflow: hidden;
}
.detail-dialog-body {
display: flex;
flex-direction: column;
gap: 16px;
}
.child-report-table {
width: 100%;
}
.detail-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.section-title {
font-size: 15px;
font-weight: 600;
color: var(--el-text-color-primary);
}
@media (max-width: 900px) {
.detail-toolbar {
align-items: flex-start;
flex-direction: column;
}
}
</style>

View File

@@ -3,7 +3,9 @@
"include": [
"env.d.ts",
"src/**/*",
"src/**/*.vue"
"src/**/*.vue",
"src/views/aireport/testReportMulti/**/*.vue",
"src/views/aireport/testReportMulti/**/*.ts"
],
"exclude": [
"src/**/__tests__/*"