chore: 完成多系统功能迭代与优化

1. 通用优化:
   - 统一系统标题为"库房客户端"/"MES产线控制",移除北自所前缀
   - 调整内置账号密码为统一123456,优化密码校验逻辑
   - 新增数据库自动创建逻辑,简化部署流程
   - 修复日志时间格式配置,优化日志输出
   - 新增纯数字密码安全提示

2. MES系统优化:
   - 新增日排产管理功能,支持增删改查与自动算料生成备料单
   - 重构BOM模块,改为按产品编码维护而非工单维度
   - 新增工艺参数模板配置与手动报工页面
   - 新增产品类型自动编码功能
   - 优化菜单结构,调整工单管理、扫码报工等页面名称与路由
   - 新增删除日排产接口与权限保护
   - 修复物料清单查询逻辑,适配新BOM结构
   - 新增refreshToken支持,优化登录会话管理

3. WMS系统优化:
   - 新增基础数据维护页面,支持区域与物料档案管理
   - 新增工单列表下拉接口,对接MES获取未完成工单优先展示
   - 优化入库管理提示文案,替换英文提示为中文
   - 修复精密件入库校验逻辑,优化错误提示
   - 优化Excel导入功能,新增备注字段支持
   - 优化登录页面与菜单文案,统一备料台账名称
   - 新增自动打开浏览器功能,优化客户端启动体验
   - 修复备料出库页面查询提示文案
   - 新增WMS与MES对接配置,完善内部API调用逻辑

4. 其他优化:
   - 删除冗余的旧版静态资源文件,更新资源引用路径
   - 新增帮助文档,完善基础数据模块说明
   - 修复多处文案不统一、英文残留问题
