Files
bj_power/bj_power_mes/internal/handler/processcard.go
T

209 lines
6.0 KiB
Go
Raw Normal View History

package handler
import (
"bytes"
"fmt"
"html/template"
"net/http"
"strings"
"time"
"bj_power_mes/common/httpx"
"bj_power_mes/internal/logic"
"bj_power_mes/internal/svc"
)
// ProcessCardHandler GET /api/v1/process-card?sn=xxx
// 按 SN 生成可打印流程卡(无公司名、含签名栏、可调样式),返回 text/html。
func ProcessCardHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
sn := strings.TrimSpace(r.URL.Query().Get("sn"))
if sn == "" {
httpx.BadRequest(w, "缺少 sn 参数")
return
}
data, err := logic.New(svcCtx).Trace(r.Context(), sn)
if err != nil {
httpx.Fail(w, 2401, err.Error())
return
}
html, err := renderProcessCard(sn, data)
if err != nil {
httpx.Fail(w, 2402, "流程卡生成失败:"+err.Error())
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_, _ = w.Write([]byte(html))
}
}
type cardTmpl struct {
Sn string
OrderNo string
Product string
CreatedAt string
Processes []cardProcess
Materials []string
Operator string
Inspector string
DoneTime string
Result string
}
type cardProcess struct {
Code string
Name string
Station string
Operator string
Status string
Result string
Steps []cardStep
}
type cardStep struct {
Name string
Value string
OK bool
}
func renderProcessCard(sn string, data map[string]any) (string, error) {
t := cardTmpl{Sn: sn}
wp, _ := data["workpiece"].(map[string]any)
if wp != nil {
t.OrderNo = strAny(wp["orderNo"])
t.Product = strAny(wp["productType"])
if c, ok := wp["createdAt"]; ok {
if ts, ok := c.(time.Time); ok {
t.CreatedAt = ts.Format("2006-01-02 15:04")
} else {
t.CreatedAt = fmt.Sprintf("%v", c)
}
}
}
procList, _ := data["processTimeline"].([]any)
for _, p := range procList {
pm, _ := p.(map[string]any)
cp := cardProcess{
Code: strAny(pm["processCode"]),
Name: strAny(pm["processName"]),
Station: strAny(pm["stationNo"]),
Operator: strAny(pm["operator"]),
Status: strAny(pm["status"]),
Result: strAny(pm["result"]),
}
t.Processes = append(t.Processes, cp)
}
steps, _ := data["stepData"].([]any)
stepByProc := map[string][]cardStep{}
for _, s := range steps {
sm, _ := s.(map[string]any)
key := fmt.Sprintf("%v", sm["processCode"])
ok, _ := sm["isOK"].(bool)
stepByProc[key] = append(stepByProc[key], cardStep{
Name: strAny(sm["stepName"]),
Value: strAny(sm["valueText"]),
OK: ok,
})
}
for i := range t.Processes {
t.Processes[i].Steps = stepByProc[t.Processes[i].Code]
}
assoc, _ := data["associationTrace"].(map[string]any)
if assoc != nil {
if b, ok := assoc["batchItems"].([]any); ok {
for _, x := range b {
t.Materials = append(t.Materials, fmt.Sprintf("%v", x))
}
}
if s, ok := assoc["serialItems"].([]any); ok {
for _, x := range s {
t.Materials = append(t.Materials, fmt.Sprintf("%v", x))
}
}
t.Operator = strAny(assoc["operator"])
}
done := "待完工"
for _, p := range t.Processes {
if p.Result == "NG" {
done = "NG"
}
}
t.Result = done
const tpl = `<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="utf-8">
<title>流程卡 {{.Sn}}</title>
<style>
* { margin:0; padding:0; box-sizing:border-box; }
body { font-family:"Microsoft YaHei",sans-serif; color:#1f2937; padding:24px; }
.card { max-width:900px; margin:0 auto; border:2px solid #333; border-radius:8px; padding:20px 24px; }
h1 { font-size:22px; text-align:center; border-bottom:2px solid #333; padding-bottom:10px; margin-bottom:14px; letter-spacing:4px; }
.meta { display:flex; flex-wrap:wrap; gap:8px 32px; font-size:14px; margin-bottom:14px; }
.meta b { font-weight:700; }
table { width:100%; border-collapse:collapse; font-size:13px; }
th, td { border:1px solid #999; padding:6px 8px; text-align:center; }
th { background:#f3f4f6; }
.ok { color:#15803d; font-weight:700; }
.ng { color:#dc2626; font-weight:700; }
.sub td { text-align:left; color:#374151; background:#fafafa; }
.sign { display:flex; justify-content:space-between; margin-top:22px; font-size:14px; }
.sign div { width:45%; border-top:1px solid #333; padding-top:6px; text-align:center; }
@media print { body { padding:0; } .card { border-color:#000; box-shadow:none; } .no-print { display:none; } }
</style>
</head>
<body>
<div class="card">
<h1>产品流程卡</h1>
<div class="meta">
<span>SN<b>{{.Sn}}</b></span>
<span>工单号:<b>{{.OrderNo}}</b></span>
<span>产品型号:<b>{{.Product}}</b></span>
<span>登记时间:<b>{{.CreatedAt}}</b></span>
<span>判定:<b>{{if eq .Result "NG"}}<span class="ng">NG</span>{{else}}{{.Result}}{{end}}</b></span>
</div>
<table>
<thead><tr><th>工序</th><th>工序名称</th><th>工位</th><th>操作人</th><th>采集参数</th><th>结果</th></tr></thead>
<tbody>
{{range .Processes}}
<tr>
<td>工序{{.Code}}</td><td>{{.Name}}</td><td>{{.Station}}</td><td>{{.Operator}}</td>
<td>{{if .Steps}}{{range .Steps}}{{.Name}}={{.Value}}({{if .OK}}<span class="ok">OK</span>{{else}}<span class="ng">NG</span>{{end}}) {{end}}{{else}}-{{end}}</td>
<td>{{if eq .Result "NG"}}<span class="ng">NG</span>{{else}}OK{{end}}</td>
</tr>
{{end}}
</tbody>
</table>
{{if .Materials}}
<p style="margin-top:10px;font-size:13px;">关联物料/批次:{{range .Materials}}{{.}} {{end}}</p>
{{end}}
<div class="sign">
<div>操作人签字:</div>
<div>检验员签字:</div>
</div>
<div style="text-align:center;margin-top:14px;">
<button class="no-print" onclick="window.print()" style="padding:8px 28px;font-size:15px;">打 印</button>
</div>
</div>
</body>
</html>`
tmpl, err := template.New("card").Parse(tpl)
if err != nil {
return "", err
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, t); err != nil {
return "", err
}
return buf.String(), nil
}
func strAny(v any) string {
if v == nil {
return ""
}
return fmt.Sprintf("%v", v)
}