424 lines
14 KiB
HTML
424 lines
14 KiB
HTML
|
|
<!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;">← 返回首页</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;">👨‍👩‍👧 父母填写</span>'
|
|||
|
|
: '<span class="filler-tag filler-tianheng" style="margin-left:12px;">👤 天恒填写</span>';
|
|||
|
|
|
|||
|
|
var html =
|
|||
|
|
'<div style="margin-bottom:20px;">' +
|
|||
|
|
'<a href="/" style="color:var(--navy-600);text-decoration:none;font-size:13px;font-weight:600;">← 返回首页</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;">👤 您的称呼(如:爸爸、妈妈、天恒)</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;">' +
|
|||
|
|
'✅ 提交答卷' +
|
|||
|
|
'</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;">← 返回首页</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>
|