Compare commits

..

4 Commits

14 changed files with 4630 additions and 456 deletions

View File

@@ -114,7 +114,7 @@ export async function updateAiHistoryBatch(data: { list: AiHistory[] }) {
}
/**
* 批量删除AI审计历史记录表
* 批量删除 AI审计历史记录表
*/
export async function removeAiHistoryBatch(ids: number[]) {
const res = await request.delete<ApiResult<unknown>>(
@@ -127,3 +127,20 @@ export async function removeAiHistoryBatch(ids: number[]) {
return Promise.reject(new Error(res.data.message));
}
/**
* 根据接口名称和项目 ID 查询最新的历史记录
*/
export async function getLatestHistoryByInterface(params: {
interfaceName: string;
projectId: number;
}) {
const res = await request.get<ApiResult<AiHistory>>(
`${MODULES_API_URL}/ai/history/latest`,
{ params }
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return null; // 如果没有数据,返回 null
}

View File

@@ -780,7 +780,7 @@ export async function generateAuditEvidence(data: {
}
/**
* 下载审计取证单Word文档
* 下载审计取证单 Word 文档
*/
export async function downloadAuditEvidence(data: {
caseIndex?: string;
@@ -814,4 +814,41 @@ export async function downloadAuditEvidence(data: {
return res.data;
}
return Promise.reject(new Error('文件下载失败'));
}
/**
* 保存审计取证单到数据库
*/
export async function saveAuditEvidence(data: {
caseIndex?: string;
projectId?: number;
projectName?: string;
auditedTarget?: string;
contentType?: number;
auditMatterType?: string;
auditMatter?: string;
summaryTitle?: string;
auditRecord?: string;
auditFinding?: string;
evidenceBasis?: string;
handling?: string;
suggestion?: string;
attachment?: string;
auditors?: string;
compileDate?: string;
providerOpinion?: string;
providerDate?: string;
attachmentPages?: string;
feedbackDeadline?: string;
history?: string;
}) {
const res = await request.post<ApiResult<any>>(
MODULES_API_URL + '/ai/auditEvidence/save',
data
);
if (res.data.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.data.message));
}

View File

@@ -67,4 +67,233 @@ export async function generateAuditReport2(
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 保存审计报告到数据库
*/
export async function saveAuditReport(data: {
projectId?: number;
projectName?: string;
caseIndex?: string;
auditedTarget?: string;
reportContent?: string;
previewHtml?: string;
sectionCount?: number;
formCommit?: number;
}) {
const res = await request.post<ApiResult<any>>(
MODULES_API_URL + '/ai/auditReport/save',
data
);
if (res.data.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询审计报告
*/
export async function queryAuditReport(params: {
projectId: number;
formCommit: number;
}) {
const res = await request.post<ApiResult<any>>(
MODULES_API_URL + '/ai/auditReport/query',
null,
{
params
}
);
if (res.data.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据项目 ID 查询审计报告和取证单数据
* @param projectId 项目 ID
* @param evidenceIds 勾选的取证单 ID 列表(可选)
*/
export async function queryAuditDataByProjectId(projectId: number, evidenceIds?: number[]) {
console.log('=== 前端发起请求 ===');
console.log('projectId:', projectId);
console.log('evidenceIds:', evidenceIds);
const res = await request.post<ApiResult<any>>(
MODULES_API_URL + '/ai/auditReport/queryAuditDataByProjectId',
evidenceIds ? { evidenceIds } : {},
{
params: { projectId },
headers: {
'Content-Type': 'application/json;charset=UTF-8'
}
}
);
console.log('=== 后端返回响应 ===', res);
console.log('res.data.code:', res.data.code);
console.log('res.data.message:', res.data.message);
console.log('res.data.data:', res.data.data);
if (res.data.code === 0) {
return res.data;
}
console.error('API 返回错误 - code:', res.data.code, ', message:', res.data.message);
return Promise.reject(new Error(res.data.message || '操作失败'));
}
/**
* 根据项目 ID 生成审计报告并下载
*/
export async function generateAuditReportByProjectId(projectId: number) {
const res = await request.post(
MODULES_API_URL + '/ai/auditReport/generateByProjectId',
null,
{
params: { projectId },
responseType: 'blob' // 处理二进制流响应
}
);
if (res.status === 200) {
return res.data;
}
return Promise.reject(new Error('文件下载失败'));
}
/**
* 根据项目 ID 和选中的取证单生成审计报告并下载
*/
export async function generateAuditReportWithEvidences(projectId: number, evidenceIds: number[]) {
const res = await request.post(
MODULES_API_URL + '/ai/auditReport/generateWithEvidences',
{ evidenceIds },
{
params: { projectId },
responseType: 'blob' // 处理二进制流响应
}
);
if (res.status === 200) {
return res.data;
}
return Promise.reject(new Error('文件下载失败'));
}
/**
* 根据项目 ID、选中的取证单和章节内容生成审计报告并下载
* @param projectId 项目 ID
* @param evidenceIds 勾选的取证单 ID 列表
* @param chapters 章节内容数组(包含 formCommit 和 reportContent
* @param evaluate 总体评价
* @param suggestion 审计建议
*/
export async function generateAuditReportWithContent(
projectId: number,
evidenceIds: number[],
chapters: Array<{
formCommit: number;
reportContent: string;
}>,
evaluate?: string,
suggestion?: string
) {
const res = await request.post(
MODULES_API_URL + '/ai/auditReport/generateWithContent',
{
evidenceIds,
chapters,
evaluate,
suggestion
},
{
params: { projectId },
responseType: 'blob' // 处理二进制流响应
}
);
if (res.status === 200) {
return res.data;
}
return Promise.reject(new Error('文件下载失败'));
}
/**
* AI 生成默认话术
*/
export async function generateDefaultText(params: {
projectId?: number;
formCommit: number;
chapterTitle?: string;
evidenceIds?: number[]; // 新增:选中的取证单 ID 列表
}) {
const res = await request.post<ApiResult<string>>(
MODULES_API_URL + '/ai/auditReport/generateDefaultText',
{
projectId: params.projectId,
formCommit: params.formCommit,
chapterTitle: params.chapterTitle,
evidenceIds: params.evidenceIds
}
);
if (res.data.code === 0) {
// 后端返回的数据在 message 字段中
return res.data.message || res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* AI 分析用户自定义输入
*/
export async function analyzeUserInput(params: {
formCommit: number;
userQuestion: string;
chapterContent?: string;
}) {
const res = await request.post<ApiResult<string>>(
MODULES_API_URL + '/ai/auditReport/analyzeUserInput',
null,
{
params
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据取证单生成审计建议
*/
export async function generateAuditSuggestion(params: {
projectId: number;
evidenceIds: number[];
}) {
const res = await request.post<ApiResult<string>>(
MODULES_API_URL + '/ai/auditReport/generateAuditSuggestion',
{ evidenceIds: params.evidenceIds },
{
params: {
projectId: params.projectId
},
headers: {
'Content-Type': 'application/json;charset=UTF-8'
}
}
);
if (res.data.code === 0) {
// 后端返回的数据在 message 字段中
return res.data.message || res.data.data;
}
return Promise.reject(new Error(res.data.message));
}

View File

@@ -10,6 +10,10 @@ export interface PwlProject {
name?: string;
// 项目标识
code?: string;
// 针对用户名称
personName?: string;
// 职务
position?: string;
// 案引号
caseIndex?: string;
// 上级id, 0是顶级

View File

@@ -1,11 +1,11 @@
<template>
<a-modal
:visible="visible"
title="编辑行数据"
@ok="handleOk"
@cancel="handleCancel"
:confirm-loading="loading"
width="600px"
:visible="visible"
title="编辑行数据"
@ok="handleOk"
@cancel="handleCancel"
:confirm-loading="loading"
width="600px"
>
<template #title>
<div class="modal-title">
@@ -17,23 +17,23 @@
</template>
<a-alert
v-if="displayRecords.length > 1"
type="info"
show-icon
style="margin-bottom: 12px"
:message="`已选择 ${displayRecords.length} 条记录,将把当前修改同步到这些记录`"
v-if="displayRecords.length > 1"
type="info"
show-icon
style="margin-bottom: 12px"
:message="`已选择 ${displayRecords.length} 条记录,将把当前修改同步到这些记录`"
/>
<a-list
v-if="displayRecords.length > 1"
size="small"
bordered
:data-source="displayRecords"
style="margin-bottom: 12px; max-height: 160px; overflow-y: auto"
v-if="displayRecords.length > 1"
size="small"
bordered
:data-source="displayRecords"
style="margin-bottom: 12px; max-height: 160px; overflow-y: auto"
>
<template #renderItem="{ item, index }">
<a-list-item
:class="['record-item', { active: index === selectedRecordIndex }]"
@click="selectRecord(index)"
:class="['record-item', { active: index === selectedRecordIndex }]"
@click="selectRecord(index)"
>
<span class="record-label">#{{ index + 1 }}</span>
</a-list-item>
@@ -45,22 +45,23 @@
<a-form-item :label="field.title" v-if="!field.children">
<template v-if="field.type === 'textarea'">
<a-textarea
v-model:value="activeFormData[field.dataIndex]"
:rows="4"
:placeholder="`请输入${field.title}`"
v-model:value="activeFormData[field.dataIndex]"
:rows="4"
:placeholder="`请输入${field.title}`"
/>
</template>
<template v-else-if="field.dataIndex === 'workPaperIndex'">
<a-textarea
v-model:value="activeFormData[field.dataIndex]"
:rows="4"
:placeholder="'每行一个文件格式为file_id||文件名||url'"
v-model:value="activeFormData[field.dataIndex]"
:rows="4"
:placeholder="'每行一个文件格式为file_id||文件名||url'"
/>
</template>
<template v-else>
<a-input
v-model:value="activeFormData[field.dataIndex]"
:placeholder="`请输入${field.title}`"
<a-textarea
:rows="4"
v-model:value="activeFormData[field.dataIndex]"
:placeholder="`请输入${field.title}`"
/>
</template>
</a-form-item>
@@ -71,14 +72,14 @@
<div class="field-group-title">{{ field.title }}</div>
<div class="field-group-content">
<a-form-item
v-for="childField in field.children"
:key="childField.key"
:label="childField.title"
class="nested-field-item"
v-for="childField in field.children"
:key="childField.key"
:label="childField.title"
class="nested-field-item"
>
<a-input
v-model:value="activeFormData[childField.dataIndex]"
:placeholder="`请输入${childField.title}`"
v-model:value="activeFormData[childField.dataIndex]"
:placeholder="`请输入${childField.title}`"
/>
</a-form-item>
</div>
@@ -90,207 +91,219 @@
</template>
<script lang="ts" setup>
import { ref, watch, computed } from 'vue';
import { message } from 'ant-design-vue';
import { ref, watch, computed } from 'vue';
import { message } from 'ant-design-vue';
const props = defineProps<{
visible: boolean;
record: any;
fields: any[];
records?: any[];
}>();
const props = defineProps<{
visible: boolean;
record: any;
fields: any[];
records?: any[];
}>();
const emit = defineEmits(['update:visible', 'save']);
const emit = defineEmits(['update:visible', 'save']);
const loading = ref(false);
const formDataList = ref<any[]>([]);
const selectedRecordIndex = ref(0);
const loading = ref(false);
const formDataList = ref<any[]>([]);
const selectedRecordIndex = ref(0);
const displayRecords = computed(() => {
if (props.records && Array.isArray(props.records) && props.records.length) {
return props.records;
}
if (props.record) return [props.record];
return [];
});
const transformRecordToFormData = (record: any) => {
if (!record) return {};
const recordCopy = JSON.parse(JSON.stringify(record));
if (
hasWorkPaperIndexField.value &&
recordCopy.workPaperIndex &&
Array.isArray(recordCopy.workPaperIndex)
) {
recordCopy.workPaperIndex = recordCopy.workPaperIndex
.map((item: any) => {
if (typeof item === 'object') {
return `${item.fileId || ''}||${item.fileName || ''}||${item.fileUrl || ''}`;
}
return item;
})
.join('\n');
}
return recordCopy;
};
const hasWorkPaperIndexField = computed(() => {
return (props.fields || []).some((field) => {
return field?.dataIndex === 'workPaperIndex' || field?.key === 'workPaperIndex';
const displayRecords = computed(() => {
if (props.records && Array.isArray(props.records) && props.records.length) {
return props.records;
}
if (props.record) return [props.record];
return [];
});
});
// 处理字段,将嵌套结构展平
const processedFields = computed(() => {
const processed: any[] = [];
const transformRecordToFormData = (record: any) => {
if (!record) return {};
const recordCopy = JSON.parse(JSON.stringify(record));
(props.fields || []).forEach(field => {
if (field.children && Array.isArray(field.children)) {
// 处理有子字段的情况(如职务)
processed.push({
...field,
children: field.children.flatMap(child =>
child.children && Array.isArray(child.children)
if (
hasWorkPaperIndexField.value &&
recordCopy.workPaperIndex &&
Array.isArray(recordCopy.workPaperIndex)
) {
recordCopy.workPaperIndex = recordCopy.workPaperIndex
.map((item: any) => {
if (typeof item === 'object') {
return `${item.fileId || ''}||${item.fileName || ''}||${
item.fileUrl || ''
}`;
}
return item;
})
.join('\n');
}
return recordCopy;
};
const hasWorkPaperIndexField = computed(() => {
return (props.fields || []).some((field) => {
return (
field?.dataIndex === 'workPaperIndex' || field?.key === 'workPaperIndex'
);
});
});
// 处理字段,将嵌套结构展平
const processedFields = computed(() => {
const processed: any[] = [];
(props.fields || []).forEach((field) => {
if (field.children && Array.isArray(field.children)) {
// 处理有子字段的情况(如职务)
processed.push({
...field,
children: field.children.flatMap(
(child) =>
child.children && Array.isArray(child.children)
? child.children // 如果是多层嵌套,直接取孙子字段
: child // 否则就是子字段
)
});
} else {
processed.push(field);
}
)
});
} else {
processed.push(field);
}
});
return processed;
});
return processed;
});
watch(
() => props.visible,
(visible) => {
if (visible) {
selectedRecordIndex.value = 0;
formDataList.value = displayRecords.value.map((rec) =>
transformRecordToFormData(rec)
);
}
},
{ immediate: true }
);
watch(
() => props.visible,
(visible) => {
if (visible) {
selectedRecordIndex.value = 0;
formDataList.value = displayRecords.value.map((rec) =>
transformRecordToFormData(rec)
const handleOk = () => {
if (!formDataList.value || formDataList.value.length === 0) {
message.warning('没有数据可保存');
return;
}
// 处理workPaperIndex将字符串转换回对象数组
const dataToSave = formDataList.value.map((item) => {
const cloned = JSON.parse(JSON.stringify(item || {}));
if (
hasWorkPaperIndexField.value &&
cloned.workPaperIndex &&
typeof cloned.workPaperIndex === 'string'
) {
const lines = cloned.workPaperIndex
.split('\n')
.filter((line: string) => line.trim() !== '');
cloned.workPaperIndex = lines.map((line: string) => {
const parts = line.split('||');
if (parts.length >= 3) {
return {
fileId: parts[0] || '',
fileName: parts[1] || '',
fileUrl: parts[2] || ''
};
}
return line;
});
}
return cloned;
});
loading.value = true;
emit('save', dataToSave);
loading.value = false;
emit('update:visible', false);
};
const handleCancel = () => {
emit('update:visible', false);
};
const selectRecord = (index: number) => {
if (index < 0 || index >= displayRecords.value.length) return;
selectedRecordIndex.value = index;
if (!formDataList.value[index]) {
formDataList.value[index] = transformRecordToFormData(
displayRecords.value[index]
);
}
},
{ immediate: true }
);
};
const handleOk = () => {
if (!formDataList.value || formDataList.value.length === 0) {
message.warning('没有数据可保存');
return;
}
// 处理workPaperIndex将字符串转换回对象数组
const dataToSave = formDataList.value.map((item) => {
const cloned = JSON.parse(JSON.stringify(item || {}));
if (hasWorkPaperIndexField.value && cloned.workPaperIndex && typeof cloned.workPaperIndex === 'string') {
const lines = cloned.workPaperIndex.split('\n').filter((line: string) => line.trim() !== '');
cloned.workPaperIndex = lines.map((line: string) => {
const parts = line.split('||');
if (parts.length >= 3) {
return {
fileId: parts[0] || '',
fileName: parts[1] || '',
fileUrl: parts[2] || ''
};
}
return line;
});
const activeFormData = computed({
get() {
if (!displayRecords.value.length) return {};
if (!formDataList.value[selectedRecordIndex.value]) {
formDataList.value[selectedRecordIndex.value] =
transformRecordToFormData(
displayRecords.value[selectedRecordIndex.value]
);
}
return formDataList.value[selectedRecordIndex.value] || {};
},
set(val) {
if (!displayRecords.value.length) return;
formDataList.value[selectedRecordIndex.value] = val || {};
}
return cloned;
});
loading.value = true;
emit('save', dataToSave);
loading.value = false;
emit('update:visible', false);
};
const handleCancel = () => {
emit('update:visible', false);
};
const selectRecord = (index: number) => {
if (index < 0 || index >= displayRecords.value.length) return;
selectedRecordIndex.value = index;
if (!formDataList.value[index]) {
formDataList.value[index] = transformRecordToFormData(
displayRecords.value[index]
);
}
};
const activeFormData = computed({
get() {
if (!displayRecords.value.length) return {};
if (!formDataList.value[selectedRecordIndex.value]) {
formDataList.value[selectedRecordIndex.value] = transformRecordToFormData(
displayRecords.value[selectedRecordIndex.value]
);
}
return formDataList.value[selectedRecordIndex.value] || {};
},
set(val) {
if (!displayRecords.value.length) return;
formDataList.value[selectedRecordIndex.value] = val || {};
}
});
</script>
<style scoped>
.nested-fields {
margin-bottom: 16px;
border: 1px solid #f0f0f0;
border-radius: 4px;
padding: 12px;
background-color: #fafafa;
}
.nested-fields {
margin-bottom: 16px;
border: 1px solid #f0f0f0;
border-radius: 4px;
padding: 12px;
background-color: #fafafa;
}
.field-group-title {
font-weight: 600;
margin-bottom: 8px;
color: #1890ff;
}
.field-group-title {
font-weight: 600;
margin-bottom: 8px;
color: #1890ff;
}
.field-group-content {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 12px;
}
.field-group-content {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 12px;
}
.nested-field-item {
margin-bottom: 0;
}
.nested-field-item {
margin-bottom: 0;
}
.modal-title {
display: flex;
align-items: center;
}
.modal-title {
display: flex;
align-items: center;
}
.record-label {
display: inline-block;
width: 36px;
color: #888;
}
.record-label {
display: inline-block;
width: 36px;
color: #888;
}
.record-text {
color: #333;
}
.record-text {
color: #333;
}
.record-item {
cursor: pointer;
transition: background-color 0.2s;
}
.record-item {
cursor: pointer;
transition: background-color 0.2s;
}
.record-item:hover {
background-color: #f5f5f5;
}
.record-item:hover {
background-color: #f5f5f5;
}
.record-item.active {
background-color: #e6f7ff;
}
.record-item.active {
background-color: #e6f7ff;
}
</style>

View File

@@ -11,9 +11,11 @@
<a-space>
<a-button @click="resetFields">重置内容</a-button>
<a-button type="primary" @click="handleExport" :loading="exporting">
导出Word文档
导出 Word 文档
</a-button>
<a-button @click="handleSave" :loading="saving">
保存到数据库
</a-button>
<a-button v-if="false" @click="printEvidence">打印预览</a-button>
</a-space>
<div class="action-tip"
>可直接在表格中编辑导出即可生成与效果图一致的取证单</div
@@ -70,11 +72,22 @@
<tr>
<th>审计调查事项</th>
<td>
<textarea
v-model="form.auditMatter"
class="cell-input single"
placeholder="填写审计(调查)事项"
></textarea>
<a-select style="min-width: 600px"
v-model:value="form.auditMatter"
placeholder="请选择审计(调查)事项"
>
<a-select-option
v-for="(item, index) in auditMatterOptions"
:key="index"
:value="item"
>{{ item }}</a-select-option
>
</a-select>
<!-- <textarea-->
<!-- v-model="form.auditMatter"-->
<!-- class="cell-input single"-->
<!-- placeholder="填写审计(调查)事项"-->
<!-- ></textarea>-->
</td>
</tr>
@@ -253,15 +266,19 @@
<script lang="ts" setup>
import { PropType, reactive, ref, watch } from 'vue';
import { message } from 'ant-design-vue';
import { message, Modal } from 'ant-design-vue';
import { PwlProject } from '@/api/pwl/pwlProject/model';
import { downloadAuditEvidence } from '@/api/ai/auditContent';
import {
downloadAuditEvidence,
saveAuditEvidence
} from '@/api/ai/auditContent';
type BaseInfo = {
caseIndex?: string;
projectName?: string;
auditedTarget?: string;
auditMatter?: string;
auditMatterType?: string; // 审计事项类型
};
const props = defineProps({
@@ -283,12 +300,24 @@
}
});
const auditMatterOptions = [
'贯彻执行党和国家经济方针政策、决策部署情况',
'企业发展战略规划的制定、执行和效果情况',
'重大经济事项的决策、执行和效果情况',
'企业法人治理结构的建立、健全和运行情况,内部控制制度的制定和执行情况',
'企业财务的真实合法效益情况,风险管控情况,境外资产管理情况,生态环境保护情况',
'在经济活动中落实有关党风廉政建设责任和遵守廉洁从业规定情况',
'以往审计发现问题的整改情况',
'其他需要审计的内容'
];
const emit = defineEmits<{
(e: 'update:visible', value: boolean): void;
}>();
const printArea = ref<HTMLElement | null>(null);
const exporting = ref(false);
const saving = ref(false);
const defaultForm = () => ({
caseIndex: '',
@@ -297,6 +326,7 @@
projectName: '',
auditedTarget: '',
auditMatter: '',
auditMatterType: '', // 审计事项类型编码code
summaryTitle: '',
auditRecord: '',
auditFinding: '',
@@ -334,21 +364,51 @@
};
const applyBaseInfo = () => {
console.log('applyBaseInfo called, selectedRows:', props.selectedRows);
console.log('baseInfo:', props.baseInfo);
console.log('===== applyBaseInfo 开始执行 =====');
console.log('1. selectedRows:', props.selectedRows);
console.log('2. baseInfo:', props.baseInfo);
console.log(
'3. baseInfo.auditMatterType:',
props.baseInfo?.auditMatterType
);
// 重置表单为默认值
Object.assign(form, defaultForm(), props.baseInfo || {});
console.log(
'4. Object.assign 后的 form.auditMatterType:',
form.auditMatterType
);
// 特殊处理:确保 auditMatterType 被正确复制Object.assign 可能会覆盖)
if (props.baseInfo?.auditMatterType) {
form.auditMatterType = props.baseInfo.auditMatterType;
console.log(
'✓ 5. 显式设置后的 form.auditMatterType:',
form.auditMatterType
);
} else {
console.warn('✗ 5. auditMatterType 为空或未设置,无法复制');
}
console.log('===== applyBaseInfo 执行完毕 =====');
// 如果有传入的selectedRows直接使用第一个对象的数据生成取证单时通常只有一个
if (props.selectedRows && props.selectedRows.length > 0) {
const evidenceData = props.selectedRows[0] || {};
console.log('Evidence data from selectedRows:', evidenceData);
// console.log('Evidence data from selectedRows:', evidenceData);
// 直接将后端返回的数据映射到表单字段
form.caseIndex =
props.baseInfo?.caseIndex || props.project?.caseIndex || form.caseIndex || '';
form.projectName = props.project?.code || evidenceData.projectName || form.projectName || '';
props.baseInfo?.caseIndex ||
props.project?.caseIndex ||
form.caseIndex ||
'';
form.projectName =
props.project?.code ||
evidenceData.projectName ||
form.projectName ||
'';
form.auditedTarget =
evidenceData.auditedTarget || form.auditedTarget || '';
form.auditMatter = evidenceData.auditMatter || form.auditMatter || '';
@@ -402,10 +462,10 @@
if (!form.pageIndex) form.pageIndex = '1';
if (!form.pageTotal) form.pageTotal = '1';
console.log(
'Form data after applyBaseInfo:',
JSON.stringify(form, null, 2)
);
// console.log(
// 'Form data after applyBaseInfo:',
// JSON.stringify(form, null, 2)
// );
};
watch(
@@ -419,7 +479,11 @@
watch(
() => props.baseInfo,
() => {
(newVal) => {
console.log(
'watch baseInfo triggered, newVal.auditMatterType:',
newVal?.auditMatterType
);
if (props.visible) {
applyBaseInfo();
}
@@ -456,6 +520,74 @@
message.success('内容已重置');
};
/**
* 保存审计取证单到数据库
*/
const handleSave = async () => {
// 弹出确认对话框
Modal.confirm({
title: '确认保存',
content: '是否确认将当前取证单数据保存到数据库?',
okText: '是',
cancelText: '否',
onOk: async () => {
try {
saving.value = true;
// 准备保存数据
const saveData = {
caseIndex: form.caseIndex,
projectId: props.project?.id,
projectName: form.projectName,
auditedTarget: form.auditedTarget,
contentType: 1, // 默认值,可根据实际情况调整
auditMatterType: form.auditMatterType, // 从 form 中获取(必须是 code
auditMatter: form.auditMatter,
summaryTitle: form.summaryTitle,
auditRecord: form.auditRecord,
auditFinding: form.auditFinding,
evidenceBasis: form.evidenceBasis,
handling: form.handling,
suggestion: form.suggestion,
attachment: form.attachment,
auditors: form.auditors,
compileDate: form.compileDate,
providerOpinion: form.providerOpinion,
providerDate: form.providerDate,
attachmentPages: form.attachmentPages,
feedbackDeadline: form.feedbackDeadline,
history: '' // 历史内容,如果需要的话
};
console.log('=== 保存取证单 ===');
console.log('form:', form);
console.log('form.auditMatterType:', form.auditMatterType);
console.log('saveData.auditMatterType:', saveData.auditMatterType);
// 调用保存接口
const result = await saveAuditEvidence(saveData);
if (result.code === 0) {
message.success('取证单保存成功');
// 可以选择关闭弹窗或者清空表单
// emit('update:visible', false);
} else {
message.error('取证单保存失败:' + (result.message || '未知错误'));
}
} catch (error: any) {
console.error('保存失败:', error);
message.error('取证单保存失败:' + (error.message || '未知错误'));
} finally {
saving.value = false;
}
},
onCancel() {
// 用户选择"否",不做任何操作
console.log('用户取消保存');
}
});
};
/**
* 导出Word文档
*/
@@ -494,7 +626,9 @@
link.href = url;
// 设置文件名
const fileName = `审计取证单_${form.projectName || '取证单'}_${form.caseIndex || ''}.docx`;
const fileName = `审计取证单_${form.projectName || '取证单'}_${
form.caseIndex || ''
}.docx`;
link.setAttribute('download', fileName);
// 触发下载

View File

@@ -80,11 +80,12 @@ const props = defineProps<{
visible: boolean;
interfaceName?: string;
projectId?: number;
chapter?: any; // 当前章节对象
}>();
const emit = defineEmits<{
(e: 'update:visible', visible: boolean): void;
(e: 'select', record: any): void;
(e: 'select', record: any, chapter?: any): void;
}>();
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
@@ -99,7 +100,11 @@ const datasource: DatasourceFunction = async ({ page, limit, orders }) => {
if (orders) {
Object.assign(params, orders);
}
console.log(props,'props');
console.log('===== HistoryModal Props =====', props);
console.log('interfaceName:', props.interfaceName);
console.log('projectId:', props.projectId);
// 使用传入的接口名称进行过滤
if (props.interfaceName) {
params.interfaceName = props.interfaceName;
@@ -107,6 +112,8 @@ const datasource: DatasourceFunction = async ({ page, limit, orders }) => {
if (props.projectId) {
params.projectId = props.projectId;
}
console.log('查询参数 params:', params);
try {
const result = await pageAiHistory(params);
@@ -116,14 +123,23 @@ const datasource: DatasourceFunction = async ({ page, limit, orders }) => {
let processingTime = '';
try {
// responseData 直接就是 AI 生成的文本内容,不是 JSON 对象
// 这里只是为了统计展示信息,不影响主要内容显示
if (record.responseData) {
const responseData = JSON.parse(record.responseData);
if (responseData.data && Array.isArray(responseData.data)) {
dataCount = responseData.data.length;
} else if (responseData.data?.data && Array.isArray(responseData.data.data)) {
dataCount = responseData.data.data.length;
// 尝试解析是否为 JSON 格式(兼容旧数据)
try {
const parsed = JSON.parse(record.responseData);
if (parsed.data && Array.isArray(parsed.data)) {
dataCount = parsed.data.length;
} else if (parsed.data?.data && Array.isArray(parsed.data.data)) {
dataCount = parsed.data.data.length;
}
processingTime = parsed.processing_time || parsed.generated_time || '';
} catch (e) {
// 如果不是 JSON说明是纯文本不需要统计这些数据
dataCount = 0;
processingTime = '';
}
processingTime = responseData.processing_time || responseData.generated_time || '';
}
} catch (error) {
console.warn('解析响应数据失败:', error);
@@ -258,6 +274,14 @@ const getRequestDataPreview = (record: any) => {
const hasValidData = (record: any) => {
try {
if (record.responseData) {
// responseData 可能是纯文本AI 生成的审计报告内容),也可能是 JSON 对象
// 对于纯文本,只要有内容就认为有效
if (typeof record.responseData === 'string') {
// 如果是字符串,检查是否非空
return record.responseData.trim().length > 0;
}
// 如果是对象,尝试解析 JSON
const responseData = JSON.parse(record.responseData);
const hasData = (responseData.data && Array.isArray(responseData.data) && responseData.data.length > 0) ||
(responseData.data?.data && Array.isArray(responseData.data.data) && responseData.data.data.length > 0);
@@ -265,13 +289,15 @@ const hasValidData = (record: any) => {
}
} catch (error) {
console.warn('检查数据有效性失败:', error);
// 如果解析失败,但有 responseData 字符串,也认为有效(说明是纯文本)
return false;
}
return false;
};
const handleSelect = (record: any) => {
if (!hasValidData(record)) return;
emit('select', record);
emit('select', record, props.chapter);
};
</script>

View File

@@ -20,6 +20,8 @@
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
"
>
<!-- 项目信息标题置顶 -->
<a-divider orientation="left">项目信息</a-divider>
<a-form-item label="被审计单位" name="name">
<SelectCompany v-model:value="form.name" @done="onCompany" />
</a-form-item>
@@ -49,6 +51,22 @@
v-model:value="form.code"
/>
</a-form-item>
<a-form-item label="针对用户名称" name="personName">
<a-input
allow-clear
style="width: 400px"
placeholder="请输入针对用户名称"
v-model:value="form.personName"
/>
</a-form-item>
<a-form-item label="职务" name="position">
<a-input
allow-clear
style="width: 400px"
placeholder="请输入职务"
v-model:value="form.position"
/>
</a-form-item>
<a-form-item label="案引号" name="caseIndex">
<a-input
allow-clear
@@ -85,13 +103,12 @@
:options="pubLibList"
></a-select>
</a-form-item>
<a-divider orientation="left" style="margin-top: 50px">项目信息</a-divider>
<a-form-item label="开票单位/汇款人" name="itemName">
<a-input
allow-clear
style="width: 400px"
placeholder="请输入项目信息-开票单位/汇款人"
placeholder="请输入项目信息 - 开票单位/汇款人"
v-model:value="form.itemName"
/>
</a-form-item>
@@ -378,6 +395,8 @@ const form = reactive<PwlProject>({
price: undefined,
recommend: undefined,
expirationTime: undefined,
personName: undefined,
position: undefined,
itemName: undefined,
itemYear: undefined,
itemType: undefined,

File diff suppressed because it is too large Load Diff

View File

@@ -513,7 +513,7 @@
</a-drawer>
</template>
<script lang="ts" setup>
import { ref, reactive, watch, onMounted, onUnmounted, computed } from 'vue';
import { ref, reactive, watch, onMounted, onUnmounted, computed, nextTick } from 'vue';
import { Form, message, Modal, TableProps } from 'ant-design-vue';
import { assignObject } from 'ele-admin-pro';
import { copyText } from '@/utils/common';
@@ -608,7 +608,8 @@
caseIndex: '',
projectName: '',
auditedTarget: '',
auditMatter: ''
auditMatter: '',
auditMatterType: '' // 审计事项类型编码code
});
const evidenceSelectedRows = ref<any[]>([]);
@@ -750,25 +751,63 @@
fileModal.value.open(tableInfo.tableKey);
};
/**
* 根据表格类型获取审计事项类型编码
*/
const getAuditMatterTypeByTable = (tableValue: string): string => {
// 根据 AuditMatterTypeEnum 枚举映射
const mapping: Record<string, string> = {
'eightReg': 'eightReg', // 八项规定
'expense': 'expense', // 决策支出表
'leaderList': 'leaderList', // 领导班子名单
'strategyAudit': 'strategyAudit', // 单位发展战略执行
'decisionTable': 'decisionTable', // 重大经济决策调查表
'tripleOne': 'tripleOne', // 三重一大
'target': 'target', // 目标责任制完成情况表
'budgetExecution': 'budgetExecution', // 预算执行情况审计
'budgetManage': 'budgetManage', // 预算管理审计
'assets': 'assets', // 国有资产管理审计
'investmentSituation': 'investmentSituation', // 重大投资情况
'internalControl': 'internalControl', // 内部控制测试表
'personnel': 'personnel', // 人员编制管理审计
'partyConduct': 'partyConduct', // 党风廉政建设责任制审计
'history': 'history' // 历史审计问题整改
};
return mapping[tableValue] || tableValue;
};
/* 打开取证单预览弹窗 */
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 || '';
// 设置审计事项类型(根据当前表格类型)- 使用 code 而不是 value
const auditMatterTypeCode = getAuditMatterTypeByTable(tableInfo?.currentTable?.value);
console.log('openEvidenceModal - tableValue:', tableInfo?.currentTable?.value);
console.log('openEvidenceModal - auditMatterTypeCode:', auditMatterTypeCode);
// 逐个字段赋值确保响应式更新
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 || '';
evidenceBaseInfo.auditMatter = tableTitle
? `${baseMatter ? `${baseMatter} - ` : ''}${tableTitle}`
: baseMatter;
evidenceBaseInfo.auditMatterType = auditMatterTypeCode; // 关键:设置审计事项类型编码
console.log('openEvidenceModal - evidenceBaseInfo:', evidenceBaseInfo);
console.log('openEvidenceModal - evidenceBaseInfo.auditMatterType:', evidenceBaseInfo.auditMatterType);
evidenceSelectedRows.value = selectedRowsMap[sectionIndex] || [];
evidenceModalVisible.value = true;
// 关键:使用 nextTick 确保响应式更新完成后再打开弹窗
nextTick(() => {
console.log('nextTick - evidenceBaseInfo.auditMatterType:', evidenceBaseInfo.auditMatterType);
evidenceModalVisible.value = true;
});
};
/* 保存表格生成数据 */
@@ -1060,6 +1099,12 @@
section?.title ||
section?.name ||
'';
// 关键:设置审计事项类型编码(根据当前表格类型)
const auditMatterTypeCode = getAuditMatterTypeByTable(currentTable.value);
console.log('generateEvidence - tableValue:', currentTable.value);
console.log('generateEvidence - auditMatterTypeCode:', auditMatterTypeCode);
evidenceBaseInfo.auditMatterType = auditMatterTypeCode;
// 将生成的取证单数据作为选中的行数据传入,包含所有字段
const evidenceData = {
@@ -1097,7 +1142,12 @@
};
evidenceSelectedRows.value = [evidenceData];
evidenceModalVisible.value = true;
// 关键:使用 nextTick 确保响应式更新完成后再打开弹窗
nextTick(() => {
console.log('generateEvidence nextTick - evidenceBaseInfo.auditMatterType:', evidenceBaseInfo.auditMatterType);
evidenceModalVisible.value = true;
});
} else {
throw new Error(
apiResult.data?.error || apiResult.message || '生成取证单失败'

View File

@@ -0,0 +1,583 @@
<template>
<!-- 生成审核报告弹窗 -->
<a-drawer
:width="`90%`"
:visible="visible"
:confirm-loading="loading"
:maxable="true"
:title="`生成审核报告 - ${form.name || ''}`"
:body-style="{ paddingBottom: '8px', background: '#f5f5f5' }"
@update:visible="updateVisible"
:maskClosable="false"
:footer="null"
>
<div class="report-generate-container">
<!-- 顶部操作区 -->
<div class="action-bar">
<div class="title">审核报告生成</div>
<div class="buttons">
<a-space>
<a-button @click="reloadData" :loading="reloading" :disabled="!projectId" class="action-button">
<template #icon><ReloadOutlined /></template>
重载数据
</a-button>
<a-button type="primary" @click="generatePreview" :loading="generating" :disabled="!hasAnySelection" class="action-button">
<template #icon><FileTextOutlined /></template>
生成报告预览
</a-button>
<a-button type="primary" @click="saveReport" :loading="saving" :disabled="!previewContent" class="action-button">
<template #icon><SaveOutlined /></template>
保存入库
</a-button>
</a-space>
</div>
</div>
<!-- 基本信息 -->
<a-card title="基本信息" style="margin-bottom: 16px" :bordered="false">
<a-descriptions>
<a-descriptions-item label="被审计单位" :labelStyle="{ width: '100px' }">
{{ form.name }}
</a-descriptions-item>
<a-descriptions-item label="项目名称" :labelStyle="{ width: '100px' }">
{{ form.code }}
</a-descriptions-item>
<a-descriptions-item label="案引号" :labelStyle="{ width: '100px' }">
{{ form.caseIndex || '-' }}
</a-descriptions-item>
</a-descriptions>
</a-card>
<!-- 步骤选择取证单记录 -->
<a-card title="一、选择取证单记录(按审核内容)" style="margin-bottom: 16px" :bordered="false">
<a-collapse v-model:activeKey="activeSections">
<a-collapse-panel
v-for="(section, index) in sectionList"
:key="String(index)"
:header="`${section.number}、${section.name}${section.title ? ' - ' + section.title : ''}`"
>
<template #extra>
<a-tag color="blue">{{ getSectionSelectionCount(index) }} 条已选</a-tag>
</template>
<div v-if="section.records.length === 0" class="empty-tip">
暂无已保存的取证单记录请先在审计内容中生成并保存对应表格
</div>
<a-table
v-else
:dataSource="section.records"
:columns="evidenceColumns"
:row-selection="getRowSelection(index)"
:pagination="false"
rowKey="__rowKey"
size="small"
bordered
>
<template #bodyCell="{ column, record, index: rowIndex }">
<template v-if="column.dataIndex === '_index'">
{{ rowIndex + 1 }}
</template>
<template v-else-if="column.dataIndex === '_summary'">
<span class="summary-cell">{{ getRecordSummary(record) }}</span>
</template>
</template>
</a-table>
</a-collapse-panel>
</a-collapse>
</a-card>
<!-- 步骤报告预览 -->
<a-card title="二、审计报告预览" style="margin-bottom: 16px" :bordered="false">
<div v-if="!previewContent" class="empty-preview">
请先选择取证单记录并点击生成报告预览
</div>
<div v-else class="preview-content" v-html="previewContent"></div>
</a-card>
</div>
</a-drawer>
</template>
<script lang="ts" setup>
import { ref, reactive, watch, computed } from 'vue';
import { message, Modal } from 'ant-design-vue';
import type { TableProps } from 'ant-design-vue';
import { FileTextOutlined, SaveOutlined, ReloadOutlined } from '@ant-design/icons-vue';
import type { PwlProject } from '@/api/pwl/pwlProject/model';
import { addAiHistory } from '@/api/ai/aiHistory';
import { tableConfigs } from './data/tableCommon';
import { saveAuditReport, queryAuditReport } from '@/api/ai/auditReport';
const AUDIT_REPORT_INTERFACE = 'auditReport';
const props = defineProps<{
visible: boolean;
data?: PwlProject | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
const form = reactive<PwlProject>({
id: undefined,
name: undefined,
code: undefined,
caseIndex: undefined
});
const loading = ref(false);
const generating = ref(false);
const saving = ref(false);
const reloading = ref(false);
const activeSections = ref<string[]>([]);
const projectId = ref<number | undefined>(undefined);
// 审核内容列表(与 reportContent 的 navigationItems 对应)
const sectionList = ref<Array<{
number: string;
name: string;
title?: string;
tableType: string;
records: any[];
}>>([]);
// 每个 section 选中的行: sectionIndex -> rowKey[]
const selectedRowKeysMap = reactive<Record<number, (string | number)[]>>({});
// 报告预览 HTML
const previewContent = ref('');
// 历史记录可能带 responseData部分后端实现会返回
const evidenceColumns = [
{ title: '序号', dataIndex: '_index', key: '_index', width: 60 },
{ title: '内容摘要', dataIndex: '_summary', key: '_summary', ellipsis: true }
];
const getChineseNumber = (num: number) => {
const arr = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '十一'];
return arr[num - 1] || String(num);
};
function getSectionListFromConfig() {
const keys = Object.keys(tableConfigs);
return keys.map((key, index) => {
const config = (tableConfigs as any)[key];
return {
index: index+1,
number: getChineseNumber(index + 1),
name: `审计内容${index + 1}`,
title: config?.title,
tableType: key,
records: [] as any[]
};
});
}
function getRecordSummary(record: any): string {
if (!record || typeof record !== 'object') return '-';
const parts: string[] = [];
const prefer = ['auditFinding', 'auditRecord', 'problemDescription', 'summaryTitle', 'title', 'content', 'auditContent', 'checkEvidence'];
for (const p of prefer) {
if (record[p] && String(record[p]).trim()) {
const s = String(record[p]).trim();
parts.push(s.length > 80 ? s.slice(0, 80) + '...' : s);
break;
}
}
if (parts.length === 0) {
const firstStr = Object.values(record).find(v => typeof v === 'string' && v.length > 0);
if (firstStr) parts.push(String(firstStr).slice(0, 60) + (String(firstStr).length > 60 ? '...' : ''));
}
return parts.length > 0 ? parts.join(' ') : JSON.stringify(record).slice(0, 80) + '...';
}
function getRowSelection(sectionIndex: number): TableProps['rowSelection'] {
return {
selectedRowKeys: selectedRowKeysMap[sectionIndex] || [],
onChange: (keys: (string | number)[]) => {
selectedRowKeysMap[sectionIndex] = keys;
}
};
}
const getSectionSelectionCount = (sectionIndex: number) => {
return (selectedRowKeysMap[sectionIndex] || []).length;
};
const hasAnySelection = computed(() => {
return sectionList.value.some((_, index) => (selectedRowKeysMap[index] || []).length > 0);
});
async function loadSectionDataFromEvidence() {
const projectIdVal = props.data?.id;
const caseIndex = props.data?.caseIndex;
if (!projectIdVal && !caseIndex) {
message.warning('无项目信息');
return;
}
try {
const res: any = await pageAuditEvidence({
projectId: projectIdVal,
caseIndex,
page: 1,
limit: 1000
} as any);
const data = res?.data;
const allRows: any[] = (() => {
if (!data) return [];
if (Array.isArray(data)) return data;
if (Array.isArray(data.records)) return data.records;
if (Array.isArray(data.rows)) return data.rows;
if (Array.isArray(data.list)) return data.list;
return [];
})();
const sections = getSectionListFromConfig();
const sectionRecordsMap: Record<number, any[]> = {};
sections.forEach((_, i) => {
sectionRecordsMap[i] = [];
});
allRows.forEach((row: any, idx: number) => {
const contentType =
row.contentType ?? row.content_type ?? row.contentNo ?? row.contentIndex;
const sectionIndex =
typeof contentType === 'number' ? contentType - 1 : -1;
if (sectionIndex < 0 || sectionIndex >= sections.length) {
return;
}
const rowKey = `${row.id ?? ''}-${idx}`;
sectionRecordsMap[sectionIndex].push({
...row,
__rowKey: rowKey
});
});
sectionList.value = sections.map((s, i) => ({
...s,
records: sectionRecordsMap[i] || []
}));
activeSections.value = sectionList.value.map((_, i) => String(i));
} catch (e: any) {
message.error('加载失败:' + (e?.message || '未知错误'));
}
}
/**
* 从数据库加载审计报告
*/
async function loadAuditReportFromDB(formCommit: number = 1) {
const projectIdVal = props.data?.id;
if (!projectIdVal) {
message.warning('无项目信息');
return null;
}
// 保存 projectId 供重载按钮使用
projectId.value = projectIdVal;
try {
const res: any = await queryAuditReport({
projectId: projectIdVal,
formCommit
});
if (res.data) {
// 解析报告内容
const reportContent = JSON.parse(res.data.reportContent || '{}');
previewContent.value = res.data.previewHtml || '';
// 恢复选中的行
if (reportContent.sections && Array.isArray(reportContent.sections)) {
reportContent.sections.forEach((section: any, sectionIndex: number) => {
if (section.records && Array.isArray(section.records)) {
// 找到对应的记录并选中
const currentSection = sectionList.value[sectionIndex];
if (currentSection) {
const selectedKeys: (string | number)[] = [];
section.records.forEach((rec: any) => {
const foundRecord = currentSection.records.find(
r => r.id === rec.id || r.summaryTitle === rec.summaryTitle
);
if (foundRecord) {
selectedKeys.push(foundRecord.__rowKey);
}
});
selectedRowKeysMap[sectionIndex] = selectedKeys;
}
}
});
}
message.success('已从数据库加载审计报告');
return res.data;
} else {
message.info('数据库中暂无审计报告记录');
return null;
}
} catch (e: any) {
console.error('加载审计报告失败:', e);
// 不显示错误,静默失败
return null;
}
}
/**
* 重载数据
*/
async function reloadData() {
const projectIdVal = props.data?.id;
if (!projectIdVal) {
message.warning('无项目信息');
return;
}
reloading.value = true;
try {
// 先清空当前内容
previewContent.value = '';
Object.keys(selectedRowKeysMap).forEach((k) => {
selectedRowKeysMap[Number(k)] = [];
});
// 从数据库重新加载
await loadAuditReportFromDB(1);
if (previewContent.value) {
message.success('数据已重新加载');
} else {
message.info('数据库中无相关记录,页面无变化');
}
} catch (e: any) {
message.error('重载失败:' + (e?.message || '未知错误'));
} finally {
reloading.value = false;
}
}
function buildPreviewFromSelections(): string {
const parts: string[] = [];
parts.push('<div class="report-preview">');
parts.push(`<h2>审计报告</h2>`);
parts.push(`<p><strong>被审计单位:</strong>${escapeHtml(form.name || '')}</p>`);
parts.push(`<p><strong>项目名称:</strong>${escapeHtml(form.code || '')}</p>`);
parts.push(`<p><strong>案引号:</strong>${escapeHtml(form.caseIndex || '')}</p>`);
parts.push('<hr/>');
sectionList.value.forEach((section, sectionIndex) => {
const keys = selectedRowKeysMap[sectionIndex] || [];
if (keys.length === 0) return;
const records = section.records.filter((r: any) => keys.includes(r.__rowKey));
parts.push(`<h3>${section.number}${section.name}${section.title ? ' - ' + section.title : ''}</h3>`);
parts.push('<table class="preview-table"><thead><tr><th>序号</th><th>内容摘要</th></tr></thead><tbody>');
records.forEach((rec: any, i: number) => {
const summary = getRecordSummary(rec);
parts.push(`<tr><td>${i + 1}</td><td>${escapeHtml(summary)}</td></tr>`);
});
parts.push('</tbody></table>');
});
parts.push('</div>');
return parts.join('');
}
function escapeHtml(s: string): string {
const div = document.createElement('div');
div.textContent = s;
return div.innerHTML;
}
function generatePreview() {
generating.value = true;
try {
// AI 生成前清空当前内容
previewContent.value = '';
previewContent.value = buildPreviewFromSelections();
message.success('报告预览已生成');
} finally {
generating.value = false;
}
}
function getReportPayload(): { sections: any[]; previewHtml: string } {
const sections = sectionList.value.map((section, sectionIndex) => {
const keys = selectedRowKeysMap[sectionIndex] || [];
const records = section.records.filter((r: any) => keys.includes(r.__rowKey));
return {
number: section.number,
name: section.name,
title: section.title,
tableType: section.tableType,
records: records.map((r: any) => {
const { __rowKey, __historyId, __interfaceName, ...rest } = r;
return rest;
})
};
});
return {
sections,
previewHtml: previewContent.value
};
}
async function saveReport() {
if (!previewContent.value) {
message.warning('请先生成报告预览');
return;
}
const projectId = props.data?.id;
if (!projectId) {
message.warning('无项目信息');
return;
}
// 弹出确认对话框
Modal.confirm({
title: '确认保存',
content: '是否确认将当前审计报告保存到数据库?',
okText: '是',
cancelText: '否',
onOk: async () => {
saving.value = true;
try {
const payload = getReportPayload();
// 调用新的保存接口
await saveAuditReport({
projectId,
projectName: form.name || '',
caseIndex: form.caseIndex || '',
auditedTarget: form.name || '',
reportContent: JSON.stringify(payload),
previewHtml: previewContent.value,
sectionCount: payload.sections.length,
formCommit: 1 // 表单提交标识,可根据实际需求调整
});
message.success('报告已保存入库');
emit('done');
} catch (e: any) {
message.error('保存失败:' + (e?.message || '未知错误'));
} finally {
saving.value = false;
}
},
onCancel() {
console.log('用户取消保存');
}
});
}
function updateVisible(value: boolean) {
emit('update:visible', value);
}
function reset() {
sectionList.value = getSectionListFromConfig();
Object.keys(selectedRowKeysMap).forEach((k) => delete selectedRowKeysMap[Number(k)]);
previewContent.value = '';
activeSections.value = [];
}
watch(
() => props.visible,
async (visible) => {
if (visible && props.data) {
Object.assign(form, props.data);
reset();
await loadSectionDataFromEvidence();
// 自动从数据库加载审计报告
await loadAuditReportFromDB(1);
}
}
);
watch(
() => props.data,
(data) => {
if (data) Object.assign(form, data);
}
);
</script>
<style lang="less" scoped>
.report-generate-container {
.action-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
background: white;
padding: 16px;
border-radius: 4px;
.title {
font-size: 16px;
font-weight: bold;
color: #1890ff;
}
.action-button {
border-radius: 20px;
}
}
.empty-tip {
color: #999;
padding: 16px;
text-align: center;
}
.summary-cell {
display: inline-block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.empty-preview {
color: #999;
padding: 24px;
text-align: center;
border: 1px dashed #ddd;
border-radius: 4px;
}
.preview-content {
padding: 16px;
background: #fff;
border: 1px solid #e8e8e8;
border-radius: 4px;
max-height: 500px;
overflow-y: auto;
}
}
:deep(.report-preview) {
.preview-table {
width: 100%;
border-collapse: collapse;
margin: 12px 0;
th,
td {
border: 1px solid #e8e8e8;
padding: 8px 12px;
text-align: left;
}
th {
background: #fafafa;
}
}
hr {
margin: 16px 0;
border: none;
border-top: 1px solid #e8e8e8;
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -99,11 +99,11 @@
<div>
<a-space>
<a class="action-btn bg-blue-500" @click="openReport(record)"
>1生成审计方案</a
>1 生成审计方案</a
>
<a class="action-btn bg-green-600" @click="openReportContent(record)">2审计内容</a>
<a class="action-btn bg-red-600" @click="openAuditCheck(record)"
>3审计核查</a
<a class="action-btn bg-green-600" @click="openReportContent(record)">2 审计内容</a>
<a class="action-btn bg-red-600" @click="openReportView(record)"
>3 审计报告</a
>
</a-space>
</div>
@@ -128,6 +128,8 @@
<!-- 生成报告 -->
<Report v-model:visible="showReport" :data="current" @done="reload" />
<ReportContent v-model:visible="showReportContent" :data="current" @done="reload" />
<!-- 审计报告查看 -->
<ReportView v-model:visible="showReportView" :data="current" @done="reload" />
<!-- 审计核查弹窗 -->
<AuditCheck
v-model:visible="showAuditCheck"
@@ -202,6 +204,8 @@
import Edit from './components/pwlProjectEdit.vue';
import Report from './components/report.vue';
import ReportContent from './components/reportContent.vue';
import ReportView from './components/reportView.vue';
import AuditCheck from './components/auditCheck.vue';
import {
pagePwlProject,
removePwlProject,
@@ -215,7 +219,6 @@
getKnowledgeBaseDocuments,
deleteKnowledgeBaseDocument
} from '@/api/ai/knowledgeBase';
import AuditCheck from './components/auditCheck.vue';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
@@ -228,6 +231,10 @@
const showEdit = ref(false);
// 是否显示报告弹窗
const showReport = ref(false);
// 是否显示审计内容弹窗
const showReportContent = ref(false);
// 是否显示审计报告查看弹窗
const showReportView = ref(false);
// 是否显示审计核查弹窗
const showAuditCheck = ref(false);
// 是否显示批量移动弹窗
@@ -273,6 +280,12 @@
}
]);
// 打开审计报告查看弹窗
const openReportView = (record: PwlProject) => {
current.value = record;
showReportView.value = true;
};
// 打开审计核查弹窗
const openAuditCheck = (record: PwlProject) => {
current.value = record;
@@ -610,7 +623,6 @@
showReport.value = true;
};
const showReportContent = ref(false)
const openReportContent = (row?: PwlProject) => {
current.value = row ?? null;
showReportContent.value = true;

View File

@@ -661,7 +661,7 @@
"@esbuild/linux-loong64@0.14.54":
version "0.14.54"
resolved "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz#de2a4be678bd4d0d1ffbb86e6de779cde5999028"
resolved "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz"
integrity sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==
"@eslint/eslintrc@^1.3.1":
@@ -2652,17 +2652,17 @@ es6-symbol@^3.1.1, es6-symbol@^3.1.3:
esbuild-android-64@0.14.54:
version "0.14.54"
resolved "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz#505f41832884313bbaffb27704b8bcaa2d8616be"
resolved "https://registry.npmmirror.com/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz"
integrity sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==
esbuild-android-arm64@0.14.54:
version "0.14.54"
resolved "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.54.tgz#8ce69d7caba49646e009968fe5754a21a9871771"
resolved "https://registry.npmmirror.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.54.tgz"
integrity sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==
esbuild-darwin-64@0.14.54:
version "0.14.54"
resolved "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.54.tgz#24ba67b9a8cb890a3c08d9018f887cc221cdda25"
resolved "https://registry.npmmirror.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.54.tgz"
integrity sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug==
esbuild-darwin-arm64@0.14.54:
@@ -2672,82 +2672,82 @@ esbuild-darwin-arm64@0.14.54:
esbuild-freebsd-64@0.14.54:
version "0.14.54"
resolved "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.54.tgz#09250f997a56ed4650f3e1979c905ffc40bbe94d"
resolved "https://registry.npmmirror.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.54.tgz"
integrity sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==
esbuild-freebsd-arm64@0.14.54:
version "0.14.54"
resolved "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.54.tgz#bafb46ed04fc5f97cbdb016d86947a79579f8e48"
resolved "https://registry.npmmirror.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.54.tgz"
integrity sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==
esbuild-linux-32@0.14.54:
version "0.14.54"
resolved "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.54.tgz#e2a8c4a8efdc355405325033fcebeb941f781fe5"
resolved "https://registry.npmmirror.com/esbuild-linux-32/-/esbuild-linux-32-0.14.54.tgz"
integrity sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==
esbuild-linux-64@0.14.54:
version "0.14.54"
resolved "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz#de5fdba1c95666cf72369f52b40b03be71226652"
resolved "https://registry.npmmirror.com/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz"
integrity sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==
esbuild-linux-arm64@0.14.54:
version "0.14.54"
resolved "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.54.tgz#dae4cd42ae9787468b6a5c158da4c84e83b0ce8b"
resolved "https://registry.npmmirror.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.54.tgz"
integrity sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==
esbuild-linux-arm@0.14.54:
version "0.14.54"
resolved "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.54.tgz#a2c1dff6d0f21dbe8fc6998a122675533ddfcd59"
resolved "https://registry.npmmirror.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.54.tgz"
integrity sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==
esbuild-linux-mips64le@0.14.54:
version "0.14.54"
resolved "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.54.tgz#d9918e9e4cb972f8d6dae8e8655bf9ee131eda34"
resolved "https://registry.npmmirror.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.54.tgz"
integrity sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==
esbuild-linux-ppc64le@0.14.54:
version "0.14.54"
resolved "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.54.tgz#3f9a0f6d41073fb1a640680845c7de52995f137e"
resolved "https://registry.npmmirror.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.54.tgz"
integrity sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==
esbuild-linux-riscv64@0.14.54:
version "0.14.54"
resolved "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.54.tgz#618853c028178a61837bc799d2013d4695e451c8"
resolved "https://registry.npmmirror.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.54.tgz"
integrity sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==
esbuild-linux-s390x@0.14.54:
version "0.14.54"
resolved "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.54.tgz#d1885c4c5a76bbb5a0fe182e2c8c60eb9e29f2a6"
resolved "https://registry.npmmirror.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.54.tgz"
integrity sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==
esbuild-netbsd-64@0.14.54:
version "0.14.54"
resolved "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz#69ae917a2ff241b7df1dbf22baf04bd330349e81"
resolved "https://registry.npmmirror.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz"
integrity sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==
esbuild-openbsd-64@0.14.54:
version "0.14.54"
resolved "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.54.tgz#db4c8495287a350a6790de22edea247a57c5d47b"
resolved "https://registry.npmmirror.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.54.tgz"
integrity sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==
esbuild-sunos-64@0.14.54:
version "0.14.54"
resolved "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz#54287ee3da73d3844b721c21bc80c1dc7e1bf7da"
resolved "https://registry.npmmirror.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz"
integrity sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==
esbuild-windows-32@0.14.54:
version "0.14.54"
resolved "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.54.tgz#f8aaf9a5667630b40f0fb3aa37bf01bbd340ce31"
resolved "https://registry.npmmirror.com/esbuild-windows-32/-/esbuild-windows-32-0.14.54.tgz"
integrity sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==
esbuild-windows-64@0.14.54:
version "0.14.54"
resolved "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.54.tgz#bf54b51bd3e9b0f1886ffdb224a4176031ea0af4"
resolved "https://registry.npmmirror.com/esbuild-windows-64/-/esbuild-windows-64-0.14.54.tgz"
integrity sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==
esbuild-windows-arm64@0.14.54:
version "0.14.54"
resolved "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz#937d15675a15e4b0e4fafdbaa3a01a776a2be982"
resolved "https://registry.npmmirror.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz"
integrity sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==
esbuild@^0.14.47:
@@ -5665,16 +5665,7 @@ stream-wormhole@^1.0.4:
resolved "https://registry.npmmirror.com/stream-wormhole/-/stream-wormhole-1.1.0.tgz"
integrity sha512-gHFfL3px0Kctd6Po0M8TzEvt3De/xu6cnRrjlfYNhwbhLPLwigI2t1nc6jrzNuaYg5C4YF78PPFuQPzRiqn9ew==
"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^4.1.0:
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0:
version "4.2.3"
resolved "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -5742,7 +5733,7 @@ stringify-entities@^4.0.2:
character-entities-html4 "^2.0.0"
character-entities-legacy "^3.0.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -5756,13 +5747,6 @@ strip-ansi@^3.0.0:
dependencies:
ansi-regex "^2.0.0"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^7.0.1:
version "7.1.0"
resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.1.0.tgz"