209 lines
6.0 KiB
Go
209 lines
6.0 KiB
Go
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)
|
|||
|
|
}
|