math-master-gen / templates /index.html
duqing2026's picture
feat: enhance features with blank mode, answer keys and Chinese font support
e56ec6a
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>智能口算大师 - Math Master</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/1.6.2/axios.min.js"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&display=swap');
body { font-family: 'Noto Sans SC', sans-serif; }
.paper-bg {
background-color: #ffffff;
background-image: radial-gradient(#e5e7eb 1px, transparent 1px);
background-size: 20px 20px;
}
/* Custom scrollbar */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 3px; }
</style>
</head>
<body class="bg-gray-100 min-h-screen text-gray-800 font-sans">
<div class="container mx-auto px-4 py-6 flex flex-col md:flex-row gap-6 h-screen overflow-hidden">
<!-- Sidebar: Settings -->
<div class="w-full md:w-1/3 lg:w-1/4 bg-white rounded-2xl shadow-xl flex flex-col h-full border border-indigo-50">
<div class="p-6 pb-2">
<h1 class="text-2xl font-bold text-indigo-600 flex items-center">
<span class="bg-indigo-100 p-2 rounded-lg mr-3">
<svg class="w-6 h-6 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 7h6m0 10v-3m-3 3h.01M9 17h.01M9 14h.01M12 14h.01M15 11h.01M12 11h.01M9 11h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z"></path></svg>
</span>
口算大师
</h1>
<p class="text-xs text-gray-400 mt-1 ml-1">专业的小学数学出题神器</p>
</div>
<div class="flex-1 overflow-y-auto px-6 py-2 space-y-6">
<!-- Operations -->
<div class="bg-gray-50 p-4 rounded-xl border border-gray-100">
<label class="block text-sm font-bold text-gray-700 mb-3 flex items-center">
<svg class="w-4 h-4 mr-1 text-indigo-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.384-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z"></path></svg>
运算类型
</label>
<div class="grid grid-cols-2 gap-3">
<label class="flex items-center space-x-2 bg-white border p-2 rounded-lg cursor-pointer hover:border-indigo-300 transition shadow-sm">
<input type="checkbox" class="op-check form-checkbox text-indigo-600 rounded w-5 h-5" value="+" checked>
<span class="font-medium text-gray-700">+ 加法</span>
</label>
<label class="flex items-center space-x-2 bg-white border p-2 rounded-lg cursor-pointer hover:border-indigo-300 transition shadow-sm">
<input type="checkbox" class="op-check form-checkbox text-indigo-600 rounded w-5 h-5" value="-" checked>
<span class="font-medium text-gray-700">- 减法</span>
</label>
<label class="flex items-center space-x-2 bg-white border p-2 rounded-lg cursor-pointer hover:border-indigo-300 transition shadow-sm">
<input type="checkbox" class="op-check form-checkbox text-indigo-600 rounded w-5 h-5" value="×">
<span class="font-medium text-gray-700">× 乘法</span>
</label>
<label class="flex items-center space-x-2 bg-white border p-2 rounded-lg cursor-pointer hover:border-indigo-300 transition shadow-sm">
<input type="checkbox" class="op-check form-checkbox text-indigo-600 rounded w-5 h-5" value="÷">
<span class="font-medium text-gray-700">÷ 除法</span>
</label>
</div>
</div>
<!-- Range & Count -->
<div class="space-y-4">
<div>
<label class="block text-sm font-bold text-gray-700 mb-2">数字范围 (1-100)</label>
<div class="flex items-center space-x-2">
<input type="number" id="minVal" value="1" class="w-full bg-gray-50 border-gray-200 rounded-lg focus:ring-indigo-500 focus:border-indigo-500 p-2.5 border text-center font-mono" placeholder="Min">
<span class="text-gray-400 font-bold">-</span>
<input type="number" id="maxVal" value="20" class="w-full bg-gray-50 border-gray-200 rounded-lg focus:ring-indigo-500 focus:border-indigo-500 p-2.5 border text-center font-mono" placeholder="Max">
</div>
</div>
<div>
<div class="flex justify-between items-center mb-2">
<label class="text-sm font-bold text-gray-700">题目数量</label>
<span id="countDisplay" class="bg-indigo-100 text-indigo-700 px-2 py-0.5 rounded text-sm font-bold">50</span>
</div>
<input type="range" id="countRange" min="10" max="100" value="50" step="10" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-indigo-600">
</div>
</div>
<!-- Advanced Options -->
<div class="bg-indigo-50/50 p-4 rounded-xl border border-indigo-100 space-y-3">
<h3 class="text-xs font-bold text-indigo-400 uppercase tracking-wider">高级选项</h3>
<label class="flex items-center space-x-3 cursor-pointer group">
<div class="relative flex items-center">
<input type="checkbox" id="enableBlank" class="peer h-5 w-5 cursor-pointer appearance-none rounded-md border border-gray-300 bg-white checked:border-indigo-600 checked:bg-indigo-600 focus:outline-none transition-all">
<svg class="pointer-events-none absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-3.5 h-3.5 text-white opacity-0 peer-checked:opacity-100" viewBox="0 0 14 14" fill="none"><path d="M3 8L6 11L11 3.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</div>
<span class="text-sm font-medium text-gray-700 group-hover:text-indigo-700 transition">填空题模式 (如: 3 + ? = 5)</span>
</label>
<label class="flex items-center space-x-3 cursor-pointer group">
<div class="relative flex items-center">
<input type="checkbox" id="enableAnswers" class="peer h-5 w-5 cursor-pointer appearance-none rounded-md border border-gray-300 bg-white checked:border-indigo-600 checked:bg-indigo-600 focus:outline-none transition-all">
<svg class="pointer-events-none absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-3.5 h-3.5 text-white opacity-0 peer-checked:opacity-100" viewBox="0 0 14 14" fill="none"><path d="M3 8L6 11L11 3.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</div>
<span class="text-sm font-medium text-gray-700 group-hover:text-indigo-700 transition">附带答案页 (PDF)</span>
</label>
<div class="flex items-center justify-between text-sm">
<span class="font-medium text-gray-700">PDF 列数:</span>
<div class="flex bg-white rounded-lg p-1 border shadow-sm">
<label class="px-3 py-1 rounded cursor-pointer transition" id="col3_label">
<input type="radio" name="cols" value="3" class="hidden" onchange="updateColUI(this)">
3列
</label>
<label class="px-3 py-1 rounded bg-indigo-100 text-indigo-700 font-bold cursor-pointer transition" id="col4_label">
<input type="radio" name="cols" value="4" class="hidden" onchange="updateColUI(this)" checked>
4列
</label>
</div>
</div>
</div>
</div>
<!-- Actions -->
<div class="p-6 pt-2 space-y-3 bg-white border-t border-gray-100">
<button onclick="generatePreview()" class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-3.5 px-4 rounded-xl shadow-lg shadow-indigo-200 transform transition hover:-translate-y-0.5 active:translate-y-0 flex justify-center items-center">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg>
生成题目预览
</button>
<button onclick="downloadPDF()" class="w-full bg-white border-2 border-indigo-600 text-indigo-600 hover:bg-indigo-50 font-bold py-3.5 px-4 rounded-xl shadow-sm transform transition hover:-translate-y-0.5 active:translate-y-0 flex justify-center items-center group">
<svg class="w-5 h-5 mr-2 group-hover:animate-bounce" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg>
下载 PDF 试卷
</button>
</div>
</div>
<!-- Main: Preview -->
<div class="flex-1 bg-white rounded-2xl shadow-xl overflow-hidden flex flex-col border border-gray-100">
<div class="bg-gray-50 px-6 py-4 border-b flex justify-between items-center">
<div class="flex items-center space-x-2">
<div class="h-3 w-3 rounded-full bg-red-400"></div>
<div class="h-3 w-3 rounded-full bg-yellow-400"></div>
<div class="h-3 w-3 rounded-full bg-green-400"></div>
</div>
<h2 class="font-bold text-gray-700">A4 试卷预览</h2>
<div class="text-xs text-gray-400">
仅供预览,请以 PDF 为准
</div>
</div>
<div class="flex-1 overflow-y-auto p-8 paper-bg relative" id="paperContainer">
<div id="previewArea" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-y-8 gap-x-4 text-xl font-mono text-gray-800">
<!-- Problems go here -->
<div class="text-center text-gray-400 col-span-full py-32 flex flex-col items-center justify-center">
<svg class="w-16 h-16 text-gray-200 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg>
<p>请点击左侧“生成题目预览”</p>
</div>
</div>
</div>
</div>
</div>
<script>
// UI Helpers
const countRange = document.getElementById('countRange');
const countDisplay = document.getElementById('countDisplay');
countRange.addEventListener('input', (e) => countDisplay.innerText = e.target.value);
function updateColUI(radio) {
document.querySelectorAll('input[name="cols"]').forEach(r => {
const label = r.parentElement;
if (r.checked) {
label.classList.add('bg-indigo-100', 'text-indigo-700', 'font-bold');
} else {
label.classList.remove('bg-indigo-100', 'text-indigo-700', 'font-bold');
}
});
// Update preview grid if generated
const grid = document.getElementById('previewArea');
if (radio.value == '3') {
grid.classList.remove('lg:grid-cols-4');
grid.classList.add('lg:grid-cols-3');
} else {
grid.classList.remove('lg:grid-cols-3');
grid.classList.add('lg:grid-cols-4');
}
}
function getSettings() {
const ops = Array.from(document.querySelectorAll('.op-check:checked')).map(cb => cb.value);
const min = parseInt(document.getElementById('minVal').value) || 1;
const max = parseInt(document.getElementById('maxVal').value) || 20;
const count = parseInt(countRange.value) || 50;
const blank_prob = document.getElementById('enableBlank').checked ? 0.5 : 0;
const with_answers = document.getElementById('enableAnswers').checked;
const cols = document.querySelector('input[name="cols"]:checked').value;
if (ops.length === 0) {
alert('请至少选择一种运算类型!');
return null;
}
return { ops, min, max, count, blank_prob, with_answers, cols };
}
async function generatePreview() {
const settings = getSettings();
if (!settings) return;
const previewArea = document.getElementById('previewArea');
// Apply grid cols immediately for visual feedback
if (settings.cols == '3') {
previewArea.classList.remove('lg:grid-cols-4');
previewArea.classList.add('lg:grid-cols-3');
} else {
previewArea.classList.remove('lg:grid-cols-3');
previewArea.classList.add('lg:grid-cols-4');
}
previewArea.innerHTML = '<div class="col-span-full text-center py-32"><div class="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600 mx-auto"></div></div>';
try {
const res = await axios.post('/api/preview', settings);
const problems = res.data;
previewArea.innerHTML = '';
problems.forEach((p, index) => {
const el = document.createElement('div');
// Simulating paper look
el.className = 'py-2 px-2 flex items-center hover:bg-indigo-50/50 rounded transition select-text';
el.innerHTML = `
<span class="font-bold text-gray-400 mr-3 text-sm w-6 text-right">${index + 1}.</span>
<span class="text-2xl font-mono text-gray-800 tracking-wide">${p.question.replace(/\s/g, '&nbsp;')}</span>
`;
previewArea.appendChild(el);
});
} catch (err) {
console.error(err);
previewArea.innerHTML = '<div class="col-span-full text-center text-red-500 py-20">生成失败,请重试</div>';
}
}
async function downloadPDF() {
const settings = getSettings();
if (!settings) return;
const btn = event.currentTarget;
const originalText = btn.innerHTML;
btn.innerHTML = '<svg class="animate-spin h-5 w-5 mr-2 text-indigo-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>生成中...';
btn.disabled = true;
try {
const response = await fetch('/api/download', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(settings)
});
if (!response.ok) throw new Error('Download failed');
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `Math_Worksheet_${new Date().getTime()}.pdf`;
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);
} catch (err) {
alert('下载失败: ' + err.message);
} finally {
btn.innerHTML = originalText;
btn.disabled = false;
}
}
// Initial load
generatePreview();
</script>
</body>
</html>