This commit is contained in:
tzy 2026-10-27 17:40:10 +08:00
parent b82ec60b61
commit e8f6e97a15
33 changed files with 8534 additions and 682 deletions

View File

@ -0,0 +1,75 @@
import request from '@/utils/request'
export function getDesignTaskSummary() {
return request({
url: '/system/designTask/summary',
method: 'get'
})
}
export function getDesignTaskBoard(query) {
return request({
url: '/system/designTask/board',
method: 'get',
params: query
})
}
export function listMyDesignTask(query) {
return request({
url: '/system/designTask/mine',
method: 'get',
params: query
})
}
export function reportDesignTask(data) {
return request({
url: '/system/designTask/report',
method: 'post',
data: data
})
}
export function listDesignTaskReports(taskId) {
return request({
url: '/system/designTask/reports/' + taskId,
method: 'get'
})
}
export function acceptDesignTask(taskId) {
return request({
url: '/system/designTask/accept/' + taskId,
method: 'post'
})
}
export function updateDesignTaskDim(data) {
return request({
url: '/system/designTask/designDim',
method: 'put',
data: data
})
}
export function deleteDesignTaskReport(reportId) {
return request({
url: '/system/designTask/reports/' + reportId,
method: 'delete'
})
}
export function listDesignTaskByOrder(orderId) {
return request({
url: '/system/designTask/byOrder/' + orderId,
method: 'get'
})
}
export function syncDesignAccounts() {
return request({
url: '/system/designTask/syncAccounts',
method: 'post'
})
}

View File

@ -76,6 +76,7 @@ export function importElectricalBom(formData) {
url: '/system/details/importElectricalBom',
method: 'post',
data: formData,
headers: { 'Content-Type': 'multipart/form-data' }
timeout: 180000,
headers: { "Content-Type": "multipart/form-data", repeatSubmit: false },
})
}

View File

@ -0,0 +1,24 @@
import request from '@/utils/request'
export function listDingTodoScenes() {
return request({
url: '/system/dingTodo/scenes',
method: 'get'
})
}
export function updateDingTodoScene(data) {
return request({
url: '/system/dingTodo/scenes',
method: 'put',
data
})
}
export function listDingTodoLogs(query) {
return request({
url: '/system/dingTodo/logs',
method: 'get',
params: query
})
}

View File

@ -0,0 +1,24 @@
import request from '@/utils/request'
export function listDrawingRouteOperLog(query) {
return request({
url: '/system/drawingRouteOperLog/list',
method: 'get',
params: query
})
}
export function getDrawingRouteOperLog(id) {
return request({
url: '/system/drawingRouteOperLog/' + id,
method: 'get'
})
}
export function renotifyDrawingRouteOperLog(id) {
return request({
url: '/system/drawingRouteOperLog/' + id + '/renotify',
method: 'post',
timeout: 180000
})
}

View File

@ -8,11 +8,21 @@ export function listMaterial_index(query) {
params: query
})
}
//
export function material_inventory(query) {
/**
* 库存查询(支持单个或多个物料)
* @param {string|string[]} codes 物料编码或编码数组
*/
export function material_inventory(codes) {
const list = Array.isArray(codes)
? codes.filter(Boolean)
: String(codes || '').split(',').map(s => s.trim()).filter(Boolean)
return request({
url: '/index/inventory?materialCode=' + query,
method: 'get'
url: '/index/inventory',
method: 'get',
params: {
materialCodes: list.join(',')
}
})
}

View File

@ -66,6 +66,24 @@ export function updateOrderPro(data) {
})
}
/** 批量设置项目状态(不走整单编辑,避免设计人必填拦住) */
export function updateOrderProBomStatus(data) {
return request({
url: '/system/orderPro/bomStatus',
method: 'put',
data: data
})
}
/** 设置出图模式:normal 正常 / part 零部件 */
export function updateDrawingMode(id, drawingMode) {
return request({
url: '/system/orderPro/drawingMode/' + id,
method: 'put',
data: { drawingMode }
})
}
// 删除项目令号
export function delOrderPro(id) {
return request({
@ -126,6 +144,14 @@ export function delSave(ids) {
})
}
// 确认图纸已齐:设计人员确认图纸上传完成,之后才允许下载 PLM PDF
export function issueDrawingQty(id) {
return request({
url: '/system/orderPro/issueDrawingQty/' + id,
method: 'post'
})
}
// 下载PDF
export function uploadPDF(id) {
return request({
@ -223,3 +249,14 @@ export function getKingdeePurchaseOrder(id) {
method: 'get'
})
}
// 临时导入:按生产令号更新出图时间、项目完成时间等字段,空单元格不更新
export function importDrawingByExcel(formData) {
return request({
url: '/system/orderPro/importDrawingBy',
method: 'post',
data: formData,
timeout: 180000,
headers: { 'Content-Type': 'multipart/form-data', repeatSubmit: false }
})
}

View File

@ -0,0 +1,34 @@
import request from '@/utils/request'
export function listOrderProProgress(query) {
return request({
url: '/system/orderProProgress/list',
method: 'get',
params: query
})
}
export function getOrderProProgressDesignerSummary(query) {
return request({
url: '/system/orderProProgress/designerSummary',
method: 'get',
params: query
})
}
export function getOrderProProgressOverview(query) {
return request({
url: '/system/orderProProgress/overview',
method: 'get',
params: query
})
}
export function syncOrderProProgressWorkload(ids) {
return request({
url: '/system/orderProProgress/syncWorkload',
method: 'post',
data: ids,
timeout: 5 * 60 * 1000
})
}

View File

@ -1,12 +1,41 @@
import request from '@/utils/request'
export function listRatioValues(seriesType) {
return request({
url: '/system/ratio/ratios',
method: 'get',
params: { seriesType }
})
}
export function listRatioModels(params) {
return request({
url: '/system/ratio/models',
method: 'get',
params: {
seriesType: params.seriesType,
ratioValue: params.ratioValue
}
})
}
export function getRatioBuildCapacity(params) {
return request({
url: '/system/ratio/buildCapacity',
method: 'get',
params: {
seriesType: params.seriesType,
ratioValue: params.ratioValue,
modelId: params.modelId,
qty: params.qty
}
})
}
export function importRatioBom(data) {
return request({
url: '/system/ratio/import',
method: 'post',
data
})
}

View File

@ -99,6 +99,17 @@ export function pushRouteBom(rooteProdet, groupName) {
});
}
/** 按物料推送工艺:只推所选物料,不改项目整单状态 */
export function pushRouteBomByMaterial(rooteProdet, groupName, materialCodes) {
const codes = Array.isArray(materialCodes) ? materialCodes.filter(Boolean).join(',') : (materialCodes || '');
return request({
url: '/system/route/pushRouteBomByMaterial',
method: 'post',
params: { rooteProdet, groupName, materialCodes: codes },
timeout: 600000,
});
}
export function approveRoute(rooteProdet) {
return request({
url: '/system/route/approveRoute',
@ -146,12 +157,28 @@ export function getAllRouteAndUse(rooteProdet) {
throw new Error('获取全部工艺失败,请重试!');
});
}
export function generatePDFs(rooteProdet) {
/** 按令号 + 图号 + 格式下载 PLM 图纸/模型,后端转发文件服务 */
export function downloadDrawingModel(productionOrderNo, materialCode, fileType) {
return request({
url: '/system/route/downloadDrawingModel',
method: 'get',
params: { productionOrderNo, materialCode, fileType },
responseType: 'blob',
timeout: 300000
});
}
export function generatePDFs(rooteProdet, materialCode) {
const params = { rooteProdet };
if (materialCode) {
params.materialCode = materialCode;
}
return request({
url: '/system/route/generatePDFs',
method: 'get',
params: { rooteProdet },
params,
responseType: 'blob',
timeout: materialCode ? 120000 : 300000,
headers:{ 'Content-Type': 'application/json; application/octet-stream'},
});
}

View File

@ -0,0 +1,96 @@
import request from '@/utils/request'
export function listVmiMaterial(query) {
return request({
url: '/system/vmiMaterial/list',
method: 'get',
params: query
})
}
export function getVmiMaterial(id) {
return request({
url: '/system/vmiMaterial/' + id,
method: 'get'
})
}
export function addVmiMaterial(data) {
return request({
url: '/system/vmiMaterial',
method: 'post',
data
})
}
export function updateVmiMaterial(data) {
return request({
url: '/system/vmiMaterial',
method: 'put',
data
})
}
export function delVmiMaterial(id) {
const ids = Array.isArray(id) ? id.join(',') : id
return request({
url: '/system/vmiMaterial/' + ids,
method: 'delete'
})
}
export function scanVmiMaterial() {
return request({
url: '/system/vmiMaterial/scan',
method: 'post',
timeout: 180000,
headers: { repeatSubmit: false }
})
}
export function confirmVmiReplenish(data) {
return request({
url: '/system/vmiMaterial/confirm',
method: 'post',
timeout: 120000,
data
})
}
export function previewVmiReplenish(data) {
return request({
url: '/system/vmiMaterial/confirm/preview',
method: 'post',
data
})
}
export function ignoreVmiAlert(alertId) {
return request({
url: '/system/vmiMaterial/ignore/' + alertId,
method: 'post'
})
}
export function notifyVmiForecast(id) {
return request({
url: '/system/vmiMaterial/forecast/notify/' + id,
method: 'post'
})
}
export function listVmiAlert(query) {
return request({
url: '/system/vmiMaterial/alert/list',
method: 'get',
params: query
})
}
export function listVmiReq(query) {
return request({
url: '/system/vmiMaterial/req/list',
method: 'get',
params: query
})
}

View File

@ -0,0 +1,58 @@
import request from '@/utils/request'
export function listGroupPushTypes() {
return request({
url: '/system/workAlertGroupPush/pushTypes',
method: 'get'
})
}
export function sendWorkAlertGroupPush(data) {
return request({
url: '/system/workAlertGroupPush/send',
method: 'post',
timeout: 180000,
headers: { repeatSubmit: false },
data
})
}
export function listGroupPushSchedule() {
return request({
url: '/system/workAlertGroupPush/schedule/list',
method: 'get'
})
}
export function addGroupPushSchedule(data) {
return request({
url: '/system/workAlertGroupPush/schedule',
method: 'post',
data
})
}
export function updateGroupPushSchedule(data) {
return request({
url: '/system/workAlertGroupPush/schedule',
method: 'put',
data
})
}
export function delGroupPushSchedule(id) {
const ids = Array.isArray(id) ? id.join(',') : id
return request({
url: '/system/workAlertGroupPush/schedule/' + ids,
method: 'delete'
})
}
export function triggerGroupPushSchedule(id) {
return request({
url: '/system/workAlertGroupPush/schedule/trigger/' + id,
method: 'post',
timeout: 180000,
headers: { repeatSubmit: false }
})
}

View File

@ -8,7 +8,7 @@ import { isRelogin } from '@/utils/request'
NProgress.configure({ showSpinner: false })
const whiteList = ['/login', '/register', '/index', '/indexDaping', '/mrp2', '/system/cost', '/DataBoard/ProduceKanBan/index']
const whiteList = ['/login', '/register', '/index', '/indexDaping', '/mrp2', '/system/cost', '/DataBoard/ProduceKanBan/index', '/devstat']
function isWhiteList(path) {
return whiteList.indexOf(path) !== -1

View File

@ -155,6 +155,13 @@ export const constantRoutes = [
hidden: true,
meta: { title: '零件成本查询' }
},
{
path: '/devstat',
component: () => import('@/views/devstat/index'),
name: 'DevStatBoard',
hidden: true,
meta: { title: '北通道实验台' }
},
{
path: '/system/operationPlanningMain',
component: Layout,
@ -246,7 +253,20 @@ export const constantRoutes = [
// component: () => import('@/views/index'),
// }
// ]
// }
// },
{
path: '/system/dingTodo',
component: Layout,
hidden: true,
children: [
{
path: '',
component: () => import('@/views/system/dingTodo/index'),
name: 'DingTodoPage',
meta: { title: '钉钉待办' }
}
]
}
]
// 动态路由,基于用户权限动态去加载
@ -307,6 +327,34 @@ export const dynamicRoutes = [
}
]
},
{
path: '/system/orderProDesigner',
component: Layout,
hidden: true,
permissions: ['system:orderProProgress:list'],
children: [
{
path: 'index',
component: () => import('@/views/system/orderProDesigner/index'),
name: 'OrderProDesigner',
meta: { title: '设计人汇总', activeMenu: '/system/orderProProgress' }
}
]
},
{
path: '/system/vmiMaterial',
component: Layout,
hidden: true,
permissions: ['system:vmiMaterial:list'],
children: [
{
path: 'index',
component: () => import('@/views/system/vmiMaterial/index'),
name: 'VmiMaterial',
meta: { title: 'VMI物料安全库存', activeMenu: '/system/vmiMaterial' }
}
]
},
{
path: '/tool/gen-edit',
component: Layout,

8
src/utils/jsyyOrderNo.js Normal file
View File

@ -0,0 +1,8 @@
/** JSYY 技术预研令号:不属于生产令号,禁止更新物料清单、推送工艺 */
export const JSYY_DENY_MSG =
"JSYY技术预研令号不属于生产令号,严禁直接用于生产执行,禁止更新物料清单、禁止推送工艺";
export function isJsyyOrderNo(orderNo) {
return !!(orderNo && String(orderNo).toUpperCase().indexOf("JSYY") >= 0);
}

View File

@ -0,0 +1,84 @@
/**
* 项目状态(bomStatus)—— 与后端 ProjectStatusLock 保持一致。
* 页面展示 / 筛选 / 设置状态都用这一套,不再读字典 process_status。
*
* 8 工艺审核完成:禁止出图、禁止上传工艺路线
* 6 开始生产 / 7 生产完成:禁止出图、上传工艺路线、推送工艺、更新物料清单
*/
export const ProjectStatusAction = {
DRAWING: "DRAWING",
UPLOAD_ROUTE: "UPLOAD_ROUTE",
PUSH_ROUTE: "PUSH_ROUTE",
UPDATE_MATERIAL: "UPDATE_MATERIAL",
};
const ACTION_LABEL = {
DRAWING: "出图",
UPLOAD_ROUTE: "上传工艺路线",
PUSH_ROUTE: "推送工艺",
UPDATE_MATERIAL: "更新物料清单",
};
/** 与后端写入值一致;5 为历史残留,展示等同开始生产,不出现在下拉 */
export const PROJECT_STATUS_MAP = {
0: "方案设计完成",
1: "设计出图完成",
2: "工艺待审核",
3: "已推送BOM",
4: "已推送工艺",
5: "开始生产",
6: "开始生产",
7: "生产完成",
8: "工艺审核完成",
9: "工艺审核拒绝",
};
export const PROJECT_STATUS_OPTIONS = [
{ value: 0, label: "方案设计完成" },
{ value: 1, label: "设计出图完成" },
{ value: 2, label: "工艺待审核" },
{ value: 3, label: "已推送BOM" },
{ value: 4, label: "已推送工艺" },
{ value: 6, label: "开始生产" },
{ value: 7, label: "生产完成" },
{ value: 8, label: "工艺审核完成" },
{ value: 9, label: "工艺审核拒绝" },
];
export function projectStatusLabel(bomStatus) {
const s = bomStatus === null || bomStatus === undefined || bomStatus === ""
? null
: Number(bomStatus);
if (s === null || Number.isNaN(s)) return "未知";
return PROJECT_STATUS_MAP[s] || String(s);
}
export function denyIfProjectStatusLocked(bomStatus, action) {
if (bomStatus === null || bomStatus === undefined || bomStatus === "" || !action) {
return null;
}
const s = Number(bomStatus);
if (Number.isNaN(s)) return null;
const productionLocked = s === 5 || s === 6 || s === 7;
const routeApproved = s === 8;
if (action === ProjectStatusAction.DRAWING || action === ProjectStatusAction.UPLOAD_ROUTE) {
if (productionLocked || routeApproved) {
return `当前项目状态为「${projectStatusLabel(s)}」,不允许${ACTION_LABEL[action]}`;
}
return null;
}
if (action === ProjectStatusAction.PUSH_ROUTE || action === ProjectStatusAction.UPDATE_MATERIAL) {
if (productionLocked) {
return `当前项目状态为「${projectStatusLabel(s)}」,不允许${ACTION_LABEL[action]}`;
}
return null;
}
return null;
}
export function isProjectStatusLocked(bomStatus, action) {
return !!denyIfProjectStatusLocked(bomStatus, action);
}

628
src/views/devstat/index.vue Normal file
View File

@ -0,0 +1,628 @@
<template>
<div class="bench">
<div class="bench-grain" aria-hidden="true"></div>
<header class="bench-head">
<div class="mark">
<span class="mark-kicker">NORTH CHANNEL</span>
<span class="mark-code">BENCH · 设备状态</span>
</div>
<div class="title-block">
<h1>北通道实验台</h1>
<p>升降及控制设备实时监控</p>
</div>
<div class="clock">
<span class="clock-time">{{ clockTime }}</span>
<span class="clock-date">{{ clockDate }}</span>
</div>
</header>
<section class="summary">
<div class="sum">
<span class="sum-label">设备</span>
<strong>{{ padCount(devices.length) }}</strong>
</div>
<div class="sum is-ok">
<span class="sum-label">正常</span>
<strong>{{ padCount(countOf('ok')) }}</strong>
</div>
<div class="sum is-alarm">
<span class="sum-label">报警</span>
<strong>{{ padCount(countOf('alarm')) }}</strong>
</div>
<div class="sum is-offline">
<span class="sum-label">离线</span>
<strong>{{ padCount(countOf('offline')) }}</strong>
</div>
</section>
<div class="link" :class="linkOk ? 'is-live' : 'is-down'">
<span class="link-dot"></span>
<span>{{ linkOk ? '实时连接' : (linkMsg || '等待数据') }}</span>
<span class="link-meta">最后更新 {{ lastSampleTime }}</span>
</div>
<section class="sheet">
<div class="panel-head">
<h2>设备实时状态</h2>
</div>
<p v-if="!devices.length" class="empty">
{{ linkOk ? '接口已连通,当前没有设备' : '正在连接实验台…' }}
</p>
<table v-else>
<thead>
<tr>
<th class="col-idx">序号</th>
<th>状态</th>
<th v-for="col in columns" :key="col.key">{{ col.label }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in devices" :key="item.key" :class="'is-' + item.tone">
<td class="col-idx">{{ index + 1 }}</td>
<td>
<span class="badge">
<i class="lamp" :class="'lamp-' + item.tone"></i>
{{ item.toneLabel }}
</span>
</td>
<td
v-for="col in columns"
:key="col.key"
:class="{
'col-hot': col.key === 'errorcode' && item.tone === 'alarm',
'col-num': col.key === 'pos',
'col-name': col.key === 'devname'
}"
>
<template v-if="col.key === 'pos'">
{{ item.posText }}
<span v-if="item.delta" class="delta" :class="item.deltaDir">{{ item.delta }}</span>
</template>
<template v-else>{{ item.cells[col.key] }}</template>
</td>
</tr>
</tbody>
</table>
</section>
</div>
</template>
<script>
import axios from 'axios'
const POLL_MS = 2000
const TONE_RANK = { alarm: 0, ok: 1, offline: 2 }
const KNOWN_COLUMNS = [
{ key: 'devname', label: '设备' },
{ key: 'devip', label: 'IP' },
{ key: 'ctrlmode', label: '控制模式' },
{ key: 'pos', label: '位置' },
{ key: 'status', label: '运行状态' },
{ key: 'errorcode', label: '错误码' },
{ key: 'online', label: '在线' }
]
export default {
name: 'DevStatBoard',
data() {
return {
devices: [],
columns: KNOWN_COLUMNS.slice(),
prevPos: {},
clockTime: '',
clockDate: '',
linkOk: false,
linkMsg: '',
lastOkAt: 0,
now: Date.now(),
pollTimer: null,
clockTimer: null
}
},
computed: {
lastSampleTime() {
if (!this.lastOkAt) return '—'
const d = new Date(this.lastOkAt)
const p = n => String(n).padStart(2, '0')
return p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds())
}
},
mounted() {
document.title = '北通道实验台'
document.body.classList.add('devstat-body')
this.tickClock()
this.clockTimer = setInterval(this.tickClock, 1000)
this.load()
this.pollTimer = setInterval(this.load, POLL_MS)
},
beforeDestroy() {
document.body.classList.remove('devstat-body')
clearInterval(this.pollTimer)
clearInterval(this.clockTimer)
},
methods: {
padCount(n) {
return String(n).padStart(2, '0')
},
tickClock() {
const d = new Date()
const p = n => String(n).padStart(2, '0')
this.clockTime = p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds())
this.clockDate = d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate())
this.now = d.getTime()
},
countOf(tone) {
return this.devices.filter(item => item.tone === tone).length
},
load() {
axios.get(process.env.VUE_APP_BASE_API + '/open/devstat/stat', { timeout: 6000 })
.then(res => {
const body = res.data || {}
if (body.code !== 200) {
this.linkOk = false
this.linkMsg = body.msg || '状态接口返回异常'
return
}
const list = (body.data && body.data.devstatus) || []
this.columns = this.buildColumns(list)
this.devices = list
.map(raw => this.decorate(raw))
.sort((a, b) => TONE_RANK[a.tone] - TONE_RANK[b.tone])
this.linkOk = true
this.linkMsg = ''
this.lastOkAt = Date.now()
})
.catch(() => {
this.linkOk = false
this.linkMsg = '实验台状态接口不可达'
})
},
buildColumns(list) {
const known = {}
KNOWN_COLUMNS.forEach(col => { known[col.key] = true })
const extra = []
list.forEach(raw => {
Object.keys(raw || {}).forEach(key => {
if (!known[key] && extra.indexOf(key) === -1) extra.push(key)
})
})
return KNOWN_COLUMNS.concat(extra.map(key => ({ key, label: key })))
},
decorate(raw) {
const online = Number(raw.online) === 1
const status = (raw.status || '').trim()
const error = (raw.errorcode || '').trim()
const hasError = error && error !== '0'
const alarm = /报警|故障/.test(status) || hasError
let tone = 'ok'
let toneLabel = '正常'
if (!online) {
tone = 'offline'
toneLabel = '离线'
} else if (alarm) {
tone = 'alarm'
toneLabel = '报警'
}
const posNum = Number(raw.pos)
const posText = Number.isFinite(posNum) ? posNum.toFixed(3) : '—'
const key = raw.devname || raw.devip || ''
let delta = ''
let deltaDir = ''
if (key && Number.isFinite(posNum) && this.prevPos[key] != null) {
const diff = posNum - this.prevPos[key]
if (Math.abs(diff) >= 0.0005) {
deltaDir = diff > 0 ? 'up' : 'down'
delta = (diff > 0 ? '+' : '') + diff.toFixed(3)
}
}
if (key && Number.isFinite(posNum)) this.prevPos[key] = posNum
const cells = {}
this.columns.forEach(col => {
const value = raw[col.key]
if (col.key === 'online') {
cells[col.key] = online ? '在线' : '离线'
} else if (col.key === 'pos') {
cells[col.key] = posText
} else if (value === null || value === undefined || String(value).trim() === '') {
cells[col.key] = '—'
} else {
cells[col.key] = value
}
})
return {
key: key || ('row-' + posText),
tone,
toneLabel,
posText,
delta,
deltaDir,
cells
}
}
}
}
</script>
<style>
@font-face {
font-family: "Alibaba PuHuiTi 3.0";
src: url("https://puhuiti.oss-cn-hangzhou.aliyuncs.com/AlibabaPuHuiTi-3/AlibabaPuHuiTi-3-55-Regular/AlibabaPuHuiTi-3-55-Regular.woff2") format("woff2");
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Alibaba PuHuiTi 3.0";
src: url("https://puhuiti.oss-cn-hangzhou.aliyuncs.com/AlibabaPuHuiTi-3/AlibabaPuHuiTi-3-65-Medium/AlibabaPuHuiTi-3-65-Medium.woff2") format("woff2");
font-weight: 500;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Alibaba PuHuiTi 3.0";
src: url("https://puhuiti.oss-cn-hangzhou.aliyuncs.com/AlibabaPuHuiTi-3/AlibabaPuHuiTi-3-85-Bold/AlibabaPuHuiTi-3-85-Bold.woff2") format("woff2");
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "D-DIN";
src: url("https://cdn.jsdelivr.net/fontsource/fonts/d-din@5.2.5/latin-500-normal.woff2") format("woff2");
font-weight: 500;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "D-DIN";
src: url("https://cdn.jsdelivr.net/fontsource/fonts/d-din@5.2.5/latin-700-normal.woff2") format("woff2");
font-weight: 700;
font-style: normal;
font-display: swap;
}
html,
body.devstat-body,
body.devstat-body #app {
margin: 0;
height: 100%;
overflow: hidden;
background: #0b0d0c;
}
</style>
<style scoped>
.bench {
--bg: #0b0d0c;
--card: #111513;
--card-2: #151816;
--line: #292d29;
--ink: #e8e8e3;
--muted: #85877f;
--gold: #c8a45d;
--ok: #8bd450;
--alarm: #ff5145;
--off: #666860;
--font-cn: "Alibaba PuHuiTi 3.0", "Source Han Sans SC", "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;
--font-num: "DIN Pro", "DIN", "D-DIN", Arial, sans-serif;
box-sizing: border-box;
display: flex;
flex-direction: column;
height: 100vh;
min-height: 100vh;
padding: 0 28px 16px;
overflow: hidden;
color: var(--ink);
background: var(--bg);
font-family: var(--font-cn);
font-weight: 400;
}
.bench-grain {
pointer-events: none;
position: fixed;
inset: 0;
opacity: 0.045;
background-image:
linear-gradient(rgba(232, 232, 227, 0.18) 1px, transparent 1px),
linear-gradient(90deg, rgba(232, 232, 227, 0.18) 1px, transparent 1px);
background-size: 48px 48px;
}
.bench-head,
.summary,
.link,
.sheet {
position: relative;
}
.bench-head {
flex: none;
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
height: 100px;
border-bottom: 1px solid var(--line);
}
.mark-kicker,
.mark-code {
letter-spacing: 0.16em;
}
.mark-kicker {
display: block;
color: var(--gold);
font-size: 11px;
font-weight: 500;
}
.mark-code {
display: block;
margin-top: 4px;
color: var(--muted);
font-size: 12px;
font-weight: 500;
}
.title-block {
text-align: center;
}
.title-block h1 {
margin: 0;
font-size: clamp(28px, 2.4vw, 42px);
font-weight: 700;
letter-spacing: 0.22em;
}
.title-block p {
margin: 6px 0 0;
color: var(--muted);
font-size: 13px;
font-weight: 400;
letter-spacing: 0.28em;
}
.clock {
justify-self: end;
text-align: right;
}
.clock-time,
.clock-date {
font-family: var(--font-num);
font-weight: 500;
font-variant-numeric: tabular-nums;
}
.clock-time {
display: block;
font-size: 34px;
letter-spacing: 0.04em;
}
.clock-date {
color: var(--muted);
font-size: 13px;
}
.summary {
flex: none;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
margin-top: 12px;
}
.sum {
height: 86px;
padding: 12px 16px;
background: var(--card-2);
border: 1px solid var(--line);
}
.sum-label {
display: block;
color: var(--muted);
font-size: 12px;
font-weight: 500;
letter-spacing: 0.18em;
}
.sum strong {
display: block;
margin-top: 2px;
font-family: var(--font-num);
font-size: 36px;
line-height: 1;
font-weight: 700;
font-variant-numeric: tabular-nums;
color: var(--ink);
}
.sum.is-ok strong { color: var(--ok); }
.sum.is-alarm strong { color: var(--alarm); }
.sum.is-offline strong { color: var(--off); }
.link {
flex: none;
display: flex;
align-items: center;
gap: 8px;
margin: 8px 0 10px;
color: var(--muted);
font-size: 13px;
font-weight: 500;
}
.link-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--off);
}
.link.is-live { color: var(--ok); }
.link.is-live .link-dot { background: var(--ok); }
.link.is-down { color: var(--alarm); }
.link.is-down .link-dot { background: var(--alarm); }
.link-meta {
margin-left: auto;
color: var(--muted);
font-family: var(--font-num);
font-weight: 500;
letter-spacing: 0.08em;
}
.empty {
margin: 0;
padding: 28px 16px;
color: var(--muted);
text-align: center;
font-size: 14px;
}
.sheet {
flex: 1 1 auto;
min-height: 0;
overflow: auto;
background: var(--card);
border: 1px solid var(--line);
}
.panel-head {
display: flex;
align-items: center;
justify-content: center;
height: 36px;
padding: 0 14px;
border-bottom: 1px solid var(--line);
}
.panel-head h2 {
margin: 0;
font-size: 13px;
font-weight: 500;
color: var(--gold);
letter-spacing: 0.16em;
}
table {
width: 100%;
border-collapse: collapse;
}
th,
td {
padding: 14px 12px;
text-align: center;
border-bottom: 1px solid var(--line);
white-space: nowrap;
font-size: 18px;
}
th {
position: sticky;
top: 36px;
z-index: 1;
background: #151816;
color: var(--muted);
font-size: 16px;
font-weight: 500;
letter-spacing: 0.08em;
}
.sheet .panel-head {
position: sticky;
top: 0;
z-index: 2;
background: var(--card);
}
tr.is-alarm td { background: #1a1212; }
tr.is-offline td { color: var(--off); }
.col-idx {
width: 64px;
color: var(--muted);
font-family: var(--font-num);
font-weight: 700;
font-variant-numeric: tabular-nums;
}
.col-name {
font-weight: 500;
}
.col-num,
.delta {
font-family: var(--font-num);
font-weight: 700;
font-variant-numeric: tabular-nums;
}
.col-hot {
color: var(--alarm);
}
.badge {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
font-size: 16px;
font-weight: 500;
}
.lamp {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--off);
}
.lamp-ok { background: var(--ok); }
.lamp-alarm {
background: var(--alarm);
animation: pulse 1.8s ease-in-out infinite;
}
.lamp-offline { background: var(--off); }
.is-ok .badge { color: var(--ok); }
.is-alarm .badge { color: var(--alarm); }
.is-offline .badge { color: var(--off); }
.delta { margin-left: 8px; font-size: 13px; }
.delta.up { color: var(--ok); }
.delta.down { color: var(--alarm); }
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
@media (max-width: 1366px) {
.bench { padding: 0 16px 12px; }
.bench-head { height: 84px; }
.title-block h1 { letter-spacing: 0.12em; }
.sum { height: 72px; }
.sum strong { font-size: 30px; }
th, td { font-size: 16px; }
}
@media (max-width: 900px) {
.bench { overflow: auto; }
.bench-head,
.summary {
grid-template-columns: 1fr;
}
.clock,
.title-block,
.mark {
text-align: left;
justify-self: start;
}
}
</style>

