587 lines
18 KiB
Go
587 lines
18 KiB
Go
package handler
|
||||
|
|
|
|||
|
|
import (
|
|||
|
|
"bytes"
|
|||
|
|
"context"
|
|||
|
|
"fmt"
|
|||
|
|
"html/template"
|
|||
|
|
"net/http"
|
|||
|
|
"sort"
|
|||
|
|
"strconv"
|
|||
|
|
"strings"
|
|||
|
|
"time"
|
|||
|
|
|
|||
|
|
"bj_power_mes/common/httpx"
|
|||
|
|
"bj_power_mes/ent"
|
|||
|
|
"bj_power_mes/ent/bomitem"
|
|||
|
|
"bj_power_mes/ent/processflow"
|
|||
|
|
"bj_power_mes/ent/processstep"
|
|||
|
|
"bj_power_mes/ent/producttype"
|
|||
|
|
"bj_power_mes/ent/station"
|
|||
|
|
"bj_power_mes/ent/stepcriterion"
|
|||
|
|
"bj_power_mes/ent/workorder"
|
|||
|
|
"bj_power_mes/ent/workpieceprocess"
|
|||
|
|
"bj_power_mes/internal/svc"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// ModelProcessCardHandler GET /api/v1/process-card/model?productTypeId=&productCode=[&format=json]
|
|||
|
|
//
|
|||
|
|
// 产品型号级「全景流程卡」:与 SN 级流程卡(按单个工件打印实绩)互补,本卡按**产品型号**聚合
|
|||
|
|
// 该型号的完整制造定义,用于工艺交底/审核/客户查阅:
|
|||
|
|
//
|
|||
|
|
// 产品型号 → 工位组合 → 每工位工艺流程与步骤(含采集方式/检测点/拧紧/考核标准/工艺图纸)
|
|||
|
|
// → 物料清单(按 BOM 分组,含装配工位/单台用量/损耗/需求总量)
|
|||
|
|
// → 检验卡项(所有"检测确认"步骤汇总为检验项清单)
|
|||
|
|
// → 各环节时间(工单计划时间 + 该型号历史实绩的首末作业时间)
|
|||
|
|
//
|
|||
|
|
// 默认返回可打印 HTML(浏览器直接打印/另存 PDF);?format=json 返回结构化数据供页面预览。
|
|||
|
|
func ModelProcessCardHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
|||
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|||
|
|
q := r.URL.Query()
|
|||
|
|
productTypeId := atoiDefault(q.Get("productTypeId"), 0)
|
|||
|
|
productCode := strings.TrimSpace(q.Get("productCode"))
|
|||
|
|
if productTypeId <= 0 && productCode == "" {
|
|||
|
|
httpx.BadRequest(w, "请指定产品型号(productTypeId 或 productCode)")
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
card, err := buildModelCard(r.Context(), svcCtx, productTypeId, productCode)
|
|||
|
|
if err != nil {
|
|||
|
|
httpx.Fail(w, 2403, err.Error())
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
if q.Get("format") == "json" {
|
|||
|
|
httpx.Ok(w, card)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
html, err := renderModelCard(card)
|
|||
|
|
if err != nil {
|
|||
|
|
httpx.Fail(w, 2404, "全景流程卡生成失败:"+err.Error())
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|||
|
|
_, _ = w.Write([]byte(html))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------- 视图数据结构 ----------
|
|||
|
|
|
|||
|
|
type modelCard struct {
|
|||
|
|
ProductName string
|
|||
|
|
ProductCode string
|
|||
|
|
Category string
|
|||
|
|
ProcessSeq string
|
|||
|
|
SeqSource string
|
|||
|
|
StationCount int
|
|||
|
|
OrderNo string
|
|||
|
|
PlanStart string
|
|||
|
|
PlanEnd string
|
|||
|
|
GeneratedAt string
|
|||
|
|
Stations []modelStation
|
|||
|
|
BomGroups []modelBomGroup
|
|||
|
|
CheckItems []modelCheckItem
|
|||
|
|
Summary string
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type modelStation struct {
|
|||
|
|
StationNo int
|
|||
|
|
StationName string
|
|||
|
|
FlowName string
|
|||
|
|
FlowStatus string
|
|||
|
|
PdfFile string
|
|||
|
|
StepCount int
|
|||
|
|
CheckCount int
|
|||
|
|
TorqueCount int
|
|||
|
|
RefTime string
|
|||
|
|
Steps []modelStep
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type modelStep struct {
|
|||
|
|
Seq int
|
|||
|
|
Name string
|
|||
|
|
CollectType string
|
|||
|
|
NeedCheck bool
|
|||
|
|
IsTorque bool
|
|||
|
|
Criteria string
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type modelBomGroup struct {
|
|||
|
|
BomName string
|
|||
|
|
Items []modelBomItem
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type modelBomItem struct {
|
|||
|
|
MaterialCode string
|
|||
|
|
MaterialName string
|
|||
|
|
Spec string
|
|||
|
|
Unit string
|
|||
|
|
ManageMode string
|
|||
|
|
StationNo string
|
|||
|
|
UnitQty string
|
|||
|
|
LossRate string
|
|||
|
|
RequiredQty string
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type modelCheckItem struct {
|
|||
|
|
StationNo int
|
|||
|
|
StationName string
|
|||
|
|
StepName string
|
|||
|
|
Criteria string
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------- 数据聚合 ----------
|
|||
|
|
|
|||
|
|
func buildModelCard(ctx context.Context, svcCtx *svc.ServiceContext, productTypeId int, productCode string) (*modelCard, error) {
|
|||
|
|
client := svcCtx.EntClient
|
|||
|
|
card := &modelCard{GeneratedAt: nowText()}
|
|||
|
|
|
|||
|
|
// 1) 定位产品型号
|
|||
|
|
var pt *ent.ProductType
|
|||
|
|
if productTypeId > 0 {
|
|||
|
|
pt, _ = client.ProductType.Get(ctx, productTypeId)
|
|||
|
|
}
|
|||
|
|
if pt == nil && productCode != "" {
|
|||
|
|
pt, _ = client.ProductType.Query().Where(producttype.Code(productCode)).First(ctx)
|
|||
|
|
}
|
|||
|
|
switch {
|
|||
|
|
case pt != nil:
|
|||
|
|
card.ProductName = pt.Name
|
|||
|
|
card.ProductCode = pt.Code
|
|||
|
|
card.Category = pt.Category
|
|||
|
|
if productCode == "" {
|
|||
|
|
productCode = pt.Code
|
|||
|
|
}
|
|||
|
|
case productCode != "":
|
|||
|
|
card.ProductCode = productCode
|
|||
|
|
card.ProductName = productCode
|
|||
|
|
default:
|
|||
|
|
return nil, fmt.Errorf("产品型号不存在")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 2) 工位组合:优先取该产品最近一张工单的工位组合;无工单则取全产线工位(升序)
|
|||
|
|
seq := []int{}
|
|||
|
|
if productCode != "" {
|
|||
|
|
if wo, err := client.WorkOrder.Query().
|
|||
|
|
Where(workorder.ProductCode(productCode)).
|
|||
|
|
Order(ent.Desc(workorder.FieldID)).First(ctx); err == nil && wo != nil {
|
|||
|
|
card.OrderNo = wo.WorkOrderNo
|
|||
|
|
if wo.PlanStart != nil {
|
|||
|
|
card.PlanStart = wo.PlanStart.Format("2006-01-02 15:04")
|
|||
|
|
}
|
|||
|
|
if wo.PlanEnd != nil {
|
|||
|
|
card.PlanEnd = wo.PlanEnd.Format("2006-01-02 15:04")
|
|||
|
|
}
|
|||
|
|
seq = parseCardSeq(wo.ProcessSeq)
|
|||
|
|
card.SeqSource = "取自工单 " + wo.WorkOrderNo
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if len(seq) == 0 {
|
|||
|
|
sts, _ := client.Station.Query().Order(ent.Asc(station.FieldStationNo)).All(ctx)
|
|||
|
|
for _, st := range sts {
|
|||
|
|
seq = append(seq, st.StationNo)
|
|||
|
|
}
|
|||
|
|
card.SeqSource = "该型号暂无工单,按全产线工位顺序展示"
|
|||
|
|
}
|
|||
|
|
sort.Ints(seq)
|
|||
|
|
card.ProcessSeq = joinInts(seq)
|
|||
|
|
card.StationCount = len(seq)
|
|||
|
|
|
|||
|
|
// 3) 工位 + 工艺流程 + 步骤 + 考核标准
|
|||
|
|
sts, _ := client.Station.Query().Where(station.StationNoIn(seq...)).All(ctx)
|
|||
|
|
stationByName := map[int]*ent.Station{}
|
|||
|
|
flowIds := []int{}
|
|||
|
|
for _, st := range sts {
|
|||
|
|
stationByName[st.StationNo] = st
|
|||
|
|
if st.FlowId > 0 {
|
|||
|
|
flowIds = append(flowIds, st.FlowId)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
flows := map[int]*ent.ProcessFlow{}
|
|||
|
|
if len(flowIds) > 0 {
|
|||
|
|
fl, _ := client.ProcessFlow.Query().Where(processflow.IDIn(flowIds...)).All(ctx)
|
|||
|
|
for _, f := range fl {
|
|||
|
|
flows[f.ID] = f
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
stepsByFlow := map[int][]*ent.ProcessStep{}
|
|||
|
|
if len(flowIds) > 0 {
|
|||
|
|
allSteps, _ := client.ProcessStep.Query().
|
|||
|
|
Where(processstep.FlowIdIn(flowIds...)).
|
|||
|
|
Order(ent.Asc(processstep.FieldFlowId), ent.Asc(processstep.FieldSeq)).All(ctx)
|
|||
|
|
for _, s := range allSteps {
|
|||
|
|
stepsByFlow[s.FlowId] = append(stepsByFlow[s.FlowId], s)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
critByStep := map[int][]*ent.StepCriterion{}
|
|||
|
|
if n, _ := client.StepCriterion.Query().Count(ctx); n > 0 {
|
|||
|
|
allCrit, _ := client.StepCriterion.Query().Order(ent.Asc(stepcriterion.FieldStepId)).All(ctx)
|
|||
|
|
for _, c := range allCrit {
|
|||
|
|
critByStep[c.StepId] = append(critByStep[c.StepId], c)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 该型号历史实绩时间(按工位聚合首末作业时间),供"各环节时间"列
|
|||
|
|
refTimes := stationRefTimes(ctx, client, productCode)
|
|||
|
|
|
|||
|
|
for _, no := range seq {
|
|||
|
|
st := stationByName[no]
|
|||
|
|
ms := modelStation{StationNo: no, StationName: fmt.Sprintf("工位%d", no), RefTime: refTimes[no]}
|
|||
|
|
if st != nil {
|
|||
|
|
if st.Name != "" {
|
|||
|
|
ms.StationName = st.Name
|
|||
|
|
}
|
|||
|
|
if f := flows[st.FlowId]; f != nil {
|
|||
|
|
ms.FlowName = f.Name
|
|||
|
|
ms.FlowStatus = flowStatusCN(f.Status)
|
|||
|
|
ms.PdfFile = f.PdfFile
|
|||
|
|
}
|
|||
|
|
for _, s := range stepsByFlow[st.FlowId] {
|
|||
|
|
crit := criteriaText(critByStep[s.ID])
|
|||
|
|
ms.Steps = append(ms.Steps, modelStep{
|
|||
|
|
Seq: s.Seq,
|
|||
|
|
Name: s.Name,
|
|||
|
|
CollectType: collectTypeCN(s.CollectType),
|
|||
|
|
NeedCheck: s.NeedCheck,
|
|||
|
|
IsTorque: s.IsTorque,
|
|||
|
|
Criteria: crit,
|
|||
|
|
})
|
|||
|
|
if s.NeedCheck {
|
|||
|
|
ms.CheckCount++
|
|||
|
|
card.CheckItems = append(card.CheckItems, modelCheckItem{
|
|||
|
|
StationNo: no, StationName: ms.StationName,
|
|||
|
|
StepName: s.Name, Criteria: crit,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
if s.IsTorque {
|
|||
|
|
ms.TorqueCount++
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
ms.StepCount = len(ms.Steps)
|
|||
|
|
}
|
|||
|
|
card.Stations = append(card.Stations, ms)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 4) 物料清单(按 BOM 名称分组)
|
|||
|
|
if productCode != "" {
|
|||
|
|
items, _ := client.BomItem.Query().
|
|||
|
|
Where(bomitem.ProductCode(productCode)).
|
|||
|
|
Order(ent.Asc(bomitem.FieldBomName), ent.Asc(bomitem.FieldMaterialCode)).All(ctx)
|
|||
|
|
groupIdx := map[string]int{}
|
|||
|
|
for _, it := range items {
|
|||
|
|
name := it.BomName
|
|||
|
|
if name == "" {
|
|||
|
|
name = "默认"
|
|||
|
|
}
|
|||
|
|
gi, ok := groupIdx[name]
|
|||
|
|
if !ok {
|
|||
|
|
card.BomGroups = append(card.BomGroups, modelBomGroup{BomName: name})
|
|||
|
|
gi = len(card.BomGroups) - 1
|
|||
|
|
groupIdx[name] = gi
|
|||
|
|
}
|
|||
|
|
stationNo := "-"
|
|||
|
|
if it.ProcessCode > 0 {
|
|||
|
|
stationNo = fmt.Sprintf("工位%d", it.ProcessCode)
|
|||
|
|
}
|
|||
|
|
card.BomGroups[gi].Items = append(card.BomGroups[gi].Items, modelBomItem{
|
|||
|
|
MaterialCode: it.MaterialCode,
|
|||
|
|
MaterialName: it.MaterialName,
|
|||
|
|
Spec: it.Spec,
|
|||
|
|
Unit: it.Unit,
|
|||
|
|
ManageMode: manageModeCN(it.ManageMode),
|
|||
|
|
StationNo: stationNo,
|
|||
|
|
UnitQty: f2s(it.UnitQty),
|
|||
|
|
LossRate: f2s(it.LossRate),
|
|||
|
|
RequiredQty: f2s(it.RequiredQty),
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
card.Summary = fmt.Sprintf("共 %d 个工位、%d 项检验卡项、%d 份物料清单",
|
|||
|
|
card.StationCount, len(card.CheckItems), len(card.BomGroups))
|
|||
|
|
return card, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// stationRefTimes 该产品型号历史实绩按工位聚合的首末作业时间(无实绩则返回空 map)
|
|||
|
|
func stationRefTimes(ctx context.Context, client *ent.Client, productCode string) map[int]string {
|
|||
|
|
out := map[int]string{}
|
|||
|
|
if productCode == "" {
|
|||
|
|
return out
|
|||
|
|
}
|
|||
|
|
wos, _ := client.WorkOrder.Query().Where(workorder.ProductCode(productCode)).
|
|||
|
|
Order(ent.Desc(workorder.FieldID)).Limit(50).All(ctx)
|
|||
|
|
if len(wos) == 0 {
|
|||
|
|
return out
|
|||
|
|
}
|
|||
|
|
orderNos := make([]string, 0, len(wos))
|
|||
|
|
for _, wo := range wos {
|
|||
|
|
orderNos = append(orderNos, wo.WorkOrderNo)
|
|||
|
|
}
|
|||
|
|
procs, _ := client.WorkpieceProcess.Query().
|
|||
|
|
Where(workpieceprocess.OrderNoIn(orderNos...)).All(ctx)
|
|||
|
|
type span struct {
|
|||
|
|
first, last int64
|
|||
|
|
seen bool
|
|||
|
|
}
|
|||
|
|
m := map[int]*span{}
|
|||
|
|
for _, p := range procs {
|
|||
|
|
s := m[p.ProcessCode]
|
|||
|
|
if s == nil {
|
|||
|
|
s = &span{}
|
|||
|
|
m[p.ProcessCode] = s
|
|||
|
|
}
|
|||
|
|
if p.StartedAt != nil {
|
|||
|
|
t := p.StartedAt.Unix()
|
|||
|
|
if !s.seen || t < s.first {
|
|||
|
|
s.first = t
|
|||
|
|
}
|
|||
|
|
s.seen = true
|
|||
|
|
}
|
|||
|
|
if p.EndedAt != nil {
|
|||
|
|
t := p.EndedAt.Unix()
|
|||
|
|
if t > s.last {
|
|||
|
|
s.last = t
|
|||
|
|
}
|
|||
|
|
s.seen = true
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
for code, s := range m {
|
|||
|
|
if !s.seen {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
if s.last == 0 {
|
|||
|
|
out[code] = tsText(s.first) + " 起(未完成)"
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
out[code] = tsText(s.first) + " → " + tsText(s.last)
|
|||
|
|
}
|
|||
|
|
return out
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------- 文案/格式化助手 ----------
|
|||
|
|
|
|||
|
|
func nowText() string { return time.Now().Format("2006-01-02 15:04:05") }
|
|||
|
|
|
|||
|
|
func tsText(unix int64) string { return time.Unix(unix, 0).Format("2006-01-02 15:04") }
|
|||
|
|
|
|||
|
|
func collectTypeCN(v string) string {
|
|||
|
|
switch v {
|
|||
|
|
case "MANUAL":
|
|||
|
|
return "手工录入"
|
|||
|
|
case "AUTO":
|
|||
|
|
return "自动采集"
|
|||
|
|
case "NONE", "":
|
|||
|
|
return "无需采集"
|
|||
|
|
}
|
|||
|
|
return v
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func flowStatusCN(v string) string {
|
|||
|
|
switch v {
|
|||
|
|
case "ACTIVE":
|
|||
|
|
return "启用"
|
|||
|
|
case "INACTIVE":
|
|||
|
|
return "停用"
|
|||
|
|
}
|
|||
|
|
return v
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func manageModeCN(v string) string {
|
|||
|
|
switch v {
|
|||
|
|
case "1":
|
|||
|
|
return "批次(结构件)"
|
|||
|
|
case "2":
|
|||
|
|
return "序列号(电气件)"
|
|||
|
|
}
|
|||
|
|
return v
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func criteriaText(cs []*ent.StepCriterion) string {
|
|||
|
|
if len(cs) == 0 {
|
|||
|
|
return "-"
|
|||
|
|
}
|
|||
|
|
parts := make([]string, 0, len(cs))
|
|||
|
|
for _, c := range cs {
|
|||
|
|
unit := c.Unit
|
|||
|
|
switch c.Logic {
|
|||
|
|
case "RANGE":
|
|||
|
|
lo, hi := "", ""
|
|||
|
|
if c.Min != nil {
|
|||
|
|
lo = f2s(*c.Min)
|
|||
|
|
}
|
|||
|
|
if c.Max != nil {
|
|||
|
|
hi = f2s(*c.Max)
|
|||
|
|
}
|
|||
|
|
parts = append(parts, fmt.Sprintf("%s ∈ [%s, %s]%s", c.Name, lo, hi, unit))
|
|||
|
|
case "GE":
|
|||
|
|
parts = append(parts, fmt.Sprintf("%s ≥ %s%s", c.Name, f2s(c.Target), unit))
|
|||
|
|
case "LE":
|
|||
|
|
parts = append(parts, fmt.Sprintf("%s ≤ %s%s", c.Name, f2s(c.Target), unit))
|
|||
|
|
case "GT":
|
|||
|
|
parts = append(parts, fmt.Sprintf("%s > %s%s", c.Name, f2s(c.Target), unit))
|
|||
|
|
case "LT":
|
|||
|
|
parts = append(parts, fmt.Sprintf("%s < %s%s", c.Name, f2s(c.Target), unit))
|
|||
|
|
case "EQUAL":
|
|||
|
|
parts = append(parts, fmt.Sprintf("%s = %s%s", c.Name, f2s(c.Target), unit))
|
|||
|
|
default:
|
|||
|
|
parts = append(parts, c.Name)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return strings.Join(parts, ";")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func f2s(v float64) string { return strconv.FormatFloat(v, 'f', -1, 64) }
|
|||
|
|
|
|||
|
|
func joinInts(a []int) string {
|
|||
|
|
p := make([]string, 0, len(a))
|
|||
|
|
for _, v := range a {
|
|||
|
|
p = append(p, strconv.Itoa(v))
|
|||
|
|
}
|
|||
|
|
return strings.Join(p, ",")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------- HTML 渲染 ----------
|
|||
|
|
|
|||
|
|
func renderModelCard(c *modelCard) (string, error) {
|
|||
|
|
const tpl = `<!DOCTYPE html>
|
|||
|
|
<html lang="zh">
|
|||
|
|
<head>
|
|||
|
|
<meta charset="utf-8">
|
|||
|
|
<title>产品型号全景流程卡 {{.ProductCode}}</title>
|
|||
|
|
<style>
|
|||
|
|
* { margin:0; padding:0; box-sizing:border-box; }
|
|||
|
|
body { font-family:"Microsoft YaHei",sans-serif; color:#1f2937; padding:24px; background:#f7f8fa; }
|
|||
|
|
.card { max-width:1080px; margin:0 auto; border:2px solid #333; border-radius:8px; padding:22px 26px; background:#fff; }
|
|||
|
|
h1 { font-size:23px; text-align:center; border-bottom:2px solid #333; padding-bottom:10px; margin-bottom:14px; letter-spacing:5px; }
|
|||
|
|
h2 { font-size:15px; margin:20px 0 8px; border-left:4px solid #1f2937; padding-left:8px; }
|
|||
|
|
.meta { display:flex; flex-wrap:wrap; gap:8px 30px; font-size:14px; margin-bottom:6px; }
|
|||
|
|
.meta b { font-weight:700; }
|
|||
|
|
.hint { font-size:12.5px; color:#6b7280; margin-bottom:4px; }
|
|||
|
|
table { width:100%; border-collapse:collapse; font-size:13px; margin-bottom:4px; }
|
|||
|
|
th, td { border:1px solid #9ca3af; padding:5px 7px; text-align:center; }
|
|||
|
|
th { background:#f3f4f6; }
|
|||
|
|
td.left { text-align:left; }
|
|||
|
|
.ok { color:#15803d; font-weight:700; }
|
|||
|
|
.off { color:#9ca3af; }
|
|||
|
|
.tag { display:inline-block; padding:1px 6px; border-radius:3px; font-size:11.5px; border:1px solid #d1d5db; background:#fafafa; }
|
|||
|
|
.tag.warn { border-color:#f59e0b; color:#b45309; background:#fffbeb; }
|
|||
|
|
.st-name { background:#eef2ff; font-weight:700; text-align:left; }
|
|||
|
|
.sign { display:flex; justify-content:space-between; margin-top:26px; font-size:14px; }
|
|||
|
|
.sign div { width:31%; border-top:1px solid #333; padding-top:6px; text-align:center; }
|
|||
|
|
.bar { text-align:center; margin-top:16px; }
|
|||
|
|
.bar button { padding:8px 30px; font-size:15px; cursor:pointer; }
|
|||
|
|
@media print { body { padding:0; background:#fff; } .card { border-color:#000; border-radius:0; max-width:none; } .no-print { display:none; } }
|
|||
|
|
</style>
|
|||
|
|
</head>
|
|||
|
|
<body>
|
|||
|
|
<div class="card">
|
|||
|
|
<h1>产品型号全景流程卡</h1>
|
|||
|
|
<div class="meta">
|
|||
|
|
<span>产品名称:<b>{{.ProductName}}</b></span>
|
|||
|
|
<span>产品编号:<b>{{.ProductCode}}</b></span>
|
|||
|
|
{{if .Category}}<span>产品分类:<b>{{.Category}}</b></span>{{end}}
|
|||
|
|
<span>工位组合:<b>{{.ProcessSeq}}</b></span>
|
|||
|
|
<span>工位数:<b>{{.StationCount}}</b></span>
|
|||
|
|
</div>
|
|||
|
|
<div class="hint">工位组合来源:{{.SeqSource}};{{.Summary}};生成时间 {{.GeneratedAt}}</div>
|
|||
|
|
{{if .OrderNo}}<div class="hint">参考工单:{{.OrderNo}}{{if .PlanStart}} 计划开始 {{.PlanStart}}{{end}}{{if .PlanEnd}} 计划结束 {{.PlanEnd}}{{end}}</div>{{end}}
|
|||
|
|
|
|||
|
|
<h2>一、工位流程总览</h2>
|
|||
|
|
<table>
|
|||
|
|
<thead><tr><th>工位号</th><th>工位名称</th><th>工艺流程</th><th>流程状态</th><th>步骤数</th><th>检验点</th><th>拧紧步骤</th><th>工艺图纸</th><th>作业时间(本型号实绩)</th></tr></thead>
|
|||
|
|
<tbody>
|
|||
|
|
{{range .Stations}}
|
|||
|
|
<tr>
|
|||
|
|
<td>{{.StationNo}}</td><td class="left">{{.StationName}}</td>
|
|||
|
|
<td class="left">{{if .FlowName}}{{.FlowName}}{{else}}<span class="off">未绑定流程</span>{{end}}</td>
|
|||
|
|
<td>{{if .FlowStatus}}{{.FlowStatus}}{{else}}<span class="off">-</span>{{end}}</td>
|
|||
|
|
<td>{{.StepCount}}</td>
|
|||
|
|
<td>{{if .CheckCount}}<span class="tag warn">{{.CheckCount}}</span>{{else}}0{{end}}</td>
|
|||
|
|
<td>{{.TorqueCount}}</td>
|
|||
|
|
<td class="left">{{if .PdfFile}}{{.PdfFile}}{{else}}<span class="off">-</span>{{end}}</td>
|
|||
|
|
<td>{{if .RefTime}}{{.RefTime}}{{else}}<span class="off">暂无实绩</span>{{end}}</td>
|
|||
|
|
</tr>
|
|||
|
|
{{end}}
|
|||
|
|
</tbody>
|
|||
|
|
</table>
|
|||
|
|
|
|||
|
|
<h2>二、各工位工艺流程与步骤明细</h2>
|
|||
|
|
{{range .Stations}}
|
|||
|
|
{{if .Steps}}
|
|||
|
|
<table>
|
|||
|
|
<thead>
|
|||
|
|
<tr><th colspan="6" class="st-name">工位{{.StationNo}} {{.StationName}} · {{if .FlowName}}{{.FlowName}}{{else}}未绑定流程{{end}}</th></tr>
|
|||
|
|
<tr><th style="width:64px;">步骤号</th><th style="width:26%;">步骤名称</th><th>采集方式</th><th>检测确认</th><th>拧紧采集</th><th>考核标准</th></tr>
|
|||
|
|
</thead>
|
|||
|
|
<tbody>
|
|||
|
|
{{range .Steps}}
|
|||
|
|
<tr>
|
|||
|
|
<td>{{.Seq}}</td><td class="left">{{.Name}}</td><td>{{.CollectType}}</td>
|
|||
|
|
<td>{{if .NeedCheck}}<span class="tag warn">需检验</span>{{else}}否{{end}}</td>
|
|||
|
|
<td>{{if .IsTorque}}<span class="ok">是</span>{{else}}否{{end}}</td>
|
|||
|
|
<td class="left">{{.Criteria}}</td>
|
|||
|
|
</tr>
|
|||
|
|
{{end}}
|
|||
|
|
</tbody>
|
|||
|
|
</table>
|
|||
|
|
{{end}}
|
|||
|
|
{{end}}
|
|||
|
|
|
|||
|
|
<h2>三、物料清单</h2>
|
|||
|
|
{{if .BomGroups}}
|
|||
|
|
{{range .BomGroups}}
|
|||
|
|
<table>
|
|||
|
|
<thead>
|
|||
|
|
<tr><th colspan="9" class="st-name">BOM:{{.BomName}}</th></tr>
|
|||
|
|
<tr><th>物料编码</th><th>物料名称</th><th>规格</th><th>单位</th><th>管理方式</th><th>装配工位</th><th>单台用量</th><th>损耗率(%)</th><th>需求总量</th></tr>
|
|||
|
|
</thead>
|
|||
|
|
<tbody>
|
|||
|
|
{{range .Items}}
|
|||
|
|
<tr>
|
|||
|
|
<td>{{.MaterialCode}}</td><td class="left">{{.MaterialName}}</td><td class="left">{{.Spec}}</td>
|
|||
|
|
<td>{{.Unit}}</td><td>{{.ManageMode}}</td><td>{{.StationNo}}</td>
|
|||
|
|
<td>{{.UnitQty}}</td><td>{{.LossRate}}</td><td>{{.RequiredQty}}</td>
|
|||
|
|
</tr>
|
|||
|
|
{{end}}
|
|||
|
|
</tbody>
|
|||
|
|
</table>
|
|||
|
|
{{end}}
|
|||
|
|
{{else}}
|
|||
|
|
<p class="hint">该产品型号暂无物料清单。</p>
|
|||
|
|
{{end}}
|
|||
|
|
|
|||
|
|
<h2>四、检验卡项(所有「检测确认」步骤)</h2>
|
|||
|
|
{{if .CheckItems}}
|
|||
|
|
<table>
|
|||
|
|
<thead><tr><th style="width:80px;">工位号</th><th>工位名称</th><th>检验步骤</th><th>判定标准</th></tr></thead>
|
|||
|
|
<tbody>
|
|||
|
|
{{range .CheckItems}}
|
|||
|
|
<tr><td>{{.StationNo}}</td><td class="left">{{.StationName}}</td><td class="left">{{.StepName}}</td><td class="left">{{.Criteria}}</td></tr>
|
|||
|
|
{{end}}
|
|||
|
|
</tbody>
|
|||
|
|
</table>
|
|||
|
|
{{else}}
|
|||
|
|
<p class="hint">该产品型号暂无检验卡项(工艺流程未设置「需要检测确认」的步骤)。</p>
|
|||
|
|
{{end}}
|
|||
|
|
|
|||
|
|
<div class="sign">
|
|||
|
|
<div>编制:</div>
|
|||
|
|
<div>审核:</div>
|
|||
|
|
<div>批准:</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="bar no-print">
|
|||
|
|
<button onclick="window.print()">打印 / 另存为 PDF</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</body>
|
|||
|
|
</html>`
|
|||
|
|
|
|||
|
|
tmpl, err := template.New("modelcard").Parse(tpl)
|
|||
|
|
if err != nil {
|
|||
|
|
return "", err
|
|||
|
|
}
|
|||
|
|
var buf bytes.Buffer
|
|||
|
|
if err := tmpl.Execute(&buf, c); err != nil {
|
|||
|
|
return "", err
|
|||
|
|
}
|
|||
|
|
return buf.String(), nil
|
|||
|
|
}
|