Files
simon b6df028839 v1.0.0 高考志愿门户完整修复
变更摘要:
- [fix] 清理根目录 index.html 遗留代码(questionnaire-error + retryLoad 移除)
- [fix] 副标题同步更新为自估591分 + 候选志愿546条数据校准
- [fix] 页脚注入版本号 v1.0.0
- [fix] 部署版 index.html: 取消家长标签,统一 filler-tianheng
- [fix] Dashboard 文案修正(4份→2份线上问卷)
- [fix] 清理冗余部署脚本(v1-v4归档至 archived_scripts/)
- [fix] 等效位次工具纳入 deploy 目录
- [chore] .gitignore 初始化
- [init] Git 仓库初始化
2026-06-24 13:11:34 +08:00

424 lines
14 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>