diff --git a/.gitignore b/.gitignore
index e4b25c6..36e8abd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,5 @@
# Binaries
-toolbox
+/toolbox
*.exe
*.exe~
*.dll
diff --git a/cmd/toolbox/main.go b/cmd/toolbox/main.go
index 5dbeb16..e09e607 100644
--- a/cmd/toolbox/main.go
+++ b/cmd/toolbox/main.go
@@ -11,6 +11,7 @@ import (
"os"
"strconv"
"toolbox/pkg/base"
+ _ "toolbox/pkg/learnnumber"
_ "toolbox/pkg/zitie" // 匿名导入以触发 init()
"github.com/gin-gonic/gin"
@@ -55,8 +56,8 @@ func main() {
r.StaticFS("/static", http.FS(subFS))
// 3. 模板引擎初始化
- // 关键修复:先 Sub 再 Parse,确保路径匹配
- tmpl, err := template.ParseFS(subFS, "layout.html", "index.html")
+ // 递归加载 web 目录下所有的 .html 文件
+ tmpl, err := template.ParseFS(subFS, "layout.html", "index.html", "tools/*.html")
if err != nil {
log.Fatalf("Failed to parse templates: %v", err)
}
diff --git a/cmd/toolbox/web/index.html b/cmd/toolbox/web/index.html
index 812d847..6917f4c 100644
--- a/cmd/toolbox/web/index.html
+++ b/cmd/toolbox/web/index.html
@@ -1,51 +1,10 @@
{{define "content"}}
-
-
-
-
+
{{end}}
diff --git a/cmd/toolbox/web/tools/learn_number.html b/cmd/toolbox/web/tools/learn_number.html
new file mode 100644
index 0000000..780d066
--- /dev/null
+++ b/cmd/toolbox/web/tools/learn_number.html
@@ -0,0 +1,38 @@
+{{define "learn_number"}}
+
+{{end}}
diff --git a/cmd/toolbox/web/tools/zitie.html b/cmd/toolbox/web/tools/zitie.html
new file mode 100644
index 0000000..6fc07f5
--- /dev/null
+++ b/cmd/toolbox/web/tools/zitie.html
@@ -0,0 +1,43 @@
+{{define "zitie"}}
+
+{{end}}
diff --git a/deploy/k8s.yaml b/deploy/k8s.yaml
index 74f0433..af47080 100644
--- a/deploy/k8s.yaml
+++ b/deploy/k8s.yaml
@@ -1,8 +1,13 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: toolbox
+---
apiVersion: apps/v1
kind: Deployment
metadata:
name: own-tools
- namespace: default
+ namespace: toolbox
spec:
replicas: 1
selector:
@@ -13,9 +18,6 @@ spec:
labels:
app: own-tools
spec:
- # 如果你的 Gitea 仓库是私有的,请取消下面注释并创建对应的 Secret
- # imagePullSecrets:
- # - name: gitea-registry-secret
containers:
- name: toolbox
image: git.pengzhan.dev/haopengzhan/own-tools:latest
@@ -52,8 +54,8 @@ spec:
apiVersion: v1
kind: Service
metadata:
- name: own-tools
- namespace: default
+ name: own-tools-service
+ namespace: toolbox
spec:
selector:
app: own-tools
@@ -67,7 +69,7 @@ apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: own-tools-ingress
- namespace: default
+ namespace: toolbox
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
@@ -80,7 +82,7 @@ spec:
pathType: Prefix
backend:
service:
- name: own-tools
+ name: own-tools-service
port:
number: 80
tls:
diff --git a/pkg/learnnumber/data/icons.go b/pkg/learnnumber/data/icons.go
new file mode 100644
index 0000000..3f174aa
--- /dev/null
+++ b/pkg/learnnumber/data/icons.go
@@ -0,0 +1,41 @@
+package data
+
+type Icon struct {
+ Name string
+ Paths []string
+}
+
+// 升级版:更卡通、更饱满的简笔画
+var CountingIcons = []Icon{
+ {Name: "Bear", Paths: []string{
+ "M 512 800 C 300 800 200 700 200 500 C 200 300 350 200 512 200 C 674 200 824 300 824 500 C 824 700 724 800 512 800 Z", // Body
+ "M 300 300 m -50 0 a 50 50 0 1 0 100 0 a 50 50 0 1 0 -100 0", // Ear L
+ "M 724 300 m -50 0 a 50 50 0 1 0 100 0 a 50 50 0 1 0 -100 0", // Ear R
+ "M 400 450 m -20 0 a 20 20 0 1 0 40 0 a 20 20 0 1 0 -40 0", // Eye L
+ "M 624 450 m -20 0 a 20 20 0 1 0 40 0 a 20 20 0 1 0 -40 0", // Eye R
+ "M 512 550 Q 512 650 400 650 M 512 550 Q 512 650 624 650", // Mouth
+ }},
+ {Name: "Cat", Paths: []string{
+ "M 200 800 L 300 400 L 400 200 L 512 350 L 624 200 L 724 400 L 824 800 Z", // Head
+ "M 400 550 m -15 0 a 15 15 0 1 0 30 0 a 15 15 0 1 0 -30 0", // Eye L
+ "M 624 550 m -15 0 a 15 15 0 1 0 30 0 a 15 15 0 1 0 -30 0", // Eye R
+ "M 512 650 L 450 700 M 512 650 L 574 700", // Nose
+ }},
+ {Name: "Car", Paths: []string{
+ "M 100 700 L 100 500 Q 100 400 300 400 L 700 400 Q 900 400 900 500 L 900 700 Z", // Body
+ "M 250 700 m -60 0 a 60 60 0 1 0 120 0 a 60 60 0 1 0 -120 0", // Wheel L
+ "M 750 700 m -60 0 a 60 60 0 1 0 120 0 a 60 60 0 1 0 -120 0", // Wheel R
+ "M 300 400 L 400 250 L 624 250 L 724 400", // Roof
+ }},
+ {Name: "Bird", Paths: []string{
+ "M 512 512 m -300 0 a 300 300 0 1 0 600 0 a 300 300 0 1 0 -600 0", // Body
+ "M 812 512 L 950 450 L 812 400 Z", // Beak
+ "M 400 400 m -20 0 a 20 20 0 1 0 40 0 a 20 20 0 1 0 -40 0", // Eye
+ "M 212 512 Q 100 400 212 300", // Wing
+ }},
+ {Name: "Rocket", Paths: []string{
+ "M 512 100 Q 700 400 700 800 L 324 800 Q 324 400 512 100 Z", // Body
+ "M 512 400 m -50 0 a 50 50 0 1 0 100 0 a 50 50 0 1 0 -100 0", // Window
+ "M 324 800 L 200 950 L 324 900 M 700 800 L 824 950 L 700 900", // Fins
+ }},
+}
diff --git a/pkg/learnnumber/logic/counting.go b/pkg/learnnumber/logic/counting.go
new file mode 100644
index 0000000..5a4b683
--- /dev/null
+++ b/pkg/learnnumber/logic/counting.go
@@ -0,0 +1,168 @@
+package logic
+
+import (
+ "bytes"
+ "math"
+ "math/rand"
+ "regexp"
+ "strconv"
+ "strings"
+ "time"
+ "toolbox/pkg/learnnumber/data"
+
+ "github.com/signintech/gopdf"
+)
+
+type CountingRequest struct {
+ TotalCount int `json:"total_count"`
+ IconTypes int `json:"icon_types"`
+ PaperSize string `json:"paper_size"`
+}
+
+var reSVG = regexp.MustCompile(`([MLQCZmlqcz])|(-?\d+\.?\d*)`)
+
+func GenerateCountingPDF(req CountingRequest) ([]byte, error) {
+ rand.Seed(time.Now().UnixNano())
+ pdf := &gopdf.GoPdf{}
+ rect := gopdf.Rect{W: 595.28, H: 841.89}
+ if req.PaperSize == "Letter" { rect = gopdf.Rect{W: 612, H: 792} }
+ pdf.Start(gopdf.Config{PageSize: rect})
+ pdf.AddPage()
+
+ drawProblem(pdf, req, 0, rect.W, rect.H/2)
+ drawProblem(pdf, req, rect.H/2, rect.W, rect.H/2)
+
+ var buf bytes.Buffer
+ _, err := pdf.WriteTo(&buf)
+ return buf.Bytes(), err
+}
+
+func drawProblem(pdf *gopdf.GoPdf, req CountingRequest, startY, pW, pH float64) {
+ margin := 40.0
+ boxW := (pW - 2*margin) * 0.7
+ boxH := pH - 60.0
+ x, y := margin, startY + 30.0
+
+ // 1. 绘制方框
+ pdf.SetStrokeColor(0, 0, 0); pdf.SetLineWidth(2.0)
+ pdf.RectFromUpperLeft(x, y, boxW, boxH)
+
+ // 2. 分配图标数量
+ numTypes := req.IconTypes; if numTypes < 1 { numTypes = 1 }
+ if numTypes > len(data.CountingIcons) { numTypes = len(data.CountingIcons) }
+
+ total := req.TotalCount; if total < numTypes { total = numTypes }
+ if total > 30 { total = 30 }
+
+ // 随机选出图标种类
+ allIcons := rand.Perm(len(data.CountingIcons))
+ selectedIcons := []data.Icon{}
+ counts := []int{}
+
+ // 先每种分配1个
+ rem := total - numTypes
+ for i := 0; i < numTypes; i++ {
+ selectedIcons = append(selectedIcons, data.CountingIcons[allIcons[i]])
+ counts = append(counts, 1)
+ }
+ // 随机分配剩下的
+ for i := 0; i < rem; i++ {
+ counts[rand.Intn(numTypes)]++
+ }
+
+ // 3. 布局计划 (使用网格,增加 Padding 避开边框)
+ rows, cols := 5, 6
+ cellW, cellH := boxW/float64(cols), boxH/float64(rows)
+ indices := rand.Perm(rows * cols)
+
+ // 严格图标大小:单元格的 70%,确保不溢出单元格边界
+ iconSize := math.Min(cellW, cellH) * 0.7
+ paddingX, paddingY := (cellW - iconSize)/2, (cellH - iconSize)/2
+
+ iconTypeIdx := 0
+ countInCurrentType := 0
+
+ for i := 0; i < total; i++ {
+ cellIdx := indices[i]
+ rIdx, cIdx := cellIdx/cols, cellIdx%cols
+
+ // 基础位置
+ baseX := x + float64(cIdx)*cellW + paddingX
+ baseY := y + float64(rIdx)*cellH + paddingY
+
+ // 渲染图标
+ drawIcon(pdf, selectedIcons[iconTypeIdx], baseX + iconSize/2, baseY + iconSize/2, iconSize)
+
+ countInCurrentType++
+ if countInCurrentType >= counts[iconTypeIdx] {
+ iconTypeIdx++
+ countInCurrentType = 0
+ }
+ }
+
+ // 4. 绘制右侧对照表
+ legendX := x + boxW + 20.0
+ legendStepY := boxH / float64(numTypes + 1)
+ for i := 0; i < numTypes; i++ {
+ lY := y + float64(i+1)*legendStepY
+ drawIcon(pdf, selectedIcons[i], legendX + 20, lY, 30)
+ pdf.SetStrokeColor(150, 150, 150); pdf.SetLineWidth(0.8)
+ pdf.RectFromUpperLeft(legendX + 50, lY - 15, 30, 30)
+ }
+}
+
+func drawIcon(pdf *gopdf.GoPdf, icon data.Icon, cX, cY, size float64) {
+ scale := size / 1024.0
+ pdf.SetStrokeColor(0, 0, 0); pdf.SetLineWidth(1.5) // 加粗线条
+
+ ox, oy := cX - size/2, cY - size/2
+ for _, pStr := range icon.Paths {
+ pts := parseSmoothPath(pStr, scale, ox, oy)
+ if len(pts) > 1 {
+ // 如果是闭合路径
+ if strings.Contains(strings.ToUpper(pStr), "Z") {
+ pdf.Polygon(pts, "D")
+ } else {
+ for i := 0; i < len(pts)-1; i++ {
+ pdf.Line(pts[i].X, pts[i].Y, pts[i+1].X, pts[i+1].Y)
+ }
+ }
+ }
+ }
+}
+
+// 支持贝塞尔曲线采样,确保图标圆润
+func parseSmoothPath(path string, scale, ox, oy float64) []gopdf.Point {
+ var pts []gopdf.Point
+ matches := reSVG.FindAllStringSubmatch(path, -1)
+ var lx, ly float64
+ for i := 0; i < len(matches); {
+ cmd := matches[i][0]
+ if cmd == "M" || cmd == "L" {
+ x, _ := strconv.ParseFloat(matches[i+1][0], 64); y, _ := strconv.ParseFloat(matches[i+2][0], 64)
+ lx, ly = x, y
+ pts = append(pts, gopdf.Point{X: ox + x*scale, Y: oy + y*scale})
+ i += 3
+ } else if cmd == "C" {
+ x1, _ := strconv.ParseFloat(matches[i+1][0], 64); y1, _ := strconv.ParseFloat(matches[i+2][0], 64)
+ x2, _ := strconv.ParseFloat(matches[i+3][0], 64); y2, _ := strconv.ParseFloat(matches[i+4][0], 64)
+ x, _ := strconv.ParseFloat(matches[i+5][0], 64); y, _ := strconv.ParseFloat(matches[i+6][0], 64)
+ for t := 0.25; t <= 1.0; t += 0.25 {
+ tx := math.Pow(1-t, 3)*lx + 3*math.Pow(1-t, 2)*t*x1 + 3*(1-t)*math.Pow(t, 2)*x2 + math.Pow(t, 3)*x
+ ty := math.Pow(1-t, 3)*ly + 3*math.Pow(1-t, 2)*t*y1 + 3*(1-t)*math.Pow(t, 2)*y2 + math.Pow(t, 3)*y
+ pts = append(pts, gopdf.Point{X: ox + tx*scale, Y: oy + ty*scale})
+ }
+ lx, ly = x, y; i += 7
+ } else if cmd == "Q" {
+ x1, _ := strconv.ParseFloat(matches[i+1][0], 64); y1, _ := strconv.ParseFloat(matches[i+2][0], 64)
+ x, _ := strconv.ParseFloat(matches[i+3][0], 64); y, _ := strconv.ParseFloat(matches[i+4][0], 64)
+ for t := 0.25; t <= 1.0; t += 0.25 {
+ tx := math.Pow(1-t, 2)*lx + 2*(1-t)*t*x1 + math.Pow(t, 2)*x
+ ty := math.Pow(1-t, 2)*ly + 2*(1-t)*t*y1 + math.Pow(t, 2)*y
+ pts = append(pts, gopdf.Point{X: ox + tx*scale, Y: oy + ty*scale})
+ }
+ lx, ly = x, y; i += 5
+ } else { i++ }
+ }
+ return pts
+}
diff --git a/pkg/learnnumber/tool.go b/pkg/learnnumber/tool.go
new file mode 100644
index 0000000..25b70ea
--- /dev/null
+++ b/pkg/learnnumber/tool.go
@@ -0,0 +1,48 @@
+package learnnumber
+
+import (
+ "fmt"
+ "net/http"
+ "toolbox/pkg/base"
+ "toolbox/pkg/learnnumber/logic"
+
+ "github.com/gin-gonic/gin"
+)
+
+type learnNumberTool struct{}
+
+func init() {
+ base.Register(&learnNumberTool{})
+}
+
+func (t *learnNumberTool) ID() string { return "learn-number" }
+func (t *learnNumberTool) Name() string { return "幼儿数学助手" }
+func (t *learnNumberTool) Description() string { return "包含数图形、基础加减法等趣味数学练习" }
+
+func (t *learnNumberTool) Init() error {
+ fmt.Println("Initializing Learn-Number tool...")
+ return nil
+}
+
+func (t *learnNumberTool) RegisterRoutes(r *gin.RouterGroup) {
+ r.POST("/counting", t.handleCounting)
+}
+
+func (t *learnNumberTool) handleCounting(c *gin.Context) {
+ var req logic.CountingRequest
+ if err := c.ShouldBindJSON(&req); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+
+ if req.TotalCount <= 0 { req.TotalCount = 20 }
+ if req.TotalCount > 30 { req.TotalCount = 30 }
+
+ pdfBytes, err := logic.GenerateCountingPDF(req)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.Data(http.StatusOK, "application/pdf", pdfBytes)
+}