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"}} - -
- -
-
2x3 教学方格
-
步进式分解
-
古风竖排
-
+ +{{template "zitie" .}} +{{template "learn_number" .}} -
-
-
- - -
- -
-
- - -
- - -
-
-
- -
- - +
{{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"}} +
+ +
+
数图形练习
+
+
+
+
+
+ + + 3 +
+
+ + + 15 +
+
+ + +
+
+ +
+
+ +
+{{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"}} +
+ +
+
2x3 教学方格
+
步进式分解
+
古风竖排
+
+
+
+
+ + +
+
+
+ + +
+ + +
+
+
+ +
+{{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) +}