survey/public/survey-renderer.js
朱潮 99796408cf Initial commit: Add survey system with enhanced features
- 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>
2025-10-28 20:28:57 +08:00

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();
});