Fix: Force add cmd/toolbox directory
Build and Push Docker Image / build (push) Failing after 5s

This commit is contained in:
2026-02-23 02:04:56 -08:00
parent 8b243027f4
commit e0798046fc
2 changed files with 310 additions and 0 deletions
+86
View File
@@ -0,0 +1,86 @@
package main
import (
"embed"
"flag"
"io/fs"
"log"
"net/http"
"os"
"strconv"
"toolbox/pkg/base"
_ "toolbox/pkg/jitie" // 匿名导入以触发 init()
"github.com/gin-gonic/gin"
)
//go:embed web/*
var webFS embed.FS
func main() {
// 定义端口 Flag
portFlag := flag.Int("port", 0, "端口号 (优先于环境变量 PORT)")
flag.Parse()
// 禁用 Gin 的默认重定向逻辑,防止与静态资源冲突
r := gin.New()
r.Use(gin.Logger(), gin.Recovery())
r.RedirectTrailingSlash = false
r.RedirectFixedPath = false
// 1. 自动发现并注册工具路由
api := r.Group("/api")
{
for id, tool := range base.Registry {
log.Printf("Loading tool: %s (%s)", tool.Name(), id)
if err := tool.Init(); err != nil {
log.Fatalf("Failed to initialize tool %s: %v", id, err)
}
group := api.Group("/" + id)
tool.RegisterRoutes(group)
}
}
// 2. 静态资源路由 (内嵌前端)
// 根路径直接返回 index.html 内容,不使用文件服务器转发
r.GET("/", func(c *gin.Context) {
content, err := webFS.ReadFile("web/index.html")
if err != nil {
c.String(http.StatusNotFound, "index.html not found")
return
}
c.Data(http.StatusOK, "text/html; charset=utf-8", content)
})
// 如果将来有 css/js,可以挂载到 /web 路径
subFS, _ := fs.Sub(webFS, "web")
r.StaticFS("/web", http.FS(subFS))
// 3. 工具列表发现接口
r.GET("/api/tools", func(c *gin.Context) {
var list []map[string]string
for _, t := range base.Registry {
list = append(list, map[string]string{
"id": t.ID(),
"name": t.Name(),
"desc": t.Description(),
})
}
c.JSON(http.StatusOK, list)
})
// 4. 确定最终使用的端口
var finalPort string
if *portFlag > 0 {
finalPort = strconv.Itoa(*portFlag)
} else if envPort := os.Getenv("PORT"); envPort != "" {
finalPort = envPort
} else {
finalPort = "8080"
}
log.Printf("Toolbox server starting on :%s", finalPort)
if err := r.Run(":" + finalPort); err != nil {
log.Fatalf("Server failed: %v", err)
}
}
+224
View File
@@ -0,0 +1,224 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>个人工具箱 - 汉字字帖生成器</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f5f5f7;
color: #1d1d1f;
}
.container {
max-width: 1000px;
margin: 0 auto;
padding: 40px 20px;
display: flex;
flex-direction: column;
gap: 24px;
}
header {
text-align: center;
margin-bottom: 10px;
}
header h1 { font-size: 32px; margin-bottom: 8px; }
header p { color: #86868b; margin: 0; }
.card {
background: white;
border-radius: 16px;
padding: 28px;
box-shadow: 0 8px 30px rgba(0,0,0,0.04);
}
.form-layout {
display: flex;
flex-direction: column;
gap: 20px;
}
.input-row-top {
width: 100%;
}
.input-row-bottom {
display: flex;
gap: 16px;
align-items: flex-end;
}
.input-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.input-group.flex-main { flex: 2; }
.input-group.flex-side { flex: 1; }
label {
font-size: 13px;
font-weight: 600;
color: #1d1d1f;
text-transform: uppercase;
letter-spacing: 0.5px;
}
input[type="text"] {
width: 100%;
box-sizing: border-box;
padding: 14px 16px;
border: 1px solid #d2d2d7;
border-radius: 10px;
font-size: 18px;
background-color: #fbfbfd;
transition: all 0.2s;
}
input[type="text"]:focus {
border-color: #0071e3;
background-color: #fff;
box-shadow: 0 0 0 4px rgba(0,113,227,0.1);
outline: none;
}
select {
appearance: none;
padding: 12px 16px;
border: 1px solid #d2d2d7;
border-radius: 10px;
font-size: 15px;
background-color: #fbfbfd;
cursor: pointer;
}
button {
background-color: #0071e3;
color: white;
border: none;
padding: 14px 32px;
border-radius: 10px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
height: 48px;
}
button:hover { background-color: #0077ed; transform: translateY(-1px); }
button:active { transform: translateY(0); }
#preview-container {
width: 100%;
height: 850px;
border-radius: 16px;
overflow: hidden;
background: #fff;
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
display: none;
}
iframe { width: 100%; height: 100%; border: none; }
.empty-state {
text-align: center;
padding: 80px 0;
border: 2px dashed #d2d2d7;
border-radius: 16px;
color: #86868b;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>汉字字帖工具箱</h1>
<p>生成标准笔顺教学字帖,支持 A4/Letter 打印</p>
</header>
<div class="card">
<div class="form-layout">
<div class="input-row-top">
<div class="input-group">
<label for="chars">输入想要生成的汉字</label>
<input type="text" id="chars" placeholder="例如:永和九年,岁在癸丑..." value="永">
</div>
</div>
<div class="input-row-bottom">
<div class="input-group flex-main">
<label for="mode">布局模式</label>
<select id="mode">
<option value="teaching">2x3 教学方格 (带笔顺、箭头、序号)</option>
<option value="step">步进式分解 (逐笔展示过程)</option>
</select>
</div>
<div class="input-group flex-side">
<label for="paper_size">纸张大小</label>
<select id="paper_size">
<option value="A4">A4 (210x297mm)</option>
<option value="Letter">Letter (8.5x11in)</option>
</select>
</div>
<button onclick="generatePDF()">生成预览</button>
</div>
</div>
</div>
<div id="preview-container">
<iframe id="pdf-frame"></iframe>
</div>
<div id="empty-state" class="empty-state">
<p>在上方输入汉字并点击按钮,即可生成高清矢量字帖预览</p>
</div>
</div>
<script>
async function generatePDF() {
const chars = document.getElementById('chars').value;
const mode = document.getElementById('mode').value;
const paper_size = document.getElementById('paper_size').value;
if (!chars.trim()) {
alert('请输入汉字');
return;
}
const btn = document.querySelector('button');
const originalText = btn.innerText;
btn.innerText = '正在绘图中...';
btn.disabled = true;
try {
const response = await fetch('/api/jitie/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
chars: chars,
mode: mode,
flip_y: true,
paper_size: paper_size
})
});
if (response.ok) {
const blob = await response.blob();
const url = URL.createObjectURL(blob);
document.getElementById('empty-state').style.display = 'none';
const container = document.getElementById('preview-container');
container.style.display = 'block';
document.getElementById('pdf-frame').src = url;
} else {
const err = await response.json();
alert('生成失败: ' + (err.error || '未知错误'));
}
} catch (e) {
alert('网络请求失败,请检查后端服务是否运行');
} finally {
btn.innerText = originalText;
btn.disabled = false;
}
}
</script>
</body>
</html>