Merge remote-tracking branch 'origin/master'

This commit is contained in:
2025-11-17 18:41:23 +08:00
4 changed files with 530 additions and 270 deletions

View File

@@ -0,0 +1,129 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { AiHistory, AiHistoryParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
/**
* 分页查询AI审计历史记录表
*/
export async function pageAiHistory(params: AiHistoryParam) {
const res = await request.get<ApiResult<PageResult<AiHistory>>>(
`${MODULES_API_URL}/ai/history/page`,
{ params }
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询AI审计历史记录表列表
*/
export async function listAiHistory(params?: AiHistoryParam) {
const res = await request.get<ApiResult<AiHistory[]>>(
`${MODULES_API_URL}/ai/history`,
{ params }
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据id查询AI审计历史记录表
*/
export async function getAiHistory(id: number) {
const res = await request.get<ApiResult<AiHistory>>(
`${MODULES_API_URL}/ai/history/${id}`
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 添加AI审计历史记录表
*/
export async function addAiHistory(data: AiHistory) {
const res = await request.post<ApiResult<unknown>>(
`${MODULES_API_URL}/ai/history`,
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 修改AI审计历史记录表
*/
export async function updateAiHistory(data: AiHistory) {
const res = await request.put<ApiResult<unknown>>(
`${MODULES_API_URL}/ai/history`,
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 删除AI审计历史记录表
*/
export async function removeAiHistory(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
`${MODULES_API_URL}/ai/history/${id}`
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 批量添加AI审计历史记录表
*/
export async function addAiHistoryBatch(data: AiHistory[]) {
const res = await request.post<ApiResult<unknown>>(
`${MODULES_API_URL}/ai/history/batch`,
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 批量修改AI审计历史记录表
*/
export async function updateAiHistoryBatch(data: { list: AiHistory[] }) {
const res = await request.put<ApiResult<unknown>>(
`${MODULES_API_URL}/ai/history/batch`,
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 批量删除AI审计历史记录表
*/
export async function removeAiHistoryBatch(ids: number[]) {
const res = await request.delete<ApiResult<unknown>>(
`${MODULES_API_URL}/ai/history/batch`,
{ data: ids }
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}

View File

@@ -0,0 +1,41 @@
import type { PageParam } from '@/api';
/**
* AI审计历史记录表
*/
export interface AiHistory {
// 主键ID
id?: number;
// 请求哈希值
requestHash?: string;
// 接口名称
interfaceName?: string;
// 用户ID
userId?: number;
// 用户名
username?: string;
// 状态, 0正常, 1冻结
status?: number;
// 是否删除, 0否, 1是
deleted?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* AI审计历史记录表搜索条件
*/
export interface AiHistoryParam extends PageParam {
id?: number;
requestHash?: string;
interfaceName?: string;
userId?: number;
username?: string;
status?: number;
deleted?: number;
tenantId?: number;
}

View File

@@ -1,271 +1,286 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
width="90%"
width="85%"
:visible="visible"
:maskClosable="false"
title="历史记录"
:body-style="{ paddingBottom: '28px' }"
:body-style="{ paddingBottom: '20px' }"
@update:visible="updateVisible"
:footer="null"
>
<a-table
<ele-pro-table
ref="tableRef"
row-key="id"
:columns="columns"
:scroll="{ y: 500 }"
:pagination="false"
:datasource="datasource"
:scroll="{ x: 1000 }"
size="small"
tool-class="ele-toolbar-form"
class="compact-history-table"
bordered
:data-source="mockData"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'createTime'">
<span>{{ toDateString(record.createTime, 'MM-DD HH:mm') }}</span>
</template>
<template v-if="column.key === 'username'">
<span>{{ record.username || '-' }}</span>
</template>
<template v-if="column.key === 'interfaceName'">
<a-tag :color="getInterfaceColor(record.interfaceName)" size="small">
{{ getInterfaceName(record.interfaceName) }}
</a-tag>
</template>
<template v-if="column.key === 'dataCount'">
<a-tag v-if="record.dataCount > 0" color="blue" size="small">
{{ record.dataCount }}
</a-tag>
<span v-else class="text-gray"></span>
</template>
<template v-if="column.key === 'processingTime'">
<a-tag v-if="record.processingTime" color="green" size="small">
{{ formatProcessingTime(record.processingTime) }}
</a-tag>
<span v-else class="text-gray">-</span>
</template>
<template v-if="column.key === 'requestData'">
<a-tooltip :title="getRequestDataPreview(record)">
<a-button type="link" size="small" class="p-0 h-auto">
查看
</a-button>
</a-tooltip>
</template>
<template v-if="column.key === 'action'">
<span>{{ record.createTime || '-' }}</span>
</template>
<template v-if="column.key === 'testResult'">
<span class="text-green-400" v-if="record.testResult === '通过'"
>通过</span
<a-button
type="primary"
size="small"
@click="handleSelect(record)"
:disabled="!hasValidData(record)"
>
<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 v-if="column.key === 'goods'">
<span v-if="record.goods === '是'"></span>
<span v-else></span>
</template>
<template v-if="column.key === 'normal'">
<span v-if="record.normal === '是'"></span>
<span v-else></span>
</template>
<template v-if="column.key === 'bad'">
<span v-if="record.bad === '是'"></span>
<span v-else></span>
选择
</a-button>
</template>
</template>
</a-table>
</ele-pro-table>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue';
import dayjs from 'dayjs';
import { cloneDeep } from 'lodash-es';
const MOCK_ROW_COUNT = 5;
const TEST_RESULT_OPTIONS = ['通过', '不通过', '待检查'];
const SCORE_COLUMN_ORDER = ['goods', 'normal', 'bad'] as const;
import { type EleProTable, toDateString } from 'ele-admin-pro';
import type {
ColumnItem,
DatasourceFunction
} from 'ele-admin-pro/es/ele-pro-table/types';
import { pageAiHistory } from '@/api/ai/aiHistory';
import type { AiHistoryParam } from '@/api/ai/aiHistory/model';
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
item: any;
interfaceName?: string;
}>();
const emit = defineEmits<{
(e: 'update:visible', visible: boolean): void;
(e: 'select', record: any): void;
}>();
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
const emit = defineEmits<{
(e: 'update:visible', visible: boolean): void;
}>();
const datasource: DatasourceFunction = async ({ page, limit, orders }) => {
const params: AiHistoryParam = { page, limit };
const mockData = ref<any>([]);
if (orders) {
Object.assign(params, orders);
}
const columns = ref<any>([]);
// 使用传入的接口名称进行过滤
if (props.interfaceName) {
params.interfaceName = props.interfaceName;
}
const createDefaultColumns = () => [
try {
const result = await pageAiHistory(params);
const processedList = result.list.map(record => {
let dataCount = 0;
let processingTime = '';
try {
if (record.responseData) {
const responseData = JSON.parse(record.responseData);
if (responseData.data && Array.isArray(responseData.data)) {
dataCount = responseData.data.length;
} else if (responseData.data?.data && Array.isArray(responseData.data.data)) {
dataCount = responseData.data.data.length;
}
processingTime = responseData.processing_time || responseData.generated_time || '';
}
} catch (error) {
console.warn('解析响应数据失败:', error);
}
return { ...record, dataCount, processingTime };
});
return { total: result.total, list: processedList };
} catch (error) {
console.error('获取历史记录失败:', error);
return { total: 0, list: [] };
}
};
// 监听 visible 变化,当弹窗显示时刷新表格
watch(() => props.visible, (newVal) => {
if (newVal && tableRef.value) {
// 延迟一下确保表格已经渲染
setTimeout(() => {
tableRef.value?.reload();
}, 100);
}
});
const columns = ref<ColumnItem[]>([
{
title: '内容',
key: 'content',
dataIndex: 'content',
title: '序号',
key: 'index',
width: 50,
align: 'center',
customRender: ({ index }) => index + (tableRef.value?.tableIndex ?? 0)
},
{
title: '接口',
key: 'interfaceName',
width: 120,
align: 'center'
},
{
title: '生成时间',
key: 'action',
dataIndex: 'createTime',
title: '数据量',
key: 'dataCount',
width: 70,
align: 'center'
}
];
const buildCreateTimeColumn = () => ({
title: '生成时间',
key: 'action',
dataIndex: 'createTime',
fixed: 'right',
align: 'center'
});
const ensureColumnIdentifiers = (cols: any[]) => {
let seed = 0;
const traverse = (columnList: any[]) => {
columnList.forEach((column) => {
if (column.children && column.children.length) {
traverse(column.children);
} else {
if (!column.key) {
column.key = `column_key_${seed++}`;
}
if (!column.dataIndex) {
column.dataIndex = column.key || `column_data_${seed++}`;
}
}
});
};
traverse(cols);
};
const applyCreateTimeColumn = (cols: any[]) => {
const actionColumn = buildCreateTimeColumn();
if (!cols.length) {
cols.push(actionColumn);
return;
}
const lastIndex = cols.length - 1;
if (cols[lastIndex]?.key === 'action') {
cols[lastIndex] = actionColumn;
} else {
cols.push(actionColumn);
}
};
const buildHistoryColumns = (sourceColumns?: any[]) => {
const parsedColumns =
sourceColumns && sourceColumns.length
? cloneDeep(sourceColumns)
: createDefaultColumns();
ensureColumnIdentifiers(parsedColumns);
applyCreateTimeColumn(parsedColumns);
return parsedColumns;
};
interface ColumnLeaf {
key: string;
dataIndex: string;
title?: string;
}
const flattenLeafColumns = (
columnList: any[],
collector: ColumnLeaf[] = []
) => {
columnList.forEach((column) => {
if (column.children && column.children.length) {
flattenLeafColumns(column.children, collector);
} else {
collector.push({
key: column.key,
dataIndex: column.dataIndex,
title: column.title
});
}
});
return collector;
};
const formatCreateTime = (offset: number) =>
dayjs().subtract(offset, 'hour').format('YYYY-MM-DD HH:mm:ss');
const buildFileList = (rowIndex: number, prefix: string) => {
const count = (rowIndex % 2) + 1;
return Array.from({ length: count }, (_, fileIndex) => {
return `${prefix}${rowIndex + 1}-${fileIndex + 1}.docx`;
});
};
const getScoreFlag = (columnKey: string, rowIndex: number) => {
const targetIndex = SCORE_COLUMN_ORDER.indexOf(
columnKey as (typeof SCORE_COLUMN_ORDER)[number]
);
if (targetIndex === -1) {
return '否';
}
return rowIndex % SCORE_COLUMN_ORDER.length === targetIndex ? '是' : '否';
};
const buildMockCellValue = (
column: ColumnLeaf,
rowIndex: number,
columnIndex: number
) => {
const columnKey = column.key || column.dataIndex;
if (columnKey === 'index') {
return rowIndex + 1;
}
if (columnKey === 'testResult') {
return TEST_RESULT_OPTIONS[rowIndex % TEST_RESULT_OPTIONS.length];
}
if (columnKey === 'workPaperIndex') {
return buildFileList(rowIndex, '工作底稿');
}
if (columnKey === 'fileIndex') {
return buildFileList(rowIndex, '附件');
}
if (['goods', 'normal', 'bad'].includes(columnKey)) {
return getScoreFlag(columnKey, rowIndex);
}
const label = column.title || columnKey || `字段${columnIndex + 1}`;
return `${label || '字段'}示例${rowIndex + 1}`;
};
const generateMockData = (columnDefs: any[]) => {
const leafColumns = flattenLeafColumns(columnDefs).filter(
(column) => column.key !== 'action'
);
return Array.from({ length: MOCK_ROW_COUNT }, (_, rowIndex) => {
const record: Record<string, any> = {
key: `row-${rowIndex + 1}`,
createTime: formatCreateTime(rowIndex)
};
leafColumns.forEach((column, columnIndex) => {
record[column.dataIndex] = buildMockCellValue(
column,
rowIndex,
columnIndex
);
});
return record;
});
};
watch(
() => props.visible,
(visible) => {
if (!visible || !props.item) {
return;
}
const historyColumns = buildHistoryColumns(props.item.columns);
columns.value = historyColumns;
mockData.value = generateMockData(historyColumns);
},
{ immediate: true }
);
{
title: '耗时',
key: 'processingTime',
width: 80,
align: 'center'
},
{
title: '用户',
dataIndex: 'username',
key: 'username',
width: 80,
align: 'center'
},
{
title: '参数',
key: 'requestData',
width: 60,
align: 'center'
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
width: 110,
align: 'center',
sorter: true
},
{
title: '操作',
key: 'action',
width: 60,
align: 'center',
fixed: 'right'
}
]);
const getInterfaceName = (interfaceName: string) => {
const nameMap: Record<string, string> = {
'/api/ai/auditContent3/generateTripleOneTable': '三重一大',
'/api/ai/auditContent3/generateDecisionTable': '重大经济决策调查表'
};
return nameMap[interfaceName] || interfaceName.split('/').pop() || interfaceName;
};
const getInterfaceColor = (interfaceName: string) => {
const colorMap: Record<string, string> = {
'/api/ai/auditContent3/generateTripleOneTable': 'blue',
'/api/ai/auditContent3/generateDecisionTable': 'green'
};
return colorMap[interfaceName] || 'default';
};
const formatProcessingTime = (time: string) => {
if (time.includes('ms')) {
const ms = parseInt(time);
if (!isNaN(ms)) return `${(ms / 1000).toFixed(1)}s`;
}
if (time.includes('CST')) {
return new Date(time).toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit'
});
}
return time.length > 8 ? time.substring(0, 7) + '...' : time;
};
const getRequestDataPreview = (record: any) => {
try {
if (record.requestData) {
const requestData = JSON.parse(record.requestData);
const preview: string[] = [];
if (requestData.kbIds) preview.push(`知识库: ${requestData.kbIds}`);
if (requestData.libraryIds) preview.push(`项目库: ${requestData.libraryIds}`);
if (requestData.suggestion) preview.push(`要求: ${requestData.suggestion.substring(0, 20)}...`);
if (requestData.docList?.length > 0) preview.push(`文档: ${requestData.docList.length}`);
if (requestData.fileList?.length > 0) preview.push(`文件: ${requestData.fileList.length}`);
return preview.join('\n') || '无参数';
}
} catch (error) {
console.warn('解析请求数据失败:', error);
}
return '解析失败';
};
const hasValidData = (record: any) => {
try {
if (record.responseData) {
const responseData = JSON.parse(record.responseData);
const hasData = (responseData.data && Array.isArray(responseData.data) && responseData.data.length > 0) ||
(responseData.data?.data && Array.isArray(responseData.data.data) && responseData.data.data.length > 0);
return hasData && responseData.success !== false;
}
} catch (error) {
console.warn('检查数据有效性失败:', error);
}
return false;
};
const handleSelect = (record: any) => {
if (!hasValidData(record)) return;
emit('select', record);
};
</script>
<style scoped>
.text-gray {
color: rgba(0, 0, 0, 0.25);
}
.compact-history-table :deep(.ant-table-thead > tr > th) {
padding: 8px 4px;
}
.compact-history-table :deep(.ant-table-tbody > tr > td) {
padding: 8px 4px;
}
</style>

View File

@@ -237,13 +237,11 @@
</template>
<span class="ml-2"
>共生成
<span class="text-red-400 mx-1 font-bold">{{
item.count
}}</span>
<span class="text-red-400 mx-1 font-bold">{{ item.data ? item.data.length : 0 }}</span>
条数据</span
>
</div>
<a @click="openHistory(item)" class="cursor-pointer">历史记录</a>
<a @click="openHistory(index)" class="cursor-pointer">历史记录</a>
</div>
<a-table
v-if="item.mode === 'table'"
@@ -383,8 +381,8 @@
<FileModal ref="fileModal" :current-company-id="currentCompanyId" />
<HistoryModal
v-model:visible="showHistory"
:item="clickedItem"
v-if="clickedItem"
:interface-name="currentInterfaceName"
@select="handleHistorySelect"
/>
<!-- 文档选择弹窗 -->
@@ -468,6 +466,9 @@
// 文档管理相关变量
const currentCompanyId = ref<number>();
// 历史记录接口相关变量
const currentInterfaceName = ref('');
// 添加计算属性检查是否已生成三重一大数据
const hasTripleOneData = computed(() => {
const section = navigationItems.value[2];
@@ -869,16 +870,7 @@
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 || []
}));
const tableData = mapTripleOneData(result.data.data);
// 保存到三重一大数据
tripleOneTableData.value = tableData;
@@ -949,18 +941,7 @@
console.log('重大经济决策调查表接口返回结果:', result);
if (result.code === 0 && result.data && result.data.success) {
const tableData = result.data.data.map((item, index) => ({
key: index,
index: index + 1,
name: item.decisionItem || '',
meetingTime: item.meetingTime || '',
decisionAmount: item.decisionAmount || '',
procedure: item.procedure || '',
executionStatus: item.executionStatus || '否',
goods: item.executionEffect?.good || '否',
normal: item.executionEffect?.normal || '否',
bad: item.executionEffect?.bad || '否'
}));
const tableData = mapDecisionTableData(result.data.data);
console.log('生成的表格数据:', tableData);
@@ -1149,12 +1130,106 @@
}
);
// 数据映射工具函数
const mapTripleOneData = (data: any[]) => {
return 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 || []
}));
};
const mapDecisionTableData = (data: any[]) => {
return data.map((item, index) => ({
key: index,
index: index + 1,
name: item.decisionItem || item.name || '',
meetingTime: item.meetingTime || '',
decisionAmount: item.decisionAmount || '',
procedure: item.procedure || '',
executionStatus: item.executionStatus || '否',
goods: item.executionEffect?.good || item.goods || '否',
normal: item.executionEffect?.normal || item.normal || '否',
bad: item.executionEffect?.bad || item.bad || '否'
}));
};
// 应用历史记录数据到指定章节
const applyHistoryData = (sectionIndex: number, data: any[], dataType: 'tripleOne' | 'decisionTable') => {
const section = navigationItems.value[sectionIndex];
if (dataType === 'tripleOne') {
const tableData = mapTripleOneData(data);
section.data = tableData;
tripleOneTableData.value = tableData;
// 强制刷新当前显示的表格
if (table3Title.value === '三重一大') {
section.columns = table3Columns.columns0;
section.data = [...tableData];
}
} else {
const tableData = mapDecisionTableData(data);
section.data = tableData;
decisionTableData.value = tableData;
// 强制刷新当前显示的表格
if (table3Title.value === '重大经济决策调查表') {
section.columns = table3Columns.columns1;
section.data = [...tableData];
}
}
};
const showHistory = ref(false);
const clickedItem = ref();
const openHistory = (item) => {
clickedItem.value = item;
const openHistory = (sectionIndex: number) => {
// 重置接口名称
currentInterfaceName.value = '';
// 根据当前章节和表格类型确定接口名称
if (sectionIndex === 2) { // 审计内容3章节
if (table3Title.value === '三重一大') {
currentInterfaceName.value = '/api/ai/auditContent3/generateTripleOneTable';
} else if (table3Title.value === '重大经济决策调查表') {
currentInterfaceName.value = '/api/ai/auditContent3/generateDecisionTable';
}
}
showHistory.value = true;
};
// 历史记录选择处理方法
const handleHistorySelect = (record: any) => {
// 关闭历史记录弹窗
showHistory.value = false;
try {
const responseData = JSON.parse(record.responseData);
if (!responseData.success || !responseData.data) {
throw new Error('历史记录数据格式错误');
}
// 根据接口名称确定数据类型
if (record.interfaceName === '/api/ai/auditContent3/generateTripleOneTable') {
applyHistoryData(2, responseData.data, 'tripleOne');
message.success('已应用选择的三重一大历史记录');
} else if (record.interfaceName === '/api/ai/auditContent3/generateDecisionTable') {
applyHistoryData(2, responseData.data, 'decisionTable');
message.success('已应用选择的重大经济决策调查表历史记录');
} else {
message.warning('不支持的历史记录类型');
}
} catch (error) {
console.error('解析历史记录数据失败:', error);
message.error('历史记录数据解析失败');
}
};
</script>
<script lang="ts">