0811
This commit is contained in:
parent
b82ec60b61
commit
e8f6e97a15
75
src/api/system/designTask.js
Normal file
75
src/api/system/designTask.js
Normal 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'
|
||||
})
|
||||
}
|
||||
@ -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 },
|
||||
})
|
||||
}
|
||||
|
||||
24
src/api/system/dingTodo.js
Normal file
24
src/api/system/dingTodo.js
Normal 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
|
||||
})
|
||||
}
|
||||
24
src/api/system/drawingRouteOperLog.js
Normal file
24
src/api/system/drawingRouteOperLog.js
Normal 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
|
||||
})
|
||||
}
|
||||
@ -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(',')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -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 }
|
||||
})
|
||||
}
|
||||
|
||||
34
src/api/system/orderProProgress.js
Normal file
34
src/api/system/orderProProgress.js
Normal 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
|
||||
})
|
||||
}
|
||||
@ -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
|
||||
})
|
||||
}
|
||||
@ -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'},
|
||||
});
|
||||
}
|
||||
|
||||
96
src/api/system/vmiMaterial.js
Normal file
96
src/api/system/vmiMaterial.js
Normal 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
|
||||
})
|
||||
}
|
||||
58
src/api/system/workAlertGroupPush.js
Normal file
58
src/api/system/workAlertGroupPush.js
Normal 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 }
|
||||
})
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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
8
src/utils/jsyyOrderNo.js
Normal file
@ -0,0 +1,8 @@
|
||||
/** JSYY 技术预研令号:不属于生产令号,禁止更新物料清单、推送工艺 */
|
||||
|
||||
export const JSYY_DENY_MSG =
|
||||
"JSYY技术预研令号不属于生产令号,严禁直接用于生产执行,禁止更新物料清单、禁止推送工艺";
|
||||
|
||||
export function isJsyyOrderNo(orderNo) {
|
||||
return !!(orderNo && String(orderNo).toUpperCase().indexOf("JSYY") >= 0);
|
||||
}
|
||||
84
src/utils/projectStatusLock.js
Normal file
84
src/utils/projectStatusLock.js
Normal 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
628
src/views/devstat/index.vue
Normal 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>
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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 : [];
|
||||
|
||||
185
src/views/system/dingTodo/index.vue
Normal file
185
src/views/system/dingTodo/index.vue
Normal 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>
|
||||
629
src/views/system/drawingRouteOperLog/index.vue
Normal file
629
src/views/system/drawingRouteOperLog/index.vue
Normal 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>
|
||||
@ -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);
|
||||
});
|
||||
},
|
||||
|
||||
/** 查询物料检索列表 */
|
||||
|
||||
@ -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
319
src/views/system/orderProDesigner/index.vue
Normal file
319
src/views/system/orderProDesigner/index.vue
Normal 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>
|
||||
696
src/views/system/orderProProgress/index.vue
Normal file
696
src/views/system/orderProProgress/index.vue
Normal 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>
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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%;
|
||||
|
||||
@ -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",
|
||||
|
||||
679
src/views/system/vmiMaterial/index.vue
Normal file
679
src/views/system/vmiMaterial/index.vue
Normal 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>
|
||||
|
||||
672
src/views/system/workAlertGroupPush/index.vue
Normal file
672
src/views/system/workAlertGroupPush/index.vue
Normal 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>
|
||||
1579
src/views/workbench/design/index.vue
Normal file
1579
src/views/workbench/design/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user