feat(pwl): 新增生成报告功能

- 添加生成报告弹窗组件
- 实现报告内容的 AI 生成逻辑
- 优化用户界面,增加导航和快捷键功能
This commit is contained in:
2025-09-17 15:29:15 +08:00
parent 8551d1bdb6
commit 4c334ee4da
2 changed files with 771 additions and 3 deletions

View File

@@ -0,0 +1,758 @@
<!-- 用户编辑弹窗 -->
<template>
<a-drawer
:width="`80%`"
: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-button type="primary" size="large">
<template #icon>
<UngroupOutlined/>
</template>
生成全部方案
</a-button>
<a-button size="large">
<template #icon>
<DownloadOutlined/>
</template>
保存草稿
</a-button>
<a-button size="large">
<template #icon>
<RedoOutlined/>
</template>
加载草稿
</a-button>
<a-button size="large">
<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-button
type="text"
size="small"
@click="generateContent(index)"
:loading="item.generating"
>
<template #icon>
<RobotOutlined/>
</template>
AI生成
</a-button>
</template>
<div class="section-description">
{{ item.description }}
</div>
<a-textarea
v-model:value="item.content"
:rows="item.rows || 8"
:placeholder="item.placeholder"
class="content-textarea"
style="margin-top: 16px"
/>
</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} 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
} from '@ant-design/icons-vue';
const useForm = Form.useForm;
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: PwlProject | null;
}>();
// 是否显示最大化切换按钮
const maxAble = ref(true);
// 当前选中的章节
const currentSection = ref(0);
// 九大审计项目导航配置
const navigationItems = ref([
{
number: '一',
name: '审计依据',
title: '1、审计依据',
description: '点击"AI生成"按钮让AI为您生成该部分内容或直接在下方编辑',
placeholder: '请输入审计依据内容...',
content: `中共中央办公厅、国务院办公厅《党政主要领导干部和国有企业领导人员经济责任审计规定》2019年7月7日起实施
XXX有限公司经济责任审计管理办法》XX****号(待补充));
(三)国家有关法律、法规及相关文件;
XXX有限公司下达的年度工作目标
XXX有限公司各项管理制度
(六)双方签订的审计业务约定书。`,
rows: 10,
generating: false
},
{
number: '二',
name: '审计目标',
title: '2、审计目标',
description: '点击"AI生成"按钮让AI为您生成该部分内容或直接在下方编辑',
placeholder: '请输入审计目标内容...',
content: `通过对XXX同志、XXX同志任职期间所负责公司资产、负债情况的真实性、合法性和效益性及其相关经济活动进行审计查清其任职期间公司财经政策指导和财务收支工作目标完成情况重大决策执行及交接情况遵守国家财经法规情况及公司资产保值增值情况分清其经济责任评价其工作业绩对其任职期间履行经济责任情况作出客观公正的评价。`,
rows: 6,
generating: false
},
{
number: '三',
name: '审计对象和范围',
title: '3、审计对象和范围',
description: '点击"AI生成"按钮让AI为您生成该部分内容或直接在下方编辑',
placeholder: '请输入审计对象和范围内容...',
content: '',
rows: 8,
generating: false
},
{
number: '四',
name: '被审计单位基本情况',
title: '4、被审计单位基本情况',
description: '点击"AI生成"按钮让AI为您生成该部分内容或直接在下方编辑',
placeholder: '请输入被审计单位基本情况内容...',
content: '',
rows: 10,
generating: false
},
{
number: '五',
name: '审计内容和重点及审计方法',
title: '5、审计内容和重点及审计方法',
description: '点击"AI生成"按钮让AI为您生成该部分内容或直接在下方编辑',
placeholder: '请输入审计内容和重点及审计方法内容...',
content: '',
rows: 12,
generating: false
},
{
number: '六',
name: '重要风险的识别及应对',
title: '6、重要风险的识别及应对',
description: '点击"AI生成"按钮让AI为您生成该部分内容或直接在下方编辑',
placeholder: '请输入重要风险的识别及应对内容...',
content: '',
rows: 10,
generating: false
},
{
number: '七',
name: '审计技术方法',
title: '7、审计技术方法',
description: '点击"AI生成"按钮让AI为您生成该部分内容或直接在下方编辑',
placeholder: '请输入审计技术方法内容...',
content: '',
rows: 8,
generating: false
},
{
number: '八',
name: '工作步骤与时间安排',
title: '8、工作步骤与时间安排',
description: '点击"AI生成"按钮让AI为您生成该部分内容或直接在下方编辑',
placeholder: '请输入工作步骤与时间安排内容...',
content: '',
rows: 10,
generating: false
},
{
number: '九',
name: '审计工作的组织实施',
title: '9、审计工作的组织实施',
description: '点击"AI生成"按钮让AI为您生成该部分内容或直接在下方编辑',
placeholder: '请输入审计工作的组织实施内容...',
content: '',
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,
});
// 请求状态
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');
};
/* AI生成内容 */
const generateContent = async (index: number) => {
const item = navigationItems.value[index];
item.generating = true;
try {
// 这里可以调用AI接口生成内容
// 模拟AI生成过程
await new Promise(resolve => setTimeout(resolve, 2000));
// 根据不同章节生成不同的示例内容
const sampleContent = getSampleContent(index);
item.content = sampleContent;
} catch (error) {
console.error('AI生成失败:', error);
} finally {
item.generating = false;
}
};
/* 获取示例内容 */
const getSampleContent = (index: number) => {
const samples = [
// 审计依据
`中共中央办公厅、国务院办公厅《党政主要领导干部和国有企业领导人员经济责任审计规定》2019年7月7日起实施
(二)${form.name}经济责任审计管理办法》(相关文件编号);
(三)国家有关法律、法规及相关文件;
(四)${form.name}下达的年度工作目标;
(五)${form.name}各项管理制度;
(六)双方签订的审计业务约定书。`,
// 审计目标
`通过对${form.name}相关负责人任职期间所负责公司资产、负债情况的真实性、合法性和效益性及其相关经济活动进行审计,查清其任职期间公司财经政策指导和财务收支工作目标完成情况,重大决策执行及交接情况,遵守国家财经法规情况及公司资产保值增值情况,分清其经济责任,评价其工作业绩,对其任职期间履行经济责任情况作出客观公正的评价。`,
// 审计对象和范围
`审计对象:${form.name}及其相关负责人
审计范围:${form.itemYear || '相关年度'}年度财务收支情况、资产管理情况、重大经济决策情况等。
具体包括:
1. 财务收支的真实性、合法性;
2. 资产、负债、损益的真实性;
3. 重大经济决策的程序性和效果性;
4. 内部控制制度的建立和执行情况。`,
// 被审计单位基本情况
`${form.name}基本情况:
成立时间:[待补充]
注册资本:[待补充]
经营范围:[待补充]
组织架构:[待补充]
主要业务:[待补充]
年末资产总额:${form.totalAssets ? form.totalAssets + '万元' : '[待补充]'}`,
// 审计内容和重点及审计方法
`审计内容:
1. 财务收支审计
2. 资产负债审计
3. 经营管理审计
4. 内部控制审计
审计重点:
1. 重大经济决策的合规性
2. 资产保值增值情况
3. 财务管理制度执行情况
4. 风险控制措施有效性
审计方法:
1. 账项基础审计
2. 制度基础审计
3. 风险导向审计
4. 计算机辅助审计`,
// 重要风险的识别及应对
`重要风险识别:
1. 财务报告风险
2. 合规性风险
3. 经营风险
4. 信息系统风险
应对措施:
1. 加强内部控制测试
2. 扩大实质性程序范围
3. 增加专业判断和职业怀疑
4. 运用专家工作和外部确认`,
// 审计技术方法
`主要采用的审计技术方法:
1. 检查:检查记录或文件
2. 观察:观察流程或程序的执行
3. 询问:向相关人员询问
4. 函证:向第三方函证
5. 重新计算:重新计算相关数据
6. 重新执行:重新执行相关控制
7. 分析程序:分析财务和非财务信息`,
// 工作步骤与时间安排
`工作步骤:
第一阶段审计准备X天
- 了解被审计单位情况
- 制定审计计划
- 组建审计组
第二阶段现场审计X天
- 内部控制测试
- 实质性程序执行
- 获取审计证据
第三阶段审计报告X天
- 汇总审计发现
- 撰写审计报告
- 与被审计单位沟通`,
// 审计工作的组织实施
`组织架构:
审计组长:[待指定]
审计组员:[待指定]
技术复核:[待指定]
实施要求:
1. 严格按照审计准则执行
2. 保持职业怀疑态度
3. 获取充分适当的审计证据
4. 及时与被审计单位沟通
5. 按时完成审计工作
6. 确保审计质量`
];
return samples[index] || '请输入相关内容...';
};
/* 监听滚动位置更新当前章节 */
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,
(visible) => {
if (visible) {
if (props.data) {
loading.value = true;
assignObject(form, props.data);
// 重置到第一个章节
currentSection.value = 0;
}
} else {
resetFields();
}
}
);
</script>
<script lang="ts">
import * as MenuIcons from '@/layout/menu-icons';
export default {
name: 'PwlProjectInfo',
components: MenuIcons
};
</script>
<style lang="less" scoped>
.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: 8px;
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: #666;
font-size: 14px;
margin-bottom: 8px;
padding: 12px;
background: #f8f9fa;
border-radius: 6px;
border-left: 4px 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>

