feat: 五项目业务实现并对接完成
- MES(B): 工单/BOM/备料/工序字典/PLC下发/拧紧/扫码报工/半成品/AGV/追溯逻辑,WMS与海康RCS客户端,看板Redis缓存API(概览/设备/进度/报警/趋势)+SSE - WMS(C): JWT滑动续签、Excel导入、盘点、内部API、种子数据、独立Postgres配置 - WMS客户端(E): Go网关8891反向代理+内嵌Vue3十页 - 工位终端(D): SQLite本地缓存+模拟拧紧源+内嵌Vue3页面 - Dashboard(A): 看板数据改接MES内部缓存API,SSE实时刷新+vite代理 - 清理各项目球形磨遗留代码,新增部署手册.md
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>北京动力 · 工位终端</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "bj_power_workstation_frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.7.9",
|
||||
"element-plus": "^2.9.3",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"vite": "^6.0.7"
|
||||
}
|
||||
}
|
||||
+1249
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
||||
# pnpm 11 构建脚本白名单(仅允许 esbuild 运行 install/postinstall 脚本)
|
||||
allowBuilds:
|
||||
esbuild: true
|
||||
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<el-config-provider :locale="zhCn">
|
||||
<router-view />
|
||||
</el-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||
</script>
|
||||
@@ -0,0 +1,26 @@
|
||||
import request from '../utils/request'
|
||||
|
||||
// 登录
|
||||
export const login = (data) => request.post('/api/auth/login', data)
|
||||
|
||||
// 当前工单任务(代理 MES)
|
||||
export const getTaskCurrent = (dockCode) =>
|
||||
request.get('/api/task/current', { params: { dockCode } })
|
||||
|
||||
// 拧紧结果列表(本地 SQLite)
|
||||
export const getTighteningList = (params) => request.get('/api/tightening/list', { params })
|
||||
|
||||
// 工序完成上报
|
||||
export const reportProcessDone = (data) => request.post('/api/report/process-done', data)
|
||||
|
||||
// 暂存退库上报
|
||||
export const reportTempStore = (data) => request.post('/api/report/temp-store', data)
|
||||
|
||||
// 手动触发一轮同步
|
||||
export const syncFlush = () => request.post('/api/sync/flush')
|
||||
|
||||
// 待同步统计
|
||||
export const getSyncStats = () => request.get('/api/sync/stats')
|
||||
|
||||
// 工艺 PDF 免鉴权直链(iframe / window.open 用)
|
||||
export const pdfOpenUrl = (name) => '/pdfopen/' + encodeURIComponent(name)
|
||||
@@ -0,0 +1,7 @@
|
||||
import { createApp } from 'vue'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
createApp(App).use(router).use(ElementPlus).mount('#app')
|
||||
@@ -0,0 +1,31 @@
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
import { getToken } from '../utils/request'
|
||||
|
||||
// hash 路由:静态托管下刷新/深链稳定,不需要服务端兜底配置
|
||||
const routes = [
|
||||
{ path: '/', redirect: '/main' },
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: () => import('../views/Login.vue')
|
||||
},
|
||||
{
|
||||
path: '/main',
|
||||
name: 'Main',
|
||||
component: () => import('../views/Main.vue')
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
router.beforeEach((to) => {
|
||||
const hasToken = !!getToken()
|
||||
if (!hasToken && to.path !== '/login') return '/login'
|
||||
if (hasToken && to.path === '/login') return '/main'
|
||||
return true
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,92 @@
|
||||
import axios from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
/**
|
||||
* 统一请求封装(风格同 bj_power_wms_client 前端 E 工程):
|
||||
* - 请求头自动携带 Bearer token
|
||||
* - 响应头 X-Renewed-Token 自动保存(滑动续签)
|
||||
* - 401 清除登录态并跳转登录页
|
||||
* - 业务 code!==0 弹错并 reject;data 缺省时返回原响应体
|
||||
*/
|
||||
const TOKEN_KEY = 'ws_token'
|
||||
const USER_KEY = 'ws_user'
|
||||
|
||||
export function getToken() {
|
||||
return localStorage.getItem(TOKEN_KEY) || ''
|
||||
}
|
||||
|
||||
export function setToken(token) {
|
||||
localStorage.setItem(TOKEN_KEY, token)
|
||||
}
|
||||
|
||||
export function getUser() {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(USER_KEY)) || {}
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
export function setAuth(token, user) {
|
||||
localStorage.setItem(TOKEN_KEY, token)
|
||||
localStorage.setItem(USER_KEY, JSON.stringify(user || {}))
|
||||
}
|
||||
|
||||
export function clearAuth() {
|
||||
localStorage.removeItem(TOKEN_KEY)
|
||||
localStorage.removeItem(USER_KEY)
|
||||
}
|
||||
|
||||
const service = axios.create({
|
||||
baseURL: '',
|
||||
timeout: 30000
|
||||
})
|
||||
|
||||
service.interceptors.request.use((config) => {
|
||||
const token = getToken()
|
||||
if (token) config.headers.Authorization = 'Bearer ' + token
|
||||
return config
|
||||
})
|
||||
|
||||
service.interceptors.response.use(
|
||||
(response) => {
|
||||
// 滑动续签:后端剩余有效期不足一半时下发新 token
|
||||
const renewed = response.headers?.['x-renewed-token']
|
||||
if (renewed) setToken(renewed)
|
||||
|
||||
const d = response.data
|
||||
if (d && typeof d === 'object' && 'code' in d) {
|
||||
if (d.code !== 0) {
|
||||
const err = new Error(d.message || '请求失败')
|
||||
showErrThrottled(err.message)
|
||||
return Promise.reject(err)
|
||||
}
|
||||
return d.data !== undefined ? d.data : d
|
||||
}
|
||||
return d
|
||||
},
|
||||
(error) => {
|
||||
if (error.response && error.response.status === 401) {
|
||||
clearAuth()
|
||||
window.location.hash = '#/login'
|
||||
error.message = '登录已失效,请重新登录'
|
||||
} else {
|
||||
error.message =
|
||||
error.response?.data?.message ||
|
||||
(error.code === 'ECONNABORTED' ? '请求超时' : '网络异常:' + (error.message || '未知错误'))
|
||||
}
|
||||
showErrThrottled(error.message)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
let lastErrAt = 0
|
||||
function showErrThrottled(msg) {
|
||||
// 简单节流,避免轮询接口失败刷屏(3 秒内只提示一次)
|
||||
const now = Date.now()
|
||||
if (now - lastErrAt < 3000) return
|
||||
lastErrAt = now
|
||||
ElMessage.error(msg)
|
||||
}
|
||||
|
||||
export default service
|
||||
@@ -0,0 +1,134 @@
|
||||
<template>
|
||||
<div class="login-page">
|
||||
<div class="login-card">
|
||||
<h1 class="title">北京动力 · 工位终端</h1>
|
||||
<p class="subtitle">产线触屏终端(D 口,端口 8892)</p>
|
||||
|
||||
<div class="field">
|
||||
<span class="lab">账号</span>
|
||||
<el-input
|
||||
v-model.trim="username"
|
||||
size="large"
|
||||
placeholder="请输入账号"
|
||||
clearable
|
||||
@keyup.enter="submit"
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<span class="lab">密码</span>
|
||||
<el-input
|
||||
v-model="password"
|
||||
size="large"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="请输入密码"
|
||||
@keyup.enter="submit"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-button class="btn-login" type="primary" size="large" :loading="loading" @click="submit">
|
||||
登 录
|
||||
</el-button>
|
||||
|
||||
<p class="hint">内置账号:admin / admin123 operator1 / 123456</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { login } from '../api'
|
||||
import { setAuth } from '../utils/request'
|
||||
|
||||
const router = useRouter()
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
const loading = ref(false)
|
||||
|
||||
async function submit() {
|
||||
if (!username.value || !password.value) {
|
||||
ElMessage.warning('请输入账号和密码')
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await login({ username: username.value, password: password.value })
|
||||
setAuth(data.token, data.user)
|
||||
ElMessage.success('登录成功')
|
||||
router.push('/main')
|
||||
} catch (e) {
|
||||
// 错误提示已由 request 封装统一弹出
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-page {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #0f2b4c 0%, #14385f 60%, #1a4a7a 100%);
|
||||
}
|
||||
|
||||
.login-card {
|
||||
width: 480px;
|
||||
padding: 48px 56px;
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0 0 8px;
|
||||
font-size: 30px;
|
||||
text-align: center;
|
||||
color: #16324f;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 0 0 32px;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
color: #7a8aa0;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.lab {
|
||||
width: 64px;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #16324f;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.field :deep(.el-input__inner) {
|
||||
height: 52px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.btn-login {
|
||||
width: calc(100% - 80px);
|
||||
margin-left: 80px;
|
||||
height: 54px;
|
||||
font-size: 24px;
|
||||
letter-spacing: 12px;
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin-top: 26px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: #9aa7b8;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,698 @@
|
||||
<template>
|
||||
<div class="page" tabindex="-1">
|
||||
<!-- 顶部:标题 / 工位选择 / 登录人 / 同步状态角标 -->
|
||||
<header class="top-bar">
|
||||
<div class="brand">北京动力 · 工位终端</div>
|
||||
|
||||
<div class="dock-select">
|
||||
<span class="dock-lab">工位</span>
|
||||
<el-select v-model="dock" size="large" class="dock-el">
|
||||
<el-option v-for="d in docks" :key="d" :label="d" :value="d" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<div class="spacer"></div>
|
||||
|
||||
<div class="sync-badge" @click="manualFlush">
|
||||
<el-tag v-if="pendingTotal > 0" type="warning" size="large" effect="dark" round>
|
||||
待同步 {{ pendingTotal }} 条
|
||||
</el-tag>
|
||||
<el-tag v-else type="success" size="large" effect="dark" round>同步正常</el-tag>
|
||||
</div>
|
||||
|
||||
<div class="user-box">
|
||||
<span class="user-name">{{ user.realName || user.username || '-' }}</span>
|
||||
<el-button size="large" text type="primary" @click="logout">退出</el-button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- NG 告警横幅 -->
|
||||
<transition name="fade">
|
||||
<div v-if="hasNg" class="alert-banner">⚠ 存在NG,请复检!</div>
|
||||
</transition>
|
||||
|
||||
<!-- 扫码区(常驻聚焦) -->
|
||||
<section class="scan-bar">
|
||||
<span class="sn-label">当前工件 SN:</span>
|
||||
<span class="sn-value">{{ currentSn || '——' }}</span>
|
||||
<el-input
|
||||
ref="scanRef"
|
||||
v-model.trim="scanInput"
|
||||
class="scan-input"
|
||||
size="large"
|
||||
placeholder="请扫描工件SN条码"
|
||||
clearable
|
||||
@keyup.enter="onScan"
|
||||
/>
|
||||
<el-button size="large" @click="focusScan">聚焦扫码</el-button>
|
||||
</section>
|
||||
|
||||
<!-- 主内容:任务区 + 工艺PDF + 拧紧结果 -->
|
||||
<main class="content">
|
||||
<!-- 任务区 -->
|
||||
<aside class="card task-card">
|
||||
<h3 class="card-title">当前工单</h3>
|
||||
<template v-if="task">
|
||||
<div class="order-no">{{ task.orderNo || '-' }}</div>
|
||||
<div class="kv"><span>产品型号</span><b>{{ task.productModel || '-' }}</b></div>
|
||||
<div class="kv"><span>进度</span><b>{{ progressDone }} / {{ progressTotal }} 件</b></div>
|
||||
<ul class="steps">
|
||||
<li
|
||||
v-for="(s, i) in taskSteps"
|
||||
:key="i"
|
||||
:class="{ done: isStepDone(s) }"
|
||||
>
|
||||
<span class="step-flag">{{ isStepDone(s) ? '✔' : i + 1 }}</span>
|
||||
<span class="step-name">{{ s.name || s.processCode || ('工序' + (i + 1)) }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
<el-empty v-else description="当前工位暂无任务" :image-size="90" />
|
||||
</aside>
|
||||
|
||||
<section class="right-col">
|
||||
<!-- 工艺 PDF 区 -->
|
||||
<div v-if="pdfFile" class="card pdf-card">
|
||||
<div class="pdf-head">
|
||||
<h3 class="card-title">工艺图纸 · {{ pdfFile }}</h3>
|
||||
<el-button type="primary" size="large" @click="openPdf">放大查看</el-button>
|
||||
</div>
|
||||
<iframe v-if="pdfUrl" :src="pdfUrl" class="pdf-frame" title="工艺图纸"></iframe>
|
||||
</div>
|
||||
|
||||
<!-- 拧紧结果区 -->
|
||||
<div class="card table-card" :class="{ 'table-card-fill': !pdfFile }">
|
||||
<h3 class="card-title">拧紧结果(实时)</h3>
|
||||
<el-table
|
||||
:data="tightRows"
|
||||
height="100%"
|
||||
size="large"
|
||||
:row-class-name="rowClass"
|
||||
empty-text="暂无拧紧数据"
|
||||
>
|
||||
<el-table-column label="时间" width="200">
|
||||
<template #default="{ row }">{{ fmtTs(row.createdAt) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="screwNo" label="螺丝" width="120" align="center" />
|
||||
<el-table-column label="扭矩 (Nm)" width="160" align="center">
|
||||
<template #default="{ row }">{{ row.torque.toFixed(1) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="角度 (°)" width="140" align="center">
|
||||
<template #default="{ row }">{{ Math.round(row.angle) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="结果" align="center">
|
||||
<template #default="{ row }">
|
||||
<span :class="row.result === 'OK' ? 'res-ok' : 'res-ng'">{{ row.result }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- 底部三大按钮 -->
|
||||
<footer class="actions">
|
||||
<el-button class="big-btn primary" size="large" @click="onProcessDone">工序完成上报</el-button>
|
||||
<el-button class="big-btn warning" size="large" @click="onTempStore">暂存退库</el-button>
|
||||
<el-button class="big-btn success" size="large" @click="manualFlush">重试同步</el-button>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
getSyncStats,
|
||||
getTaskCurrent,
|
||||
getTighteningList,
|
||||
pdfOpenUrl,
|
||||
reportProcessDone,
|
||||
reportTempStore,
|
||||
syncFlush
|
||||
} from '../api'
|
||||
import { clearAuth, getUser } from '../utils/request'
|
||||
|
||||
const DOCK_KEY = 'ws_dock'
|
||||
|
||||
const router = useRouter()
|
||||
const user = getUser()
|
||||
|
||||
// ---------- 工位 ----------
|
||||
const dockCount = 21 // DOCK01 ~ DOCK21
|
||||
const docks = Array.from({ length: dockCount }, (_, i) => 'DOCK' + String(i + 1).padStart(2, '0'))
|
||||
const dock = ref(localStorage.getItem(DOCK_KEY) || 'DOCK01')
|
||||
watch(dock, (v) => {
|
||||
localStorage.setItem(DOCK_KEY, v)
|
||||
refreshAll()
|
||||
})
|
||||
|
||||
// ---------- 同步状态角标 ----------
|
||||
const stats = ref({ pendingTorque: 0, pendingQueue: 0 })
|
||||
const pendingTotal = computed(() => stats.value.pendingTorque + stats.value.pendingQueue)
|
||||
|
||||
async function loadStats() {
|
||||
try {
|
||||
stats.value = await getSyncStats()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// ---------- 当前任务 ----------
|
||||
const task = ref(null)
|
||||
const taskSteps = computed(() => {
|
||||
const t = task.value
|
||||
if (!t) return []
|
||||
return Array.isArray(t.steps) ? t.steps : Array.isArray(t.stepList) ? t.stepList : []
|
||||
})
|
||||
const progressDone = computed(() => {
|
||||
const p = task.value?.progress
|
||||
if (p && typeof p === 'object') return Number(p.done ?? 0)
|
||||
return Number(task.value?.doneCount ?? 0)
|
||||
})
|
||||
const progressTotal = computed(() => {
|
||||
const p = task.value?.progress
|
||||
if (p && typeof p === 'object') return Number(p.total ?? 0)
|
||||
return Number(task.value?.totalCount ?? 0)
|
||||
})
|
||||
|
||||
function isStepDone(s) {
|
||||
return s && (s.done === true || s.status === 'done' || s.finished === true)
|
||||
}
|
||||
|
||||
function firstPendingStepNo() {
|
||||
for (let i = 0; i < taskSteps.value.length; i++) {
|
||||
if (!isStepDone(taskSteps.value[i])) {
|
||||
const s = taskSteps.value[i]
|
||||
return String(s.no ?? s.code ?? i + 1)
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
async function loadTask() {
|
||||
try {
|
||||
task.value = await getTaskCurrent(dock.value)
|
||||
} catch {
|
||||
task.value = null
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 工艺 PDF ----------
|
||||
const pdfFile = computed(() => {
|
||||
const f = task.value?.processFile || task.value?.processPdf
|
||||
return typeof f === 'string' && f.trim() !== '' ? f.trim() : ''
|
||||
})
|
||||
const pdfUrl = computed(() => (pdfFile.value ? pdfOpenUrl(pdfFile.value) : ''))
|
||||
|
||||
function openPdf() {
|
||||
if (pdfUrl.value) window.open(pdfUrl.value, '_blank')
|
||||
}
|
||||
|
||||
// ---------- 扫码 ----------
|
||||
const scanRef = ref()
|
||||
const scanInput = ref('')
|
||||
const currentSn = ref('')
|
||||
|
||||
function focusScan() {
|
||||
scanRef.value?.focus?.()
|
||||
}
|
||||
|
||||
function onScan() {
|
||||
const sn = scanInput.value.trim()
|
||||
if (!sn) return
|
||||
currentSn.value = sn
|
||||
scanInput.value = ''
|
||||
loadTight()
|
||||
}
|
||||
|
||||
watch(currentSn, () => loadTight())
|
||||
|
||||
// ---------- 拧紧结果 ----------
|
||||
const tightRows = ref([])
|
||||
const hasNg = computed(() => tightRows.value.some((r) => r.result === 'NG'))
|
||||
|
||||
async function loadTight() {
|
||||
try {
|
||||
const data = await getTighteningList({ sn: currentSn.value, limit: 50 })
|
||||
tightRows.value = Array.isArray(data?.list) ? data.list : []
|
||||
} catch {
|
||||
/* 轮询失败静默,避免刷屏 */
|
||||
}
|
||||
}
|
||||
|
||||
function rowClass({ row }) {
|
||||
return row.result === 'NG' ? 'ng-row' : ''
|
||||
}
|
||||
|
||||
function fmtTs(ts) {
|
||||
if (!ts) return '-'
|
||||
const d = new Date(Number(ts))
|
||||
const p = (n) => String(n).padStart(2, '0')
|
||||
return `${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`
|
||||
}
|
||||
|
||||
// ---------- 底部按钮 ----------
|
||||
async function onProcessDone() {
|
||||
focusScan()
|
||||
try {
|
||||
let value
|
||||
try {
|
||||
const res = await ElMessageBox.prompt(
|
||||
`确认对工位 ${dock.value} 执行「工序完成上报」?请输入当前工序号`,
|
||||
'工序完成上报',
|
||||
{
|
||||
inputValue: firstPendingStepNo(),
|
||||
inputPlaceholder: '如:OP10',
|
||||
confirmButtonText: '确 认',
|
||||
cancelButtonText: '取 消',
|
||||
inputValidator: (v) => (v && String(v).trim() !== '' ? true : '请输入工序号'),
|
||||
distinguishCancelAndClose: true
|
||||
}
|
||||
)
|
||||
value = String(res.value || '').trim()
|
||||
} catch (e) {
|
||||
return // 用户取消/关闭
|
||||
}
|
||||
|
||||
const data = await reportProcessDone({
|
||||
dockCode: dock.value,
|
||||
orderNo: task.value?.orderNo || '',
|
||||
sn: currentSn.value,
|
||||
processCode: value,
|
||||
operator: user.realName || user.username || ''
|
||||
})
|
||||
if (data?.queued) {
|
||||
ElMessage.warning('MES 暂不可达,已离线缓存,稍后自动重传')
|
||||
} else {
|
||||
ElMessage.success('工序完成上报成功')
|
||||
}
|
||||
loadTask()
|
||||
loadStats()
|
||||
} catch (e) {
|
||||
/* 统一错误提示已在 request 中弹出 */
|
||||
}
|
||||
}
|
||||
|
||||
async function onTempStore() {
|
||||
focusScan()
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确认将当前工件「${currentSn.value || '未扫码'}」暂存退库?`,
|
||||
'暂存退库',
|
||||
{ confirmButtonText: '确认暂存', cancelButtonText: '取消', type: 'warning' }
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const data = await reportTempStore({
|
||||
dockCode: dock.value,
|
||||
orderNo: task.value?.orderNo || '',
|
||||
sn: currentSn.value,
|
||||
operator: user.realName || user.username || ''
|
||||
})
|
||||
if (data?.queued) {
|
||||
ElMessage.warning('MES 暂不可达,已离线缓存,稍后自动重传')
|
||||
} else {
|
||||
ElMessage.success('暂存退库已上报')
|
||||
}
|
||||
loadTask()
|
||||
loadStats()
|
||||
} catch (e) {
|
||||
/* 统一错误提示 */
|
||||
}
|
||||
}
|
||||
|
||||
async function manualFlush() {
|
||||
try {
|
||||
const data = await syncFlush()
|
||||
ElMessage.success(`本轮同步成功 ${data?.count ?? 0} 条`)
|
||||
loadStats()
|
||||
} catch (e) {
|
||||
/* 统一错误提示 */
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 轮询与生命周期 ----------
|
||||
let timerTask = null
|
||||
let timerTight = null
|
||||
let timerStats = null
|
||||
|
||||
function refreshAll() {
|
||||
loadTask()
|
||||
loadTight()
|
||||
loadStats()
|
||||
}
|
||||
|
||||
function logout() {
|
||||
clearAuth()
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
refreshAll()
|
||||
focusScan()
|
||||
window.addEventListener('focus', focusScan)
|
||||
|
||||
timerTask = setInterval(loadTask, 5000) // 任务 5 秒
|
||||
timerTight = setInterval(loadTight, 2000) // 拧紧结果 2 秒
|
||||
timerStats = setInterval(loadStats, 10000) // 同步统计 10 秒
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearInterval(timerTask)
|
||||
clearInterval(timerTight)
|
||||
clearInterval(timerStats)
|
||||
window.removeEventListener('focus', focusScan)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 触屏终端:大字号、高对比、无滚动闪烁 */
|
||||
.page {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #eef2f7;
|
||||
overflow: hidden;
|
||||
font-family: 'Microsoft YaHei', system-ui, sans-serif;
|
||||
}
|
||||
|
||||
/* ---- 顶部 ---- */
|
||||
.top-bar {
|
||||
height: 76px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
padding: 0 28px;
|
||||
background: #16324f;
|
||||
color: #fff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.brand {
|
||||
font-size: 30px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 2px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dock-select {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.dock-lab {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.dock-el {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.dock-el :deep(.el-input__inner) {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sync-badge {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sync-badge :deep(.el-tag) {
|
||||
font-size: 18px;
|
||||
height: 40px;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.user-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.user-box :deep(.el-button) {
|
||||
font-size: 18px;
|
||||
color: #9fd0ff;
|
||||
}
|
||||
|
||||
/* ---- NG 告警横幅 ---- */
|
||||
.alert-banner {
|
||||
flex-shrink: 0;
|
||||
background: #d80f16;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
font-size: 34px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 8px;
|
||||
padding: 14px 0;
|
||||
animation: blink 1.2s infinite;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.55; }
|
||||
}
|
||||
|
||||
.fade-enter-active, .fade-leave-active { transition: opacity 0.3s; }
|
||||
.fade-enter-from, .fade-leave-to { opacity: 0; }
|
||||
|
||||
/* ---- 扫码区 ---- */
|
||||
.scan-bar {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
padding: 12px 28px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #dde4ee;
|
||||
}
|
||||
|
||||
.sn-label {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
color: #16324f;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sn-value {
|
||||
min-width: 220px;
|
||||
font-size: 26px;
|
||||
font-weight: bold;
|
||||
color: #d80f16;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.scan-input {
|
||||
flex: 1;
|
||||
max-width: 640px;
|
||||
}
|
||||
|
||||
.scan-bar :deep(.el-input__inner) {
|
||||
height: 52px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
/* ---- 主内容 ---- */
|
||||
.content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
padding: 20px 28px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 2px 10px rgba(22, 50, 79, 0.08);
|
||||
padding: 20px 24px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
margin: 0 0 16px;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #16324f;
|
||||
}
|
||||
|
||||
.task-card {
|
||||
width: 360px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.order-no {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
color: #16324f;
|
||||
word-break: break-all;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.kv {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 22px;
|
||||
margin-bottom: 10px;
|
||||
color: #5a6a80;
|
||||
}
|
||||
|
||||
.kv b {
|
||||
color: #16324f;
|
||||
}
|
||||
|
||||
.steps {
|
||||
list-style: none;
|
||||
margin: 14px 0 0;
|
||||
padding: 0;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.steps li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 20px;
|
||||
padding: 9px 4px;
|
||||
border-bottom: 1px dashed #e6ecf4;
|
||||
color: #16324f;
|
||||
}
|
||||
|
||||
.steps li.done {
|
||||
color: #2aa861;
|
||||
}
|
||||
|
||||
.steps li.done .step-name {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.step-flag {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #eaf1f8;
|
||||
font-weight: bold;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.steps li.done .step-flag {
|
||||
background: #2aa861;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.right-col {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.pdf-card {
|
||||
height: 38%;
|
||||
min-height: 240px;
|
||||
}
|
||||
|
||||
.pdf-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.pdf-head .card-title {
|
||||
margin-bottom: 8px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.pdf-frame {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
border: 1px solid #dde4ee;
|
||||
border-radius: 8px;
|
||||
background: #fafbfd;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
flex: 1;
|
||||
min-height: 220px;
|
||||
}
|
||||
|
||||
.table-card :deep(.el-table) {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.table-card :deep(.el-table th.el-table__cell) {
|
||||
font-size: 20px;
|
||||
color: #16324f;
|
||||
}
|
||||
|
||||
.res-ok {
|
||||
color: #2aa861;
|
||||
font-size: 26px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.res-ng {
|
||||
color: #d80f16;
|
||||
font-size: 26px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
:deep(.ng-row) {
|
||||
--el-table-tr-bg-color: #ffecec;
|
||||
background: #ffecec !important;
|
||||
}
|
||||
|
||||
/* ---- 底部三大按钮 ---- */
|
||||
.actions {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 48px;
|
||||
padding: 14px 0 18px;
|
||||
background: #fff;
|
||||
box-shadow: 0 -2px 10px rgba(22, 50, 79, 0.06);
|
||||
}
|
||||
|
||||
.big-btn {
|
||||
width: 320px;
|
||||
height: 72px;
|
||||
font-size: 27px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 6px;
|
||||
border-radius: 12px;
|
||||
color: #fff;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.big-btn.primary { background: #1668dc; }
|
||||
.big-btn.primary:hover { background: #0e57c3; }
|
||||
|
||||
.big-btn.warning { background: #e6870f; }
|
||||
.big-btn.warning:hover { background: #cf7508; }
|
||||
|
||||
.big-btn.success { background: #2aa861; }
|
||||
.big-btn.success:hover { background: #239154; }
|
||||
</style>
|
||||
@@ -0,0 +1,18 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
// 构建输出到 Go 服务内嵌目录 ../web/static(emptyOutDir 清空旧产物)
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
build: {
|
||||
outDir: '../web/static',
|
||||
emptyOutDir: true
|
||||
},
|
||||
server: {
|
||||
port: 8893,
|
||||
proxy: {
|
||||
'/api': 'http://127.0.0.1:8892',
|
||||
'/pdfopen': 'http://127.0.0.1:8892'
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user