View File

@ -1,10 +1,10 @@
<template>
<div class="app-container">
<div style="display: flex; align-items: center;">
<img src="@/assets/logo/logo.png" alt="Logo" style="width: 170px; height: 55px; margin-right: 10px;">
<h3 style="text-align: left; color: black; font-family: '黑体', sans-serif;font-weight: bold;font-size: 38px; margin-left: 10px;" class="title">物料检索</h3>
<div class="page-header">
<img src="@/assets/logo/logo.png" alt="Logo" class="page-logo">
<h3 class="page-title">物料检索</h3>
</div>
<h3> </h3>
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="物料编号" prop="materialCode">
<el-input
@ -21,7 +21,6 @@
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="名称" prop="unit">
<el-input
@ -39,7 +38,6 @@
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="材质" prop="materialQuality">
<el-input
v-model="queryParams.materialQuality"
@ -57,137 +55,141 @@
style="font-family: '黑体', Arial, sans-serif;"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-card class="select-card" shadow="never">
<div class="select-row">
<label class="select-label">指定物料</label>
<el-radio-group v-model="searchMode" size="mini" class="search-mode">
<el-radio-button label="code">按编码</el-radio-button>
<el-radio-button label="name">按名称</el-radio-button>
</el-radio-group>
<el-select
v-model="selectedMaterials"
multiple
filterable
remote
reserve-keyword
collapse-tags
:placeholder="searchMode === 'code' ? '可选:搜索并多选物料后查询库存' : '可选:按名称搜索并多选'"
:remote-method="remoteSearchMaterial"
:loading="materialLoading"
value-key="materialCode"
class="material-select"
>
<el-option
v-for="item in materialOptions"
:key="item.materialCode"
:label="item.materialCode + ' - ' + (item.materialName || '')"
:value="item"
/>
</el-select>
<el-button type="primary" icon="el-icon-search" size="mini" :loading="loading" @click="handleQueryInventory">
查询库存
</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="clearSelection">清除指定</el-button>
</div>
<div v-if="selectedMaterials.length" class="selected-tags">
<el-tag
v-for="m in selectedMaterials"
:key="m.materialCode"
type="info"
closable
size="small"
@close="removeSelected(m)"
>{{ m.materialCode }} - {{ m.materialName }}</el-tag>
</div>
<div class="select-tip">
未指定物料时展示默认列表;指定后点「查询库存」只查所选物料(最多 50 个)
</div>
</el-card>
<el-table v-loading="loading" :data="materialList" class="material-table">
<!-- <el-table-column type="selection" width="55" align="center" />-->
<el-table-column label="ID" width="60" align="center" prop="id" v-if="true"/>
<el-table-column label="物料编号" align="center" prop="materialCode">
<el-table v-loading="loading" :data="materialList" class="material-table" style="width: 100%">
<el-table-column label="ID" width="60" align="center" prop="id" v-if="!selectedMode"/>
<el-table-column label="物料编号" align="center" prop="materialCode" min-width="110" show-overflow-tooltip>
<template slot-scope="scope">
<el-link type="primary" :underline="true" class="material-code-link" @click="handleClick(scope.row)">
{{ scope.row.materialCode }}
</el-link>
<span class="material-code-link">{{ scope.row.materialCode }}</span>
</template>
</el-table-column>
<el-table-column label="物料名称" align="center" prop="materialName">
<template slot-scope="scope">
<span style="font-family: '微软雅黑',sans-serif;">{{ scope.row.materialName }}</span>
</template>
<el-table-column label="物料名称" align="center" prop="materialName" min-width="120" show-overflow-tooltip>
<template slot-scope="scope">
<span style="font-family: '微软雅黑',sans-serif;">{{ scope.row.materialName }}</span>
</template>
</el-table-column>
<el-table-column label="单位" align="center" prop="unit"/>
<el-table-column label="即时库存" align="center" prop="inInventory"/>
<el-table-column label="材质" align="center" prop="materialQuality" >
<el-table-column label="单位" align="center" prop="unit" width="60"/>
<el-table-column label="库存" align="center" prop="kucun" min-width="90">
<template slot-scope="scope">
<span :class="qtyClass(scope.row.kucun)">{{ formatQty(scope.row.kucun) }}</span>
</template>
</el-table-column>
<el-table-column label="子项未领料量" align="center" prop="childNotPickedQty" min-width="110">
<template slot-scope="scope">
<span class="val">{{ formatQty(scope.row.childNotPickedQty) }}</span>
</template>
</el-table-column>
<el-table-column label="生产订单未入库" align="center" prop="prdNotInQty" min-width="120">
<template slot-scope="scope">
<span class="val">{{ formatQty(scope.row.prdNotInQty) }}</span>
</template>
</el-table-column>
<el-table-column label="采购订单未入库" align="center" prop="poNotInQty" min-width="120">
<template slot-scope="scope">
<span class="val">{{ formatQty(scope.row.poNotInQty) }}</span>
</template>
</el-table-column>
<el-table-column label="销售订单未出库" align="center" prop="soNotOutQty" min-width="120">
<template slot-scope="scope">
<span class="val">{{ formatQty(scope.row.soNotOutQty) }}</span>
</template>
</el-table-column>
<el-table-column label="预计可用库存" align="center" prop="availableQty" min-width="110">
<template slot-scope="scope">
<span :class="qtyClass(scope.row.availableQty)">{{ formatQty(scope.row.availableQty) }}</span>
</template>
</el-table-column>
<el-table-column label="材质" align="center" prop="materialQuality" min-width="120" show-overflow-tooltip>
<template slot-scope="scope">
<dict-tag :options="dict.type.material_properties" :value="scope.row.materialQuality"/>
</template>
</el-table-column>
<el-table-column label="所属类别" align="center" prop="imCategory" >
<el-table-column label="所属类别" align="center" prop="imCategory" min-width="120" show-overflow-tooltip>
<template slot-scope="scope">
<dict-tag :options="dict.type.im_category" :value="scope.row.imCategory"/>
</template>
</el-table-column>
<!-- <el-table-column label="单价" align="center" prop="unitPrice" />-->
<el-table-column label="物料属性" align="center" prop="classificationNumber" >
<el-table-column label="物料属性" align="center" prop="classificationNumber" min-width="90">
<template slot-scope="scope">
<dict-tag :options="dict.type.item_attributes" :value="scope.row.classificationNumber"/>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
v-show="!selectedMode && total > 0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<transition name="fade">
<el-dialog :visible="dialogVisible" @close="dialogVisible = false" width="36%">
<el-card class="box-card" v-loading="dialogLoading" element-loading-text="加载中..." element-loading-background="rgba(255, 255, 255, 0.8)">
<div slot="header" class="clearfix header-bar">
<span class="dialog-title">库存信息</span>
<span class="material-title">{{ inventoryData.materialCode }}</span>
</div>
<el-descriptions :column="2" border class="inventory-desc" size="small">
<el-descriptions-item label="库存">
<span :class="['val', inventoryData.kucun > 0 ? 'val-pos' : (inventoryData.kucun < 0 ? 'val-neg' : 'val-zero')]">{{ inventoryData.kucun }}</span>
</el-descriptions-item>
<el-descriptions-item label="子项未领料量">
<span class="val">{{ inventoryData.inventory }}</span>
</el-descriptions-item>
<el-descriptions-item label="生产订单未入库">
<span class="val">{{ inventoryData.stockName }}</span>
</el-descriptions-item>
<el-descriptions-item label="采购订单未入库">
<span class="val">{{ inventoryData.materialName }}</span>
</el-descriptions-item>
<el-descriptions-item label="销售订单未出库">
<span class="val">{{ inventoryData.salesOrder }}</span>
</el-descriptions-item>
<el-descriptions-item label="预计可用库存">
<span :class="['val', inventoryData.stockUnit > 0 ? 'val-pos' : (inventoryData.stockUnit < 0 ? 'val-neg' : 'val-zero')]">{{ inventoryData.stockUnit }}</span>
</el-descriptions-item>
</el-descriptions>
</el-card>
</el-dialog>
</transition>
</div>
</template>
<script>
import {listMaterial_index, material_inventory} from "@/api/system/material";
/*import upload from "svg-sprite-loader/examples/custom-runtime-generator/build/main";*/
import dict from "@/utils/dict";
import DictData from "@/components/DictData";
import { listMaterial_index, material_inventory } from "@/api/system/material";
export default {
name: "Material",
dicts: ['material_properties','item_attributes','im_category'],
computed: {
upload() {
return upload
}
},
dicts: ['material_properties', 'item_attributes', 'im_category'],
data() {
return {
// 按钮loading
buttonLoading: false,
dialogLoading: false,
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 显示搜索条件
showSearch: true,
// 总条数
total: 0,
// 物料检索表格数据
materialList: [],
dialogVisible: false, // 控制弹框的显示与隐藏
inventoryData: {materialCode: '', inventory: 0, materialName:''}, // 库存数据
// 弹出层标题
title: "",
// 是否显示弹出层
open: false,
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
@ -200,173 +202,229 @@ export default {
unitPrice: undefined,
unit: undefined,
flag: undefined,
salesOrder:undefined
salesOrder: undefined
},
// 表单参数
form: {},
materialLoading: false,
searchMode: 'code',
selectedMaterials: [],
materialOptions: [],
/** 是否处于「指定物料查询」模式(无分页) */
selectedMode: false,
inventoryReqSeq: 0
};
},
created() {
this.getList();
},
methods: {
handleClick(row) {
// 在这里调用后台方法,获取数据
this.showInventory(row.materialCode); // 打开弹框
formatQty(val) {
if (val === undefined || val === null || val === "") {
return "-";
}
return val;
},
showInventory(materialCode) {
this.dialogLoading = true; // 显示加载动画
material_inventory(materialCode).then(response => {
this.inventoryData.materialCode = materialCode;
this.inventoryData.inventory = response.data.quantity;
this.inventoryData.materialName = response.data.materialName;
this.inventoryData.stockName = response.data.stockName;
this.inventoryData.stockUnit = response.data.stockUnit;
this.inventoryData.kucun = response.data.kucun;
this.inventoryData.salesOrder = response.data.salesOrder;
this.dialogVisible = true;
})
.catch(error => {
console.error('获取库存数失败:', error);
})
.finally(() => {
this.dialogLoading = false; // 隐藏加载动画
qtyClass(val) {
const n = Number(val);
if (!Number.isFinite(n)) {
return "val";
}
if (n > 0) return "val val-pos";
if (n < 0) return "val val-neg";
return "val val-zero";
},
clearInventoryFields(row) {
this.$set(row, "kucun", undefined);
this.$set(row, "childNotPickedQty", undefined);
this.$set(row, "prdNotInQty", undefined);
this.$set(row, "poNotInQty", undefined);
this.$set(row, "soNotOutQty", undefined);
this.$set(row, "availableQty", undefined);
},
applyInventoryToRow(row, data) {
if (!data) {
return;
}
this.$set(row, "kucun", data.kucun);
this.$set(row, "childNotPickedQty", data.quantity);
this.$set(row, "prdNotInQty", data.stockName);
this.$set(row, "poNotInQty", data.materialName);
this.$set(row, "soNotOutQty", data.salesOrder);
this.$set(row, "availableQty", data.stockUnit);
},
async fillInventoryForList(list, reqSeq) {
if (!list || !list.length) {
return;
}
const codes = list.map(r => r && r.materialCode).filter(Boolean);
if (!codes.length) {
return;
}
try {
const response = await material_inventory(codes);
if (reqSeq !== this.inventoryReqSeq) {
return;
}
const invList = (response && response.data) || [];
const byCode = {};
invList.forEach(item => {
if (item && item.materialCode) {
byCode[item.materialCode] = item;
}
});
},
getMaterialCodeColor(materialCode) {
if (materialCode.startsWith('009')) {
return 'blue';
} else if (materialCode.startsWith('002N')||materialCode.startsWith('002Y')) {
return 'gold';
} else if (materialCode.startsWith('005')) {
return 'red';
} else if (materialCode.startsWith('006')) {
return 'pink';
} else if (materialCode.startsWith('015')) {
return 'green';
} else if (materialCode.startsWith('016')) {
return 'shrimp';
} else if (materialCode.startsWith('017')) {
return 'teal';
} else {
return ''; // 默认颜色
list.forEach(row => {
this.applyInventoryToRow(row, byCode[row.materialCode]);
});
} catch (error) {
console.error("获取库存失败:", error);
}
},
/** 查询物料检索列表 */
/** 默认列表:分页物料 + 批量填充库存 */
getList() {
this.selectedMode = false;
this.loading = true;
listMaterial_index(this.queryParams).then(response => {
this.materialList = response.rows;
this.total = response.total;
this.loading = false;
});
const reqSeq = ++this.inventoryReqSeq;
listMaterial_index(this.queryParams)
.then(async (response) => {
const rows = response.rows || [];
rows.forEach((row) => this.clearInventoryFields(row));
this.materialList = rows;
this.total = response.total;
this.loading = false;
await this.fillInventoryForList(rows, reqSeq);
})
.catch(() => {
this.loading = false;
});
},
// 表单重置
reset() {
this.form = {
id: undefined,
materialCode: undefined,
materialName: undefined,
materialQuality: undefined,
singleWeight: undefined,
unitPrice: undefined,
unit: undefined,
flag: undefined,
createTime: undefined,
updateTime: undefined
};
this.resetForm("form");
},
/** 搜索按钮操作 */
handleQuery() {
this.selectedMode = false;
this.queryParams.pageNum = 1;
this.getList();
},
/** 重置按钮操作 */
resetQuery() {
this.resetForm("queryForm");
this.handleQuery();
},
remoteSearchMaterial(query) {
if (!query || !String(query).trim()) {
this.materialOptions = [];
return;
}
this.materialLoading = true;
const params = { pageNum: 1, pageSize: 30 };
if (this.searchMode === 'code') {
params.materialCode = query.trim();
} else {
params.materialName = query.trim();
}
listMaterial_index(params)
.then(res => {
this.materialOptions = res.rows || [];
})
.finally(() => {
this.materialLoading = false;
});
},
removeSelected(item) {
this.selectedMaterials = this.selectedMaterials.filter(m => m.materialCode !== item.materialCode);
},
clearSelection() {
this.selectedMaterials = [];
this.materialOptions = [];
this.getList();
},
/** 指定物料:一次批量查库存 */
async handleQueryInventory() {
if (!this.selectedMaterials.length) {
this.$message.info("未指定物料,已按默认列表查询");
this.getList();
return;
}
if (this.selectedMaterials.length > 50) {
this.$message.warning("单次最多查询 50 个物料");
return;
}
this.selectedMode = true;
const reqSeq = ++this.inventoryReqSeq;
const codes = this.selectedMaterials.map(m => m.materialCode).filter(Boolean);
const rows = this.selectedMaterials.map(m => ({
id: m.id,
materialCode: m.materialCode,
materialName: m.materialName,
unit: m.unit,
materialQuality: m.materialQuality,
imCategory: m.imCategory,
classificationNumber: m.classificationNumber
}));
rows.forEach(row => this.clearInventoryFields(row));
this.materialList = rows;
this.total = rows.length;
this.loading = true;
try {
await this.fillInventoryForList(rows, reqSeq);
} finally {
if (reqSeq === this.inventoryReqSeq) {
this.loading = false;
}
}
}
}
};
</script>
<style>
/*.app-container {
background: linear-gradient(to right, #ff7e5f, #feb47b); !* 从左到右的渐变 *!
}*/
</style>
<style>
.el-dialog {
left: 50%;
top: 40%;
transform: translate(-45%, -32%);
margin: 0px !important;
}
.dialog-content {
display: flex;
flex-direction: column;
align-items: flex-start; /* 左对齐字段数据 */
margin-left: 5px; /* 调整字段数据与边框的距禿 */
}
.fade-enter-active, .fade-leave-active {
transition: opacity 0.4s ease;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
.box-card {
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.dialog-title {
font-size: 18px;
font-weight: bold;
color: #409EFF;
}
.dialog-body {
font-family: '微软雅黑', sans-serif;
line-height: 1.6;
}
.dialog-item {
margin-bottom: 10px;
font-size: 14px;
color: #333;
}
.clearfix::after {
content: "";
display: block;
clear: both;
}
.dialog-text {
margin: 8px 0;
}
.dialog-title {
font-size: 20px;
font-weight: bold;
color: burlywood;
text-align: center;
margin-bottom: 10px;
}
.header-bar {
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.material-title {
font-size: 16px;
.page-logo {
width: 170px;
height: 55px;
margin-right: 10px;
}
.page-title {
text-align: left;
color: black;
font-family: '黑体', sans-serif;
font-weight: bold;
font-size: 38px;
margin: 0 0 0 10px;
}
.select-card {
margin-bottom: 16px;
}
.select-row {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
}
.select-label {
font-size: 14px;
font-weight: 600;
color: #409EFF;
color: #606266;
white-space: nowrap;
}
.search-mode {
margin-right: 4px;
}
.material-select {
min-width: 360px;
flex: 1;
}
.selected-tags {
margin-top: 10px;
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.select-tip {
margin-top: 8px;
font-size: 12px;
color: #909399;
}
.material-table {
width: 100%;
}
.material-table .el-table__header .cell {
font-size: 16px;
@ -381,19 +439,6 @@ export default {
font-size: 16px;
font-weight: 700;
}
.inventory-desc {
--label-bg: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
--label-color: #409EFF;
}
.inventory-desc .el-descriptions__label {
background: var(--label-bg);
color: var(--label-color);
font-weight: 600;
}
.inventory-desc .el-descriptions__content {
color: #303133;
font-size: 13px;
}
.val {
font-weight: 600;
}

View File

@ -93,7 +93,10 @@
<el-input v-model="groupName" placeholder="请输入上传分组" size="mini" clearable />
</el-col>
<el-col :span="1.5">
<el-button type="info" plain icon="el-icon-upload2" size="mini" @click="updateFBMaterial" v-hasPermi="['system:details:updateFBMaterial']">更新物料清单</el-button>
<el-button type="info" plain icon="el-icon-upload2" size="mini" @click="updateFBMaterial"
:disabled="jsyyBlocked || updateMaterialStatusLocked"
:title="(jsyyBlocked ? jsyyDenyMsg : '') || updateMaterialStatusLockMsg || ''"
v-hasPermi="['system:details:updateFBMaterial']">更新物料清单</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="primary" plain icon="el-icon-view" size="mini" @click="getBomUploadStatus" v-hasPermi="['system:details:viewGetBomUploadStatus']">查看BOM上传状态</el-button>
@ -303,6 +306,12 @@
<script>
import { listDetails, getDetails, delDetails, addDetails, updateDetails, updateFBl, viewGetBomUploadStatus } from "@/api/system/details";
import { getToken } from "@/utils/auth";
import { isJsyyOrderNo, JSYY_DENY_MSG } from "@/utils/jsyyOrderNo";
import {
ProjectStatusAction,
denyIfProjectStatusLocked,
} from "@/utils/projectStatusLock";
import { listOrderPro } from "@/api/system/orderPro";
export default {
name: "Details",
@ -345,6 +354,8 @@ export default {
remarks: undefined,
},
form: {},
/** 当前令号对应项目状态,用于状态锁 */
projectBomStatus: undefined,
upload: {
open: false,
title: "",
@ -367,6 +378,24 @@ export default {
embedOrderName() {
return (this.params && this.params.productionName) || "";
},
jsyyBlocked() {
return isJsyyOrderNo(this.embedOrderNo);
},
jsyyDenyMsg() {
return JSYY_DENY_MSG;
},
resolvedBomStatus() {
if (this.params && this.params.bomStatus !== undefined && this.params.bomStatus !== null && this.params.bomStatus !== "") {
return this.params.bomStatus;
}
return this.projectBomStatus;
},
updateMaterialStatusLockMsg() {
return denyIfProjectStatusLocked(this.resolvedBomStatus, ProjectStatusAction.UPDATE_MATERIAL);
},
updateMaterialStatusLocked() {
return !!this.updateMaterialStatusLockMsg;
},
tableHeaderStyle() {
return {
background: "#eef2f7",
@ -427,6 +456,21 @@ export default {
return this.hasFieldData(["stateImportance"]);
},
},
watch: {
"params.productionOrderNo": {
immediate: true,
handler(val) {
if (val) {
this.refreshProjectBomStatus(val);
}
},
},
"queryParams.totalWeight"(val) {
if (val) {
this.refreshProjectBomStatus(val);
}
},
},
created() {
this.queryParams.totalWeight = "";
if (this.params && this.params.productionOrderNo) {
@ -436,6 +480,23 @@ export default {
this.getList();
},
methods: {
async refreshProjectBomStatus(orderNo) {
if (!orderNo) {
this.projectBomStatus = undefined;
return;
}
if (this.params && this.params.bomStatus !== undefined && this.params.bomStatus !== null && this.params.bomStatus !== "") {
this.projectBomStatus = this.params.bomStatus;
return;
}
try {
const res = await listOrderPro({ productionOrderNo: orderNo, pageNum: 1, pageSize: 1 });
const row = (res.rows && res.rows[0]) || null;
this.projectBomStatus = row ? row.bomStatus : undefined;
} catch (e) {
this.projectBomStatus = undefined;
}
},
isParentRow(row) {
return !!(row && row.children && row.children.length);
},
@ -694,6 +755,16 @@ export default {
},
updateFBMaterial() {
this.loading = true;
if (this.jsyyBlocked) {
this.$modal.msgError(this.jsyyDenyMsg);
this.loading = false;
return;
}
if (this.updateMaterialStatusLocked) {
this.$modal.msgError(this.updateMaterialStatusLockMsg);
this.loading = false;
return;
}
if (!this.selectedRows || this.selectedRows.length === 0) {
this.$modal.msgError("请先选择要上传的行!");
this.loading = false;
@ -724,6 +795,11 @@ export default {
this.loading = false;
return;
}
if (selectedData.some((row) => isJsyyOrderNo(row.totalWeight))) {
this.$modal.msgError(this.jsyyDenyMsg);
this.loading = false;
return;
}
updateFBl(selectedData)
.then((response) => {
const list = Array.isArray(response.data) ? response.data : [];

View File

@ -0,0 +1,185 @@
<template>
<div class="app-container">
<el-card shadow="never">
<div slot="header">哪些方法发送待办</div>
<p class="hint">
类型选「姓名」时,接收人填姓名,多个用逗号,例如田志阳;选「工号」时填考勤编码。创建人类型与接收人相同,留空则用第一个接收人。
</p>
<el-table v-loading="sceneLoading" :data="scenes" border>
<el-table-column label="方法" prop="sceneName" min-width="220" />
<el-table-column label="发送" width="90" align="center">
<template slot-scope="scope">
<el-switch
v-model="scope.row.enabled"
active-value="1"
inactive-value="0"
@change="saveScene(scope.row)"
/>
</template>
</el-table-column>
<el-table-column label="类型" width="120">
<template slot-scope="scope">
<el-select v-model="scope.row.recipientType" size="small">
<el-option label="姓名" value="name" />
<el-option label="工号" value="code" />
</el-select>
</template>
</el-table-column>
<el-table-column label="创建人" min-width="160">
<template slot-scope="scope">
<el-input
v-model="scope.row.createUser"
size="small"
:placeholder="scope.row.recipientType === 'code' ? '工号,空则用第一个接收人' : '姓名,空则用第一个接收人'"
clearable
/>
</template>
</el-table-column>
<el-table-column label="接收人" min-width="220">
<template slot-scope="scope">
<el-input
v-model="scope.row.userIds"
size="small"
:placeholder="scope.row.recipientType === 'code' ? '考勤工号,多个用逗号' : '姓名,多个用逗号,如田志阳'"
clearable
/>
</template>
</el-table-column>
<el-table-column label="截止天数" width="120">
<template slot-scope="scope">
<el-input-number v-model="scope.row.dueDays" size="small" :min="1" :max="365" controls-position="right" />
</template>
</el-table-column>
<el-table-column label="待办说明" min-width="220">
<template slot-scope="scope">
<el-input v-model="scope.row.contentLead" size="small" />
</template>
</el-table-column>
<el-table-column label="操作" width="90" align="center">
<template slot-scope="scope">
<el-button type="text" size="mini" @click="saveScene(scope.row)">保存</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-card shadow="never" style="margin-top: 16px">
<div slot="header">待办记录</div>
<el-form :model="query" size="small" :inline="true">
<el-form-item label="生产令号">
<el-input v-model="query.productionOrderNo" clearable placeholder="令号" @keyup.enter.native="loadLogs" />
</el-form-item>
<el-form-item label="方法">
<el-select v-model="query.sceneCode" clearable placeholder="全部" style="width: 240px">
<el-option v-for="item in scenes" :key="item.sceneCode" :label="item.sceneName" :value="item.sceneCode" />
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="query.status" clearable placeholder="全部" style="width: 120px">
<el-option label="成功" value="SUCCESS" />
<el-option label="失败" value="FAIL" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" size="mini" @click="loadLogs">查询</el-button>
</el-form-item>
</el-form>
<el-table v-loading="logLoading" :data="logs" border>
<el-table-column label="时间" prop="createTime" width="160" />
<el-table-column label="方法" prop="sceneName" min-width="180" />
<el-table-column label="令号" prop="productionOrderNo" width="140" />
<el-table-column label="接收人" prop="userIds" min-width="180" show-overflow-tooltip />
<el-table-column label="创建人" prop="createUser" width="160" show-overflow-tooltip />
<el-table-column label="文件" prop="fileName" min-width="160" show-overflow-tooltip />
<el-table-column label="状态" width="90">
<template slot-scope="scope">
<el-tag :type="scope.row.status === 'SUCCESS' ? 'success' : 'danger'" size="mini">
{{ scope.row.status === 'SUCCESS' ? '成功' : '失败' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="失败原因" prop="errorMsg" min-width="200" show-overflow-tooltip />
</el-table>
<pagination
v-show="total > 0"
:total="total"
:page.sync="query.pageNum"
:limit.sync="query.pageSize"
@pagination="loadLogs"
/>
</el-card>
</div>
</template>
<script>
import { listDingTodoLogs, listDingTodoScenes, updateDingTodoScene } from '@/api/system/dingTodo'
export default {
name: 'DingTodo',
data() {
return {
sceneLoading: false,
logLoading: false,
scenes: [],
logs: [],
total: 0,
query: {
pageNum: 1,
pageSize: 10,
productionOrderNo: undefined,
sceneCode: undefined,
status: undefined
}
}
},
created() {
this.loadScenes()
this.loadLogs()
},
methods: {
loadScenes() {
this.sceneLoading = true
listDingTodoScenes().then(res => {
this.scenes = (res.data || []).map(item => {
if (!item.recipientType) {
item.recipientType = 'name'
}
return item
})
}).finally(() => {
this.sceneLoading = false
})
},
saveScene(row) {
updateDingTodoScene({
id: row.id,
enabled: row.enabled,
recipientType: row.recipientType || 'name',
createUser: row.createUser,
userIds: row.userIds,
dueDays: row.dueDays,
contentLead: row.contentLead
}).then(() => {
this.$modal.msgSuccess('已保存')
})
},
loadLogs() {
this.logLoading = true
listDingTodoLogs(this.query).then(res => {
this.logs = res.rows || []
this.total = res.total || 0
}).finally(() => {
this.logLoading = false
})
}
}
}
</script>
<style scoped>
.hint {
margin: 0 0 12px;
color: #606266;
line-height: 1.6;
}
</style>

View File

@ -0,0 +1,629 @@
<template>
<div class="dro-page app-container">
<section class="page-hero">
<div class="page-hero__main">
<div class="page-hero__icon">
<i class="el-icon-notebook-2"></i>
</div>
<div>
<h1 class="page-hero__title">出图 / 工艺 / 钉钉推送日志</h1>
<p class="page-hero__desc">记录执行出图、出图后推送、工艺上传、上传后钉钉、无需做工艺通知、电气BOM上传通知,便于按令号追溯成功与失败。</p>
</div>
</div>
<div class="page-hero__stats">
<div class="stat-chip">
<span class="stat-chip__label">当前页</span>
<span class="stat-chip__value">{{ logList.length }}</span>
</div>
<div class="stat-chip stat-chip--accent">
<span class="stat-chip__label">累计</span>
<span class="stat-chip__value">{{ total }}</span>
</div>
<div class="stat-chip">
<span class="stat-chip__label">本页成功</span>
<span class="stat-chip__value">{{ pageSuccessCount }}</span>
</div>
</div>
</section>
<el-card shadow="never" class="search-card">
<el-form
:model="queryParams"
ref="queryForm"
size="small"
:inline="true"
v-show="showSearch"
label-width="88px"
class="search-form"
>
<el-form-item label="生产令号" prop="productionOrderNo">
<el-input
v-model="queryParams.productionOrderNo"
placeholder="模糊匹配生产令号"
clearable
style="width: 200px"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="业务类型" prop="bizType">
<el-select v-model="queryParams.bizType" clearable placeholder="全部" style="width: 170px">
<el-option label="执行出图" value="DRAWING_EXEC" />
<el-option label="出图后推送" value="DRAWING_NOTIFY" />
<el-option label="工艺上传" value="ROUTE_UPLOAD" />
<el-option label="上传后钉钉" value="ROUTE_NOTIFY" />
<!-- 无需做工艺 / 电气 BOM 钉钉通知 -->
<el-option label="无需做工艺通知" value="SKIP_PROCESS_NOTIFY" />
<el-option label="电气BOM通知" value="ELECTRICAL_BOM_NOTIFY" />
</el-select>
</el-form-item>
<el-form-item label="来源" prop="channel">
<el-select v-model="queryParams.channel" clearable placeholder="全部" style="width: 120px">
<el-option label="手动" value="MANUAL" />
<el-option label="PLM" value="PLM" />
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" clearable placeholder="全部" style="width: 120px">
<el-option label="成功" value="SUCCESS" />
<el-option label="失败" value="FAIL" />
<el-option label="部分成功" value="PARTIAL" />
</el-select>
</el-form-item>
<el-form-item label="操作时间">
<el-date-picker
v-model="dateRange"
type="datetimerange"
value-format="yyyy-MM-dd HH:mm:ss"
range-separator="至"
start-placeholder="开始时间"
end-placeholder="结束时间"
:default-time="['00:00:00', '23:59:59']"
style="width: 360px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-row :gutter="10" class="toolbar-row">
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-card shadow="never" class="table-card">
<el-table
v-loading="loading"
:data="logList"
:header-cell-style="headerCellStyle"
stripe
>
<el-table-column type="index" label="#" width="52" align="center" />
<el-table-column label="生产令号" min-width="150" show-overflow-tooltip>
<template slot-scope="scope">
<span class="order-no">{{ scope.row.productionOrderNo || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="类型" width="120" align="center">
<template slot-scope="scope">
<el-tag size="mini" :type="bizTypeTag(scope.row.bizType)" effect="plain">
{{ bizTypeLabel(scope.row.bizType) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="来源" width="80" align="center">
<template slot-scope="scope">
{{ scope.row.channel === 'PLM' ? 'PLM' : (scope.row.channel === 'MANUAL' ? '手动' : (scope.row.channel || '-')) }}
</template>
</el-table-column>
<el-table-column label="状态" width="96" align="center">
<template slot-scope="scope">
<el-tag size="mini" :type="statusTag(scope.row.status)">
{{ statusLabel(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作人" width="100" align="center" show-overflow-tooltip prop="operator" />
<el-table-column label="接收人" min-width="140" show-overflow-tooltip prop="notifyUserIds" />
<el-table-column label="文件" min-width="140" show-overflow-tooltip prop="fileName" />
<el-table-column label="耗时" width="80" align="center">
<template slot-scope="scope">
{{ formatDuration(scope.row.durationMs) }}
</template>
</el-table-column>
<el-table-column label="操作时间" width="168" align="center">
<template slot-scope="scope">
{{ parseTime(scope.row.operTime, '{y}-{m}-{d} {h}:{i}:{s}') }}
</template>
</el-table-column>
<el-table-column label="失败原因" min-width="160" show-overflow-tooltip prop="errorMsg" />
<el-table-column label="操作" width="168" align="center" fixed="right">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-view"
@click="handleDetail(scope.row)"
v-hasPermi="['system:drawingRouteOperLog:query']"
>详情</el-button>
<el-button
v-if="canRenotify(scope.row)"
size="mini"
type="text"
icon="el-icon-message"
:loading="renotifyId === scope.row.id"
@click="handleRenotify(scope.row)"
v-hasPermi="['system:drawingRouteOperLog:query']"
>重新通知</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total > 0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
</el-card>
<el-drawer
:title="detailTitle"
:visible.sync="detailOpen"
direction="rtl"
size="720px"
append-to-body
@closed="resetDetail"
>
<div v-loading="detailLoading" class="detail-body">
<template v-if="detail">
<div class="detail-summary">
<div class="detail-summary__item">
<span class="detail-summary__label">生产令号</span>
<span class="detail-summary__value">{{ detail.productionOrderNo || '-' }}</span>
</div>
<div class="detail-summary__item">
<span class="detail-summary__label">业务类型</span>
<span class="detail-summary__value">{{ bizTypeLabel(detail.bizType) }}</span>
</div>
<div class="detail-summary__item">
<span class="detail-summary__label">来源</span>
<span class="detail-summary__value">{{ detail.channel === 'PLM' ? 'PLM' : '手动' }}</span>
</div>
<div class="detail-summary__item">
<span class="detail-summary__label">状态</span>
<el-tag size="mini" :type="statusTag(detail.status)">{{ statusLabel(detail.status) }}</el-tag>
</div>
<div class="detail-summary__item">
<span class="detail-summary__label">操作人</span>
<span class="detail-summary__value">{{ detail.operator || '-' }}</span>
</div>
<div class="detail-summary__item">
<span class="detail-summary__label">客户端 IP</span>
<span class="detail-summary__value">{{ detail.clientIp || '-' }}</span>
</div>
<div class="detail-summary__item">
<span class="detail-summary__label">接口</span>
<code class="path-text">{{ detail.apiPath || '-' }}</code>
</div>
<div class="detail-summary__item">
<span class="detail-summary__label">出图模式</span>
<span class="detail-summary__value">{{ detail.drawingMode || '-' }}</span>
</div>
<div class="detail-summary__item">
<span class="detail-summary__label">通知类型</span>
<span class="detail-summary__value">{{ notifyTypeLabel(detail.notifyType) }}</span>
</div>
<div class="detail-summary__item">
<span class="detail-summary__label">接收人</span>
<span class="detail-summary__value">{{ detail.notifyUserIds || '-' }}</span>
</div>
<div class="detail-summary__item">
<span class="detail-summary__label">文件</span>
<span class="detail-summary__value">{{ detail.fileName || '-' }}</span>
</div>
<div class="detail-summary__item">
<span class="detail-summary__label">耗时</span>
<span class="detail-summary__value">{{ formatDuration(detail.durationMs) }}</span>
</div>
<div class="detail-summary__item">
<span class="detail-summary__label">操作时间</span>
<span class="detail-summary__value">{{ parseTime(detail.operTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</div>
<div class="detail-summary__item detail-summary__item--wide" v-if="detail.messageTitle">
<span class="detail-summary__label">消息标题</span>
<span class="detail-summary__value">{{ detail.messageTitle }}</span>
</div>
<div class="detail-summary__item detail-summary__item--wide" v-if="detail.messageContent">
<span class="detail-summary__label">消息摘要</span>
<span class="detail-summary__value">{{ detail.messageContent }}</span>
</div>
<div class="detail-summary__item detail-summary__item--wide" v-if="detail.errorMsg">
<span class="detail-summary__label">失败原因</span>
<span class="detail-summary__value error-text">{{ detail.errorMsg }}</span>
</div>
</div>
<div class="json-toolbar">
<el-button
v-if="canRenotify(detail)"
size="mini"
type="primary"
icon="el-icon-message"
:loading="renotifyId === detail.id"
@click="handleRenotify(detail)"
v-hasPermi="['system:drawingRouteOperLog:query']"
>重新通知</el-button>
<el-button size="mini" type="primary" plain icon="el-icon-document-copy" @click="copyJson(formattedSummary)">复制摘要</el-button>
</div>
<pre class="json-block">{{ formattedSummary }}</pre>
</template>
</div>
</el-drawer>
</div>
</template>
<script>
import { listDrawingRouteOperLog, getDrawingRouteOperLog, renotifyDrawingRouteOperLog } from '@/api/system/drawingRouteOperLog'
export default {
name: 'DrawingRouteOperLog',
data() {
return {
loading: false,
showSearch: true,
total: 0,
logList: [],
dateRange: [],
queryParams: {
pageNum: 1,
pageSize: 10,
productionOrderNo: undefined,
bizType: undefined,
channel: undefined,
status: undefined
},
detailOpen: false,
detailLoading: false,
detail: null,
renotifyId: null
}
},
computed: {
pageSuccessCount() {
return this.logList.filter(r => r.status === 'SUCCESS').length
},
detailTitle() {
if (!this.detail) return '操作记录详情'
const orderNo = this.detail.productionOrderNo || ''
return orderNo ? `详情 · ${orderNo}` : '操作记录详情'
},
formattedSummary() {
return this.prettyJson(this.detail && this.detail.summaryJson)
}
},
created() {
this.getList()
},
methods: {
headerCellStyle() {
return {
background: '#f0f5f4',
color: '#1a3a36',
fontWeight: '600',
fontSize: '13px'
}
},
bizTypeLabel(type) {
const map = {
DRAWING_EXEC: '执行出图',
DRAWING_NOTIFY: '出图后推送',
ROUTE_UPLOAD: '工艺上传',
ROUTE_NOTIFY: '上传后钉钉',
// 项目管理「是否做工艺」改为否
SKIP_PROCESS_NOTIFY: '无需做工艺通知',
// 电气 BOM 导入成功
ELECTRICAL_BOM_NOTIFY: '电气BOM通知'
}
return map[type] || type || '-'
},
bizTypeTag(type) {
if (type === 'DRAWING_EXEC') return ''
if (type === 'DRAWING_NOTIFY') return 'success'
if (type === 'ROUTE_UPLOAD') return 'warning'
if (type === 'ROUTE_NOTIFY') return 'info'
if (type === 'SKIP_PROCESS_NOTIFY') return 'warning'
if (type === 'ELECTRICAL_BOM_NOTIFY') return 'success'
return ''
},
statusLabel(status) {
if (status === 'SUCCESS') return '成功'
if (status === 'FAIL') return '失败'
if (status === 'PARTIAL') return '部分成功'
if (status === 'SKIP') return '已跳过'
return status || '-'
},
statusTag(status) {
if (status === 'SUCCESS') return 'success'
if (status === 'FAIL') return 'danger'
if (status === 'PARTIAL') return 'warning'
if (status === 'SKIP') return 'info'
return 'info'
},
notifyTypeLabel(type) {
const map = {
MARKDOWN: 'Markdown',
FILE: '文件',
CARD: '互动卡片',
MARKDOWN_AND_FILE: 'Markdown + 文件',
FILE_AND_CARD: '文件 + 卡片'
}
return map[type] || type || '-'
},
formatDuration(ms) {
if (ms == null) return '-'
if (ms < 1000) return ms + ' ms'
if (ms < 60000) return (ms / 1000).toFixed(1) + ' s'
return (ms / 60000).toFixed(1) + ' min'
},
prettyJson(raw) {
if (!raw) return '—'
try {
return JSON.stringify(JSON.parse(raw), null, 2)
} catch (e) {
return raw
}
},
buildQueryParams() {
let params = { ...this.queryParams }
params = this.addDateRange(params, this.dateRange, 'OperTime')
return params
},
getList() {
this.loading = true
listDrawingRouteOperLog(this.buildQueryParams())
.then(response => {
this.logList = response.rows || []
this.total = Number(response.total) || 0
})
.finally(() => {
this.loading = false
})
},
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
resetQuery() {
this.dateRange = []
this.resetForm('queryForm')
this.handleQuery()
},
canRenotify(row) {
if (!row || row.status === 'SKIP') return false
return ['DRAWING_NOTIFY', 'ROUTE_NOTIFY', 'SKIP_PROCESS_NOTIFY', 'ELECTRICAL_BOM_NOTIFY'].includes(row.bizType)
},
handleRenotify(row) {
if (!row || row.id == null || !this.canRenotify(row)) return
const orderNo = row.productionOrderNo || '该记录'
const label = this.bizTypeLabel(row.bizType)
this.$modal.confirm('确认按「' + orderNo + '」重新发送「' + label + '」?').then(() => {
this.renotifyId = row.id
return renotifyDrawingRouteOperLog(row.id)
}).then(res => {
this.$modal.msgSuccess((res && res.msg) || '已重新通知')
this.getList()
if (this.detailOpen && this.detail && this.detail.id === row.id) {
this.handleDetail(row)
}
}).catch(() => {}).finally(() => {
this.renotifyId = null
})
},
handleDetail(row) {
if (!row || row.id == null) return
this.detailOpen = true
this.detailLoading = true
getDrawingRouteOperLog(row.id)
.then(response => {
this.detail = response.data || null
})
.finally(() => {
this.detailLoading = false
})
},
resetDetail() {
this.detail = null
},
copyJson(text) {
if (!text || text === '—') {
this.$modal.msgWarning('暂无可复制内容')
return
}
const textarea = document.createElement('textarea')
textarea.value = text
textarea.style.position = 'fixed'
textarea.style.opacity = '0'
document.body.appendChild(textarea)
textarea.select()
try {
document.execCommand('copy')
this.$modal.msgSuccess('已复制到剪贴板')
} catch (e) {
this.$modal.msgError('复制失败')
}
document.body.removeChild(textarea)
}
}
}
</script>
<style lang="scss" scoped>
.dro-page {
--plm-teal: #00b4ad;
--plm-slate: #1a3a36;
--plm-bg: #f4f8f7;
background:
linear-gradient(180deg, rgba(0, 180, 173, 0.04) 0%, transparent 280px),
var(--plm-bg);
min-height: calc(100vh - 84px);
}
.page-hero {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 24px;
margin-bottom: 18px;
padding: 22px 24px;
border-radius: 12px;
background: linear-gradient(135deg, var(--plm-slate) 0%, #24524c 55%, #2d6a62 100%);
box-shadow: 0 8px 24px rgba(26, 58, 54, 0.18);
}
.page-hero__main {
display: flex;
align-items: flex-start;
gap: 16px;
}
.page-hero__icon {
width: 48px;
height: 48px;
border-radius: 12px;
background: rgba(0, 180, 173, 0.2);
border: 1px solid rgba(0, 180, 173, 0.35);
display: flex;
align-items: center;
justify-content: center;
color: #7fe7e1;
font-size: 22px;
}
.page-hero__title {
margin: 0 0 6px;
font-size: 20px;
color: #fff;
font-weight: 600;
}
.page-hero__desc {
margin: 0;
font-size: 13px;
color: rgba(255, 255, 255, 0.72);
line-height: 1.5;
}
.page-hero__stats {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.stat-chip {
min-width: 88px;
padding: 10px 14px;
border-radius: 10px;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.12);
text-align: center;
&--accent {
background: rgba(0, 180, 173, 0.22);
border-color: rgba(0, 180, 173, 0.35);
}
}
.stat-chip__label {
display: block;
font-size: 12px;
color: rgba(255, 255, 255, 0.65);
margin-bottom: 4px;
}
.stat-chip__value {
display: block;
font-size: 20px;
font-weight: 700;
color: #fff;
}
.search-card,
.table-card {
border: 1px solid #d8e8e5;
border-radius: 10px;
margin-bottom: 14px;
}
.toolbar-row {
margin-bottom: 8px;
}
.order-no {
font-weight: 600;
color: #1a3a36;
}
.path-text {
font-family: Consolas, Monaco, monospace;
font-size: 12px;
color: #456;
word-break: break-all;
}
.error-text {
color: #c45656;
}
.detail-body {
padding: 0 20px 24px;
}
.detail-summary {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin-bottom: 16px;
padding: 14px;
border-radius: 10px;
background: #f4f8f7;
border: 1px solid #d8e8e5;
}
.detail-summary__item--wide {
grid-column: 1 / -1;
}
.detail-summary__label {
display: block;
font-size: 12px;
color: #6a8682;
margin-bottom: 4px;
}
.detail-summary__value {
font-size: 14px;
color: #1a3a36;
font-weight: 500;
}
.json-toolbar {
margin-bottom: 8px;
}
.json-block {
margin: 0;
max-height: 52vh;
overflow: auto;
padding: 12px;
border-radius: 8px;
background: #1e2a28;
color: #d7f3ef;
font-size: 12px;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
}
</style>

View File

@ -368,18 +368,15 @@ export default {
this.showInventory(row.materialCode); // 打开弹框
},
showInventory(materialCode) {
const data = {
matrilascode: materialCode
}
// 向后端发送请求获取库存数
material_inventory(data).then(response => {
this.inventoryData.materialCode = materialCode; // 设置物料编号
this.inventoryData.inventory = response; // 设置库存数
this.dialogVisible = true; // 打开弹框显示数据
})
.catch(error => {
console.error('获取库存数失败:', error);
});
material_inventory(materialCode).then(response => {
const list = (response && response.data) || [];
const item = list.find(r => r && r.materialCode === materialCode) || list[0];
this.inventoryData.materialCode = materialCode;
this.inventoryData.inventory = item ? item.kucun : 0;
this.dialogVisible = true;
}).catch(error => {
console.error('获取库存数失败:', error);
});
},
/** 查询物料检索列表 */

View File

@ -105,26 +105,26 @@
</div>
</template>
</el-table-column>
<el-table-column label="整体状态" width="92" align="center" column-key="overallStatus" :filters="statusFilters" :filter-method="filterOrderStatus">
<el-table-column label="工序状态" width="92" align="center" column-key="processStatus" :filters="statusFilters" :filter-method="filterOrderStatus">
<template slot-scope="scope">
<el-tag size="mini" effect="plain" :class="overallStatusTagClass(scope.row)">{{ overallStatusText(scope.row) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="进度" width="140" align="center">
<el-table-column label="工序进度" width="140" align="center">
<template slot-scope="scope">
<div class="progress-cell">
<el-progress
class="progress-cell__bar"
:percentage="scope.row.overallProgress"
:percentage="scope.row.processProgress || 0"
:stroke-width="8"
:show-text="false"
:color="'#409eff'"
/>
<span class="progress-text">{{ scope.row.overallProgress }}%</span>
<span class="progress-text">{{ scope.row.processProgress != null ? scope.row.processProgress : 0 }}%</span>
</div>
</template>
</el-table-column>
<el-table-column label="工序进度" min-width="440">
<el-table-column label="工序节点" min-width="440">
<template slot-scope="scope">
<div class="proc">
<template v-if="!(scope.row.processSteps || []).length">
@ -420,8 +420,7 @@ export default {
total: 0,
statusFilters: [
{ text: '未开工', value: '未开工' },
{ text: '进行中', value: '进行中' },
{ text: '已延期', value: '已延期' },
{ text: '生产中', value: '生产中' },
{ text: '已完工', value: '已完工' }
],
// 工序计划主表格数据
@ -718,10 +717,6 @@ export default {
const overallOkQty = row.overallOkQty != null && row.overallOkQty !== ''
? this.normalizeNumber(row.overallOkQty)
: this.normalizeNumber((finalProcess && finalProcess.okQty) ?? 0);
const backendOverallProgress = this.normalizePercent(row.overallProgress);
const overallProgress = backendOverallProgress != null
? Math.round(backendOverallProgress)
: (overallPlanQty > 0 ? Math.min(100, Math.round((overallOkQty / overallPlanQty) * 100)) : 0);
const processSteps = processList.map(p => {
const planQty = this.normalizeNumber(p.__effectivePlanQty != null ? p.__effectivePlanQty : (p.planQty || overallPlanQty));
@ -729,13 +724,13 @@ export default {
const inQty = this.normalizeNumber(p.__effectiveInQty != null ? p.__effectiveInQty : p.inQty);
const ngQty = this.normalizeNumber(p.ngQty);
const transOutQty = this.normalizeNumber(p.transOutQty);
const backendProgress = this.normalizePercent(p.progress);
const baseQty = p.inQty != null ? inQty : okQty;
const fallbackProgress = planQty > 0 ? (baseQty / planQty) * 100 : 0;
const percent = backendProgress != null ? backendProgress : this.normalizePercent(fallbackProgress) || 0;
// 进度 = 合格数量 / 工序数量 * 100
const percent = planQty > 0
? Math.min(100, Math.max(0, Math.round((okQty / planQty) * 100)))
: 0;
const phase = this.calcProcessStepPhase(p, planQty, okQty, inQty, ngQty, transOutQty);
const displayPercent = this.calcStepDisplayPercent(phase, planQty, okQty, inQty, ngQty, percent);
const activeLabel = this.calcStepActiveLabel(planQty, okQty, inQty, ngQty, displayPercent);
const displayPercent = phase === 'done' ? 100 : percent;
const activeLabel = percent > 0 ? `${percent}%` : '入';
return {
operNo: p.operNo,
@ -750,6 +745,8 @@ export default {
});
const lineText = this.buildLineText(processList);
const processProgress = this.resolveRowProcessProgress(processList, processSteps);
const processStatus = this.resolveStatusByProgress(processProgress);
return {
...row,
@ -757,18 +754,64 @@ export default {
processSteps,
overallPlanQty,
overallOkQty,
overallProgress,
processStatus,
processProgress,
lineText
};
},
/** 当前工序:优先取未完工的第一道,否则取最后一道 */
resolveCurrentProcess(processList) {
if (!processList || !processList.length) return null;
for (let i = 0; i < processList.length; i++) {
const p = processList[i];
const planQty = this.normalizeNumber(p.__effectivePlanQty != null ? p.__effectivePlanQty : p.planQty);
const okQty = this.normalizeNumber(p.okQty);
const inQty = this.normalizeNumber(p.__effectiveInQty != null ? p.__effectiveInQty : p.inQty);
const transOutQty = this.normalizeNumber(p.transOutQty);
const phase = this.calcProcessStepPhase(p, planQty, okQty, inQty, this.normalizeNumber(p.ngQty), transOutQty);
if (phase !== 'done') return p;
}
return processList[processList.length - 1];
},
/**
* 工序状态按进度:0 未开工;>0 生产中;100 已完工
*/
resolveStatusByProgress(progress) {
const p = this.normalizeNumber(progress);
if (p <= 0) return '未开工';
if (p >= 100) return '已完工';
return '生产中';
},
/** 工序进度:合格数量 / 工序数量 * 100(取当前工序) */
resolveRowProcessProgress(processList, processSteps) {
const current = this.resolveCurrentProcess(processList);
if (!current) return 0;
const planQty = this.normalizeNumber(
current.__effectivePlanQty != null ? current.__effectivePlanQty : current.planQty
);
const okQty = this.normalizeNumber(current.okQty);
if (planQty <= 0) return 0;
if (processSteps && processSteps.length && processSteps.every(s => s.phase === 'done')) {
return 100;
}
return Math.min(100, Math.max(0, Math.round((okQty / planQty) * 100)));
},
calcProcessStepPhase(process, planQty, okQty, inQty, ngQty, transOutQty) {
const status = this.calcProcessStatus(process);
if (status === '已转出' || status === '工序完成' || status === '已关闭') return 'done';
if (transOutQty > 0) return 'done';
if (planQty > 0 && okQty >= planQty) return 'done';
if (status === '已关闭') return 'done';
// 工序数=转入=合格=转出 才算完工
if (planQty > 0
&& this.qtyEquals(planQty, inQty)
&& this.qtyEquals(planQty, okQty)
&& this.qtyEquals(planQty, transOutQty)) {
return 'done';
}
if (inQty === 0) return 'idle';
return 'active';
},
qtyEquals(a, b) {
return Math.abs(this.normalizeNumber(a) - this.normalizeNumber(b)) <= 0.0001;
},
calcStepDisplayPercent(phase, planQty, okQty, inQty, ngQty, percent) {
if (phase === 'done') return 100;
if (phase === 'idle') return 0;
@ -833,7 +876,7 @@ export default {
},
overallStatusText(row) {
if (!row) return '-';
return row.overallStatus || '-';
return row.processStatus || '-';
},
filterOrderStatus(value, row) {
return this.overallStatusText(row) === value;
@ -841,8 +884,7 @@ export default {
overallStatusTagClass(row) {
const text = this.overallStatusText(row);
if (text === '未开工') return 'tag-gray';
if (text === '进行中') return 'tag-green';
if (text === '已延期') return 'tag-red';
if (text === '生产中') return 'tag-green';
if (text === '已完工') return 'tag-blue';
return 'tag-gray';
},
@ -1051,16 +1093,26 @@ export default {
if (!process) return '';
const rawK3 = process.k3Status ?? process.operStatus ?? process.k3OperStatus;
const k3 = rawK3 == null || rawK3 === '' ? 0 : Number(rawK3);
if (k3 === 5 || k3 === 6) return '已关闭';
const k3Code = rawK3 == null || rawK3 === '' ? '' : String(rawK3).trim();
const k3Num = Number(k3Code);
if (k3Num === 5 || k3Num === 6 || k3Code === '完工' || k3Code === '关闭' || k3Code === '已关闭') {
return '已关闭';
}
const planQty = this.normalizeNumber(process.planQty);
const inQty = this.normalizeNumber(process.__effectiveInQty != null ? process.__effectiveInQty : process.inQty);
const okQty = this.normalizeNumber(process.okQty);
const ngQty = this.normalizeNumber(process.ngQty);
const transOutQty = this.normalizeNumber(process.transOutQty);
if (inQty === 0) return '未开工';
if (transOutQty > 0) return '已转出';
// 工序数=转入=合格=转出 才算已转出完工
if (planQty > 0
&& this.qtyEquals(planQty, inQty)
&& this.qtyEquals(planQty, okQty)
&& this.qtyEquals(planQty, transOutQty)) {
return '已转出';
}
if (okQty + ngQty >= inQty) return '工序完成';
if (okQty + ngQty > 0) return '质检中';
return '生产中';

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,319 @@
<template>
<div class="designer-page app-container">
<section class="page-hero">
<div class="page-hero__main">
<div class="page-hero__icon">
<i class="el-icon-user"></i>
</div>
<div>
<h1 class="page-hero__title">设计人汇总</h1>
<p class="page-hero__desc">按设计人查看项目数和图纸工作量。点击详情进入该设计人的生产令号出图时间。</p>
</div>
</div>
</section>
<el-card shadow="never" class="search-card" v-show="showSearch">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" label-width="88px">
<el-form-item label="设计人" prop="drawingBy">
<el-select v-model="queryParams.drawingBy" clearable filterable placeholder="全部" style="width: 160px">
<el-option
v-for="d in dict.type.plan_proer || []"
:key="d.value"
:label="d.label"
:value="d.value"
/>
</el-select>
</el-form-item>
<el-form-item label="项目类型" prop="isEnterpriseStandardList">
<el-select
v-model="queryParams.isEnterpriseStandardList"
clearable
multiple
collapse-tags
placeholder="全部"
style="width: 200px"
>
<el-option label="产品 (CP)" value="产品" />
<el-option label="非标 (FB)" value="非标" />
<el-option label="研发 (EY)" value="研发" />
<el-option label="企标 (QB)" value="企标" />
<el-option label="库存 (KC)" value="库存" />
<el-option label="固资 (GZ)" value="固资" />
<el-option label="展品 (ZP)" value="展品" />
</el-select>
</el-form-item>
<el-form-item label="预计出图">
<el-date-picker
v-model="drawingTimeRange"
type="daterange"
value-format="yyyy-MM-dd"
range-separator="至"
start-placeholder="开始"
end-placeholder="结束"
style="width: 240px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-row class="toolbar-row">
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-card shadow="never" class="table-card">
<el-table
ref="designerTable"
v-loading="loading"
:data="designerList"
:fit="false"
border
stripe
size="small"
:header-cell-style="headerCellStyle"
empty-text="当前筛选下没有设计人数据"
>
<el-table-column label="设计人" :width="colWidth" align="center">
<template slot-scope="scope">
<span class="designer-name">{{ designerLabel(scope.row.drawingBy) }}</span>
</template>
</el-table-column>
<el-table-column label="项目数" :width="colWidth" align="center" prop="projectCount" />
<el-table-column label="已出图" :width="colWidth" align="center" prop="drawn" />
<el-table-column label="未逾期" :width="colWidth" align="center">
<template slot-scope="scope">
<span class="is-ontime">{{ scope.row.onTime }}</span>
</template>
</el-table-column>
<el-table-column label="出图逾期" :width="colWidth" align="center">
<template slot-scope="scope">
<span :class="{ 'is-late': Number(scope.row.delayed) > 0 }">{{ scope.row.delayed }}</span>
</template>
</el-table-column>
<el-table-column label="A4图纸合计" :width="colWidth" align="center">
<template slot-scope="scope">
<span class="workload-num">{{ formatNumber(scope.row.a4Total) }}</span>
</template>
</el-table-column>
<el-table-column label="等效A4合计" :width="colWidth" align="center">
<template slot-scope="scope">
<span class="workload-num">{{ formatNumber(scope.row.equivalentA4Total) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" :width="colWidth" align="center">
<template slot-scope="scope">
<el-button type="text" size="mini" @click="openDetail(scope.row)">详情</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
<script>
import { getOrderProProgressDesignerSummary } from '@/api/system/orderProProgress'
export default {
name: 'OrderProDesigner',
dicts: ['plan_proer'],
data() {
return {
loading: false,
showSearch: true,
colWidth: 140,
designerList: [],
drawingTimeRange: [],
queryParams: {
drawingBy: undefined,
isEnterpriseStandardList: []
}
}
},
created() {
this.getList()
},
mounted() {
this.equalizeColumns()
this._onResize = () => this.equalizeColumns()
window.addEventListener('resize', this._onResize)
},
beforeDestroy() {
window.removeEventListener('resize', this._onResize)
},
methods: {
equalizeColumns() {
this.$nextTick(() => {
const table = this.$refs.designerTable
const el = table && table.$el
if (!el || !el.clientWidth) return
const width = Math.floor(el.clientWidth / 8)
if (width > 0 && width !== this.colWidth) {
this.colWidth = width
this.$nextTick(() => {
if (table.doLayout) table.doLayout()
})
}
})
},
headerCellStyle() {
return {
background: '#f3f6fb',
color: '#1a2942',
fontWeight: '600',
fontSize: '13px'
}
},
buildQueryParams() {
const params = { ...this.queryParams }
if (!Array.isArray(params.isEnterpriseStandardList) || params.isEnterpriseStandardList.length === 0) {
delete params.isEnterpriseStandardList
}
return this.addDateRange(params, this.drawingTimeRange, 'DrawingTime')
},
getList() {
this.loading = true
getOrderProProgressDesignerSummary(this.buildQueryParams())
.then(res => {
this.designerList = (res && res.data) || []
})
.finally(() => {
this.loading = false
this.equalizeColumns()
})
},
handleQuery() {
this.getList()
},
resetQuery() {
this.drawingTimeRange = []
this.resetForm('queryForm')
this.queryParams.isEnterpriseStandardList = []
this.getList()
},
openDetail(row) {
const query = { from: 'designer' }
if (!row || !row.drawingBy || row.drawingBy === '未指定') {
query.unassignedDesigner = '1'
} else {
query.drawingBy = row.drawingBy
}
this.$router.push({
path: this.resolveMenuPath('orderProProgress', '/system/orderProProgress'),
query
})
},
resolveMenuPath(key, fallback) {
const routes = (this.$store && this.$store.getters && this.$store.getters.sidebarRouters) || []
return this.findMenuPath(routes, key) || fallback
},
findMenuPath(routes, key, parent) {
const list = Array.isArray(routes) ? routes : []
for (let i = 0; i < list.length; i++) {
const route = list[i]
if (!route) continue
const raw = route.path || ''
const current = raw.charAt(0) === '/' ? raw : [parent, raw].filter(Boolean).join('/')
const normalized = ('/' + current).replace(/\/+/g, '/')
const leaf = !route.children || !route.children.length
if (leaf && (raw === key || normalized.endsWith('/' + key))) {
return normalized
}
if (route.children && route.children.length) {
const found = this.findMenuPath(route.children, key, normalized)
if (found) return found
}
}
return ''
},
formatNumber(value) {
return value == null || value === '' ? '—' : value
},
designerLabel(value) {
if (!value || value === '未指定') return value || '未指定'
const opts = (this.dict && this.dict.type && this.dict.type.plan_proer) || []
const match = opts.find(o => String(o.value) === String(value) || String(o.dictValue || '') === String(value))
return match ? (match.label || match.dictLabel || value) : value
}
}
}
</script>
<style lang="scss" scoped>
.designer-page {
background:
linear-gradient(180deg, rgba(44, 82, 130, 0.06) 0%, transparent 220px),
#f0f2f5;
min-height: calc(100vh - 84px);
}
.page-hero {
margin-bottom: 16px;
padding: 20px 24px;
border-radius: 12px;
background: linear-gradient(135deg, #1a2942 0%, #243b5c 48%, #2c5282 100%);
box-shadow: 0 8px 24px rgba(26, 41, 66, 0.18);
}
.page-hero__main {
display: flex;
align-items: flex-start;
gap: 16px;
}
.page-hero__icon {
width: 48px;
height: 48px;
border-radius: 12px;
background: rgba(201, 162, 39, 0.18);
border: 1px solid rgba(201, 162, 39, 0.35);
display: flex;
align-items: center;
justify-content: center;
color: #f3d98a;
font-size: 22px;
}
.page-hero__title {
margin: 0 0 6px;
font-size: 20px;
color: #fff;
font-weight: 600;
}
.page-hero__desc {
margin: 0;
font-size: 13px;
color: rgba(255, 255, 255, 0.72);
line-height: 1.5;
}
.search-card,
.table-card {
border: 1px solid #dce3ec;
border-radius: 10px;
margin-bottom: 14px;
}
.toolbar-row {
margin-bottom: 8px;
}
.designer-name,
.workload-num {
font-weight: 600;
color: #1a2942;
}
.is-late {
color: #c0392b;
font-weight: 600;
}
.is-ontime {
color: #1e7e4a;
font-weight: 600;
}
</style>

View File

@ -0,0 +1,696 @@
<template>
<div class="progress-page app-container">
<section class="page-hero">
<div class="page-hero__main">
<div class="page-hero__icon">
<i class="el-icon-data-analysis"></i>
</div>
<div>
<h1 class="page-hero__title">{{ pageTitle }}</h1>
<p v-if="focusedDesignerLabel" class="page-hero__desc">设计人:{{ focusedDesignerLabel }}</p>
</div>
</div>
<div class="page-hero__stats">
<div class="stat-chip">
<span class="stat-chip__label">项目总数</span>
<span class="stat-chip__value">{{ overview.total }}</span>
</div>
<div class="stat-chip stat-chip--accent">
<span class="stat-chip__label">已出图</span>
<span class="stat-chip__value">{{ overview.drawn }}</span>
</div>
<div class="stat-chip stat-chip--warn">
<span class="stat-chip__label">出图逾期</span>
<span class="stat-chip__value">{{ overview.delayed }}</span>
</div>
<div class="stat-chip">
<span class="stat-chip__label">工艺已完成</span>
<span class="stat-chip__value">{{ overview.routeDone }}</span>
</div>
<div class="stat-chip">
<span class="stat-chip__label">BOM已更新</span>
<span class="stat-chip__value">{{ overview.bomUpdated }}</span>
</div>
<div class="stat-chip stat-chip--accent">
<span class="stat-chip__label">A4图纸</span>
<span class="stat-chip__value">{{ overview.a4Total }}</span>
</div>
<div class="stat-chip">
<span class="stat-chip__label">等效A4</span>
<span class="stat-chip__value">{{ overview.equivalentA4Total }}</span>
</div>
</div>
</section>
<el-card shadow="never" class="search-card" v-show="showSearch">
<el-form
:model="queryParams"
ref="queryForm"
size="small"
:inline="true"
label-width="88px"
class="search-form"
>
<el-form-item label="生产令号" prop="productionOrderNo">
<el-input
v-model="queryParams.productionOrderNo"
placeholder="模糊匹配生产令号"
clearable
style="width: 180px"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="项目名称" prop="productionName">
<el-input
v-model="queryParams.productionName"
placeholder="模糊匹配项目名称"
clearable
style="width: 180px"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="设计人" prop="drawingBy">
<el-select v-model="queryParams.drawingBy" clearable filterable placeholder="全部" style="width: 140px">
<el-option
v-for="d in dict.type.plan_proer || []"
:key="d.value"
:label="d.label"
:value="d.value"
/>
</el-select>
</el-form-item>
<el-form-item label="来源" prop="source">
<el-select v-model="queryParams.source" clearable placeholder="全部" style="width: 110px">
<el-option label="手动" value="手动" />
<el-option label="PLM" value="PLM" />
</el-select>
</el-form-item>
<el-form-item label="项目类型" prop="isEnterpriseStandardList">
<el-select
v-model="queryParams.isEnterpriseStandardList"
clearable
multiple
collapse-tags
placeholder="全部"
style="width: 200px"
>
<el-option label="产品 (CP)" value="产品" />
<el-option label="非标 (FB)" value="非标" />
<el-option label="研发 (EY)" value="研发" />
<el-option label="企标 (QB)" value="企标" />
<el-option label="库存 (KC)" value="库存" />
<el-option label="固资 (GZ)" value="固资" />
<el-option label="展品 (ZP)" value="展品" />
</el-select>
</el-form-item>
<el-form-item label="创建人" prop="createBy">
<el-input
v-model="queryParams.createBy"
placeholder="创建人"
clearable
style="width: 140px"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="预计出图">
<el-date-picker
v-model="drawingTimeRange"
type="daterange"
value-format="yyyy-MM-dd"
range-separator="至"
start-placeholder="开始"
end-placeholder="结束"
style="width: 240px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-row :gutter="10" class="toolbar-row">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="el-icon-user"
size="mini"
@click="goDesignerSummary"
v-if="!fromDesigner"
>设计人汇总</el-button>
</el-col>
<el-col :span="1.5" v-if="fromDesigner">
<el-button
plain
icon="el-icon-back"
size="mini"
@click="goDesignerSummary"
>返回设计人汇总</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="el-icon-download"
size="mini"
@click="handleExport"
v-hasPermi="['system:orderProProgress:export']"
>导出</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="el-icon-upload2"
size="mini"
:loading="workloadSyncLoading"
:disabled="!dataList.length"
@click="handleSyncWorkload"
>读取图纸工作量</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="refreshAll"></right-toolbar>
</el-row>
<el-card shadow="never" class="table-card">
<el-table
v-loading="loading"
:data="dataList"
border
stripe
size="small"
:header-cell-style="headerCellStyle"
:cell-style="cellStyle"
>
<el-table-column label="生产令号" min-width="150" fixed="left" show-overflow-tooltip>
<template slot-scope="scope">
<span class="order-no">{{ scope.row.productionOrderNo || '—' }}</span>
</template>
</el-table-column>
<el-table-column label="预计出图时间" width="120" align="center">
<template slot-scope="scope">
<span :class="drawingTimeClass(scope.row)">{{ formatDate(scope.row.drawingTime) }}</span>
</template>
</el-table-column>
<el-table-column label="实际出图时间" width="160" align="center">
<template slot="header">
<span title="手动项目取产品图纸最后上传时间;PLM 项目取图纸推送接收记录的接收时间。预计出图当天完成不算逾期:未逾期绿色,逾期红色">实际出图时间</span>
</template>
<template slot-scope="scope">
<span :class="drawingTimeClass(scope.row)">
{{ formatDateTime(scope.row.actualDrawingTime) }}
</span>
</template>
</el-table-column>
<el-table-column label="A4图纸数量" width="110" align="center">
<template slot="header">
<span title="图纸工作量统计表中的 A4 图纸数量">A4图纸数量</span>
</template>
<template slot-scope="scope">
<span class="workload-num">{{ formatNumber(scope.row.a4Count) }}</span>
</template>
</el-table-column>
<el-table-column label="等效A4" width="96" align="center">
<template slot="header">
<span title="图纸工作量统计表中的等效 A4 合计">等效A4</span>
</template>
<template slot-scope="scope">
<span class="workload-num">{{ formatNumber(scope.row.equivalentA4Total) }}</span>
</template>
</el-table-column>
<el-table-column label="项目名称" min-width="160" show-overflow-tooltip prop="productionName">
<template slot-scope="scope">{{ scope.row.productionName || '—' }}</template>
</el-table-column>
<el-table-column label="设计人" min-width="130" align="center">
<template slot-scope="scope">
<div v-if="getDesignerLabels(scope.row.drawingBy).length">
<el-tag
v-for="n in getDesignerLabels(scope.row.drawingBy)"
:key="n"
size="mini"
style="margin: 0 4px 2px 0"
>{{ n }}</el-tag>
</div>
<span v-else class="text-muted">—</span>
</template>
</el-table-column>
<el-table-column label="来源" width="80" align="center">
<template slot-scope="scope">
<span class="source-badge" :class="'source-badge--' + sourceKey(scope.row.source)">
{{ formatSource(scope.row.source) }}
</span>
</template>
</el-table-column>
<el-table-column label="项目类型" width="90" align="center">
<template slot-scope="scope">{{ scope.row.projectTypeLabel || '—' }}</template>
</el-table-column>
<el-table-column label="项目完成时间" width="160" align="center">
<template slot-scope="scope">{{ formatDateTime(scope.row.projectEndTime) }}</template>
</el-table-column>
<el-table-column label="工艺预计完成时间" width="160" align="center">
<template slot-scope="scope">{{ formatDateTime(scope.row.routeUptime) }}</template>
</el-table-column>
<el-table-column label="实际完成时间" width="160" align="center">
<template slot="header">
<span title="手动项目取工艺上传完成时间;PLM 项目取工艺接收记录的接收时间">实际完成时间</span>
</template>
<template slot-scope="scope">
<span :class="{ 'is-late': isLaterDate(scope.row.actualCompleteTime, scope.row.routeUptime) }">
{{ formatDateTime(scope.row.actualCompleteTime) }}
</span>
</template>
</el-table-column>
<el-table-column label="计划排产时间" width="160" align="center">
<template slot-scope="scope">{{ formatDateTime(scope.row.planUptime) }}</template>
</el-table-column>
<el-table-column label="BOM物料更新时间" width="160" align="center">
<template slot-scope="scope">{{ formatDateTime(scope.row.bomUptime) }}</template>
</el-table-column>
<el-table-column label="创建人" width="100" align="center" show-overflow-tooltip>
<template slot-scope="scope">{{ scope.row.createBy || '—' }}</template>
</el-table-column>
</el-table>
<pagination
v-show="total > 0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
</el-card>
</div>
</template>
<script>
import { listOrderProProgress, getOrderProProgressOverview, syncOrderProProgressWorkload } from '@/api/system/orderProProgress'
export default {
name: 'OrderProProgress',
dicts: ['plan_proer'],
data() {
return {
loading: false,
workloadSyncLoading: false,
showSearch: true,
fromDesigner: false,
total: 0,
dataList: [],
drawingTimeRange: [],
overview: {
total: 0,
drawn: 0,
delayed: 0,
routeDone: 0,
bomUpdated: 0,
workloadReady: 0,
a4Total: 0,
equivalentA4Total: 0
},
queryParams: {
pageNum: 1,
pageSize: 10,
productionOrderNo: undefined,
productionName: undefined,
drawingBy: undefined,
source: undefined,
createBy: undefined,
isEnterpriseStandardList: [],
unassignedDesigner: undefined
}
}
},
computed: {
pageTitle() {
return this.fromDesigner ? '生产令号出图时间' : '生产项目进度报表'
},
focusedDesignerLabel() {
if (!this.fromDesigner) return ''
if (this.queryParams.unassignedDesigner) return '未指定'
return this.designerLabel(this.queryParams.drawingBy)
}
},
created() {
this.applyDesignerRoute()
this.refreshAll()
},
watch: {
'$route.query': {
handler() {
if (!this.isProgressRoute()) return
this.applyDesignerRoute()
this.queryParams.pageNum = 1
this.refreshAll()
}
}
},
methods: {
headerCellStyle() {
return {
background: '#f3f6fb',
color: '#1a2942',
fontWeight: '600',
fontSize: '13px'
}
},
cellStyle() {
return { fontSize: '13px' }
},
buildQueryParams() {
const params = { ...this.queryParams }
if (!Array.isArray(params.isEnterpriseStandardList) || params.isEnterpriseStandardList.length === 0) {
delete params.isEnterpriseStandardList
}
if (!params.unassignedDesigner) {
delete params.unassignedDesigner
}
return this.addDateRange(params, this.drawingTimeRange, 'DrawingTime')
},
isProgressRoute() {
return (this.$route.path || '').indexOf('orderProProgress') !== -1
},
applyDesignerRoute() {
const query = (this.$route && this.$route.query) || {}
const fromDesigner = query.from === 'designer'
if (!fromDesigner) {
if (this.fromDesigner) {
this.queryParams.drawingBy = undefined
this.queryParams.unassignedDesigner = undefined
}
this.fromDesigner = false
return
}
this.fromDesigner = true
if (String(query.unassignedDesigner) === '1') {
this.queryParams.drawingBy = undefined
this.queryParams.unassignedDesigner = true
} else {
this.queryParams.drawingBy = query.drawingBy || undefined
this.queryParams.unassignedDesigner = undefined
}
},
goDesignerSummary() {
this.$router.push({ path: this.resolveMenuPath('orderProDesigner', '/system/orderProDesigner/index') })
},
resolveMenuPath(key, fallback) {
const routes = (this.$store && this.$store.getters && this.$store.getters.sidebarRouters) || []
return this.findMenuPath(routes, key) || fallback
},
findMenuPath(routes, key, parent) {
const list = Array.isArray(routes) ? routes : []
for (let i = 0; i < list.length; i++) {
const route = list[i]
if (!route) continue
const raw = route.path || ''
const current = raw.charAt(0) === '/' ? raw : [parent, raw].filter(Boolean).join('/')
const normalized = ('/' + current).replace(/\/+/g, '/')
const leaf = !route.children || !route.children.length
if (leaf && (raw === key || normalized.endsWith('/' + key))) {
return normalized
}
if (route.children && route.children.length) {
const found = this.findMenuPath(route.children, key, normalized)
if (found) return found
}
}
return ''
},
refreshAll() {
this.getOverview()
this.getList()
},
getOverview() {
getOrderProProgressOverview(this.buildQueryParams()).then(res => {
const data = (res && res.data) || {}
this.overview = {
total: Number(data.total) || 0,
drawn: Number(data.drawn) || 0,
delayed: Number(data.delayed) || 0,
routeDone: Number(data.routeDone) || 0,
bomUpdated: Number(data.bomUpdated) || 0,
workloadReady: Number(data.workloadReady) || 0,
a4Total: Number(data.a4Total) || 0,
equivalentA4Total: Number(data.equivalentA4Total) || 0
}
})
},
getList() {
this.loading = true
listOrderProProgress(this.buildQueryParams())
.then(res => {
this.dataList = res.rows || []
this.total = Number(res.total) || 0
})
.finally(() => {
this.loading = false
})
},
handleQuery() {
this.queryParams.pageNum = 1
this.refreshAll()
},
resetQuery() {
this.drawingTimeRange = []
this.resetForm('queryForm')
this.queryParams.isEnterpriseStandardList = []
this.handleQuery()
},
handleExport() {
this.download(
'system/orderProProgress/export',
this.buildQueryParams(),
`项目进度报表_${new Date().getTime()}.xlsx`
)
},
async handleSyncWorkload() {
const ids = (this.dataList || []).map(row => row.id).filter(id => id != null)
if (!ids.length) {
this.$message.warning('当前页没有可读取的项目')
return
}
this.workloadSyncLoading = true
try {
const res = await syncOrderProProgressWorkload(ids)
const payload = (res && res.data) || {}
const summary = (res && res.msg) || payload.message || '读取完成'
const details = Array.isArray(payload.failDetails) ? payload.failDetails : []
if (details.length) {
this.$alert(
details.slice(0, 20).join('<br/>') + (details.length > 20 ? '<br/>...' : ''),
summary,
{ dangerouslyUseHTMLString: true }
)
} else {
this.$message.success(summary)
}
this.refreshAll()
} catch (e) {
/* 拦截器已提示 */
} finally {
this.workloadSyncLoading = false
}
},
formatDate(value) {
return this.parseTime(value, '{y}-{m}-{d}') || '—'
},
formatDateTime(value) {
return this.parseTime(value, '{y}-{m}-{d} {h}:{i}:{s}') || '—'
},
formatNumber(value) {
return value == null || value === '' ? '—' : value
},
formatSource(value) {
return value && String(value).trim() ? String(value).trim() : '手动'
},
sourceKey(value) {
return this.formatSource(value) === 'PLM' ? 'plm' : 'manual'
},
drawingTimeClass(row) {
if (!row) return ''
if (row.drawingOverdue === true) return 'is-late'
if (row.drawingOverdue === false && row.actualDrawingTime) return 'is-ontime'
return ''
},
designerLabel(value) {
if (!value || value === '未指定') return value || '未指定'
const labels = this.getDesignerLabels(value)
return labels.length ? labels.join('、') : value
},
isLaterDate(actual, expected) {
if (!actual || !expected) return false
return new Date(actual).getTime() > new Date(expected).getTime()
},
parseDesignerArray(value) {
if (!value) return []
if (Array.isArray(value)) return value.filter(Boolean).map(x => String(x))
return String(value)
.split(',')
.map(s => s.trim())
.filter(s => s.length > 0)
},
getDesignerLabels(value) {
const opts = (this.dict && this.dict.type && this.dict.type.plan_proer) || []
return this.parseDesignerArray(value).map(c => {
const match = opts.find(o => String(o.value) === String(c) || String(o.dictValue || '') === String(c))
return match ? match.label || match.dictLabel || c : c
})
}
}
}
</script>
<style lang="scss" scoped>
.progress-page {
--ink: #1a2942;
--muted: #5c6b7f;
--accent: #c9a227;
--steel: #2c5282;
--bg: #f0f2f5;
background:
linear-gradient(180deg, rgba(44, 82, 130, 0.06) 0%, transparent 240px),
var(--bg);
min-height: calc(100vh - 84px);
}
.page-hero {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 24px;
margin-bottom: 16px;
padding: 20px 24px;
border-radius: 12px;
background: linear-gradient(135deg, #1a2942 0%, #243b5c 48%, #2c5282 100%);
box-shadow: 0 8px 24px rgba(26, 41, 66, 0.18);
}
.page-hero__main {
display: flex;
align-items: flex-start;
gap: 16px;
}
.page-hero__icon {
width: 48px;
height: 48px;
border-radius: 12px;
background: rgba(201, 162, 39, 0.18);
border: 1px solid rgba(201, 162, 39, 0.35);
display: flex;
align-items: center;
justify-content: center;
color: #f3d98a;
font-size: 22px;
}
.page-hero__title {
margin: 0 0 6px;
font-size: 20px;
color: #fff;
font-weight: 600;
}
.page-hero__desc {
margin: 0;
font-size: 13px;
color: rgba(255, 255, 255, 0.72);
line-height: 1.5;
max-width: 560px;
}
.page-hero__stats {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.stat-chip {
min-width: 88px;
padding: 10px 14px;
border-radius: 10px;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.12);
text-align: center;
&--accent {
background: rgba(201, 162, 39, 0.22);
border-color: rgba(201, 162, 39, 0.38);
}
&--warn {
background: rgba(192, 57, 43, 0.22);
border-color: rgba(192, 57, 43, 0.38);
}
}
.stat-chip__label {
display: block;
font-size: 12px;
color: rgba(255, 255, 255, 0.65);
margin-bottom: 4px;
}
.stat-chip__value {
display: block;
font-size: 20px;
font-weight: 700;
color: #fff;
}
.search-card,
.table-card {
border: 1px solid #dce3ec;
border-radius: 10px;
margin-bottom: 14px;
}
.toolbar-row {
margin-bottom: 8px;
}
.order-no {
font-weight: 600;
color: #1a2942;
}
.text-muted {
color: #8a97a8;
}
.is-late {
color: #c0392b;
font-weight: 600;
}
.is-ontime {
color: #1e7e4a;
font-weight: 600;
}
.workload-num {
font-variant-numeric: tabular-nums;
font-weight: 600;
color: #1a2942;
}
.source-badge {
display: inline-block;
min-width: 44px;
padding: 2px 8px;
border-radius: 999px;
font-size: 12px;
font-weight: 600;
&--plm {
color: #1e7e4a;
background: #edf7f1;
}
&--manual {
color: #2c5282;
background: #eef3f9;
}
}
</style>

View File

@ -60,7 +60,7 @@
<div slot="header" class="card-header">
<span class="sclh-title">生产令号: {{ item.sclh }}</span>
<div class="header-tags">
<el-tag size="small">生产总量: {{ formatNumber(item.totalMoQty) }}</el-tag>
<el-tag size="small">生产总量: {{ formatNumber(displayTotalMoQty(item)) }}</el-tag>
<el-tag type="success" size="small">生产入库: {{ formatNumber(item.totalProductionInstockQty) }}</el-tag>
</div>
</div>
@ -88,8 +88,14 @@
style="width: 100%"
class="report-table process-sticky-table"
:row-class-name="processRowClassName"
:span-method="processSpanMethod"
>
<el-table-column prop="materialNumber" label="物料编码" min-width="150" show-overflow-tooltip fixed="left" />
<el-table-column prop="materialNumber" label="物料编码" min-width="150" show-overflow-tooltip fixed="left">
<template slot-scope="scope">
<div>{{ scope.row.materialNumber || '-' }}</div>
<div v-if="scope.row.moBillNo" class="cell-sub">{{ scope.row.moBillNo }}</div>
</template>
</el-table-column>
<el-table-column prop="materialName" label="物料名称" min-width="150" show-overflow-tooltip fixed="left" />
<el-table-column prop="moInstockQty" label="入库数量" width="80" align="center">
<template slot-scope="scope">{{ formatNumber(scope.row.moInstockQty) }}</template>
@ -107,9 +113,10 @@
</template>
</el-table-column>
<el-table-column label="工序" min-width="160" show-overflow-tooltip fixed="left">
<template slot-scope="scope">{{ formatOperText(scope.row) }}</template>
<el-table-column prop="operNo" label="工序号" width="70" align="center" fixed="left">
<template slot-scope="scope">{{ scope.row.operNo == null || scope.row.operNo === '' ? '-' : scope.row.operNo }}</template>
</el-table-column>
<el-table-column prop="processName" label="工序名称" min-width="120" show-overflow-tooltip fixed="left" />
<el-table-column prop="workCenter" label="工作中心" min-width="100" show-overflow-tooltip fixed="left" />
<el-table-column label="计划数量" width="80" align="center">
<template slot-scope="scope">{{ formatNumber(scope.row.planQty) }}</template>
@ -135,9 +142,6 @@
<el-table-column label="当日不良" width="80" align="center">
<template slot-scope="scope">{{ scope.row.dayReportFailQty == null ? '-' : formatNumber(scope.row.dayReportFailQty) }}</template>
</el-table-column>
<el-table-column label="入库数量" width="80" align="center">
<template slot-scope="scope">{{ scope.row.operationInstockQty == null ? '-' : formatNumber(scope.row.operationInstockQty) }}</template>
</el-table-column>
<el-table-column label="委外(统计日前)" align="center">
<el-table-column label="发出" width="80" align="center">
@ -197,47 +201,24 @@
</el-tab-pane>
<el-tab-pane label="生产跟踪明细">
<div class="p-10">
<div class="section-title">批次跟踪(productionTraceList)</div>
<el-empty v-if="!item.productionTraceList || item.productionTraceList.length === 0" description="没有"></el-empty>
<el-table v-else :data="item.productionTraceList" border size="small" class="mt-10">
<el-table-column prop="moBillNo" label="生产订单号" min-width="140" show-overflow-tooltip />
<el-table-column prop="materialNumber" label="物料编码" min-width="150" show-overflow-tooltip />
<el-table-column prop="materialName" label="物料名称" min-width="200" show-overflow-tooltip />
<el-table-column label="下达数量" width="100" align="center">
<template slot-scope="scope">{{ formatNumber(scope.row.qty) }}</template>
</el-table-column>
<el-table-column prop="orderDate" label="下达日期" min-width="160" />
<el-table-column prop="operPlanningBillNo" label="工序计划单" min-width="140" show-overflow-tooltip />
<el-table-column prop="planStartDate" label="计划开工" min-width="160" />
<el-table-column prop="planFinishDate" label="计划完工" min-width="160" />
<el-table-column prop="instockBillNo" label="入库单号" min-width="140" show-overflow-tooltip />
<el-table-column prop="instockDate" label="入库日期" min-width="160" />
<el-table-column label="入库数量" width="100" align="center">
<template slot-scope="scope">{{ formatNumber(scope.row.instockQty) }}</template>
</el-table-column>
</el-table>
<div class="section-title mt-10">订单跟踪(orders.moTraceRows)</div>
<el-empty v-if="getMoTraceRows(item.orders).length === 0" description="没有"></el-empty>
<el-table v-else :data="getMoTraceRows(item.orders)" border size="small" class="mt-10">
<el-table-column prop="moBillNo" label="生产订单号" min-width="140" show-overflow-tooltip />
<el-table-column prop="materialNumber" label="物料编码" min-width="150" show-overflow-tooltip />
<el-table-column prop="materialName" label="物料名称" min-width="200" show-overflow-tooltip />
<el-table-column label="下达数量" width="100" align="center">
<template slot-scope="scope">{{ formatNumber(scope.row.qty) }}</template>
</el-table-column>
<el-table-column prop="orderDate" label="下达日期" min-width="160" />
<el-table-column prop="operPlanningBillNo" label="工序计划单" min-width="140" show-overflow-tooltip />
<el-table-column prop="planStartDate" label="计划开工" min-width="160" />
<el-table-column prop="planFinishDate" label="计划完工" min-width="160" />
<el-table-column prop="instockBillNo" label="入库单号" min-width="140" show-overflow-tooltip />
<el-table-column prop="instockDate" label="入库日期" min-width="160" />
<el-table-column label="入库数量" width="100" align="center">
<template slot-scope="scope">{{ formatNumber(scope.row.instockQty) }}</template>
</el-table-column>
</el-table>
</div>
<el-empty v-if="!item.productionTraceList || item.productionTraceList.length === 0" description="没有"></el-empty>
<el-table v-else :data="getTraceDisplayRows(item.productionTraceList)" border size="small" :span-method="traceSpanMethod">
<el-table-column prop="moBillNo" label="生产订单号" min-width="140" show-overflow-tooltip />
<el-table-column prop="materialNumber" label="物料编码" min-width="150" show-overflow-tooltip />
<el-table-column prop="materialName" label="物料名称" min-width="200" show-overflow-tooltip />
<el-table-column label="下达数量" width="100" align="center">
<template slot-scope="scope">{{ formatNumber(scope.row.qty) }}</template>
</el-table-column>
<el-table-column prop="orderDate" label="下达日期" min-width="160" />
<el-table-column prop="operPlanningBillNo" label="工序计划单" min-width="140" show-overflow-tooltip />
<el-table-column prop="planStartDate" label="计划开工" min-width="160" />
<el-table-column prop="planFinishDate" label="计划完工" min-width="160" />
<el-table-column prop="instockBillNo" label="入库单号" min-width="140" show-overflow-tooltip />
<el-table-column prop="instockDate" label="入库日期" min-width="160" />
<el-table-column label="入库数量" width="100" align="center">
<template slot-scope="scope">{{ formatNumber(scope.row.instockQty) }}</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="采购明细">
@ -310,7 +291,21 @@
<el-tag type="success" size="small">转入: {{ formatNumber(getSubcontractInOutSummary(item.orders).inQty) }}</el-tag>
</div>
<el-table :data="getSubcontractTransferRows(item.orders)" border size="small" class="mt-10">
<el-table-column v-for="col in getDynamicColumns(getSubcontractTransferRows(item.orders))" :key="col" :prop="col" :label="col" min-width="120" show-overflow-tooltip />
<el-table-column prop="documentDate" label="单据日期" width="120" />
<el-table-column prop="productOrderCodes" label="生产订单号" min-width="140" show-overflow-tooltip />
<el-table-column prop="productNumber" label="物料编码" min-width="150" show-overflow-tooltip />
<el-table-column prop="productName" label="物料名称" min-width="160" show-overflow-tooltip />
<el-table-column prop="shiftTaskName" label="委外作业" min-width="120" show-overflow-tooltip />
<el-table-column prop="providerName" label="供应商" min-width="160" show-overflow-tooltip />
<el-table-column label="发出数量" width="90" align="center">
<template slot-scope="scope">{{ formatNumber(scope.row.leaveQty) }}</template>
</el-table-column>
<el-table-column label="转入数量" width="90" align="center">
<template slot-scope="scope">{{ formatNumber(scope.row.enterQty) }}</template>
</el-table-column>
<el-table-column label="合格数量" width="90" align="center">
<template slot-scope="scope">{{ formatNumber(scope.row.qualifiedQty) }}</template>
</el-table-column>
</el-table>
</div>
</el-tab-pane>
@ -550,18 +545,159 @@ export default {
const wcClass = this.getWorkCenterClass(row)
if (wcClass) classes.push(wcClass)
if (this.isOutsourceRow(row)) classes.push('is-outsourcing-row')
if (row && row._orderSpan > 0) classes.push('order-start')
return classes.join(' ')
},
formatOperText(row) {
if (!row) return '-'
const no = row.operNo == null || row.operNo === '' ? '' : `${row.operNo}`
const name = row.processName || ''
if (!no && !name) return '-'
if (!no) return name
if (!name) return no
return `${no}-${name}`
processSpanMethod({ row, column }) {
const prop = column && column.property
const label = column && column.label
const mergeOrderCell = prop === 'materialNumber'
|| prop === 'materialName'
|| prop === 'moInstockQty'
|| prop === 'overallStatus'
|| label === '总进度(%)'
if (!mergeOrderCell) return [1, 1]
const span = row && row._orderSpan
if (span > 0) return [span, 1]
return [0, 0]
},
// 将嵌套的订单 -> 计划 -> 工序数据拍平展示
traceSpanMethod({ row, columnIndex }) {
if (columnIndex <= 7) {
const span = row && row._traceSpan
if (span > 0) return [span, 1]
return [0, 0]
}
return [1, 1]
},
displayTotalMoQty(item) {
const rows = (item && item.productionTraceList) || []
if (!rows.length) return item && item.totalMoQty
const seen = new Set()
let sum = 0
rows.forEach(r => {
const key = r.moBillNo || `__${r.materialNumber}|${r.orderDate}|${r.qty}`
if (seen.has(key)) return
seen.add(key)
sum += this.toNumber(r.qty)
})
return sum
},
getTraceDisplayRows(list) {
const rows = Array.isArray(list) ? list : []
const orderKeys = []
const groups = new Map()
rows.forEach((r, index) => {
const key = r.moBillNo || `__row_${index}`
if (!groups.has(key)) {
groups.set(key, [])
orderKeys.push(key)
}
groups.get(key).push(r)
})
const out = []
orderKeys.forEach(key => {
const group = groups.get(key)
group.forEach((r, i) => {
out.push({ ...r, _traceSpan: i === 0 ? group.length : 0 })
})
})
return out
},
sortProcesses(rows) {
return (rows || []).slice().sort((a, b) => {
const ao = a.operNo == null ? 0 : Number(a.operNo)
const bo = b.operNo == null ? 0 : Number(b.operNo)
if (ao !== bo) return ao - bo
return (a.k3DetailId || 0) - (b.k3DetailId || 0)
})
},
isSequenceActive(rows) {
return (rows || []).some(p => {
const status = p.status || ''
if (status && status !== '未开工') return true
const code = p.operStatus == null ? '' : `${p.operStatus}`
if (code && code !== '0' && code !== '1' && code !== '2') return true
if (this.toNumber(p.okQty) > 0 || this.toNumber(p.transOutQty) > 0) return true
return false
})
},
selectPlanProcesses(plan) {
const list = plan && Array.isArray(plan.processList) ? plan.processList : []
if (!list.length) return []
const groups = new Map()
list.forEach(p => {
const key = p.entityEntryId == null || p.entityEntryId === '' ? '_single' : `e:${p.entityEntryId}`
if (!groups.has(key)) groups.set(key, [])
groups.get(key).push(p)
})
const sequences = Array.from(groups.values())
if (sequences.length <= 1) return this.sortProcesses(sequences[0]).map(p => ({ ...p }))
const active = sequences.filter(rows => this.isSequenceActive(rows))
let chosen = active
if (!chosen.length) {
const planStart = `${plan.planStartTime || ''}`.slice(0, 10)
const matched = sequences.find(rows => `${(rows[0] && rows[0].seqPlanStartTime) || ''}`.slice(0, 10) === planStart && planStart)
chosen = [matched || sequences[0]]
}
const kept = []
const dropped = []
sequences.forEach(rows => {
if (chosen.indexOf(rows) >= 0) kept.push(...rows)
else dropped.push(...rows)
})
const keptClones = this.sortProcesses(kept).map(p => ({ ...p }))
this.copyMissingMetrics(keptClones, dropped)
this.dedupeCopiedDayMetrics(keptClones)
return keptClones
},
copyMissingMetrics(kept, dropped) {
const fields = [
'dayReportFinishQty', 'dayReportQuaQty', 'dayReportFailQty',
'subcontractLeaveQtyBeforeReportDay', 'subcontractEnterQtyBeforeReportDay', 'subcontractQualifiedQtyBeforeReportDay',
'subcontractLeaveQtyOnReportDay', 'subcontractEnterQtyOnReportDay', 'subcontractQualifiedQtyOnReportDay',
'subcontractLeaveEnterSumOnReportDay'
]
;(dropped || []).forEach(src => {
const target = kept.find(p => p.operNo === src.operNo && p.processName && p.processName === src.processName)
|| kept.find(p => p.operNo === src.operNo && this.isOutsourceRow(p) && this.isOutsourceRow(src))
if (!target) return
fields.forEach(field => {
if (target[field] == null && src[field] != null) target[field] = src[field]
})
})
},
dedupeCopiedDayMetrics(rows) {
const fields = ['dayReportFinishQty', 'dayReportQuaQty', 'dayReportFailQty']
const groups = new Map()
;(rows || []).forEach(row => {
const key = `${row.operNo}`
if (!groups.has(key)) groups.set(key, [])
groups.get(key).push(row)
})
groups.forEach(list => {
const withDay = list.filter(row => fields.some(field => row[field] != null))
if (withDay.length < 2) return
const preferred = withDay.find(row => row.status && row.status !== '未开工') || withDay[0]
withDay.forEach(row => {
if (row === preferred) return
fields.forEach(field => { row[field] = null })
})
})
},
summarizeVisibleProcesses(processes) {
if (!processes || !processes.length) return { overallProgress: null, overallStatus: '' }
const last = processes[processes.length - 1]
const statuses = processes.map(p => p.status || '')
let overallStatus = '未开工'
if (statuses.every(status => status === '已关闭')) overallStatus = '已完工'
else if (statuses.some(status => status && status !== '未开工')) overallStatus = '进行中'
return {
overallProgress: last.progress == null ? null : last.progress,
overallStatus
}
},
// 将嵌套的订单 -> 计划 -> 工序数据拍平展示。同一计划里未开工的备用序列不展示。
getFlattenedProcessData(item) {
if (!item || !item.orders || item.orders.length === 0) return [];
@ -574,30 +710,36 @@ export default {
moInstockQty: order.moInstockQty
};
const block = [];
if (order.planningList && order.planningList.length > 0) {
order.planningList.forEach(plan => {
const processes = this.selectPlanProcesses(plan)
const summary = this.summarizeVisibleProcesses(processes)
const planBase = {
...orderBase,
overallProgress: plan.overallProgress,
overallStatus: plan.overallStatus
overallProgress: summary.overallProgress,
overallStatus: summary.overallStatus
};
if (plan.processList && plan.processList.length > 0) {
plan.processList.forEach(process => {
flatData.push({
if (processes.length > 0) {
processes.forEach(process => {
block.push({
...planBase,
...process
...process,
overallProgress: summary.overallProgress,
overallStatus: summary.overallStatus
});
});
} else {
// 有计划单但没有工序
flatData.push(planBase);
block.push(planBase);
}
});
} else {
// 有订单但没有计划单
flatData.push(orderBase);
block.push(orderBase);
}
block.forEach((row, index) => {
row._orderSpan = index === 0 ? block.length : 0
flatData.push(row)
})
});
return flatData;
},
@ -810,9 +952,14 @@ export default {
return cur
}
const seenMo = new Set()
;(item.productionTraceList || []).forEach(r => {
const cur = upsert(r.materialNumber, r.materialName)
cur.moQty += this.toNumber(r.qty)
const moKey = `${r.materialNumber || '-'}|${r.moBillNo || ''}`
if (!seenMo.has(moKey)) {
seenMo.add(moKey)
cur.moQty += this.toNumber(r.qty)
}
cur.productionInstockQty += this.toNumber(r.instockQty)
})
@ -871,10 +1018,6 @@ export default {
})
return { inQty, outQty }
},
getDynamicColumns(rows) {
if (!rows || rows.length === 0) return []
return Object.keys(rows[0])
},
// 格式化数值,保留一位小数
formatNumber(num) {
if (num === null || num === undefined || num === '') return '-';
@ -1020,13 +1163,21 @@ export default {
.p-10 {
padding: 10px;
}
.section-title {
font-weight: bold;
color: #303133;
}
.summary-tags .el-tag {
margin-right: 10px;
}
.cell-sub {
margin-top: 2px;
color: #909399;
font-size: 12px;
line-height: 1.2;
}
.process-sticky-table ::v-deep .el-table__body tr.order-start > td {
border-top: 2px solid #dcdfe6;
}
.process-sticky-table ::v-deep .el-table__body tr.order-start:first-child > td {
border-top-width: 1px;
}
.kpi-card {
height: 92px;
}

View File

@ -1,13 +1,19 @@
<template>
<div class="app-container">
<el-form ref="queryForm" :model="queryParams" size="small" :inline="true" label-width="90px">
<el-form-item label="速比" prop="ratioValue">
<el-input
v-model="queryParams.ratioValue"
placeholder="请输入速比(如: 10.50)"
<el-form ref="queryForm" :model="queryParams" size="small" :inline="true" label-width="80px">
<el-form-item label="选型">
<el-cascader
v-model="cascadeValue"
class="ratio-cascader"
popper-class="ratio-cascader-popper"
:props="cascadeProps"
size="medium"
clearable
style="width: 200px"
@keyup.enter.native="handleQuery"
filterable
separator=" / "
placeholder="型号 / 减速比 / 减速机型号"
style="width: 560px"
@change="onCascadeChange"
/>
</el-form-item>
<el-form-item label="输入数量" prop="qty">
@ -22,38 +28,42 @@
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" :loading="loading" @click="handleQuery">查询</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
<el-button type="success" icon="el-icon-upload2" size="mini" @click="handleImport">导入</el-button>
</el-form-item>
</el-form>
<div v-if="hasSearched">
<el-card shadow="never" class="box-card">
<div slot="header" class="clearfix" style="display: flex; align-items: center; flex-wrap: wrap; gap: 10px;">
<span style="font-weight: bold; font-size: 15px;">速比齐套查询结果</span>
<template v-if="reportData">
<el-tag size="mini" type="info">速比: {{ reportData.ratioValue }}</el-tag>
<el-tag size="mini" type="info">最大输出扭矩: {{ reportData.maxTorqueNm }} Nm</el-tag>
<el-tag v-if="reportData.inputQty" size="mini" type="success">
输入数量: {{ reportData.inputQty }}
</el-tag>
<el-tag size="mini" type="warning" style="font-weight: bold;">
最大可齐套数量: {{ reportData.maxBuildQty || 0 }}
</el-tag>
</template>
</div>
<el-table v-loading="loading" :data="dataList" border size="small" max-height="650">
<div v-if="hasSearched" class="ratio-result">
<div class="ratio-result__title">
<span>速比齐套查询结果</span>
<template v-if="reportData">
<el-tag size="mini" type="info">机型: {{ reportData.seriesType }}</el-tag>
<el-tag size="mini" type="info">速比: {{ reportData.ratioValue }}</el-tag>
<el-tag size="mini" type="info">型号: {{ reportData.modelCode }}</el-tag>
<el-tag v-if="reportData.onlyCommonParts" size="mini">该型号无法兰、行星架、轴套,仅显示通用件</el-tag>
<el-tag size="mini" type="info">最大输出扭矩: {{ reportData.maxTorqueNm }} Nm</el-tag>
<el-tag v-if="reportData.inputQty" size="mini" type="success">
输入数量: {{ reportData.inputQty }}
</el-tag>
<el-tag size="mini" type="warning" style="font-weight: bold;">
最大可齐套数量: {{ reportData.maxBuildQty || 0 }}
</el-tag>
</template>
</div>
<el-table v-loading="loading" class="ratio-table" :data="dataList" border :row-class-name="rowClassName">
<template slot="empty">
<el-empty description="暂无数据" :image-size="80" />
</template>
<el-table-column label="配件类别" prop="partCategory" width="80" align="center" show-overflow-tooltip />
<el-table-column label="物料编码" prop="materialCode" min-width="120" show-overflow-tooltip />
<el-table-column label="物料名称" prop="materialName" min-width="150" show-overflow-tooltip />
<el-table-column label="单重" prop="danzhong" width="60" align="center" show-overflow-tooltip />
<el-table-column label="材质" prop="canzhi" width="80" align="center" show-overflow-tooltip />
<el-table-column label="需求数量" prop="requiredQty" width="80" align="right" />
<el-table-column label="物料编码" prop="materialCode" min-width="168" show-overflow-tooltip />
<el-table-column label="物料名称" prop="materialName" min-width="188" show-overflow-tooltip />
<el-table-column label="配件类别" prop="partCategory" width="96" align="center" show-overflow-tooltip />
<el-table-column label="单重" prop="danzhong" width="72" align="center" show-overflow-tooltip />
<el-table-column label="材质" prop="canzhi" width="120" align="center" show-overflow-tooltip />
<el-table-column label="单台用量" prop="bomQty" width="96" align="right" />
<el-table-column label="需求数量" prop="requiredQty" width="96" align="right" />
<el-table-column label="即时库存" prop="onHandQty" width="80" align="right" />
<el-table-column label="未领料" prop="moNotPickQty" width="70" align="right" />
<el-table-column label="可用" prop="availableQty" width="70" align="right">
<el-table-column label="即时库存" prop="onHandQty" width="96" align="right" />
<el-table-column label="未领料" prop="moNotPickQty" width="84" align="right" />
<el-table-column label="可用" prop="availableQty" width="84" align="right">
<template slot="header">
<span>可用</span>
<el-tooltip class="item" effect="dark" content="不包含不良品和废品" placement="top">
@ -66,7 +76,7 @@
</el-table-column>
<el-table-column label="在途" align="center">
<el-table-column label="采购在途" prop="poNotInQty" width="80" align="right">
<el-table-column label="采购在途" prop="poNotInQty" width="96" align="right">
<template slot-scope="scope">
<el-popover v-if="scope.row.poInTransit && scope.row.poInTransit.length > 0" trigger="hover" placement="top" width="300">
<div style="max-height: 200px; overflow-y: auto;">
@ -85,7 +95,7 @@
</template>
</el-table-column>
<el-table-column label="生产在途" prop="moNotInQty" width="80" align="right">
<el-table-column label="生产在途" prop="moNotInQty" width="96" align="right">
<template slot-scope="scope">
<el-popover v-if="scope.row.moInTransit && scope.row.moInTransit.length > 0" trigger="hover" placement="top" width="300">
<div style="max-height: 200px; overflow-y: auto;">
@ -105,31 +115,60 @@
</el-table-column>
</el-table-column>
<el-table-column label="最终可用" prop="finalAvailableQty" width="80" align="right">
<el-table-column label="最终可用" prop="finalAvailableQty" width="96" align="right">
<template slot-scope="scope">
<span style="font-weight: bold; color: #409EFF;">{{ scope.row.finalAvailableQty || 0 }}</span>
</template>
</el-table-column>
<el-table-column label="可支持数量" prop="canSupportQty" width="90" align="right" />
<el-table-column label="可支持数量" prop="canSupportQty" width="112" align="right" />
<el-table-column v-if="queryParams.qty" label="齐套补差" prop="shortageQty" width="80" align="right" fixed="right">
<el-table-column v-if="queryParams.qty" label="齐套补差" prop="shortageQty" width="96" align="right">
<template slot-scope="scope">
<span :style="{ color: (scope.row.shortageQty || 0) > 0 ? '#f56c6c' : '#67c23a', fontWeight: 'bold' }">
{{ scope.row.shortageQty || 0 }}
</span>
</template>
</el-table-column>
</el-table>
</el-card>
</el-table>
</div>
<el-empty v-else description="请输入速比并点击查询获取数据" />
<el-empty v-else description="请选择型号、减速比和减速机型号后查询" />
<el-dialog title="导入速比物料" :visible.sync="upload.open" width="460px" append-to-body @close="resetUpload">
<el-form size="small" label-width="70px">
<el-form-item label="机型">
<el-radio-group v-model="upload.seriesType">
<el-radio label="KP08">KP08</el-radio>
<el-radio label="KP14">KP14</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<el-upload
ref="upload"
:limit="1"
accept=".xlsx,.xls"
:http-request="doImport"
:disabled="upload.isUploading"
:auto-upload="false"
drag
>
<i class="el-icon-upload" />
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
<div slot="tip" class="el-upload__tip">
先选择 KP08 或 KP14。文件里的每个 Sheet 都会导入,Sheet 名是速比值,例如 91B。导入只覆盖该机型下这些速比原有的型号和物料。
</div>
</el-upload>
<div slot="footer" class="dialog-footer">
<el-button type="primary" :loading="upload.isUploading" @click="submitUpload">确 定</el-button>
<el-button @click="upload.open = false">取 消</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { getRatioBuildCapacity } from '@/api/system/ratio'
import { getRatioBuildCapacity, importRatioBom, listRatioModels, listRatioValues } from '@/api/system/ratio'
export default {
name: 'RatioBuildCapacity',
@ -137,18 +176,122 @@ export default {
return {
loading: false,
hasSearched: false,
cascadeValue: [],
cascadeProps: {
lazy: true,
lazyLoad: (node, resolve) => {}
},
queryParams: {
seriesType: undefined,
ratioValue: undefined,
modelId: undefined,
qty: undefined
},
reportData: null,
dataList: []
dataList: [],
upload: {
open: false,
isUploading: false,
seriesType: 'KP08'
}
}
},
created() {
const vm = this
this.cascadeProps = {
lazy: true,
lazyLoad(node, resolve) {
vm.loadCascadeNode(node, resolve)
}
}
},
methods: {
loadCascadeNode(node, resolve) {
const level = node.level
if (level === 0) {
resolve([
{ value: 'KP08', label: 'KP08', leaf: false },
{ value: 'KP14', label: 'KP14', leaf: false }
])
return
}
if (level === 1) {
listRatioValues(node.value).then(res => {
const ratios = res.data || []
if (!ratios.length) {
this.$modal.msgWarning('该型号下没有减速比')
}
resolve(ratios.map(item => ({ value: item, label: item, leaf: false })))
}).catch(() => resolve([]))
return
}
const seriesType = node.parent && node.parent.value
listRatioModels({ seriesType: seriesType, ratioValue: node.value }).then(res => {
const models = res.data || []
if (!models.length) {
this.$modal.msgWarning('该减速比下没有减速机型号')
}
resolve(models.map(item => ({ value: item.id, label: item.modelCode, leaf: true })))
}).catch(() => resolve([]))
},
onCascadeChange(value) {
const path = value || []
this.queryParams.seriesType = path[0]
this.queryParams.ratioValue = path[1]
this.queryParams.modelId = path[2]
this.clearResult()
},
clearResult() {
this.reportData = null
this.dataList = []
this.hasSearched = false
},
rowClassName({ row }) {
const category = row.partCategory || ''
if (category.indexOf('轴套') >= 0) {
return 'row-sleeve'
}
if (category.indexOf('行星架') >= 0 || category.indexOf('行程架') >= 0) {
return 'row-carrier'
}
if (category.indexOf('法兰') >= 0) {
return 'row-flange'
}
if (category.indexOf('通用') >= 0) {
return 'row-common'
}
return ''
},
sortItems(items) {
const rank = (category) => {
if (!category) {
return 9
}
if (category.indexOf('轴套') >= 0) {
return 0
}
if (category.indexOf('行星架') >= 0 || category.indexOf('行程架') >= 0) {
return 1
}
if (category.indexOf('法兰') >= 0) {
return 2
}
if (category === '通用件') {
return 3
}
return 9
}
return items.slice().sort((a, b) => {
const diff = rank(a.partCategory) - rank(b.partCategory)
if (diff !== 0) {
return diff
}
return (a.materialCode || '').localeCompare(b.materialCode || '')
})
},
handleQuery() {
if (!this.queryParams.ratioValue) {
this.$modal.msgWarning('请输入速比')
if (!this.queryParams.seriesType || !this.queryParams.ratioValue || !this.queryParams.modelId) {
this.$modal.msgWarning('请选择型号、减速比和减速机型号')
return
}
this.hasSearched = true
@ -156,7 +299,7 @@ export default {
getRatioBuildCapacity(this.queryParams).then(res => {
if (res.data) {
this.reportData = res.data
this.dataList = res.data.items || []
this.dataList = this.sortItems(res.data.items || [])
} else {
this.reportData = null
this.dataList = []
@ -169,17 +312,157 @@ export default {
})
},
resetQuery() {
this.resetForm('queryForm')
this.reportData = null
this.dataList = []
this.hasSearched = false
this.cascadeValue = []
this.queryParams.seriesType = undefined
this.queryParams.ratioValue = undefined
this.queryParams.modelId = undefined
this.queryParams.qty = undefined
this.clearResult()
},
handleImport() {
this.upload.open = true
},
resetUpload() {
this.upload.isUploading = false
if (this.$refs.upload) {
this.$refs.upload.clearFiles()
}
},
submitUpload() {
if (!this.upload.seriesType) {
this.$modal.msgWarning('请选择机型 KP08 或 KP14')
return
}
this.$refs.upload.submit()
},
doImport(option) {
const formData = new FormData()
formData.append('file', option.file)
formData.append('seriesType', this.upload.seriesType)
this.upload.isUploading = true
importRatioBom(formData).then(res => {
this.upload.open = false
this.$alert(res.msg || '导入完成', '导入结果', { dangerouslyUseHTMLString: false })
option.onSuccess(res)
}).catch(err => {
option.onError(err)
}).finally(() => {
this.upload.isUploading = false
})
}
}
}
</script>
<style scoped>
.box-card {
margin-top: 15px;
.ratio-result {
margin-top: 8px;
}
.ratio-result__title {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 10px;
font-size: 15px;
font-weight: bold;
color: #303133;
}
.ratio-table {
font-size: 15px;
font-weight: bold;
color: #303133;
}
.ratio-table >>> th,
.ratio-table >>> td,
.ratio-table >>> .cell {
font-size: 15px;
font-weight: bold;
color: #303133;
white-space: nowrap;
line-height: 22px;
}
.ratio-table >>> .el-table__body tr.row-sleeve > td {
background: #e8f3ff;
}
.ratio-table >>> .el-table__body tr.row-carrier > td {
background: #fff3e0;
}
.ratio-table >>> .el-table__body tr.row-flange > td {
background: #e8f8ef;
}
.ratio-table >>> .el-table__body tr.row-common > td {
background: #f4f5f7;
}
.ratio-table >>> .el-table__body tr.row-sleeve:hover > td {
background: #d4e8ff;
}
.ratio-table >>> .el-table__body tr.row-carrier:hover > td {
background: #ffe4c2;
}
.ratio-table >>> .el-table__body tr.row-flange:hover > td {
background: #d4f0df;
}
.ratio-table >>> .el-table__body tr.row-common:hover > td {
background: #e6e8eb;
}
.ratio-cascader >>> .el-input__inner {
height: 40px;
line-height: 40px;
font-size: 15px;
font-weight: bold;
}
</style>
<style>
.ratio-cascader-popper .el-cascader-menu {
position: relative;
box-sizing: border-box;
min-width: 180px;
height: 340px;
padding-top: 42px;
overflow: hidden;
}
.ratio-cascader-popper .el-cascader-menu:nth-child(3) {
min-width: 320px;
}
.ratio-cascader-popper .el-cascader-menu__wrap {
height: 298px;
}
.ratio-cascader-popper .el-cascader-menu::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: 2;
height: 42px;
line-height: 42px;
padding: 0 20px;
box-sizing: border-box;
font-size: 15px;
font-weight: bold;
color: #303133;
background: #f5f7fa;
border-bottom: 1px solid #e4e7ed;
}
.ratio-cascader-popper .el-cascader-menu:nth-child(1)::before {
content: '型号';
}
.ratio-cascader-popper .el-cascader-menu:nth-child(2)::before {
content: '速比';
}
.ratio-cascader-popper .el-cascader-menu:nth-child(3)::before {
content: '减速机型号';
}
.ratio-cascader-popper .el-cascader-node {
height: 40px;
line-height: 40px;
font-size: 15px;
font-weight: bold;
}
.ratio-cascader-popper .el-cascader-node__label {
font-size: 15px;
font-weight: bold;
}
</style>

View File

@ -19,63 +19,57 @@
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<div class="route-toolbar">
<div class="route-toolbar__left">
<el-button type="primary" plain icon="el-icon-download" size="mini" @click="handleExport"
v-hasPermi="['system:route:export']">导出
</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="el-icon-upload" size="mini" @click="handleImport"
:disabled="uploadRouteStatusLocked" :title="uploadRouteStatusLockMsg || ''"
v-hasPermi="['system:route:importRoute']">导入工艺
</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="el-icon-time" size="mini" @click="handleImportTime"
v-hasPermi="['system:route:importDataTime']">导入时间
</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="info" plain icon="el-icon-share" size="mini" @click="pushRouteBom"
:disabled="jsyyBlocked || pushRouteStatusLocked"
:title="(jsyyBlocked ? jsyyDenyMsg : '') || pushRouteStatusLockMsg || ''"
v-hasPermi="['system:route:pushRouteBom']">推送工艺
</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="el-icon-share" size="mini" @click="pushSelectedMaterialRoute"
:disabled="jsyyBlocked || pushRouteStatusLocked || !ids.length"
:title="(jsyyBlocked ? jsyyDenyMsg : '') || pushRouteStatusLockMsg || (!ids.length ? '请先勾选物料' : '')"
v-hasPermi="['system:route:pushRouteBom']">推送单个工艺
</el-button>
<el-button type="primary" plain icon="el-icon-check" size="mini" @click="approveRoute"
v-hasPermi="['system:route:approveRoute']">审核通过
</el-button>
</el-col>
<el-col :span="3">
<el-input v-model="routeGroupName" placeholder="请输入推送分组" size="mini" clearable />
</el-col>
<el-col :span="1.5">
<el-input
class="route-toolbar__group"
v-model="routeGroupName"
placeholder="请输入推送分组"
size="mini"
clearable
/>
<el-button type="danger" plain icon="el-icon-edit" size="mini" :disabled="multiple" @click="updateProcessPlan"
v-hasPermi="['system:route:updateProcessPlan']">更新计划时间
</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="el-icon-setting" size="mini" @click="updateProcesTime"
v-hasPermi="['system:route:updateProcesTime']">更新生产订单
</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="primary" plain icon="el-icon-document" size="mini" @click="generatePDFs1"
:loading="generatingAllPdf"
v-hasPermi="['system:route:generatePDFs']">生成PDF
</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="el-icon-warning" size="mini" @click="getKindeeExcel"
v-hasPermi="['system:route:getKindeeExcel']">获取标准工艺
</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="el-icon-warning" size="mini" @click="getAllRouteAndUse"
v-hasPermi="['system:route:getAllRouteAndUse']">获取全部工艺
</el-button>
</el-col>
</div>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</div>
<el-form :inline="true" :model="form" ref="ruleForm" size="mini" :rules="rules">
<el-table v-if="showtable" v-loading="loading" row-key="id" ref="tree" v-drag:[config]="form.routeList"
@ -99,7 +93,7 @@
scope.row.routeDescription }}</router-link>
</template>
</el-table-column> -->
<el-table-column label="物料编码" width="120" align="center" prop="materialCode" fixed="left" />
<el-table-column label="物料编码" width="140" align="center" prop="materialCode" fixed="left" />
<el-table-column label="名称" width="120" align="center" prop="materialName" fixed="left" />
<el-table-column label="材质" width="80" align="center" prop="material" />
@ -224,12 +218,44 @@
</template>
</el-table-column>
<el-table-column class="maxWidth" label="操作" width="220" fixed="right" align="center"
<el-table-column class="maxWidth" label="操作" width="250" fixed="right" align="center"
class-name="small-padding fixed-width">
<template slot-scope="scope">
<div class="route-row-ops">
<el-button class="btn" size="mini" @click="onAddChild(scope)" v-if="!scope.row.parentId">
添加工艺
</el-button>
<el-dropdown
v-if="rowMoreVisible(scope.row)"
trigger="click"
@command="command => handleRowMore(scope.row, command)"
>
<el-button class="btn" size="mini">
更多<i class="el-icon-arrow-down el-icon--right"></i>
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item
v-hasPermi="['system:route:pushRouteBom']"
command="push"
:disabled="jsyyBlocked || pushRouteStatusLocked || pushingMaterialCode === scope.row.materialCode"
>{{ pushingMaterialCode === scope.row.materialCode ? '推送中…' : '推送该物料工艺' }}</el-dropdown-item>
<el-dropdown-item
v-hasPermi="['system:route:generatePDFs']"
command="pdf"
:disabled="downloadingPdfCode === scope.row.materialCode"
>{{ downloadingPdfCode === scope.row.materialCode ? '下载中…' : '下载PDF' }}</el-dropdown-item>
<template v-if="isPlmProject">
<el-dropdown-item
v-for="(fmt, index) in drawingModelFormats"
:key="fmt.type"
v-hasPermi="['system:route:downloadDrawingModel']"
:divided="index === 0"
:command="'model:' + fmt.type"
:disabled="downloadingDrawingCode === scope.row.materialCode"
>{{ fmt.label }}</el-dropdown-item>
</template>
</el-dropdown-menu>
</el-dropdown>
<el-button class="btn" size="mini" type="success" @click="onSave(scope)"
v-if="!scope.row.parentId && showParentSaveBtn === scope.row.id">
保存工艺
@ -261,6 +287,7 @@
</template>
</template>
</template>
</div>
</template>
</el-table-column>
</el-table>
@ -417,7 +444,7 @@
<!-- 导入导入对话框 -->
<el-dialog :title="upload.title" :visible.sync="upload.open" width="400px" append-to-body>
<el-upload ref="upload" :limit="1" accept=".xlsx, .xls" :headers="upload.headers"
:action="upload.url + '?updateSupport=' + upload.updateSupport" :disabled="upload.isUploading"
:action="importUploadAction" :disabled="upload.isUploading"
:on-progress="handleFileUploadProgress" :on-success="handleFileSuccess" :auto-upload="false" drag>
<i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
@ -628,17 +655,25 @@ import {
addRoute,
updateRoute,
getDistinctProjectCodes,
updateProcessPlan, generatePDFs, updateBom, addBom, deleteBom, getKindeeExcel, getAllRouteAndUse, updateProcesTime
updateProcessPlan, generatePDFs, downloadDrawingModel as requestDrawingModel, updateBom, addBom, deleteBom, getKindeeExcel, getAllRouteAndUse, updateProcesTime
} from "@/api/system/route";
import { getToken } from "../../../utils/auth";
import Fromed from './fromed'
import Bomfrom from './bomfrom'
import { getProcessInfoList, pushRouteBom, approveRoute, onSave, getBomInfo, getProcessRouteList } from "@/api/system/route";
import { getProcessInfoList, pushRouteBom, pushRouteBomByMaterial, approveRoute, onSave, getBomInfo, getProcessRouteList } from "@/api/system/route";
import { listMaterial } from "@/api/system/material";
import { blobValidate } from "@/utils/ruoyi";
import { checkPermi } from "@/utils/permission";
import dict from "@/utils/dict";
import DictData from "@/components/DictData";
import { isJsyyOrderNo, JSYY_DENY_MSG } from "@/utils/jsyyOrderNo";
import {
ProjectStatusAction,
denyIfProjectStatusLocked,
} from "@/utils/projectStatusLock";
import { listOrderPro } from "@/api/system/orderPro";
DictData.install();
export default {
@ -698,12 +733,33 @@ export default {
//expandRowKeys:[],
omdelData: '',
routeDescription: '', // 用于存储 productionOrderNo 的变量
/** 当前令号对应项目状态,用于状态锁 */
projectBomStatus: undefined,
/** 当前令号来源:手动 / PLM */
projectSource: undefined,
/** 正在下载图纸模型的物料编码 */
downloadingDrawingCode: "",
// 按钮loading
buttonLoading: false,
// 遮罩层
loading: true,
// 选中数组
ids: [],
selectedRows: [],
/** 正在下载 PDF 的物料编码,用于行内按钮 loading */
downloadingPdfCode: "",
/** 图纸模型文件服务支持的格式,propertyValue 传物料编码(图号) */
drawingModelFormats: [
{ type: "sldprt", label: "零件模型 sldprt" },
{ type: "sldasm", label: "装配模型 sldasm" },
{ type: "dwg", label: "DWG" },
{ type: "pdf", label: "PDF" },
{ type: "x_t", label: "x_t" }
],
/** 正在按物料推送工艺的物料编码 */
pushingMaterialCode: "",
/** 工具栏「生成PDF」整包 zip 的 loading */
generatingAllPdf: false,
// 非单禁用
single: true,
// 非多个禁用
@ -950,17 +1006,6 @@ export default {
// }
this.getList();
},
watch: {
'$route.query.productionOrderNo': function (newVal) {
// 当路由参数 productionOrderNo 变化时,重新获取
this.setTotalWeight();
this.getList();
// 设置查询参数中的 routeDescription
if (newVal) {
this.queryParams.routeDescription = newVal;
}
}
},
mounted() {
this.rowDrop();
},
@ -973,9 +1018,112 @@ export default {
String(value).toLowerCase().includes(searchKey)
);
});
}
},
jsyyBlocked() {
const orderNo = (this.params && this.params.productionOrderNo) || this.queryParams.routeDescription;
return isJsyyOrderNo(orderNo);
},
/** 当前要导入的项目令号:抽屉传入优先,否则用页面所选令号 */
currentImportOrderNo() {
const fromParams = this.params && this.params.productionOrderNo;
const fromQuery = this.queryParams && this.queryParams.routeDescription;
return String(fromParams || fromQuery || "").trim();
},
/** 上传地址带上当前令号,后端据此与 Excel 内令号核对 */
importUploadAction() {
let url = this.upload.url + "?updateSupport=" + this.upload.updateSupport;
const orderNo = this.currentImportOrderNo;
if (orderNo) {
url += "&productionOrderNo=" + encodeURIComponent(orderNo);
}
return url;
},
jsyyDenyMsg() {
return JSYY_DENY_MSG;
},
resolvedBomStatus() {
if (this.params && this.params.bomStatus !== undefined && this.params.bomStatus !== null && this.params.bomStatus !== "") {
return this.params.bomStatus;
}
return this.projectBomStatus;
},
uploadRouteStatusLockMsg() {
return denyIfProjectStatusLocked(this.resolvedBomStatus, ProjectStatusAction.UPLOAD_ROUTE);
},
uploadRouteStatusLocked() {
return !!this.uploadRouteStatusLockMsg;
},
pushRouteStatusLockMsg() {
return denyIfProjectStatusLocked(this.resolvedBomStatus, ProjectStatusAction.PUSH_ROUTE);
},
pushRouteStatusLocked() {
return !!this.pushRouteStatusLockMsg;
},
isPlmProject() {
const source = (this.params && this.params.source) || this.projectSource;
return String(source || "").trim().toUpperCase() === "PLM";
},
},
watch: {
'$route.query.productionOrderNo': function (newVal) {
this.setTotalWeight();
this.getList();
if (newVal) {
this.queryParams.routeDescription = newVal;
}
},
"params.productionOrderNo": {
immediate: true,
handler(val) {
if (val) {
this.refreshProjectBomStatus(val);
}
},
},
"queryParams.routeDescription"(val) {
if (val) {
this.refreshProjectBomStatus(val);
}
},
},
methods: {
async refreshProjectBomStatus(orderNo) {
if (!orderNo) {
this.projectBomStatus = undefined;
this.projectSource = undefined;
return;
}
const hasParamStatus = this.params
&& this.params.bomStatus !== undefined
&& this.params.bomStatus !== null
&& this.params.bomStatus !== "";
if (hasParamStatus) {
this.projectBomStatus = this.params.bomStatus;
}
if (this.params && this.params.source) {
this.projectSource = this.params.source;
}
if (hasParamStatus && this.projectSource) {
return;
}
try {
const res = await listOrderPro({ productionOrderNo: orderNo, pageNum: 1, pageSize: 1 });
const row = (res.rows && res.rows[0]) || null;
if (!hasParamStatus) {
this.projectBomStatus = row ? row.bomStatus : undefined;
}
if (!(this.params && this.params.source)) {
this.projectSource = row ? row.source : undefined;
}
} catch (e) {
if (!hasParamStatus) {
this.projectBomStatus = undefined;
}
if (!(this.params && this.params.source)) {
this.projectSource = undefined;
}
}
},
// dict,
getKindeeExcel() {
// 显示加载状态
@ -1318,7 +1466,16 @@ export default {
},
/** 导入钮操作 */
handleImport() {
this.upload.title = "工艺导入";
if (this.uploadRouteStatusLocked) {
this.$modal.msgError(this.uploadRouteStatusLockMsg);
return;
}
// 未选令号则无法与文件核对,先拦住
if (!this.currentImportOrderNo) {
this.$modal.msgWarning("请先选择项目令号,再导入工艺");
return;
}
this.upload.title = "工艺导入(当前令号:" + this.currentImportOrderNo + ")";
this.upload.open = true;
},
/** 下载模板操作 */
@ -1334,12 +1491,20 @@ export default {
this.upload.open = false;
this.upload.isUploading = false;
this.$refs.upload.clearFiles();
const msg = (response && response.msg) || "导入失败";
const ok = response && (response.code === 200 || response.code === 0);
this.$alert("<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>"
+ response.msg + "</div>", "导入结果", { dangerouslyUseHTMLString: true });
this.getList();
+ msg + "</div>", ok ? "导入结果" : "导入失败", { dangerouslyUseHTMLString: true });
if (ok) {
this.getList();
}
},
// 提交上传文件
submitFileForm() {
if (!this.currentImportOrderNo) {
this.$modal.msgWarning("请先选择项目令号,再导入工艺");
return;
}
this.$refs.upload.submit();
},
// 取消按钮
@ -1381,6 +1546,7 @@ export default {
// 多选框选中数据
handleSelectionChange(selection) {
this.ids = selection.map(item => item.id)
this.selectedRows = selection || []
this.single = selection.length !== 1
this.multiple = !selection.length
},
@ -1416,13 +1582,21 @@ export default {
},
/** 推送工艺 / 审核并推送 共用逻辑 */
async executeRoutePush({ confirmMessage, loadingText, apiFn, errorTitle, successTitle }) {
/** 推送工艺 / 按物料推送 共用逻辑 */
async executeRoutePush({ confirmMessage, loadingText, apiFn, errorTitle, successTitle, materialCodes }) {
const productionOrderNo = this.params.productionOrderNo;
if (!productionOrderNo) {
this.$modal.msgError("未获取到生产订单号,请检查!");
return;
}
if (isJsyyOrderNo(productionOrderNo)) {
this.$modal.msgError(this.jsyyDenyMsg);
return;
}
if (this.pushRouteStatusLocked) {
this.$modal.msgError(this.pushRouteStatusLockMsg);
return;
}
if (!this.routeGroupName || !this.routeGroupName.trim()) {
this.$modal.msgError("请填写分组名称!");
return;
@ -1448,10 +1622,13 @@ export default {
}, timeout);
});
const response = await Promise.race([
apiFn(productionOrderNo, this.routeGroupName.trim()),
apiFn(productionOrderNo, this.routeGroupName.trim(), materialCodes),
timeoutPromise
]);
const { duplicateRoutes, failedRoutes, successfulRoutes } = response.data;
const result = (response && response.data) || {};
const duplicateRoutes = result.duplicateRoutes || [];
const failedRoutes = result.failedRoutes || [];
const successfulRoutes = result.successfulRoutes || [];
const duplicateCodes = duplicateRoutes.join(', ');
const failedInfo = failedRoutes.map(route =>
`${route.materialCode} (${route.errorMessage || '未知错误'})`
@ -1470,6 +1647,9 @@ export default {
});
await this.getList();
} catch (error) {
if (error === 'cancel') {
return;
}
console.error(`[${errorTitle}]:`, error);
this.$notify({
title: errorTitle,
@ -1486,6 +1666,25 @@ export default {
}
},
collectSelectedMaterialCodes(rows) {
const codes = [];
const seen = new Set();
const source = rows || this.selectedRows || [];
source.forEach(row => {
const code = row && row.materialCode ? String(row.materialCode).trim() : "";
if (!code) {
return;
}
const key = code.toUpperCase();
if (seen.has(key)) {
return;
}
seen.add(key);
codes.push(code);
});
return codes;
},
/**推送工艺 */
pushRouteBom() {
return this.executeRoutePush({
@ -1496,6 +1695,43 @@ export default {
});
},
/** 勾选物料后单独推送金蝶 */
pushSelectedMaterialRoute() {
const codes = this.collectSelectedMaterialCodes();
if (!codes.length) {
this.$modal.msgError("请先勾选要推送的物料");
return;
}
return this.executeRoutePush({
confirmMessage: `将只推送所选物料(${codes.join('、')})的工艺到金蝶,不会整单推送、也不会改项目状态。是否继续?`,
loadingText: '正在推送所选物料工艺,请稍候...',
apiFn: pushRouteBomByMaterial,
errorTitle: '推送选中物料失败',
successTitle: '物料推送完成',
materialCodes: codes
});
},
/** 物料行单独推送 */
pushMaterialRoute(row) {
const code = row && row.materialCode ? String(row.materialCode).trim() : "";
if (!code) {
this.$modal.msgError("该行没有物料编码");
return;
}
this.pushingMaterialCode = code;
return this.executeRoutePush({
confirmMessage: `将只推送物料 ${code} 的工艺到金蝶,不会整单推送、也不会改项目状态。是否继续?`,
loadingText: `正在推送物料 ${code} 的工艺,请稍候...`,
apiFn: pushRouteBomByMaterial,
errorTitle: '推送该物料工艺失败',
successTitle: '物料推送完成',
materialCodes: [code]
}).finally(() => {
this.pushingMaterialCode = "";
});
},
/** 审核通过:更新状态并发送钉钉通知,不推送金蝶、不发送工艺计划表 */
async approveRoute() {
const productionOrderNo = this.params.productionOrderNo;
@ -1597,39 +1833,166 @@ export default {
this.loading = false;
});
},
generatePDFs1(row) {
// 显示加载状态
this.loading = true;
// 调用 reset 方法,重置表单或状态
this.reset();
// 获取生产订单号
const rooteProdet = this.params.productionOrderNo;
console.log(rooteProdet);
// 调用封装好的 API 方法
/**
* 工具栏「生成PDF」:当前令号下全部工序计划单打 zip。
*/
generatePDFs1() {
const rooteProdet = (this.params && this.params.productionOrderNo)
|| this.queryParams.routeDescription
|| this.$route.query.productionOrderNo;
if (!rooteProdet) {
this.$modal.msgWarning("请先选择项目令号");
return;
}
this.generatingAllPdf = true;
const loading = this.$loading({
lock: true,
text: "正在生成全部工序计划单PDF,请稍候...",
spinner: "el-icon-loading",
background: "rgba(0, 0, 0, 0.7)"
});
generatePDFs(rooteProdet)
.then(response => {
let blob = new Blob([response], { type: 'application/zip' })
let url = window.URL.createObjectURL(blob)
const link = document.createElement('a') // 创建a标签
link.href = url
link.download = rooteProdet + '.zip' // 重命名文件
link.click()
URL.revokeObjectURL(url) // 释放内存
// 弹出成功消息
this.$modal.msgSuccess("生成" + rooteProdet + "的PDF 成功 ");
// 调用方法更新列表
this.getList();
.then(async (response) => {
const isBlob = blobValidate(response);
if (!isBlob) {
const resText = await response.text();
const rspObj = JSON.parse(resText);
this.$modal.msgError((rspObj && rspObj.msg) || "生成PDF失败");
return;
}
const blob = new Blob([response], { type: "application/zip" });
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = rooteProdet + "_工序计划单.zip";
link.click();
URL.revokeObjectURL(url);
this.$modal.msgSuccess("生成" + rooteProdet + "的PDF成功");
})
.catch(error => {
// 处理错误,弹出错误提示
.catch((error) => {
this.$modal.msgError("生成PDF失败,请重试!");
console.error('生成PDF失败:', error);
console.error("生成PDF失败:", error);
})
.finally(() => {
// 隐藏加载状态
this.loading = false;
this.generatingAllPdf = false;
loading.close();
});
},
/**
* 物料行「下载PDF」:用当前令号 + 该行物料编码查金蝶生产订单,直接下单份 PDF。
*/
downloadMaterialPdf(row) {
const materialCode = row && row.materialCode ? String(row.materialCode).trim() : "";
const rooteProdet = (this.params && this.params.productionOrderNo)
|| this.queryParams.routeDescription
|| this.$route.query.productionOrderNo;
if (!rooteProdet) {
this.$modal.msgWarning("请先选择项目令号");
return;
}
if (!materialCode) {
this.$modal.msgWarning("该行没有物料编码");
return;
}
this.downloadingPdfCode = materialCode;
generatePDFs(rooteProdet, materialCode)
.then(async (response) => {
const isBlob = blobValidate(response);
if (!isBlob) {
const resText = await response.text();
const rspObj = JSON.parse(resText);
this.$modal.msgError((rspObj && rspObj.msg) || "下载PDF失败");
return;
}
const filename = `${rooteProdet}_${materialCode}_工序计划单.pdf`;
const blob = new Blob([response], { type: "application/pdf" });
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = filename;
link.click();
URL.revokeObjectURL(url);
this.$modal.msgSuccess(`已下载物料 ${materialCode} 的工序计划单`);
})
.catch((error) => {
this.$modal.msgError("下载PDF失败,请重试!");
console.error("下载PDF失败:", error);
})
.finally(() => {
this.downloadingPdfCode = "";
});
},
rowMoreVisible(row) {
if (!row || row.parentId || !row.materialCode) return false
return checkPermi(['system:route:pushRouteBom'])
|| checkPermi(['system:route:generatePDFs'])
|| checkPermi(['system:route:downloadDrawingModel'])
},
handleRowMore(row, command) {
if (!command) return
if (command === 'push') {
this.pushMaterialRoute(row)
return
}
if (command === 'pdf') {
this.downloadMaterialPdf(row)
return
}
if (String(command).indexOf('model:') === 0) {
this.downloadDrawingModel(row, String(command).slice(6))
}
},
/**
* 物料行「下载图纸模型」:仅 PLM 项目。前端请求本系统,由后端转发文件服务。
*/
downloadDrawingModel(row, fileType) {
const materialCode = row && row.materialCode ? String(row.materialCode).trim() : "";
const format = this.drawingModelFormats.find(item => item.type === fileType);
const productionOrderNo = (this.params && this.params.productionOrderNo)
|| this.queryParams.routeDescription
|| this.$route.query.productionOrderNo;
if (!this.isPlmProject) {
this.$modal.msgWarning("仅 PLM 来源项目可下载图纸模型");
return;
}
if (!productionOrderNo) {
this.$modal.msgWarning("请先选择项目令号");
return;
}
if (!materialCode) {
this.$modal.msgWarning("该行没有物料编码");
return;
}
if (!format) {
this.$modal.msgWarning("请选择图纸格式");
return;
}
this.downloadingDrawingCode = materialCode;
requestDrawingModel(productionOrderNo, materialCode, fileType)
.then(async (response) => {
const isBlob = blobValidate(response);
if (!isBlob) {
const resText = await response.text();
const rspObj = JSON.parse(resText);
this.$modal.msgError((rspObj && rspObj.msg) || "下载图纸模型失败");
return;
}
const filename = `${materialCode}.${fileType}`;
const blob = new Blob([response], { type: "application/octet-stream" });
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = filename;
link.click();
URL.revokeObjectURL(url);
this.$modal.msgSuccess(`已下载物料 ${materialCode} 的 ${format.label}`);
})
.catch((error) => {
this.$modal.msgError("下载图纸模型失败,请重试!");
console.error("下载图纸模型失败:", error);
})
.finally(() => {
this.downloadingDrawingCode = "";
});
},
@ -1979,12 +2342,50 @@ export default {
};
</script>
<style>
.route-toolbar {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
margin-bottom: 8px;
}
.route-toolbar__left {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
flex: 1;
min-width: 0;
}
.route-toolbar__left .el-button {
margin-left: 0;
}
.route-toolbar__group {
width: 160px;
}
.btn {
font-size: 12px;
padding: 7px 10px !important;
border-radius: 0;
}
.route-row-ops {
display: inline-flex;
align-items: center;
justify-content: center;
flex-wrap: nowrap;
gap: 6px;
white-space: nowrap;
}
.route-row-ops .el-button {
margin-left: 0;
}
.handle {
position: absolute;
left: 50%;

View File

@ -144,6 +144,7 @@
<el-table-column label="用户昵称" align="center" key="nickName" prop="nickName" v-if="columns[2].visible" :show-overflow-tooltip="true" />
<el-table-column label="部门" align="center" key="deptName" prop="dept.deptName" v-if="columns[3].visible" :show-overflow-tooltip="true" />
<el-table-column label="手机号码" align="center" key="phonenumber" prop="phonenumber" v-if="columns[4].visible" width="120" />
<el-table-column label="员工编号" align="center" key="employeeNumber" prop="employeeNumber" width="110" :show-overflow-tooltip="true" />
<el-table-column label="状态" align="center" key="status" v-if="columns[5].visible">
<template slot-scope="scope">
<el-switch
@ -230,6 +231,13 @@
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="员工编号" prop="employeeNumber">
<el-input v-model="form.employeeNumber" placeholder="金蝶员工工号,如00094" maxlength="64" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item v-if="form.userId == undefined" label="用户名称" prop="userName">
@ -518,6 +526,7 @@ export default {
nickName: undefined,
password: undefined,
phonenumber: undefined,
employeeNumber: undefined,
email: undefined,
sex: undefined,
status: "0",

View File

@ -0,0 +1,679 @@
<template>
<div class="app-container">
<el-tabs v-model="activeTab">
<el-tab-pane label="VMI物料" name="material">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="88px">
<el-form-item label="物料编码" prop="materialCode">
<el-input v-model="queryParams.materialCode" placeholder="物料编码" clearable @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="物料名称" prop="materialName">
<el-input v-model="queryParams.materialName" placeholder="物料名称" clearable @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="材料" prop="materialQuality">
<el-input v-model="queryParams.materialQuality" placeholder="材料" clearable @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="供应商" prop="supplierName">
<el-input v-model="queryParams.supplierName" placeholder="供应商" clearable @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd" v-hasPermi="['system:vmiMaterial:add']">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete" v-hasPermi="['system:vmiMaterial:remove']">删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="el-icon-refresh" size="mini" :loading="scanning" @click="handleScan" v-hasPermi="['system:vmiMaterial:edit']">扫描低库存</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList" />
</el-row>
<el-table
v-loading="loading"
:data="materialList"
class="vmi-table"
:header-cell-style="headerCellStyle"
:cell-style="cellStyle"
:row-class-name="materialRowClass"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="50" align="center" />
<el-table-column label="物料编码" min-width="130" align="center" show-overflow-tooltip>
<template slot-scope="scope">
<span class="cell-code">{{ scope.row.materialCode || '—' }}</span>
</template>
</el-table-column>
<el-table-column label="名称" prop="materialName" min-width="140" align="center" show-overflow-tooltip />
<el-table-column label="材料" prop="materialQuality" min-width="100" align="center" show-overflow-tooltip />
<el-table-column label="供应商" min-width="110" align="center" show-overflow-tooltip>
<template slot-scope="scope">
<span class="cell-supplier">{{ scope.row.supplierName || '—' }}</span>
</template>
</el-table-column>
<el-table-column label="最小包装量" width="110" align="center">
<template slot-scope="scope">
<span class="cell-num">{{ fmtQty(scope.row.minPackQty) }}</span>
</template>
</el-table-column>
<el-table-column label="建议最高安全库存" width="150" align="center">
<template slot-scope="scope">
<span class="cell-num cell-max">{{ fmtQty(scope.row.maxSafeQty) }}</span>
</template>
</el-table-column>
<el-table-column label="建议最低安全库存" width="150" align="center">
<template slot-scope="scope">
<span class="cell-num cell-min">{{ fmtQty(scope.row.minSafeQty) }}</span>
</template>
</el-table-column>
<el-table-column label="备注" prop="remark" min-width="140" align="center" show-overflow-tooltip />
<el-table-column label="即时库存" width="110" align="center">
<template slot-scope="scope">
<span class="cell-num" :class="{ 'is-low': scope.row.stockStatus === 'LOW' }">
{{ fmtQty(scope.row.lastStockQty) }}
</span>
</template>
</el-table-column>
<el-table-column label="操作" width="220" align="center" fixed="right">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:vmiMaterial:edit']">修改</el-button>
<el-button size="mini" type="text" icon="el-icon-message" @click="handleForecast(scope.row)" v-hasPermi="['system:vmiMaterial:edit']">通知预测</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)" v-hasPermi="['system:vmiMaterial:remove']">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
</el-tab-pane>
<el-tab-pane label="低库存触发记录" name="alert">
<el-form :model="alertQuery" size="small" :inline="true" label-width="88px">
<el-form-item label="物料编码">
<el-input v-model="alertQuery.materialCode" clearable @keyup.enter.native="getAlertList" />
</el-form-item>
<el-form-item label="处理状态">
<el-select v-model="alertQuery.alertStatus" clearable placeholder="全部" style="width: 120px">
<el-option label="待确认" value="PENDING" />
<el-option label="已确认" value="CONFIRMED" />
<el-option label="已忽略" value="IGNORED" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="getAlertList">搜索</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="el-icon-s-promotion"
size="mini"
:disabled="!alertPendingIds.length"
:loading="confirmLoading"
@click="handleBatchConfirm"
v-hasPermi="['system:vmiMaterial:edit']"
>批量确认补货</el-button>
</el-col>
<el-col :span="12">
<span class="batch-hint" v-if="alertPendingIds.length">
已选 {{ alertPendingIds.length }} 条待确认;相同供应商将合并为 1 张采购申请
</span>
</el-col>
</el-row>
<el-table
v-loading="alertLoading"
:data="alertList"
class="vmi-table"
:header-cell-style="headerCellStyle"
:cell-style="cellStyle"
@selection-change="handleAlertSelectionChange"
>
<el-table-column type="selection" width="50" align="center" :selectable="alertSelectable" />
<el-table-column label="物料编码" min-width="120" align="center" show-overflow-tooltip>
<template slot-scope="scope"><span class="cell-code">{{ scope.row.materialCode }}</span></template>
</el-table-column>
<el-table-column label="物料名称" prop="materialName" min-width="140" align="center" show-overflow-tooltip />
<el-table-column label="供应商" min-width="120" align="center" show-overflow-tooltip>
<template slot-scope="scope">
<span class="cell-supplier">{{ scope.row.supplierName || scope.row.supplierCode || '—' }}</span>
</template>
</el-table-column>
<el-table-column label="最低安全库存" width="130" align="center">
<template slot-scope="scope"><span class="cell-num cell-min">{{ fmtQty(scope.row.minSafeQty) }}</span></template>
</el-table-column>
<el-table-column label="即时库存" width="110" align="center">
<template slot-scope="scope"><span class="cell-num is-low">{{ fmtQty(scope.row.currentQty) }}</span></template>
</el-table-column>
<el-table-column label="预计可用" width="110" align="center">
<template slot-scope="scope"><span class="cell-num">{{ fmtQty(scope.row.availableQty) }}</span></template>
</el-table-column>
<el-table-column label="状态" width="90" align="center">
<template slot-scope="scope">
<el-tag size="mini" :type="alertTag(scope.row.alertStatus)">{{ alertLabel(scope.row.alertStatus) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="钉钉" width="90" align="center">
<template slot-scope="scope">
<el-tag size="mini" :type="scope.row.dingPushStatus === 'SUCCESS' ? 'success' : 'danger'">{{ scope.row.dingPushStatus || '-' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="触发时间" prop="createTime" width="160" align="center" />
<el-table-column label="确认人" prop="confirmBy" width="90" align="center" />
<el-table-column label="操作" width="180" align="center" fixed="right">
<template slot-scope="scope">
<el-button
v-if="scope.row.alertStatus === 'PENDING'"
size="mini"
type="text"
@click="handleConfirm(scope.row)"
v-hasPermi="['system:vmiMaterial:edit']"
>确认补货</el-button>
<el-button
v-if="scope.row.alertStatus === 'PENDING'"
size="mini"
type="text"
@click="handleIgnore(scope.row)"
v-hasPermi="['system:vmiMaterial:edit']"
>忽略</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="alertTotal>0" :total="alertTotal" :page.sync="alertQuery.pageNum" :limit.sync="alertQuery.pageSize" @pagination="getAlertList" />
</el-tab-pane>
<el-tab-pane label="采购申请记录" name="req">
<el-form :model="reqQuery" size="small" :inline="true" label-width="88px">
<el-form-item label="物料编码">
<el-input v-model="reqQuery.materialCode" clearable @keyup.enter.native="getReqList" />
</el-form-item>
<el-form-item label="金蝶单号">
<el-input v-model="reqQuery.kingdeeBillNo" clearable @keyup.enter.native="getReqList" />
</el-form-item>
<el-form-item label="推送结果">
<el-select v-model="reqQuery.pushStatus" clearable placeholder="全部" style="width: 120px">
<el-option label="成功" value="SUCCESS" />
<el-option label="失败" value="FAIL" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="getReqList">搜索</el-button>
</el-form-item>
</el-form>
<el-table v-loading="reqLoading" :data="reqList" class="vmi-table" :header-cell-style="headerCellStyle" :cell-style="cellStyle">
<el-table-column label="物料编码" min-width="120" align="center" show-overflow-tooltip>
<template slot-scope="scope"><span class="cell-code">{{ scope.row.materialCode }}</span></template>
</el-table-column>
<el-table-column label="物料名称" prop="materialName" min-width="140" align="center" show-overflow-tooltip />
<el-table-column label="申请数量" width="110" align="center">
<template slot-scope="scope"><span class="cell-num cell-max">{{ fmtQty(scope.row.reqQty) }}</span></template>
</el-table-column>
<el-table-column label="金蝶申请单号" min-width="140" align="center" show-overflow-tooltip>
<template slot-scope="scope"><span class="cell-code">{{ scope.row.kingdeeBillNo || '—' }}</span></template>
</el-table-column>
<el-table-column label="结果" width="90" align="center">
<template slot-scope="scope">
<el-tag size="mini" :type="scope.row.pushStatus === 'SUCCESS' ? 'success' : 'danger'">{{ scope.row.pushStatus }}</el-tag>
</template>
</el-table-column>
<el-table-column label="返回信息" prop="pushMsg" min-width="200" align="center" show-overflow-tooltip />
<el-table-column label="操作人" prop="createBy" width="90" align="center" />
<el-table-column label="时间" prop="createTime" width="160" align="center" />
</el-table>
<pagination v-show="reqTotal>0" :total="reqTotal" :page.sync="reqQuery.pageNum" :limit.sync="reqQuery.pageSize" @pagination="getReqList" />
</el-tab-pane>
</el-tabs>
<el-dialog :title="title" :visible.sync="open" width="560px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="130px">
<el-form-item label="物料编码" prop="materialCode">
<el-input v-model="form.materialCode" placeholder="金蝶物料编码" />
</el-form-item>
<el-form-item label="名称" prop="materialName">
<el-input v-model="form.materialName" />
</el-form-item>
<el-form-item label="材料" prop="materialQuality">
<el-input v-model="form.materialQuality" />
</el-form-item>
<el-form-item label="供应商" prop="supplierName">
<el-input v-model="form.supplierName" />
</el-form-item>
<el-form-item label="供应商编码" prop="supplierCode">
<el-input v-model="form.supplierCode" placeholder="金蝶供应商编码,推送采购申请时使用" />
</el-form-item>
<el-form-item label="最小包装量" prop="minPackQty">
<el-input v-model="form.minPackQty" type="number" />
</el-form-item>
<el-form-item label="建议最高安全库存" prop="maxSafeQty">
<el-input v-model="form.maxSafeQty" type="number" />
</el-form-item>
<el-form-item label="建议最低安全库存" prop="minSafeQty">
<el-input v-model="form.minSafeQty" type="number" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" />
</el-form-item>
<el-form-item label="VMI负责人钉钉ID" prop="dingUserId">
<el-input v-model="form.dingUserId" placeholder="仓储部VMI负责人 userId,多个逗号分隔" />
</el-form-item>
<el-form-item label="启用" prop="enabled">
<el-radio-group v-model="form.enabled">
<el-radio label="1">启用</el-radio>
<el-radio label="0">停用</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
</el-dialog>
<el-dialog
title="确认补货并推送金蝶采购申请"
:visible.sync="confirmOpen"
width="1200px"
append-to-body
:close-on-click-modal="false"
>
<div class="confirm-tip">
以下为系统按规则计算的建议补货数量,可直接修改「申请数量」后推送。相同供应商将合并为 1 张采购申请,每行到货时间会写入金蝶采购申请。
</div>
<div class="confirm-arrival">
<span>统一到货日期</span>
<el-date-picker
v-model="confirmArrivalDate"
type="date"
value-format="yyyy-MM-dd"
placeholder="选择后填入全部行"
size="mini"
@change="applyArrivalDate"
/>
</div>
<el-table
v-loading="confirmLoading"
:data="confirmLines"
class="vmi-table"
border
max-height="460"
:header-cell-style="headerCellStyle"
:cell-style="cellStyle"
>
<el-table-column label="物料编码" min-width="120" align="center" show-overflow-tooltip>
<template slot-scope="scope"><span class="cell-code">{{ scope.row.materialCode }}</span></template>
</el-table-column>
<el-table-column label="名称" prop="materialName" min-width="130" align="center" show-overflow-tooltip />
<el-table-column label="供应商" min-width="100" align="center" show-overflow-tooltip>
<template slot-scope="scope">{{ scope.row.supplierName || scope.row.supplierCode || '—' }}</template>
</el-table-column>
<el-table-column label="即时库存" width="100" align="center">
<template slot-scope="scope"><span class="cell-num is-low">{{ fmtQty(scope.row.currentQty) }}</span></template>
</el-table-column>
<el-table-column label="预计可用" width="100" align="center">
<template slot-scope="scope"><span class="cell-num">{{ fmtQty(scope.row.availableQty) }}</span></template>
</el-table-column>
<el-table-column label="最低安全" width="100" align="center">
<template slot-scope="scope"><span class="cell-num cell-min">{{ fmtQty(scope.row.minSafeQty) }}</span></template>
</el-table-column>
<el-table-column label="最高安全" width="100" align="center">
<template slot-scope="scope"><span class="cell-num cell-max">{{ fmtQty(scope.row.maxSafeQty) }}</span></template>
</el-table-column>
<el-table-column label="最小包装" width="100" align="center">
<template slot-scope="scope"><span class="cell-num">{{ fmtQty(scope.row.minPackQty) }}</span></template>
</el-table-column>
<el-table-column label="建议数量" width="100" align="center">
<template slot-scope="scope"><span class="cell-num cell-max">{{ fmtQty(scope.row.suggestQty) }}</span></template>
</el-table-column>
<el-table-column label="申请数量" width="120" align="center" fixed="right">
<template slot-scope="scope">
<el-input
v-model="scope.row.reqQty"
type="number"
size="mini"
controls-position="right"
/>
</template>
</el-table-column>
<el-table-column label="到货日期" width="160" align="center" fixed="right">
<template slot-scope="scope">
<el-date-picker
v-model="scope.row.arrivalDate"
type="date"
value-format="yyyy-MM-dd"
placeholder="到货日期"
size="mini"
style="width: 140px"
/>
</template>
</el-table-column>
</el-table>
<div slot="footer">
<el-button type="primary" :loading="confirming" :disabled="!confirmLines.length" @click="submitConfirm">确认并推送</el-button>
<el-button @click="confirmOpen = false">取消</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import {
listVmiMaterial, getVmiMaterial, addVmiMaterial, updateVmiMaterial, delVmiMaterial,
scanVmiMaterial, confirmVmiReplenish, previewVmiReplenish, ignoreVmiAlert, notifyVmiForecast,
listVmiAlert, listVmiReq
} from '@/api/system/vmiMaterial'
export default {
name: 'VmiMaterial',
data() {
return {
activeTab: 'material',
loading: false,
scanning: false,
showSearch: true,
ids: [],
single: true,
multiple: true,
total: 0,
materialList: [],
title: '',
open: false,
queryParams: { pageNum: 1, pageSize: 10, materialCode: undefined, materialName: undefined, materialQuality: undefined, supplierName: undefined },
form: {},
rules: {
materialCode: [{ required: true, message: '物料编码不能为空', trigger: 'blur' }],
materialName: [{ required: true, message: '名称不能为空,新增时会推送金蝶', trigger: 'blur' }],
minSafeQty: [{ required: true, message: '建议最低安全库存不能为空', trigger: 'blur' }]
},
alertLoading: false,
alertList: [],
alertTotal: 0,
alertQuery: { pageNum: 1, pageSize: 10, materialCode: undefined, alertStatus: undefined },
alertPendingIds: [],
batchConfirming: false,
reqLoading: false,
reqList: [],
reqTotal: 0,
reqQuery: { pageNum: 1, pageSize: 10, materialCode: undefined, kingdeeBillNo: undefined, pushStatus: undefined },
confirmOpen: false,
confirming: false,
confirmLoading: false,
confirmArrivalDate: undefined,
confirmLines: [],
headerCellStyle: {
textAlign: 'center',
fontWeight: '600',
color: '#303133',
background: '#f5f7fa'
}
}
},
created() {
this.getList()
this.getAlertList()
this.getReqList()
},
methods: {
getList() {
this.loading = true
listVmiMaterial(this.queryParams).then(res => {
this.materialList = res.rows
this.total = res.total
this.loading = false
}).catch(() => { this.loading = false })
},
getAlertList() {
this.alertLoading = true
this.alertPendingIds = []
listVmiAlert(this.alertQuery).then(res => {
this.alertList = res.rows
this.alertTotal = res.total
this.alertLoading = false
}).catch(() => { this.alertLoading = false })
},
alertSelectable(row) {
return row.alertStatus === 'PENDING'
},
handleAlertSelectionChange(selection) {
this.alertPendingIds = (selection || [])
.filter(item => item.alertStatus === 'PENDING')
.map(item => item.id)
},
summarizeSupplierGroups(rows) {
const map = {}
;(rows || []).forEach(row => {
const key = row.supplierCode || row.supplierName || '未维护供应商'
if (!map[key]) {
map[key] = { name: row.supplierName || row.supplierCode || '未维护供应商', count: 0 }
}
map[key].count += 1
})
return Object.keys(map).map(k => map[k].name + '(' + map[k].count + '行)').join(';')
},
handleBatchConfirm() {
if (!this.alertPendingIds.length) {
this.$modal.msgWarning('请先勾选待确认的触发记录')
return
}
this.openConfirmPreview({ alertIds: this.alertPendingIds.slice() })
},
handleConfirm(row) {
this.openConfirmPreview({ alertId: row.id })
},
applyArrivalDate(val) {
;(this.confirmLines || []).forEach(row => {
this.$set(row, 'arrivalDate', val)
})
},
openConfirmPreview(payload) {
this.confirmLoading = true
this.confirmLines = []
this.confirmArrivalDate = undefined
this.confirmOpen = true
previewVmiReplenish(payload).then(res => {
this.confirmLines = (res.data || []).map(item => ({
...item,
reqQty: item.reqQty != null ? item.reqQty : item.suggestQty
}))
if (!this.confirmLines.length) {
this.$modal.msgWarning('没有可确认的记录')
this.confirmOpen = false
}
}).catch(() => {
this.confirmOpen = false
}).finally(() => { this.confirmLoading = false })
},
submitConfirm() {
const lines = (this.confirmLines || []).map(row => ({
alertId: row.alertId,
reqQty: row.reqQty,
arrivalDate: row.arrivalDate
}))
for (const line of lines) {
const n = Number(line.reqQty)
if (!line.alertId || Number.isNaN(n) || n <= 0) {
this.$modal.msgWarning('请为每行填写大于 0 的申请数量')
return
}
if (!line.arrivalDate) {
this.$modal.msgWarning('请为每行填写到货日期')
return
}
}
const supplierHint = this.summarizeSupplierGroups(this.confirmLines)
this.$modal.confirm(
'确认推送 ' + lines.length + ' 行补货到金蝶?\n相同供应商合并申请:' + (supplierHint || '—')
).then(() => {
this.confirming = true
return confirmVmiReplenish({ lines })
}).then(res => {
this.$modal.msgSuccess('已确认并推送,生成采购申请 ' + (res.data || 0) + ' 张')
this.confirmOpen = false
this.getAlertList()
this.getReqList()
}).catch(() => {}).finally(() => { this.confirming = false })
},
getReqList() {
this.reqLoading = true
listVmiReq(this.reqQuery).then(res => {
this.reqList = res.rows
this.reqTotal = res.total
this.reqLoading = false
}).catch(() => { this.reqLoading = false })
},
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
resetQuery() {
this.resetForm('queryForm')
this.handleQuery()
},
handleSelectionChange(selection) {
this.ids = selection.map(item => item.id)
this.single = selection.length !== 1
this.multiple = !selection.length
},
handleAdd() {
this.reset()
this.open = true
this.title = '新增VMI物料'
},
handleUpdate(row) {
this.reset()
getVmiMaterial(row.id).then(res => {
this.form = res.data
this.open = true
this.title = '修改VMI物料'
})
},
submitForm() {
this.$refs.form.validate(valid => {
if (!valid) return
const req = this.form.id ? updateVmiMaterial(this.form) : addVmiMaterial(this.form)
req.then(() => {
this.$modal.msgSuccess(this.form.id ? '修改成功' : '新增成功')
this.open = false
this.getList()
})
})
},
cancel() {
this.open = false
this.reset()
},
reset() {
this.form = { enabled: '1' }
this.resetForm('form')
},
handleDelete(row) {
const ids = row.id || this.ids
this.$modal.confirm('是否确认删除?').then(() => delVmiMaterial(ids)).then(() => {
this.getList()
this.$modal.msgSuccess('删除成功')
}).catch(() => {})
},
handleScan() {
this.scanning = true
scanVmiMaterial().then(res => {
this.$modal.msgSuccess('扫描完成,新触发 ' + (res.data || 0) + ' 条')
this.getList()
this.getAlertList()
}).finally(() => { this.scanning = false })
},
handleForecast(row) {
this.$modal.confirm('将需求预测推送到该物料的VMI负责人钉钉?').then(() => notifyVmiForecast(row.id)).then(() => {
this.$modal.msgSuccess('已发送')
}).catch(() => {})
},
handleIgnore(row) {
this.$modal.confirm('忽略该条低库存提醒?').then(() => ignoreVmiAlert(row.id)).then(() => {
this.$modal.msgSuccess('已忽略')
this.getAlertList()
}).catch(() => {})
},
fmtQty(v) {
if (v === null || v === undefined || v === '') return '—'
const n = Number(v)
if (Number.isNaN(n)) return '—'
return n.toFixed(2)
},
cellStyle() {
return { textAlign: 'center' }
},
materialRowClass({ row }) {
return row.stockStatus === 'LOW' ? 'row-low' : ''
},
alertLabel(s) {
return { PENDING: '待确认', CONFIRMED: '已确认', IGNORED: '已忽略' }[s] || s
},
alertTag(s) {
return { PENDING: 'warning', CONFIRMED: 'success', IGNORED: 'info' }[s] || ''
}
}
}
</script>
<style scoped>
.vmi-table ::v-deep th,
.vmi-table ::v-deep td {
text-align: center;
}
.cell-code {
font-weight: 700;
color: #303133;
letter-spacing: 0.02em;
}
.cell-supplier {
font-weight: 600;
color: #606266;
}
.cell-num {
font-variant-numeric: tabular-nums;
font-weight: 600;
}
.cell-max {
color: #3a7bd5;
}
.cell-min {
color: #c45616;
}
.cell-num.is-low {
color: #f56c6c;
font-weight: 700;
}
.confirm-arrival {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
color: #606266;
}
.vmi-table ::v-deep .row-low td {
background: #fff6f6;
}
.batch-hint {
display: inline-block;
margin-top: 6px;
color: #909399;
font-size: 12px;
}
.confirm-tip {
margin-bottom: 12px;
color: #606266;
font-size: 13px;
line-height: 1.5;
}
</style>

View File

@ -0,0 +1,672 @@
<template>
<div class="group-push-page app-container">
<section class="page-hero">
<div class="page-hero__main">
<div class="page-hero__icon">
<i class="el-icon-chat-dot-round"></i>
</div>
<div>
<h1 class="page-hero__title">群预警推送</h1>
<p class="page-hero__desc">
填写钉钉群会话 ID,勾选任务后可立即发送,也可设置每日时间由定时任务自动发到该群。数据与「预警推送配置」相同。
</p>
</div>
</div>
<div class="page-hero__stats">
<div class="stat-chip">
<span class="stat-chip__label">可选任务</span>
<span class="stat-chip__value">{{ taskList.length }}</span>
</div>
<div class="stat-chip stat-chip--accent">
<span class="stat-chip__label">已选</span>
<span class="stat-chip__value">{{ selectedCodes.length }}</span>
</div>
<div class="stat-chip">
<span class="stat-chip__label">定时任务</span>
<span class="stat-chip__value">{{ scheduleList.length }}</span>
</div>
<div class="stat-chip">
<span class="stat-chip__label">本次成功</span>
<span class="stat-chip__value">{{ successCount }}</span>
</div>
</div>
</section>
<el-card shadow="never" class="form-card">
<el-form ref="form" :model="form" :rules="rules" label-width="110px" size="small">
<el-form-item label="群会话ID" prop="openConversationId">
<el-input
v-model="form.openConversationId"
placeholder="openConversationId,将预警数据发到该钉钉群"
clearable
@change="persistForm"
/>
</el-form-item>
<el-form-item label="机器人Code">
<el-input
v-model="form.robotCode"
placeholder="留空使用默认预警推送机器人(需已添加进该群)"
clearable
@change="persistForm"
/>
</el-form-item>
<el-form-item label="配置名称">
<el-input
v-model="form.configName"
placeholder="可选,如:调度群早报"
clearable
maxlength="64"
/>
</el-form-item>
<el-form-item label="每日推送时间" prop="pushTime">
<el-time-select
v-model="form.pushTime"
:picker-options="{ start: '00:00', step: '00:05', end: '23:55' }"
placeholder="选择时间"
style="width: 220px"
@change="persistForm"
/>
</el-form-item>
<el-form-item label="启用定时" prop="enabled">
<el-radio-group v-model="form.enabled" @change="persistForm">
<el-radio label="1">启用(每天到点自动发送)</el-radio>
<el-radio label="0">停用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item>
<el-button
type="success"
size="mini"
icon="el-icon-time"
:loading="savingSchedule"
v-hasPermi="['system:workAlertGroupPush:send']"
@click="handleSaveSchedule"
>{{ form.id ? '更新定时' : '保存定时' }}</el-button>
<el-button v-if="form.id" size="mini" @click="resetScheduleForm">新建定时</el-button>
<span class="task-hint">勾选下方任务后保存。依赖 XXL-JOB「workAlertScheduledPushHandler」每分钟扫描。</span>
</el-form-item>
</el-form>
</el-card>
<el-card shadow="never" class="task-card">
<div class="task-toolbar">
<div class="task-toolbar__left">
<el-checkbox
:indeterminate="isIndeterminate"
:value="checkAll"
@change="handleCheckAll"
>全选</el-checkbox>
<span class="task-hint">企业微信 / 固定私发类任务不在本页,请仍用预警推送配置。</span>
</div>
<div class="task-toolbar__right">
<el-button size="mini" icon="el-icon-refresh" :disabled="sending" @click="loadPushTypes">刷新任务</el-button>
<el-button
type="primary"
size="mini"
icon="el-icon-s-promotion"
:loading="sending"
:disabled="!selectedCodes.length"
v-hasPermi="['system:workAlertGroupPush:send']"
@click="handleSendSelected"
>推送到指定群</el-button>
</div>
</div>
<el-table
v-loading="loading"
:data="taskList"
border
stripe
row-key="code"
@selection-change="handleSelectionChange"
ref="taskTable"
>
<el-table-column type="selection" width="48" align="center" :selectable="() => !sending" />
<el-table-column label="推送任务" min-width="180" show-overflow-tooltip>
<template slot-scope="{ row }">
<div class="task-name">{{ row.name }}</div>
<div class="task-code">{{ row.code }}</div>
</template>
</el-table-column>
<el-table-column label="说明" prop="remark" min-width="220" show-overflow-tooltip />
<el-table-column label="状态" width="200" align="center">
<template slot-scope="{ row }">
<el-tag v-if="row.status === 'idle'" type="info" size="mini">待推送</el-tag>
<el-tag v-else-if="row.status === 'sending'" type="warning" size="mini">推送中</el-tag>
<el-tag v-else-if="row.status === 'success'" type="success" size="mini">成功</el-tag>
<el-tag v-else-if="row.status === 'fail'" type="danger" size="mini">失败</el-tag>
<div v-if="row.message" class="status-msg" :class="{ fail: row.status === 'fail' }">{{ row.message }}</div>
</template>
</el-table-column>
<el-table-column label="操作" width="110" align="center">
<template slot-scope="{ row }">
<el-button
type="text"
size="mini"
icon="el-icon-s-promotion"
:disabled="sending"
v-hasPermi="['system:workAlertGroupPush:send']"
@click="handleSendOne(row)"
>推送</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-card shadow="never" class="task-card">
<div class="task-toolbar">
<div class="task-toolbar__left">
<span class="schedule-title">已保存的定时任务</span>
<span class="task-hint">到点后自动发到对应群,每天每条配置只发一次。</span>
</div>
<el-button size="mini" icon="el-icon-refresh" :disabled="scheduleLoading" @click="loadSchedules">刷新</el-button>
</div>
<el-table v-loading="scheduleLoading" :data="scheduleList" border stripe>
<el-table-column label="名称" prop="configName" min-width="120" show-overflow-tooltip />
<el-table-column label="推送时间" prop="pushTime" width="90" align="center" />
<el-table-column label="启用" width="80" align="center">
<template slot-scope="{ row }">
<el-tag :type="row.enabled === '1' ? 'success' : 'info'" size="mini">{{ row.enabled === '1' ? '启用' : '停用' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="推送任务" prop="pushCodeNames" min-width="200" show-overflow-tooltip />
<el-table-column label="群会话ID" prop="openConversationId" min-width="180" show-overflow-tooltip />
<el-table-column label="机器人" prop="robotCode" min-width="140" show-overflow-tooltip>
<template slot-scope="{ row }">{{ row.robotCode || '默认' }}</template>
</el-table-column>
<el-table-column label="上次推送" width="110" align="center">
<template slot-scope="{ row }">{{ row.lastPushDate || '—' }}</template>
</el-table-column>
<el-table-column label="操作" width="260" align="center">
<template slot-scope="{ row }">
<el-button type="text" size="mini" @click="loadScheduleToForm(row)">载入</el-button>
<el-button
type="text"
size="mini"
v-hasPermi="['system:workAlertGroupPush:send']"
@click="handleToggleEnabled(row)"
>{{ row.enabled === '1' ? '停用' : '启用' }}</el-button>
<el-button
type="text"
size="mini"
v-hasPermi="['system:workAlertGroupPush:send']"
@click="handleTriggerSchedule(row)"
>立即执行</el-button>
<el-button
type="text"
size="mini"
class="danger-text"
v-hasPermi="['system:workAlertGroupPush:send']"
@click="handleDeleteSchedule(row)"
>删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
<script>
import {
listGroupPushTypes,
sendWorkAlertGroupPush,
listGroupPushSchedule,
addGroupPushSchedule,
updateGroupPushSchedule,
delGroupPushSchedule,
triggerGroupPushSchedule
} from '@/api/system/workAlertGroupPush'
const STORAGE_KEY = 'workAlertGroupPush.form'
export default {
name: 'WorkAlertGroupPush',
data() {
return {
loading: false,
sending: false,
savingSchedule: false,
scheduleLoading: false,
taskList: [],
selectedCodes: [],
scheduleList: [],
successCount: 0,
form: {
id: undefined,
configName: '',
openConversationId: '',
robotCode: '',
pushTime: '08:30',
enabled: '1'
},
rules: {
openConversationId: [{ required: true, message: '请填写群会话ID', trigger: 'blur' }],
pushTime: [{ required: true, message: '请选择每日推送时间', trigger: 'change' }]
}
}
},
computed: {
checkAll() {
return this.taskList.length > 0 && this.selectedCodes.length === this.taskList.length
},
isIndeterminate() {
return this.selectedCodes.length > 0 && this.selectedCodes.length < this.taskList.length
}
},
created() {
this.restoreForm()
this.loadPushTypes()
this.loadSchedules()
},
methods: {
restoreForm() {
try {
const raw = localStorage.getItem(STORAGE_KEY)
if (!raw) return
const saved = JSON.parse(raw)
if (saved.openConversationId) this.form.openConversationId = saved.openConversationId
if (saved.robotCode) this.form.robotCode = saved.robotCode
if (saved.pushTime) this.form.pushTime = saved.pushTime
if (saved.enabled === '0' || saved.enabled === '1') this.form.enabled = saved.enabled
if (saved.configName) this.form.configName = saved.configName
} catch (e) {
/* ignore */
}
},
persistForm() {
localStorage.setItem(STORAGE_KEY, JSON.stringify({
openConversationId: this.form.openConversationId,
robotCode: this.form.robotCode,
pushTime: this.form.pushTime,
enabled: this.form.enabled,
configName: this.form.configName
}))
},
loadPushTypes() {
this.loading = true
listGroupPushTypes().then(res => {
this.taskList = (res.data || []).map(item => ({
...item,
status: 'idle',
message: ''
}))
this.selectedCodes = []
this.successCount = 0
}).finally(() => {
this.loading = false
})
},
handleSelectionChange(selection) {
this.selectedCodes = selection.map(item => item.code)
},
handleCheckAll(val) {
this.$refs.taskTable.clearSelection()
if (val) {
this.taskList.forEach(row => {
this.$refs.taskTable.toggleRowSelection(row, true)
})
}
},
validateForm() {
return new Promise((resolve, reject) => {
this.$refs.form.validate(valid => {
if (valid) {
this.persistForm()
resolve()
} else {
reject(new Error('请先填写群会话ID'))
}
})
})
},
sendOne(row) {
row.status = 'sending'
row.message = ''
return sendWorkAlertGroupPush({
pushCode: row.code,
openConversationId: this.form.openConversationId.trim(),
robotCode: this.form.robotCode ? this.form.robotCode.trim() : undefined
}).then(() => {
row.status = 'success'
row.message = '已发送'
this.successCount += 1
}).catch(err => {
row.status = 'fail'
row.message = (err && err.message) || (typeof err === 'string' ? err : '推送失败')
})
},
handleSendOne(row) {
this.validateForm().then(() => {
this.$modal.confirm('确认将「' + row.name + '」发到该群?').then(() => {
this.sending = true
return this.sendOne(row)
}).finally(() => {
this.sending = false
})
}).catch(() => {})
},
handleSendSelected() {
this.validateForm().then(() => {
const rows = this.taskList.filter(item => this.selectedCodes.includes(item.code))
if (!rows.length) {
this.$modal.msgWarning('请先勾选要推送的任务')
return
}
this.$modal.confirm('确认将已选 ' + rows.length + ' 项预警数据发到该群?').then(() => {
this.sending = true
this.successCount = 0
rows.forEach(row => {
row.status = 'idle'
row.message = ''
})
const runNext = (index) => {
if (index >= rows.length) {
this.sending = false
const failCount = rows.filter(r => r.status === 'fail').length
if (failCount) {
this.$modal.msgWarning('推送完成:成功 ' + this.successCount + ',失败 ' + failCount)
} else {
this.$modal.msgSuccess('已全部推送到指定群')
}
return Promise.resolve()
}
return this.sendOne(rows[index]).then(() => runNext(index + 1))
}
return runNext(0)
}).catch(() => {})
}).catch(() => {})
},
loadSchedules() {
this.scheduleLoading = true
listGroupPushSchedule().then(res => {
this.scheduleList = res.data || []
}).finally(() => {
this.scheduleLoading = false
})
},
applyTaskSelection(codes) {
this.$nextTick(() => {
const table = this.$refs.taskTable
if (!table) return
table.clearSelection()
const set = new Set(codes || [])
this.taskList.forEach(row => {
if (set.has(row.code)) {
table.toggleRowSelection(row, true)
}
})
})
},
resetScheduleForm() {
this.form.id = undefined
this.form.configName = ''
this.form.enabled = '1'
},
loadScheduleToForm(row) {
this.form.id = row.id
this.form.configName = row.configName || ''
this.form.openConversationId = row.openConversationId || ''
this.form.robotCode = row.robotCode || ''
this.form.pushTime = row.pushTime || '08:30'
this.form.enabled = row.enabled === '0' ? '0' : '1'
this.persistForm()
const codes = (row.pushCodes || '').split(/[,,;;\s]+/).filter(Boolean)
this.applyTaskSelection(codes)
this.$modal.msgSuccess('已载入,可修改后点「更新定时」')
},
buildSchedulePayload() {
return {
id: this.form.id,
configName: this.form.configName || undefined,
openConversationId: this.form.openConversationId.trim(),
robotCode: this.form.robotCode ? this.form.robotCode.trim() : undefined,
pushCodes: this.selectedCodes.join(','),
pushTime: this.form.pushTime,
enabled: this.form.enabled
}
},
handleSaveSchedule() {
this.validateForm().then(() => {
if (!this.selectedCodes.length) {
this.$modal.msgWarning('请先勾选要定时推送的任务')
return
}
if (!this.form.pushTime) {
this.$modal.msgWarning('请选择每日推送时间')
return
}
this.savingSchedule = true
const payload = this.buildSchedulePayload()
const req = payload.id ? updateGroupPushSchedule(payload) : addGroupPushSchedule(payload)
req.then(() => {
this.$modal.msgSuccess(payload.id ? '定时已更新' : '定时已保存')
this.resetScheduleForm()
this.loadSchedules()
}).finally(() => {
this.savingSchedule = false
})
}).catch(() => {})
},
handleToggleEnabled(row) {
const next = row.enabled === '1' ? '0' : '1'
updateGroupPushSchedule({
id: row.id,
configName: row.configName,
openConversationId: row.openConversationId,
robotCode: row.robotCode,
pushCodes: row.pushCodes,
pushTime: row.pushTime,
enabled: next,
remark: row.remark
}).then(() => {
this.$modal.msgSuccess(next === '1' ? '已启用' : '已停用')
this.loadSchedules()
})
},
handleTriggerSchedule(row) {
this.$modal.confirm('确认立即执行「' + (row.configName || row.pushTime) + '」?').then(() => {
return triggerGroupPushSchedule(row.id)
}).then(() => {
this.$modal.msgSuccess('已触发推送')
this.loadSchedules()
}).catch(() => {})
},
handleDeleteSchedule(row) {
this.$modal.confirm('确认删除该定时配置?').then(() => {
return delGroupPushSchedule(row.id)
}).then(() => {
this.$modal.msgSuccess('已删除')
if (this.form.id === row.id) {
this.resetScheduleForm()
}
this.loadSchedules()
}).catch(() => {})
}
}
}
</script>
<style lang="scss" scoped>
.group-push-page {
--ding-blue: #3b6cf5;
--ding-indigo: #1e2a4a;
--ding-amber: #f5a623;
--ding-bg: #f5f7fc;
--ding-border: #d8e0f0;
background:
linear-gradient(180deg, rgba(59, 108, 245, 0.05) 0%, transparent 280px),
var(--ding-bg);
min-height: calc(100vh - 84px);
}
.page-hero {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 24px;
margin-bottom: 18px;
padding: 22px 24px;
border-radius: 12px;
background: linear-gradient(135deg, var(--ding-indigo) 0%, #2a3d6e 55%, #354a82 100%);
box-shadow: 0 8px 24px rgba(30, 42, 74, 0.2);
position: relative;
overflow: hidden;
&::after {
content: '';
position: absolute;
right: -40px;
top: -40px;
width: 200px;
height: 200px;
border-radius: 50%;
background: radial-gradient(circle, rgba(245, 166, 35, 0.22) 0%, transparent 70%);
pointer-events: none;
}
}
.page-hero__main {
display: flex;
align-items: flex-start;
gap: 16px;
position: relative;
z-index: 1;
}
.page-hero__icon {
width: 48px;
height: 48px;
border-radius: 12px;
background: rgba(59, 108, 245, 0.22);
border: 1px solid rgba(91, 140, 255, 0.4);
display: flex;
align-items: center;
justify-content: center;
color: #a8c4ff;
font-size: 24px;
flex-shrink: 0;
}
.page-hero__title {
margin: 0 0 6px;
font-size: 22px;
font-weight: 700;
color: #fff;
letter-spacing: 0.02em;
}
.page-hero__desc {
margin: 0;
font-size: 13px;
color: rgba(255, 255, 255, 0.72);
line-height: 1.5;
max-width: 560px;
}
.page-hero__stats {
display: flex;
gap: 12px;
flex-shrink: 0;
position: relative;
z-index: 1;
}
.stat-chip {
min-width: 88px;
padding: 10px 14px;
border-radius: 10px;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.12);
text-align: center;
&--accent .stat-chip__value {
color: var(--ding-amber);
}
}
.stat-chip__label {
display: block;
font-size: 11px;
color: rgba(255, 255, 255, 0.55);
margin-bottom: 4px;
}
.stat-chip__value {
display: block;
font-size: 20px;
font-weight: 700;
color: #fff;
font-variant-numeric: tabular-nums;
}
.form-card,
.task-card {
border-radius: 10px;
border: 1px solid var(--ding-border);
margin-bottom: 12px;
}
.form-card ::v-deep .el-card__body {
padding: 16px 16px 4px;
}
.task-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 12px;
flex-wrap: wrap;
}
.task-toolbar__left,
.task-toolbar__right {
display: flex;
align-items: center;
gap: 12px;
}
.task-hint {
font-size: 12px;
color: #909399;
}
.schedule-title {
font-weight: 600;
color: #303133;
}
.danger-text {
color: #f56c6c;
}
.task-name {
font-weight: 600;
color: #303133;
}
.task-code {
margin-top: 2px;
font-size: 12px;
color: #909399;
font-family: Consolas, Monaco, monospace;
}
.status-msg {
margin-top: 4px;
font-size: 12px;
color: #67c23a;
line-height: 1.4;
word-break: break-all;
&.fail {
color: #f56c6c;
}
}
@media (max-width: 900px) {
.page-hero {
flex-direction: column;
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,6 @@
<template>
<div class="app-container workbench-container">
<design-task-panel v-if="showDesignEntry" />
<div v-else class="app-container workbench-container">
<!-- 顶部概览卡片 -->
<overview-cards :list="overviewList" />
@ -50,6 +51,7 @@ import OverviewCards from './components/OverviewCards'
import WorkOrderBar from './components/WorkOrderBar'
import ProcessDefectRank from './components/ProcessDefectRank'
import WorkOrderProgress from './components/WorkOrderProgress'
import DesignTaskPanel from './design/index'
import {
getOverview,
@ -64,7 +66,8 @@ export default {
OverviewCards,
WorkOrderBar,
ProcessDefectRank,
WorkOrderProgress
WorkOrderProgress,
DesignTaskPanel
},
data() {
return {
@ -79,8 +82,20 @@ export default {
workOrderProgressList: []
}
},
computed: {
showDesignEntry() {
const roles = (this.$store.getters && this.$store.getters.roles) || []
const perms = (this.$store.getters && this.$store.getters.permissions) || []
return roles.includes('admin')
|| roles.includes('designer')
|| perms.includes('system:designTask:list')
|| perms.includes('*:*:*')
}
},
created() {
this.fetchData()
if (!this.showDesignEntry) {
this.fetchData()
}
},
methods: {
fetchData() {
@ -204,4 +219,5 @@ export default {
padding: 20px;
}
}
</style>