Files
jzsj-vue/src/views/pwl/pwlProject/components/report.vue

1857 lines
55 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- User编辑弹窗 -->
<template>
<a-drawer
:width="`70%`"
:visible="visible"
:confirm-loading="loading"
:maxable="maxAble"
title="AI审计方案生成器"
:body-style="{ paddingBottom: '8px', background: '#f3f3f3' }"
@update:visible="updateVisible"
:maskClosable="false"
:footer="null"
@ok="save"
>
<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-tooltip title="并行生成所有 9 个章节的内容(包含各小节)">
<a-button size="large" type="primary" @click="handleGenerateAll" :loading="generatingAll" class="generate-all-button">
<template #icon>
<UngroupOutlined/>
</template>
生成全部方案
</a-button>
</a-tooltip>
<a-tooltip title="依次保存所有章节到数据库(每个章节独立保存)">
<a-button size="large" @click="saveAllDrafts">
<template #icon>
<DownloadOutlined/>
</template>
保存草稿
</a-button>
</a-tooltip>
<a-tooltip title="从数据库加载所有已保存的章节内容">
<a-button size="large" @click="loadDrafts">
<template #icon>
<RedoOutlined/>
</template>
加载草稿
</a-button>
</a-tooltip>
<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
:title="`${item.number}、${item.name}`"
style="margin-bottom: 20px"
:bordered="false"
>
<template #extra>
<a-space>
<a-button
type="primary"
size="small"
@click="reloadSectionData(index)"
:loading="sectionReloading[index]"
class="section-action-button reload-button"
>
<template #icon>
<ReloadOutlined />
</template>
重载数据
</a-button>
<a-button
type="primary"
size="small"
@click="saveSectionContent(index)"
:loading="sectionSaving[index]"
class="section-action-button save-button"
>
<template #icon>
<SaveOutlined />
</template>
保存
</a-button>
<a-button
type="primary"
size="small"
@click="generateContent(index)"
:loading="item.generating"
class="section-action-button ai-generate-button"
>
<template #icon>
<RobotOutlined/>
</template>
AI 生成
</a-button>
</a-space>
</template>
<!-- <div v-if="item.description" class="section-description">-->
<!-- {{ item.description }}-->
<!-- </div>-->
<!-- 单内容部分 -->
<template v-if="!item.children">
<a-textarea
v-model:value="item.content"
:rows="item.rows || 8"
:placeholder="'点击(AI生成)按钮让AI为您生成该部分内容或直接填入'"
class="content-textarea"
style="margin-top: 16px; background-color: #f0fdf4"
/>
<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>
</template>
<!-- 多内容部分 -->
<template v-else>
<div v-for="(child, childIndex) in item.children" :key="childIndex" class="child-section" style="border-left: 3px solid #1890ff; padding-left: 16px; margin-bottom: 20px;">
<div class="child-title" style="display: flex; align-items: center; justify-content: space-between;">
<span style="font-weight: bold;">{{ child.name }}</span>
<a-space>
<a-button
type="primary"
size="small"
@click="saveChildContent(index, childIndex)"
:loading="child.saving"
class="child-action-button save-button"
>
<template #icon>
<SaveOutlined />
</template>
保存
</a-button>
<a-button
type="default"
size="small"
@click="reloadChildContent(index, childIndex)"
:loading="child.reloading"
class="child-action-button reload-button"
>
<template #icon>
<ReloadOutlined />
</template>
重载
</a-button>
<a-button
type="primary"
size="small"
@click.stop="generateContent(index, childIndex)"
:loading="child.generating"
class="child-action-button ai-generate-button"
>
<template #icon>
<RobotOutlined/>
</template>
AI 生成
</a-button>
</a-space>
</div>
<a-textarea
v-model:value="child.content"
:rows="child.rows || 6"
:placeholder="child.placeholder"
class="content-textarea"
style="margin-top: 12px; background-color: #f0fdf4"
/>
<!-- 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="child.suggestion"
:rows="3"
placeholder="请输入优化提示词并回车..."
class="suggestion-textarea-inner"
@pressEnter="generateContent(index, childIndex)"
/>
<a-button
type="danger"
size="small"
@click="generateContent(index, childIndex)"
:loading="child.generating"
class="send-button-inner"
>
<template #icon>
<RobotOutlined/>
</template>
发送
</a-button>
</div>
</div>
</div>
</template>
</a-card>
</div>
</div>
<!-- 返回顶部按钮 -->
<a-back-top :target="getScrollContainer"/>
</a-drawer>
</template>
<script lang="ts" setup>
import {ref, reactive, watch, onMounted, onUnmounted} from 'vue';
import {Form, message} from 'ant-design-vue';
import {assignObject} from 'ele-admin-pro';
import {copyText} from '@/utils/common';
import {PwlProject} from "@/api/pwl/pwlProject/model";
import {
UngroupOutlined,
DownloadOutlined,
UploadOutlined,
RedoOutlined,
RobotOutlined,
QuestionCircleOutlined,
ReloadOutlined,
SaveOutlined
} from '@ant-design/icons-vue';
import {generateAuditReport, downloadAuditReport, queryAuditReport, saveAuditReport} from "@/api/ai/auditReport";
import {getPwlProjectLibraryByIds} from '@/api/pwl/pwlProjectLibrary';
const useForm = Form.useForm;
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: PwlProject | null;
}>();
// 是否显示最大化切换按钮
const maxAble = ref(true);
// 添加生成状态
const generatingAll = ref(false);
// 当前选中的章节
const currentSection = ref(0);
// 每个章节的重载状态
const sectionReloading = reactive<Record<number, boolean>>({});
// 每个章节的保存状态
const sectionSaving = reactive<Record<number, boolean>>({});
// 存储拼接后的 kbIds
const combinedKbIds = ref('');
// 九大审计项目导航配置
const navigationItems = ref([
{
number: '一',
name: '审计依据',
title: '1、审计依据',
description: '点击"AI生成"按钮让AI为您生成该部分内容或直接在下方编辑',
placeholder: '请输入审计依据内容...',
content: '',
suggestion: '',
formCommit: 10,
rows: 10,
generating: false
},
{
number: '二',
name: '审计目标',
title: '2、审计目标',
description: '点击"AI生成"按钮让AI为您生成该部分内容或直接在下方编辑',
placeholder: '请输入审计目标内容...',
content: '',
suggestion: '',
formCommit: 20,
rows: 6,
generating: false
},
{
number: '三',
name: '审计对象和范围',
title: '3、审计对象和范围',
description: '点击"AI生成"按钮让AI为您生成该部分内容或直接在下方编辑',
placeholder: '请输入审计对象和范围内容...',
content: '',
suggestion: '',
formCommit: 30,
rows: 8,
generating: false
},
{
number: '四',
name: '被审计单位基本情况',
title: '4、被审计单位基本情况',
description: '点击"AI生成"按钮让AI为您生成该部分内容或直接在下方编辑',
generating: false,
children: [
{
name: '(一)单位概况',
placeholder: '请输入单位概况内容...',
content: '',
suggestion: '',
formCommit: 41,
rows: 6,
generating: false
},
{
name: '(二)机构和人员相关情况',
placeholder: '请输入机构和人员相关情况...',
content: '',
suggestion: '',
formCommit: 42,
rows: 6,
generating: false
},
{
name: '(三)财务会计、重大会计政策选用及变动情况',
placeholder: '请输入财务会计、重大会计政策选用及变动情况...',
content: '',
suggestion: '',
formCommit: 43,
rows: 6,
generating: false
},
{
name: '(四)审计期间合并口径年度资产负债及各项业务总体情况',
placeholder: '请输入审计期间合并口径年度资产负债及各项业务总体情况...',
content: '',
suggestion: '',
formCommit: 44,
rows: 6,
generating: false
},
{
name: '(五)相关内部控制',
placeholder: '请输入相关内部控制内容...',
content: '',
suggestion: '',
formCommit: 45,
rows: 6,
generating: false
}
]
},
{
number: '五',
name: '审计内容和重点及审计方法',
title: '5、审计内容和重点及审计方法',
description: '点击"AI生成"按钮让AI为您生成该部分内容或直接在下方编辑',
generating: false,
children: [
{
name: '(一)贯彻执行党和国家有关经济方针和上级决策部署情况',
placeholder: '请输入贯彻执行党和国家有关经济方针和上级决策部署情况...',
content: '',
suggestion: '',
formCommit: 51,
rows: 6,
generating: false,
saving: false,
reloading: false
},
{
name: '(二)公司发展战略规划的制定、执行和效果情况以及年度责任目标完成情况',
placeholder: '请输入公司发展战略规划的制定、执行和效果情况以及年度责任目标完成情况...',
content: '',
suggestion: '',
formCommit: 52,
rows: 6,
generating: false,
saving: false,
reloading: false
},
{
name: '(三)重大经济事项的决策、执行和效果情况',
placeholder: '请输入重大经济事项的决策、执行和效果情况...',
content: '',
suggestion: '',
formCommit: 53,
rows: 6,
generating: false,
saving: false,
reloading: false
},
{
name: '(四)公司法人治理结构的建立、健全和运行情况,内部控制制度的制定和执行情况',
placeholder: '请输入公司法人治理结构的建立、健全和运行情况,内部控制制度的制定和执行情况...',
content: '',
suggestion: '',
formCommit: 54,
rows: 6,
generating: false,
saving: false,
reloading: false
},
{
name: '(五)公司财务的真实合法效益情况,风险管控情况,境外资产管理情况',
placeholder: '请输入公司财务的真实合法效益情况,风险管控情况,境外资产管理情况...',
content: '',
suggestion: '',
formCommit: 55,
rows: 6,
generating: false,
saving: false,
reloading: false
},
{
name: '(六)在经济活动中落实有关党风廉政建设责任和遵守廉洁从业规定情况',
placeholder: '请输入在经济活动中落实有关党风廉政建设责任和遵守廉洁从业规定情况...',
content: '',
suggestion: '',
formCommit: 56,
rows: 6,
generating: false,
saving: false,
reloading: false
},
{
name: '(七)对以往审计中发现问题的整改情况',
placeholder: '请输入对以往审计中发现问题的整改情况...',
content: '',
suggestion: '',
formCommit: 57,
rows: 6,
generating: false,
saving: false,
reloading: false
},
{
name: '(八)其他需要审计的事项',
placeholder: '请输入其他需要审计的事项...',
content: '',
suggestion: '',
formCommit: 58,
rows: 6,
generating: false,
saving: false,
reloading: false
}
]
},
{
number: '六',
name: '重要风险的识别及应对',
title: '6、重要风险的识别及应对',
description: '点击"AI 生成"按钮让 AI 为您生成该部分内容,或直接在下方编辑',
generating: false,
children: [
{
name: '(一)重要风险的识别',
placeholder: '请输入重要风险的识别内容...',
content: '',
suggestion: '',
formCommit: 61,
rows: 6,
generating: false,
saving: false,
reloading: false
},
{
name: '(二)风险的应对策略',
placeholder: '请输入风险的应对策略内容...',
content: '',
suggestion: '',
formCommit: 62,
rows: 6,
generating: false,
saving: false,
reloading: false
}
]
},
{
number: '七',
name: '审计技术方法',
title: '7、审计技术方法',
description: '点击"AI生成"按钮让AI为您生成该部分内容或直接在下方编辑',
placeholder: '请输入审计技术方法内容...',
content: '',
suggestion: '',
formCommit: 70,
rows: 8,
generating: false
},
{
number: '八',
name: '工作步骤与时间安排',
title: '8、工作步骤与时间安排',
description: '点击"AI生成"按钮让AI为您生成该部分内容或直接在下方编辑',
placeholder: '请输入工作步骤与时间安排内容...',
content: '',
suggestion: '',
formCommit: 80,
rows: 10,
generating: false
},
{
number: '九',
name: '审计工作的组织实施',
title: '9、审计工作的组织实施',
description: '点击"AI生成"按钮让AI为您生成该部分内容或直接在下方编辑',
placeholder: '请输入审计工作的组织实施内容...',
content: '',
suggestion: '',
formCommit: 90,
rows: 8,
generating: false
}
]);
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 订单信息
const form = reactive<PwlProject>({
// ID
id: undefined,
// 项目名称
name: undefined,
// 项目标识
code: undefined,
// 上级id, 0是顶级
parentId: undefined,
// 项目类型
type: undefined,
// 项目图标
image: undefined,
// 二维码
qrcode: undefined,
// 链接地址
url: undefined,
// 应用截图
images: undefined,
// 底稿情况
files: undefined,
// 应用介绍
content: undefined,
// 年末资产总额(万元)
totalAssets: undefined,
// 合同金额
contractPrice: undefined,
// 实收金额
payPrice: undefined,
// 软件定价
price: undefined,
// 是否推荐
recommend: undefined,
// 到期时间
expirationTime: undefined,
// 项目信息-开票单位/汇款人
itemName: undefined,
// 项目信息-年度
itemYear: undefined,
// 项目信息-类型
itemType: undefined,
// 项目信息-审计意见
itemOpinion: undefined,
// 到账信息-银行名称
bankName: undefined,
// 到账日期
bankPayTime: undefined,
// 到账金额
bankPrice: undefined,
// 发票类型
invoiceType: undefined,
// 开票日期
invoiceTime: undefined,
// 开票金额
invoicePrice: undefined,
// 报告份数
reportNum: undefined,
// 底稿人员
draftUserId: undefined,
// 底稿人员
draftUser: undefined,
// 参与成员
userIds: undefined,
users: undefined,
// 签字注会
signUserId: undefined,
signUser: undefined,
// 展业人员
saleUserId: undefined,
saleUser: undefined,
// 备注
comments: undefined,
// 排序(数字越小越靠前)
sortNumber: undefined,
// 状态, 0正常, 1冻结
status: undefined,
// 电子报告是否已完成
electron: undefined,
// 纸质报告是否已完成
paper: undefined,
// 是否删除, 0否, 1是
deleted: undefined,
// 创建人ID
userId: undefined,
// 创建人
realName: undefined,
// 租户id
tenantId: undefined,
// 创建时间
createTime: undefined,
// 修改时间
updateTime: undefined,
// 知识库id
kbId: undefined,
});
// 请求状态
const loading = ref(true);
const {resetFields} = useForm(form);
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
/* 滚动到指定章节 */
const scrollToSection = (index: number) => {
currentSection.value = index;
const element = document.getElementById(`section-${index}`);
if (element) {
element.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
};
/* 获取滚动容器 */
const getScrollContainer = () => {
return document.querySelector('.ant-modal-body');
};
/* 批量生成全部内容 - 清空后并行生成 */
const handleGenerateAll = async () => {
generatingAll.value = true;
try {
// 先清空所有内容
navigationItems.value.forEach(item => {
if (item.children) {
item.children.forEach(child => {
child.content = '';
});
} else {
item.content = '';
}
});
// 使用 Promise.all 并行生成所有 9 个方案
const promises: Promise<void>[] = [];
navigationItems.value.forEach((item, index) => {
if (item.children) {
// 有子项的章节,为每个子项生成内容
item.children.forEach((child, childIndex) => {
promises.push(generateContent(index, childIndex));
});
} else {
// 没有子项的章节
promises.push(generateContent(index));
}
});
await Promise.all(promises);
message.success('全部内容生成完成');
} catch (error) {
console.error('批量生成失败:', error);
message.error('部分内容生成失败,请检查');
} finally {
generatingAll.value = false;
}
};
// 新增内容检查方法
const hasContent = (index: number) => {
const section = navigationItems.value[index];
if (!section) return false;
if (section.children) {
return section.children.some(child => child.content?.trim());
}
return !!section.content?.trim();
};
/**
* 保存所有方案到数据库(保存草稿)- 按章节分别保存
*/
const saveAllDrafts = async () => {
if (!form.id) {
message.warning('无项目信息');
return;
}
try {
// 按章节顺序依次保存
let savedCount = 0;
for (const navItem of navigationItems.value) {
// 如果章节有子项,为每个子项单独保存一条记录
if (navItem.children && navItem.children.length > 0) {
// 有子项,为每个小节单独保存
for (const child of navItem.children) {
const currentChildData = {
number: navItem.number,
name: navItem.name,
title: navItem.title,
tableType: 'audit_report',
content: '', // 主章节内容为空
formCommit: navItem.formCommit,
records: [{
name: child.name,
content: child.content || '',
formCommit: child.formCommit
}]
};
// 构建当前小节的预览 HTML
let previewHtml = '<div class="audit-report">';
previewHtml += `<h3>${currentChildData.number}${currentChildData.name}${currentChildData.title ? ' - ' + currentChildData.title : ''}</h3>`;
previewHtml += `<div><strong>${child.name}:</strong> ${child.content || ''}</div>`;
previewHtml += '</div>';
await saveAuditReport({
projectId: form.id,
projectName: form.name || '',
caseIndex: form.caseIndex || '',
auditedTarget: form.name || '',
reportContent: JSON.stringify({
sections: [currentChildData]
}),
previewHtml: previewHtml,
sectionCount: 1,
formCommit: child.formCommit || 1
});
savedCount++;
console.log(`[保存草稿] 已保存:${navItem.number}${navItem.name} - ${child.name}, formCommit: ${child.formCommit}`);
}
} else {
// 无子项,直接保存主章节
const currentSectionData = {
number: navItem.number,
name: navItem.name,
title: navItem.title,
tableType: 'audit_report',
content: navItem.content || '',
formCommit: navItem.formCommit,
records: []
};
// 构建当前章节的预览 HTML
let previewHtml = '<div class="audit-report">';
previewHtml += `<h3>${currentSectionData.number}${currentSectionData.name}${currentSectionData.title ? ' - ' + currentSectionData.title : ''}</h3>`;
if (currentSectionData.content) {
previewHtml += `<div>${currentSectionData.content}</div>`;
}
previewHtml += '</div>';
await saveAuditReport({
projectId: form.id,
projectName: form.name || '',
caseIndex: form.caseIndex || '',
auditedTarget: form.name || '',
reportContent: JSON.stringify({
sections: [currentSectionData]
}),
previewHtml: previewHtml,
sectionCount: 1,
formCommit: navItem.formCommit || 1
});
savedCount++;
console.log(`[保存草稿] 已保存:${navItem.number}${navItem.name}, formCommit: ${navItem.formCommit}`);
}
}
message.success(`草稿已保存到数据库(共 ${savedCount} 条记录)`);
} catch (e: any) {
console.error('[保存草稿] 失败:', e);
message.error('保存草稿失败:' + (e?.message || '未知错误'));
}
};
/**
* 从数据库加载草稿(根据每个方案的 formCommit 并行加载)
*/
const loadDrafts = async () => {
if (!form.id) {
message.warning('无项目信息');
return;
}
try {
console.log('[加载草稿] 开始加载projectId:', form.id);
// 收集所有的 formCommit 值(包括主章节和所有小节)
const formCommits: number[] = [];
navigationItems.value.forEach(item => {
if (item.formCommit) {
// 主章节有 formCommit直接使用
formCommits.push(item.formCommit);
} else if (item.children && item.children.length > 0) {
// 有子项的章节,收集所有小节的 formCommit
item.children.forEach(child => {
if (child.formCommit) {
formCommits.push(child.formCommit);
}
});
}
});
console.log('[加载草稿] 收集到的 formCommits:', formCommits);
// 并行查询所有 formCommit 的数据
const promises = formCommits.map(async (fc) => {
try {
console.log('[加载草稿] 查询 formCommit:', fc);
const res: any = await queryAuditReport({
projectId: form.id,
formCommit: fc
});
return { formCommit: fc, data: res?.data || null };
} catch (e) {
console.warn(`[加载草稿] 查询 formCommit=${fc} 失败:`, e);
return { formCommit: fc, data: null };
}
});
const results = await Promise.all(promises);
console.log('[加载草稿] 查询结果:', results);
// 解析并填充数据
let hasData = false;
results.forEach(result => {
if (result.data) {
hasData = true;
const reportContent = JSON.parse(result.data.reportContent || '{}');
if (reportContent.sections && Array.isArray(reportContent.sections)) {
const section = reportContent.sections[0];
if (section) {
// 找到对应的 navigationItem通过匹配任意一个 formCommit
const navItemIndex = navigationItems.value.findIndex(item =>
item.formCommit === result.formCommit ||
(item.children && item.children.some(child => child.formCommit === result.formCommit))
);
if (navItemIndex >= 0) {
const navItem = navigationItems.value[navItemIndex];
console.log(`[加载草稿] 恢复章节:${navItem.number}${navItem.name}, formCommit: ${result.formCommit}`);
// 恢复内容
if (navItem.children && section.records && Array.isArray(section.records)) {
// 有子项,根据 formCommit 匹配每个小节
section.records.forEach((rec: any) => {
const childIndex = navItem.children.findIndex(child => child.formCommit === rec.formCommit);
if (childIndex >= 0) {
navItem.children[childIndex].content = rec.content || '';
console.log(` - 恢复小节:${navItem.children[childIndex].name}, content length: ${rec.content?.length || 0}`);
}
});
} else if (!navItem.children && section.content) {
// 无子项,直接恢复主章节内容
navItem.content = section.content || '';
}
} else {
console.warn('[加载草稿] 未找到匹配的章节formCommit:', result.formCommit);
}
}
}
}
});
if (hasData) {
message.success('草稿已加载');
} else {
message.info('暂无保存的草稿');
}
} catch (e: any) {
console.error('[加载草稿] 失败:', e);
message.error('加载草稿失败:' + (e?.message || '未知错误'));
}
};
/**
* 保存单个章节的内容到数据库
*/
const saveSectionContent = async (sectionIndex: number) => {
if (!form.id) {
message.warning('无项目信息');
return;
}
const section = navigationItems.value[sectionIndex];
if (!section) return;
sectionSaving[sectionIndex] = true;
try {
// 如果章节有子项,为每个子项单独保存一条记录
if (section.children && section.children.length > 0) {
// 有子项,为每个小节单独保存
let savedChildrenCount = 0;
for (const child of section.children) {
// 检查内容是否为空,为空则跳过不保存
if (!child.content || !child.content.trim()) {
console.log(`[保存整章] 跳过空内容:${section.number}${section.name} - ${child.name}`);
continue;
}
const currentChildData = {
number: section.number,
name: section.name,
title: section.title,
tableType: 'audit_report',
content: '', // 主章节内容为空
formCommit: section.formCommit,
records: [{
name: child.name,
content: child.content || '',
formCommit: child.formCommit
}]
};
// 构建当前小节的预览 HTML
let previewHtml = '<div class="audit-report">';
previewHtml += `<h3>${currentChildData.number}${currentChildData.name}${currentChildData.title ? ' - ' + currentChildData.title : ''}</h3>`;
previewHtml += `<div><strong>${child.name}:</strong> ${child.content || ''}</div>`;
previewHtml += '</div>';
await saveAuditReport({
projectId: form.id,
projectName: form.name || '',
caseIndex: form.caseIndex || '',
auditedTarget: form.name || '',
reportContent: JSON.stringify({
sections: [currentChildData]
}),
previewHtml: previewHtml,
sectionCount: 1,
formCommit: child.formCommit || 1
});
savedChildrenCount++;
console.log(`[保存整章] 已保存:${section.number}${section.name} - ${child.name}, formCommit: ${child.formCommit}`);
}
if (savedChildrenCount === 0) {
message.info('该章节没有需要保存的内容(所有小节均为空)');
} else {
message.success(`${section.number}${section.name} 已保存(共 ${savedChildrenCount}/${section.children.length} 个小节)`);
}
} else {
// 无子项,直接保存主章节
// 检查内容是否为空,为空则跳过不保存
if (!section.content || !section.content.trim()) {
console.log(`[保存整章] 跳过空内容:${section.number}${section.name}`);
message.info('该章节没有需要保存的内容');
sectionSaving[sectionIndex] = false;
return;
}
const currentSectionData = {
number: section.number,
name: section.name,
title: section.title,
tableType: 'audit_report',
content: section.content || '',
formCommit: section.formCommit,
records: []
};
// 只构建当前章节的预览 HTML
let previewHtml = '<div class="audit-report">';
previewHtml += `<h3>${currentSectionData.number}${currentSectionData.name}${currentSectionData.title ? ' - ' + currentSectionData.title : ''}</h3>`;
if (currentSectionData.content) {
previewHtml += `<div>${currentSectionData.content}</div>`;
}
previewHtml += '</div>';
// 调用保存接口(使用当前章节的 formCommit
await saveAuditReport({
projectId: form.id,
projectName: form.name || '',
caseIndex: form.caseIndex || '',
auditedTarget: form.name || '',
reportContent: JSON.stringify({
sections: [currentSectionData] // 只保存当前章节
}),
previewHtml: previewHtml, // 只包含当前章节的 HTML
sectionCount: 1, // 只有 1 个章节
formCommit: section.formCommit || 1
});
message.success(`${section.number}${section.name} 已保存`);
}
} catch (e: any) {
console.error('保存失败:', e);
message.error('保存失败:' + (e?.message || '未知错误'));
} finally {
sectionSaving[sectionIndex] = false;
}
};
/**
* 保存单个小节的内容到数据库
*/
const saveChildContent = async (sectionIndex: number, childIndex: number) => {
if (!form.id) {
message.warning('无项目信息');
return;
}
const section = navigationItems.value[sectionIndex];
if (!section || !section.children || !section.children[childIndex]) return;
const child = section.children[childIndex];
// 检查内容是否为空,为空则不保存
if (!child.content || !child.content.trim()) {
message.info('该小节内容为空,无需保存');
return;
}
child.saving = true;
try {
// 构建只包含当前小节的数据结构
const currentChildData = {
number: section.number,
name: section.name,
title: section.title,
tableType: 'audit_report',
content: '', // 主章节内容为空
formCommit: section.formCommit,
records: [{
name: child.name,
content: child.content || '',
formCommit: child.formCommit
}]
};
// 构建当前小节的预览 HTML
let previewHtml = '<div class="audit-report">';
previewHtml += `<h3>${currentChildData.number}${currentChildData.name}${currentChildData.title ? ' - ' + currentChildData.title : ''}</h3>`;
previewHtml += `<div><strong>${child.name}:</strong> ${child.content || ''}</div>`;
previewHtml += '</div>';
// 调用保存接口(使用当前小节的 formCommit
await saveAuditReport({
projectId: form.id,
projectName: form.name || '',
caseIndex: form.caseIndex || '',
auditedTarget: form.name || '',
reportContent: JSON.stringify({
sections: [currentChildData]
}),
previewHtml: previewHtml,
sectionCount: 1,
formCommit: child.formCommit || 1
});
message.success(`${child.name} 已保存`);
} catch (e: any) {
console.error('保存小节失败:', e);
message.error('保存失败:' + (e?.message || '未知错误'));
} finally {
child.saving = false;
}
};
/**
* 重载单个章节的数据(包括所有子小节)
*/
const reloadSectionData = async (sectionIndex: number) => {
if (!form.id) {
message.warning('无项目信息');
return;
}
const section = navigationItems.value[sectionIndex];
if (!section) return;
sectionReloading[sectionIndex] = true;
try {
// 如果章节有子项,需要查询所有子项的数据
if (section.children && section.children.length > 0) {
console.log(`[重载章节数据] 开始重载有子项的章节:${section.number}${section.name}`);
// 收集所有子项的 formCommit
const childFormCommits = section.children.map(child => child.formCommit).filter(fc => fc);
console.log(`[重载章节数据] 子项 formCommits:`, childFormCommits);
// 并行查询所有子项的数据
const promises = childFormCommits.map(async (fc) => {
try {
const res: any = await queryAuditReport({
projectId: form.id,
formCommit: fc
});
return { formCommit: fc, data: res?.data || null };
} catch (e) {
console.warn(`[重载章节数据] 查询 formCommit=${fc} 失败:`, e);
return { formCommit: fc, data: null };
}
});
const results = await Promise.all(promises);
console.log(`[重载章节数据] 查询结果:`, results);
// 解析并填充每个子项的数据
let loadedCount = 0;
results.forEach(result => {
if (result.data) {
const reportContent = JSON.parse(result.data.reportContent || '{}');
if (reportContent.sections && Array.isArray(reportContent.sections)) {
const sectionData = reportContent.sections[0];
if (sectionData && sectionData.records && Array.isArray(sectionData.records)) {
// 找到对应的子项并填充数据
sectionData.records.forEach((rec: any) => {
const childIndex = section.children.findIndex(child => child.formCommit === rec.formCommit);
if (childIndex >= 0) {
section.children[childIndex].content = rec.content || '';
loadedCount++;
}
});
}
}
}
});
console.log(`[重载章节数据] 已加载 ${loadedCount}/${section.children.length} 个小节的数据`);
if (loadedCount === 0) {
message.info('该章节暂无保存的数据');
} else {
message.success(`${section.number}${section.name} 数据已重新加载(共 ${loadedCount}/${section.children.length} 个小节)`);
}
} else {
// 无子项,直接加载主章节
const formCommitToUse = section.formCommit || 1;
console.log(`[重载章节数据] 开始重载主章节sectionIndex: ${sectionIndex}, formCommit: ${formCommitToUse}`);
const res: any = await queryAuditReport({
projectId: form.id,
formCommit: formCommitToUse
});
if (res.data) {
const reportContent = JSON.parse(res.data.reportContent || '{}');
if (reportContent.sections && Array.isArray(reportContent.sections)) {
const sectionData = reportContent.sections[0];
if (sectionData) {
section.content = sectionData.content || '';
console.log(`[重载章节数据] 已加载主章节内容,长度:${section.content?.length || 0}`);
message.success(`${section.number}${section.name} 数据已重新加载`);
}
}
}
// 没有数据时不提示
}
} catch (e: any) {
console.error('重载章节数据失败:', e);
message.error('重载失败:' + (e?.message || '未知错误'));
} finally {
sectionReloading[sectionIndex] = false;
}
};
/**
* 重载单个小节的数据
*/
const reloadChildContent = async (sectionIndex: number, childIndex: number) => {
if (!form.id) {
message.warning('无项目信息');
return;
}
const section = navigationItems.value[sectionIndex];
if (!section || !section.children || !section.children[childIndex]) return;
const child = section.children[childIndex];
child.reloading = true;
try {
const res: any = await queryAuditReport({
projectId: form.id,
formCommit: child.formCommit
});
if (res.data) {
const reportContent = JSON.parse(res.data.reportContent || '{}');
if (reportContent.sections && Array.isArray(reportContent.sections)) {
const sectionData = reportContent.sections[0]; // 取第一个章节
if (sectionData && sectionData.records && Array.isArray(sectionData.records)) {
// 找到对应的小节记录
const record = sectionData.records.find((r: any) => r.formCommit === child.formCommit);
if (record) {
child.content = record.content || '';
message.success(`${child.name} 数据已重新加载`);
}
}
}
}
// 没有数据时不提示
} catch (e: any) {
console.error('重载小节数据失败:', e);
message.error('重载失败:' + (e?.message || '未知错误'));
} finally {
child.reloading = false;
}
};
/* 新增:构建导出数据的公共方法 */
const buildExportData = () => {
const exportData: any = {
from00: form.code,
from10: navigationItems.value[0].content,
from20: navigationItems.value[1].content,
from30: navigationItems.value[2].content,
};
// 被审计单位基本情况
const basicInfoSection = navigationItems.value[3];
if (basicInfoSection?.children) {
exportData.from41 = basicInfoSection.children[0].content;
exportData.from42 = basicInfoSection.children[1].content;
exportData.from43 = basicInfoSection.children[2].content;
exportData.from44 = [basicInfoSection.children[3].content];
exportData.from45 = basicInfoSection.children[4].content;
}
// 审计内容和重点及审计方法
const auditContentSection = navigationItems.value[4];
if (auditContentSection?.children) {
exportData.from51 = auditContentSection.children[0].content;
exportData.from52 = auditContentSection.children[1].content;
exportData.from53 = auditContentSection.children[2].content;
exportData.from54 = auditContentSection.children[3].content;
exportData.from55 = auditContentSection.children[4].content;
exportData.from56 = auditContentSection.children[5].content;
exportData.from57 = auditContentSection.children[6].content;
exportData.from58 = auditContentSection.children[7].content;
}
// 重要风险的识别及应对
const riskSection = navigationItems.value[5];
if (riskSection?.children) {
exportData.from61 = riskSection.children[0].content;
exportData.from62 = riskSection.children[1].content;
}
// 其他部分
exportData.from70 = navigationItems.value[6].content;
exportData.from80 = navigationItems.value[7].content;
exportData.from90 = navigationItems.value[8].content;
return exportData;
};
/* 修改导出方法使用公共构建方法 */
const handleExport = async () => {
try {
const exportData = buildExportData();
const blob = await downloadAuditReport(exportData);
// 创建下载链接
const url = window.URL.createObjectURL(new Blob([blob]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', `审计报告_${form.name}.docx`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
} catch (error) {
console.error('导出失败:', error);
message.error('文档导出失败');
}
};
/* AI生成内容 */
const generateContent = async (sectionIndex: number, childIndex?: number) => {
const section = navigationItems.value[sectionIndex];
// 处理主项生成当主项没有formCommit时
if (childIndex === undefined && !section.formCommit && section.children) {
section.generating = true;
try {
await Promise.all(section.children.map((_, i) =>
generateContent(sectionIndex, i)
));
} finally {
section.generating = false;
}
return;
}
// 处理单个项生成
let item: any;
let formCommit: number;
const isChild = childIndex !== undefined;
if (isChild) {
item = section.children[childIndex];
formCommit = item.formCommit;
} else {
item = section;
formCommit = item.formCommit;
}
// 新增:构建上下文数据
const contextData = await buildExportData(); // 复用导出数据的构建逻辑
item.generating = true;
try {
const result = await generateAuditReport({
kbId: form.kbId || '',
kbIds: combinedKbIds.value,
formCommit: formCommit,
history: item.content || '',
suggestion: item.suggestion || '',
// 新增:传递完整的上下文数据
...contextData
});
item.content = result;
} finally {
item.generating = false;
}
};
/* 监听滚动位置更新当前章节 */
const handleScroll = () => {
const sections = navigationItems.value.map((_, index) =>
document.getElementById(`section-${index}`)
);
const scrollContainer = getScrollContainer();
if (!scrollContainer) return;
const containerTop = scrollContainer.getBoundingClientRect().top;
for (let i = sections.length - 1; i >= 0; i--) {
const section = sections[i];
if (section) {
const sectionTop = section.getBoundingClientRect().top - containerTop;
if (sectionTop <= 100) { // 100px 的偏移量
currentSection.value = i;
break;
}
}
}
};
/* 键盘快捷键处理 */
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);
}
}
// 上下箭头键导航
if (event.key === 'ArrowUp' && event.ctrlKey) {
event.preventDefault();
const prevIndex = Math.max(0, currentSection.value - 1);
scrollToSection(prevIndex);
} else if (event.key === 'ArrowDown' && event.ctrlKey) {
event.preventDefault();
const nextIndex = Math.min(navigationItems.value.length - 1, currentSection.value + 1);
scrollToSection(nextIndex);
}
};
/* 保存编辑 */
const save = () => {
};
// 组件挂载时添加滚动监听和键盘监听
onMounted(() => {
const scrollContainer = getScrollContainer();
if (scrollContainer) {
scrollContainer.addEventListener('scroll', handleScroll);
}
document.addEventListener('keydown', handleKeydown);
});
// 组件卸载时移除监听
onUnmounted(() => {
const scrollContainer = getScrollContainer();
if (scrollContainer) {
scrollContainer.removeEventListener('scroll', handleScroll);
}
document.removeEventListener('keydown', handleKeydown);
});
watch(
() => props.visible,
async (visible) => {
if (visible) {
if (props.data) {
loading.value = true;
assignObject(form, props.data);
// 获取知识库并拼接 kbIds
if (props.data.libraryIds?.length > 0) {
try {
const result = await getPwlProjectLibraryByIds(props.data.libraryIds);
const kbIds = result.map(lib => lib.kbId).filter(kbId => kbId);
if (form.kbId) {
kbIds.unshift(form.kbId);
}
combinedKbIds.value = kbIds.join(',');
} catch (error) {
console.error('获取知识库失败:', error);
combinedKbIds.value = form.kbId || '';
}
} else {
combinedKbIds.value = form.kbId || '';
}
// 重置到第一个章节
currentSection.value = 0;
// 自动从数据库加载审计报告
await loadAuditReportFromDB();
}
} else {
resetFields();
combinedKbIds.value = ''; // 关闭时清空
}
}
);
</script>
<script lang="ts">
import * as MenuIcons from '@/layout/menu-icons';
export default {
name: 'PwlProjectInfo',
components: MenuIcons
};
</script>
<style lang="less" scoped>
.audit-content {
.audit-section {
.child-title {
font-weight: 600;
font-size: 15px;
color: #333;
margin-bottom: 8px;
padding-left: 8px;
border-left: 3px solid #1890ff;
display: flex;
justify-content: space-between;
align-items: center;
.ant-btn {
margin-left: 0;
}
}
}
}
.question-prompt {
color: #1677ff;
font-weight: 500;
margin-bottom: 8px;
font-size: 14px;
cursor: pointer;
display: flex;
align-items: center;
&::before {
content: '✍️';
margin-right: 6px;
font-size: 16px;
}
&:hover {
color: #0958d9;
}
}
.suggestion-textarea {
&:focus {
border-color: #1677ff !important;
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.1) !important;
}
&:hover {
border-color: #4096ff !important;
}
}
/* textarea内嵌按钮样式 */
.textarea-with-button {
position: relative;
.suggestion-textarea-inner {
padding-right: 70px !important;
border-radius: 6px;
&:focus {
border-color: #1677ff !important;
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.1) !important;
}
&:hover {
border-color: #4096ff !important;
}
}
.send-button-inner {
position: absolute;
right: 8px;
bottom: 8px;
z-index: 10;
padding: 4px 12px;
height: 28px;
font-size: 12px;
&:hover {
transform: none;
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.3);
}
}
}
:deep(.export-button) {
border: 2px solid #ff4d4f !important;
border-radius: 20px !important;
&:hover, &:focus {
border-color: #ff7875 !important;
box-shadow: 0 0 8px rgba(255, 77, 79, 0.3);
}
}
/* 统一设置所有按钮为圆角 */
:deep(.ant-btn) {
border-radius: 20px !important;
transition: all 0.3s ease;
&:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
}
/* 发送按钮特殊样式 */
:deep(.ant-btn-primary) {
border-radius: 20px !important;
&:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
}
}
/* 生成全部方案按钮橙色样式 */
:deep(.generate-all-button) {
background-color: #ff7b00 !important;
border-color: #ff7b00 !important;
&:hover {
background-color: #e56500 !important;
border-color: #e56500 !important;
box-shadow: 0 4px 12px rgba(255, 123, 0, 0.4) !important;
}
&:focus {
background-color: #e56500 !important;
border-color: #e56500 !important;
box-shadow: 0 4px 12px rgba(255, 123, 0, 0.4) !important;
}
}
/* 章节操作按钮统一样式 */
.section-action-button {
border-radius: 20px !important;
transition: all 0.3s ease;
&:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
}
/* 重载数据按钮 - 蓝色 */
:deep(.reload-button) {
background-color: #1890ff !important;
border-color: #1890ff !important;
&:hover {
background-color: #40a9ff !important;
border-color: #40a9ff !important;
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.4) !important;
}
}
/* 保存按钮 - 绿色 */
:deep(.save-button) {
background-color: #52c41a !important;
border-color: #52c41a !important;
&:hover {
background-color: #73d13d !important;
border-color: #73d13d !important;
box-shadow: 0 4px 12px rgba(82, 196, 26, 0.4) !important;
}
}
/* AI 生成按钮 - 紫色 */
:deep(.ai-generate-button) {
background-color: #722ed1 !important;
border-color: #722ed1 !important;
&:hover {
background-color: #9254de !important;
border-color: #9254de !important;
box-shadow: 0 4px 12px rgba(114, 46, 209, 0.4) !important;
}
}
.navigation-container {
.nav-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
margin-bottom: 20px;
}
.nav-button {
height: 48px;
border-radius: 20px !important;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: flex-start;
padding: 0 16px;
text-align: left;
.nav-number {
font-weight: bold;
margin-right: 8px;
min-width: 20px;
}
.nav-text {
flex: 1;
font-size: 14px;
}
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
&.active {
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
}
}
.progress-container {
display: flex;
align-items: center;
gap: 12px;
.progress-bar {
flex: 1;
height: 6px;
background: #f0f0f0;
border-radius: 3px;
overflow: hidden;
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #1890ff, #52c41a);
border-radius: 3px;
transition: width 0.3s ease;
}
}
.progress-text {
font-size: 12px;
color: #666;
min-width: 40px;
text-align: center;
}
}
}
.audit-content {
.audit-section {
scroll-margin-top: 20px;
.section-description {
color: #999999;
font-size: 14px;
margin-bottom: 8px;
padding: 12px;
background: #f8f9fa;
border-radius: 6px;
}
.child-section {
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px dashed #e8e8e8;
&:last-child {
border-bottom: none;
margin-bottom: 0;
}
}
.child-title {
font-weight: 600;
font-size: 15px;
color: #333;
margin-bottom: 8px;
padding-left: 8px;
border-left: 3px solid #1890ff;
}
.content-textarea {
border-radius: 6px;
transition: all 0.3s ease;
&:focus {
border-color: #1890ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
}
}
}
:deep(.ant-card-head) {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
border-radius: 6px 6px 0 0;
}
:deep(.ant-card-body) {
padding: 20px;
}
:deep(.ant-back-top) {
right: 30px;
bottom: 30px;
}
</style>