1016 lines
33 KiB
Vue
1016 lines
33 KiB
Vue
<template>
|
||
<a-drawer
|
||
:width="`95%`"
|
||
:visible="visible"
|
||
:confirm-loading="loading"
|
||
:maxable="maxAble"
|
||
title="AI审计内容生成器"
|
||
:body-style="{ paddingBottom: '8px', background: '#f3f3f3' }"
|
||
@update:visible="updateVisible"
|
||
:maskClosable="false"
|
||
:footer="null"
|
||
>
|
||
<!-- 基本信息卡片 -->
|
||
<a-card title="基本信息" style="margin-bottom: 20px" :bordered="false">
|
||
<a-descriptions>
|
||
<a-descriptions-item label="公司名称" :labelStyle="{ width: '90px', color: '#808080' }">
|
||
<span @click="copyText(form.name)">{{ form.name }}</span>
|
||
</a-descriptions-item>
|
||
<a-descriptions-item label="被审计人" :labelStyle="{ width: '90px', color: '#808080' }">
|
||
<span>{{ form.nickname || '暂无' }}</span>
|
||
</a-descriptions-item>
|
||
<a-descriptions-item label="审计时间" :labelStyle="{ width: '90px', color: '#808080' }">
|
||
<span>{{ form.expirationTime }}</span>
|
||
</a-descriptions-item>
|
||
</a-descriptions>
|
||
</a-card>
|
||
|
||
<!-- 操作按钮区域 -->
|
||
<a-card
|
||
style="margin-bottom: 20px; text-align: center; background: transparent"
|
||
:bordered="false"
|
||
>
|
||
<a-space>
|
||
<a-button
|
||
size="large"
|
||
type="primary"
|
||
@click="handleGenerateAll"
|
||
:loading="generatingAll"
|
||
class="generate-all-button"
|
||
:disabled="!hasTripleOneData"
|
||
>
|
||
<template #icon>
|
||
<UngroupOutlined />
|
||
</template>
|
||
生成全部方案
|
||
<a-tooltip
|
||
v-if="!hasTripleOneData"
|
||
title="请先生成三重一大制度对比分析表"
|
||
>
|
||
<QuestionCircleOutlined style="margin-left: 5px" />
|
||
</a-tooltip>
|
||
</a-button>
|
||
<a-button size="large">
|
||
<template #icon>
|
||
<DownloadOutlined />
|
||
</template>
|
||
保存草稿
|
||
</a-button>
|
||
<a-button size="large">
|
||
<template #icon>
|
||
<RedoOutlined />
|
||
</template>
|
||
加载草稿
|
||
</a-button>
|
||
<a-button
|
||
size="large"
|
||
type="danger"
|
||
class="export-button"
|
||
@click="handleExport"
|
||
>
|
||
<template #icon>
|
||
<UploadOutlined />
|
||
</template>
|
||
下载文件
|
||
</a-button>
|
||
</a-space>
|
||
</a-card>
|
||
|
||
<!-- 快速导航 -->
|
||
<a-card style="margin-bottom: 20px" :bordered="false">
|
||
<template #title>
|
||
<div
|
||
style="
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
"
|
||
>
|
||
<span>快速导航</span>
|
||
<a-tooltip title="快捷键:Ctrl+1~9 快速跳转,Ctrl+↑↓ 上下导航">
|
||
<QuestionCircleOutlined style="color: #999; cursor: help" />
|
||
</a-tooltip>
|
||
</div>
|
||
</template>
|
||
|
||
<div class="navigation-container">
|
||
<div class="nav-grid">
|
||
<a-button
|
||
v-for="(item, index) in navigationItems"
|
||
:key="index"
|
||
:type="currentSection === index ? 'primary' : 'default'"
|
||
size="small"
|
||
@click="scrollToSection(index)"
|
||
class="nav-button"
|
||
:class="{ active: currentSection === index }"
|
||
>
|
||
<span class="nav-number">{{ item.number }}</span>
|
||
<span class="nav-text">{{ item.name }}</span>
|
||
</a-button>
|
||
</div>
|
||
|
||
<!-- 进度指示器 -->
|
||
<div class="progress-container">
|
||
<div class="progress-bar">
|
||
<div
|
||
class="progress-fill"
|
||
:style="{
|
||
width: `${((currentSection + 1) / navigationItems.length) * 100}%`
|
||
}"
|
||
></div>
|
||
</div>
|
||
<div class="progress-text">
|
||
{{ currentSection + 1 }} / {{ navigationItems.length }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</a-card>
|
||
|
||
<!-- 审计方案内容 -->
|
||
<div class="audit-content">
|
||
<div
|
||
v-for="(item, index) in navigationItems"
|
||
:key="index"
|
||
:id="`section-${index}`"
|
||
class="audit-section"
|
||
>
|
||
<a-card style="margin-bottom: 20px" :bordered="false">
|
||
<template #title>
|
||
<span class="font-bold">{{ `${item.number}、${item.name}` }}</span>
|
||
<span class="ml-4 text-gray-400" v-if="item.title">
|
||
{{ item.title }}
|
||
</span>
|
||
<!-- 所有表格都显示选择文件按钮 -->
|
||
<span class="ml-4 text-gray-400" v-if="item.showFileSelect">
|
||
<a-button
|
||
type="link"
|
||
@click="openDocSelect(index)"
|
||
class="select-file-btn"
|
||
>
|
||
<FolderOpenOutlined />
|
||
选择文件
|
||
</a-button>
|
||
</span>
|
||
</template>
|
||
<template #extra>
|
||
<a-space>
|
||
<!-- 所有表格都显示导出按钮 -->
|
||
<template v-if="item.mode === 'table' && item.tableOptions.length > 0">
|
||
<a-button
|
||
type="primary"
|
||
@click="handleExportTable(index)"
|
||
:loading="exportStates[`${item.tableType}_${item.tableOptions[item.currentTableIndex].value}`]"
|
||
>
|
||
<DownloadOutlined />
|
||
导出表格
|
||
</a-button>
|
||
</template>
|
||
|
||
<!-- AI生成按钮 -->
|
||
<a-button
|
||
type="primary"
|
||
@click="generateContent(index)"
|
||
:loading="item.generating"
|
||
>
|
||
<template #icon>
|
||
<RobotOutlined />
|
||
</template>
|
||
AI生成
|
||
</a-button>
|
||
</a-space>
|
||
</template>
|
||
|
||
<!-- 通用表格切换器 - 所有表格都显示 -->
|
||
<div class="flex justify-between items-center mb-4" v-if="item.mode === 'table'">
|
||
<div class="flex justify-start items-center">
|
||
<template v-if="item.tableOptions.length > 0">
|
||
<div class="flex items-center">
|
||
<span class="mr-2">切换表格:</span>
|
||
<a-radio-group
|
||
v-model:value="item.currentTableIndex"
|
||
@change="(e) => handleTableChange(index, e.target.value)"
|
||
>
|
||
<a-radio-button
|
||
v-for="(option, optIndex) in item.tableOptions"
|
||
:key="optIndex"
|
||
:value="optIndex"
|
||
>
|
||
{{ option.title }}
|
||
</a-radio-button>
|
||
</a-radio-group>
|
||
</div>
|
||
|
||
<span class="ml-4">
|
||
共生成
|
||
<span class="text-red-400 mx-1 font-bold">
|
||
{{ item.data ? item.data.length : 0 }}
|
||
</span>
|
||
条数据
|
||
</span>
|
||
</template>
|
||
</div>
|
||
<!-- 所有表格都显示历史记录 -->
|
||
<a @click="openHistory(index)" class="cursor-pointer">历史记录</a>
|
||
</div>
|
||
|
||
<!-- 表格内容 -->
|
||
<a-table
|
||
v-if="item.mode === 'table' && item.columns"
|
||
:columns="item.columns"
|
||
:scroll="{ y: 500, x: 1000 }"
|
||
:pagination="false"
|
||
bordered
|
||
:data-source="item.data"
|
||
>
|
||
<template #bodyCell="{ column, record }">
|
||
<!-- 操作列 -->
|
||
<template v-if="column.key === 'action'">
|
||
<a-space>
|
||
<a-button type="link" size="small" @click="editRow(record, index)">编辑</a-button>
|
||
<a-button type="link" size="small" danger @click="deleteRow(record, index)">删除</a-button>
|
||
</a-space>
|
||
</template>
|
||
|
||
<!-- 测试结果列 -->
|
||
<template v-if="column.key === 'testResult' || column.key === 'result'">
|
||
<span class="text-green-400" v-if="record[column.key] === '通过'">
|
||
通过
|
||
</span>
|
||
<span class="text-red-400" v-else-if="record[column.key] === '不通过'">
|
||
不通过
|
||
</span>
|
||
<span class="text-gray-400" v-else>
|
||
{{ record[column.key] || '待检查' }}
|
||
</span>
|
||
</template>
|
||
|
||
<!-- 工作底稿索引列 -->
|
||
<template v-if="column.key === 'workPaperIndex'">
|
||
<div v-if="record.workPaperIndex && record.workPaperIndex.length > 0">
|
||
<div
|
||
v-for="(fileItem, fileIndex) in record.workPaperIndex"
|
||
:key="fileIndex"
|
||
class="file-item"
|
||
>
|
||
<img
|
||
src="@/assets/word.png"
|
||
style="width: 20px; height: 20px; vertical-align: middle; margin-right: 4px;"
|
||
alt=""
|
||
/>
|
||
<!-- 新格式支持 -->
|
||
<a
|
||
v-if="fileItem.fileUrl"
|
||
:href="fileItem.fileUrl"
|
||
target="_blank"
|
||
@click.stop="handleFileDownload(fileItem, $event)"
|
||
class="file-link"
|
||
style="color: #1890ff; text-decoration: none; cursor: pointer;"
|
||
>
|
||
{{ fileItem.fileName || '未命名文件' }}
|
||
</a>
|
||
<!-- 旧格式兼容 -->
|
||
<span v-else class="file-name">
|
||
{{ fileItem.fileName || fileItem || '未命名文件' }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- 文件索引列 -->
|
||
<template v-if="column.key === 'fileIndex'">
|
||
<div v-if="record.fileIndex && record.fileIndex.length > 0">
|
||
<div
|
||
v-for="(fileItem, fileIndex) in record.fileIndex"
|
||
:key="fileIndex"
|
||
>
|
||
<img
|
||
src="@/assets/word.png"
|
||
style="width: 20px; height: 20px"
|
||
alt=""
|
||
/>
|
||
<span class="cursor-pointer text-wrap">{{ fileItem }}</span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- 执行效果相关列 -->
|
||
<template v-if="column.key === 'goods' || column.key === 'normal' || column.key === 'bad'">
|
||
<span v-if="record[column.key] === '是'">✅</span>
|
||
<span v-else>❌</span>
|
||
</template>
|
||
|
||
<!-- 审计内容列(长文本处理) -->
|
||
<template v-if="column.key === 'auditContent'">
|
||
<div class="text-left">
|
||
{{ record[column.key] }}
|
||
<!-- <a-tooltip v-if="record[column.key] && record[column.key].length > 50" :title="record[column.key]">
|
||
<MoreOutlined class="ml-1" />
|
||
</a-tooltip>-->
|
||
</div>
|
||
</template>
|
||
|
||
<!-- 检查的证据及测试内容列 -->
|
||
<template v-if="column.key === 'checkEvidence'">
|
||
<div class="text-left">
|
||
{{ record[column.key] || '待填写' }}
|
||
<!-- <a-tooltip v-if="record[column.key] && record[column.key].length > 30" :title="record[column.key]">
|
||
<MoreOutlined class="ml-1" />
|
||
</a-tooltip>-->
|
||
</div>
|
||
</template>
|
||
</template>
|
||
</a-table>
|
||
|
||
<!-- 单选模式 -->
|
||
<template v-else-if="item.mode === 'radio'">
|
||
<div
|
||
class="mb-1"
|
||
v-for="(radio, radioIndex) in item.radioList"
|
||
:key="radioIndex"
|
||
>
|
||
<p class="mb-1">{{ radio.label }}</p>
|
||
<a-radio-group v-model:value="radio.content" :name="radio.label">
|
||
<a-radio value="是">是</a-radio>
|
||
<a-radio value="否">否</a-radio>
|
||
</a-radio-group>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- 文本区域模式 -->
|
||
<template v-else>
|
||
<!-- 多文本区域 -->
|
||
<template v-if="item.textareaList && item.textareaList.length">
|
||
<div
|
||
class="py-2 box-border"
|
||
:key="textareaIndex"
|
||
v-for="(textarea, textareaIndex) in item.textareaList"
|
||
>
|
||
<p class="mb-1">{{ textarea.label }}</p>
|
||
<a-textarea
|
||
v-model:value="textarea.content"
|
||
:rows="item.rows || 8"
|
||
placeholder="点击(AI生成)按钮让AI为您生成该部分内容,或直接填入"
|
||
class="content-textarea"
|
||
style="margin-top: 16px; background-color: #f0fdf4"
|
||
/>
|
||
</div>
|
||
</template>
|
||
<!-- 单文本区域 -->
|
||
<template v-else>
|
||
<a-textarea
|
||
v-model:value="item.content"
|
||
:rows="item.rows || 8"
|
||
placeholder="点击(AI生成)按钮让AI为您生成该部分内容,或直接填入"
|
||
class="content-textarea"
|
||
style="margin-top: 16px; background-color: #f0fdf4"
|
||
/>
|
||
</template>
|
||
</template>
|
||
|
||
<!-- AI助手 -->
|
||
<div style="margin-top: 12px">
|
||
<div class="question-prompt">AI小助手</div>
|
||
<div class="textarea-with-button" style="width: 600px">
|
||
<a-textarea
|
||
v-model:value="item.suggestion"
|
||
:rows="3"
|
||
placeholder="请输入您的要求并回车..."
|
||
class="suggestion-textarea-inner"
|
||
@pressEnter="generateContent(index)"
|
||
/>
|
||
<a-button
|
||
type="danger"
|
||
size="small"
|
||
@click="generateContent(index)"
|
||
:loading="item.generating"
|
||
class="send-button-inner"
|
||
>
|
||
<template #icon>
|
||
<RobotOutlined />
|
||
</template>
|
||
发送
|
||
</a-button>
|
||
</div>
|
||
</div>
|
||
</a-card>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 返回顶部按钮 -->
|
||
<a-back-top :target="getScrollContainer" />
|
||
<FileModal ref="fileModal" :current-company-id="currentCompanyId" />
|
||
<HistoryModal
|
||
v-model:visible="showHistory"
|
||
:interface-name="currentInterfaceName"
|
||
:project-id="props.data?.id"
|
||
@select="handleHistorySelect"
|
||
/>
|
||
</a-drawer>
|
||
</template>
|
||
<script lang="ts" setup>
|
||
import { ref, reactive, watch, onMounted, onUnmounted, computed } from 'vue';
|
||
import { Form, message } from 'ant-design-vue';
|
||
import { assignObject } from 'ele-admin-pro';
|
||
import { copyText } from '@/utils/common';
|
||
import type { PwlProject } from '@/api/pwl/pwlProject/model';
|
||
import {
|
||
RedoOutlined,
|
||
RobotOutlined,
|
||
UngroupOutlined,
|
||
QuestionCircleOutlined,
|
||
DownloadOutlined,
|
||
UploadOutlined,
|
||
FolderOpenOutlined,
|
||
FileOutlined,
|
||
MoreOutlined
|
||
} from '@ant-design/icons-vue';
|
||
import { generateAuditReport, downloadAuditReport } from '@/api/ai/auditReport';
|
||
import { getPwlProjectLibraryByIds } from '@/api/pwl/pwlProjectLibrary';
|
||
import * as auditContentApi from '@/api/ai/auditContent';
|
||
|
||
import FileModal from '@/views/pwl/pwlProject/components/components/FileModal.vue';
|
||
import HistoryModal from '@/views/pwl/pwlProject/components/components/HistoryModal.vue';
|
||
|
||
import useNavigationItems from './data/navigationItems';
|
||
import { tableConfigs, getTableConfig, createDataMapper, apiMethodMap, findTableConfigByInterface } from './data/tableCommon';
|
||
|
||
// 工具函数
|
||
import {
|
||
scrollToSection,
|
||
getScrollContainer,
|
||
handleScroll,
|
||
buildExportData,
|
||
hasContent
|
||
} from './data/funcs';
|
||
|
||
const useForm = Form.useForm;
|
||
|
||
const props = defineProps<{
|
||
visible: boolean;
|
||
data?: PwlProject | null;
|
||
}>();
|
||
|
||
const emit = defineEmits<{
|
||
(e: 'done'): void;
|
||
(e: 'update:visible', visible: boolean): void;
|
||
}>();
|
||
|
||
// ========== 基础状态 ==========
|
||
const maxAble = ref(true);
|
||
const loading = ref(true);
|
||
const generatingAll = ref(false);
|
||
const currentSection = ref(0);
|
||
const combinedKbIds = ref('');
|
||
const currentCompanyId = ref<number>();
|
||
|
||
// 使用响应式导航项
|
||
const navigationItems = useNavigationItems();
|
||
|
||
// ========== 导出状态管理 ==========
|
||
const exportStates = reactive({});
|
||
|
||
// ========== 文档选择相关 ==========
|
||
const fileModal = ref();
|
||
const showDocSelect = ref(false);
|
||
const currentSectionIndex = ref(0);
|
||
const selectedDocList = ref<string[]>([]);
|
||
const selectedFileList = ref<string[]>([]);
|
||
const selectedFileKeys = ref<(string | number)[]>([]);
|
||
const checkedDirKeys = ref<(string | number)[]>([]);
|
||
const lastSelectedDirKeys = ref<(string | number)[]>([]);
|
||
const lastSelectedFileKeys = ref<(string | number)[]>([]);
|
||
|
||
// ========== 历史记录相关 ==========
|
||
const showHistory = ref(false);
|
||
const currentInterfaceName = ref('');
|
||
|
||
// ========== 三重一大数据 ==========
|
||
const tripleOneData = ref(null);
|
||
// ========== 国资管理数据 ==========
|
||
const stateAssetsData = ref(null);
|
||
|
||
// ========== 表格数据存储 ==========
|
||
const tableData = reactive({});
|
||
|
||
// ========== 表单数据 ==========
|
||
const form = reactive<PwlProject>({
|
||
// ... 表单属性保持不变
|
||
});
|
||
|
||
const { resetFields } = useForm(form);
|
||
|
||
// ========== 计算属性 ==========
|
||
const hasTripleOneData = computed(() => {
|
||
const section = navigationItems.value[2];
|
||
return section.data && section.data.length > 0;
|
||
});
|
||
|
||
// ========== 通用方法 ==========
|
||
|
||
/* 更新visible */
|
||
const updateVisible = (value: boolean) => {
|
||
emit('update:visible', value);
|
||
};
|
||
|
||
/* 表格切换处理 */
|
||
const handleTableChange = async (sectionIndex: number, tableIndex: number) => {
|
||
const section = navigationItems.value[sectionIndex];
|
||
if (!section || !section.tableOptions[tableIndex]) return;
|
||
|
||
section.currentTableIndex = tableIndex;
|
||
const tableOption = section.tableOptions[tableIndex];
|
||
|
||
// 动态加载列配置
|
||
if (typeof tableOption.columns === 'function') {
|
||
const module = await tableOption.columns();
|
||
section.columns = module.default || module;
|
||
} else {
|
||
section.columns = tableOption.columns;
|
||
}
|
||
|
||
// 加载对应的数据
|
||
const dataKey = `${section.tableType}_${tableOption.value}`;
|
||
if (tableData[dataKey]) {
|
||
section.data = tableData[dataKey];
|
||
} else {
|
||
section.data = [];
|
||
}
|
||
|
||
message.success(`已切换到:${tableOption.title}`);
|
||
};
|
||
|
||
/* 打开文档选择弹窗 */
|
||
const openDocSelect = (sectionIndex: number) => {
|
||
currentSectionIndex.value = sectionIndex;
|
||
showDocSelect.value = true;
|
||
|
||
const section = navigationItems.value[sectionIndex];
|
||
const currentTable = section.tableOptions[section.currentTableIndex];
|
||
const dataKey = `${section.tableType}_${currentTable.value}`;
|
||
|
||
checkedDirKeys.value = [...lastSelectedDirKeys.value];
|
||
selectedFileKeys.value = [...lastSelectedFileKeys.value];
|
||
selectedDocList.value = lastSelectedDirKeys.value.map(key => key.toString());
|
||
selectedFileList.value = lastSelectedFileKeys.value.map(key => key.toString());
|
||
|
||
fileModal.value.open(dataKey);
|
||
};
|
||
|
||
/* 通用表格内容生成方法 */
|
||
const generateTableContent = async (sectionIndex: number) => {
|
||
const section = navigationItems.value[sectionIndex];
|
||
if (!section) {
|
||
message.error('章节不存在');
|
||
return;
|
||
}
|
||
|
||
const currentTable = section.tableOptions[section.currentTableIndex];
|
||
if (!currentTable) {
|
||
message.error('表格配置不存在');
|
||
return;
|
||
}
|
||
|
||
section.generating = true;
|
||
|
||
try {
|
||
// 构建请求参数
|
||
const requestData = {
|
||
projectId: props.data?.id || '',
|
||
kbIds: props.data?.kbId || '',
|
||
libraryIds: props.data?.libraryIds || '',
|
||
projectLibrary: props.data?.projectLibrary || '',
|
||
history: section.data?.length > 0 ? JSON.stringify(section.data, null, 2) : '',
|
||
suggestion: section.suggestion || '',
|
||
docList: checkedDirKeys.value,
|
||
fileList: selectedFileKeys.value,
|
||
// 重大经济决策调查表需要三重一大数据
|
||
...(currentTable.value === 'decisionTable' ? { data: tripleOneData.value } : {}),
|
||
// 预算管理审计表需要国资管理数据
|
||
...(currentTable.value === 'budgetManage' ? { data: stateAssetsData.value } : {}),
|
||
// 预算执行情况审计表需要预算管理审计的数据
|
||
...(currentTable.value === 'budgetExecution' ? { data: tableData['auditContent5_budgetManage'] } : {}),
|
||
};
|
||
|
||
// 获取对应的生成方法
|
||
const apiMethod = apiMethodMap[currentTable.value] || apiMethodMap.default;
|
||
const generateMethod = auditContentApi[apiMethod.generate];
|
||
|
||
if (!generateMethod) {
|
||
throw new Error(`生成方法 ${apiMethod.generate} 不存在`);
|
||
}
|
||
|
||
const result = await generateMethod(requestData);
|
||
|
||
if (result.code === 0 && result.data?.success) {
|
||
// 使用对应的数据映射函数
|
||
const dataMapper = createDataMapper(currentTable.value);
|
||
const mappedData = dataMapper(result.data.data);
|
||
|
||
// 保存数据
|
||
const dataKey = `${section.tableType}_${currentTable.value}`;
|
||
tableData[dataKey] = mappedData;
|
||
|
||
// 特殊处理数据存储
|
||
if (currentTable.value === 'tripleOne') {
|
||
tripleOneData.value = result.data.data;
|
||
} else if (currentTable.value === 'assets') {
|
||
// 存储国资管理数据,供预算管理审计表使用
|
||
stateAssetsData.value = result.data.data;
|
||
}
|
||
|
||
section.data = mappedData;
|
||
section.suggestion = '';
|
||
message.success(`成功生成 ${mappedData.length} 条${currentTable.title}记录`);
|
||
} else {
|
||
throw new Error(result.data?.error || result.message || '生成失败');
|
||
}
|
||
} catch (error: any) {
|
||
console.error(`生成表格内容失败:`, error);
|
||
message.error(`生成失败: ${error.message || '未知错误'}`);
|
||
} finally {
|
||
section.generating = false;
|
||
}
|
||
};
|
||
|
||
/* 通用表格导出方法 */
|
||
const handleExportTable = async (sectionIndex: number) => {
|
||
const section = navigationItems.value[sectionIndex];
|
||
const currentTable = section.tableOptions[section.currentTableIndex];
|
||
|
||
if (!section.data || section.data.length === 0) {
|
||
message.warning(`没有可导出的${currentTable.title}数据`);
|
||
return;
|
||
}
|
||
|
||
// 使用正确的公司名称和审计时间
|
||
const companyName = form.name || props.data?.name || '未知公司';
|
||
const auditTime = form.expirationTime || props.data?.expirationTime || '未知时间';
|
||
|
||
// 设置导出状态
|
||
const exportKey = `${section.tableType}_${currentTable.value}`;
|
||
exportStates[exportKey] = true;
|
||
|
||
try {
|
||
const exportData = {
|
||
data: section.data,
|
||
companyName: companyName,
|
||
auditTime: auditTime
|
||
};
|
||
|
||
// 获取对应的导出方法
|
||
const apiMethod = apiMethodMap[currentTable.value] || apiMethodMap.default;
|
||
const exportMethod = auditContentApi[apiMethod.export];
|
||
|
||
if (!exportMethod) {
|
||
throw new Error(`导出方法 ${apiMethod.export} 不存在`);
|
||
}
|
||
|
||
const blob = await exportMethod(exportData);
|
||
|
||
const url = window.URL.createObjectURL(new Blob([blob]));
|
||
const link = document.createElement('a');
|
||
link.href = url;
|
||
link.setAttribute('download', `${currentTable.title}_${companyName}.xlsx`);
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
document.body.removeChild(link);
|
||
window.URL.revokeObjectURL(url);
|
||
|
||
message.success(`${currentTable.title}导出成功`);
|
||
} catch (error: any) {
|
||
console.error(`导出表格失败:`, error);
|
||
message.error(`导出失败: ${error.message || '未知错误'}`);
|
||
} finally {
|
||
exportStates[exportKey] = false;
|
||
}
|
||
};
|
||
|
||
/* 批量生成全部内容 */
|
||
const handleGenerateAll = async () => {
|
||
generatingAll.value = true;
|
||
try {
|
||
for (let i = 0; i < navigationItems.value.length; i++) {
|
||
// 跳过未完成的依赖项
|
||
if (i === 4 && !hasContent(navigationItems.value[3])) {
|
||
message.warning('请先生成第四项「被审计单位基本情况」');
|
||
continue;
|
||
}
|
||
if (i === 5 && !hasContent(navigationItems.value[4])) {
|
||
message.warning('请先生成第五项「审计内容和重点及审计方法」');
|
||
continue;
|
||
}
|
||
|
||
await generateContent(i);
|
||
}
|
||
message.success('全部内容生成完成');
|
||
} catch (error) {
|
||
console.error('批量生成失败:', error);
|
||
message.error('部分内容生成失败,请检查');
|
||
} finally {
|
||
generatingAll.value = false;
|
||
}
|
||
};
|
||
|
||
/* AI生成内容 */
|
||
const generateContent = async (sectionIndex: number, childIndex?: number) => {
|
||
const section = navigationItems.value[sectionIndex];
|
||
|
||
if (section.mode === 'table') {
|
||
await generateTableContent(sectionIndex);
|
||
} else {
|
||
await generateDefaultContent(sectionIndex, childIndex);
|
||
}
|
||
};
|
||
|
||
/* 默认内容生成方法 */
|
||
const generateDefaultContent = async (sectionIndex: number, childIndex?: number) => {
|
||
const section = navigationItems.value[sectionIndex];
|
||
const contextData = await buildExportData();
|
||
|
||
section.generating = true;
|
||
try {
|
||
section.content = await generateAuditReport({
|
||
kbId: form.kbId || '',
|
||
kbIds: combinedKbIds.value,
|
||
formCommit: section.formCommit || 0,
|
||
history: section.content || '',
|
||
suggestion: section.suggestion || '',
|
||
...contextData
|
||
});
|
||
} finally {
|
||
section.generating = false;
|
||
}
|
||
};
|
||
|
||
/* 打开历史记录 */
|
||
const openHistory = (sectionIndex: number) => {
|
||
const section = navigationItems.value[sectionIndex];
|
||
const currentTable = section.tableOptions[section.currentTableIndex];
|
||
|
||
if (currentTable) {
|
||
// 通过表格配置获取接口名称
|
||
const tableConfig = getTableConfig(sectionIndex);
|
||
if (tableConfig && tableConfig.interfaceMap) {
|
||
currentInterfaceName.value = tableConfig.interfaceMap[currentTable.value] || '';
|
||
}
|
||
}
|
||
|
||
showHistory.value = true;
|
||
};
|
||
|
||
/* 处理历史记录选择 */
|
||
const handleHistorySelect = (record: any) => {
|
||
showHistory.value = false;
|
||
|
||
try {
|
||
const responseData = JSON.parse(record.responseData);
|
||
if (!responseData.success || !responseData.data) {
|
||
throw new Error('历史记录数据格式错误');
|
||
}
|
||
|
||
// 通过接口名称查找对应的表格配置
|
||
const tableConfig = findTableConfigByInterface(record.interfaceName);
|
||
if (!tableConfig) {
|
||
message.warning('不支持的历史记录类型');
|
||
return;
|
||
}
|
||
|
||
// 找到对应的章节索引
|
||
const sectionIndex = Object.keys(tableConfigs).findIndex(key => key === tableConfig.sectionType);
|
||
if (sectionIndex === -1) {
|
||
message.warning('未找到对应的章节');
|
||
return;
|
||
}
|
||
|
||
// 应用历史数据
|
||
applyHistoryData(sectionIndex, responseData.data, tableConfig.tableValue);
|
||
message.success('已应用选择的历史记录');
|
||
} catch (error: any) {
|
||
console.error('解析历史记录数据失败:', error);
|
||
message.error('历史记录数据解析失败');
|
||
}
|
||
};
|
||
|
||
/* 应用历史数据 */
|
||
const applyHistoryData = async (sectionIndex: number, data: any[], tableValue: string) => {
|
||
const section = navigationItems.value[sectionIndex];
|
||
if (!section) return;
|
||
|
||
const tableConfig = getTableConfig(sectionIndex);
|
||
if (!tableConfig) return;
|
||
|
||
// 找到对应的表格选项索引
|
||
const tableOptionIndex = section.tableOptions.findIndex(option => option.value === tableValue);
|
||
if (tableOptionIndex === -1) return;
|
||
|
||
// 使用数据映射函数
|
||
const dataMapper = createDataMapper(tableValue);
|
||
const mappedData = dataMapper(data);
|
||
|
||
// 保存数据
|
||
const dataKey = `${section.tableType}_${tableValue}`;
|
||
tableData[dataKey] = mappedData;
|
||
|
||
// 切换表格(如果当前不是这个表格)
|
||
if (section.currentTableIndex !== tableOptionIndex) {
|
||
await handleTableChange(sectionIndex, tableOptionIndex);
|
||
} else {
|
||
// 如果已经是当前表格,直接更新数据
|
||
section.data = mappedData;
|
||
}
|
||
|
||
// 特殊处理数据存储
|
||
if (tableValue === 'tripleOne') {
|
||
tripleOneData.value = data;
|
||
} else if (tableValue === 'assets') {
|
||
// 存储国资管理数据,供预算管理审计表使用
|
||
stateAssetsData.value = data;
|
||
}
|
||
};
|
||
|
||
/* 编辑行 */
|
||
const editRow = (record: any, sectionIndex: number) => {
|
||
const section = navigationItems.value[sectionIndex];
|
||
message.info(`编辑第${record.index}行数据`);
|
||
// 这里可以打开一个编辑弹窗
|
||
};
|
||
|
||
/* 删除行 */
|
||
const deleteRow = (record: any, sectionIndex: number) => {
|
||
const section = navigationItems.value[sectionIndex];
|
||
if (section.data) {
|
||
const index = section.data.findIndex((item: any) => item.key === record.key);
|
||
if (index !== -1) {
|
||
section.data.splice(index, 1);
|
||
message.success('删除成功');
|
||
}
|
||
}
|
||
};
|
||
|
||
/* 键盘快捷键处理 */
|
||
const handleKeydown = (event: KeyboardEvent) => {
|
||
if (!props.visible) return;
|
||
|
||
// Ctrl/Cmd + 数字键 1-9 快速跳转
|
||
if ((event.ctrlKey || event.metaKey) && event.key >= '1' && event.key <= '9') {
|
||
event.preventDefault();
|
||
const index = parseInt(event.key) - 1;
|
||
if (index < navigationItems.value.length) {
|
||
scrollToSection(index);
|
||
currentSection.value = index;
|
||
}
|
||
}
|
||
|
||
// 上下箭头键导航
|
||
if (event.key === 'ArrowUp' && event.ctrlKey) {
|
||
event.preventDefault();
|
||
const prevIndex = Math.max(0, currentSection.value - 1);
|
||
scrollToSection(prevIndex);
|
||
currentSection.value = prevIndex;
|
||
} else if (event.key === 'ArrowDown' && event.ctrlKey) {
|
||
event.preventDefault();
|
||
const nextIndex = Math.min(
|
||
navigationItems.value.length - 1,
|
||
currentSection.value + 1
|
||
);
|
||
scrollToSection(nextIndex);
|
||
currentSection.value = nextIndex;
|
||
}
|
||
};
|
||
|
||
// ========== 生命周期钩子 ==========
|
||
|
||
onMounted(() => {
|
||
const scrollContainer = getScrollContainer();
|
||
if (scrollContainer) {
|
||
scrollContainer.addEventListener('scroll', handleScroll);
|
||
}
|
||
document.addEventListener('keydown', handleKeydown);
|
||
});
|
||
|
||
onUnmounted(() => {
|
||
const scrollContainer = getScrollContainer();
|
||
if (scrollContainer) {
|
||
scrollContainer.removeEventListener('scroll', handleScroll);
|
||
}
|
||
document.removeEventListener('keydown', handleKeydown);
|
||
});
|
||
|
||
// ========== 观察者 ==========
|
||
|
||
watch(
|
||
() => props.visible,
|
||
async (visible) => {
|
||
if (visible) {
|
||
if (props.data) {
|
||
loading.value = true;
|
||
assignObject(form, props.data);
|
||
|
||
// 获取知识库并拼接 kbIds
|
||
if (props.data.libraryIds?.length > 0) {
|
||
try {
|
||
const result = await getPwlProjectLibraryByIds(props.data.libraryIds);
|
||
const kbIds = result.map((lib) => lib.kbId).filter((kbId) => kbId);
|
||
if (form.kbId) {
|
||
kbIds.unshift(form.kbId);
|
||
}
|
||
combinedKbIds.value = kbIds.join(',');
|
||
} catch (error) {
|
||
console.error('获取知识库失败:', error);
|
||
combinedKbIds.value = form.kbId || '';
|
||
}
|
||
} else {
|
||
combinedKbIds.value = form.kbId || '';
|
||
}
|
||
|
||
// 设置当前公司ID用于文档管理
|
||
currentCompanyId.value = props.data.companyId;
|
||
|
||
// 初始化所有表格的列配置
|
||
for (let i = 0; i < navigationItems.value.length; i++) {
|
||
const section = navigationItems.value[i];
|
||
if (section.tableOptions.length > 0) {
|
||
const tableOption = section.tableOptions[section.currentTableIndex];
|
||
if (typeof tableOption.columns === 'function') {
|
||
try {
|
||
const module = await tableOption.columns();
|
||
section.columns = module.default || module;
|
||
} catch (error) {
|
||
console.error(`加载表格${i}列配置失败:`, error);
|
||
section.columns = [];
|
||
}
|
||
} else {
|
||
section.columns = tableOption.columns;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 重置到第一个章节
|
||
currentSection.value = 0;
|
||
loading.value = false;
|
||
}
|
||
} else {
|
||
resetFields();
|
||
combinedKbIds.value = '';
|
||
// 清空表格数据
|
||
Object.keys(tableData).forEach(key => {
|
||
delete tableData[key];
|
||
});
|
||
// 清空特殊数据
|
||
tripleOneData.value = null;
|
||
stateAssetsData.value = null;
|
||
}
|
||
}
|
||
);
|
||
</script>
|
||
|
||
<script lang="ts">
|
||
import * as MenuIcons from '@/layout/menu-icons';
|
||
|
||
export default {
|
||
name: 'PwlProjectInfo',
|
||
components: MenuIcons
|
||
};
|
||
</script>
|
||
|
||
<style lang="less" scoped>
|
||
@import './style/style.scss';
|
||
|
||
/* 表格样式优化 */
|
||
:deep(.ant-table) {
|
||
.ant-table-thead > tr > th {
|
||
background-color: #fafafa;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.ant-table-tbody > tr > td {
|
||
padding: 8px 12px;
|
||
}
|
||
}
|
||
|
||
/* 长文本显示优化 */
|
||
.text-truncate {
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* 表格操作按钮 */
|
||
.action-buttons {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
|
||
/* 文件图标样式 */
|
||
.file-icon {
|
||
color: #1890ff;
|
||
margin-right: 4px;
|
||
}
|
||
|
||
/* 表格提示图标 */
|
||
.tooltip-icon {
|
||
color: #8c8c8c;
|
||
cursor: help;
|
||
}
|
||
</style>
|