- Complete survey management system with web interface - Question generation tools and prompts - Report generation and analysis capabilities - Docker configuration for deployment - Database initialization scripts 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
534 lines
22 KiB
JavaScript
534 lines
22 KiB
JavaScript
// Survey renderer - 使用JSON数据渲染页面
|
|
class SurveyRenderer {
|
|
constructor() {
|
|
this.data = null;
|
|
this.init();
|
|
}
|
|
|
|
async init() {
|
|
try {
|
|
// 从URL查询参数获取ID
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const id = urlParams.get('id');
|
|
|
|
if (!id) {
|
|
throw new Error('缺少报告ID参数');
|
|
}
|
|
|
|
// 调用本地API获取数据
|
|
const response = await fetch(`/api/report/${id}`, {
|
|
method: 'GET',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`API请求失败: ${response.status}`);
|
|
}
|
|
|
|
this.data = await response.json();
|
|
|
|
// 渲染页面
|
|
this.render();
|
|
} catch (error) {
|
|
console.error('加载数据失败:', error);
|
|
document.getElementById('content').innerHTML = `
|
|
<div style="text-align: center; padding: 50px; max-width: 600px; margin: 0 auto;">
|
|
<div style="background: #fff9f7; border-radius: 12px; padding: 30px; box-shadow: 0 4px 20px rgba(255, 126, 95, 0.1); border-left: 4px solid #ff7e5f;">
|
|
<h3 style="color: #ff7e5f; margin-bottom: 20px;">⚠️ 报告生成失败</h3>
|
|
<p style="color: #666; margin-bottom: 25px; line-height: 1.6;">${error.message}</p>
|
|
<div style="display: flex; gap: 15px; justify-content: center; flex-wrap: wrap;">
|
|
<a href="/" style="background: linear-gradient(135deg, #ff7e5f, #feb47b); color: white; padding: 12px 24px; border-radius: 8px; text-decoration: none; font-weight: 600; transition: transform 0.2s; box-shadow: 0 4px 15px rgba(255, 126, 95, 0.3);">
|
|
返回首页
|
|
</a>
|
|
<button onclick="checkRegenerationOptions()" style="background: linear-gradient(135deg, #4a90e2, #357abd); color: white; padding: 12px 24px; border-radius: 8px; border: none; font-weight: 600; cursor: pointer; transition: transform 0.2s; box-shadow: 0 4px 15px rgba(74, 144, 226, 0.3);">
|
|
检查可重新生成的报告
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div id="regeneration-options" style="margin-top: 20px; display: none;">
|
|
<!-- 重新生成选项将在这里显示 -->
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
render() {
|
|
// 设置固定页面标题
|
|
const fixedTitle = "尚逸基石学科能力测评报告";
|
|
document.getElementById('page-title').textContent = fixedTitle;
|
|
document.getElementById('report-title').textContent = fixedTitle;
|
|
|
|
if (this.data.footer && this.data.footer.copyright) {
|
|
document.getElementById('footer-text').textContent = this.data.footer.copyright;
|
|
}
|
|
|
|
// 获取report数据
|
|
const report = this.data.report;
|
|
|
|
// 渲染内容
|
|
const content = document.getElementById('content');
|
|
content.innerHTML = `
|
|
${this.renderOverview(this.data)}
|
|
${this.renderErrorAnalysis(report.errorAnalysis)}
|
|
${this.renderKnowledgeAnalysis(report.knowledgeAnalysis)}
|
|
${this.renderCognitiveAnalysis(report.cognitiveAnalysis)}
|
|
${this.renderLearningPlan(report.learningPlan)}
|
|
`;
|
|
|
|
// 渲染雷达图
|
|
this.renderRadarChart(report.radarData);
|
|
}
|
|
|
|
renderOverview(data) {
|
|
const report = data.report;
|
|
return `
|
|
<div class="overview-section">
|
|
<div class="overview-grid">
|
|
${this.renderStudentInfo(data.studentInfo)}
|
|
${this.renderCoreDashboard(report.summaryData)}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
renderStudentInfo(studentInfo) {
|
|
return `
|
|
<div class="student-info">
|
|
<h3>📋 学员信息概览</h3>
|
|
<div class="info-grid">
|
|
<div class="info-item">
|
|
<span class="info-label">姓名</span>
|
|
<span class="info-value">${studentInfo.name}</span>
|
|
</div>
|
|
<div class="info-item">
|
|
<span class="info-label">测评时间</span>
|
|
<span class="info-value">${studentInfo.testDate}</span>
|
|
</div>
|
|
<div class="info-item">
|
|
<span class="info-label">测评科目</span>
|
|
<span class="info-value">${studentInfo.subject}</span>
|
|
</div>
|
|
<div class="info-item">
|
|
<span class="info-label">学校</span>
|
|
<span class="info-value">${studentInfo.school}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
renderCoreDashboard(summaryData) {
|
|
return `
|
|
<div class="core-dashboard">
|
|
<h3>📊 核心数据看板</h3>
|
|
<div class="data-cards">
|
|
<div class="data-card">
|
|
<h4>总分/等级</h4>
|
|
<div class="value">${summaryData.totalScore}</div>
|
|
<div class="subtitle">${summaryData.level}</div>
|
|
</div>
|
|
<div class="data-card">
|
|
<h4>得分率</h4>
|
|
<div class="value">${summaryData.scoreRate}</div>
|
|
<div class="subtitle">${summaryData.scoreRateDescription}</div>
|
|
</div>
|
|
<div class="data-card">
|
|
<h4>群体位置</h4>
|
|
<div class="value">${summaryData.groupPosition}</div>
|
|
<div class="subtitle">${summaryData.groupPositionDescription}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="ai-summary">
|
|
<h4>专家点评</h4>
|
|
<p>${summaryData.summary}</p>
|
|
</div>
|
|
|
|
<div class="radar-chart-container">
|
|
<h4>五维能力雷达图</h4>
|
|
<div class="radar-chart">
|
|
<canvas id="radarChart" width="500" height="500"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
renderErrorAnalysis(errorAnalysis) {
|
|
return `
|
|
<div class="error-section">
|
|
<h2 class="section-title">${errorAnalysis.title}</h2>
|
|
|
|
<div class="error-list">
|
|
${errorAnalysis.errors.map(error => `
|
|
<div class="error-item">
|
|
<div class="error-header">
|
|
<div class="error-title">${error.questionNumber} ${error.questionTitle}</div>
|
|
<div class="error-type">${error.errorType}</div>
|
|
</div>
|
|
<div class="error-analysis">
|
|
<strong>原题核心考查点:</strong>${error.corePoint}<br>
|
|
<strong>学生错误选项:</strong>${error.wrongOption}<br>
|
|
<strong>正确选项:</strong>${error.correctOption}<br>
|
|
<strong>错题分析:</strong>${error.analysis}<br>
|
|
<strong>正确思路引导:</strong>${error.guidance}
|
|
</div>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
|
|
<h3 style="color: #2d5a3d; margin-top: 30px; margin-bottom: 15px;">${errorAnalysis.tagLibrary.title}</h3>
|
|
<div class="tag-library">
|
|
${errorAnalysis.tagLibrary.tags.map(tag => `
|
|
<div class="tag-item">
|
|
<h4>${tag.emoji} ${tag.name}</h4>
|
|
<p>${tag.description}</p>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
renderKnowledgeAnalysis(knowledgeAnalysis) {
|
|
return `
|
|
<div class="knowledge-section">
|
|
<h2 class="section-title">${knowledgeAnalysis.title}</h2>
|
|
|
|
<div class="chapter-grid">
|
|
${knowledgeAnalysis.chapters.map(chapter => `
|
|
<div class="chapter-card ${chapter.type === 'excellent' ? 'excellent' : chapter.type === 'good' ? 'good' : 'needs-work'}">
|
|
<h4>${chapter.title}</h4>
|
|
<div class="chapter-score">${chapter.score}</div>
|
|
<p class="chapter-description">${chapter.description}</p>
|
|
<p style="font-size: 12px; color: #666; margin-top: 5px;">${chapter.note}</p>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
renderCognitiveAnalysis(cognitiveAnalysis) {
|
|
return `
|
|
<div class="cognitive-section">
|
|
<h2 class="section-title">${cognitiveAnalysis.title}</h2>
|
|
|
|
<div class="cognitive-grid">
|
|
${cognitiveAnalysis.dimensions.map(dimension => `
|
|
<div class="cognitive-item">
|
|
<h4>${dimension.emoji} ${dimension.name}</h4>
|
|
<div class="cognitive-score">${dimension.score}</div>
|
|
<p class="cognitive-description">${dimension.description}</p>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
|
|
<div style="margin-top: 25px; padding: 20px; background: #fff8f3; border-radius: 8px; border-left: 4px solid #ff7e5f; border-top: 1px solid rgba(255, 126, 95, 0.2);">
|
|
<h4 style="color: #2d5a3d; margin-bottom: 10px; font-weight: 600;">${cognitiveAnalysis.behaviorInsight.title}</h4>
|
|
<p style="color: #8b5a2b; line-height: 1.6;">${cognitiveAnalysis.behaviorInsight.content}</p>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
renderLearningPlan(learningPlan) {
|
|
return `
|
|
<div class="learning-section">
|
|
<h2 class="section-title">${learningPlan.title}</h2>
|
|
|
|
<div class="plan-cards">
|
|
${learningPlan.plans.map(plan => `
|
|
<div class="plan-card ${plan.type}">
|
|
<h3>${plan.emoji} ${plan.title}</h3>
|
|
<p style="color: #666; margin-bottom: 15px;"><strong>目标:</strong>${plan.goal}</p>
|
|
|
|
${plan.sections.map(section => `
|
|
<h4>${section.title}</h4>
|
|
<ul>
|
|
${section.items.map(item => `<li>${item}</li>`).join('')}
|
|
</ul>
|
|
`).join('')}
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
|
|
<div class="parent-advice">
|
|
<h3>${learningPlan.parentAdvice.emoji} ${learningPlan.parentAdvice.title}</h3>
|
|
<ul>
|
|
${learningPlan.parentAdvice.advice.map(item => `<li>${item}</li>`).join('')}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
|
|
renderRadarChart(radarData) {
|
|
const canvas = document.getElementById('radarChart');
|
|
if (!canvas) return;
|
|
|
|
// 根据屏幕大小调整canvas尺寸
|
|
const containerWidth = canvas.parentElement.offsetWidth;
|
|
const maxSize = Math.min(containerWidth, 500);
|
|
const size = window.innerWidth < 768 ? Math.min(containerWidth, 300) : maxSize;
|
|
|
|
canvas.width = size;
|
|
canvas.height = size;
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
const centerX = canvas.width / 2;
|
|
const centerY = canvas.height / 2;
|
|
const radius = size * 0.32; // 响应式半径
|
|
|
|
// 绘制雷达图
|
|
function drawRadarChart() {
|
|
// 清空画布
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
|
// 绘制网格
|
|
ctx.strokeStyle = '#e0e0e0';
|
|
ctx.lineWidth = 1;
|
|
|
|
for (let i = 1; i <= 5; i++) {
|
|
ctx.beginPath();
|
|
const r = radius * i / 5;
|
|
for (let j = 0; j < 5; j++) {
|
|
const angle = (Math.PI * 2 / 5) * j - Math.PI / 2;
|
|
const x = centerX + r * Math.cos(angle);
|
|
const y = centerY + r * Math.sin(angle);
|
|
if (j === 0) {
|
|
ctx.moveTo(x, y);
|
|
} else {
|
|
ctx.lineTo(x, y);
|
|
}
|
|
}
|
|
ctx.closePath();
|
|
ctx.stroke();
|
|
}
|
|
|
|
// 绘制轴线
|
|
for (let i = 0; i < 5; i++) {
|
|
const angle = (Math.PI * 2 / 5) * i - Math.PI / 2;
|
|
ctx.beginPath();
|
|
ctx.moveTo(centerX, centerY);
|
|
ctx.lineTo(centerX + radius * Math.cos(angle), centerY + radius * Math.sin(angle));
|
|
ctx.stroke();
|
|
}
|
|
|
|
// 绘制数据区域
|
|
ctx.beginPath();
|
|
ctx.fillStyle = 'rgba(255, 126, 95, 0.25)';
|
|
ctx.strokeStyle = '#ff7e5f';
|
|
ctx.lineWidth = 2;
|
|
|
|
for (let i = 0; i < 5; i++) {
|
|
const angle = (Math.PI * 2 / 5) * i - Math.PI / 2;
|
|
const value = radarData.abilities[i].value / 100;
|
|
const x = centerX + radius * value * Math.cos(angle);
|
|
const y = centerY + radius * value * Math.sin(angle);
|
|
if (i === 0) {
|
|
ctx.moveTo(x, y);
|
|
} else {
|
|
ctx.lineTo(x, y);
|
|
}
|
|
}
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
|
|
// 绘制数据点
|
|
for (let i = 0; i < 5; i++) {
|
|
const angle = (Math.PI * 2 / 5) * i - Math.PI / 2;
|
|
const value = radarData.abilities[i].value / 100;
|
|
const x = centerX + radius * value * Math.cos(angle);
|
|
const y = centerY + radius * value * Math.sin(angle);
|
|
|
|
// 外圈橙色
|
|
ctx.beginPath();
|
|
ctx.fillStyle = '#ff7e5f';
|
|
ctx.arc(x, y, 5, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
// 内圈深绿
|
|
ctx.beginPath();
|
|
ctx.fillStyle = '#2d5a3d';
|
|
ctx.arc(x, y, 3, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
|
|
// 绘制标签 - 响应式字体大小
|
|
const fontSize = window.innerWidth < 768 ? 10 : 13;
|
|
const labelOffset = window.innerWidth < 768 ? 15 : 20;
|
|
const lineHeight = window.innerWidth < 768 ? 5 : 7;
|
|
|
|
ctx.fillStyle = '#333';
|
|
ctx.font = `${fontSize}px Microsoft YaHei`;
|
|
|
|
for (let i = 0; i < 5; i++) {
|
|
const angle = (Math.PI * 2 / 5) * i - Math.PI / 2;
|
|
const labelRadius = radius + labelOffset;
|
|
const x = centerX + labelRadius * Math.cos(angle);
|
|
const y = centerY + labelRadius * Math.sin(angle);
|
|
|
|
// 根据角度调整文本对齐方式
|
|
if (angle < -Math.PI / 2 || angle > Math.PI / 2) {
|
|
ctx.textAlign = 'right';
|
|
} else {
|
|
ctx.textAlign = 'left';
|
|
}
|
|
|
|
// 智能换行处理
|
|
const name = radarData.abilities[i].name;
|
|
if (name.includes('与')) {
|
|
const lines = name.split('与');
|
|
// 第一行:第一个词 + "与"
|
|
ctx.fillText(lines[0] + '与', x, y - lineHeight);
|
|
// 第二行:第二个词
|
|
ctx.fillText(lines[1], x, y + lineHeight);
|
|
} else {
|
|
// 如果没有"与",直接显示完整名称
|
|
ctx.fillText(name, x, y + 1);
|
|
}
|
|
}
|
|
|
|
// 绘制刻度标签
|
|
ctx.textAlign = 'center';
|
|
ctx.fillStyle = '#666';
|
|
const scaleFontSize = window.innerWidth < 768 ? 8 : 10;
|
|
ctx.font = `${scaleFontSize}px Microsoft YaHei`;
|
|
for (let i = 1; i <= 5; i++) {
|
|
const value = i * 20;
|
|
ctx.fillText(value, centerX - 12, centerY - radius * i / 5 + 3);
|
|
}
|
|
}
|
|
|
|
// 初始化雷达图
|
|
drawRadarChart();
|
|
|
|
// 响应式调整 - 重新绘制而不是缩放
|
|
window.addEventListener('resize', function() {
|
|
// 重新设置canvas尺寸并重绘
|
|
const containerWidth = canvas.parentElement.offsetWidth;
|
|
const maxSize = Math.min(containerWidth, 500);
|
|
const size = window.innerWidth < 768 ? Math.min(containerWidth, 300) : maxSize;
|
|
|
|
canvas.width = size;
|
|
canvas.height = size;
|
|
drawRadarChart();
|
|
});
|
|
|
|
// 触发初始调整
|
|
window.dispatchEvent(new Event('resize'));
|
|
}
|
|
}
|
|
|
|
// 检查可重新生成的报告
|
|
async function checkRegenerationOptions() {
|
|
const container = document.getElementById('regeneration-options');
|
|
|
|
try {
|
|
const response = await fetch('/api/sessions-can-regenerate', {
|
|
method: 'GET',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`请求失败: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success && data.sessions.length > 0) {
|
|
// 显示可重新生成的会话列表
|
|
container.style.display = 'block';
|
|
container.innerHTML = `
|
|
<div style="background: #f0f8ff; border-radius: 12px; padding: 25px; box-shadow: 0 4px 20px rgba(74, 144, 226, 0.1); border-left: 4px solid #4a90e2;">
|
|
<h4 style="color: #4a90e2; margin-bottom: 20px;">🔄 可重新生成的报告</h4>
|
|
<div style="display: grid; gap: 15px;">
|
|
${data.sessions.map(session => `
|
|
<div style="background: white; padding: 20px; border-radius: 8px; border: 1px solid #e3f2fd; box-shadow: 0 2px 8px rgba(0,0,0,0.05);">
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
|
<div>
|
|
<strong style="color: #333;">${session.name}</strong>
|
|
<span style="color: #666; margin-left: 10px; font-size: 14px;">${session.school} - ${session.grade}</span>
|
|
</div>
|
|
<button onclick="regenerateReport('${session.id}')" style="background: linear-gradient(135deg, #4a90e2, #357abd); color: white; padding: 8px 16px; border-radius: 6px; border: none; font-size: 14px; cursor: pointer; transition: transform 0.2s;">
|
|
重新生成
|
|
</button>
|
|
</div>
|
|
<div style="color: #888; font-size: 12px;">
|
|
创建时间: ${new Date(session.created_at).toLocaleString('zh-CN', {
|
|
timeZone: 'Asia/Shanghai',
|
|
year: 'numeric',
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
})}
|
|
</div>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
</div>
|
|
`;
|
|
} else {
|
|
// 没有可重新生成的报告
|
|
container.style.display = 'block';
|
|
container.innerHTML = `
|
|
<div style="background: #f9f9f9; border-radius: 12px; padding: 25px; text-align: center; border-left: 4px solid #ccc;">
|
|
<p style="color: #666;">暂无可重新生成的报告</p>
|
|
</div>
|
|
`;
|
|
}
|
|
} catch (error) {
|
|
container.style.display = 'block';
|
|
container.innerHTML = `
|
|
<div style="background: #fff9f7; border-radius: 12px; padding: 25px; text-align: center; border-left: 4px solid #ff7e5f;">
|
|
<p style="color: #ff7e5f;">检查可重新生成报告失败: ${error.message}</p>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
// 重新生成报告
|
|
async function regenerateReport(sessionId) {
|
|
try {
|
|
const response = await fetch('/api/regenerate-report', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
sessionId: sessionId
|
|
})
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
// 重新生成成功,跳转到报告页面
|
|
if (result.reportId) {
|
|
window.location.href = `/report.html?id=${result.reportId}`;
|
|
} else {
|
|
alert('报告重新生成成功!请前往报告列表查看。');
|
|
window.location.href = '/';
|
|
}
|
|
} else {
|
|
alert('重新生成失败: ' + result.message);
|
|
}
|
|
} catch (error) {
|
|
alert('重新生成失败: ' + error.message);
|
|
}
|
|
}
|
|
|
|
// 初始化渲染器
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
new SurveyRenderer();
|
|
});
|