2033 lines
64 KiB
Vue
2033 lines
64 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 danger @click="clearCurrentTable(index)">
|
||
清空表格
|
||
</a-button>
|
||
<a-button
|
||
type="primary"
|
||
@click="handleExportTable(index)"
|
||
:loading="exportStates[getTableKey(index)]"
|
||
>
|
||
<DownloadOutlined />
|
||
导出表格
|
||
</a-button>
|
||
<!-- 添加保存按钮 -->
|
||
<a-button
|
||
type="primary"
|
||
@click="saveCurrentTable(index)"
|
||
:loading="savingStates[getTableKey(index)]"
|
||
>
|
||
<SaveOutlined />
|
||
保存表格
|
||
</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">
|
||
<a-space>
|
||
<a-button type="primary" @click.native="editRow(index)"
|
||
>编辑</a-button
|
||
>
|
||
<a-button type="primary" danger @click.native="deleteRow(index)"
|
||
>删除</a-button
|
||
>
|
||
<a-button>导出</a-button>
|
||
<a-button @click="openEvidenceModal(index)"
|
||
>导出取证单</a-button
|
||
>
|
||
<a-button
|
||
type="primary"
|
||
@click="generateEvidence(index)"
|
||
:loading="generatingEvidenceStates[getTableKey(index)]"
|
||
>
|
||
生成取证单
|
||
</a-button>
|
||
<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>
|
||
</a-space>
|
||
</div>
|
||
<!-- 所有表格都显示历史记录 -->
|
||
<a @click="openHistory(index)" class="cursor-pointer">历史记录</a>
|
||
</div>
|
||
|
||
<!-- 表格内容 -->
|
||
<template v-if="item.mode === 'table' && item.columns">
|
||
<a-table
|
||
:row-selection="getRowSelection(index)"
|
||
:columns="item.columns"
|
||
:scroll="{ y: 500, x: 1000 }"
|
||
:pagination="false"
|
||
bordered
|
||
:row-key="(row) => row?.key ?? row?.id ?? row?.index"
|
||
: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
|
||
@click="openDoc(fileItem)"
|
||
v-for="(fileItem, fileIndex) in record.workPaperIndex"
|
||
:key="fileIndex"
|
||
>
|
||
<img
|
||
src="@/assets/word.png"
|
||
style="
|
||
width: 20px;
|
||
height: 20px;
|
||
vertical-align: middle;
|
||
margin-right: 4px;
|
||
"
|
||
alt=""
|
||
/>
|
||
|
||
<!-- 新格式支持 -->
|
||
<span
|
||
v-if="fileItem.fileUrl"
|
||
@click.stop="handleFilePreview(fileItem)"
|
||
class="file-link cursor-pointer text-wrap"
|
||
style="color: #1890ff; text-decoration: none"
|
||
>
|
||
{{ fileItem.fileName || '未命名文件' }}
|
||
</span>
|
||
|
||
<!-- 旧格式兼容 -->
|
||
<span v-else class="cursor-pointer text-wrap">
|
||
{{ 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] }}
|
||
</div>
|
||
</template>
|
||
|
||
<!-- 检查的证据及测试内容列 -->
|
||
<template v-if="column.key === 'checkEvidence'">
|
||
<div class="text-left">
|
||
{{ record[column.key] || '待填写' }}
|
||
</div>
|
||
</template>
|
||
</template>
|
||
</a-table>
|
||
<p v-if="item.extraTableTitle" class="font-bold my-2">{{
|
||
item.extraTableTitle
|
||
}}</p>
|
||
<a-table
|
||
v-if="item.extraColumns && item.extraColumns.length"
|
||
:columns="item.extraColumns"
|
||
:scroll="{ y: 500, x: 1000 }"
|
||
:pagination="false"
|
||
bordered
|
||
:data-source="item.extraData"
|
||
></a-table>
|
||
</template>
|
||
<!-- 单选模式 -->
|
||
<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"
|
||
/>
|
||
|
||
<!-- 编辑弹窗 -->
|
||
<EditModal
|
||
v-model:visible="editModalVisible"
|
||
:record="editingRecord"
|
||
:records="editingTargetRows"
|
||
:fields="editingFields"
|
||
@save="saveEdit"
|
||
/>
|
||
<EvidenceModal
|
||
v-model:visible="evidenceModalVisible"
|
||
:base-info="evidenceBaseInfo"
|
||
:project="data"
|
||
:selected-rows="evidenceSelectedRows"
|
||
/>
|
||
</a-drawer>
|
||
</template>
|
||
<script lang="ts" setup>
|
||
import { ref, reactive, watch, onMounted, onUnmounted, computed } from 'vue';
|
||
import { Form, message, Modal, TableProps } 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,
|
||
SaveOutlined
|
||
} from '@ant-design/icons-vue';
|
||
import { generateAuditReport } from '@/api/ai/auditReport';
|
||
import { getPwlProjectLibraryByIds } from '@/api/pwl/pwlProjectLibrary';
|
||
import { addAiHistory } from '@/api/ai/aiHistory';
|
||
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 EvidenceModal from '@/views/pwl/pwlProject/components/components/EvidenceModal.vue';
|
||
import EditModal from './EditModal.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 savingStates = reactive({});
|
||
|
||
// ========== 生成取证单状态管理 ==========
|
||
const generatingEvidenceStates = reactive({});
|
||
|
||
// ========== 编辑相关状态 ==========
|
||
const editModalVisible = ref(false);
|
||
const editingRecord = ref<any>(null);
|
||
const editingSectionIndex = ref(0);
|
||
const editingTableKey = ref('');
|
||
const editingFields = ref<any[]>([]);
|
||
const editingTargetRows = ref<any[]>([]);
|
||
|
||
// ========== 文档选择相关 ==========
|
||
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 evidenceModalVisible = ref(false);
|
||
const evidenceBaseInfo = reactive({
|
||
caseIndex: '',
|
||
projectName: '',
|
||
auditedTarget: '',
|
||
auditMatter: ''
|
||
});
|
||
const evidenceSelectedRows = ref<any[]>([]);
|
||
|
||
// ========== 表格选择 ==========
|
||
const selectedRowsMap = reactive<Record<number, any[]>>({});
|
||
const selectedRowKeysMap = reactive<Record<number, (string | number)[]>>({});
|
||
|
||
const getRowSelection = (
|
||
sectionIndex: number
|
||
): TableProps['rowSelection'] => {
|
||
return {
|
||
selectedRowKeys: selectedRowKeysMap[sectionIndex] || [],
|
||
onChange: (selectedRowKeys: (string | number)[], selectedRows: any[]) => {
|
||
selectedRowsMap[sectionIndex] = selectedRows;
|
||
selectedRowKeysMap[sectionIndex] = selectedRowKeys;
|
||
}
|
||
};
|
||
};
|
||
// ========== 历史记录相关 ==========
|
||
const showHistory = ref(false);
|
||
const currentInterfaceName = ref('');
|
||
|
||
// ========== 三重一大数据 ==========
|
||
const tripleOneData = ref<any>(null);
|
||
// ========== 国资管理数据 ==========
|
||
const stateAssetsData = ref<any>(null);
|
||
|
||
// ========== 表格数据存储 ==========
|
||
const tableData = reactive({});
|
||
|
||
// ========== 统一的表格生成数据存储 ==========
|
||
const tableGenerationData = reactive<
|
||
Record<
|
||
string,
|
||
{
|
||
requestData?: any;
|
||
responseData?: any;
|
||
interfaceName?: string;
|
||
timestamp?: number;
|
||
}
|
||
>
|
||
>({});
|
||
|
||
// ========== 表单数据 ==========
|
||
const form = reactive<PwlProject>({
|
||
// ... 表单属性保持不变
|
||
});
|
||
|
||
const { resetFields } = useForm(form);
|
||
|
||
// ========== 计算属性 ==========
|
||
const hasTripleOneData = computed(() => {
|
||
const section: any = navigationItems.value[2];
|
||
return section.data && section.data.length > 0;
|
||
});
|
||
|
||
// ========== 工具函数 ==========
|
||
const getTableKey = (sectionIndex: number) => {
|
||
const section: any = navigationItems.value[sectionIndex];
|
||
if (!section || !section.tableOptions || section.tableOptions.length === 0)
|
||
return '';
|
||
const currentTable = section.tableOptions[section.currentTableIndex];
|
||
if (!currentTable) return '';
|
||
return `${section.tableType}_${currentTable.value}`;
|
||
};
|
||
|
||
const getTableInfo = (sectionIndex: number) => {
|
||
const section: any = navigationItems.value[sectionIndex];
|
||
if (!section || !section.tableOptions || section.tableOptions.length === 0)
|
||
return null;
|
||
const currentTable = section.tableOptions[section.currentTableIndex];
|
||
if (!currentTable) return null;
|
||
|
||
return {
|
||
section,
|
||
currentTable,
|
||
tableKey: `${section.tableType}_${currentTable.value}`,
|
||
tableValue: currentTable.value
|
||
};
|
||
};
|
||
|
||
// ========== 通用方法 ==========
|
||
|
||
/* 更新visible */
|
||
const updateVisible = (value: boolean) => {
|
||
emit('update:visible', value);
|
||
};
|
||
|
||
/* 表格切换处理 */
|
||
const handleTableChange = async (
|
||
sectionIndex: number,
|
||
tableIndex: number
|
||
) => {
|
||
const section: any = 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 tableKey = `${section.tableType}_${tableOption.value}`;
|
||
if (tableData[tableKey]) {
|
||
section.data = tableData[tableKey];
|
||
} else {
|
||
section.data = [];
|
||
}
|
||
|
||
selectedRowsMap[sectionIndex] = [];
|
||
selectedRowKeysMap[sectionIndex] = [];
|
||
|
||
message.success(`已切换到:${tableOption.title}`);
|
||
};
|
||
|
||
/* 打开文档选择弹窗 */
|
||
const openDocSelect = (sectionIndex: number) => {
|
||
currentSectionIndex.value = sectionIndex;
|
||
showDocSelect.value = true;
|
||
|
||
const tableInfo = getTableInfo(sectionIndex);
|
||
if (!tableInfo) return;
|
||
|
||
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(tableInfo.tableKey);
|
||
};
|
||
|
||
/* 打开取证单预览弹窗 */
|
||
const openEvidenceModal = (sectionIndex: number) => {
|
||
const section: any = navigationItems.value[sectionIndex];
|
||
const tableInfo = getTableInfo(sectionIndex);
|
||
|
||
evidenceBaseInfo.caseIndex =
|
||
(form as any).caseIndex || props.data?.caseIndex || '';
|
||
evidenceBaseInfo.projectName = form.name || props.data?.name || '';
|
||
evidenceBaseInfo.auditedTarget =
|
||
(form as any).nickname || (props.data as any)?.nickname || '';
|
||
|
||
const baseMatter = section?.title || section?.name || '';
|
||
const tableTitle = tableInfo?.currentTable?.title || '';
|
||
evidenceBaseInfo.auditMatter = tableTitle
|
||
? `${baseMatter ? `${baseMatter} - ` : ''}${tableTitle}`
|
||
: baseMatter;
|
||
|
||
evidenceSelectedRows.value = selectedRowsMap[sectionIndex] || [];
|
||
evidenceModalVisible.value = true;
|
||
};
|
||
|
||
/* 保存表格生成数据 */
|
||
const saveTableGenerationData = (
|
||
tableKey: string,
|
||
requestData: any,
|
||
responseData: any,
|
||
tableValue?: string
|
||
) => {
|
||
// 根据tableKey找到对应的表格配置,获取正确的接口名称
|
||
let correctInterfaceName = tableKey;
|
||
|
||
// 从tableKey中提取sectionType和tableValue
|
||
const [sectionType, value] = tableKey.split('_');
|
||
|
||
// 查找对应的表格配置
|
||
const sectionKeys = Object.keys(tableConfigs);
|
||
const sectionIndex = sectionKeys.findIndex((key) => key === sectionType);
|
||
|
||
if (sectionIndex !== -1) {
|
||
const tableConfig = getTableConfig(sectionIndex);
|
||
if (
|
||
tableConfig &&
|
||
tableConfig.interfaceMap &&
|
||
tableConfig.interfaceMap[value || tableValue]
|
||
) {
|
||
correctInterfaceName = tableConfig.interfaceMap[value || tableValue];
|
||
}
|
||
}
|
||
|
||
tableGenerationData[tableKey] = {
|
||
requestData,
|
||
responseData,
|
||
interfaceName: correctInterfaceName, // 保存正确的接口名称
|
||
timestamp: Date.now()
|
||
};
|
||
};
|
||
|
||
/* 获取表格生成数据 */
|
||
const getTableGenerationData = (tableKey: string) => {
|
||
return tableGenerationData[tableKey] || null;
|
||
};
|
||
|
||
/* 通用表格内容生成方法 */
|
||
const generateTableContent = async (sectionIndex: number) => {
|
||
const tableInfo = getTableInfo(sectionIndex);
|
||
if (!tableInfo) {
|
||
message.error('表格配置不存在');
|
||
return;
|
||
}
|
||
|
||
const { section, currentTable, tableKey } = tableInfo;
|
||
|
||
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) {
|
||
// 保存表格生成数据
|
||
saveTableGenerationData(
|
||
tableKey,
|
||
requestData,
|
||
result.data,
|
||
currentTable.value
|
||
);
|
||
|
||
// 使用对应的数据映射函数
|
||
const dataMapper = createDataMapper(currentTable.value);
|
||
const mappedData = dataMapper(result.data.data);
|
||
|
||
// 处理不同类型的表格数据
|
||
if (currentTable.value === 'personnel') {
|
||
// 对于人员编制管理审计,需要分别处理主表格和额外表格数据
|
||
tableData[tableKey] = mappedData.mainData;
|
||
section.data = mappedData.mainData;
|
||
section.extraData = mappedData.extraData || []; // 设置额外表格数据
|
||
|
||
// 如果有额外数据,确保extraColumns存在
|
||
if (mappedData.extraData && mappedData.extraData.length > 0) {
|
||
if (!section.extraColumns) {
|
||
// 从配置中获取extraColumns
|
||
const tableConfig = getTableConfig(sectionIndex);
|
||
if (tableConfig && tableConfig.extraColumns) {
|
||
section.extraColumns = tableConfig.extraColumns;
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
// 其他表格的正常处理
|
||
tableData[tableKey] = mappedData;
|
||
section.data = mappedData;
|
||
}
|
||
|
||
// 特殊处理数据存储
|
||
if (currentTable.value === 'tripleOne') {
|
||
tripleOneData.value = result.data.data;
|
||
} else if (currentTable.value === 'assets') {
|
||
// 存储国资管理数据,供预算管理审计表使用
|
||
stateAssetsData.value = result.data.data;
|
||
}
|
||
|
||
section.suggestion = '';
|
||
const recordCount =
|
||
currentTable.value === 'personnel'
|
||
? mappedData.mainData.length
|
||
: mappedData.length;
|
||
message.success(
|
||
`成功生成 ${recordCount} 条${currentTable.title}记录` +
|
||
(currentTable.value === 'personnel' &&
|
||
mappedData.extraData?.length > 0
|
||
? `,以及 ${mappedData.extraData.length} 条福利费明细记录`
|
||
: '')
|
||
);
|
||
} else {
|
||
throw new Error(result.data?.error || result.message || '生成失败');
|
||
}
|
||
} catch (error: any) {
|
||
console.error(`生成表格内容失败:`, error);
|
||
message.error(`生成失败: ${error.message || '未知错误'}`);
|
||
} finally {
|
||
section.generating = false;
|
||
}
|
||
};
|
||
|
||
/* 生成取证单 */
|
||
const generateEvidence = async (sectionIndex: number) => {
|
||
const tableInfo = getTableInfo(sectionIndex);
|
||
if (!tableInfo) {
|
||
message.error('表格配置不存在');
|
||
return;
|
||
}
|
||
|
||
const { section, currentTable, tableKey } = tableInfo;
|
||
|
||
// 设置生成状态
|
||
generatingEvidenceStates[tableKey] = true;
|
||
|
||
try {
|
||
// 获取当前表格的生成数据
|
||
const generationData = getTableGenerationData(tableKey);
|
||
|
||
// 构建history参数,使用当前表格的responseData内容
|
||
let historyData = '';
|
||
if (generationData && generationData.responseData) {
|
||
historyData = JSON.stringify(generationData.responseData, null, 2);
|
||
} else if (section.data && section.data.length > 0) {
|
||
// 如果没有生成数据,使用当前表格数据
|
||
historyData = JSON.stringify(
|
||
{
|
||
success: true,
|
||
data: section.data,
|
||
data_source: currentTable.value
|
||
},
|
||
null,
|
||
2
|
||
);
|
||
}
|
||
|
||
// 检查history是否为空 - 新增的验证逻辑
|
||
if (!historyData || historyData.trim() === '') {
|
||
message.error(
|
||
'无法生成取证单:没有找到相关的审计数据,请先生成表格内容'
|
||
);
|
||
return;
|
||
}
|
||
|
||
// 调用generateAuditEvidence API
|
||
const projectCaseIndex =
|
||
(form as any).caseIndex || props.data?.caseIndex || '';
|
||
const requestData = {
|
||
// 基础信息
|
||
caseIndex: projectCaseIndex,
|
||
projectName: form.name || props.data?.name || '',
|
||
auditedTarget:
|
||
(form as any).nickname || (props.data as any)?.nickname || '',
|
||
auditMatter:
|
||
currentTable.title || section?.title || section?.name || '',
|
||
summaryTitle: section?.title || section?.name || '',
|
||
auditRecord: '',
|
||
auditFinding: '',
|
||
evidenceBasis: '',
|
||
handling: '',
|
||
suggestion: section.suggestion || '',
|
||
attachment: '',
|
||
auditors: '',
|
||
compileDate: new Date().toISOString().split('T')[0], // 当前日期
|
||
providerOpinion: '',
|
||
providerDate: '',
|
||
attachmentPages: '',
|
||
feedbackDeadline: '',
|
||
|
||
// 历史内容(用于工作流生成)
|
||
history: historyData,
|
||
|
||
// 项目相关参数
|
||
projectId: props.data?.id || '',
|
||
kbIds: props.data?.kbId || '',
|
||
libraryIds: props.data?.libraryIds || '',
|
||
projectLibrary: props.data?.projectLibrary || '',
|
||
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'] }
|
||
: {}),
|
||
|
||
// 用户信息(后端会自动设置,这里可以留空或传空字符串)
|
||
userName: ''
|
||
};
|
||
|
||
const apiResult = await auditContentApi.generateAuditEvidence(
|
||
requestData
|
||
);
|
||
|
||
if (apiResult.code === 0 && apiResult.data?.success) {
|
||
message.success('取证单生成成功');
|
||
|
||
console.log('生成的取证单数据:', apiResult.data);
|
||
|
||
// 自动打开取证单预览弹窗并填充数据
|
||
evidenceBaseInfo.caseIndex = projectCaseIndex;
|
||
evidenceBaseInfo.projectName =
|
||
apiResult.data.projectName || form.name || props.data?.name || '';
|
||
evidenceBaseInfo.auditedTarget =
|
||
apiResult.data.auditedTarget ||
|
||
(form as any).nickname ||
|
||
(props.data as any)?.nickname ||
|
||
'';
|
||
evidenceBaseInfo.auditMatter =
|
||
apiResult.data.auditMatter ||
|
||
currentTable.title ||
|
||
section?.title ||
|
||
section?.name ||
|
||
'';
|
||
|
||
// 将生成的取证单数据作为选中的行数据传入,包含所有字段
|
||
const evidenceData = {
|
||
// 基础信息字段
|
||
caseIndex: projectCaseIndex,
|
||
projectName: apiResult.data.projectName || '',
|
||
auditedTarget: apiResult.data.auditedTarget || '',
|
||
auditMatter: apiResult.data.auditMatter || '',
|
||
summaryTitle: apiResult.data.summaryTitle || '',
|
||
auditRecord: apiResult.data.auditRecord || '',
|
||
auditFinding: apiResult.data.auditFinding || '',
|
||
evidenceBasis: apiResult.data.evidenceBasis || '',
|
||
handling: apiResult.data.handling || '',
|
||
suggestion: apiResult.data.suggestion || '',
|
||
auditors: apiResult.data.auditors || '',
|
||
compileDate: apiResult.data.compileDate || '',
|
||
providerOpinion: apiResult.data.providerOpinion || '',
|
||
providerDate: apiResult.data.providerDate || '',
|
||
attachmentPages: apiResult.data.attachmentPages || '',
|
||
feedbackDeadline: apiResult.data.feedbackDeadline || '',
|
||
|
||
// 处理附件字段,如果是数组则转换为字符串
|
||
attachment: Array.isArray(apiResult.data.attachment)
|
||
? apiResult.data.attachment.join('\n')
|
||
: apiResult.data.attachment || '',
|
||
|
||
// 确保页码字段有默认值
|
||
pageIndex: '1',
|
||
pageTotal: '1',
|
||
|
||
// 保留其他可能的字段(如success、processing_time、generated_time等)
|
||
success: apiResult.data.success,
|
||
processing_time: apiResult.data.processing_time,
|
||
generated_time: apiResult.data.generated_time
|
||
};
|
||
|
||
evidenceSelectedRows.value = [evidenceData];
|
||
evidenceModalVisible.value = true;
|
||
} else {
|
||
throw new Error(
|
||
apiResult.data?.error || apiResult.message || '生成取证单失败'
|
||
);
|
||
}
|
||
} catch (error: any) {
|
||
console.error('生成取证单失败:', error);
|
||
message.error(`生成取证单失败: ${error.message || '未知错误'}`);
|
||
} finally {
|
||
generatingEvidenceStates[tableKey] = false;
|
||
}
|
||
};
|
||
|
||
/* 通用表格导出方法 */
|
||
const handleExportTable = async (sectionIndex: number) => {
|
||
const tableInfo = getTableInfo(sectionIndex);
|
||
if (!tableInfo) return;
|
||
|
||
const { section, currentTable, tableKey } = tableInfo;
|
||
|
||
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 || '未知时间';
|
||
|
||
// 设置导出状态
|
||
exportStates[tableKey] = 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[tableKey] = 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: any = navigationItems.value[sectionIndex];
|
||
|
||
if (section.mode === 'table') {
|
||
await generateTableContent(sectionIndex);
|
||
} else {
|
||
await generateDefaultContent(sectionIndex, childIndex);
|
||
}
|
||
};
|
||
|
||
/* 默认内容生成方法 */
|
||
const generateDefaultContent = async (
|
||
sectionIndex: number,
|
||
childIndex?: number
|
||
) => {
|
||
const section: any = 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 tableInfo = getTableInfo(sectionIndex);
|
||
if (!tableInfo) return;
|
||
|
||
const { section, currentTable } = tableInfo;
|
||
|
||
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,
|
||
record.requestData,
|
||
record.interfaceName
|
||
);
|
||
message.success('已应用选择的历史记录');
|
||
} catch (error: any) {
|
||
console.error('解析历史记录数据失败:', error);
|
||
message.error('历史记录数据解析失败');
|
||
}
|
||
};
|
||
|
||
/* 应用历史数据 */
|
||
const applyHistoryData = async (
|
||
sectionIndex: number,
|
||
data: any[],
|
||
tableValue: string,
|
||
requestDataStr?: string,
|
||
interfaceName?: string
|
||
) => {
|
||
const section: any = 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;
|
||
|
||
// 解析请求数据
|
||
let requestData = null;
|
||
try {
|
||
requestData = requestDataStr ? JSON.parse(requestDataStr) : null;
|
||
} catch (e) {
|
||
console.warn('解析历史请求数据失败:', e);
|
||
}
|
||
|
||
// 构建响应数据对象
|
||
const responseData = {
|
||
success: true,
|
||
data: data,
|
||
data_source: interfaceName || tableValue
|
||
};
|
||
|
||
// 保存表格生成数据
|
||
const tableKey = `${section.tableType}_${tableValue}`;
|
||
saveTableGenerationData(tableKey, requestData, responseData, interfaceName);
|
||
|
||
// 使用数据映射函数
|
||
const dataMapper = createDataMapper(tableValue);
|
||
const mappedData = dataMapper(data);
|
||
|
||
// 保存数据
|
||
if (tableValue === 'personnel') {
|
||
tableData[tableKey] = mappedData.mainData;
|
||
section.extraData = mappedData.extraData || []; // 设置额外表格数据
|
||
|
||
// 如果有额外数据,确保extraColumns存在
|
||
if (mappedData.extraData && mappedData.extraData.length > 0) {
|
||
if (!section.extraColumns) {
|
||
section.extraColumns = tableConfig.extraColumns || [];
|
||
}
|
||
}
|
||
} else {
|
||
tableData[tableKey] = mappedData;
|
||
}
|
||
|
||
// 切换表格(如果当前不是这个表格)
|
||
if (section.currentTableIndex !== tableOptionIndex) {
|
||
await handleTableChange(sectionIndex, tableOptionIndex);
|
||
} else {
|
||
// 如果已经是当前表格,直接更新数据
|
||
if (tableValue === 'personnel') {
|
||
section.data = mappedData.mainData;
|
||
} else {
|
||
section.data = mappedData;
|
||
}
|
||
}
|
||
|
||
// 特殊处理数据存储
|
||
if (tableValue === 'tripleOne') {
|
||
tripleOneData.value = data;
|
||
} else if (tableValue === 'assets') {
|
||
// 存储国资管理数据,供预算管理审计表使用
|
||
stateAssetsData.value = data;
|
||
}
|
||
|
||
// 清空选中行
|
||
selectedRowsMap[sectionIndex] = [];
|
||
selectedRowKeysMap[sectionIndex] = [];
|
||
};
|
||
|
||
/* 编辑行 */
|
||
const editRow = (sectionIndex: number) => {
|
||
const section: any = navigationItems.value[sectionIndex];
|
||
if (!section || !section.columns) return;
|
||
|
||
const selectedRows = selectedRowsMap[sectionIndex] || [];
|
||
if (!selectedRows.length) {
|
||
message.warning('请先选择要编辑的行');
|
||
return;
|
||
}
|
||
|
||
// 获取可编辑的字段(排除操作列)
|
||
editingFields.value = section.columns.filter(
|
||
(col) =>
|
||
col.key !== 'action' &&
|
||
col.dataIndex !== 'key' &&
|
||
col.dataIndex !== 'index' &&
|
||
col.dataIndex !== 'workPaperIndex' &&
|
||
col.key !== 'workPaperIndex'
|
||
);
|
||
|
||
// 将所有选中的行传递给编辑弹窗
|
||
editingRecord.value = { ...selectedRows[0] };
|
||
editingTargetRows.value = selectedRows.map((row: any) => ({ ...row }));
|
||
editingSectionIndex.value = sectionIndex;
|
||
|
||
// 记录当前编辑的表格key
|
||
const tableInfo = getTableInfo(sectionIndex);
|
||
if (tableInfo) {
|
||
editingTableKey.value = tableInfo.tableKey;
|
||
} else {
|
||
editingTableKey.value = '';
|
||
}
|
||
|
||
editModalVisible.value = true;
|
||
};
|
||
|
||
/* 保存编辑 */
|
||
const saveEdit = (updatedRecords: any | any[]) => {
|
||
const section: any = navigationItems.value[editingSectionIndex.value];
|
||
if (!section || !section.data) return;
|
||
|
||
const targets =
|
||
editingTargetRows.value && editingTargetRows.value.length
|
||
? editingTargetRows.value
|
||
: editingRecord.value
|
||
? [editingRecord.value]
|
||
: [];
|
||
|
||
if (!targets.length) {
|
||
message.warning('未找到需要编辑的行');
|
||
return;
|
||
}
|
||
|
||
const updatesArray = Array.isArray(updatedRecords)
|
||
? updatedRecords
|
||
: [updatedRecords];
|
||
|
||
// 只提取允许编辑的字段,避免把首行的其他值覆盖到所有行
|
||
const allowedKeys = new Set<string>();
|
||
const collectKeys = (field: any) => {
|
||
if (field?.dataIndex) allowedKeys.add(field.dataIndex);
|
||
if (field?.children && Array.isArray(field.children)) {
|
||
field.children.forEach((child: any) => collectKeys(child));
|
||
}
|
||
};
|
||
editingFields.value.forEach((field: any) => collectKeys(field));
|
||
|
||
const buildApplyFields = (source: any) => {
|
||
const applied: Record<string, any> = {};
|
||
allowedKeys.forEach((key) => {
|
||
if (key === 'key' || key === 'index') return;
|
||
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
||
applied[key] = source[key];
|
||
}
|
||
});
|
||
return applied;
|
||
};
|
||
|
||
const stripResponseFields = (data: any) => {
|
||
const { workPaperIndex, key, index, ...rest } = data;
|
||
return rest;
|
||
};
|
||
|
||
const findRawIndex = (
|
||
responseList: any[],
|
||
targetRow: any,
|
||
fallbackUiIndex: number
|
||
) => {
|
||
const hasValidId =
|
||
targetRow?.id !== undefined &&
|
||
targetRow?.id !== null &&
|
||
targetRow?.id !== '';
|
||
|
||
if (hasValidId) {
|
||
const byId = responseList.findIndex(
|
||
(item: any) => item.id === targetRow.id
|
||
);
|
||
if (byId !== -1) return byId;
|
||
}
|
||
|
||
if (
|
||
typeof targetRow?.key === 'number' &&
|
||
targetRow.key >= 0 &&
|
||
targetRow.key < responseList.length
|
||
) {
|
||
return targetRow.key;
|
||
}
|
||
|
||
if (targetRow?.key !== undefined && targetRow?.key !== null) {
|
||
const byKey = responseList.findIndex(
|
||
(item: any) => item.key !== undefined && item.key === targetRow.key
|
||
);
|
||
if (byKey !== -1) return byKey;
|
||
}
|
||
|
||
if (
|
||
fallbackUiIndex !== -1 &&
|
||
fallbackUiIndex >= 0 &&
|
||
fallbackUiIndex < responseList.length
|
||
) {
|
||
return fallbackUiIndex;
|
||
}
|
||
|
||
return -1;
|
||
};
|
||
|
||
let hasApplied = false;
|
||
|
||
targets.forEach((targetRow: any, idx: number) => {
|
||
const updateSource = updatesArray[idx] || updatesArray[0] || {};
|
||
const appliedFields = buildApplyFields(updateSource);
|
||
|
||
if (Object.keys(appliedFields).length === 0) {
|
||
return;
|
||
}
|
||
|
||
hasApplied = true;
|
||
|
||
const uiIndex = section.data.findIndex(
|
||
(item: any) => item.key === targetRow.key
|
||
);
|
||
|
||
if (uiIndex !== -1) {
|
||
const originalRecord = section.data[uiIndex];
|
||
const merged = { ...originalRecord, ...appliedFields };
|
||
merged.key = originalRecord.key;
|
||
if (Object.prototype.hasOwnProperty.call(originalRecord, 'index')) {
|
||
merged.index = originalRecord.index;
|
||
}
|
||
section.data.splice(uiIndex, 1, merged);
|
||
}
|
||
|
||
if (editingTableKey.value && tableGenerationData[editingTableKey.value]) {
|
||
const responseData =
|
||
tableGenerationData[editingTableKey.value].responseData;
|
||
|
||
if (
|
||
responseData &&
|
||
responseData.data &&
|
||
Array.isArray(responseData.data)
|
||
) {
|
||
const rawIndex = findRawIndex(responseData.data, targetRow, uiIndex);
|
||
|
||
if (rawIndex !== -1) {
|
||
responseData.data[rawIndex] = {
|
||
...responseData.data[rawIndex],
|
||
...stripResponseFields(appliedFields)
|
||
};
|
||
} else {
|
||
responseData.data.push(stripResponseFields(appliedFields));
|
||
}
|
||
|
||
tableGenerationData[editingTableKey.value].responseData =
|
||
responseData;
|
||
}
|
||
}
|
||
});
|
||
|
||
if (editingTableKey.value) {
|
||
tableData[editingTableKey.value] = section.data;
|
||
}
|
||
|
||
const currentSelections = selectedRowsMap[editingSectionIndex.value] || [];
|
||
selectedRowsMap[editingSectionIndex.value] = currentSelections.map(
|
||
(row: any) =>
|
||
section.data.find((item: any) => item.key === row.key) || row
|
||
);
|
||
|
||
if (!hasApplied) {
|
||
message.warning('没有可应用的字段');
|
||
return;
|
||
}
|
||
|
||
message.success('编辑成功');
|
||
editModalVisible.value = false;
|
||
editingTableKey.value = '';
|
||
editingTargetRows.value = [];
|
||
};
|
||
|
||
/* 删除行(多选) */
|
||
const deleteRow = (sectionIndex: number) => {
|
||
const section: any = navigationItems.value[sectionIndex];
|
||
if (!section || !section.data) return;
|
||
|
||
const selectedRows = selectedRowsMap[sectionIndex] || [];
|
||
if (!selectedRows.length) {
|
||
message.warning('请先选择要删除的行');
|
||
return;
|
||
}
|
||
|
||
const tableInfo = getTableInfo(sectionIndex);
|
||
const responseData =
|
||
tableInfo &&
|
||
tableInfo.tableKey &&
|
||
tableGenerationData[tableInfo.tableKey] &&
|
||
tableGenerationData[tableInfo.tableKey].responseData &&
|
||
tableGenerationData[tableInfo.tableKey].responseData.data &&
|
||
Array.isArray(tableGenerationData[tableInfo.tableKey].responseData.data)
|
||
? tableGenerationData[tableInfo.tableKey].responseData
|
||
: null;
|
||
|
||
const findRawIndex = (
|
||
responseList: any[],
|
||
targetRow: any,
|
||
fallbackUiIndex: number
|
||
) => {
|
||
const hasValidId =
|
||
targetRow?.id !== undefined &&
|
||
targetRow?.id !== null &&
|
||
targetRow?.id !== '';
|
||
if (hasValidId) {
|
||
const byId = responseList.findIndex(
|
||
(item: any) => item.id === targetRow.id
|
||
);
|
||
if (byId !== -1) return byId;
|
||
}
|
||
|
||
if (
|
||
typeof targetRow?.key === 'number' &&
|
||
targetRow.key >= 0 &&
|
||
targetRow.key < responseList.length
|
||
) {
|
||
return targetRow.key;
|
||
}
|
||
|
||
if (targetRow?.key !== undefined && targetRow?.key !== null) {
|
||
const byKey = responseList.findIndex(
|
||
(item: any) => item.key !== undefined && item.key === targetRow.key
|
||
);
|
||
if (byKey !== -1) return byKey;
|
||
}
|
||
|
||
if (
|
||
fallbackUiIndex !== -1 &&
|
||
fallbackUiIndex >= 0 &&
|
||
fallbackUiIndex < responseList.length
|
||
) {
|
||
return fallbackUiIndex;
|
||
}
|
||
|
||
return -1;
|
||
};
|
||
|
||
const uiIndexesToDelete: number[] = [];
|
||
const rawIndexesToDelete: number[] = [];
|
||
|
||
selectedRows.forEach((row: any) => {
|
||
const uiIndex = section.data.findIndex(
|
||
(item: any) => item.key === row.key
|
||
);
|
||
if (uiIndex !== -1) uiIndexesToDelete.push(uiIndex);
|
||
|
||
if (responseData) {
|
||
const rawIndex = findRawIndex(responseData.data, row, uiIndex);
|
||
if (rawIndex !== -1) rawIndexesToDelete.push(rawIndex);
|
||
}
|
||
});
|
||
|
||
const uniqueUiIndexes = Array.from(new Set(uiIndexesToDelete)).sort(
|
||
(a, b) => b - a
|
||
);
|
||
uniqueUiIndexes.forEach((idx) => {
|
||
section.data.splice(idx, 1);
|
||
});
|
||
|
||
// 删除后重排 key/index,保证后续操作按 key(索引) 能正确匹配
|
||
section.data = section.data.map((item: any, index: number) => {
|
||
const next: any = { ...item, key: index };
|
||
if (item && Object.prototype.hasOwnProperty.call(item, 'index')) {
|
||
next.index = index + 1;
|
||
}
|
||
return next;
|
||
});
|
||
|
||
if (tableInfo && tableInfo.tableKey) {
|
||
tableData[tableInfo.tableKey] = section.data;
|
||
}
|
||
|
||
if (responseData && tableInfo && tableInfo.tableKey) {
|
||
const uniqueRawIndexes = Array.from(new Set(rawIndexesToDelete)).sort(
|
||
(a, b) => b - a
|
||
);
|
||
uniqueRawIndexes.forEach((idx) => {
|
||
responseData.data.splice(idx, 1);
|
||
});
|
||
tableGenerationData[tableInfo.tableKey].responseData = responseData;
|
||
}
|
||
|
||
selectedRowsMap[sectionIndex] = [];
|
||
selectedRowKeysMap[sectionIndex] = [];
|
||
|
||
message.success(
|
||
`已删除${uniqueUiIndexes.length || selectedRows.length}条记录`
|
||
);
|
||
};
|
||
|
||
/* 保存当前表格到历史记录 */
|
||
const saveCurrentTable = async (sectionIndex: number) => {
|
||
const tableInfo = getTableInfo(sectionIndex);
|
||
if (!tableInfo) return;
|
||
|
||
const { section, currentTable, tableKey } = tableInfo;
|
||
|
||
if (!section || !section.data || section.data.length === 0) {
|
||
message.warning('没有数据可保存');
|
||
return;
|
||
}
|
||
|
||
// 设置保存状态
|
||
savingStates[tableKey] = true;
|
||
|
||
try {
|
||
// 获取表格生成数据
|
||
const generationData = getTableGenerationData(tableKey);
|
||
|
||
if (!generationData || !generationData.responseData) {
|
||
message.warning('没有找到该表格的生成记录,请先生成数据');
|
||
return;
|
||
}
|
||
|
||
// 关键修复:从表格配置中获取正确的接口名称
|
||
const tableConfig = getTableConfig(sectionIndex);
|
||
let correctInterfaceName = generationData.interfaceName; // 默认为当前值
|
||
|
||
if (
|
||
tableConfig &&
|
||
tableConfig.interfaceMap &&
|
||
tableConfig.interfaceMap[currentTable.value]
|
||
) {
|
||
correctInterfaceName = tableConfig.interfaceMap[currentTable.value];
|
||
}
|
||
|
||
const normalizedResponseData = (() => {
|
||
const src = generationData.responseData;
|
||
if (!src || !src.data || !Array.isArray(src.data)) return src;
|
||
|
||
const cloned = JSON.parse(JSON.stringify(src));
|
||
cloned.data = cloned.data.map((row: any) => {
|
||
const workPaperIndex = row?.workPaperIndex;
|
||
if (!Array.isArray(workPaperIndex)) return row;
|
||
|
||
const normalized = workPaperIndex
|
||
.map((item: any) => {
|
||
if (typeof item === 'string') return item;
|
||
if (item && typeof item === 'object') {
|
||
const fileId = item.fileId || item.file_id || '';
|
||
const fileName = item.fileName || item.file_name || '';
|
||
const fileUrl = item.fileUrl || item.url || '';
|
||
return `${fileId}||${fileName}||${fileUrl}`;
|
||
}
|
||
return '';
|
||
})
|
||
.filter((s: string) => s && s.trim() !== '');
|
||
|
||
return {
|
||
...row,
|
||
workPaperIndex: normalized
|
||
};
|
||
});
|
||
|
||
return cloned;
|
||
})();
|
||
|
||
const aiHistory = {
|
||
projectId: props.data?.id,
|
||
interfaceName: correctInterfaceName, // 使用正确的接口名称
|
||
requestData: generationData.requestData
|
||
? JSON.stringify(generationData.requestData)
|
||
: '',
|
||
responseData: JSON.stringify(normalizedResponseData)
|
||
// 其他字段后端会自动设置
|
||
};
|
||
|
||
// 调用后端保存接口
|
||
await addAiHistory(aiHistory);
|
||
message.success('保存成功');
|
||
} catch (error: any) {
|
||
console.error('保存表格失败:', error);
|
||
message.error('保存失败: ' + (error.message || '未知错误'));
|
||
} finally {
|
||
savingStates[tableKey] = false;
|
||
}
|
||
};
|
||
|
||
const clearCurrentTable = (sectionIndex: number) => {
|
||
const section: any = navigationItems.value[sectionIndex];
|
||
if (
|
||
!section ||
|
||
section.mode !== 'table' ||
|
||
!section.tableOptions ||
|
||
section.tableOptions.length === 0
|
||
)
|
||
return;
|
||
|
||
Modal.confirm({
|
||
title: '确认清空表格?',
|
||
content: '将清空当前章节下所有表格选项卡的数据,且无法恢复。',
|
||
okText: '清空',
|
||
okType: 'danger',
|
||
cancelText: '取消',
|
||
onOk: () => {
|
||
section.tableOptions.forEach((option: any) => {
|
||
const tableKey = `${section.tableType}_${option.value}`;
|
||
|
||
tableData[tableKey] = [];
|
||
|
||
const gen = tableGenerationData[tableKey];
|
||
if (gen && gen.responseData) {
|
||
const next = { ...gen.responseData, data: [] };
|
||
tableGenerationData[tableKey].responseData = next;
|
||
}
|
||
|
||
if (exportStates[tableKey] !== undefined)
|
||
delete exportStates[tableKey];
|
||
if (savingStates[tableKey] !== undefined)
|
||
delete savingStates[tableKey];
|
||
if (generatingEvidenceStates[tableKey] !== undefined)
|
||
delete generatingEvidenceStates[tableKey];
|
||
|
||
if (option.value === 'tripleOne') {
|
||
tripleOneData.value = null;
|
||
}
|
||
if (option.value === 'assets') {
|
||
stateAssetsData.value = null;
|
||
}
|
||
});
|
||
|
||
section.data = [];
|
||
message.success('已清空表格');
|
||
|
||
selectedRowsMap[sectionIndex] = [];
|
||
selectedRowKeysMap[sectionIndex] = [];
|
||
}
|
||
});
|
||
};
|
||
|
||
const openDoc = (fileItem) => {
|
||
console.log(fileItem);
|
||
// window.open(
|
||
// `http://view.officeapps.live.com/op/view.aspx?src=文件地址`,
|
||
// '_blank'
|
||
// );
|
||
};
|
||
|
||
/* 键盘快捷键处理 */
|
||
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 && 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: any = 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 = '';
|
||
evidenceModalVisible.value = false;
|
||
// 清空表格数据
|
||
Object.keys(tableData).forEach((key) => {
|
||
delete tableData[key];
|
||
});
|
||
// 清空表格生成数据
|
||
Object.keys(tableGenerationData).forEach((key) => {
|
||
delete tableGenerationData[key];
|
||
});
|
||
// 清空特殊数据
|
||
tripleOneData.value = null;
|
||
stateAssetsData.value = null;
|
||
// 清空选中行
|
||
Object.keys(selectedRowsMap).forEach((key) => {
|
||
delete selectedRowsMap[key as any];
|
||
});
|
||
Object.keys(selectedRowKeysMap).forEach((key) => {
|
||
delete selectedRowKeysMap[key as any];
|
||
});
|
||
// 清空生成取证单状态
|
||
Object.keys(generatingEvidenceStates).forEach((key) => {
|
||
delete generatingEvidenceStates[key as any];
|
||
});
|
||
}
|
||
}
|
||
);
|
||
|
||
const handleFilePreview = (file) => {
|
||
console.log(
|
||
file.fileUrl,
|
||
`https://view.officeapps.live.com/op/view.aspx?src=${encodeURIComponent(
|
||
file.fileUrl
|
||
)}`
|
||
);
|
||
window.open(
|
||
`https://view.officeapps.live.com/op/view.aspx?src=${encodeURIComponent(
|
||
file.fileUrl
|
||
)}`
|
||
);
|
||
};
|
||
</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>
|