feat: 完成MES工位终端系统全量功能迭代

本次提交包含以下核心变更:
1. 新增按钮级权限控制框架与前端权限校验
2. 新增工位管理、工艺流程、巡检终端等核心业务模块
3. 完善用户体系,支持工位终端专属登录权限
4. 新增文件上传下载、报表查询等配套功能
5. 修复多系统兼容性与交互体验问题
6. 完成所有文档与配置项的规范化更新
This commit is contained in:
SunYF
2026-08-31 08:06:55 +08:00
parent dab03ac0cf
commit 4ec6784629
99 changed files with 17436 additions and 479 deletions
+4 -3
View File
@@ -8,7 +8,7 @@
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查询</el-button>
<el-button @click="openAdd">添加物料</el-button>
<el-button v-if="can('produce.bom:edit')" @click="openAdd">添加物料</el-button>
</el-form-item>
</el-form>
@@ -25,7 +25,7 @@
<el-table-column prop="requiredQty" label="需求量" width="90" />
<el-table-column label="操作" width="90">
<template #default="{row}">
<el-button size="small" type="danger" @click="remove(row)">移除</el-button>
<el-button v-if="can('produce.bom:edit')" size="small" type="danger" @click="remove(row)">移除</el-button>
</template>
</el-table-column>
</el-table>
@@ -48,7 +48,7 @@
</el-form>
<template #footer>
<el-button @click="add.show=false">取消</el-button>
<el-button type="primary" @click="save">保存</el-button>
<el-button v-if="can('produce.bom:edit')" type="primary" @click="save">保存</el-button>
</template>
</el-dialog>
</el-card>
@@ -59,6 +59,7 @@
import { reactive, ref, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import request from '../utils/request'
import { can } from '../utils/perm'
import PageHelp from '../components/PageHelp.vue'
import { helpBom } from '../help'
@@ -11,7 +11,7 @@
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查询</el-button>
<el-button type="success" @click="openAdd">新建排产</el-button>
<el-button v-if="can('produce.workorder:dailyplan')" type="success" @click="openAdd">新建排产</el-button>
</el-form-item>
</el-form>
@@ -26,8 +26,8 @@
<el-table-column prop="operator" label="操作人" min-width="90" />
<el-table-column label="操作" width="160">
<template #default="{ row }">
<el-button size="small" @click="openEdit(row)">编辑</el-button>
<el-button size="small" type="danger" @click="del(row)">删除</el-button>
<el-button v-if="can('produce.workorder:dailyplan')" size="small" @click="openEdit(row)">编辑</el-button>
<el-button v-if="can('produce.workorder:dailyplan')" size="small" type="danger" @click="del(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
@@ -48,7 +48,7 @@
</el-form>
<template #footer>
<el-button @click="dialog.show=false">取消</el-button>
<el-button type="primary" @click="save">保存</el-button>
<el-button v-if="can('produce.workorder:dailyplan')" type="primary" @click="save">保存</el-button>
</template>
</el-dialog>
</el-card>
@@ -59,6 +59,7 @@
import { reactive, ref, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import request from '../utils/request'
import { can } from '../utils/perm'
import PageHelp from '../components/PageHelp.vue'
import { helpDailyPlan } from '../help'
+361
View File
@@ -0,0 +1,361 @@
<template>
<el-card class="inspect-card">
<el-tabs v-model="tab">
<!-- 1. 开工签到 -->
<el-tab-pane label="开工签到" name="CHECKIN">
<el-form label-position="top" class="pad-form" @submit.prevent>
<el-form-item label="工位号">
<el-select v-model="checkin.stationNo" placeholder="请选择工位" style="width:100%">
<el-option v-for="i in 12" :key="i" :label="`工位${i}`" :value="i" />
</el-select>
</el-form-item>
<el-form-item label="班次">
<el-radio-group v-model="checkin.shift">
<el-radio-button v-for="s in shifts" :key="s" :value="s">{{ s }}</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="签到拍照">
<input type="file" accept="image/*" capture="environment" class="file-input" @change="handlePhoto($event, checkin)">
<div class="photo-box">
<el-image v-if="checkin.photo" :src="checkin.photo" :preview-src-list="[checkin.photo]" fit="cover" class="photo-preview" preview-teleported />
<span v-else class="photo-tip">点击上方按钮拍照上传</span>
</div>
</el-form-item>
<el-form-item>
<el-button v-if="canCheckin" type="primary" :loading="submitting" @click="submitCheckin">提交签到</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
<!-- 2. 工位点检 -->
<el-tab-pane label="工位点检" name="POINT">
<el-form label-position="top" class="pad-form" @submit.prevent>
<el-form-item label="工位号">
<el-select v-model="point.stationNo" placeholder="请选择工位" style="width:100%">
<el-option v-for="i in 12" :key="i" :label="`工位${i}`" :value="i" />
</el-select>
</el-form-item>
<el-form-item label="点检项(可多选)">
<el-checkbox-group v-model="point.items">
<el-checkbox v-for="it in pointItems" :key="it" :value="it">{{ it }}</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item label="点检拍照">
<input type="file" accept="image/*" capture="environment" class="file-input" @change="handlePhoto($event, point)">
<div class="photo-box">
<el-image v-if="point.photo" :src="point.photo" :preview-src-list="[point.photo]" fit="cover" class="photo-preview" preview-teleported />
<span v-else class="photo-tip">点击上方按钮拍照上传</span>
</div>
</el-form-item>
<el-form-item>
<el-button v-if="canPoint" type="primary" :loading="submitting" @click="submitPoint">提交点检</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
<!-- 3. 过程巡检 -->
<el-tab-pane label="过程巡检" name="PROCESS">
<el-form label-position="top" class="pad-form" @submit.prevent>
<el-form-item label="工单号(输入搜索)">
<el-select v-model="process.orderNo" filterable placeholder="输入工单号搜索" style="width:100%" @change="onProcessOrder">
<el-option v-for="wo in workOrders" :key="wo.workOrderNo" :value="wo.workOrderNo"
:label="wo.workOrderNo + (wo.productName ? ' · ' + wo.productName : '')" />
</el-select>
</el-form-item>
<el-alert v-if="process.order" type="info" :closable="false" class="order-info">
已选工单{{ process.order.workOrderNo }}<template v-if="process.order.productName"> · {{ process.order.productName }}</template><template v-if="process.order.processSeq"> · 工序组合 {{ process.order.processSeq }}</template><template v-if="process.order.status"> · {{ statusText(process.order.status) }}</template>
</el-alert>
<el-form-item label="巡检结果">
<el-radio-group v-model="process.result">
<el-radio-button value="OK">合格</el-radio-button>
<el-radio-button value="NG">不合格</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="签字确认">
<el-input v-model="process.remark" placeholder="输入姓名签字确认" />
</el-form-item>
<el-form-item>
<el-button v-if="canProcess" type="primary" :loading="submitting" @click="submitProcess">提交巡检</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
<!-- 4. 完工确认 -->
<el-tab-pane label="完工确认" name="DONE">
<el-form label-position="top" class="pad-form" @submit.prevent>
<el-form-item label="工单号(输入搜索)">
<el-select v-model="done.orderNo" filterable placeholder="输入工单号搜索" style="width:100%" @change="onDoneOrder">
<el-option v-for="wo in workOrders" :key="wo.workOrderNo" :value="wo.workOrderNo"
:label="wo.workOrderNo + (wo.productName ? ' · ' + wo.productName : '')" />
</el-select>
</el-form-item>
<el-form-item label="完工确认">
<el-checkbox v-model="done.confirmed">确认该工单全部工序已完成</el-checkbox>
</el-form-item>
<el-form-item label="签字确认">
<el-input v-model="done.remark" placeholder="输入姓名签字确认" />
</el-form-item>
<el-form-item>
<el-button v-if="canDone" type="primary" :loading="submitting" @click="submitDone">确认完工</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
<!-- 5. 异常上报 -->
<el-tab-pane label="异常上报" name="ALARM">
<el-form label-position="top" class="pad-form" @submit.prevent>
<el-form-item label="工位号">
<el-select v-model="alarm.stationNo" placeholder="请选择工位" style="width:100%">
<el-option v-for="i in 12" :key="i" :label="`工位${i}`" :value="i" />
</el-select>
</el-form-item>
<el-form-item label="异常类型(可多选)">
<el-checkbox-group v-model="alarm.items">
<el-checkbox v-for="t in alarmTypes" :key="t" :value="t">{{ t }}</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item label="异常拍照">
<input type="file" accept="image/*" capture="environment" class="file-input" @change="handlePhoto($event, alarm)">
<div class="photo-box">
<el-image v-if="alarm.photo" :src="alarm.photo" :preview-src-list="[alarm.photo]" fit="cover" class="photo-preview" preview-teleported />
<span v-else class="photo-tip">点击上方按钮拍照上传</span>
</div>
</el-form-item>
<el-form-item label="异常描述">
<el-input v-model="alarm.remark" type="textarea" :rows="3" placeholder="请描述异常情况(提交后将触发看板报警)" />
</el-form-item>
<el-form-item>
<el-button v-if="canAlarm" type="danger" :loading="submitting" @click="submitAlarm">上报异常</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
<!-- 6. 记录查看 -->
<el-tab-pane label="记录查看" name="RECORDS">
<el-form inline class="query-form" @submit.prevent>
<el-form-item label="类型">
<el-select v-model="query.category" clearable placeholder="全部" style="width:130px">
<el-option v-for="c in categoryOptions" :key="c.v" :label="c.l" :value="c.v" />
</el-select>
</el-form-item>
<el-form-item label="操作人">
<el-input v-model="query.operator" clearable placeholder="操作人" style="width:140px" />
</el-form-item>
<el-form-item label="日期">
<el-date-picker v-model="query.range" type="daterange" value-format="YYYY-MM-DD"
start-placeholder="开始日期" end-placeholder="结束日期" style="width:250px" />
</el-form-item>
<el-form-item>
<el-button v-if="canView" type="primary" @click="loadRecords">查询</el-button>
</el-form-item>
</el-form>
<el-table :data="records" border stripe size="small">
<el-table-column label="时间" min-width="150">
<template #default="{ row }">{{ row.createdAt || row.time || '-' }}</template>
</el-table-column>
<el-table-column label="类型" width="90">
<template #default="{ row }">{{ categoryText(row.category) }}</template>
</el-table-column>
<el-table-column prop="stationNo" label="工位" width="70" />
<el-table-column prop="orderNo" label="工单号" min-width="130" />
<el-table-column label="结果" width="80">
<template #default="{ row }">{{ row.result || '-' }}</template>
</el-table-column>
<el-table-column prop="operator" label="操作人" width="90" />
<el-table-column label="图片" width="80">
<template #default="{ row }">
<el-image v-if="row.photo" :src="row.photo" :preview-src-list="[row.photo]" fit="cover" class="thumb" preview-teleported />
<span v-else>-</span>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
</el-tabs>
</el-card>
<PageHelp :data="helpInspect" />
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import PageHelp from '../components/PageHelp.vue'
import { helpInspect } from '../help'
import request from '../utils/request'
// ---------- 当前用户与权限 ----------
const me = (() => {
try { return JSON.parse(localStorage.getItem('user')) || null } catch { return null }
})()
const owned = me?.permissionCodes || []
const can = (code) => !!me && (me.roleCode === 'SUPER_ADMIN' || owned.includes('*') || owned.includes(code))
const canCheckin = can('sys.inspect:checkin')
const canPoint = can('sys.inspect:point')
const canProcess = can('sys.inspect:process')
const canDone = can('sys.inspect:done')
const canAlarm = can('sys.inspect:alarm')
const canView = can('sys.inspect:view')
const operator = me ? (me.name || me.username || '') : ''
// ---------- 常量 ----------
const shifts = ['早班', '中班', '晚班']
const pointItems = ['设备运行正常', '安全防护到位', '工具齐全', '工位清洁']
const alarmTypes = ['设备故障', '物料异常', '工艺偏差', '安全隐患', '其他']
const categoryOptions = [
{ v: 'CHECKIN', l: '开工签到' },
{ v: 'POINT', l: '工位点检' },
{ v: 'PROCESS', l: '过程巡检' },
{ v: 'DONE', l: '完工确认' },
{ v: 'ALARM', l: '异常上报' }
]
const categoryText = (c) => (categoryOptions.find((x) => x.v === c) || {}).l || c || '-'
const statusText = (s) => ({
CREATED: '已创建', RELEASED: '已发布', IN_PROGRESS: '生产中',
DONE: '已完成', PENDING: '待排产', CONFIRMED: '已确认'
})[s] || s || '-'
const tab = ref('CHECKIN')
const submitting = ref(false)
// ---------- 各 tab 表单 ----------
const checkin = reactive({ stationNo: 1, shift: '早班', photo: '' })
const point = reactive({ stationNo: 1, items: [], photo: '' })
const process = reactive({ orderNo: '', order: null, result: 'OK', remark: '' })
const done = reactive({ orderNo: '', order: null, confirmed: false, remark: '' })
const alarm = reactive({ stationNo: 1, items: [], photo: '', remark: '' })
// ---------- 工单下拉(GET /work-orders 返回 {list:[...]} ----------
const workOrders = ref([])
async function loadWorkOrders() {
try {
const data = await request.get('/work-orders')
workOrders.value = (data && Array.isArray(data.list) ? data.list : data) || []
} catch { workOrders.value = [] }
}
function onProcessOrder(no) { process.order = workOrders.value.find((w) => w.workOrderNo === no) || null }
function onDoneOrder(no) { done.order = workOrders.value.find((w) => w.workOrderNo === no) || null }
// ---------- 拍照上传(FormData 字段名 file,返回 {filename, url} ----------
async function handlePhoto(e, form) {
const file = e.target.files && e.target.files[0]
e.target.value = ''
if (!file) return
if (!/^image\//.test(file.type)) return ElMessage.warning('请选择图片文件')
const fd = new FormData()
fd.append('file', file)
try {
const data = await request.post('/inspections/upload', fd)
form.photo = data?.url || (data?.filename ? '/api/v1/files/' + data.filename : '')
ElMessage.success('照片上传成功')
} catch { /* 失败已由拦截器提示 */ }
}
// ---------- 提交 ----------
async function submitCheckin() {
if (!checkin.shift) return ElMessage.warning('请选择班次')
submitting.value = true
try {
await request.post('/inspections', {
category: 'CHECKIN', shift: checkin.shift, stationNo: checkin.stationNo,
photo: checkin.photo, operator
})
ElMessage.success('签到成功')
checkin.photo = ''
} finally { submitting.value = false }
}
async function submitPoint() {
if (!point.items.length) return ElMessage.warning('请至少勾选一项点检项')
submitting.value = true
try {
await request.post('/inspections', {
category: 'POINT', stationNo: point.stationNo, items: point.items,
photo: point.photo, operator
})
ElMessage.success('点检提交成功')
point.items = []
point.photo = ''
} finally { submitting.value = false }
}
async function submitProcess() {
if (!process.orderNo) return ElMessage.warning('请选择工单号')
if (!process.remark.trim()) return ElMessage.warning('请签字确认')
submitting.value = true
try {
await request.post('/inspections', {
category: 'PROCESS', orderNo: process.orderNo, result: process.result,
remark: process.remark, operator
})
ElMessage.success('过程巡检提交成功')
process.remark = ''
} finally { submitting.value = false }
}
async function submitDone() {
if (!done.orderNo) return ElMessage.warning('请选择工单号')
if (!done.confirmed) return ElMessage.warning('请确认全部工序已完成')
if (!done.remark.trim()) return ElMessage.warning('请签字确认')
submitting.value = true
try {
await request.post('/inspections', {
category: 'DONE', orderNo: done.orderNo, remark: done.remark, operator
})
ElMessage.success('完工确认成功')
done.confirmed = false
done.remark = ''
} finally { submitting.value = false }
}
async function submitAlarm() {
if (!alarm.items.length) return ElMessage.warning('请选择异常类型')
submitting.value = true
try {
await request.post('/inspections', {
category: 'ALARM', stationNo: alarm.stationNo, items: alarm.items,
photo: alarm.photo, remark: alarm.remark, operator
})
ElMessage.success('异常已上报,看板已触发报警')
alarm.items = []
alarm.photo = ''
alarm.remark = ''
} finally { submitting.value = false }
}
// ---------- 记录查询(GET /inspections?category=&operator=&from=&to= ----------
const query = reactive({ category: '', operator: '', range: [] })
const records = ref([])
async function loadRecords() {
const params = {
category: query.category || undefined,
operator: query.operator || undefined,
from: query.range && query.range[0] || undefined,
to: query.range && query.range[1] || undefined
}
records.value = (await request.get('/inspections', { params })) || []
}
onMounted(() => { loadWorkOrders(); loadRecords() })
</script>
<style scoped>
.inspect-card { max-width: 900px; margin: 0 auto; }
.file-input { display: block; }
.photo-box { margin-top: 8px; }
.photo-preview { width: 130px; height: 96px; border-radius: 6px; border: 1px solid #ebeef5; cursor: zoom-in; }
.photo-tip { color: #909399; font-size: 12px; }
.order-info { margin-bottom: 14px; }
.thumb { width: 50px; height: 38px; border-radius: 4px; cursor: zoom-in; display: block; }
.query-form { margin-bottom: 10px; }
/* 小屏(平板/手机)自适应:表单控件占满宽度 */
@media (max-width: 640px) {
.query-form .el-form-item { display: block; margin-right: 0; margin-bottom: 12px; }
.query-form .el-form-item__label { display: block; text-align: left; padding: 0 0 6px; }
.query-form .el-form-item__content,
.query-form .el-form-item__content .el-select,
.query-form .el-form-item__content .el-date-editor,
.query-form .el-form-item__content .el-input,
.query-form .el-form-item__content .el-input__wrapper { width: 100% !important; }
.el-tabs__item { padding: 0 10px; font-size: 13px; }
}
</style>
@@ -8,7 +8,7 @@
<el-form-item label="状态"><el-input v-model="query.status" style="width:120px" @keyup.enter="load" /></el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查询</el-button>
<el-button type="success" @click="generate">按日排产生成备料</el-button>
<el-button v-if="can('produce.material:generate')" type="success" @click="generate">按日排产生成备料</el-button>
</el-form-item>
</el-form>
@@ -34,6 +34,7 @@
import { reactive, ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import request from '../utils/request'
import { can } from '../utils/perm'
import PageHelp from '../components/PageHelp.vue'
import { helpMaterial } from '../help'
@@ -0,0 +1,144 @@
<template>
<el-card>
<el-form inline>
<el-form-item label="操作人">
<el-input v-model="query.operator" placeholder="输入操作人" clearable style="width:180px" @keyup.enter="load" />
</el-form-item>
<el-form-item label="日期范围">
<el-date-picker
v-model="query.range"
type="daterange"
value-format="YYYY-MM-DD"
start-placeholder="开始日期"
end-placeholder="结束日期"
style="width:260px"
@change="load" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查询</el-button>
</el-form-item>
<el-form-item>
<el-button type="success" @click="exportCsv">导出 CSV</el-button>
</el-form-item>
</el-form>
<!-- 汇总卡 -->
<el-row :gutter="16" style="margin-bottom:14px">
<el-col :span="6">
<el-card shadow="never" class="stat-card">
<div class="stat-label">总完成数</div>
<div class="stat-value">{{ summary.doneCount }}</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="never" class="stat-card">
<div class="stat-label">总合格数</div>
<div class="stat-value ok">{{ summary.okCount }}</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="never" class="stat-card">
<div class="stat-label">总NG数</div>
<div class="stat-value ng">{{ summary.ngCount }}</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="never" class="stat-card">
<div class="stat-label">总合格率</div>
<div class="stat-value">{{ summary.rate }}</div>
</el-card>
</el-col>
</el-row>
<el-table :data="rows" border size="small">
<el-table-column prop="operator" label="操作人" min-width="110" />
<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 />
<el-table-column prop="doneCount" label="完成数" width="90" align="right" />
<el-table-column prop="okCount" label="合格数" width="90" align="right" />
<el-table-column prop="ngCount" label="NG数" width="90" align="right" />
<el-table-column label="合格率" width="100" align="center">
<template #default="{ row }">
<el-tag :type="rateOf(row) >= 100 ? 'success' : (rateOf(row) >= 0 ? 'warning' : 'info')" effect="plain">
{{ rateText(row) }}
</el-tag>
</template>
</el-table-column>
</el-table>
</el-card>
<PageHelp :data="helpPerformance" />
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import PageHelp from '../components/PageHelp.vue'
import { helpPerformance } from '../help'
import request from '../utils/request'
const query = reactive({ operator: '', range: null })
const rows = ref([])
const summary = computed(() => {
let doneCount = 0, okCount = 0, ngCount = 0
rows.value.forEach((r) => {
doneCount += Number(r.doneCount) || 0
okCount += Number(r.okCount) || 0
ngCount += Number(r.ngCount) || 0
})
return {
doneCount, okCount, ngCount,
rate: doneCount ? ((okCount / doneCount) * 100).toFixed(1) + '%' : '-'
}
})
function rateOf(row) {
const done = Number(row.doneCount) || 0
const ok = Number(row.okCount) || 0
return done ? (ok / done) * 100 : -1
}
function rateText(row) {
const r = rateOf(row)
return r < 0 ? '-' : r.toFixed(1) + '%'
}
async function load() {
const [from, to] = query.range || []
const params = {
operator: query.operator || '',
from: from || '',
to: to || ''
}
const data = (await request.get('/performance', { params })) || {}
rows.value = data.rows || []
}
// 导出 CSV(块6):按当前查询结果生成,含 BOM 以便 Excel 打开中文不乱码
function exportCsv() {
if (!rows.value.length) return ElMessage.warning('当前无数据可导出')
const header = ['操作人', '日期', '工序', '工序名', '完成数', '合格数', 'NG数', '合格率']
const lines = rows.value.map((r) => {
const rate = Number(r.doneCount) ? ((Number(r.okCount) / Number(r.doneCount)) * 100).toFixed(1) + '%' : '-'
return [r.operator, r.date, r.processCode, r.processName, r.doneCount, r.okCount, r.ngCount, rate]
})
const esc = (v) => `"${String(v ?? '').replace(/"/g, '""')}"`
const csv = '\ufeff' + [header, ...lines].map((row) => row.map(esc).join(',')).join('\r\n')
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' })
const a = document.createElement('a')
a.href = URL.createObjectURL(blob)
a.download = `绩效报表_${new Date().toISOString().slice(0, 10)}.csv`
a.click()
URL.revokeObjectURL(a.href)
}
onMounted(load)
</script>
<style scoped>
.stat-card { text-align: center; }
.stat-label { color: #909399; font-size: 13px; margin-bottom: 6px; }
.stat-value { font-size: 26px; font-weight: 700; color: #303133; }
.stat-value.ok { color: #67c23a; }
.stat-value.ng { color: #f56c6c; }
</style>
+2 -1
View File
@@ -6,7 +6,7 @@
<el-form-item label="工位"><el-input-number v-model="send.stationNo" :min="1" :max="12" /></el-form-item>
<el-form-item label="工序组合"><el-input v-model="send.processCombination" placeholder="如 135" style="width:110px" /></el-form-item>
<el-form-item>
<el-button type="primary" @click="doSend">下发工序码</el-button>
<el-button v-if="can('produce.plc:send')" type="primary" @click="doSend">下发工序码</el-button>
</el-form-item>
</el-form>
@@ -39,6 +39,7 @@
import { reactive, ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import request from '../utils/request'
import { can } from '../utils/perm'
import PageHelp from '../components/PageHelp.vue'
import { helpPlc } from '../help'
@@ -0,0 +1,479 @@
<template>
<el-card>
<el-tabs v-model="tab" @tab-change="onTabChange">
<!-- ============ Tab1工艺流程管理 ============ -->
<el-tab-pane label="工艺流程管理" name="flows">
<el-form inline>
<el-form-item label="工序号">
<el-select v-model="query.processCode" placeholder="全部" clearable style="width:140px" @change="loadFlows">
<el-option v-for="i in 12" :key="i" :label="`工序${i}`" :value="i" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="loadFlows">查询</el-button>
</el-form-item>
<el-form-item>
<el-button v-if="can('produce.processflow:add')" type="success" @click="openFlow()">新增流程</el-button>
</el-form-item>
</el-form>
<el-table :data="flows" border size="small">
<el-table-column prop="id" label="ID" width="60" />
<el-table-column prop="name" label="流程名" min-width="120" show-overflow-tooltip />
<el-table-column prop="processCode" label="工序编号" width="90" align="center" />
<el-table-column label="PDF" width="130" align="center">
<template #default="{ row }">
<el-button v-if="row.pdfFile" link type="primary" @click="previewPdf(row.pdfFile)">预览</el-button>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="状态" width="90" align="center">
<template #default="{ row }">
<el-tag :type="row.status === 'ACTIVE' ? 'success' : 'info'">{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column label="绑定工位" min-width="110">
<template #default="{ row }">{{ (row.stations || []).join(', ') || '-' }}</template>
</el-table-column>
<el-table-column label="步骤" min-width="150">
<template #default="{ row }">
<el-tooltip
v-if="row.steps && row.steps.length"
:content="row.steps.map(s => `${s.seq}.${s.name}`).join(' ')">
<span>{{ row.steps.length }} </span>
</el-tooltip>
<span v-else>0 </span>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip />
<el-table-column label="创建时间" width="150">
<template #default="{ row }">{{ fmt(row.createdAt) }}</template>
</el-table-column>
<el-table-column label="操作" width="150" align="center" fixed="right">
<template #default="{ row }">
<el-button
v-if="can('produce.processflow:add') || can('produce.processflow:edit')"
size="small" @click="openFlow(row)">编辑</el-button>
<el-button v-if="can('produce.processflow:delete')" size="small" type="danger" @click="delFlow(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<!-- ============ Tab2工位管理 ============ -->
<el-tab-pane label="工位管理" name="stations">
<div style="margin-bottom:10px">
<el-button v-if="can('produce.station:edit')" type="success" @click="openStation()">新增工位</el-button>
</div>
<el-table :data="stations" border size="small">
<el-table-column prop="id" label="ID" width="60" />
<el-table-column prop="stationNo" label="工位号" width="90" align="center" />
<el-table-column prop="name" label="工位名称" min-width="130" show-overflow-tooltip />
<el-table-column prop="flowName" label="绑定流程" min-width="150" show-overflow-tooltip />
<el-table-column prop="processCode" label="工序编号" width="90" align="center" />
<el-table-column label="状态" width="90" align="center">
<template #default="{ row }">
<el-tag :type="row.status === 'ACTIVE' ? 'success' : 'info'">{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="90" align="center" fixed="right">
<template #default="{ row }">
<el-button v-if="can('produce.station:edit')" size="small" @click="openStation(row)">编辑</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
</el-tabs>
<!-- ============ 工艺流程 新增/编辑 ============ -->
<el-dialog v-model="flowDlg.show" :title="flowDlg.id ? '编辑工艺流程' : '新增工艺流程'" width="920px" top="6vh">
<el-form :model="flowDlg" label-width="90px">
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="流程名" required>
<el-input v-model="flowDlg.name" placeholder="如 一号线总装流程" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="工序编号" required>
<el-select v-model="flowDlg.processCode" style="width:100%">
<el-option v-for="i in 12" :key="i" :label="`工序${i}`" :value="i" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="状态">
<el-select v-model="flowDlg.status" style="width:100%">
<el-option label="启用" value="ACTIVE" />
<el-option label="停用" value="INACTIVE" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="工序PDF">
<el-button
v-if="can('produce.processflow:upload')"
type="primary" plain :loading="flowDlg.uploading" @click="pickPdf">
上传 PDF
</el-button>
<span v-if="flowDlg.pdfFile" class="pdf-name">
{{ flowDlg.pdfFile }}
<el-button link type="primary" @click="previewPdf(flowDlg.pdfFile)">预览</el-button>
</span>
<input ref="pdfInput" type="file" accept="application/pdf" style="display:none" @change="onPdfChange" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="备注">
<el-input v-model="flowDlg.remark" placeholder="备注(可空)" />
</el-form-item>
<!-- 步骤模板子表 -->
<el-form-item label="步骤模板">
<div style="width:100%">
<el-table :data="flowDlg.steps" border size="small">
<el-table-column label="步骤" width="80" align="center">
<template #default="{ row }">
<el-input-number v-model="row.seq" :min="1" size="small" controls-position="right" style="width:70px" />
</template>
</el-table-column>
<el-table-column label="步骤名称" min-width="140">
<template #default="{ row }"><el-input v-model="row.name" placeholder="如 拧紧扭矩" /></template>
</el-table-column>
<el-table-column label="采集方式" width="130">
<template #default="{ row }">
<el-select v-model="row.collectType" placeholder="请选择">
<el-option value="AUTO" label="拧紧枪自动" />
<el-option value="MANUAL" label="手填" />
<el-option value="NONE" label="仅记录" />
</el-select>
</template>
</el-table-column>
<el-table-column label="是否拧紧" width="90" align="center">
<template #default="{ row }"><el-switch v-model="row.isTorque" /></template>
</el-table-column>
<el-table-column label="备注" min-width="120">
<template #default="{ row }"><el-input v-model="row.remark" placeholder="备注(可空)" /></template>
</el-table-column>
<el-table-column label="操作" width="120" align="center">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="row._showCrit = !row._showCrit">
{{ row._showCrit ? '收起标准' : '考核标准' }}
</el-button>
<el-button link type="danger" size="small" @click="removeStep(row)"></el-button>
</template>
</el-table-column>
</el-table>
<!-- 考核标准编辑 -->
<template v-for="row in flowDlg.steps" :key="row._key">
<div v-if="row._showCrit" class="crit-box">
<b>考核标准{{ row.name || '(未命名步骤)' }}</b>
<el-table :data="row.criteria" border size="small" style="margin-top:8px">
<el-table-column label="指标名" min-width="110">
<template #default="{ row: c }"><el-input v-model="c.name" placeholder="如 扭矩" /></template>
</el-table-column>
<el-table-column label="单位" width="80">
<template #default="{ row: c }"><el-input v-model="c.unit" placeholder="N·m" /></template>
</el-table-column>
<el-table-column label="逻辑" width="120">
<template #default="{ row: c }">
<el-select v-model="c.logic" placeholder="请选择">
<el-option value="GE" label="≥" />
<el-option value="LE" label="≤" />
<el-option value="GT" label=">" />
<el-option value="LT" label="<" />
<el-option value="RANGE" label="区间" />
<el-option value="EQUAL" label="=" />
</el-select>
</template>
</el-table-column>
<el-table-column v-if="cLogicNeedsTarget(row)" label="目标值" width="110">
<template #default="{ row: c }">
<el-input-number v-model="c.target" :step="1" controls-position="right" style="width:100%" />
</template>
</el-table-column>
<el-table-column v-if="editingRanges(row)" label="下限" width="110">
<template #default="{ row: c }">
<el-input-number v-model="c.min" :step="1" controls-position="right" style="width:100%" />
</template>
</el-table-column>
<el-table-column v-if="editingRanges(row)" label="上限" width="110">
<template #default="{ row: c }">
<el-input-number v-model="c.max" :step="1" controls-position="right" style="width:100%" />
</template>
</el-table-column>
<el-table-column label="操作" width="70" align="center">
<template #default="{ row: c }">
<el-button link type="danger" size="small" @click="removeCriterion(row, c)"></el-button>
</template>
</el-table-column>
</el-table>
<el-button size="small" style="margin-top:8px" @click="addCriterion(row)">新增标准行</el-button>
</div>
</template>
<el-button size="small" style="margin-top:10px" @click="addStep">新增步骤</el-button>
</div>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="flowDlg.show = false">取消</el-button>
<el-button
v-if="can('produce.processflow:add') || can('produce.processflow:edit')"
type="primary" @click="saveFlow">保存</el-button>
</template>
</el-dialog>
<!-- ============ 工位 新增/编辑 ============ -->
<el-dialog v-model="stationDlg.show" :title="stationDlg.id ? '编辑工位' : '新增工位'" width="460px">
<el-form :model="stationDlg" label-width="90px">
<el-form-item label="工位号" required>
<el-select v-model="stationDlg.stationNo" style="width:100%">
<el-option v-for="i in 12" :key="i" :label="`工位${i}`" :value="i" />
</el-select>
</el-form-item>
<el-form-item label="工位名称" required>
<el-input v-model="stationDlg.name" placeholder="如 一号工位" />
</el-form-item>
<el-form-item label="绑定流程" required>
<el-select v-model="stationDlg.flowId" filterable placeholder="请选择工艺流程" style="width:100%">
<el-option v-for="f in flowOptions" :key="f.id" :label="f.name" :value="f.id" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="stationDlg.show = false">取消</el-button>
<el-button v-if="can('produce.station:edit')" type="primary" @click="saveStation">保存</el-button>
</template>
</el-dialog>
</el-card>
<PageHelp :data="helpProcessFlow" />
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import PageHelp from '../components/PageHelp.vue'
import { helpProcessFlow } from '../help'
import { ElMessage, ElMessageBox } from 'element-plus'
import request from '../utils/request'
const tab = ref('flows')
const flows = ref([])
const stations = ref([])
const flowOptions = ref([])
const query = reactive({ processCode: undefined })
const pdfInput = ref(null)
/* ---------- 权限 ---------- */
function can(code) {
try {
const u = JSON.parse(localStorage.getItem('user') || 'null')
if (!u) return false
const owned = u.permissionCodes || []
return u.roleCode === 'SUPER_ADMIN' || owned.includes('*') || owned.includes(code)
} catch {
return false
}
}
/* ---------- 工艺流程 ---------- */
function blankStep(seq) {
return {
_key: Date.now() + Math.random(),
seq,
name: '',
collectType: 'MANUAL',
isTorque: false,
remark: '',
criteria: [],
_showCrit: false
}
}
async function loadFlows() {
const params = {}
if (query.processCode) params.processCode = query.processCode
flows.value = (await request.get('/process-flows', { params })) || []
}
function fmt(v) { return v ? String(v).replace('T', ' ').slice(0, 19) : '-' }
function previewPdf(pdfFile) {
if (pdfFile) window.open('/api/v1/files/' + pdfFile, '_blank')
}
function pickPdf() {
if (pdfInput.value) pdfInput.value.click()
}
async function onPdfChange(e) {
const file = e.target.files && e.target.files[0]
e.target.value = ''
if (!file) return
if (file.type !== 'application/pdf' && !/\.pdf$/i.test(file.name)) {
return ElMessage.warning('仅支持上传 PDF 文件')
}
flowDlg.uploading = true
try {
const fd = new FormData()
fd.append('file', file)
const data = await request.post('/process-flows/upload', fd, {
headers: { 'Content-Type': 'multipart/form-data' }
})
flowDlg.pdfFile = (data && data.filename) || ''
ElMessage.success('PDF 上传成功')
} catch {
// 错误提示已由 request 拦截器统一弹出
} finally {
flowDlg.uploading = false
}
}
const flowDlg = reactive({
show: false, id: 0, name: '', processCode: 1, pdfFile: '', status: 'ACTIVE',
remark: '', uploading: false, steps: []
})
function openFlow(row) {
if (row) {
flowDlg.id = row.id
flowDlg.name = row.name || ''
flowDlg.processCode = row.processCode
flowDlg.pdfFile = row.pdfFile || ''
flowDlg.status = row.status || 'ACTIVE'
flowDlg.remark = row.remark || ''
flowDlg.steps = (row.steps || []).map((s, i) => ({
_key: Date.now() + Math.random(),
seq: s.seq || i + 1,
name: s.name || '',
collectType: s.collectType || 'MANUAL',
isTorque: !!s.isTorque,
remark: s.remark || '',
criteria: (s.criteria || []).map((c) => ({ ...c })),
_showCrit: false
}))
} else {
Object.assign(flowDlg, {
id: 0, name: '', processCode: 1, pdfFile: '', status: 'ACTIVE', remark: '', steps: []
})
}
if (!flowDlg.steps.length) flowDlg.steps.push(blankStep(1))
flowDlg.show = true
}
function addStep() {
const maxSeq = flowDlg.steps.reduce((m, s) => Math.max(m, s.seq || 0), 0)
flowDlg.steps.push(blankStep(maxSeq + 1))
}
function removeStep(row) {
const i = flowDlg.steps.indexOf(row)
if (i >= 0) flowDlg.steps.splice(i, 1)
}
function addCriterion(row) {
row.criteria.push({ name: '', unit: '', logic: 'GE', target: null, min: null, max: null })
}
function removeCriterion(row, c) {
const i = row.criteria.indexOf(c)
if (i >= 0) row.criteria.splice(i, 1)
}
function cLogicNeedsTarget(row) {
return row.criteria.some((c) => c.logic && c.logic !== 'RANGE')
}
function editingRanges(row) {
return row.criteria.some((c) => c.logic === 'RANGE')
}
async function saveFlow() {
if (!flowDlg.name) return ElMessage.warning('请填写流程名')
if (!flowDlg.processCode) return ElMessage.warning('请选择工序编号')
const body = {
id: flowDlg.id || undefined,
name: flowDlg.name,
processCode: flowDlg.processCode,
pdfFile: flowDlg.pdfFile,
status: flowDlg.status,
remark: flowDlg.remark,
steps: flowDlg.steps.map(({ _key, _showCrit, ...st }) => ({
seq: st.seq,
name: st.name,
collectType: st.collectType,
isTorque: st.isTorque,
remark: st.remark,
criteria: st.criteria
}))
}
await request.post('/process-flows', body)
ElMessage.success('保存成功')
flowDlg.show = false
loadFlows()
}
async function delFlow(row) {
await ElMessageBox.confirm('确认删除该工艺流程?有工位绑定时将无法删除。', '提示', { type: 'warning' })
await request.delete(`/process-flows/${row.id}`)
ElMessage.success('删除成功')
loadFlows()
}
/* ---------- 工位管理 ---------- */
async function loadStations() {
stations.value = (await request.get('/stations')) || []
}
async function loadFlowOptions() {
const list = (await request.get('/process-flows', { params: {} })) || []
flowOptions.value = list.map((f) => ({ id: f.id, name: f.name }))
}
const stationDlg = reactive({ show: false, id: 0, stationNo: 1, name: '', flowId: null })
function openStation(row) {
if (row) {
Object.assign(stationDlg, {
show: true, id: row.id, stationNo: row.stationNo, name: row.name || '', flowId: row.flowId || null
})
} else {
Object.assign(stationDlg, { show: true, id: 0, stationNo: 1, name: '', flowId: null })
}
}
async function saveStation() {
if (!stationDlg.stationNo) return ElMessage.warning('请选择工位号')
if (!stationDlg.name) return ElMessage.warning('请填写工位名称')
if (!stationDlg.flowId) return ElMessage.warning('请选择绑定流程')
const body = {
id: stationDlg.id || undefined,
stationNo: stationDlg.stationNo,
name: stationDlg.name,
flowId: stationDlg.flowId
}
await request.post('/stations', body)
ElMessage.success('保存成功')
stationDlg.show = false
loadStations()
}
function onTabChange(name) {
if (name === 'stations') {
loadStations()
loadFlowOptions()
}
}
onMounted(() => {
loadFlows()
loadStations()
loadFlowOptions()
})
</script>
<style scoped>
.pdf-name { margin-left: 10px; color: #606266; font-size: 13px; }
.crit-box {
border: 1px solid #ebeef5; border-radius: 6px; padding: 10px; margin: 8px 0;
background: #fafbfc;
}
</style>
@@ -7,7 +7,7 @@
</el-select>
</el-form-item>
<el-form-item><el-button type="primary" @click="load">加载</el-button></el-form-item>
<el-form-item><el-button type="success" @click="save">保存该工序参数</el-button></el-form-item>
<el-form-item><el-button v-if="can('produce.processflow:edit')" type="success" @click="save">保存该工序参数</el-button></el-form-item>
</el-form>
<el-alert type="info" :closable="false"
@@ -85,7 +85,7 @@
</template>
<div style="margin:12px 0">
<el-button @click="addStep">新增步骤</el-button>
<el-button v-if="can('produce.processflow:edit')" @click="addStep">新增步骤</el-button>
</div>
</el-card>
<PageHelp :data="helpProcessStep" />
@@ -95,6 +95,7 @@
import { reactive, ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import request from '../utils/request'
import { can } from '../utils/perm'
import PageHelp from '../components/PageHelp.vue'
import { helpProcessStep } from '../help'
@@ -1,7 +1,7 @@
<template>
<el-card>
<el-form inline>
<el-form-item><el-button type="primary" @click="openAdd">新增产品类型</el-button></el-form-item>
<el-form-item><el-button v-if="can('produce.product:add')" type="primary" @click="openAdd">新增产品类型</el-button></el-form-item>
</el-form>
<el-table :data="list" border size="small">
<el-table-column prop="id" label="ID" width="60" />
@@ -14,8 +14,8 @@
<el-table-column prop="remark" label="备注" min-width="160" />
<el-table-column label="操作" width="140">
<template #default="{row}">
<el-button size="small" @click="openEdit(row)">编辑</el-button>
<el-button size="small" type="danger" @click="del(row)">删除</el-button>
<el-button v-if="can('produce.product:edit')" size="small" @click="openEdit(row)">编辑</el-button>
<el-button v-if="can('produce.product:delete')" size="small" type="danger" @click="del(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
@@ -30,7 +30,7 @@
</el-form>
<template #footer>
<el-button @click="dialog.show=false">取消</el-button>
<el-button type="primary" @click="save">保存</el-button>
<el-button v-if="can('produce.product:add') || can('produce.product:edit')" type="primary" @click="save">保存</el-button>
</template>
</el-dialog>
</el-card>
@@ -41,6 +41,7 @@
import { reactive, ref, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import request from '../utils/request'
import { can } from '../utils/perm'
import PageHelp from '../components/PageHelp.vue'
import { helpProductType } from '../help'
+43 -8
View File
@@ -4,7 +4,7 @@
<!-- 用户 -->
<el-tab-pane label="用户" name="users">
<div style="margin-bottom:10px">
<el-button type="primary" @click="openUser()">新增用户</el-button>
<el-button v-if="can('sys.rbac:user')" type="primary" @click="openUser()">新增用户</el-button>
</div>
<el-table :data="users" border size="small">
<el-table-column prop="id" label="ID" width="60" />
@@ -12,10 +12,23 @@
<el-table-column prop="name" label="姓名" min-width="110" />
<el-table-column prop="roleId" label="角色ID" width="80" />
<el-table-column prop="status" label="状态" width="90" />
<el-table-column label="工位终端" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.canLoginWorkstation ? 'success' : 'info'" effect="plain">
{{ row.canLoginWorkstation ? '已开通' : '未开通' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="允许工位" min-width="110">
<template #default="{ row }">
<span v-if="row.stations && row.stations.length">{{ row.stations.join('') }}</span>
<span v-else>全部</span>
</template>
</el-table-column>
<el-table-column label="操作" width="140">
<template #default="{row}">
<el-button size="small" @click="openUser(row)">编辑</el-button>
<el-button size="small" type="danger" @click="delUser(row)">删除</el-button>
<el-button v-if="can('sys.rbac:user')" size="small" @click="openUser(row)">编辑</el-button>
<el-button v-if="can('sys.rbac:user')" size="small" type="danger" @click="delUser(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
@@ -50,8 +63,8 @@
</el-tabs>
<!-- 用户编辑 -->
<el-dialog v-model="u.show" :title="u.id?'编辑用户':'新增用户'" width="420px">
<el-form :model="u" label-width="80px">
<el-dialog v-model="u.show" :title="u.id?'编辑用户':'新增用户'" width="480px">
<el-form :model="u" label-width="90px">
<el-form-item label="用户名"><el-input v-model="u.username" :disabled="u.id===1" /></el-form-item>
<el-form-item v-if="!u.id" label="密码"><el-input v-model="u.password" type="password" show-password placeholder="至少6位,不能纯数字" /></el-form-item>
<el-form-item v-else label="重置密码"><el-input v-model="u.password" type="password" show-password placeholder="留空则不修改;至少6位,不能纯数字" /></el-form-item>
@@ -66,8 +79,23 @@
<el-option label="启用" value="ENABLED" /><el-option label="禁用" value="DISABLED" />
</el-select>
</el-form-item>
<el-divider content-position="left">工位终端块1</el-divider>
<el-form-item label="终端登录">
<el-switch v-model="u.canLoginWorkstation" active-text="允许登录工位终端" />
</el-form-item>
<template v-if="u.canLoginWorkstation">
<el-form-item label="终端密码">
<el-input v-model="u.workstationPassword" type="password" show-password
:placeholder="u.id ? '留空则沿用原终端密码' : '留空默认复用登录密码'" />
</el-form-item>
<el-form-item label="允许工位">
<el-select v-model="u.stations" multiple collapse-tags clearable placeholder="留空=全部工位" style="width:100%">
<el-option v-for="i in 12" :key="i" :label="`工位${i}`" :value="i" />
</el-select>
</el-form-item>
</template>
</el-form>
<template #footer><el-button @click="u.show=false">取消</el-button><el-button type="primary" @click="saveUser">保存</el-button></template>
<template #footer><el-button @click="u.show=false">取消</el-button><el-button v-if="can('sys.rbac:user')" type="primary" @click="saveUser">保存</el-button></template>
</el-dialog>
</el-card>
<PageHelp :data="helpRbac" />
@@ -77,19 +105,26 @@
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import request from '../utils/request'
import { can } from '../utils/perm'
import PageHelp from '../components/PageHelp.vue'
import { helpRbac } from '../help'
const tab = ref('users')
const users = ref([]), roles = ref([]), perms = ref([])
const u = reactive({ show: false, id: 0, username: '', password: '', name: '', roleId: 0, status: 'ENABLED' })
const u = reactive({ show: false, id: 0, username: '', password: '', name: '', roleId: 0, status: 'ENABLED', canLoginWorkstation: false, workstationPassword: '', stations: [] })
async function loadAll() {
users.value = (await request.get('/users')) || []
roles.value = (await request.get('/roles')) || []
perms.value = (await request.get('/permissions')) || []
}
function openUser(row) { Object.assign(u, row ? { ...row, show: true, password: '' } : { show: true, id: 0, username: '', password: '', name: '', roleId: roles.value[0]?.id || 0, status: 'ENABLED' }) }
function openUser(row) {
if (row) {
Object.assign(u, { ...row, show: true, password: '', workstationPassword: '', stations: row.stations || [] })
} else {
Object.assign(u, { show: true, id: 0, username: '', password: '', name: '', roleId: roles.value[0]?.id || 0, status: 'ENABLED', canLoginWorkstation: false, workstationPassword: '', stations: [] })
}
}
// 前端密码规则校验(与后端一致):>=6位 且不能纯数字
function pwdOk(pwd) {
if (pwd.length < 6) return '密码长度至少 6 位'
+2 -1
View File
@@ -15,7 +15,7 @@
<el-card v-if="steps.length" shadow="never" style="margin-bottom:12px">
<div slot="header" style="display:flex;justify-content:space-between;align-items:center">
<b>工位 {{ form.stationNo }} / 工序 {{ form.processCode }} 参数采集</b>
<el-button type="primary" size="small" :loading="saving" @click="submit">提交报工</el-button>
<el-button v-if="can('produce.scan')" type="primary" size="small" :loading="saving" @click="submit">提交报工</el-button>
</div>
<el-table :data="steps" border size="small">
<el-table-column prop="seq" label="步骤" width="60" align="center" />
@@ -70,6 +70,7 @@
import { reactive, ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import request from '../utils/request'
import { can } from '../utils/perm'
import PageHelp from '../components/PageHelp.vue'
import { helpScan } from '../help'
@@ -5,6 +5,9 @@
<el-input v-model="sn" style="width:220px" placeholder="输入SN查询追溯" @keyup.enter="search" />
</el-form-item>
<el-form-item><el-button type="primary" @click="search">追溯查询</el-button></el-form-item>
<el-form-item>
<el-button type="warning" :disabled="!sn" @click="printCard">打印流程卡</el-button>
</el-form-item>
</el-form>
<el-alert v-if="error" :title="error" type="error" show-icon closable style="margin-bottom:10px" />
@@ -80,5 +83,9 @@ async function search() {
error.value = e.message || '查询失败'
}
}
function printCard() {
if (!sn.value) return
window.open('/api/v1/process-card?sn=' + encodeURIComponent(sn.value), '_blank')
}
function fmt(v) { return v ? String(v).replace('T', ' ').slice(0, 19) : '-' }
</script>
@@ -12,7 +12,7 @@
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查询</el-button>
<el-button @click="openCreate">新建工单</el-button>
<el-button v-if="can('produce.workorder:add')" @click="openCreate">新建工单</el-button>
</el-form-item>
</el-form>
@@ -29,9 +29,9 @@
</el-table-column>
<el-table-column label="操作" width="230" fixed="right">
<template #default="{ row }">
<el-button size="small" @click="openDaily(row)">日排产</el-button>
<el-button size="small" type="primary" plain @click="openSeq(row)">工序组合</el-button>
<el-button size="small" type="warning" @click="openEdit(row)">编辑</el-button>
<el-button v-if="can('produce.workorder:dailyplan')" size="small" @click="openDaily(row)">日排产</el-button>
<el-button v-if="can('produce.workorder:edit')" size="small" type="primary" plain @click="openSeq(row)">工序组合</el-button>
<el-button v-if="can('produce.workorder:edit')" size="small" type="warning" @click="openEdit(row)">编辑</el-button>
</template>
</el-table-column>
</el-table>
@@ -58,7 +58,7 @@
</el-form>
<template #footer>
<el-button @click="dialog.show=false">取消</el-button>
<el-button type="primary" @click="save">保存</el-button>
<el-button v-if="can('produce.workorder:add') || can('produce.workorder:edit')" type="primary" @click="save">保存</el-button>
</template>
</el-dialog>
@@ -72,7 +72,7 @@
<el-input-number v-model="plan.qty" :min="1" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="savePlan">保存排产</el-button>
<el-button v-if="can('produce.workorder:dailyplan')" type="primary" @click="savePlan">保存排产</el-button>
<el-button @click="loadPlans">刷新</el-button>
</el-form-item>
</el-form>
@@ -106,7 +106,7 @@
</el-form>
<template #footer>
<el-button @click="seq.show=false">取消</el-button>
<el-button type="primary" @click="saveSeq">保存</el-button>
<el-button v-if="can('produce.workorder:edit')" type="primary" @click="saveSeq">保存</el-button>
</template>
</el-dialog>
</div>
@@ -117,6 +117,7 @@
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import request from '../utils/request'
import { can } from '../utils/perm'
import PageHelp from '../components/PageHelp.vue'
import { helpWorkOrder } from '../help'