This commit is contained in:
SunYF
2026-08-29 15:30:25 +08:00
parent e82b744bbd
commit ebf901ad16
124 changed files with 1815 additions and 480 deletions
+50 -13
View File
@@ -8,7 +8,12 @@ export const helpProductType = {
'产品类型是"生产什么产品"的分类档案,是整条产线的源头基础数据。\n\n生产主数据链条:产品类型 → 建工单(选产品类型) → 配物料清单(BOM) → 日排产 → 备料 → PLC/AGV 配送 → 工位装配。\n必须先在这里维护好产品类型,后面所有环节(工单/BOM/备料/拧紧/追溯)才有可选对象。',
fields: [
{ name: '名称', source: '手工填写。用户自定义,无上游来源。', purpose: '产品类型的中文名,用于识别与下拉展示', fill: '如:智能电表、断路器、配电柜' },
{ name: '编码', source: '手工填写。你自定义的编码,保存入库后成为该记录的唯一标识。', purpose: '产品唯一编码,工单/物料清单等下游按此关联', fill: '如 METER-01;全局唯一,建了不要随意改,否则下游关联会断' },
{
name: '编码',
source: '系统自动生成 + 可手动修改。新建时会自动带出(格式 PROD-YYYYMMDD-序号),你可直接使用或改为自定义编码;编辑时保留已存值。一个产品编码可对应多个工单(同一型号今天做一批、明天做一批,工单号不同,产品编码相同)。',
purpose: '产品唯一编码。它是 MES 内部识别该产品的标识,决定该产品的物料清单(BOM)内容,也是工单、备料、拧紧、追溯的挂靠键。',
fill: '建议用有意义的编码,如 METER-01。要点:①本编码与 WMS 是两套系统、互相解耦,MES 这边独立维护,WMS 那边各自单独录入;②成品入库时,由操作人员把本编码**手动录入 WMS**,两系统靠人工对齐;③如产品编码就是"产品类型",则直接沿用该类型编码即可;④建好后不要随意改,否则下游工单/追溯的关联会断。'
},
{ name: '类别', source: '手工填写。可选,无上游来源。', purpose: '产品所属大类,便于分类统计', fill: '如:计量表 / 开关柜;可空' },
{ name: '启用', source: '开关选择。默认开启。', purpose: '启用后才允许在新建工单/备料的下拉里被选到', fill: '默认开启;暂时停产的可关闭(不删除,保留档案)' },
{ name: '备注', source: '手工填写。可选。', purpose: '补充说明', fill: '可不填' }
@@ -18,7 +23,7 @@ export const helpProductType = {
export const helpWorkOrder = {
title: '工单管理(新建 / 编辑工单)',
overview:
'工单 = 一个"合同/生产批次"的唯一载体,是生产执行和仓库出库挂靠的核心对象。\n\n全流程:先有产品类型 → 建工单(选产品类型、定数量、定工序组合) → 进"物料清单"给该工单配料 → 行内"日排产"排哪天产多少 → 仓库按排产备料出库 → 产线装配/拧紧/报工 → 完工。\n工单号会贯穿 备料单、拧紧查询、扫码报工、工件追溯、操作日志 全链路。',
'工单 = 一个"合同/生产批次"的唯一载体,是生产执行和仓库出库挂靠的核心对象。\n\n全流程:先有产品类型 → 建工单(选产品类型、定数量、定工序组合) → 进"物料清单"给该产品配料 → 行内"日排产"排哪天产多少 → 仓库按排产备料出库 → 产线装配/拧紧/报工 → 完工。\n工单号会贯穿 备料单、拧紧查询、手动报工、工件追溯、操作日志 全链路。',
fields: [
{ name: '工单号', source: '系统自动生成(WO+时间戳),可手工改。新建时自动带出。', purpose: '唯一标识一个工单,备料/出库/报工/追溯都挂在此号下', fill: '打开新建对话框时已自动生成,一般不用改;如改动须保证唯一' },
{ name: '产品类型', source: '下拉选择。数据来自"产品类型"页面已启用的记录。', purpose: '说明这批生产的是什么产品,并决定物料清单内容', fill: '先到"产品类型"页面建好记录,再到这边下拉选择;没有可选项=还没建产品类型' },
@@ -36,16 +41,36 @@ export const helpWorkOrder = {
{ name: '日期', source: '手动选择日期。', purpose: '计划生产日期,仓库按它哪天备料', fill: '选一个日期,可对同一工单排未来多天' },
{ name: '计划数量', source: '手动填写整数。', purpose: '当天计划产量,作为仓库备料/AGV配送的前瞻依据', fill: '≤ 工单总数量;如今天产 20 台' }
]
},
{
title: '行内"工序组合"弹窗',
overview: '对当前这行工单快速维护要执行的工序组合,PLC 按此组合下发到工位。\n可自由拆组合、补工序(半成品再上线):如本应做第3/5序却做成了半成品,可在此把该件的组合改成只做漏掉的第2序,重上线完成闭环。',
fields: [
{ name: '工序组合', source: '人工自由维护。', purpose: '本工单在产线执行的工序集合,PLC 组合下发', fill: '只做第1/3/5序填 135;全序填 123456789101112;半成品补做第2序填 2' }
]
}
]
}
export const helpBom = {
title: '物料清单(BOM',
export const helpDailyPlan = {
title: '日排产(生产计划',
overview:
'物料清单 = 生产 1 台该工单产品需要哪些物料、各用多少。\n仓库按它计算备料/出库需求量,并强约束累计出库 ≤ 需求总量。\n\n数据链路:先建工单 → 到本页选工单 → 为它逐条添加物料(编码要与 WMS 物料档案一致) → 之后仓库才能按此为工单领料。',
'日排产 = 安排"哪个工单、在哪天、生产多少",是备料出库的触发依据。\n\n数据链路:工单(总数) → 日排产(按天拆分量,合计不得超过工单总数) → 仓库按日排产备料出库 → 产线按日生产。\n可以按 工单号 / 排产日期 两个维度查询,也可直接新增/编辑/删除排产。',
fields: [
{ name: '工单号', source: '下拉/输入选择。来自已建的工单。', purpose: '要为哪个工单配置物料清单', fill: '选择工单号后加载该工单已有清单' },
{ name: '工单号', source: '下拉选择。来自"工单管理"已建的工单。', purpose: '该排产对应哪个生产工单', fill: '选择工单号;也可手动输入未列出的工单号' },
{ name: '排产日期', source: '日历选择。', purpose: '计划在哪一天生产', fill: '选具体日期,如 2026-08-29' },
{ name: '计划数量', source: '手工填写整数。', purpose: '这一天计划生产多少件', fill: '整数≥1;注意:该工单所有日排产的数量合计不能超过工单总数量' },
{ name: '已完成', source: '系统自动累计。只读。', purpose: '该排产已实际完成多少件', fill: '无需填写' },
{ name: '状态', source: '系统维护。只读。', purpose: '排产执行状态(待排产/已确认)', fill: '无需填写' }
]
}
export const helpBom = {
title: '物料清单(产品 BOM',
overview:
'物料清单 = 生产 1 台该产品需要哪些物料、各用多少。\n按产品编码维护:同型号产品今天做一批、明天做一批,工单号不同、产品编码相同,共用这一份 BOM。\n仓库(库房)按工单取其产品的 BOM 计算备料/出库需求量,并强约束累计出库 ≤ 需求总量。\n\n职责分工:本页(MES)只维护「产品由哪几种料、各用几个」;每种物料叫什么、规格、是批次件还是 SN 件、库存有多少,统一在 WMS(库房)的「物料档案」里维护,两套系统互相解耦。\n\n数据链路:先建「产品类型」→ 到本页选产品编码,为它逐条添加物料(编码要与 WMS 物料档案一致) → 之后仓库才能为对应工单领料。',
fields: [
{ name: '产品编码', source: '下拉/输入选择。来自「产品类型」管理里建的编码;同一型号可对应多个工单。', purpose: '要为哪个产品配置物料清单', fill: '选择产品编码后加载该产品已有清单' },
{ name: '物料编码', source: '手工填写。必须与 WMS(库房) 物料档案的编码一致。', purpose: '该物料在库房的全库唯一编码,两条系统靠它关联', fill: '如 螺丝=SCREW-M3、主控芯片=IC-001;编码对不上 WMS 将无法备料' },
{ name: '物料名称', source: '手工填写。', purpose: '物料中文名,便于识别', fill: '如 十字螺丝、主控芯片' },
{ name: '规格', source: '手工填写。', purpose: '物料规格型号', fill: '如 M3×8 / SMT-48脚' },
@@ -95,15 +120,27 @@ export const helpTorque = {
}
export const helpScan = {
title: '扫码报工',
title: '手动报工(工位工序完成上报)',
overview:
'扫码枪扫到工件,按类型记录工件在产线的流转事件。这是工单进度与工件追溯的核心数据来源。\n\n类型含义:进线(进入产线) → 工序(完成某道工序) → 完工(整件完成) → 暂存退回(退回库房)。',
'每个工位装配/检测相对工序完成后,在【手动报工】页选择工位、扫入工件SN、选工序号,系统按该工序已维护的【工艺参数】模板动态展开需要采集的参数,人工录入并提交。\n提交后写工序实绩 + 步骤数据,系统按步骤考核标准自动判定 OK/NG。\n\n数据链路:工艺参数(维护每道工序要收集哪些参数、判定标准) → 手动报工页扫SN逐参数录入 → 判 OK/NG → 工件追溯可见。',
fields: [
{ name: 'SN', source: '扫码枪扫描。来自工件条码。', purpose: '工件的唯一序列号', fill: '扫码枪扫入' },
{ name: '工单号', source: '自动带出/选择。来自工。', purpose: '工件所属工单', fill: '输入或选择' },
{ name: '工', source: '手动填写/扫码枪绑定。', purpose: '当前扫码的工位编号', fill: '如 5' },
{ name: '类型', source: '下拉选择。业务定义的事件类型。', purpose: '本次扫码的业务含义', fill: '进线 ONLINE / 工序 PROCESS / 完工 DONE / 暂存 TEMP_STORE' },
{ name: '工序号', source: '手动填写。', purpose: '类型为"工序"时填第几道工序', fill: '如 3' }
{ name: '工位', source: '下拉选择(1~12)。', purpose: '哪个工位在报工', fill: '选择工位 1~12' },
{ name: '工件SN', source: '扫码枪扫描。来自工件条码。', purpose: '工件的唯一序列号,报工对象', fill: '扫码枪扫入,触发载入参数模板' },
{ name: '工序号', source: '手动选择(1~12)。', purpose: '本次完成的工序,展开该工序的参数模板', fill: '选择 1~12' },
{ name: '各步骤采集值', source: '手工填写 或 拧紧枪自动带出(视采集方式)。字段由【工艺参数】维护页人为定义,可增加/删除/改采集方式与判定标准。', purpose: '收集该工序的扭矩/角度/尺寸等参数,按标准判定合格与否', fill: '逐条填采集值,未维护标准的步骤仅记录' }
]
}
export const helpProcessStep = {
title: '工艺参数(工序步骤与考核标准维护)',
overview:
'人为维护【每道工序要收集哪些参数】及【合格判定标准】,是手动报工的基础——报工页按这里的模板动态渲染参数输入框。\n· 一道工序可有多个步骤(参数);\n· 每步可选择采集方式:拧紧枪自动 / 手填 / 仅记录;\n· 每步可设考核标准(如扭矩在 [35,45] N·m 为合格),报工时系统自动判 OK/NG。',
fields: [
{ name: '工序号', source: '手动选择(1~12)。', purpose: '给哪道工序配置步骤', fill: '选择工序号,加载该工序现有步骤' },
{ name: '步骤名称', source: '人为定义。', purpose: '该参数叫什么(如 装配完成/拧紧扭矩)', fill: '输入中文名称' },
{ name: '采集方式', source: '人为选择。', purpose: '该参数怎么来:拧紧枪自动/手填/仅记录', fill: 'AUTO=拧紧枪自动;MANUAL=手填;NONE=仅记录' },
{ name: '是否为拧紧', source: '开关。', purpose: '标识是否属于拧紧采集', fill: '拧紧扭矩/角度步骤打开' },
{ name: '判定标准', source: '人为定义。可为一条或多条。', purpose: '报工时自动判定 OK/NG 的依据', fill: '如 扭矩 RANGE [35,45];角度 GE 90 等' }
]
}
@@ -44,11 +44,13 @@ const menus = computed(() => {
const defaultMenus = [
{ code: 'produce.workorder', name: '工单管理', path: '/work-order' },
{ code: 'produce.dailyplan', name: '日排产', path: '/daily-plan' },
{ code: 'produce.bom', name: '物料清单', path: '/bom' },
{ code: 'produce.material', name: '备料单', path: '/material-request' },
{ code: 'produce.plc', name: 'PLC工序下发', path: '/plc-send' },
{ code: 'produce.torque', name: '拧紧查询', path: '/torque' },
{ code: 'produce.scan', name: '扫码报工', path: '/scan' },
{ code: 'produce.step', name: '工艺参数', path: '/process-step' },
{ code: 'produce.scan', name: '手动报工', path: '/scan' },
{ code: 'produce.trace', name: '工件追溯', path: '/trace' },
{ code: 'produce.product', name: '产品类型', path: '/product-type' },
{ code: 'sys.eventlog', name: '操作日志', path: '/event-log' },
+10 -10
View File
@@ -1,9 +1,9 @@
<template>
<el-card>
<el-form inline>
<el-form-item label="工单号">
<el-select v-model="orderNo" filterable allow-create default-first-option style="width:220px" @change="load">
<el-option v-for="w in workOrders" :key="w.workOrderNo" :label="w.workOrderNo" :value="w.workOrderNo" />
<el-form-item label="产品编码">
<el-select v-model="productCode" filterable allow-create default-first-option placeholder="请选择/输入产品编码" style="width:240px" @change="load">
<el-option v-for="p in products" :key="p.code" :label="`${p.code} ${p.name || ''}`" :value="p.code" />
</el-select>
</el-form-item>
<el-form-item>
@@ -62,20 +62,20 @@ import request from '../utils/request'
import PageHelp from '../components/PageHelp.vue'
import { helpBom } from '../help'
const orderNo = ref('')
const workOrders = ref([])
const productCode = ref('')
const products = ref([])
const list = ref([])
const add = reactive({ show: false, materialCode: '', materialName: '', spec: '', unit: '', manageMode: '2', unitQty: 1, lossRate: 0, requiredQty: 1 })
async function load() {
if (!orderNo.value) return ElMessage.warning('请选择/输入工单号')
list.value = (await request.get('/bom', { params: { workOrderNo: orderNo.value } })) || []
if (!productCode.value) return ElMessage.warning('请选择/输入产品编码')
list.value = (await request.get('/bom', { params: { productCode: productCode.value } })) || []
}
function openAdd() {
Object.assign(add, { show: true, materialCode: '', materialName: '', spec: '', unit: '', manageMode: '2', unitQty: 1, lossRate: 0, requiredQty: 1 })
}
async function save() {
await request.put('/bom', { workOrderNo: orderNo.value, items: [add] })
await request.put('/bom', { productCode: productCode.value, items: [add] })
ElMessage.success('保存成功')
add.show = false
load()
@@ -85,10 +85,10 @@ async function remove(row) {
const items = list.value
.filter((x) => x.id !== row.id)
.map(({ materialCode, materialName, spec, unit, manageMode, unitQty, lossRate, requiredQty }) => ({ materialCode, materialName, spec, unit, manageMode, unitQty, lossRate, requiredQty }))
await request.put('/bom', { workOrderNo: orderNo.value, items })
await request.put('/bom', { productCode: productCode.value, items })
await load()
}
onMounted(async () => {
workOrders.value = (await request.get('/work-orders')) || []
products.value = (await request.get('/product-types')) || []
})
</script>
@@ -0,0 +1,101 @@
<template>
<el-card>
<el-form inline>
<el-form-item label="工单号">
<el-select v-model="query.orderNo" filterable allow-create clearable placeholder="请选择/输入工单号" style="width:220px" @change="load">
<el-option v-for="w in workOrders" :key="w.workOrderNo" :label="w.workOrderNo" :value="w.workOrderNo" />
</el-select>
</el-form-item>
<el-form-item label="排产日期">
<el-date-picker v-model="query.planDate" type="date" value-format="YYYY-MM-DD" placeholder="请选择日期" @change="load" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查询</el-button>
<el-button type="success" @click="openAdd">新建排产</el-button>
</el-form-item>
</el-form>
<el-table :data="list" border size="small">
<el-table-column prop="orderNo" label="工单号" min-width="180" />
<el-table-column prop="planDate" label="排产日期" width="120" />
<el-table-column prop="planQty" label="计划数量" width="100" />
<el-table-column prop="completedQty" label="已完成" width="90" />
<el-table-column label="状态" width="100">
<template #default="{ row }">{{ statusText(row.status) }}</template>
</el-table-column>
<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>
</template>
</el-table-column>
</el-table>
<el-dialog v-model="dialog.show" title="新建/编辑排产" width="460px">
<el-form :model="dialog" label-width="90px">
<el-form-item label="工单号">
<el-select v-model="dialog.orderNo" filterable allow-create placeholder="请选择/输入工单号" style="width:100%">
<el-option v-for="w in workOrders" :key="w.workOrderNo" :label="w.workOrderNo" :value="w.workOrderNo" />
</el-select>
</el-form-item>
<el-form-item label="排产日期">
<el-date-picker v-model="dialog.planDate" type="date" value-format="YYYY-MM-DD" style="width:100%" />
</el-form-item>
<el-form-item label="计划数量">
<el-input-number v-model="dialog.planQty" :min="1" style="width:100%" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialog.show=false">取消</el-button>
<el-button type="primary" @click="save">保存</el-button>
</template>
</el-dialog>
</el-card>
<PageHelp :data="helpDailyPlan" />
</template>
<script setup>
import { reactive, ref, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import request from '../utils/request'
import PageHelp from '../components/PageHelp.vue'
import { helpDailyPlan } from '../help'
const query = reactive({ orderNo: '', planDate: '' })
const list = ref([])
const workOrders = ref([])
const dialog = reactive({ show: false, id: 0, orderNo: '', planDate: '', planQty: 1 })
const statusOptions = [
{ v: 'PENDING', l: '待排产' }, { v: 'CONFIRMED', l: '已确认' }
]
const statusText = (s) => (statusOptions.find((x) => x.v === s) || {}).l || s
async function load() {
list.value = (await request.get('/daily-plans', { params: query })) || []
}
function openAdd() {
Object.assign(dialog, { show: true, id: 0, orderNo: '', planDate: new Date().toISOString().slice(0, 10), planQty: 1 })
}
function openEdit(row) {
Object.assign(dialog, { show: true, id: row.id, orderNo: row.orderNo, planDate: row.planDate, planQty: row.planQty })
}
async function save() {
if (!dialog.orderNo || !dialog.planDate) return ElMessage.warning('请选择工单号和排产日期')
if (dialog.id) await request.put(`/daily-plans/${dialog.id}`, { ...dialog })
else await request.post('/daily-plans', { orderNo: dialog.orderNo, planDate: dialog.planDate, planQty: dialog.planQty })
ElMessage.success('保存成功')
dialog.show = false
load()
}
async function del(row) {
await ElMessageBox.confirm('确认删除该排产?', '提示', { type: 'warning' })
await request.delete(`/daily-plans/${row.id}`)
load()
}
onMounted(async () => {
workOrders.value = (await request.get('/work-orders')) || []
load()
})
</script>
+5 -1
View File
@@ -37,9 +37,13 @@ async function onLogin() {
loading.value = true
try {
const data = await request.post('/login', { username: username.value, password: password.value })
setSession(data.accessToken, { username: username.value })
setSession(data.accessToken, { username: username.value }, data.refreshToken)
const u = await request.get('/userinfo')
localStorage.setItem('user', JSON.stringify(u))
// 纯数字密码:提示安全级别低,但仍允许继续登录
if (/^\d+$/.test(password.value)) {
ElMessage.warning('当前密码为纯数字,安全级别低,建议登录后立即修改密码')
}
router.replace(route.query.redirect || '/work-order')
} catch (e) {
// 错误已在拦截器提示
@@ -0,0 +1,140 @@
<template>
<el-card>
<el-form inline>
<el-form-item label="工序号">
<el-select v-model="processCode" placeholder="请选择工序" style="width:140px">
<el-option v-for="i in 12" :key="i" :value="i" :label="`工序${i}`" />
</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>
<el-alert type="info" :closable="false"
title="为每道工序配置【步骤/参数】与【判定标准】,手动报工页会按此动态渲染采集框;标准可为空=仅记录不判定"
style="margin-bottom:12px" />
<el-table :data="steps" border size="small">
<el-table-column prop="seq" label="步骤" width="70" align="center">
<template #default="{ row }"><el-input-number v-model="row.seq" :min="1" size="small" controls-position="right" style="width:80px" /></template>
</el-table-column>
<el-table-column label="步骤名称" min-width="150">
<template #default="{ row }"><el-input v-model="row.name" placeholder="如 拧紧扭矩" /></template>
</el-table-column>
<el-table-column label="采集方式" width="140">
<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="140">
<template #default="{ row }"><el-input v-model="row.remark" placeholder="备注(可空)" /></template>
</el-table-column>
<el-table-column label="操作" width="110" align="center">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="addCriterion(row)">加标准</el-button>
<el-button link type="danger" size="small" @click="remove(row)"></el-button>
</template>
</el-table-column>
</el-table>
<!-- 编辑判定标准 -->
<template v-for="row in steps" :key="row.id + '-' + row.seq">
<div v-if="row._showCrit" style="border:1px solid #ebeef5;border-radius:6px;padding:10px;margin:8px 0">
<b>标准{{ row.name }}</b>
<el-table :data="row.criteria" border size="small" style="margin-top:8px">
<el-table-column label="指标名" min-width="120">
<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="80" 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="row.criteria.push({ name: '', unit: '', logic: 'GE', target: 0, min: null, max: null })">新增标准行</el-button>
</div>
</template>
<div style="margin:12px 0">
<el-button @click="addStep">新增步骤</el-button>
</div>
</el-card>
<PageHelp :data="helpProcessStep" />
</template>
<script setup>
import { reactive, ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import request from '../utils/request'
import PageHelp from '../components/PageHelp.vue'
import { helpProcessStep } from '../help'
const processCode = ref(1)
const steps = ref([])
function blankStep() {
return { id: 0, processCode: processCode.value, seq: steps.value.length + 1, name: '', collectType: 'MANUAL', isTorque: false, remark: '', criteria: [], _showCrit: false }
}
function addStep() { steps.value.push(blankStep()) }
function addCriterion(row) {
row._showCrit = !row._showCrit
}
function remove(row) {
const i = steps.value.indexOf(row)
if (i >= 0) steps.value.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')
}
function removeCriterion(row, c) {
const i = row.criteria.indexOf(c)
if (i >= 0) row.criteria.splice(i, 1)
}
async function load() {
try {
const data = await request.get('/process-steps', { params: { processCode: processCode.value } }) || []
steps.value = data.map(s => ({ ...s, criteria: s.criteria || [], _showCrit: false }))
if (!steps.value.length) steps.value.push(blankStep())
} catch { steps.value = [blankStep()] }
}
async function save() {
const body = steps.value.map(({ _showCrit, ...st }) => st)
await request.post('/process-steps', body)
ElMessage.success('工艺参数已保存')
load()
}
onMounted(load)
</script>
@@ -48,7 +48,20 @@ const list = ref([])
const dialog = reactive({ show: false, id: 0, name: '', code: '', category: '', remark: '', isActive: true })
async function load() { list.value = (await request.get('/product-types')) || [] }
function openAdd() { Object.assign(dialog, { show: true, id: 0, name: '', code: '', category: '', remark: '', isActive: true }) }
// 自动生成一个可修改的编码:PROD-日期-序号(序号取当前已存在编码数的下一位,避免重复)
async function genCode() {
const d = new Date(), p = (n) => String(n).padStart(2, '0')
const stamp = `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}`
const max = (list.value || []).reduce((m, it) => {
const mch = /^PROD-\d{8}-(\d+)$/.exec(it.code || '')
return mch ? Math.max(m, parseInt(mch[1])) : m
}, 0)
return `PROD-${stamp}-${String(max + 1).padStart(3, '0')}`
}
async function openAdd() {
Object.assign(dialog, { show: true, id: 0, name: '', code: '', category: '', remark: '', isActive: true })
dialog.code = await genCode()
}
function openEdit(row) { Object.assign(dialog, { show: true, ...row }) }
async function save() {
if (dialog.id) await request.put('/product-types', { ...dialog })
+30 -73
View File
@@ -21,30 +21,23 @@
</el-table>
</el-tab-pane>
<!-- 角色 -->
<!-- 角色只读不允许新增/编辑/删除 -->
<el-tab-pane label="角色" name="roles">
<div style="margin-bottom:10px">
<el-button type="primary" @click="openRole()">新增角色</el-button>
</div>
<p class="tip">角色为系统预置只读查看</p>
<el-table :data="roles" border size="small">
<el-table-column prop="id" label="ID" width="60" />
<el-table-column prop="name" label="名称" min-width="120" />
<el-table-column prop="code" label="编码" min-width="130" />
<el-table-column prop="remark" label="备注" min-width="150" />
<el-table-column label="操作" width="140">
<template #default="{row}">
<el-button size="small" @click="openRole(row)">编辑</el-button>
<el-button size="small" type="danger" @click="delRole(row)">删除</el-button>
</template>
<el-table-column prop="permissionCodes" label="权限" min-width="200">
<template #default="{row}">{{ (row.permissionCodes||[]).join(', ') || '-' }}</template>
</el-table-column>
</el-table>
</el-tab-pane>
<!-- 权限 -->
<!-- 权限菜单只读不允许新增/编辑/删除 -->
<el-tab-pane label="权限菜单" name="perms">
<div style="margin-bottom:10px">
<el-button type="primary" @click="openPerm()">新增权限</el-button>
</div>
<p class="tip">权限菜单为系统预置只读查看</p>
<el-table :data="perms" border size="small">
<el-table-column prop="id" label="ID" width="60" />
<el-table-column prop="code" label="编码" min-width="150" />
@@ -52,12 +45,6 @@
<el-table-column prop="type" label="类型" width="80" />
<el-table-column prop="path" label="路径" min-width="120" />
<el-table-column prop="sort" label="排序" width="70" />
<el-table-column label="操作" width="140">
<template #default="{row}">
<el-button size="small" @click="openPerm(row)">编辑</el-button>
<el-button size="small" type="danger" @click="delPerm(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
</el-tabs>
@@ -65,12 +52,13 @@
<!-- 用户编辑 -->
<el-dialog v-model="u.show" :title="u.id?'编辑用户':'新增用户'" width="420px">
<el-form :model="u" label-width="80px">
<el-form-item label="用户名"><el-input v-model="u.username" /></el-form-item>
<el-form-item label="密码"><el-input v-model="u.password" type="password" show-password /></el-form-item>
<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>
<el-form-item label="姓名"><el-input v-model="u.name" /></el-form-item>
<el-form-item label="角色">
<el-select v-model="u.roleId" style="width:100%">
<el-option v-for="r in roles" :key="r.id" :label="r.name" :value="r.id" />
<el-option v-for="rl in roles" :key="rl.id" :label="rl.name" :value="rl.id" />
</el-select>
</el-form-item>
<el-form-item label="状态">
@@ -81,34 +69,6 @@
</el-form>
<template #footer><el-button @click="u.show=false">取消</el-button><el-button type="primary" @click="saveUser">保存</el-button></template>
</el-dialog>
<!-- 角色编辑 -->
<el-dialog v-model="r.show" :title="r.id?'编辑角色':'新增角色'" width="560px">
<el-form :model="r" label-width="80px">
<el-form-item label="名称"><el-input v-model="r.name" /></el-form-item>
<el-form-item label="编码"><el-input v-model="r.code" /></el-form-item>
<el-form-item label="备注"><el-input v-model="r.remark" /></el-form-item>
<el-form-item label="权限">
<el-select v-model="r.permissionCodes" multiple filterable placeholder="请选择权限" style="width:100%">
<el-option v-for="p in perms" :key="p.code" :label="p.name" :value="p.code" />
</el-select>
</el-form-item>
</el-form>
<template #footer><el-button @click="r.show=false">取消</el-button><el-button type="primary" @click="saveRole">保存</el-button></template>
</el-dialog>
<!-- 权限编辑 -->
<el-dialog v-model="p.show" :title="p.id?'编辑权限':'新增权限'" width="460px">
<el-form :model="p" label-width="80px">
<el-form-item label="编码"><el-input v-model="p.code" /></el-form-item>
<el-form-item label="名称"><el-input v-model="p.name" /></el-form-item>
<el-form-item label="类型"><el-input v-model="p.type" /></el-form-item>
<el-form-item label="路径"><el-input v-model="p.path" /></el-form-item>
<el-form-item label="排序"><el-input-number v-model="p.sort" /></el-form-item>
<el-form-item label="备注"><el-input v-model="p.remark" /></el-form-item>
</el-form>
<template #footer><el-button @click="p.show=false">取消</el-button><el-button type="primary" @click="savePerm">保存</el-button></template>
</el-dialog>
</el-card>
<PageHelp :data="helpRbac" />
</template>
@@ -123,8 +83,6 @@ 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 r = reactive({ show: false, id: 0, name: '', code: '', remark: '', permissionCodes: [] })
const p = reactive({ show: false, id: 0, code: '', name: '', type: 'MENU', path: '', sort: 0, remark: '' })
async function loadAll() {
users.value = (await request.get('/users')) || []
@@ -132,34 +90,33 @@ async function loadAll() {
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' }) }
// 前端密码规则校验(与后端一致):>=6位 且不能纯数字
function pwdOk(pwd) {
if (pwd.length < 6) return '密码长度至少 6 位'
if (/^\d+$/.test(pwd)) return '密码不能是纯数字'
return ''
}
async function saveUser() {
// 新增必须设置密码
if (!u.id) {
const err = pwdOk(u.password || '')
if (err) return ElMessage.error(err)
} else if (u.password) {
const err = pwdOk(u.password)
if (err) return ElMessage.error(err)
}
if (u.id) await request.put('/users', { ...u })
else await request.post('/users', { ...u })
ElMessage.success('保存成功'); u.show = false; loadAll()
}
async function delUser(row) {
if (row.id === 1) return ElMessage.warning('系统管理员不可删除')
await ElMessageBox.confirm('确认删除该用户?', '提示', { type: 'warning' })
await request.delete(`/users/${row.id}`); loadAll()
}
function openRole(row) { Object.assign(r, row ? { ...row, show: true } : { show: true, id: 0, name: '', code: '', remark: '', permissionCodes: [] }) }
async function saveRole() {
if (r.id) await request.put('/roles', { ...r })
else await request.post('/roles', { ...r })
ElMessage.success('保存成功'); r.show = false; loadAll()
}
async function delRole(row) {
await ElMessageBox.confirm('确认删除该角色?', '提示', { type: 'warning' })
await request.delete(`/roles/${row.id}`); loadAll()
}
function openPerm(row) { Object.assign(p, row ? { ...row, show: true } : { show: true, id: 0, code: '', name: '', type: 'MENU', path: '', sort: 0, remark: '' }) }
async function savePerm() {
if (p.id) await request.put('/permissions', { ...p })
else await request.post('/permissions', { ...p })
ElMessage.success('保存成功'); p.show = false; loadAll()
}
async function delPerm(row) {
await ElMessageBox.confirm('确认删除该权限?', '提示', { type: 'warning' })
await request.delete(`/permissions/${row.id}`); loadAll()
}
onMounted(loadAll)
</script>
</script>
<style scoped>
.tip { color: #909399; font-size: 13px; margin: 0 0 10px; }
</style>
+88 -16
View File
@@ -1,21 +1,52 @@
<template>
<el-card>
<el-form inline>
<el-form-item label="SN"><el-input v-model="scan.sn" style="width:170px" /></el-form-item>
<el-form-item label="工单号"><el-input v-model="scan.orderNo" style="width:170px" /></el-form-item>
<el-form-item label="工位"><el-input v-model="scan.station" style="width:90px" /></el-form-item>
<el-form-item label="类型">
<el-select v-model="scan.type" placeholder="请选择类型" style="width:130px">
<el-option label="进线ONLINE" value="ONLINE" />
<el-option label="工序PROCESS" value="PROCESS" />
<el-option label="完工DONE" value="DONE" />
<el-option label="暂存TEMP_STORE" value="TEMP_STORE" />
<el-form-item label="工位">
<el-select v-model="form.stationNo" placeholder="请选择工位" style="width:110px">
<el-option v-for="i in 12" :key="i" :value="i" :label="`工位${i}`" />
</el-select>
</el-form-item>
<el-form-item label="工序号"><el-input-number v-model="scan.processCode" :min="0" /></el-form-item>
<el-form-item><el-button type="primary" @click="report">扫码报工</el-button></el-form-item>
<el-form-item label="工件SN"><el-input v-model="form.sn" style="width:170px" @change="loadSteps" /></el-form-item>
<el-form-item label="工序号"><el-input-number v-model="form.processCode" :min="1" :max="12" @change="loadSteps" /></el-form-item>
<el-form-item><el-button type="primary" @click="loadSteps">载入参数模板</el-button></el-form-item>
</el-form>
<!-- 参数采集区按已维护的步骤模板动态渲染 -->
<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>
</div>
<el-table :data="steps" border size="small">
<el-table-column prop="seq" label="步骤" width="60" align="center" />
<el-table-column prop="name" label="步骤名称" min-width="130" />
<el-table-column label="采集方式" width="130" align="center">
<template #default="{ row }">
<el-tag v-if="row.collectType === 'AUTO'" type="warning" size="small">拧紧枪自动</el-tag>
<el-tag v-else-if="row.collectType === 'MANUAL'" type="primary" size="small">手填</el-tag>
<el-tag v-else type="info" size="small">仅记录</el-tag>
</template>
</el-table-column>
<el-table-column label="采集值" min-width="180">
<template #default="{ row }">
<el-input v-if="row.collectType !== 'NONE'" v-model="row.value" placeholder="采集值(可空)" style="width:130px" />
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="判定标准" min-width="200">
<template #default="{ row }">
<template v-if="row.criteria.length">
<el-tag v-for="c in row.criteria" :key="c.id" size="small" style="margin-right:4px">
{{ c.name }}{{ critText(c) }}
</el-tag>
</template>
<span v-else style="color:#909399">未设标准</span>
</template>
</el-table-column>
</el-table>
</el-card>
<el-alert v-else type="info" :closable="false" title="请选择工位、SN 与工序号后点【载入参数模板】" />
<el-divider content-position="left">报工记录</el-divider>
<el-form inline>
<el-form-item label="SN"><el-input v-model="query.sn" clearable style="width:160px" /></el-form-item>
@@ -42,17 +73,58 @@ import request from '../utils/request'
import PageHelp from '../components/PageHelp.vue'
import { helpScan } from '../help'
const scan = reactive({ sn: '', orderNo: '', station: '', type: 'PROCESS', processCode: 0 })
const form = reactive({ stationNo: 1, sn: '', processCode: 1 })
const steps = ref([])
const saving = ref(false)
const query = reactive({ sn: '', orderNo: '' })
const list = ref([])
async function report() {
if (!scan.sn) return ElMessage.warning('请输入SN')
await request.post('/scan/report', scan)
function critText(c) {
switch (c.logic) {
case 'GE': return `${c.target}${c.unit}`
case 'LE': return `${c.target}${c.unit}`
case 'GT': return ` >${c.target}${c.unit}`
case 'LT': return ` <${c.target}${c.unit}`
case 'RANGE': return ` [${c.min},${c.max}]${c.unit}`
case 'EQUAL': return ` =${c.target}${c.unit}`
default: return c.name || ''
}
}
// 从后端步骤模板构造报工所需字段(value 挂在 row 上,提交时收集)
async function loadSteps() {
if (!form.sn) return ElMessage.warning('请先输入工件SN')
if (!form.processCode) return ElMessage.warning('请选择工序号')
try {
const data = await request.get('/process-steps', { params: { processCode: form.processCode } }) || []
steps.value = data.map(s => ({ ...s, value: '', text: '' }))
} catch { steps.value = [] }
}
async function submit() {
if (!form.sn) return ElMessage.warning('请输入SN')
if (!form.processCode) return ElMessage.warning('请选择工序号')
const stepReq = steps.value
.filter(s => s.collectType !== 'NONE' && (s.text !== '' || s.value !== ''))
.map(s => ({
stepId: s.id,
name: s.name,
value: Number(s.value) || 0,
text: s.text || String(s.value || '')
}))
// 手动报工走工序实绩 + 步骤考核
await request.post('/workpiece/process/report', {
sn: form.sn,
processCode: form.processCode,
stationNo: form.stationNo,
steps: stepReq
})
ElMessage.success('报工成功')
Object.assign(scan, { sn: '', station: '', processCode: 0 })
steps.value = []
Object.assign(form, { sn: '' })
load()
}
async function load() {
list.value = (await request.get('/scan/records', { params: query })) || []
}
+50 -3
View File
@@ -27,9 +27,10 @@
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">{{ statusText(row.status) }}</template>
</el-table-column>
<el-table-column label="操作" width="170" fixed="right">
<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>
</template>
</el-table-column>
@@ -79,10 +80,35 @@
<el-table-column prop="planDate" label="日期" width="120" />
<el-table-column prop="planQty" label="计划数量" width="100" />
<el-table-column prop="completedQty" label="已完成" />
<el-table-column prop="status" label="状态" />
<el-table-column label="状态">
<template #default="{ row }">{{ statusText(row.status) }}</template>
</el-table-column>
<el-table-column prop="operator" label="操作人" />
</el-table>
</el-dialog>
<!-- 工序组合维护 -->
<el-dialog v-model="seq.show" :title="`维护工序组合 - ${seq.orderNo}`" width="520px">
<el-alert type="info" :closable="false"
title="人工自由维护本工单执行的工序组合(如只做第1/3/5序可填135;半成品再上线补做第2序填2)。PLC 会按此组合下发。"
style="margin-bottom:14px" />
<el-form label-width="90px">
<el-form-item label="工单号"><el-input :model-value="seq.orderNo" disabled /></el-form-item>
<el-form-item label="产品编码">
<el-input :model-value="seq.productCode" disabled />
</el-form-item>
<el-form-item label="产品名称">
<el-input :model-value="seq.productName" disabled />
</el-form-item>
<el-form-item label="工序组合">
<el-input v-model="seq.processSeq" placeholder="如 135;全序=123456789101112" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="seq.show=false">取消</el-button>
<el-button type="primary" @click="saveSeq">保存</el-button>
</template>
</el-dialog>
</div>
<PageHelp :data="helpWorkOrder" />
</template>
@@ -96,7 +122,8 @@ import { helpWorkOrder } from '../help'
const statusOptions = [
{ v: 'CREATED', l: '已创建' }, { v: 'RELEASED', l: '已发布' },
{ v: 'IN_PROGRESS', l: '生产中' }, { v: 'DONE', l: '已完成' }
{ v: 'IN_PROGRESS', l: '生产中' }, { v: 'DONE', l: '已完成' },
{ v: 'PENDING', l: '待排产' }, { v: 'CONFIRMED', l: '已确认' }
]
const statusText = (s) => (statusOptions.find((x) => x.v === s) || {}).l || s
@@ -107,6 +134,7 @@ const productTypes = ref([])
const dialog = reactive({ show: false, id: 0, workOrderNo: '', productTypeId: 0, productCode: '', productName: '', quantity: 1, processSeq: '', status: 'CREATED' })
const plan = reactive({ show: false, orderNo: '', date: '', qty: 1 })
const planList = ref([])
const seq = reactive({ show: false, id: 0, orderNo: '', productCode: '', productName: '', processSeq: '' })
async function load() {
const data = await request.get('/work-orders', { params: query })
@@ -136,6 +164,25 @@ function openDaily(row) {
plan.show = true
loadPlans()
}
function openSeq(row) {
Object.assign(seq, {
show: true, id: row.id, orderNo: row.workOrderNo,
productCode: row.productCode, productName: row.productName,
processSeq: row.processSeq || ''
})
}
async function saveSeq() {
if (!seq.processSeq) return ElMessage.warning('工序组合不能为空')
await request.put('/work-orders', {
id: seq.id, workOrderNo: seq.orderNo, productCode: seq.productCode,
productName: seq.productName, processSeq: seq.processSeq
})
ElMessage.success('工序组合已保存')
seq.show = false
load()
}
async function loadPlans() {
planList.value = (await request.get('/daily-plans', { params: { orderNo: plan.orderNo } })) || []
}
+3 -1
View File
@@ -9,11 +9,13 @@ const routes = [
children: [
{ path: '', redirect: '/work-order' },
{ path: 'work-order', name: 'WorkOrder', component: () => import('../pages/WorkOrder.vue'), meta: { title: '工单管理' } },
{ path: 'daily-plan', name: 'DailyPlan', component: () => import('../pages/DailyPlan.vue'), meta: { title: '日排产' } },
{ path: 'bom', name: 'Bom', component: () => import('../pages/Bom.vue'), meta: { title: '物料清单' } },
{ path: 'material-request', name: 'MaterialRequest', component: () => import('../pages/MaterialRequest.vue'), meta: { title: '备料单' } },
{ path: 'plc-send', name: 'PlcSend', component: () => import('../pages/PlcSend.vue'), meta: { title: 'PLC工序下发' } },
{ path: 'torque', name: 'Torque', component: () => import('../pages/Torque.vue'), meta: { title: '拧紧查询' } },
{ path: 'scan', name: 'Scan', component: () => import('../pages/Scan.vue'), meta: { title: '扫码报工' } },
{ path: 'process-step', name: 'ProcessStep', component: () => import('../pages/ProcessStep.vue'), meta: { title: '工艺参数' } },
{ path: 'scan', name: 'Scan', component: () => import('../pages/Scan.vue'), meta: { title: '手动报工' } },
{ path: 'trace', name: 'Trace', component: () => import('../pages/Trace.vue'), meta: { title: '工件追溯' } },
{ path: 'product-type', name: 'ProductType', component: () => import('../pages/ProductType.vue'), meta: { title: '产品类型' } },
{ path: 'event-log', name: 'EventLog', component: () => import('../pages/EventLog.vue'), meta: { title: '操作日志' } },
+6 -1
View File
@@ -1,6 +1,9 @@
export function getToken() {
return localStorage.getItem('token') || ''
}
export function getRefreshToken() {
return localStorage.getItem('refreshToken') || ''
}
export function getUser() {
try {
return JSON.parse(localStorage.getItem('user')) || null
@@ -8,11 +11,13 @@ export function getUser() {
return null
}
}
export function setSession(token, user) {
export function setSession(token, user, refreshToken) {
localStorage.setItem('token', token)
localStorage.setItem('user', JSON.stringify(user))
if (refreshToken) localStorage.setItem('refreshToken', refreshToken)
}
export function logout() {
localStorage.removeItem('token')
localStorage.removeItem('refreshToken')
localStorage.removeItem('user')
}
+56 -3
View File
@@ -1,9 +1,47 @@
import axios from 'axios'
import { ElMessage } from 'element-plus'
import { getToken, logout } from './auth'
import { getToken, getRefreshToken, setSession, getUser, logout } from './auth'
const request = axios.create({ baseURL: '/api/v1', timeout: 30000 })
// 是否正在用 refreshToken 续签,避免并发多次
let refreshing = false
// 续签期间排队等待重放的请求
let waiters = []
async function doRefresh() {
const rToken = getRefreshToken()
if (!rToken) throw new Error('无刷新令牌')
const resp = await axios.post('/api/v1/refresh', { refreshToken: rToken })
const body = resp.data
if (!body || body.code !== 0 || !body.data) throw new Error(body?.message || '续签失败')
const d = body.data
setSession(d.accessToken, getUser(), d.refreshToken)
return d.accessToken
}
function refreshToken() {
if (refreshing) {
// 已有续签进行中:排队等待其完成即可
return new Promise((resolve, reject) => {
waiters.push({ resolve, reject })
})
}
refreshing = true
return doRefresh()
.then(() => {
refreshing = false
waiters.forEach((w) => w.resolve())
waiters = []
})
.catch((e) => {
refreshing = false
waiters.forEach((w) => w.reject(e))
waiters = []
throw e
})
}
request.interceptors.request.use((config) => {
const token = getToken()
if (token) config.headers.Authorization = `Bearer ${token}`
@@ -22,8 +60,23 @@ request.interceptors.response.use(
}
return body
},
(error) => {
if (error.response?.status === 401) {
async (error) => {
// 登录接口失败(如密码错误)不触发续签
const url = error.config?.url || ''
if (error.response?.status === 401 && !url.includes('/login') && !url.includes('/refresh')) {
const token = getToken()
// 只有确实带了 accessToken 的请求失败才尝试滑动续签(说明是过期而非未登录)
try {
await refreshToken()
// 续签成功,重放原请求
error.config.headers.Authorization = `Bearer ${getToken()}`
return request(error.config)
} catch (e) {
logout()
ElMessage.error('登录已失效,请重新登录')
if (!window.location.pathname.startsWith('/login')) window.location.href = '/login'
}
} else if (error.response?.status === 401) {
logout()
ElMessage.error('登录已失效,请重新登录')
if (!window.location.pathname.startsWith('/login')) window.location.href = '/login'