Files

424 lines
14 KiB
HTML
Raw Permalink Normal View History

2026-06-24 13:11:34 +08:00
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>填写问卷 — 天恒高考志愿门户</title>
<link rel="stylesheet" href="/css/style.css">
<style>
/* 自动保存状态指示器 */
.auto-save-indicator {
text-align: center;
margin-top: 16px;
font-size: 12px;
color: var(--text-muted);
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
transition: opacity 0.3s;
}
.auto-save-indicator .save-dot {
width: 8px; height: 8px;
border-radius: 50%;
background: var(--border);
transition: background 0.3s;
}
.auto-save-indicator.saving .save-dot { background: var(--amber); animation: pulse 0.6s infinite; }
.auto-save-indicator.saved .save-dot { background: var(--green); }
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
</style>
</head>
<body>
<div class="container">
<div id="app">
<!-- 加载中 -->
<div id="loading" style="text-align:center;padding:60px 0;color:var(--text-muted);">
<p>加载问卷中...</p>
</div>
</div>
</div>
<script>
// ============================================================
// 问卷填写页 — 从 API 加载问卷,渲染表单,提交答案
// 含 localStorage 自动保存功能
// ============================================================
// 从 URL 参数获取问卷 ID(如 ?id=1
var params = new URLSearchParams(window.location.search);
var qid = params.get('id');
// localStorage 缓存 key
var STORAGE_KEY = 'q_' + qid + '_answers';
var saveTimer = null;
var SAVE_DEBOUNCE_MS = 800;
if (!qid) {
document.getElementById('app').innerHTML =
'<div style="text-align:center;padding:60px;"><p style="color:var(--red);">缺少问卷ID参数,请从首页进入</p><a href="/" class="btn btn-primary" style="margin-top:16px;">&#8592; 返回首页</a></div>';
} else {
loadQuestionnaire(qid);
}
function loadQuestionnaire(qid) {
fetch('/api/questionnaire/' + qid)
.then(function(resp) {
if (!resp.ok) throw new Error('问卷不存在');
return resp.json();
})
.then(function(q) {
// 页面标题
document.title = q.title + ' — 天恒高考志愿门户';
// 渲染表单
var targetUser = q.target_user || '天恒';
var targetBadge = targetUser === '父母'
? '<span class="filler-tag filler-parents" style="margin-left:12px;">&#x1F468;&#x200D;&#x1F469;&#x200D;&#x1F467; 父母填写</span>'
: '<span class="filler-tag filler-tianheng" style="margin-left:12px;">&#x1F464; 天恒填写</span>';
var html =
'<div style="margin-bottom:20px;">' +
'<a href="/" style="color:var(--navy-600);text-decoration:none;font-size:13px;font-weight:600;">&#8592; 返回首页</a>' +
'</div>' +
'<div class="hero" style="padding:20px 0 16px;">' +
'<h1>' + q.title + ' ' + targetBadge + '</h1>' +
'<p class="sub">' + q.description + '</p>' +
'</div>' +
'<div style="background:var(--card);border-radius:var(--radius);padding:20px;margin-bottom:20px;border:1px solid var(--border);">' +
'<label style="font-weight:600;font-size:14px;display:block;margin-bottom:8px;">&#x1F464; 您的称呼(如:爸爸、妈妈、天恒)</label>' +
'<input type="text" id="user-name" class="text-input" placeholder="请输入您的称呼" style="max-width:300px;">' +
'</div>' +
'<form id="q-form" class="form-container">';
// 根据问卷类型渲染题目
if (q.questions.type === 'likert') {
var opts = q.questions.options;
q.questions.items.forEach(function(item, idx) {
html +=
'<div class="question-block">' +
'<div class="q-num">第 ' + (idx + 1) + ' 题 / 共 ' + q.questions.items.length + ' 题</div>' +
'<div class="q-text">' + item.text + '</div>' +
'<ul class="option-list">' +
opts.map(function(o) {
return '<li onclick="selectLikert(this, \'' + item.id + '\', ' + o.value + ')" data-qid="' + item.id + '" data-value="' + o.value + '">' +
'<label>' +
'<input type="radio" name="' + item.id + '" value="' + o.value + '">' +
'<span>' + o.label + '' + o.value + '分)</span>' +
'</label>' +
'</li>';
}).join('') +
'</ul>' +
'</div>';
});
} else {
// 计算实际题目数(跳过section)
var totalQuestions = q.questions.filter(function(it) { return it.type !== 'section'; }).length;
var qIdx = 0;
q.questions.forEach(function(item, idx) {
// 分区标题
if (item.type === 'section') {
html += '<div class="section-header" style="margin-top:24px;margin-bottom:12px;border-bottom:2px solid var(--navy-300);padding-bottom:8px;">' +
'<h3 style="font-size:16px;color:var(--navy-700);margin-bottom:2px;">' + item.title + '</h3>' +
(item.desc ? '<p style="font-size:12px;color:var(--text-muted);">' + item.desc + '</p>' : '') +
'</div>';
return;
}
qIdx++;
html += '<div class="question-block">' +
'<div class="q-num">第 ' + qIdx + ' 题 / 共 ' + totalQuestions + ' 题</div>' +
'<div class="q-text">' + item.text + '</div>';
if (item.type === 'text') {
html += '<textarea class="text-input" name="' + item.id + '" data-qid="' + item.id + '" placeholder="' + (item.placeholder || '请输入...') + '" rows="3"></textarea>';
} else if (item.type === 'multiple') {
html += '<ul class="option-list">' + item.options.map(function(o) {
return '<li onclick="toggleCheckbox(this)" data-qid="' + item.id + '" data-value="' + o.value + '">' +
'<label>' +
'<input type="checkbox" name="' + item.id + '" value="' + o.value + '">' +
'<span>' + o.label + '</span>' +
'</label>' +
'</li>';
}).join('') + '</ul>';
} else {
html += '<ul class="option-list">' + item.options.map(function(o) {
return '<li onclick="selectRadio(this, \'' + item.id + '\', \'' + o.value + '\')" data-qid="' + item.id + '" data-value="' + o.value + '">' +
'<label>' +
'<input type="radio" name="' + item.id + '" value="' + o.value + '">' +
'<span>' + o.label + '</span>' +
'</label>' +
'</li>';
}).join('') + '</ul>';
}
html += '</div>';
});
}
// 提交按钮 + 自动保存指示器
html +=
'<div style="text-align:center;margin-top:20px;">' +
'<button type="button" class="btn btn-primary" onclick="submitForm(' + qid + ')" style="font-size:16px;padding:12px 40px;">' +
'&#x2705; 提交答卷' +
'</button>' +
'</div>' +
'<div class="auto-save-indicator" id="auto-save-indicator">' +
'<span class="save-dot" id="save-dot"></span>' +
'<span id="save-text">已就绪</span>' +
'</div>' +
'</form>';
document.getElementById('app').innerHTML = html;
// ============ 恢复 localStorage 缓存 ============
restoreFromCache();
// ============ 绑定自动保存事件 ============
bindAutoSave();
})
.catch(function(err) {
document.getElementById('app').innerHTML =
'<div style="text-align:center;padding:60px;">' +
'<p style="color:var(--red);">加载失败:' + err.message + '</p>' +
'<a href="/" class="btn btn-primary" style="margin-top:16px;">&#8592; 返回首页</a>' +
'</div>';
});
}
// ============ 交互辅助函数(含自动保存触发) ============
function selectRadio(li, name, value) {
var group = li.parentElement.querySelectorAll('li');
group.forEach(function(g) { g.classList.remove('selected'); });
li.classList.add('selected');
li.querySelector('input').checked = true;
triggerAutoSave();
}
function selectLikert(li, name, value) {
var group = li.parentElement.querySelectorAll('li');
group.forEach(function(g) { g.classList.remove('selected'); });
li.classList.add('selected');
li.querySelector('input').checked = true;
triggerAutoSave();
}
function toggleCheckbox(li) {
var cb = li.querySelector('input');
cb.checked = !cb.checked;
li.classList.toggle('selected', cb.checked);
triggerAutoSave();
}
// ============ localStorage 自动保存 ============
function collectAllAnswers() {
var data = {
userName: '',
answers: {}
};
// 用户名
var userNameEl = document.getElementById('user-name');
if (userNameEl) {
data.userName = userNameEl.value.trim();
}
// 收集表单中的所有答案
var form = document.getElementById('q-form');
if (!form) return data;
// 单选
form.querySelectorAll('input[type="radio"]:checked').forEach(function(input) {
data.answers[input.name] = input.value;
});
// 多选
var multiGroups = {};
form.querySelectorAll('input[type="checkbox"]:checked').forEach(function(input) {
if (!multiGroups[input.name]) multiGroups[input.name] = [];
multiGroups[input.name].push(input.value);
});
Object.keys(multiGroups).forEach(function(name) {
data.answers[name] = multiGroups[name].join(',');
});
// 文本
form.querySelectorAll('textarea').forEach(function(ta) {
if (ta.value.trim()) data.answers[ta.name] = ta.value.trim();
});
return data;
}
function saveToCache() {
var data = collectAllAnswers();
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
showSaveStatus('saved', '已保存');
} catch(e) {
// localStorage 满或其他错误
console.warn('自动保存失败:', e.message);
showSaveStatus('error', '保存失败');
}
}
function restoreFromCache() {
try {
var raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return;
var data = JSON.parse(raw);
// 恢复用户名
if (data.userName) {
var userNameEl = document.getElementById('user-name');
if (userNameEl) userNameEl.value = data.userName;
}
// 恢复答案 — 先触发所有匹配的 input
if (data.answers) {
var form = document.getElementById('q-form');
if (!form) return;
Object.keys(data.answers).forEach(function(name) {
var val = data.answers[name];
if (val.indexOf(',') > -1) {
// 多选:可能有多个值用逗号分隔
var vals = val.split(',');
vals.forEach(function(v) {
var cb = form.querySelector('input[type="checkbox"][name="' + name + '"][value="' + v + '"]');
if (cb) {
cb.checked = true;
var li = cb.closest('li');
if (li) li.classList.add('selected');
}
});
} else {
// 单选或文本
var radio = form.querySelector('input[type="radio"][name="' + name + '"][value="' + val + '"]');
if (radio) {
radio.checked = true;
var li = radio.closest('li');
if (li) li.classList.add('selected');
return;
}
var ta = form.querySelector('textarea[name="' + name + '"]');
if (ta) {
ta.value = val;
}
}
});
}
} catch(e) {
console.warn('恢复缓存失败:', e.message);
}
}
function clearCache() {
try {
localStorage.removeItem(STORAGE_KEY);
} catch(e) {
// 忽略
}
}
function showSaveStatus(status, text) {
var indicator = document.getElementById('auto-save-indicator');
var dot = document.getElementById('save-dot');
var txt = document.getElementById('save-text');
if (!indicator) return;
indicator.className = 'auto-save-indicator ' + status;
if (dot) dot.className = 'save-dot';
if (txt) txt.textContent = text;
}
function triggerAutoSave() {
showSaveStatus('saving', '自动保存中...');
if (saveTimer) clearTimeout(saveTimer);
saveTimer = setTimeout(function() {
saveToCache();
}, SAVE_DEBOUNCE_MS);
}
function bindAutoSave() {
var form = document.getElementById('q-form');
if (!form) return;
// textarea 输入事件
form.querySelectorAll('textarea').forEach(function(ta) {
ta.addEventListener('input', function() {
triggerAutoSave();
});
});
// 用户名输入事件
var userNameEl = document.getElementById('user-name');
if (userNameEl) {
userNameEl.addEventListener('input', function() {
triggerAutoSave();
});
}
}
// ============ 提交表单 ============
function submitForm(qid) {
var userName = document.getElementById('user-name').value.trim();
if (!userName) {
alert('请先填写您的称呼!');
return;
}
var data = collectAllAnswers();
// 检查是否至少答了一题
if (Object.keys(data.answers).length === 0) {
alert('请至少回答一题再提交!');
return;
}
// 发送请求
var submitBtn = document.querySelector('button[onclick^="submitForm"]');
submitBtn.disabled = true;
submitBtn.textContent = '提交中...';
var respOk;
fetch('/api/questionnaire/' + qid + '/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user_name: userName, answers: data.answers })
})
.then(function(resp) {
respOk = resp.ok;
return resp.json();
})
.then(function(result) {
if (respOk) {
clearCache();
alert('提交成功!' + (result.message || '') + '\n正在生成分析报告...');
window.location.href = '/report.html?id=' + qid;
} else {
alert('提交失败:' + (result.error || '未知错误'));
submitBtn.disabled = false;
submitBtn.textContent = '✅ 提交答卷';
}
})
.catch(function(err) {
alert('网络错误,请检查后端服务是否正常');
submitBtn.disabled = false;
submitBtn.textContent = '✅ 提交答卷';
});
}
</script>
</body>
</html>