feat: 完成产线MES系统全流程功能迭代与优化

本提交完成了多个核心模块的功能完善与业务对齐:
1. 工位终端:固定工位配置、移除自选工位逻辑、适配配置化工位号
2. MES核心:重构工位组合逻辑、新增工单/流程状态管理、补全报工/追溯逻辑
3. WMS客户端:新增修改密码功能、优化帮助弹窗逻辑
4. 文档与权限:补充完整操作手册、统一菜单名称与权限描述
5. 修复多业务校验:排产数量校验、工单状态校验、工位绑定规则
This commit is contained in:
SunYF
2026-08-31 12:58:28 +08:00
parent a3145a8bd6
commit ae4c292487
53 changed files with 1475 additions and 912 deletions
@@ -24,9 +24,10 @@ Internal:
Sim:
Enable: true
# 工位配置:Code 留空则前端可自选工位;ScrewCount 为本工位需拧紧的螺丝颗数
# 工位配置:StationNo 固定本终端工位号(1~12,仅允许运维改配置后重启生效,工人无法在前端修改);
# ScrewCount 为本工位需拧紧的螺丝颗数
Station:
Code: ""
StationNo: 1
ScrewCount: 10
# 工艺文件 PDF 本地预缓存目录(留空关闭缓存)
+1 -1
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>北京动力 · 工位终端</title>
<title>工位终端</title>
</head>
<body>
<div id="app"></div>
@@ -27,16 +27,7 @@
</div>
<div class="field">
<span class="lab">工位</span>
<el-select
v-if="!fixedStation"
v-model="stationNo"
size="large"
class="station-el"
placeholder="请选择工位"
>
<el-option v-for="n in 12" :key="n" :label="'工位 ' + n" :value="n" />
</el-select>
<span v-else class="station-fixed">工位 {{ fixedStation }}</span>
<span class="station-fixed">{{ fixedStation ? '工位 ' + fixedStation : '工位加载中…' }}</span>
</div>
<el-button class="btn-login" type="primary" size="large" :loading="loading" @click="submit">
@@ -55,22 +46,20 @@ import { ElMessage } from 'element-plus'
import { getConfig, login } from '../api'
import { setAuth } from '../utils/request'
const STATION_KEY = 'ws_station_no'
const router = useRouter()
const username = ref('')
const password = ref('')
const stationNo = ref(null)
const fixedStation = ref(0)
const loading = ref(false)
// 工位由服务端配置文件固定(1~12),一个工位一个终端,终端不允许修改工位
onMounted(async () => {
try {
const cfg = await getConfig()
if (cfg?.stationNo > 0) fixedStation.value = cfg.stationNo
} catch {
// 网络异常时忽略,工位仍可自选
// 网络异常时保留 0,登录会提示配置未加载
}
stationNo.value = fixedStation.value || Number(localStorage.getItem(STATION_KEY)) || null
})
async function submit() {
@@ -78,9 +67,8 @@ async function submit() {
ElMessage.warning('请输入账号和密码')
return
}
const sn = fixedStation.value || stationNo.value
if (!sn) {
ElMessage.warning('请选择工位')
if (!fixedStation.value) {
ElMessage.warning('工位配置未加载,请检查服务端配置文件后重启')
return
}
loading.value = true
@@ -88,10 +76,9 @@ async function submit() {
const data = await login({
username: username.value,
password: password.value,
stationNo: sn
stationNo: fixedStation.value
})
setAuth(data.accessToken, data.user)
localStorage.setItem(STATION_KEY, String(sn))
ElMessage.success('登录成功')
router.push('/main')
} catch (e) {
@@ -4,15 +4,10 @@
<header class="top-bar">
<div class="brand">工位终端</div>
<div v-if="!fixedStation" class="dock-select">
<!-- 工位固定由服务端配置文件加载1~12终端不允许修改 -->
<div class="dock-select">
<span class="dock-lab">工位</span>
<el-select v-model="stationNo" size="large" class="dock-el">
<el-option v-for="n in 12" :key="n" :label="'工位 ' + n" :value="n" />
</el-select>
</div>
<div v-else class="dock-select">
<span class="dock-lab">工位</span>
<span class="dock-fixed">{{ fixedStation }}</span>
<span class="dock-fixed">{{ stationNo ? '工位 ' + stationNo : '…' }}</span>
</div>
<div class="spacer"></div>
@@ -169,9 +164,15 @@
<!-- 操作说明帮助 -->
<PageHelp :data="helpWorkstation" />
<!-- 我的工作量弹窗块6 -->
<el-dialog v-model="workload.show" title="我的工作量" width="720px" top="8vh">
<!-- 我的工作量弹窗块6当前登录人所有工位的工作默认当前工位可按工位/时间筛选 -->
<el-dialog v-model="workload.show" title="我的工作量" width="760px" top="8vh">
<el-form inline>
<el-form-item label="工位">
<el-select v-model="workload.stationNo" size="large" style="width:160px">
<el-option label="全部工位" :value="0" />
<el-option v-for="n in 12" :key="n" :label="'工位 ' + n" :value="n" />
</el-select>
</el-form-item>
<el-form-item label="日期范围">
<el-date-picker
v-model="workload.range"
@@ -192,6 +193,7 @@
<el-col :span="6"><el-card shadow="never" class="wl-stat"><div class="wl-label">总合格率</div><div class="wl-value">{{ workload.summary.rate }}</div></el-card></el-col>
</el-row>
<el-table :data="workload.rows" border size="small" max-height="360">
<el-table-column prop="stationNo" label="工位" width="90" align="center" />
<el-table-column prop="date" label="日期" width="110" />
<el-table-column prop="processCode" label="工序" width="80" align="center" />
<el-table-column prop="processName" label="工序名" min-width="130" show-overflow-tooltip />
@@ -209,7 +211,7 @@
</template>
<script setup>
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import PageHelp from '../components/PageHelp.vue'
@@ -227,30 +229,23 @@ import {
} from '../api'
import { clearAuth, getUser } from '../utils/request'
const STATION_KEY = 'ws_station_no'
const router = useRouter()
const user = getUser()
// ---------- 工位配置(固定工位号锁定,否则 1~12 自选,存 localStorage ----------
const fixedStation = ref(0)
const stationNo = ref(Number(localStorage.getItem(STATION_KEY)) || 1)
// ---------- 工位配置(固定工位号:由服务端 yaml 配置在启动时加载,终端不允许修改 ----------
const stationNo = ref(0)
const screwCount = ref(0)
async function loadConfig() {
try {
const cfg = await getConfig()
if (cfg?.stationNo > 0) fixedStation.value = cfg.stationNo
if (cfg?.stationNo > 0) stationNo.value = cfg.stationNo
if (cfg?.screwCount > 0) screwCount.value = cfg.screwCount
} catch {
/* 配置加载失败静默 */
}
}
watch(stationNo, (v) => {
localStorage.setItem(STATION_KEY, String(v))
refreshAll()
})
// ---------- 当前任务(MES ----------
const task = ref(null)
const flowName = computed(() => task.value?.flow?.name || '-')
@@ -534,15 +529,17 @@ async function manualFlush() {
}
}
// ---------- 我的工作量(块6 ----------
// ---------- 我的工作量(块6:当前登录人所有工位的工作,可筛工位(默认当前工位)/时间 ----------
const workload = reactive({
show: false,
stationNo: 0, // 0=全部工位;默认打开时设为当前工位
range: null,
rows: [],
summary: { doneCount: 0, okCount: 0, ngCount: 0, rate: '-' }
})
function openWorkload() {
if (!workload.stationNo) workload.stationNo = stationNo.value // 默认当前工位
workload.show = true
loadWorkload()
}
@@ -551,6 +548,7 @@ async function loadWorkload() {
const [from, to] = workload.range || []
const params = {
operator: user.name || user.username || '',
stationNo: workload.stationNo || 0, // 0=全部工位
from: from || '',
to: to || ''
}
@@ -591,7 +589,6 @@ function logout() {
onMounted(async () => {
await loadConfig()
if (fixedStation.value) stationNo.value = fixedStation.value
refreshAll()
focusScan()
window.addEventListener('focus', focusScan)
@@ -31,10 +31,11 @@ type SqliteConfig struct {
Path string `json:",default=workstation.db"`
}
// StationConfig 工位配置:可从配置文件指定固定工位号(1~12),留空则由终端自选
// StationConfig 工位配置:启动时由 yaml 加载到内存,固定本终端工位号(1~12)。
// 一个工位一个终端,终端不允许修改工位;变更工位只能改配置并重启服务。
type StationConfig struct {
Code string `json:",optional"` // 固定工位号(如 "3");留空则由终端自选 1~12
ScrewCount int `json:",default=10"` // 本工位需拧紧的螺丝颗数(进度 9/10 用)
StationNo int `json:",default=1"` // 固定工位号(1~12),启动时校验范围
ScrewCount int `json:",default=10"` // 本工位需拧紧的螺丝颗数(进度 9/10 用)
}
// PdfCacheConfig 工艺文件 PDF 本地预缓存目录
@@ -5,10 +5,7 @@ import (
"strings"
"time"
"bj_power_workstation/internal/auth"
"bj_power_workstation/internal/svc"
"golang.org/x/crypto/bcrypt"
)
func healthHandler(ctx *svc.ServiceContext) http.HandlerFunc {
@@ -60,11 +57,11 @@ func loginHandler(ctx *svc.ServiceContext) http.HandlerFunc {
}
// configHandler GET /api/config 返回工位配置:
// 固定工位号 stationNoStation.Code 为空则前端可自选 1~12)、本工位需拧紧的螺丝颗数。
// 固定工位号 stationNo1~12,由 yaml 配置在启动时加载,终端不可修改)、本工位需拧紧的螺丝颗数。
func configHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ok(w, map[string]any{
"stationNo": atoi(ctx.Config.Station.Code, 0),
"stationNo": ctx.Config.Station.StationNo,
"screwCount": ctx.Config.Station.ScrewCount,
})
}
@@ -34,17 +34,21 @@ func taskCurrentHandler(ctx *svc.ServiceContext) http.HandlerFunc {
}
}
// workloadHandler 代理 MES GET /api/internal/station/workload?operator=&from=&to=(块6)。
// 工位终端「我的工作量」查询当前登录人数据MES 响应体原样透传。
// workloadHandler 代理 MES GET /api/internal/station/workload?operator=&stationNo=&from=&to=(块6)。
// 工位终端「我的工作量」查询当前登录人全部工位的工作,stationNo 为空=全部工位MES 响应体原样透传。
func workloadHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
operator := strings.TrimSpace(r.URL.Query().Get("operator"))
stationNo := strings.TrimSpace(r.URL.Query().Get("stationNo"))
from := strings.TrimSpace(r.URL.Query().Get("from"))
to := strings.TrimSpace(r.URL.Query().Get("to"))
params := url.Values{}
if operator != "" {
params.Set("operator", operator)
}
if stationNo != "" {
params.Set("stationNo", stationNo)
}
if from != "" {
params.Set("from", from)
}
@@ -28,11 +28,9 @@ type MockSource struct {
dockCode string // 模拟枪挂载的工位
}
// NewMockSource dockCode 为空时回落到 DOCK01
func NewMockSource(db *store.Store, mc *mes.Client, dockCode string) *MockSource {
if dockCode == "" {
dockCode = "DOCK01"
}
// NewMockSource 模拟枪挂载在固定工位(stationNo 1~12),DOCK 编码为 DOCK01~DOCK12
func NewMockSource(db *store.Store, mc *mes.Client, stationNo int) *MockSource {
dockCode := fmt.Sprintf("DOCK%02d", stationNo)
return &MockSource{db: db, mes: mc, dockCode: dockCode}
}
+8 -1
View File
@@ -32,6 +32,13 @@ func main() {
var c config.Config
conf.MustLoad(*configFile, &c)
// 工位号固定(1~12):一个工位一个终端,终端不允许修改工位。
// 启动时加载到内存并强校验,变更工位只能改配置文件后重启服务。
if c.Station.StationNo < 1 || c.Station.StationNo > 12 {
panic(fmt.Sprintf("Station.StationNo 必须在 1~12 之间,当前配置为 %d(工位由配置文件固定,请修改 yaml 后重启)", c.Station.StationNo))
}
logx.Infof("固定工位号: %d, 本工位螺丝颗数: %d", c.Station.StationNo, c.Station.ScrewCount)
staticFS, err := fs.Sub(webFS, "web/static")
if err != nil {
panic(fmt.Sprintf("加载内嵌前端资源失败: %v", err))
@@ -59,7 +66,7 @@ func main() {
// 内置模拟拧紧枪数据源(Sim.Enable=true 时启动;正式丹尼科尔协议接入后由 NetSource 替代)
if c.Sim.Enable {
go tightening.NewMockSource(st, mc, c.Station.Code).Start(rootCtx)
go tightening.NewMockSource(st, mc, c.Station.StationNo).Start(rootCtx)
}
// 工艺文件 PDF 本地预缓存
+1 -1
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>北京动力 · 工位终端</title>
<title>工位终端</title>
<script type="module" crossorigin src="/assets/index-CpYENLUl.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Dz4RGjfJ.css">
</head>