Compare commits
2 Commits
53a4f259c6
...
9b8e655f3d
| Author | SHA1 | Date | |
|---|---|---|---|
| 9b8e655f3d | |||
| 45f5bcf764 |
@ -74,3 +74,17 @@ export const questionnaireAnalyzeWithAIApi = async (params: any) => {
|
||||
return await post("/api/questionnaireAnswer/analyzeWithAI", params);
|
||||
};
|
||||
|
||||
/**
|
||||
* 保存问卷AI分析结果
|
||||
*/
|
||||
export const questionnaireAnalysisSaveApi = async (params: any) => {
|
||||
return await post("/api/questionnaireAnalysis/save", params);
|
||||
};
|
||||
|
||||
/**
|
||||
* 查询问卷AI分析结果(分页)
|
||||
*/
|
||||
export const questionnaireAnalysisFindPageApi = async (params: any) => {
|
||||
return await get("/api/questionnaireAnalysis/findPage", params);
|
||||
};
|
||||
|
||||
|
||||
@ -157,6 +157,13 @@
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/base/wj/wjEvaluate",
|
||||
"style": {
|
||||
"navigationBarTitleText": "AI分析",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/base/xk/index",
|
||||
"style": {
|
||||
|
||||
@ -284,28 +284,17 @@
|
||||
<text class="success-time">{{ formatTime(new Date()) }}</text>
|
||||
<text class="success-tip">您已成功提交问卷</text>
|
||||
|
||||
<!-- 添加AI分析按钮 -->
|
||||
<button v-if="!analysisResult && !analyzing" @click="analyzeWithAI" class="ai-analyze-btn">
|
||||
<!-- 查看AI分析报告按钮 -->
|
||||
<button v-if="showAIAnalysisButton" @click="viewAIAnalysisReport" class="ai-analyze-btn">
|
||||
<u-icon name="file-text" size="18" color="#fff" style="margin-right: 8rpx;"></u-icon>
|
||||
AI智能分析
|
||||
查看AI分析报告
|
||||
</button>
|
||||
|
||||
<!-- 分析中状态 -->
|
||||
<view v-if="analyzing" class="analyzing-container">
|
||||
<u-loading-icon mode="spinner" size="24" color="#409EFF"></u-loading-icon>
|
||||
<text class="analyzing-text">AI分析中,请稍候...</text>
|
||||
</view>
|
||||
|
||||
<!-- 分析结果 -->
|
||||
<view v-if="analysisResult" class="analysis-result">
|
||||
<view class="analysis-header">
|
||||
<u-icon name="file-text" size="20" color="#409EFF"></u-icon>
|
||||
<text class="analysis-title">AI分析报告</text>
|
||||
</view>
|
||||
<view class="analysis-content">
|
||||
<text class="analysis-text">{{ analysisResult }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 重新填写按钮(当允许重复提交时显示) -->
|
||||
<button v-if="allowResubmit" @click="handleResubmit" class="resubmit-btn">
|
||||
<u-icon name="reload" size="18" color="#fff" style="margin-right: 8rpx;"></u-icon>
|
||||
重新填写
|
||||
</button>
|
||||
|
||||
<button @click="goBack" class="back-btn">返回</button>
|
||||
</view>
|
||||
@ -327,6 +316,19 @@
|
||||
<u-icon name="info-circle" size="80" color="#409EFF"></u-icon>
|
||||
<text class="already-filled-title">已填写</text>
|
||||
<text class="already-filled-subtitle">您已经填写过此问卷</text>
|
||||
|
||||
<!-- 查看AI分析报告按钮 -->
|
||||
<button v-if="showAIAnalysisButton" @click="viewAIAnalysisReport" class="ai-analyze-btn">
|
||||
<u-icon name="file-text" size="18" color="#fff" style="margin-right: 8rpx;"></u-icon>
|
||||
查看AI分析报告
|
||||
</button>
|
||||
|
||||
<!-- 重新填写按钮(当允许重复提交时显示) -->
|
||||
<button v-if="allowResubmit" @click="handleResubmit" class="resubmit-btn">
|
||||
<u-icon name="reload" size="18" color="#fff" style="margin-right: 8rpx;"></u-icon>
|
||||
重新填写
|
||||
</button>
|
||||
|
||||
<button @click="goBack" class="back-btn">返回</button>
|
||||
</view>
|
||||
</view>
|
||||
@ -352,7 +354,7 @@ import {
|
||||
questionnaireQuestionFindPageApi,
|
||||
questionnaireAnswerSaveApi,
|
||||
questionnaireAnswerFindByUserApi,
|
||||
questionnaireAnalyzeWithAIApi
|
||||
questionnaireAnswerFindDetailApi
|
||||
} from '@/api/base/wjApi';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
import { attachmentUpload } from '@/api/system/upload';
|
||||
@ -394,10 +396,6 @@ const fileUploadData = reactive<Record<string, {
|
||||
// 压缩配置
|
||||
const compressConfig = ref(COMPRESS_PRESETS.high);
|
||||
|
||||
// AI分析相关
|
||||
const analyzing = ref(false);
|
||||
const analysisResult = ref('');
|
||||
|
||||
// 签名组件相关
|
||||
const signCompRef = ref<any>(null);
|
||||
const signTitle = ref<string>("问卷签名");
|
||||
@ -405,6 +403,14 @@ const sign_file = ref<string>("");
|
||||
const showSignature = ref<boolean>(false);
|
||||
const currentSignatureQuestionId = ref<string>(""); // 当前正在签名的题目ID
|
||||
|
||||
// 提交批次ID,用于查看AI分析报告
|
||||
const submitId = ref<string>('');
|
||||
|
||||
// 控制是否显示AI智能分析按钮
|
||||
const showAIAnalysisButton = ref(false);
|
||||
// 控制是否允许重复提交
|
||||
const allowResubmit = ref(false);
|
||||
|
||||
onLoad(async (params) => {
|
||||
options.value = params;
|
||||
openId.value = params?.openId || '';
|
||||
@ -445,6 +451,17 @@ const initializePage = async () => {
|
||||
const questionnaireResult = await questionnaireFindByIdApi({ id: questionnaireId.value });
|
||||
if (questionnaireResult && questionnaireResult.resultCode === 1) {
|
||||
questionnaireInfo.value = questionnaireResult.result || questionnaireResult.data;
|
||||
console.log('【问卷信息】questionnaireInfo.value:', questionnaireInfo.value);
|
||||
console.log('【allowAnalysis值】', questionnaireInfo.value?.allowAnalysis);
|
||||
// 判断是否显示AI智能分析按钮:如果 allowAnalysis = 1,则显示(兼容字符串和数字)
|
||||
const allowAnalysisValue = questionnaireInfo.value?.allowAnalysis;
|
||||
showAIAnalysisButton.value = allowAnalysisValue === 1 || allowAnalysisValue === '1';
|
||||
console.log('【是否显示AI按钮】showAIAnalysisButton.value:', showAIAnalysisButton.value);
|
||||
|
||||
// 判断是否允许重复提交:如果 allowResubmit = 1,则允许(兼容字符串和数字)
|
||||
const allowResubmitValue = questionnaireInfo.value?.allowResubmit;
|
||||
allowResubmit.value = allowResubmitValue === 1 || allowResubmitValue === '1';
|
||||
console.log('【是否允许重复提交】allowResubmit.value:', allowResubmit.value);
|
||||
} else {
|
||||
throw new Error('获取问卷信息失败');
|
||||
}
|
||||
@ -481,8 +498,8 @@ const initializePage = async () => {
|
||||
throw new Error('检查权限失败');
|
||||
}
|
||||
|
||||
// 4. 检查是否已填写
|
||||
if (questionnaireInfo.value.allowResubmit !== 1) {
|
||||
// 4. 检查是否已填写(如果不允许重复提交)
|
||||
if (!allowResubmit.value) {
|
||||
const answerResult = await questionnaireAnswerFindByUserApi({
|
||||
questionnaireId: questionnaireId.value
|
||||
});
|
||||
@ -577,6 +594,11 @@ const initializePage = async () => {
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// 6. 如果允许重复提交,尝试加载已填写的答案进行回显
|
||||
if (allowResubmit.value) {
|
||||
await loadExistingAnswers(allQuestions);
|
||||
}
|
||||
}
|
||||
|
||||
currentStep.value = 'fill';
|
||||
@ -1074,6 +1096,11 @@ const doSubmitQuestionnaire = async () => {
|
||||
const result = await questionnaireAnswerSaveApi(submitParams);
|
||||
|
||||
if (result && result.resultCode === 1) {
|
||||
// 保存 submitId(从返回结果中获取)
|
||||
const resultData = result.result || result.data || {};
|
||||
if (resultData.submitId) {
|
||||
submitId.value = resultData.submitId;
|
||||
}
|
||||
currentStep.value = 'success';
|
||||
} else {
|
||||
throw new Error(result?.message || '提交失败');
|
||||
@ -1102,70 +1129,117 @@ const formatTime = (time: string | Date) => {
|
||||
});
|
||||
};
|
||||
|
||||
// AI分析函数
|
||||
const analyzeWithAI = async () => {
|
||||
// 查看AI分析报告
|
||||
const viewAIAnalysisReport = () => {
|
||||
// 获取当前用户ID
|
||||
const currentUserId = userStore.userdata?.id || userStore.userdata?.userId || '';
|
||||
// 直接跳转到分析页面,由分析页面负责调用AI接口并保存结果
|
||||
uni.navigateTo({
|
||||
url: `/pages/base/wj/wjEvaluate?questionnaireId=${questionnaireId.value}&userId=${currentUserId}`
|
||||
});
|
||||
};
|
||||
|
||||
// 加载已填写的答案(用于回显)
|
||||
const loadExistingAnswers = async (allQuestions: any[]) => {
|
||||
try {
|
||||
analyzing.value = true;
|
||||
analysisResult.value = '';
|
||||
// 获取当前用户ID
|
||||
const currentUserId = userStore.userdata?.id || userStore.userdata?.userId || '';
|
||||
if (!currentUserId) {
|
||||
console.log('未找到用户ID,跳过加载已填写答案');
|
||||
return;
|
||||
}
|
||||
|
||||
// 构造发送给AI的问卷数据,包含所有分页的题目
|
||||
const allQuestions: any[] = [];
|
||||
pageList.value.forEach(page => {
|
||||
allQuestions.push(...page.questions);
|
||||
// 获取已填写的答案
|
||||
const answerResult = await questionnaireAnswerFindDetailApi({
|
||||
questionnaireId: questionnaireId.value,
|
||||
userId: currentUserId
|
||||
});
|
||||
|
||||
const questionnaireData = {
|
||||
questionnaireId: questionnaireId.value,
|
||||
title: questionnaireInfo.value?.title,
|
||||
description: questionnaireInfo.value?.description,
|
||||
questions: allQuestions.map((q: any) => {
|
||||
let answerValue = answers[q.id]?.value;
|
||||
if (answerResult && answerResult.resultCode === 1) {
|
||||
const answerList = answerResult.result || answerResult.data || [];
|
||||
if (answerList && answerList.length > 0) {
|
||||
console.log('找到已填写的答案,开始回显:', answerList);
|
||||
|
||||
// 处理不同类型的答案格式
|
||||
if (q.questionType === 'CHECKBOX' && Array.isArray(answerValue)) {
|
||||
answerValue = answerValue.join(',');
|
||||
} else if (q.questionType === 'FILE') {
|
||||
const uploadData = fileUploadData[q.id];
|
||||
const imageUrls = (uploadData?.imageList || []).filter(img => img.url).map(img => img.url);
|
||||
const fileUrls = (uploadData?.fileList || []).filter(file => file.url).map(file => file.url);
|
||||
answerValue = [...imageUrls, ...fileUrls].join(',');
|
||||
} else if (q.questionType === 'VIDEO') {
|
||||
const uploadData = fileUploadData[q.id];
|
||||
const videoUrls = (uploadData?.videoList || []).filter(video => video.url).map(video => video.url);
|
||||
answerValue = videoUrls.join(',');
|
||||
} else if (q.questionType === 'SIGNATURE') {
|
||||
answerValue = answerValue ? '[已签名]' : '[未签名]';
|
||||
}
|
||||
// 将答案按题目ID建立映射
|
||||
const answerMap = new Map<string, any>();
|
||||
answerList.forEach((answer: any) => {
|
||||
answerMap.set(answer.questionId, answer);
|
||||
});
|
||||
|
||||
// 获取分页名称和排序
|
||||
const pageName = q.pageName || '第一部分';
|
||||
const pageSortOrder = q.pageSortOrder !== undefined ? q.pageSortOrder : 0;
|
||||
// 回显答案到表单
|
||||
allQuestions.forEach((question: any) => {
|
||||
const existingAnswer = answerMap.get(question.id);
|
||||
if (existingAnswer && existingAnswer.answerContent) {
|
||||
const answerContent = existingAnswer.answerContent;
|
||||
|
||||
if (question.questionType === 'CHECKBOX') {
|
||||
// 多选框:逗号分隔的字符串转为数组
|
||||
if (answerContent) {
|
||||
answers[question.id].value = answerContent.split(',').map((v: string) => v.trim()).filter((v: string) => v);
|
||||
}
|
||||
} else if (question.questionType === 'FILE') {
|
||||
// 文件类型:解析URL字符串
|
||||
if (answerContent) {
|
||||
const urls = answerContent.split(',').map((v: string) => v.trim()).filter((v: string) => v);
|
||||
const imageUrls: string[] = [];
|
||||
const fileUrls: string[] = [];
|
||||
|
||||
urls.forEach((url: string) => {
|
||||
const lowerUrl = url.toLowerCase();
|
||||
// 判断是图片还是文档
|
||||
if (lowerUrl.match(/\.(jpg|jpeg|png|gif|bmp|webp)$/i)) {
|
||||
imageUrls.push(url);
|
||||
} else {
|
||||
fileUrls.push(url);
|
||||
}
|
||||
});
|
||||
|
||||
// 设置到文件上传数据
|
||||
if (fileUploadData[question.id]) {
|
||||
fileUploadData[question.id].imageList = imageUrls.map(url => ({ url, name: url.split('/').pop() || '' }));
|
||||
fileUploadData[question.id].fileList = fileUrls.map(url => ({ url, name: url.split('/').pop() || '' }));
|
||||
}
|
||||
}
|
||||
} else if (question.questionType === 'VIDEO') {
|
||||
// 视频类型:解析URL字符串
|
||||
if (answerContent) {
|
||||
const urls = answerContent.split(',').map((v: string) => v.trim()).filter((v: string) => v);
|
||||
if (fileUploadData[question.id]) {
|
||||
fileUploadData[question.id].videoList = urls.map(url => ({ url, name: url.split('/').pop() || '' }));
|
||||
}
|
||||
}
|
||||
} else if (question.questionType === 'SIGNATURE') {
|
||||
// 签名类型:直接使用base64字符串
|
||||
answers[question.id].value = answerContent;
|
||||
} else {
|
||||
// 其他类型(TEXT, TEXTAREA, NUMBER, DATE, RADIO, SELECT):直接赋值
|
||||
answers[question.id].value = answerContent;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
content: q.questionContent,
|
||||
type: q.questionType,
|
||||
answer: answerValue || '[未填写]',
|
||||
pageName: pageName,
|
||||
pageSortOrder: pageSortOrder
|
||||
};
|
||||
})
|
||||
};
|
||||
|
||||
// 调用后端AI分析接口
|
||||
const response = await questionnaireAnalyzeWithAIApi(questionnaireData);
|
||||
|
||||
if (response && response.resultCode === 1) {
|
||||
analysisResult.value = response.result || response.data || '分析完成';
|
||||
} else {
|
||||
throw new Error(response?.message || 'AI分析失败');
|
||||
// 如果有问卷级别的签名,也需要回显
|
||||
// 注意:问卷级别的签名可能不在答案列表中,需要单独处理
|
||||
// 这里暂时不处理,因为签名通常存储在单独的字段中
|
||||
|
||||
console.log('答案回显完成');
|
||||
} else {
|
||||
console.log('未找到已填写的答案');
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('AI分析失败:', error);
|
||||
uni.showToast({ title: error.message || 'AI分析失败,请稍后重试', icon: 'none', duration: 2000 });
|
||||
} finally {
|
||||
analyzing.value = false;
|
||||
console.error('加载已填写答案失败:', error);
|
||||
// 加载失败不影响主流程,继续显示空白表单
|
||||
}
|
||||
};
|
||||
|
||||
// 重新填写问卷
|
||||
const handleResubmit = async () => {
|
||||
// 重置状态,允许重新填写
|
||||
currentStep.value = 'fill';
|
||||
// 重新初始化页面(会自动加载并回显已填写的答案)
|
||||
await initializePage();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@ -1695,54 +1769,25 @@ const analyzeWithAI = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
.analyzing-container {
|
||||
.resubmit-btn {
|
||||
width: 320rpx;
|
||||
height: 80rpx;
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
color: white;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
border-radius: 40rpx;
|
||||
margin-bottom: 30rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 30rpx;
|
||||
padding: 30rpx;
|
||||
background: #f5f7fa;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 4rpx 12rpx rgba(245, 87, 108, 0.4);
|
||||
transition: all 0.3s;
|
||||
|
||||
.analyzing-text {
|
||||
margin-left: 20rpx;
|
||||
font-size: 28rpx;
|
||||
color: #409EFF;
|
||||
}
|
||||
}
|
||||
|
||||
.analysis-result {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 30rpx;
|
||||
text-align: left;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
max-width: 100%;
|
||||
|
||||
.analysis-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
padding-bottom: 16rpx;
|
||||
border-bottom: 2rpx solid #eee;
|
||||
|
||||
.analysis-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-left: 12rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.analysis-content {
|
||||
.analysis-text {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
line-height: 1.8;
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
&:active {
|
||||
transform: scale(0.96);
|
||||
box-shadow: 0 2rpx 8rpx rgba(245, 87, 108, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
1253
src/pages/base/wj/wjEvaluate.vue
Normal file
1253
src/pages/base/wj/wjEvaluate.vue
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user