feat: enhance learn-number with dynamic SVG icons, multi-page support, and modular UI
Build and Push Docker Image / build (push) Successful in 2m26s

This commit is contained in:
2026-02-25 23:29:35 -08:00
parent 0be94026c5
commit 7cd611140e
12 changed files with 4330 additions and 279 deletions
+144 -20
View File
@@ -2,37 +2,161 @@
<div id="panel-learn-number" class="tool-panel">
<header class="page-header">
<h1>幼儿数学助手</h1>
<p>通过趣味图形和基础练习,培养孩子的数感与逻辑。</p>
<p>通过高品质卡通图形,培养孩子的数感与逻辑思维</p>
</header>
<div class="tabs-container">
<div id="tab-counting" class="tab-btn active">数图形练习</div>
</div>
<div class="card">
<div style="display: flex; flex-direction: column; gap: 24px;">
<div style="display: flex; gap: 30px; flex-wrap: wrap;">
<div style="flex: 1; min-width: 250px;">
<label style="font-size: 13px; font-weight: 600; color: #86868b; display: block; margin-bottom: 12px;">图标种类数 (1 - 5)</label>
<input type="range" id="icon_types" min="1" max="5" value="3" style="width: 100%;" oninput="this.nextElementSibling.value = this.value">
<output style="font-size: 18px; font-weight: 700; color: var(--apple-blue); margin-top: 10px; display: block;">3</output>
</div>
<div style="flex: 1; min-width: 250px;">
<label style="font-size: 13px; font-weight: 600; color: #86868b; display: block; margin-bottom: 12px;">总图案数量 (种类数 - 30)</label>
<input type="range" id="total_count" min="5" max="30" value="15" style="width: 100%;" oninput="this.nextElementSibling.value = this.value">
<output style="font-size: 18px; font-weight: 700; color: var(--apple-blue); margin-top: 10px; display: block;">15</output>
</div>
<div style="width: 240px;">
<label>纸张大小</label>
<select id="math_paper_size">
<option value="A4">A4</option>
<option value="Letter">Letter</option>
<div style="display: flex; flex-direction: column; gap: 32px;">
<!-- 设置区 -->
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 20px; align-items: flex-end;">
<div class="input-group">
<label style="font-size: 13px; font-weight: 600; color: #86868b; margin-bottom: 8px; display: block;">练习主题</label>
<select id="math_category" onchange="updateIconPreview()" class="apple-select">
<option value="fruits">素材库 (SVG)</option>
<option value="shapes">基础几何图形</option>
</select>
</div>
<div class="input-group">
<label style="font-size: 13px; font-weight: 600; color: #86868b; margin-bottom: 8px; display: block;">种类 (Max 6)</label>
<input type="number" id="icon_types" min="1" max="6" value="3" class="apple-input">
</div>
<div class="input-group">
<label style="font-size: 13px; font-weight: 600; color: #86868b; margin-bottom: 8px; display: block;">总量 (Max 30)</label>
<input type="number" id="total_count" min="1" max="30" value="15" class="apple-input">
</div>
<div class="input-group">
<label style="font-size: 13px; font-weight: 600; color: #86868b; margin-bottom: 8px; display: block;">页数 (1-10)</label>
<input type="number" id="page_count" min="1" max="10" value="1" class="apple-input">
</div>
<div class="input-group">
<label style="font-size: 13px; font-weight: 600; color: #86868b; margin-bottom: 8px; display: block;">纸张大小</label>
<select id="math_paper_size" class="apple-select">
<option value="Letter" selected>Letter (8.5x11in)</option>
<option value="A4">A4 (210x297mm)</option>
</select>
</div>
<button id="btn-generate-math" onclick="generateMathPDF()" style="height: 46px; padding: 0 20px;">生成练习帖</button>
</div>
<!-- 图标预览 -->
<div id="icon-preview-container" style="padding: 24px; background: #fbfbfd; border-radius: 16px; border: 1px solid #e5e5e7;">
<label style="font-size: 11px; font-weight: 700; color: #86868b; display: block; margin-bottom: 16px; text-transform: uppercase; letter-spacing: 1px;">当前主题包含的图案:</label>
<div id="icon-list" style="display: flex; gap: 16px; flex-wrap: wrap;">
<!-- JS 动态注入 -->
</div>
</div>
<div id="math-error-msg" style="color: #ff3b30; font-size: 14px; display: none; font-weight: 500; text-align: center; padding: 10px; background: #fff2f2; border-radius: 8px;">
⚠️ 请确保:总数 >= 种类数,页数 1-10。
</div>
<button onclick="generateMathPDF()">生成数学练习帖</button>
</div>
</div>
<div id="math-preview" style="display:none; width: 100%; height: 800px; border-radius: 20px; overflow: hidden; box-shadow: 0 10px 40px rgba(0,0,0,0.1); background: #fff; border: 1px solid #d2d2d7;">
<div id="math-preview" style="display:none; width: 100%; height: 850px; border-radius: 24px; overflow: hidden; box-shadow: 0 20px 60px rgba(0,0,0,0.12); background: #fff; border: 1px solid #d2d2d7; margin-top: 40px;">
<iframe id="math-frame" style="width:100%; height:100%; border:none;"></iframe>
</div>
</div>
<style>
.apple-input, .apple-select {
width: 100%; height: 46px; padding: 0 12px; border: 1px solid #d2d2d7; border-radius: 10px;
font-size: 15px; font-weight: 500; background-color: #ffffff; transition: all 0.2s ease;
outline: none; box-shadow: inset 0 1px 2px rgba(0,0,0,0.05); line-height: 46px;
}
.apple-input:focus, .apple-select:focus { border-color: #0071e3; box-shadow: 0 0 0 4px rgba(0,113,227,0.15); }
.icon-thumbnail {
width: 64px; height: 64px; background: white; border-radius: 12px; border: 1px solid #e5e5e7;
display: flex; align-items: center; justify-content: center; padding: 8px;
box-shadow: 0 2px 6px rgba(0,0,0,0.04); transition: all 0.2s ease;
}
.icon-thumbnail:hover { transform: translateY(-3px); border-color: #0071e3; box-shadow: 0 8px 20px rgba(0,0,0,0.08); }
</style>
<script>
let allCategories = {};
async function initMathTool() {
try {
const res = await fetch('/api/learn-number/categories');
allCategories = await res.json();
updateIconPreview();
} catch (e) { console.error(e); }
}
function calculateBBox(paths) {
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
// 增强正则:匹配所有数字,包括带逗号的
const numRegex = /-?\d+\.?\d*/g;
paths.forEach(path => {
const matches = path.match(numRegex);
if (matches) {
for (let i=0; i<matches.length; i+=2) {
const x = parseFloat(matches[i]);
const y = parseFloat(matches[i+1]);
if (!isNaN(x) && !isNaN(y)) {
if (x < minX) minX = x; if (x > maxX) maxX = x;
if (y < minY) minY = y; if (y > maxY) maxY = y;
}
}
}
});
if (minX === Infinity) return "0 0 1024 1024";
const w = maxX - minX, h = maxY - minY;
const padding = Math.max(w, h) * 0.15;
return `${minX - padding} ${minY - padding} ${w + padding*2} ${h + padding*2}`;
}
function updateIconPreview() {
const cat = document.getElementById('math_category').value;
const icons = allCategories[cat] || [];
const container = document.getElementById('icon-list');
container.innerHTML = '';
icons.forEach(icon => {
const div = document.createElement('div');
div.className = 'icon-thumbnail';
const viewBox = calculateBBox(icon.Paths);
const pathsHtml = icon.Paths.map(p => `<path d="${p}" fill="none" stroke="black" stroke-width="2%" stroke-linecap="round" stroke-linejoin="round" />`).join('');
div.innerHTML = `<svg viewBox="${viewBox}" style="width: 100%; height: 100%; overflow: visible;">${pathsHtml}</svg>`;
div.title = icon.Name;
container.appendChild(div);
});
}
async function generateMathPDF() {
const total_count = parseInt(document.getElementById('total_count').value);
const icon_types = parseInt(document.getElementById('icon_types').value);
const page_count = parseInt(document.getElementById('page_count').value || 1);
const category = document.getElementById('math_category').value;
const paper_size = document.getElementById('math_paper_size').value;
const errorMsg = document.getElementById('math-error-msg');
if (total_count < icon_types || icon_types > 6 || total_count > 30) {
errorMsg.style.display = 'block';
return;
}
errorMsg.style.display = 'none';
const btn = document.getElementById('btn-generate-math');
const originalText = btn.innerText;
btn.innerText = '生成中...'; btn.disabled = true;
try {
const response = await fetch('/api/learn-number/counting', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ total_count, icon_types, page_count, category, paper_size })
});
const blob = await response.blob();
const url = URL.createObjectURL(blob);
document.getElementById('math-preview').style.display = 'block';
document.getElementById('math-frame').src = url;
} catch (e) {
alert('生成失败: ' + e);
} finally {
btn.innerText = originalText; btn.disabled = false;
}
}
setTimeout(initMathTool, 200);
</script>
{{end}}