Files
template-10561/src/views/pwl/pwlProject/components/reportContent.vue

2080 lines
53 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.

<!-- 用户编辑弹窗 -->
<template>
<a-drawer
:width="`95%`"
:visible="visible"
:confirm-loading="loading"
:maxable="maxAble"
title="AI审计内容生成器"
:body-style="{ paddingBottom: '8px', background: '#f3f3f3' }"
@update:visible="updateVisible"
:maskClosable="false"
:footer="null"
@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
size="large"
type="primary"
@click="handleGenerateAll"
:loading="generatingAll"
class="generate-all-button"
>
<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"
type="danger"
class="export-button"
@click="handleExport"
>
<template #icon>
<UploadOutlined />
</template>
下载文件
</a-button>
</a-space>
</a-card>
<!-- 快速导航 -->
<a-card style="margin-bottom: 20px" :bordered="false">
<template #title>
<div
style="
display: flex;
align-items: center;
justify-content: space-between;
"
>
<span>快速导航</span>
<a-tooltip title="快捷键Ctrl+1~9 快速跳转Ctrl+↑↓ 上下导航">
<QuestionCircleOutlined style="color: #999; cursor: help" />
</a-tooltip>
</div>
</template>
<div class="navigation-container">
<div class="nav-grid">
<a-button
v-for="(item, index) in navigationItems"
:key="index"
:type="currentSection === index ? 'primary' : 'default'"
size="small"
@click="scrollToSection(index)"
class="nav-button"
:class="{ active: currentSection === index }"
>
<span class="nav-number">{{ item.number }}</span>
<span class="nav-text">{{ item.name }}</span>
</a-button>
</div>
<!-- 进度指示器 -->
<div class="progress-container">
<div class="progress-bar">
<div
class="progress-fill"
:style="{
width: `${(
((currentSection + 1) / navigationItems.length) * 100
)}%`
}"
></div>
</div>
<div class="progress-text">
{{ currentSection + 1 }} / {{ navigationItems.length }}
</div>
</div>
</div>
</a-card>
<!-- 审计方案内容 -->
<div class="audit-content">
<div
v-for="(item, index) in navigationItems"
:key="index"
:id="`section-${index}`"
class="audit-section"
>
<a-card style="margin-bottom: 20px" :bordered="false">
<template #title>
<span class="font-bold">{{ `${item.number}${item.name}` }}</span>
<span class="ml-4 text-gray-400" v-if="item.extra3">{{ table3Title }}</span>
<span class="ml-4 text-gray-400" v-if="item.file3">
<!-- 修改选择文件组件 -->
<a-button
type="link"
@click="openDocSelect(index)"
class="select-file-btn"
>
<FolderOpenOutlined />
选择文件
</a-button>
</span>
</template>
<template #extra>
<a-button
type="primary"
@click="generateContent(index)"
:loading="item.generating"
>
<template #icon>
<RobotOutlined />
</template>
AI生成
</a-button>
</template>
<template v-if="item.extra3">
<div class="mb-2">
<a-space>
<div
class="btn"
:class="[
table3Title === '重大经济决策调查表'
? 'btn-green'
: 'btn-gray'
]"
@click="changeTable3('重大经济决策调查表')"
>重大经济决策调查表</div
>
<div
class="btn"
:class="[
table3Title === '三重一大' ? 'btn-green' : 'btn-gray'
]"
@click="changeTable3('三重一大')"
>三重一大</div
>
</a-space>
</div>
</template>
<a-table :columns="item.columns" bordered :data-source="item.data">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<a-button type="primary" danger>删除</a-button>
</template>
<template v-if="column.key === 'testResult'">
<span class="text-green-400" v-if="record.testResult === '通过'">通过</span>
<span class="text-red-400" v-else-if="record.testResult === '不通过'">不通过</span>
<span class="text-gray-400" v-else>{{ record.testResult || '待检查' }}</span>
</template>
<template v-if="column.key === 'workPaperIndex'">
<div v-for="(fileItem, fileIndex) in record.workPaperIndex" :key="fileIndex">
<img src="@/assets/word.png" style="width: 20px; height: 20px" alt="">
<span class="cursor-pointer text-wrap">{{ fileItem }}</span>
</div>
</template>
<template v-if="column.key === 'fileIndex'">
<div v-for="(fileItem, fileIndex) in record.fileIndex" :key="fileIndex">
<img src="@/assets/word.png" style="width: 20px; height: 20px" alt="">
<span class="cursor-pointer text-wrap">{{ fileItem }}</span>
</div>
</template>
</template>
</a-table>
<div style="margin-top: 12px">
<div class="question-prompt">AI小助手</div>
<div class="textarea-with-button" style="width: 600px">
<a-textarea
v-model:value="item.suggestion"
:rows="3"
placeholder="请输入您的要求并回车..."
class="suggestion-textarea-inner"
@pressEnter="generateContent(index)"
/>
<a-button
type="danger"
size="small"
@click="generateContent(index)"
:loading="item.generating"
class="send-button-inner"
>
<template #icon>
<RobotOutlined />
</template>
发送
</a-button>
</div>
</div>
</a-card>
</div>
</div>
<!-- 返回顶部按钮 -->
<a-back-top :target="getScrollContainer" />
<!-- 文档选择弹窗 -->
<a-modal
v-model:visible="showDocSelect"
title="选择文档"
width="1200px"
:footer="null"
wrap-class-name="doc-select-modal"
@cancel="handleDocSelectCancel"
>
<div class="doc-select-container">
<div class="doc-layout">
<!-- 左侧目录树 -->
<div class="dir-tree-panel">
<div class="dir-header">
<span>文档目录</span>
</div>
<div class="tree-container">
<a-tree
v-if="treeData.length > 0"
:tree-data="treeData"
:expanded-keys="expandedKeys"
:selected-keys="selectedKeys"
:load-data="onLoadData"
@expand="onExpand"
@select="onSelect"
:field-names="{ title: 'name', key: 'id', children: 'children' }"
checkable
:checked-keys="checkedDirKeys"
@check="onDirCheck"
>
<template #title="{ name, id }">
<span :class="{ 'active-dir': selectedKeys[0] === id }">{{ name }}</span>
</template>
</a-tree>
<a-empty v-else :image="simpleImage" description="暂无目录" />
</div>
</div>
<!-- 右侧文档列表 -->
<div class="doc-list-panel">
<div class="doc-header">
<div class="doc-actions">
<span class="doc-tips">已选择 {{ checkedDirKeys.length }} 个目录, {{ selectedFileKeys.length }} 个文件</span>
<a-space>
<a-button @click="clearSelection">清空选择</a-button>
<a-button type="primary" @click="confirmSelection">确认选择</a-button>
</a-space>
</div>
</div>
<div class="doc-content">
<a-table
:dataSource="docList"
:columns="docColumns"
:loading="docLoading"
rowKey="id"
:scroll="{ y: 400 }"
:pagination="pagination"
@change="handleTableChange"
size="middle"
:row-selection="{
selectedRowKeys: selectedFileKeys,
onChange: onFileSelectionChange,
type: 'checkbox'
}"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'fileName'">
<div style="display: flex; align-items: center; gap: 8px">
<FileOutlined />
<span>{{ record.fileName }}</span>
</div>
</template>
<template v-if="column.key === 'fileSize'">
{{ formatFileSize(record.fileSize) }}
</template>
<template v-if="column.key === 'fileType'">
<a-tag color="blue">{{ record.fileType }}</a-tag>
</template>
</template>
</a-table>
</div>
</div>
</div>
</div>
</a-modal>
</a-drawer>
</template>
<script lang="ts" setup>
import { ref, reactive, watch, onMounted, onUnmounted, computed } from 'vue';
import { Form, message } from 'ant-design-vue';
import { assignObject } from 'ele-admin-pro';
import { copyText } from '@/utils/common';
import { PwlProject } from '@/api/pwl/pwlProject/model';
import {
UngroupOutlined,
DownloadOutlined,
UploadOutlined,
RedoOutlined,
RobotOutlined,
QuestionCircleOutlined,
FolderOpenOutlined,
FileOutlined
} from '@ant-design/icons-vue';
import {
generateAuditReport,
downloadAuditReport
} from '@/api/ai/auditReport';
import { getPwlProjectLibraryByIds } from '@/api/pwl/pwlProjectLibrary';
import { generateTripleOneTable } from '@/api/ai/auditContent';
import { Empty } from 'ant-design-vue';
import { listAiCloudDoc } from '@/api/ai/aiCloudDoc';
import { listAiCloudFile } from '@/api/ai/aiCloudFile';
import type { AiCloudDoc } from '@/api/ai/aiCloudDoc/model';
import type { AiCloudFile } from '@/api/ai/aiCloudFile/model';
const useForm = Form.useForm;
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: PwlProject | null;
}>();
let columns = ref([
{
title: '序号',
dataIndex: 'index',
key: 'index',
width: 80,
align: 'center'
},
{
title: '重大经济决策事项',
dataIndex: 'name',
key: 'name'
},
{
title: '会议时间',
dataIndex: 'content',
key: 'content',
width: 120
},
{
title: '决策事项金额',
dataIndex: 'amount',
key: 'amount',
width: 120
},
{
title: '程序程序',
dataIndex: 'progress',
key: 'progress',
width: 300
},
{
title: '执行情况(是/否)',
dataIndex: 'done',
key: 'done'
},
{
title: '执行效果(是否实现决策目标)',
children: [
{
title: '好',
dataIndex: 'goods'
},
{
title: '一般',
dataIndex: 'normal'
},
{
title: '差',
dataIndex: 'bad'
}
]
}
]);
// 是否显示最大化切换按钮
const maxAble = ref(true);
// 添加生成状态
const generatingAll = ref(false);
// 当前选中的章节
const currentSection = ref(0);
// 存储拼接后的 kbIds
const combinedKbIds = ref('');
// 九大审计项目导航配置
const table3Title = ref('重大经济决策调查表');
const navigationItems = ref([
{
number: '一',
name: '审计内容1',
title: '',
description: '点击"AI生成"按钮让AI为您生成该部分内容或直接在下方编辑',
placeholder: '请输入审计依据内容...',
content: '',
suggestion: '',
formCommit: 10,
rows: 10,
generating: false,
columns: columns.value
},
{
number: '二',
name: '审计内容2',
title: '',
description: '点击"AI生成"按钮让AI为您生成该部分内容或直接在下方编辑',
placeholder: '请输入审计目标内容...',
content: '',
suggestion: '',
formCommit: 20,
rows: 6,
generating: false,
columns: columns.value
},
{
number: '三',
name: '审计内容3',
title: '重大经济决策调查表',
description: '点击"AI生成"按钮生成重大经济决策调查表数据',
generating: false,
extra3: true,
file3: true,
columns: [
{
title: '序号',
dataIndex: 'index',
key: 'index',
width: 80,
align: 'center'
},
{
title: '重大经济决策事项',
dataIndex: 'name',
key: 'name',
width: 200,
align: 'center'
},
{
title: '会议时间',
dataIndex: 'meetingTime',
key: 'meetingTime',
align: 'center',
width: 120
},
{
title: '决策事项金额',
dataIndex: 'decisionAmount',
key: 'decisionAmount',
width: 120,
align: 'center'
},
{
title: '程序',
dataIndex: 'procedure',
key: 'procedure',
align: 'center',
customRender: ({ text }) => text || '程序未记录'
},
{
title: '执行情况(是/否)',
dataIndex: 'executionStatus',
key: 'executionStatus',
width: 80,
align: 'center'
},
{
title: '执行效果(是否实现决策目标)',
children: [
{
title: '好',
dataIndex: 'goods',
key: 'goods',
width: 60,
align: 'center',
customRender: ({ text }) => (text === '是' ? '✅' : '')
},
{
title: '一般',
dataIndex: 'normal',
key: 'normal',
width: 60,
align: 'center',
customRender: ({ text }) => (text === '是' ? '✅' : '')
},
{
title: '差',
dataIndex: 'bad',
key: 'bad',
width: 60,
align: 'center',
customRender: ({ text }) => (text === '是' ? '✅' : '')
}
]
},
{
title: '操作',
key: 'action',
align: 'center',
width: 140
}
],
data: []
},
{
number: '四',
name: '审计内容4',
title: '',
description: '点击"AI生成"按钮让AI为您生成该部分内容或直接在下方编辑',
generating: false,
columns: columns.value
},
{
number: '五',
name: '审计内容5',
title: '',
description: '点击"AI生成"按钮让AI为您生成该部分内容或直接在下方编辑',
generating: false,
columns: [
{
title: '预算科目名称',
dataIndex: 'index',
key: 'index',
width: 80,
align: 'center'
},
{
title: '指标来源',
dataIndex: 'name',
key: 'name',
children: [
{
title: '合计'
},
{
title: '上年结余'
},
{
title: '年初部门预算'
},
{
title: '追加(减)预算'
}
]
},
{
title: '指标运用',
dataIndex: 'name',
key: 'name',
children: [
{
title: '合计'
},
{
title: '拨款',
children: [
{
title: '小计'
},
{
title: '拨款',
children: [
{
title: '工资统发'
},
{
title: '政府采购'
}
]
}
]
},
{
title: '财政管理转户'
},
{
title: '部门预算用于其他'
}
]
},
{
title: '指标结余',
children: [
{
title: '小计'
},
{
title: '政府采购'
},
{
title: '应拨单位款'
},
{
title: '其他'
}
]
}
]
},
{
number: '六',
name: '审计内容6',
title: '',
description: '点击"AI生成"按钮让AI为您生成该部分内容或直接在下方编辑',
generating: false,
columns: columns.value
},
{
number: '七',
name: '审计内容7',
title: '',
description: '点击"AI生成"按钮让AI为您生成该部分内容或直接在下方编辑',
placeholder: '请输入审计技术方法内容...',
content: '',
suggestion: '',
formCommit: 70,
rows: 8,
generating: false,
columns: columns.value
},
{
number: '八',
name: '审计内容8',
title: '',
description: '点击"AI生成"按钮让AI为您生成该部分内容或直接在下方编辑',
placeholder: '请输入工作步骤与时间安排内容...',
content: '',
suggestion: '',
formCommit: 80,
rows: 10,
generating: false,
columns: columns.value
},
{
number: '九',
name: '审计内容9',
title: '',
description: '点击"AI生成"按钮让AI为您生成该部分内容或直接在下方编辑',
placeholder: '请输入审计工作的组织实施内容...',
content: '',
suggestion: '',
formCommit: 90,
rows: 8,
generating: false,
columns: columns.value
}
]);
// 文档选择相关变量
const showDocSelect = ref(false);
const currentSectionIndex = ref(2); // 默认是审计内容3
const selectedDocList = ref<string[]>([]);
const selectedFileList = ref<string[]>([]);
const selectedFileKeys = ref<(string | number)[]>([]);
const checkedDirKeys = ref<(string | number)[]>([]); // 新增勾选的目录keys
// 新增:保存上次选择的状态
const lastSelectedDirKeys = ref<(string | number)[]>([]);
const lastSelectedFileKeys = ref<(string | number)[]>([]);
// 文档管理相关变量
const currentKbId = ref('');
const currentKbName = ref('');
const currentCompanyId = ref<number>();
const docList = ref<AiCloudFile[]>([]);
const docLoading = ref(false);
const allDirs = ref<AiCloudDoc[]>([]);
// 树形结构相关
const expandedKeys = ref<(string | number)[]>([]);
const selectedKeys = ref<(string | number)[]>([]);
const simpleImage = Empty.PRESENTED_IMAGE_SIMPLE;
// 分页配置
const pagination = ref({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: false,
showQuickJumper: true,
showTotal: (total: number) => `${total}`,
pageSizeOptions: ['10', '20', '50', '100']
});
// 文档表格列配置
const docColumns = ref([
{
title: '文件名',
dataIndex: 'fileName',
key: 'fileName',
ellipsis: true
},
{
title: '文件大小',
dataIndex: 'fileSize',
key: 'fileSize',
width: 120
},
{
title: '文件类型',
dataIndex: 'fileType',
key: 'fileType',
width: 120,
ellipsis: true
},
{
title: '上传时间',
dataIndex: 'uploadTime',
key: 'uploadTime',
width: 180
}
]);
// 计算树形数据
const treeData = computed(() => {
const buildTree = (parentId: number = 0): any[] => {
return allDirs.value
.filter(item => item.parentId === parentId)
.map(item => ({
...item,
key: item.id,
title: item.name,
children: buildTree(item.id),
isLeaf: allDirs.value.filter(child => child.parentId === item.id).length === 0
}));
};
return buildTree(0);
});
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 handleGenerateAll2 = async () => {
generatingAll.value = true;
try {
// 使用Promise.all进行并行生成
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 handleGenerateAll = async () => {
generatingAll.value = true;
try {
// 改为顺序生成
for (const [index, item] of navigationItems.value.entries()) {
// 跳过未完成的依赖项
if (index === 4 && !hasContent(3)) {
message.warning('请先生成第四项「被审计单位基本情况」');
continue;
}
if (index === 5 && !hasContent(4)) {
message.warning('请先生成第五项「审计内容和重点及审计方法」');
continue;
}
if (item.children) {
for (const [childIndex] of item.children.entries()) {
await generateContent(index, childIndex);
}
} else {
await generateContent(index);
}
}
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 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) => {
// 特殊处理审计内容3重大经济决策调查表
if (sectionIndex === 2) {
await generateContent3();
return;
}
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 generateContent3 = async () => {
const section = navigationItems.value[2];
section.generating = true;
// 构建history - 将当前已有的数据作为历史记录
const history = section.data && section.data.length > 0
? JSON.stringify(section.data, null, 2)
: '';
// 获取用户输入的建议
const suggestion = section.suggestion || '';
console.log('生成三重一大参数:', {
kbIds: props.data?.kbId,
libraryIds: props.data?.libraryIds,
projectLibrary: props.data?.projectLibrary,
history: history ? `历史数据(${section.data.length}条)` : '无',
suggestion: suggestion || '无',
docList: checkedDirKeys.value, // 修复使用勾选的目录keys
fileList: selectedFileKeys.value // 修复使用勾选的文件keys
});
try {
// 构建请求参数对象
const requestData = {
kbIds: props.data?.kbId || '',
libraryIds: props.data?.libraryIds || '',
projectLibrary: props.data?.projectLibrary || '',
history: history,
suggestion: suggestion,
docList: checkedDirKeys.value, // 修复传入目录id数组
fileList: selectedFileKeys.value // 修复传入文件id数组
};
// 调用三重一大生成接口
const result = await generateTripleOneTable(requestData);
console.log('三重一大接口返回结果:', result);
if (result.code === 0 && result.data && result.data.success) {
const tableData = result.data.data.map((item, index) => ({
key: index,
category: item.category || '',
policyContent: item.policyContent || '',
groupSystem: item.groupSystem || '',
companyFormulation: item.companyFormulation || '',
checkEvidence: item.checkEvidence || '',
testResult: item.testResult || '待检查',
workPaperIndex: item.workPaperIndex || []
}));
section.data = tableData;
// 生成成功后清空建议输入框
section.suggestion = '';
message.success(`成功生成 ${tableData.length} 条三重一大制度对比记录`);
} else {
const errorMsg = result.data?.error || result.message || '生成失败';
throw new Error(errorMsg);
}
} catch (error) {
console.error('生成三重一大制度对比分析表失败:', error);
message.error('生成失败: ' + (error.message || '未知错误'));
} finally {
section.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 = () => {};
// 打开文档选择弹窗
const openDocSelect = (sectionIndex: number) => {
currentSectionIndex.value = sectionIndex;
showDocSelect.value = true;
// 恢复上次选择的状态
checkedDirKeys.value = [...lastSelectedDirKeys.value];
selectedFileKeys.value = [...lastSelectedFileKeys.value];
selectedDocList.value = lastSelectedDirKeys.value.map(key => key.toString());
selectedFileList.value = lastSelectedFileKeys.value.map(key => key.toString());
// 加载文档目录
loadAllCloudDocs();
};
// 新增:文档选择弹窗关闭处理
const handleDocSelectCancel = () => {
// 保存当前选择状态
lastSelectedDirKeys.value = [...checkedDirKeys.value];
lastSelectedFileKeys.value = [...selectedFileKeys.value];
showDocSelect.value = false;
};
// 加载所有目录
const loadAllCloudDocs = async () => {
try {
const params = {
companyId: currentCompanyId.value
};
const result = await listAiCloudDoc(params);
allDirs.value = result || [];
// 默认展开根节点并选中第一个目录
if (allDirs.value.length > 0) {
const rootDirs = allDirs.value.filter(item => item.parentId === 0);
if (rootDirs.length > 0) {
expandedKeys.value = [0];
// 如果已经有选中的目录,保持选中状态,否则选中第一个目录
if (selectedKeys.value.length === 0) {
selectedKeys.value = [rootDirs[0].id!];
}
loadCloudFiles();
}
}
} catch (error) {
message.error('加载目录列表失败');
console.error('加载目录错误:', error);
}
};
// 树节点展开
const onExpand = (keys: (string | number)[]) => {
expandedKeys.value = keys;
};
// 树节点选择
const onSelect = (keys: (string | number)[], { node }: any) => {
selectedKeys.value = keys;
pagination.value.current = 1;
loadCloudFiles();
};
// 新增:目录勾选处理
const onDirCheck = (checkedKeys: (string | number)[], { checked }: any) => {
checkedDirKeys.value = checkedKeys;
selectedDocList.value = checkedKeys.map(key => key.toString());
};
// 异步加载子节点
const onLoadData = (node: any) => {
return new Promise<void>((resolve) => {
resolve();
});
};
// 表格分页变化处理
const handleTableChange = (pag: any) => {
pagination.value.current = pag.current;
pagination.value.pageSize = pag.pageSize;
loadCloudFiles();
};
// 文件选择变化
const onFileSelectionChange = (selectedRowKeys: (string | number)[], selectedRows: AiCloudFile[]) => {
selectedFileKeys.value = selectedRowKeys;
selectedFileList.value = selectedRows.map(row => row.id!.toString());
};
// 清空选择
const clearSelection = () => {
selectedDocList.value = [];
selectedFileList.value = [];
selectedFileKeys.value = [];
checkedDirKeys.value = []; // 新增:清空勾选的目录
// 重新选择当前目录
if (selectedKeys.value.length > 0) {
selectedDocList.value = [selectedKeys.value[0].toString()];
checkedDirKeys.value = [selectedKeys.value[0]]; // 新增:重新勾选当前目录
}
};
// 确认选择
const confirmSelection = () => {
// 保存当前选择状态
lastSelectedDirKeys.value = [...checkedDirKeys.value];
lastSelectedFileKeys.value = [...selectedFileKeys.value];
message.success(`已选择 ${checkedDirKeys.value.length} 个目录和 ${selectedFileKeys.value.length} 个文件`);
showDocSelect.value = false;
};
// 加载文档列表
const loadCloudFiles = async () => {
docLoading.value = true;
try {
if (!selectedKeys.value.length) {
docList.value = [];
pagination.value.total = 0;
return;
}
const params = {
docId: parseInt(selectedKeys.value[0].toString()),
page: pagination.value.current,
pageSize: pagination.value.pageSize
};
const result = await listAiCloudFile(params);
if (result && result.records) {
docList.value = result.records;
pagination.value.total = result.total;
} else {
docList.value = result || [];
pagination.value.total = docList.value.length;
}
} catch (error) {
message.error('加载文件列表失败');
console.error('加载文件错误:', error);
} finally {
docLoading.value = false;
}
};
// 格式化文件大小
const formatFileSize = (size: number) => {
if (!size) return '-';
if (size < 1024) return size + ' B';
if (size < 1024 * 1024) return (size / 1024).toFixed(1) + ' KB';
return (size / (1024 * 1024)).toFixed(1) + ' MB';
};
const file3List = ref([])
const changeTable3 = (title) => {
table3Title.value = title;
if (title === '三重一大') {
navigationItems.value[2].columns = [
{
title: '',
dataIndex: 'category',
key: 'category',
width: 120,
align: 'center'
},
{
title: '政策内容',
dataIndex: 'policyContent',
key: 'policyContent',
align: 'center'
},
{
title: '集团制度',
dataIndex: 'groupSystem',
key: 'groupSystem',
align: 'center'
},
{
title: '公司制定',
dataIndex: 'companyFormulation',
key: 'companyFormulation',
align: 'center'
},
{
title: '检查的证据及测试内容',
dataIndex: 'checkEvidence',
key: 'checkEvidence',
align: 'center'
},
{
title: '测试结果',
dataIndex: 'testResult',
key: 'testResult',
align: 'center',
customRender: ({ text }) => {
if (text === '通过') return '<span style="color: #52c41a">通过</span>';
if (text === '不通过') return '<span style="color: #ff4d4f">不通过</span>';
return text;
}
},
{
title: '工作底稿索引',
dataIndex: 'workPaperIndex',
key: 'workPaperIndex',
align: 'center',
width: 140,
ellipsis: true,
},
{
title: '操作',
key: 'action',
align: 'center',
width: 100,
}
];
// 清空数据,准备生成新的三重一大数据
navigationItems.value[2].data = [];
} else {
navigationItems.value[2].columns = [
{
title: '序号',
dataIndex: 'index',
key: 'index',
width: 80,
align: 'center'
},
{
title: '重大经济决策事项',
dataIndex: 'name',
key: 'name',
width: 200,
align: 'center'
},
{
title: '会议时间',
dataIndex: 'meetingTime',
key: 'meetingTime',
align: 'center',
width: 120
},
{
title: '决策事项金额',
dataIndex: 'decisionAmount',
key: 'decisionAmount',
width: 120,
align: 'center'
},
{
title: '程序',
dataIndex: 'procedure',
key: 'procedure',
align: 'center',
customRender: ({ text }) => text || '程序未记录'
},
{
title: '执行情况(是/否)',
dataIndex: 'executionStatus',
key: 'executionStatus',
width: 80,
align: 'center'
},
{
title: '执行效果(是否实现决策目标)',
children: [
{
title: '好',
dataIndex: 'goods',
key: 'goods',
width: 60,
align: 'center',
customRender: ({ text }) => (text === '是' ? '✅' : '')
},
{
title: '一般',
dataIndex: 'normal',
key: 'normal',
width: 60,
align: 'center',
customRender: ({ text }) => (text === '是' ? '✅' : '')
},
{
title: '差',
dataIndex: 'bad',
key: 'bad',
width: 60,
align: 'center',
customRender: ({ text }) => (text === '是' ? '✅' : '')
}
]
},
{
title: '操作',
key: 'action',
align: 'center',
width: 140
}
];
// 恢复重大经济决策调查表的默认数据
navigationItems.value[2].data = [
{
index: 1,
name: '千汇公司废水处理站扩容升级改造项目',
meetingTime: '2024-01-03',
decisionAmount: '190万元',
procedure:
'经专题会议研究并组织外部专家二次评勖审(《2023年第九次总办会会议材料汇总》),形成方案后提交总经理办公会审鈀还户,以记名投票方式全票通过。议案履行了可行性论证、专家咨询、集体讨论和会议决策程序,符台《广西千汇食品有限公司总经理办公会议事规则(试行)》第十条、第十八条关于重大项目投资业务决策程序的要求。',
executionStatus: '是',
goods: '是'
}
];
}
};
// 组件挂载时添加滚动监听和键盘监听
onMounted(() => {
const scrollContainer = getScrollContainer();
if (scrollContainer) {
scrollContainer.addEventListener('scroll', handleScroll);
}
document.addEventListener('keydown', handleKeydown);
});
// 组件卸载时移除监听
onUnmounted(() => {
const scrollContainer = getScrollContainer();
if (scrollContainer) {
scrollContainer.removeEventListener('scroll', handleScroll);
}
document.removeEventListener('keydown', handleKeydown);
});
watch(
() => props.visible,
async (visible) => {
if (visible) {
if (props.data) {
loading.value = true;
assignObject(form, props.data);
// 获取知识库并拼接 kbIds
if (props.data.libraryIds?.length > 0) {
try {
const result = await getPwlProjectLibraryByIds(
props.data.libraryIds
);
const kbIds = result
.map((lib) => lib.kbId)
.filter((kbId) => kbId);
if (form.kbId) {
kbIds.unshift(form.kbId);
}
combinedKbIds.value = kbIds.join(',');
} catch (error) {
console.error('获取知识库失败:', error);
combinedKbIds.value = form.kbId || '';
}
} else {
combinedKbIds.value = form.kbId || '';
}
// 设置当前公司ID用于文档管理
currentCompanyId.value = props.data.companyId;
// 重置到第一个章节
currentSection.value = 0;
}
} 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;
}
}
.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;
}
.btn {
padding: 5px 10px;
color: white;
border-radius: 999px;
cursor: pointer;
}
.btn-gray {
background-color: #bbbbbb;
}
.btn-green {
background-color: #479b33;
}
/* 选择文件按钮样式 */
.select-file-btn {
color: #1890ff;
font-size: 12px;
padding: 2px 8px;
border: 1px solid #d9d9d9;
border-radius: 4px;
background: #fff;
&:hover {
color: #40a9ff;
border-color: #40a9ff;
}
}
/* 文档选择弹窗样式 */
.doc-select-container {
height: 550px;
}
.doc-layout {
display: flex;
height: 100%;
gap: 16px;
}
.dir-tree-panel {
width: 280px;
border: 1px solid #e8e8e8;
border-radius: 6px;
display: flex;
flex-direction: column;
flex-shrink: 0;
}
.dir-header {
padding: 12px 16px;
border-bottom: 1px solid #e8e8e8;
background: #fafafa;
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 500;
}
.tree-container {
flex: 1;
padding: 8px;
overflow: auto;
}
.doc-list-panel {
flex: 1;
display: flex;
flex-direction: column;
border: 1px solid #e8e8e8;
border-radius: 6px;
min-width: 0;
overflow: hidden;
}
.doc-header {
padding: 12px 16px;
border-bottom: 1px solid #e8e8e8;
background: #fafafa;
}
.doc-actions {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.doc-tips {
color: #1890ff;
font-size: 14px;
font-weight: 500;
}
.doc-content {
flex: 1;
padding: 16px;
display: flex;
flex-direction: column;
overflow: hidden;
min-width: 0;
}
/* 树节点激活样式 */
:deep(.active-dir) {
color: #1890ff;
font-weight: 500;
}
:deep(.ant-tree-node-content-wrapper) {
border-radius: 4px;
transition: all 0.3s;
}
:deep(.ant-tree-node-content-wrapper:hover) {
background-color: #f5f5f5;
}
:deep(.ant-tree .ant-tree-treenode-selected .ant-tree-node-content-wrapper) {
background-color: #e6f7ff;
}
/* 优化表格样式 */
:deep(.doc-select-modal .ant-modal-body) {
padding: 16px;
}
:deep(.doc-content .ant-table-wrapper) {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
}
:deep(.doc-content .ant-spin-nested-loading) {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
}
:deep(.doc-content .ant-spin-container) {
flex: 1;
display: flex;
flex-direction: column;
height: 100%;
min-width: 0;
}
:deep(.doc-content .ant-table) {
width: 100%;
flex: 1;
}
:deep(.doc-content .ant-table-container) {
flex: 1;
display: flex;
flex-direction: column;
}
:deep(.doc-content .ant-table-body) {
flex: 1;
}
:deep(.doc-content .ant-table-thead > tr > th) {
background: #fafafa;
font-weight: 600;
white-space: nowrap;
}
:deep(.doc-content .ant-table-tbody > tr > td) {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* 文件名列特殊处理,允许换行 */
:deep(.doc-content .ant-table-tbody > tr > td:first-child) {
white-space: normal;
word-break: break-word;
line-height: 1.4;
max-width: 400px;
}
/* 分页样式调整 */
:deep(.doc-content .ant-pagination) {
margin-top: 16px;
margin-bottom: 0;
}
:deep(.doc-content .ant-table-pagination) {
margin-top: 16px;
margin-bottom: 0;
flex-shrink: 0;
}
</style>