Compare commits

...

2 Commits

5 changed files with 2060 additions and 294 deletions

View File

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

View File

@@ -184,3 +184,116 @@ export async function generateAuditReportWithEvidences(projectId: number, eviden
} }
return Promise.reject(new Error('文件下载失败')); return Promise.reject(new Error('文件下载失败'));
} }
/**
* 根据项目 ID、选中的取证单和章节内容生成审计报告并下载
* @param projectId 项目 ID
* @param evidenceIds 勾选的取证单 ID 列表
* @param chapters 章节内容数组(包含 formCommit 和 reportContent
* @param evaluate 总体评价
* @param suggestion 审计建议
*/
export async function generateAuditReportWithContent(
projectId: number,
evidenceIds: number[],
chapters: Array<{
formCommit: number;
reportContent: string;
}>,
evaluate?: string,
suggestion?: string
) {
const res = await request.post(
MODULES_API_URL + '/ai/auditReport/generateWithContent',
{
evidenceIds,
chapters,
evaluate,
suggestion
},
{
params: { projectId },
responseType: 'blob' // 处理二进制流响应
}
);
if (res.status === 200) {
return res.data;
}
return Promise.reject(new Error('文件下载失败'));
}
/**
* AI 生成默认话术
*/
export async function generateDefaultText(params: {
projectId?: number;
formCommit: number;
chapterTitle?: string;
evidenceIds?: number[]; // 新增:选中的取证单 ID 列表
}) {
const res = await request.post<ApiResult<string>>(
MODULES_API_URL + '/ai/auditReport/generateDefaultText',
{
projectId: params.projectId,
formCommit: params.formCommit,
chapterTitle: params.chapterTitle,
evidenceIds: params.evidenceIds
}
);
if (res.data.code === 0) {
// 后端返回的数据在 message 字段中
return res.data.message || res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* AI 分析用户自定义输入
*/
export async function analyzeUserInput(params: {
formCommit: number;
userQuestion: string;
chapterContent?: string;
}) {
const res = await request.post<ApiResult<string>>(
MODULES_API_URL + '/ai/auditReport/analyzeUserInput',
null,
{
params
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据取证单生成审计建议
*/
export async function generateAuditSuggestion(params: {
projectId: number;
evidenceIds: number[];
}) {
const res = await request.post<ApiResult<string>>(
MODULES_API_URL + '/ai/auditReport/generateAuditSuggestion',
{ evidenceIds: params.evidenceIds },
{
params: {
projectId: params.projectId
},
headers: {
'Content-Type': 'application/json;charset=UTF-8'
}
}
);
if (res.data.code === 0) {
// 后端返回的数据在 message 字段中
return res.data.message || res.data.data;
}
return Promise.reject(new Error(res.data.message));
}

View File

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

View File

@@ -170,45 +170,34 @@
<!-- </div>--> <!-- </div>-->
<!-- 单内容部分 --> <!-- 单内容部分 -->
<template v-if="!item.children"> <template v-if="!item.children">
<a-textarea <div class="content-with-ai-button">
v-model:value="item.content" <a-textarea
:rows="item.rows || 8" v-model:value="item.content"
:placeholder="'点击(AI生成)按钮让AI为您生成该部分内容或直接填入'" :rows="item.rows || 8"
class="content-textarea" :placeholder="'点击 (AI 生成) 按钮让 AI 为您生成该部分内容,或直接填入'"
style="margin-top: 16px; background-color: #f0fdf4" class="content-textarea"
/> style="background-color: #f0fdf4"
/>
<div style="margin-top: 12px;"> <a-button
<div class="question-prompt">AI小助手</div> type="primary"
<div class="textarea-with-button" style="width: 600px"> size="large"
<a-textarea @click="generateContent(index)"
v-model:value="item.suggestion" :loading="item.generating"
:rows="3" class="ai-generate-btn"
placeholder="请输入您的要求并回车..." >
class="suggestion-textarea-inner" <template #icon>
@pressEnter="generateContent(index)" <RobotOutlined/>
/> </template>
<a-button AI 生成
type="danger" </a-button>
size="small"
@click="generateContent(index)"
:loading="item.generating"
class="send-button-inner"
>
<template #icon>
<RobotOutlined/>
</template>
发送
</a-button>
</div>
</div> </div>
</template> </template>
<!-- 多内容部分 --> <!-- 多内容部分 -->
<template v-else> <template v-if="item.children">
<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 v-for="(child, childIndex) in item.children" :key="childIndex" class="child-section">
<div class="child-title" style="display: flex; align-items: center; justify-content: space-between;"> <div class="child-header">
<span style="font-weight: bold;">{{ child.name }}</span> <span class="child-title">{{ `${childIndex + 1}${child.name}` }}</span>
<a-space> <a-space>
<a-button <a-button
type="primary" type="primary"
@@ -236,10 +225,10 @@
</a-button> </a-button>
<a-button <a-button
type="primary" type="primary"
size="small" size="large"
@click.stop="generateContent(index, childIndex)" @click.stop="generateContent(index, childIndex)"
:loading="child.generating" :loading="child.generating"
class="child-action-button ai-generate-button" class="ai-generate-btn"
> >
<template #icon> <template #icon>
<RobotOutlined/> <RobotOutlined/>
@@ -253,34 +242,8 @@
:rows="child.rows || 6" :rows="child.rows || 6"
:placeholder="child.placeholder" :placeholder="child.placeholder"
class="content-textarea" class="content-textarea"
style="margin-top: 12px; background-color: #f0fdf4" style="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> </div>
</template> </template>
</a-card> </a-card>
@@ -1807,24 +1770,68 @@ export default {
border-radius: 6px; border-radius: 6px;
} }
.content-with-ai-button {
position: relative;
margin-top: 16px;
.ant-textarea {
min-height: 120px;
}
.ai-generate-btn {
position: absolute;
top: 8px;
right: 8px;
z-index: 10;
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;
}
}
}
.child-section { .child-section {
margin-bottom: 24px; margin-bottom: 24px;
padding-bottom: 16px; padding-bottom: 16px;
border-bottom: 1px dashed #e8e8e8; border-bottom: 1px dashed #e8e8e8;
position: relative;
&:last-child { &:last-child {
border-bottom: none; border-bottom: none;
margin-bottom: 0; margin-bottom: 0;
} }
}
.child-title { .child-header {
font-weight: 600; display: flex;
font-size: 15px; align-items: center;
color: #333; justify-content: space-between;
margin-bottom: 8px; margin-bottom: 12px;
padding-left: 8px; padding: 12px 16px;
border-left: 3px solid #1890ff; background: #f5f7fa;
border-radius: 6px 6px 0 0;
border-left: 3px solid #1890ff;
.child-title {
font-weight: 600;
font-size: 15px;
color: #333;
}
.ai-generate-btn {
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;
}
}
}
} }
.content-textarea { .content-textarea {

File diff suppressed because it is too large Load Diff