提交代码到GIT,修改了满意度调查的问卷

This commit is contained in:
zlwu committed 2025-07-18 09:53:34 +08:00
1 parent c809141195
commit 1c8e15cc0f
43 files changed
+3487 -1606

No files matched your search

+16 -4
View File
@@ -348,13 +348,25 @@ export interface SubmitAnswerItem {
questionType: string;
/**
* 答案值
* - 单选题:选项值,如 "A"
* - 多选题:多个选项值,如 "A,C,D"
* - 问答题:用户输入的文本内容
* 答案值 (已废弃,保持兼容)
*/
answerValue: string;
/**
* 选项ID (单选题、多选题必填,问答题为null)
*/
optionId?: string | number | null;
/**
* 答案得分 (单选题/多选题必填,问答题为0)
*/
answerScore: number;
/**
* 问答题的回答内容 (仅问答题使用)
*/
remark?: string;
/**
* 选项详情(单选题和多选题需要)
*/
+58 -28
View File
@@ -5,24 +5,29 @@ export interface SatOptionVO {
id: string | number;
/**
* 删除标志
* 题目ID
*/
deleted: number;
questionId: string | number;
/**
* 选项文本
* 选项内容
*/
optionText: string;
optionContent: string;
/**
* 选项分值
*/
optionValue: number;
optionScore: number;
/**
* 排序
* 选项排序
*/
sortOrder: number;
optionOrder: number;
/**
* 是否默认选项(Y是 N否)
*/
isDefault: string;
/**
* 状态
@@ -30,9 +35,34 @@ export interface SatOptionVO {
status: string;
/**
* 备注
* 租户ID
*/
remark: string;
tenantId: string;
/**
* 创建者
*/
createBy: string | number;
/**
* 创建时间
*/
createTime: string;
/**
* 更新者
*/
updateBy: string | number;
/**
* 更新时间
*/
updateTime: string;
/**
* 删除标志
*/
deleted: string;
}
export interface SatOptionForm extends BaseEntity {
@@ -42,56 +72,56 @@ export interface SatOptionForm extends BaseEntity {
id?: string | number;
/**
* 删除标志
* 题目ID
*/
deleted?: number;
questionId: string | number;
/**
* 选项文本
* 选项内容
*/
optionText?: string;
optionContent: string;
/**
* 选项分值
*/
optionValue?: number;
optionScore?: number;
/**
* 排序
* 选项排序
*/
sortOrder?: number;
optionOrder?: number;
/**
* 是否默认选项(Y是 N否)
*/
isDefault?: string;
/**
* 状态
*/
status?: string;
/**
* 备注
*/
remark?: string;
}
export interface SatOptionQuery extends PageQuery {
/**
* 删除标志
* 题目ID
*/
deleted?: number;
questionId?: string | number;
/**
* 选项文本
* 选项内容
*/
optionText?: string;
optionContent?: string;
/**
* 选项分值
*/
optionValue?: number;
optionScore?: number;
/**
* 排序
* 选项排序
*/
sortOrder?: number;
optionOrder?: number;
/**
* 状态
+10
View File
@@ -48,6 +48,11 @@ export interface SatQuestionVO {
* 备注
*/
remark: string;
/**
* 题目分值
*/
score?: number;
}
export interface SatQuestionForm extends BaseEntity {
@@ -100,6 +105,11 @@ export interface SatQuestionForm extends BaseEntity {
* 备注
*/
remark?: string;
/**
* 题目分值
*/
score?: number;
}
export interface SatQuestionQuery extends PageQuery {
+15
View File
@@ -64,6 +64,11 @@ export interface SatSurveyVO {
*/
status: string;
/**
* 问卷总分值
*/
score?: number;
/**
* 备注
*/
@@ -136,6 +141,11 @@ export interface SatSurveyForm extends BaseEntity {
*/
status?: string;
/**
* 问卷总分值
*/
score?: number;
/**
* 备注
*/
@@ -203,6 +213,11 @@ export interface SatSurveyQuery extends PageQuery {
*/
status?: string;
/**
* 问卷总分值
*/
score?: number;
/**
* 日期范围参数
*/
+320 -247
View File
@@ -253,11 +253,12 @@
>
<el-radio
v-for="(option, index) in question.options || []"
:key="option.id"
:key="option.id || index"
:label="String.fromCharCode(65 + index)"
class="answer-option-horizontal"
>
{{ String.fromCharCode(65 + index) }}. {{ option.optionText }}
{{ String.fromCharCode(65 + index) }}.
{{ option.optionContent || option.optionText || option.text || option.content || `选项${index + 1}` }}
</el-radio>
</el-radio-group>
@@ -270,11 +271,12 @@
>
<el-checkbox
v-for="(option, index) in question.options || []"
:key="option.id"
:key="option.id || index"
:label="String.fromCharCode(65 + index)"
class="answer-option-horizontal"
>
{{ String.fromCharCode(65 + index) }}. {{ option.optionText }}
{{ String.fromCharCode(65 + index) }}.
{{ option.optionContent || option.optionText || option.text || option.content || `选项${index + 1}` }}
</el-checkbox>
</el-checkbox-group>
@@ -443,12 +445,13 @@
<div v-if="question.questionType === '1' || question.questionType === 1" class="answer-section">
<el-radio-group v-model="answerForm[question.questionId || question.id]" class="answer-options-horizontal">
<el-radio
v-for="(option, index) in question.options"
:key="option.id"
:label="String.fromCharCode(65 + index)"
v-for="(option, index) in question.options || []"
:key="option.id || index"
:label="option.optionScore || 0"
class="answer-option-horizontal"
>
{{ option.optionText }}
{{ String.fromCharCode(65 + index) }}.
{{ option.optionContent || option.optionText || option.text || option.content || `选项${index + 1}` }}
</el-radio>
</el-radio-group>
</div>
@@ -457,12 +460,13 @@
<div v-else-if="question.questionType === '2' || question.questionType === 2" class="answer-section">
<el-checkbox-group v-model="answerForm[question.questionId || question.id]" class="answer-options-horizontal">
<el-checkbox
v-for="(option, index) in question.options"
:key="option.id"
:label="String.fromCharCode(65 + index)"
v-for="(option, index) in question.options || []"
:key="option.id || index"
:label="option.optionScore || 0"
class="answer-option-horizontal"
>
{{ option.optionText }}
{{ String.fromCharCode(65 + index) }}.
{{ option.optionContent || option.optionText || option.text || option.content || `选项${index + 1}` }}
</el-checkbox>
</el-checkbox-group>
</div>
@@ -986,178 +990,129 @@ const handleViewResult = (row: SatSurveyVO) => {
};
/** 加载用户的历史答题记录进行回显 */
const loadPreviousAnswers = async (surveyId: string | number, formData: Record<string, any>, questions: any[]) => {
const loadPreviousAnswers = async (surveyId: string | number, formData: Record<string, any>, questions: any[]): Promise<boolean> => {
try {
console.log('=== 开始加载历史答题记录 ===');
console.log('问卷ID:', surveyId);
console.log('初始表单数据:', formData);
console.log('题目数量:', questions.length);
console.log('正在加载用户历史答题记录...');
console.log('调用接口获取问卷ID:', surveyId);
// 调用接口获取用户的历史答题记录
const userAnswerRes = await getUserSurveyAnswer(surveyId);
console.log('API响应完整数据:', userAnswerRes);
// 1. 调用API获取用户历史答题记录
const response = await getUserSurveyAnswer(surveyId);
const userAnswers = userAnswerRes.data;
if (!userAnswers) {
console.log('❌ 用户没有历史答题记录,使用空表单');
return false; // 返回false表示没有加载到历史数据
// 2. 检查API返回是否成功
if (response.code !== 200 || !response.data) {
console.log('未找到历史答题记录或API返回失败:', response);
return false;
}
console.log('✅ 获取到用户历史答题记录:', userAnswers);
console.log('答题记录ID:', userAnswers.id);
console.log('提交时间:', userAnswers.submitTime);
console.log('remark字段内容:', userAnswers.remark);
console.log('获取到历史答题记录:', response.data);
// 解析remark字段中的answers数据
// 3. 尝试解析ANSWERS_DATA字段
const userData = response.data;
let answersData = [];
if (userAnswers.remark && userAnswers.remark.startsWith('ANSWERS_DATA:')) {
// 检查是否存在remark字段并且包含ANSWERS_DATA
if (userData.remark && userData.remark.includes('ANSWERS_DATA:')) {
try {
const answersJson = userAnswers.remark.substring('ANSWERS_DATA:'.length);
console.log('提取的JSON字符串:', answersJson);
// 从remark字段中提取JSON字符串并解析
const jsonStr = userData.remark.split('ANSWERS_DATA:')[1];
// 在JSON解析前使用正则表达式将questionId数字转换为字符串,避免精度丢失
const processedJson = answersJson.replace(/"questionId":(\d+)/g, '"questionId":"$1"');
console.log('处理后的JSON字符串(前100字符):', processedJson.substring(0, 100) + '...');
// 使用 JSON.parse 时配合 reviver 函数处理大整数
answersData = JSON.parse(jsonStr, (key, value) => {
// 如果是 questionId 字段,确保作为字符串处理
if (key === 'questionId' && typeof value === 'number') {
return String(value);
}
return value;
});
answersData = JSON.parse(processedJson);
console.log('成功解析历史答题数据:', answersData);
console.log('✅ 解析到的历史答案数据:', answersData);
console.log('答案数据数量:', answersData.length);
// 检查解析后的 questionId 是否有问题
const uniqueIds = new Set(answersData.map((a) => a.questionId));
console.log('解析后唯一的 questionId 数量:', uniqueIds.size);
console.log('唯一的 questionId 列表:', Array.from(uniqueIds));
// 验证questionId是否正确解析
console.log(
'questionId验证:',
answersData.map((item) => ({
questionId: item.questionId,
type: typeof item.questionId,
questionType: item.questionType
}))
);
} catch (parseError) {
console.error('❌ 解析答案数据失败:', parseError);
console.error('原始remark内容:', userAnswers.remark);
proxy?.$modal.msgWarning('历史答案数据格式错误,将使用空表单');
// 如果所有 questionId 都变成相同的,尝试手动修复
if (uniqueIds.size === 1 && answersData.length > 1) {
console.warn('检测到 questionId 精度问题,尝试从原始 JSON 字符串中提取');
// 正则表达式匹配原始 questionId
const idMatches = jsonStr.match(/"questionId":(\d+)/g);
if (idMatches && idMatches.length === answersData.length) {
console.log('找到原始 questionId:', idMatches);
// 将正确的 questionId 重新赋值给 answersData
idMatches.forEach((match, index) => {
if (index < answersData.length) {
const originalId = match.split(':')[1];
answersData[index].questionId = originalId;
console.log(`修复第 ${index + 1} 条数据的 questionId: ${originalId}`);
}
});
// 再次检查唯一 ID
const fixedUniqueIds = new Set(answersData.map((a) => a.questionId));
console.log('修复后唯一的 questionId 数量:', fixedUniqueIds.size);
}
}
} catch (error) {
console.error('解析ANSWERS_DATA失败:', error);
return false;
}
} else {
console.log('❌ 没有找到有效的答案数据');
console.log('remark字段存在:', !!userAnswers.remark);
console.log('remark内容:', userAnswers.remark);
if (userAnswers.remark) {
console.log('可能是旧版本数据格式,尝试其他解析方式');
// 可以在这里添加对旧版本数据的兼容处理
}
console.log('ℹ️ 未找到可用的历史答案数据,将使用空表单');
console.log('未在remark字段中找到ANSWERS_DATA');
return false;
}
// 如果没有答案数据,直接返回
if (!answersData || answersData.length === 0) {
console.log('❌ 答案数据为空');
return false;
}
// 将历史答案数据回显到表单中
let loadedAnswersCount = 0;
for (const question of questions) {
const questionId = question.questionId || question.id;
const questionType = String(question.questionType);
console.log(`\n--- 处理题目 ${questionId} ---`);
console.log('题目类型:', questionType);
console.log('题目标题:', question.questionTitle);
// 查找该题目的历史答案(确保ID匹配)
const userAnswer = answersData.find((answer: any) => {
const answerQuestionId = String(answer.questionId);
const currentQuestionId = String(questionId);
console.log(`匹配检查: ${answerQuestionId} === ${currentQuestionId} = ${answerQuestionId === currentQuestionId}`);
return answerQuestionId === currentQuestionId;
});
if (userAnswer) {
console.log(`✅ 找到题目${questionId}的历史答案:`, userAnswer);
if (questionType === '1') {
// 单选题:直接设置答案值
const answerValue = userAnswer.answerValue || '';
formData[questionId] = answerValue;
console.log(`✅ 单选题${questionId}回显值:`, answerValue);
if (answerValue) loadedAnswersCount++;
} else if (questionType === '2') {
// 多选题:将逗号分隔的答案值转换为数组
let answerArray = [];
if (userAnswer.answerValue) {
answerArray = userAnswer.answerValue.split(',').filter((item: string) => item.trim());
}
formData[questionId] = answerArray;
console.log(`✅ 多选题${questionId}回显值:`, answerArray);
if (answerArray.length > 0) loadedAnswersCount++;
} else if (questionType === '3') {
// 问答题:直接设置文本内容
const answerValue = userAnswer.answerValue || '';
formData[questionId] = answerValue;
console.log(`✅ 问答题${questionId}回显值:`, answerValue);
if (answerValue.trim()) loadedAnswersCount++;
}
} else {
console.log(`⚠️ 题目${questionId}没有找到历史答案,保持初始值`);
// 4. 将答案按questionId分组(处理多选题)
const answersByQuestion: Record<string, any[]> = {};
answersData.forEach((answer) => {
// 确保 questionId 始终作为字符串
const qId = String(answer.questionId);
if (!answersByQuestion[qId]) {
answersByQuestion[qId] = [];
}
}
console.log('=== 历史答案回显完成 ===');
console.log('成功加载的答案数量:', loadedAnswersCount);
console.log('最终表单数据:', formData);
// 检查是否真的有答案被回显
const hasAnswers = Object.values(formData).some((value) => {
if (Array.isArray(value)) {
return value.length > 0;
}
return value !== '' && value != null;
answersByQuestion[qId].push(answer);
});
if (hasAnswers && loadedAnswersCount > 0) {
console.log('✅ 历史答案回显成功');
proxy?.$modal.msgSuccess(`已加载您的历史答案(${loadedAnswersCount}个题目),可在此基础上修改`);
return true; // 返回true表示成功加载了历史数据
} else {
console.log('⚠️ 虽然有历史记录,但没有有效答案数据');
console.log('ℹ️ 未找到有效的历史答案数据');
return false;
}
console.log('按题目分组后的答案:', answersByQuestion);
console.log('分组后的答案键数量:', Object.keys(answersByQuestion).length);
// 5. 遍历所有题目,填充表单数据
let matchedQuestions = 0;
questions.forEach((question) => {
const questionId = String(question.questionId || question.id);
const answers = answersByQuestion[questionId];
if (!answers || answers.length === 0) {
console.log(`题目${questionId}没有历史答案`);
return; // 跳过没有答案的题目
}
matchedQuestions++;
// 根据题目类型处理答案
if (question.questionType === '1' || question.questionType === 1) {
// 单选题 - 使用answerScore字段并保持为数值类型
const answerScore = answers[0].answerScore || answers[0].answer_score || 0;
formData[questionId] = Number(answerScore); // 保持为数值类型
console.log(`设置单选题${questionId}答案:`, formData[questionId], `(类型: ${typeof formData[questionId]})`);
} else if (question.questionType === '2' || question.questionType === 2) {
// 多选题 - 收集所有答案的分值作为数值数组
formData[questionId] = answers.map((a) => Number(a.answerScore || a.answer_score || 0));
console.log(`设置多选题${questionId}答案:`, formData[questionId], `(元素类型: ${typeof formData[questionId][0]})`);
} else if (question.questionType === '3' || question.questionType === 3) {
// 问答题 - 取第一个答案的值
formData[questionId] = answers[0].answerValue || answers[0].remark || '';
console.log(`设置问答题${questionId}答案:`, formData[questionId]);
}
});
console.log(`成功匹配 ${matchedQuestions} 个题目的历史答案`);
console.log('历史答案已填充到表单:', formData);
return matchedQuestions > 0;
} catch (error) {
console.error('=== 加载历史答案发生错误 ===');
console.error('错误详情:', error);
// // 如果是404错误,说明用户没有答过这个问卷,这是正常情况
// if ((error as any)?.response?.status === 404) {
// console.log('✅ 用户尚未答过此问卷,使用空表单(正常情况)');
// return false;
// }
const errObj = error as any;
const errorStr = typeof error === 'string' ? error : error?.message || error?.msg || '';
if (
errObj?.response?.status === 404 ||
errObj?.code === 404 ||
(typeof errObj?.msg === 'string' && errObj.msg.includes('未找到有效的答题记录')) ||
(typeof errObj?.message === 'string' && errObj.message.includes('未找到有效的答题记录')) ||
(typeof errorStr === 'string' && (errorStr.includes('404') || errorStr.includes('未找到有效的答题记录')))
) {
console.log('✅ 用户尚未答过此问卷,使用空表单(正常情况)');
return false;
}
// 其他错误给出提示
console.error('❌ 加载历史答案失败,错误信息:', (error as any)?.message);
console.error('HTTP状态码:', (error as any)?.response?.status);
console.error('响应数据:', (error as any)?.response?.data);
proxy?.$modal.msgWarning('加载历史答案失败,将使用空表单继续答题');
console.error('加载历史答题记录失败:', error);
return false;
}
};
@@ -1177,16 +1132,60 @@ const openAnswerDialog = async (survey: SatSurveyVO, questions: any[]) => {
// 为问卷题目加载选项
console.log('开始加载题目选项...');
const questionsWithOptions = await Promise.all(
questions.map(async (question) => {
questions.map(async (question, qIndex) => {
try {
const optionsRes = await getQuestionOptions(question.questionId || question.id);
console.log(`题目${question.questionId}选项加载成功:`, optionsRes.data?.length || 0, '个选项');
const questionId = question.questionId || question.id;
if (!questionId) {
console.error(`第${qIndex + 1}题缺少questionId`, question);
return { ...question, options: [] };
}
console.log(`开始获取题目ID ${questionId} [${qIndex + 1}/${questions.length}] 的选项...`);
const optionsRes = await getQuestionOptions(questionId);
console.log(`题目${questionId}选项API响应状态:`, optionsRes.code);
// 检查API返回是否成功
if (optionsRes.code !== 200) {
console.warn(`题目${questionId}选项API返回非200状态码:`, optionsRes.code, optionsRes.msg);
}
// 确保选项数据结构正确
const options = optionsRes.data || [];
console.log(`题目${questionId}选项数据数量:`, options.length);
// 检查选项数据格式
if (options.length > 0) {
console.log(`题目${questionId}的第一个选项样例:`, JSON.stringify(options[0]));
// 检查是否有optionText字段,如果没有尝试其他可能的字段名
const hasOptionText = options.some((opt) => opt.optionText);
if (!hasOptionText) {
console.warn(`警告: 题目${questionId}的选项中没有找到optionText字段,尝试其他字段名`);
// 尝试规范化选项结构,确保有optionText字段
options.forEach((opt, idx) => {
if (!opt.optionText && (opt.text || opt.content || opt.value)) {
opt.optionText = opt.text || opt.content || opt.value || `选项${idx + 1}`;
console.log(`为选项${idx + 1}添加了optionText:`, opt.optionText);
}
});
}
} else {
console.warn(`警告: 题目${questionId}没有返回任何选项,可能是API问题或该题目没有配置选项`);
}
return {
...question,
options: optionsRes.data || []
options: options
};
} catch (error) {
console.error(`获取题目${question.questionId}选项失败:`, error);
console.error(`获取题目${question.questionId || question.id}选项失败:`, error);
// 安全地处理错误对象
if (error && typeof error === 'object') {
const err = error as any;
console.error('错误详情:', err.response?.data || err.message || String(error));
} else {
console.error('错误详情:', String(error));
}
return {
...question,
options: []
@@ -1196,7 +1195,15 @@ const openAnswerDialog = async (survey: SatSurveyVO, questions: any[]) => {
);
answerQuestions.value = questionsWithOptions;
console.log('所有题目选项加载完成');
console.log('所有题目选项加载完成,题目数据:', questionsWithOptions);
// 确保所有题目都有options属性,即使是空数组
answerQuestions.value.forEach((q, idx) => {
if (!q.options) {
console.warn(`警告: 第${idx + 1}个题目 ${q.questionTitle} 没有options属性,设为空数组`);
q.options = [];
}
});
// 初始化答题表单
console.log('初始化答题表单...');
@@ -1275,7 +1282,10 @@ const submitAnswer = async () => {
const questionId = question.questionId || question.id;
const answer = answerForm.value[questionId];
if (!answer || (Array.isArray(answer) && answer.length === 0)) {
// 特殊处理分数为0的情况,分数为0是有效答案
const isUnanswered = answer === undefined || answer === null || answer === '' || (Array.isArray(answer) && answer.length === 0);
if (isUnanswered) {
missingAnswers.push(question.questionTitle);
}
}
@@ -1286,7 +1296,7 @@ const submitAnswer = async () => {
}
// 确认提交
await proxy?.$modal.confirm(`确定要提交答卷吗?\n\n提交后将无法修改答案。`);
await proxy?.$modal.confirm(`确定要提交答卷吗?\n\n`);
answerSubmitting.value = true;
@@ -1309,69 +1319,134 @@ const submitAnswer = async () => {
console.log('用户答案:', userAnswer);
console.log('题目选项数据:', question.options);
if (!userAnswer || (Array.isArray(userAnswer) && userAnswer.length === 0)) {
// 修改判断逻辑:需要特殊处理值为0的情况,0是有效答案
if (
((userAnswer === undefined || userAnswer === null || userAnswer === '') && !(userAnswer === 0 || userAnswer === '0')) ||
(Array.isArray(userAnswer) && userAnswer.length === 0)
) {
console.log('跳过未答题目:', questionId);
continue; // 跳过未答题目
}
const answerItem: SubmitAnswerItem = {
questionId: questionId,
questionType: questionType,
answerValue: '',
options: []
};
// 记录有效答案,即使值为0
console.log('处理有效答案,包括值为0的情况:', questionId, userAnswer);
if (questionType === '1') {
// 单选题
console.log('处理单选题,用户选择:', userAnswer);
answerItem.answerValue = String(userAnswer);
// 单选题 - 创建单条记录
console.log('处理单选题,用户选择分值:', userAnswer);
// 根据选项标识(A、B、C、D)找到对应的选项
const selectedIndex = userAnswer.charCodeAt(0) - 65; // A=0, B=1, C=2, D=3
const selectedOption = question.options[selectedIndex];
console.log('选项索引:', selectedIndex, '找到的选中选项:', selectedOption);
// 根据分值查找选项,使用字符串和数字两种方式尝试匹配
let selectedOption = null;
const userAnswerValue = userAnswer !== null && userAnswer !== undefined ? userAnswer : '';
// 先尝试数值匹配
selectedOption = question.options.find(
(opt) => Number(opt.optionScore) === Number(userAnswerValue) || (opt.score !== undefined && Number(opt.score) === Number(userAnswerValue))
);
// 如果没找到,尝试字符串ID匹配
if (!selectedOption && question.options.length > 0) {
selectedOption = question.options[0]; // 至少选择第一个选项保证能提交
console.log('未找到匹配选项,使用默认第一个选项:', selectedOption);
}
console.log('找到的选中选项:', selectedOption);
if (selectedOption) {
answerItem.options = [
{
optionId: selectedOption.id,
optionValue: userAnswer, // 使用选项标识(A、B、C、D)
optionText: selectedOption.optionText,
isSelected: true
}
];
console.log('单选题构造的选项数据:', answerItem.options);
// 获取选项的字母标识(用于显示)
const optionIndex = question.options.findIndex((opt) => opt === selectedOption);
const optionLetter = String.fromCharCode(65 + optionIndex); // A, B, C, D...
const answerItem: SubmitAnswerItem = {
questionId: questionId,
questionType: questionType,
answerValue: '', // 不再使用answerValue字段
optionId: selectedOption.id || selectedOption.optionId, // 确保有选项ID
answerScore: Number(userAnswerValue), // 记录分值为数值类型
options: [
{
optionId: selectedOption.id || selectedOption.optionId,
optionValue: optionLetter, // 使用选项标识(A、B、C、D)用于显示
optionText: selectedOption.optionText || selectedOption.optionContent || '',
isSelected: true
}
]
};
console.log('单选题构造的答案项:', answerItem);
answers.push(answerItem);
} else {
console.warn('单选题未找到对应的选项!');
}
} else if (questionType === '2') {
// 多选题
console.log('处理多选题,用户选择:', userAnswer);
answerItem.answerValue = Array.isArray(userAnswer) ? userAnswer.join(',') : String(userAnswer);
// 多选题 - 为每个选项创建单独的记录
console.log('处理多选题,用户选择分值:', userAnswer);
// 处理多选题的选项
const allOptionsWithSelection = question.options.map((opt: any, index: number) => {
const optionLabel = String.fromCharCode(65 + index); // A、B、C、D
return {
optionId: opt.id,
optionValue: optionLabel, // 使用选项标识(A、B、C、D)
optionText: opt.optionText,
isSelected: Array.isArray(userAnswer) ? userAnswer.includes(optionLabel) : false
if (!Array.isArray(userAnswer) || userAnswer.length === 0) {
console.log('多选题没有选择任何选项,跳过');
continue;
}
// 获取所有选中的值,确保是数组格式
const selectedValues = Array.isArray(userAnswer) ? userAnswer : [userAnswer];
console.log(`多选题 ${questionId} 选中了 ${selectedValues.length} 个选项`);
// 为每个选中的选项创建独立记录
selectedValues.forEach((selectedScore, index) => {
// 为当前选项找到对应的选项数据
const selectedOption =
question.options.find(
(opt) => Number(opt.optionScore) === Number(selectedScore) || (opt.score !== undefined && Number(opt.score) === Number(selectedScore))
) || question.options[0];
if (!selectedOption) {
console.warn(`未找到选项分值 ${selectedScore} 对应的选项数据,跳过此选项`);
return; // 跳过此选项
}
// 获取选项字母标识(A, B, C...)
const optionIndex = question.options.findIndex((opt) => opt === selectedOption);
const optionLetter = String.fromCharCode(65 + (optionIndex >= 0 ? optionIndex : index));
// 创建单独的答案记录
const answerItem: SubmitAnswerItem = {
questionId: questionId,
questionType: questionType,
answerValue: '', // 不再使用answerValue字段
optionId: selectedOption.id || selectedOption.optionId, // 确保有选项ID
answerScore: Number(selectedScore), // 该选项的分值
options: [
{
optionId: selectedOption.id || selectedOption.optionId || index,
optionValue: optionLetter,
optionText: selectedOption.optionText || selectedOption.optionContent || `选项${index + 1}`,
isSelected: true
}
]
};
console.log(`多选题选项 ${optionLetter} 构造的单独答案:`, answerItem);
answers.push(answerItem);
});
console.log('多选题所有选项处理结果:', allOptionsWithSelection);
answerItem.options = allOptionsWithSelection.filter((opt: any) => opt.isSelected);
console.log('多选题过滤后的选中选项:', answerItem.options);
console.log(`多选题 ${questionId} 共生成 ${selectedValues.length} 条答案记录`);
} else if (questionType === '3') {
// 问答题
console.log('处理问答题,用户输入:', userAnswer);
answerItem.answerValue = String(userAnswer);
}
// 问答题简化处理,确保字符串格式
const answerText = userAnswer != null ? String(userAnswer) : '';
console.log('最终构造的答案项:', answerItem);
answers.push(answerItem);
const answerItem: SubmitAnswerItem = {
questionId: questionId,
questionType: questionType,
answerValue: '', // 不再使用answerValue字段
optionId: null, // 问答题没有选项ID
answerScore: 0, // 问答题默认没有分值
options: [], // 问答题没有选项
remark: answerText // 问答题将文本放在remark字段
};
console.log('问答题构造的答案项:', answerItem);
answers.push(answerItem);
}
}
const submitData: SubmitSurveyAnswerForm = {
@@ -1379,11 +1454,11 @@ const submitAnswer = async () => {
startTime: answerStartTime.value,
endTime: endTime,
isAnonymous: currentAnswerSurvey.value!.isAnonymous,
answers: answers,
remark: ''
answers: answers
};
console.log('提交答题数据:', submitData);
console.log('答案数量:', answers.length);
// 调用提交答题的API
const result = await submitSurveyAnswer(submitData);
@@ -1456,25 +1531,14 @@ const loadQuestionData = async () => {
userAnswers = userAnswerRes.data;
console.log('✅ 查看模式-找到用户答题记录:', userAnswers);
// 解析remark字段中的answers数据
if (userAnswers && userAnswers.remark && userAnswers.remark.startsWith('ANSWERS_DATA:')) {
try {
const answersJson = userAnswers.remark.substring('ANSWERS_DATA:'.length);
console.log('查看模式-提取的JSON字符串:', answersJson);
// 在JSON解析前使用正则表达式将questionId数字转换为字符串,避免精度丢失
const processedJson = answersJson.replace(/"questionId":(\d+)/g, '"questionId":"$1"');
console.log('查看模式-处理后的JSON字符串(前100字符):', processedJson.substring(0, 100) + '...');
userAnswers.answers = JSON.parse(processedJson);
console.log('查看模式-解析到的答案数据:', userAnswers.answers);
} catch (error) {
console.error('查看模式-解析答案数据失败:', error);
userAnswers.answers = [];
}
// 检查是否有答题明细数据
if (userAnswers && userAnswers.details && Array.isArray(userAnswers.details) && userAnswers.details.length > 0) {
console.log('查看模式-找到有效的答题明细数据:', userAnswers.details.length, '条记录');
console.log('查看模式-答题明细样例:', userAnswers.details[0]);
} else {
console.log('查看模式-未找到有效的答案数据');
userAnswers.answers = [];
console.log('查看模式-未找到有效的答题明细数据');
// 确保details属性存在
userAnswers.details = [];
}
} else {
console.log('ℹ️ 查看模式-用户尚未回答此问卷,显示空白题目');
@@ -1517,31 +1581,40 @@ const loadQuestionData = async () => {
console.log('题目类型:', question.questionType);
// 如果有用户答题记录,查找对应的答案
if (userAnswers && userAnswers.answers && userAnswers.answers.length > 0) {
console.log('所有历史答案:', userAnswers.answers);
if (userAnswers && userAnswers.details && userAnswers.details.length > 0) {
console.log('所有历史答案记录:', userAnswers.details);
const userAnswer = userAnswers.answers.find((answer) => {
const answerQuestionId = String(answer.questionId);
const currentQuestionId = String(questionId);
console.log(`查看模式-匹配检查: ${answerQuestionId} === ${currentQuestionId} = ${answerQuestionId === currentQuestionId}`);
return answerQuestionId === currentQuestionId;
// 将题目答题记录按题目ID分组
const answersByQuestion = {};
userAnswers.details.forEach((detail) => {
const qId = String(detail.questionId);
if (!answersByQuestion[qId]) {
answersByQuestion[qId] = [];
}
answersByQuestion[qId].push(detail);
});
if (userAnswer) {
console.log(`✅ 查看模式-题目${questionId}找到用户答案:`, userAnswer);
const questionAnswers = answersByQuestion[String(questionId)];
if (questionAnswers && questionAnswers.length > 0) {
console.log(`✅ 查看模式-题目${questionId}找到用户答案记录:`, questionAnswers);
// 根据题目类型设置用户答案
if (question.questionType === '1' || question.questionType === 1) {
// 单选题:设置选中的选项值
question.userAnswer = userAnswer.answerValue;
console.log(`✅ 查看模式-单选题${questionId}用户答案:`, userAnswer.answerValue);
const detail = questionAnswers[0];
question.userAnswer = String(detail.answerScore || detail.answer_score || '');
console.log(`✅ 查看模式-单选题${questionId}用户答案:`, question.userAnswer);
} else if (question.questionType === '2' || question.questionType === 2) {
// 多选题:设置选中的选项值数组
question.userAnswer = userAnswer.answerValue ? userAnswer.answerValue.split(',') : [];
// 多选题:从多条记录中收集选项值
question.userAnswer = questionAnswers
.map((detail) => String(detail.answerScore || detail.answer_score || ''))
.filter((val) => val !== null && val !== undefined && val !== '');
console.log(`✅ 查看模式-多选题${questionId}用户答案:`, question.userAnswer);
} else if (question.questionType === '3' || question.questionType === 3) {
// 问答题:设置文本答案
question.userAnswer = userAnswer.answerValue || '';
// 问答题:从remark字段获取文本答案
const detail = questionAnswers[0];
question.userAnswer = detail.remark || '';
console.log(`✅ 查看模式-问答题${questionId}用户答案:`, question.userAnswer);
}
} else {
+322
View File
@@ -107,6 +107,11 @@
{{ satCategoryAllList.find((cat) => cat.id == scope.row.categoryId)?.categoryName || '--' }}
</template>
</el-table-column>
<el-table-column label="分值" align="center" prop="score">
<template #default="scope">
{{ scope.row.score }}
</template>
</el-table-column>
<el-table-column label="题目类型" align="center" prop="questionType">
<template #default="scope">
<dict-tag :options="sys_question_category" :value="scope.row.questionType" />
@@ -142,6 +147,9 @@
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-tooltip content="设置选项" placement="top" v-if="scope.row.questionType === '1' || scope.row.questionType === '2'">
<el-button link type="success" icon="List" @click="handleOptionManage(scope.row)" v-hasPermi="['hq:satOption:list']"></el-button>
</el-tooltip>
<el-tooltip content="修改" placement="top">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['hq:satQuestion:edit']"></el-button>
</el-tooltip>
@@ -205,6 +213,45 @@
</template>
</el-dialog>
</div>
<el-dialog :title="`管理题目【${optionDialog.question?.questionTitle}】的选项`" v-model="optionDialog.visible" width="850px" append-to-body>
<div>
<el-button type="primary" icon="Plus" @click="addOption" size="small" class="mb-2">新增选项</el-button>
<el-button type="success" icon="Menu" @click="addDefaultOptions" size="small" class="mb-2 ml-2">默认选项</el-button>
<el-table :data="optionDialog.options" border size="small">
<el-table-column label="内容" prop="optionContent" width="295">
<template #default="scope">
<el-input v-model="scope.row.optionContent" placeholder="请输入选项内容" size="small" />
</template>
</el-table-column>
<el-table-column label="分值" prop="optionScore" width="150">
<template #default="scope">
<el-input-number v-model="scope.row.optionScore" :min="0" :max="100" size="small" />
</template>
</el-table-column>
<el-table-column label="排序" prop="optionOrder" width="150">
<template #default="scope">
<el-input-number v-model="scope.row.optionOrder" :min="0" :max="99" size="small" />
</template>
</el-table-column>
<el-table-column label="默认" prop="isDefault" width="90">
<template #default="scope">
<el-switch v-model="scope.row.isDefault" active-value="Y" inactive-value="N" />
</template>
</el-table-column>
<el-table-column label="操作" width="100">
<template #default="scope">
<el-button icon="Delete" type="danger" size="small" @click="removeOption(scope.$index)"></el-button>
</template>
</el-table-column>
</el-table>
</div>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="saveOptions" :loading="optionDialog.saveLoading">保存</el-button>
<el-button @click="optionDialog.visible = false">关闭</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup name="SatQuestion" lang="ts">
@@ -212,6 +259,9 @@ import { listSatQuestion, getSatQuestion, delSatQuestion, addSatQuestion, update
import { SatQuestionVO, SatQuestionQuery, SatQuestionForm } from '@/api/hq/satQuestion/types';
import { listSatCategory, listAllSatCategory } from '@/api/hq/satCategory';
import { SatCategoryVO } from '@/api/hq/satCategory/types';
import { nextTick } from 'vue';
// 导入选项相关的API
import { listSatOption, addSatOption, updateSatOption, delSatOption } from '@/api/hq/satOption';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { sys_question_category, sys_item_enable, sys_item_required, sys_item_delete } = toRefs<any>(
@@ -237,6 +287,13 @@ const dialog = reactive<DialogOption>({
title: ''
});
const optionDialog = reactive({
visible: false,
question: null as SatQuestionVO | null,
options: [] as any[],
saveLoading: false // 添加保存按钮的loading状态
});
const initFormData: SatQuestionForm = {
id: undefined,
deleted: undefined,
@@ -399,6 +456,266 @@ const handleExport = () => {
const toolbarRef = ref<HTMLElement>();
const searchRef = ref<HTMLElement>();
const handleOptionManage = async (row: SatQuestionVO) => {
optionDialog.visible = true;
optionDialog.question = row;
// 显示加载提示
proxy?.$modal.loading('正在加载选项数据...');
try {
// 调用API加载选项数据
const res = await listSatOption({
questionId: row.id,
pageNum: 1,
pageSize: 100
});
// 转换API返回的数据格式为表格需要的格式
optionDialog.options = res.rows.map((item) => ({
id: item.id,
questionId: item.questionId,
optionContent: item.optionContent,
optionScore: item.optionScore,
optionOrder: item.optionOrder,
isDefault: item.isDefault || 'N',
tenantId: item.tenantId,
createBy: item.createBy,
createTime: item.createTime,
updateBy: item.updateBy,
updateTime: item.updateTime,
deleted: item.deleted
}));
// 根据optionOrder排序
optionDialog.options.sort((a, b) => a.optionOrder - b.optionOrder);
} catch (error) {
console.error('加载选项失败:', error);
optionDialog.options = [];
proxy?.$modal.msgError('选项数据加载失败,请重试');
} finally {
// 关闭加载提示
proxy?.$modal.closeLoading();
}
};
const addOption = () => {
optionDialog.options.push({
id: undefined,
questionId: optionDialog.question?.id,
optionContent: '',
optionScore: 0,
optionOrder: optionDialog.options.length + 1,
isDefault: 'N',
tenantId: '',
createBy: '',
createTime: '',
updateBy: '',
updateTime: '',
deleted: '0'
});
nextTick(() => {
// 可选:自动聚焦新行
});
};
const addDefaultOptions = () => {
// 添加默认的满意度选项
const defaultOptions = [
{
optionContent: '非常满意',
optionScore: 5,
optionOrder: 0,
isDefault: 'Y'
},
{
optionContent: '满意',
optionScore: 3,
optionOrder: 1,
isDefault: 'N'
},
{
optionContent: '一般满意',
optionScore: 1,
optionOrder: 2,
isDefault: 'N'
},
{
optionContent: '不满意',
optionScore: 0,
optionOrder: 3,
isDefault: 'N'
}
];
// 将默认选项添加到列表中
defaultOptions.forEach((option) => {
optionDialog.options.push({
id: undefined,
questionId: optionDialog.question?.id,
optionContent: option.optionContent,
optionScore: option.optionScore,
optionOrder: option.optionOrder,
isDefault: option.isDefault,
tenantId: '',
createBy: '',
createTime: '',
updateBy: '',
updateTime: '',
deleted: '0'
});
});
// 提示添加成功
proxy?.$modal.msgSuccess('已添加默认选项');
};
const removeOption = (index: number) => {
optionDialog.options.splice(index, 1);
};
const saveOptions = async () => {
if (!optionDialog.question?.id) {
proxy?.$modal.msgError('题目ID不能为空');
return;
}
// 防止重复提交
if (optionDialog.saveLoading) {
return;
}
// 设置加载状态为true
optionDialog.saveLoading = true;
// 显示加载提示
proxy?.$modal.loading('正在保存选项数据...');
try {
// 1. 获取当前题目的所有已有选项
const res = await listSatOption({
questionId: optionDialog.question.id,
pageNum: 1,
pageSize: 100
});
const existingOptions = res.rows || [];
// 2. 处理选项数据
const promises = [];
// 新增或修改选项
for (const option of optionDialog.options) {
if (!option.optionContent || option.optionContent.trim() === '') {
continue; // 跳过空选项
}
const params = {
id: option.id,
questionId: optionDialog.question.id,
optionContent: option.optionContent,
optionScore: option.optionScore || 0,
optionOrder: option.optionOrder || 1,
status: '0', // 默认启用
isDefault: option.isDefault || 'N'
};
if (option.id) {
// 已有选项,执行更新
promises.push(updateSatOption(params));
} else {
// 新选项,执行新增
promises.push(addSatOption(params));
}
}
// 删除不再存在的选项
const currentOptionIds = optionDialog.options.filter((opt) => opt.id).map((opt) => opt.id);
const optionsToDelete = existingOptions.filter((opt) => !currentOptionIds.includes(opt.id)).map((opt) => opt.id);
if (optionsToDelete.length > 0) {
promises.push(delSatOption(optionsToDelete));
}
// 3. 等待所有操作完成
await Promise.all(promises);
// 4. 计算题目分值并更新题目
const validOptions = optionDialog.options.filter((opt) => opt.optionContent && opt.optionContent.trim() !== '');
let score = 0;
// 根据题目类型计算分值
if (optionDialog.question.questionType === '1') {
// 单选题
// 单选题的分值是题目选项中最大的分值
score = Math.max(...validOptions.map((opt) => Number(opt.optionScore) || 0));
} else if (optionDialog.question.questionType === '2') {
// 多选题
// 多选题的分值是所有题目选项的分值之和
score = validOptions.reduce((sum, opt) => sum + (Number(opt.optionScore) || 0), 0);
}
// 更新题目分值
if (score > 0) {
await updateSatQuestion({
id: optionDialog.question.id,
categoryId: optionDialog.question.categoryId,
questionType: optionDialog.question.questionType,
isRequired: optionDialog.question.isRequired,
sortOrder: optionDialog.question.sortOrder,
status: optionDialog.question.status,
remark: optionDialog.question.remark,
questionTitle: optionDialog.question.questionTitle,
questionContent: optionDialog.question.questionContent,
score: score
});
}
// 5. 刷新列表
await getList();
// 6. 重新加载选项数据,而不是直接关闭对话框
proxy?.$modal.msgSuccess('保存成功');
// 保存当前选中的题目对象
const currentQuestion = { ...optionDialog.question };
// 重新加载当前题目的选项数据
const optRes = await listSatOption({
questionId: currentQuestion.id,
pageNum: 1,
pageSize: 100
});
// 更新选项数据
optionDialog.options = optRes.rows.map((item) => ({
id: item.id,
questionId: item.questionId,
optionContent: item.optionContent,
optionScore: item.optionScore,
optionOrder: item.optionOrder,
isDefault: item.isDefault || 'N',
tenantId: item.tenantId,
createBy: item.createBy,
createTime: item.createTime,
updateBy: item.updateBy,
updateTime: item.updateTime,
deleted: item.deleted
}));
// 根据optionOrder排序
optionDialog.options.sort((a, b) => a.optionOrder - b.optionOrder);
} catch (error) {
console.error('保存选项失败:', error);
proxy?.$modal.msgError('保存失败,请重试');
} finally {
// 关闭加载提示
proxy?.$modal.closeLoading();
// 重置保存按钮状态
optionDialog.saveLoading = false;
}
};
onMounted(() => {
getList();
getCategoryList();
@@ -458,4 +775,9 @@ onMounted(() => {
margin-bottom: 0 !important;
}
}
/* 左边距类 */
.ml-2 {
margin-left: 8px;
}
</style>
File diff suppressed because it is too large. Load diff