本次提交包含以下核心变更: 1. 新增按钮级权限控制框架与前端权限校验 2. 新增工位管理、工艺流程、巡检终端等核心业务模块 3. 完善用户体系,支持工位终端专属登录权限 4. 新增文件上传下载、报表查询等配套功能 5. 修复多系统兼容性与交互体验问题 6. 完成所有文档与配置项的规范化更新
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)
|
||
}
|