View File

@@ -69,7 +69,7 @@
</a-tooltip> </a-tooltip>
</template> </template>
<template v-if="column.key === 'action'"> <template v-if="column.key === 'action'">
<a class="text-pink-500" @click="openEdit(record)">生成报告</a> <a class="text-pink-500" @click="openReport(record)">生成报告</a>
<a-divider type="vertical"/> <a-divider type="vertical"/>
<a @click="openEdit(record)">修改</a> <a @click="openEdit(record)">修改</a>
<a-divider type="vertical"/> <a-divider type="vertical"/>
@@ -85,7 +85,9 @@
</a-card> </a-card>
<!-- 编辑弹窗 --> <!-- 编辑弹窗 -->
<PwlProjectEdit v-model:visible="showEdit" :data="current" @done="reload"/> <Edit v-model:visible="showEdit" :data="current" @done="reload"/>
<!-- 生成报告 -->
<Report v-model:visible="showReport" :data="current" @done="reload"/>
</a-page-header> </a-page-header>
</template> </template>
@@ -100,7 +102,8 @@ import type {
ColumnItem ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types'; } from 'ele-admin-pro/es/ele-pro-table/types';
import Search from './components/search.vue'; import Search from './components/search.vue';
import PwlProjectEdit from './components/pwlProjectEdit.vue'; import Edit from './components/pwlProjectEdit.vue';
import Report from './components/report.vue';
import {pagePwlProject, removePwlProject, removeBatchPwlProject} from '@/api/pwl/pwlProject'; import {pagePwlProject, removePwlProject, removeBatchPwlProject} from '@/api/pwl/pwlProject';
import type {PwlProject, PwlProjectParam} from '@/api/pwl/pwlProject/model'; import type {PwlProject, PwlProjectParam} from '@/api/pwl/pwlProject/model';
import {getPageTitle} from "@/utils/common"; import {getPageTitle} from "@/utils/common";
@@ -115,6 +118,8 @@ const selection = ref<PwlProject[]>([]);
const current = ref<PwlProject | null>(null); const current = ref<PwlProject | null>(null);
// 是否显示编辑弹窗 // 是否显示编辑弹窗
const showEdit = ref(false); const showEdit = ref(false);
// 是否显示报告弹窗
const showReport = ref(false);
// 是否显示批量移动弹窗 // 是否显示批量移动弹窗
const showMove = ref(false); const showMove = ref(false);
// const draftUser = ref<string[]>([]); // const draftUser = ref<string[]>([]);
@@ -380,6 +385,11 @@ const openEdit = (row?: PwlProject) => {
showEdit.value = true; showEdit.value = true;
}; };
const openReport = (row?: PwlProject) => {
current.value = row ?? null;
showReport.value = true;
}
/* 打开批量移动弹窗 */ /* 打开批量移动弹窗 */
const openMove = () => { const openMove = () => {
showMove.value = true; showMove.value = true;