0811
35
.gitignore
vendored
@ -1,3 +1,36 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
node_modules
|
||||
|
||||
# Build outputs
|
||||
dist/
|
||||
dist_electron/
|
||||
unpackage/
|
||||
*.local
|
||||
|
||||
# Logs / cache
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
*.log
|
||||
.cache/
|
||||
.temp/
|
||||
|
||||
# IDE / OS
|
||||
.idea/
|
||||
.vscode/
|
||||
*.iml
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
Desktop.ini
|
||||
.cursor/
|
||||
|
||||
# Local / temp
|
||||
response.json
|
||||
*.tmp
|
||||
*.bak
|
||||
*.swp
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
10
package.json
@ -5,10 +5,12 @@
|
||||
"author": "tzy",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "vue-cli-service serve",
|
||||
"build:prod": "vue-cli-service build",
|
||||
"dev": "set NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve",
|
||||
"build:prod": "set NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service build",
|
||||
"preview": "node build/index.js --preview",
|
||||
"lint": "eslint --ext .js,.vue src"
|
||||
"lint": "eslint --ext .js,.vue src",
|
||||
"electron:serve": "vue-cli-service electron:serve",
|
||||
"electron:build": "vue-cli-service electron:build"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
@ -82,6 +84,7 @@
|
||||
"compression-webpack-plugin": "5.0.2",
|
||||
"connect": "3.6.6",
|
||||
"echarts": "^5.6.0",
|
||||
"electron": "^13.0.0",
|
||||
"eslint": "^6.0.1",
|
||||
"eslint-plugin-vue": "7.2.0",
|
||||
"less": "^4.2.0",
|
||||
@ -92,6 +95,7 @@
|
||||
"sass-loader": "10.1.1",
|
||||
"script-ext-html-webpack-plugin": "2.1.5",
|
||||
"svg-sprite-loader": "5.1.1",
|
||||
"vue-cli-plugin-electron-builder": "^2.1.1",
|
||||
"vue-template-compiler": "2.6.12"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
27
src/api/system/dingSchemeApplication.js
Normal file
@ -0,0 +1,27 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 分页查询技术方案申请归档
|
||||
export function listDingSchemeApplication(query) {
|
||||
return request({
|
||||
url: '/system/ding/scheme/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 查询技术方案申请详情
|
||||
export function getDingSchemeApplication(id) {
|
||||
return request({
|
||||
url: '/system/ding/scheme/' + id,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 手动推送 PLM
|
||||
export function pushDingSchemePlm(id, force) {
|
||||
return request({
|
||||
url: '/system/ding/scheme/' + id + '/push-plm',
|
||||
method: 'post',
|
||||
params: { force: !!force }
|
||||
})
|
||||
}
|
||||
67
src/api/system/materialDocHub.js
Normal file
@ -0,0 +1,67 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/** 金蝶生产单据整合(BOM 主轴,需令号) */
|
||||
export function queryMaterialDocHub(sclh) {
|
||||
return request({
|
||||
url: '/system/kingdee/materialDocHub/query',
|
||||
method: 'get',
|
||||
params: { sclh },
|
||||
timeout: 300000
|
||||
})
|
||||
}
|
||||
|
||||
/** 工序计划看板分页(令号可选) */
|
||||
export function queryMaterialDocHubBoardPage(params) {
|
||||
return request({
|
||||
url: '/system/kingdee/materialDocHub/board/page',
|
||||
method: 'get',
|
||||
params,
|
||||
timeout: 120000
|
||||
})
|
||||
}
|
||||
|
||||
/** 展开工单:仅工序计划 */
|
||||
export function queryMaterialDocHubBoardDetail(moBillNo, materialNumber) {
|
||||
return request({
|
||||
url: '/system/kingdee/materialDocHub/board/detail',
|
||||
method: 'get',
|
||||
params: { moBillNo, materialNumber },
|
||||
timeout: 60000
|
||||
})
|
||||
}
|
||||
|
||||
/** 按需:工序汇报 */
|
||||
export function queryMaterialDocHubReports(moBillNo, materialNumber, operNumber) {
|
||||
const params = { moBillNo }
|
||||
if (materialNumber) params.materialNumber = materialNumber
|
||||
if (operNumber != null && operNumber !== '') params.operNumber = operNumber
|
||||
return request({
|
||||
url: '/system/kingdee/materialDocHub/board/reports',
|
||||
method: 'get',
|
||||
params,
|
||||
timeout: 120000
|
||||
})
|
||||
}
|
||||
|
||||
/** 按需:入库点质检与入库 */
|
||||
export function queryMaterialDocHubInstockQc(moBillNo, materialNumber, operNumber) {
|
||||
const params = { moBillNo }
|
||||
if (materialNumber) params.materialNumber = materialNumber
|
||||
if (operNumber != null && operNumber !== '') params.operNumber = operNumber
|
||||
return request({
|
||||
url: '/system/kingdee/materialDocHub/board/instockQc',
|
||||
method: 'get',
|
||||
params,
|
||||
timeout: 120000
|
||||
})
|
||||
}
|
||||
|
||||
/** 按需:委外 C/G */
|
||||
export function queryMaterialDocHubSubcontract(moBillNo) {
|
||||
return request({
|
||||
url: '/system/kingdee/materialDocHub/board/subcontract',
|
||||
method: 'get',
|
||||
params: { moBillNo },
|
||||
timeout: 60000
|
||||
})
|
||||
}
|
||||
38
src/api/system/nd3PhasedProduct.js
Normal file
@ -0,0 +1,38 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 检索 ND 项目(选令号 / 切换项目)
|
||||
export function searchNd3Project(params) {
|
||||
return request({
|
||||
url: '/system/nd3/phased-product/search',
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
// 报表(页面主数据)
|
||||
export function getNd3Report(params) {
|
||||
return request({
|
||||
url: '/system/nd3/phased-product/report',
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
// 自选多种物料齐套
|
||||
export function getNd3CustomKit(data) {
|
||||
return request({
|
||||
url: '/system/nd3/phased-product/custom-kit',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 多主产品联合试算导出
|
||||
export function exportNd3CustomKit(data) {
|
||||
return request({
|
||||
url: '/system/nd3/phased-product/custom-kit/export',
|
||||
method: 'post',
|
||||
data: data,
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
@ -74,3 +74,40 @@ export function queryFromK3OperationPlanningMain(query) {
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 产能分析
|
||||
export function getCapacityAnalysisApi(query) {
|
||||
return request({
|
||||
url: '/system/material/buildCapacity',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 产能分析:获取生产令号下拉选项
|
||||
export function getProductionOrderNoOptions(query) {
|
||||
return request({
|
||||
url: '/system/plan/productionOrderNoOptions',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 产能分析:导出
|
||||
export function exportCapacityAnalysisApi(query) {
|
||||
return request({
|
||||
url: '/system/material/buildCapacity/export',
|
||||
method: 'post',
|
||||
params: query,
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
|
||||
// 产能分析:根据生产令号模糊查询带出产品信息
|
||||
export function selectByProCode(productionOrderNo) {
|
||||
return request({
|
||||
url: '/system/plan/selectByProCode',
|
||||
method: 'post',
|
||||
params: { productionOrderNo }
|
||||
})
|
||||
}
|
||||
|
||||
@ -137,6 +137,15 @@ export function uploadPDF(id) {
|
||||
})
|
||||
}
|
||||
|
||||
// 下载 PLM DWG 图纸 ZIP
|
||||
export function downloadPlmDwg(id) {
|
||||
return request({
|
||||
url: '/system/orderPro/downloadPlmDwg/' + id,
|
||||
method: 'get',
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
|
||||
// 下载生产工艺计划表
|
||||
export function exportRoute(id) {
|
||||
return request({
|
||||
@ -156,3 +165,61 @@ export function exportRoute3(id) {
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
|
||||
// 查询工艺计划表钉钉发送成功次数(项目令号主键 id,与列表/详情一致;后端可不配菜单权限)
|
||||
export function getRoutePlanSendCount(id) {
|
||||
return request({
|
||||
url: '/system/orderPro/routePlanSendCount/' + id,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 发送工艺计划表(钉钉推送分类 BOM Excel)
|
||||
export function sendProcessRoutePlanExcel(id) {
|
||||
return request({
|
||||
url: '/system/orderPro/sendProcessRoutePlanExcel/' + id,
|
||||
method: 'post',
|
||||
data: {}
|
||||
})
|
||||
}
|
||||
|
||||
// 从 FTP 读取图纸数量统计表并入库
|
||||
export function syncDrawingWorkload(id) {
|
||||
return request({
|
||||
url: '/system/orderPro/drawingWorkload/sync/' + id,
|
||||
method: 'post',
|
||||
timeout: 5 * 60 * 1000
|
||||
})
|
||||
}
|
||||
|
||||
// 查询已入库图纸工作量
|
||||
export function getDrawingWorkload(id) {
|
||||
return request({
|
||||
url: '/system/orderPro/drawingWorkload/' + id,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 获取出图状态
|
||||
export function getPlotState() {
|
||||
return request({
|
||||
url: '/system/orderPro/getstate',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 金蝶生产订单(按项目 id)
|
||||
export function getKingdeePrdMo(id) {
|
||||
return request({
|
||||
url: '/system/orderPro/kingdee/prdMo/' + id,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 金蝶采购订单(按项目 id)
|
||||
export function getKingdeePurchaseOrder(id) {
|
||||
return request({
|
||||
url: '/system/orderPro/kingdee/purchaseOrder/' + id,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
27
src/api/system/plmPdfZipPush.js
Normal file
@ -0,0 +1,27 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 查询 PLM PDF 推送记录列表
|
||||
export function listPlmPdfZipPush(query) {
|
||||
return request({
|
||||
url: '/system/plmPdfZipPush/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 查询 PLM PDF 推送记录详情
|
||||
export function getPlmPdfZipPush(id) {
|
||||
return request({
|
||||
url: '/system/plmPdfZipPush/' + id,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 下载推送明细中的 PLM DWG
|
||||
export function downloadPlmDwgItem(itemId) {
|
||||
return request({
|
||||
url: '/system/plmPdfZipPush/downloadDwgItem/' + itemId,
|
||||
method: 'get',
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
26
src/api/system/plmReceiveLog.js
Normal file
@ -0,0 +1,26 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 查询 PLM 接收留痕列表
|
||||
export function listPlmReceiveLog(query) {
|
||||
return request({
|
||||
url: '/system/plmReceiveLog/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 查询 PLM 接收留痕详情
|
||||
export function getPlmReceiveLog(id) {
|
||||
return request({
|
||||
url: '/system/plmReceiveLog/' + id,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 按留痕记录手动将总装同步到 bom_details
|
||||
export function syncPlmReceiveLogBomDetails(id) {
|
||||
return request({
|
||||
url: '/system/plmReceiveLog/' + id + '/syncBomDetails',
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
10
src/api/system/productionBoard.js
Normal file
@ -0,0 +1,10 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 生产看板查询
|
||||
export function getProductionBoardQuery(query) {
|
||||
return request({
|
||||
url: '/system/kingdee/productionBoard/query',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
12
src/api/system/ratio.js
Normal file
@ -0,0 +1,12 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function getRatioBuildCapacity(params) {
|
||||
return request({
|
||||
url: '/system/ratio/buildCapacity',
|
||||
method: 'get',
|
||||
params: {
|
||||
ratioValue: params.ratioValue,
|
||||
qty: params.qty
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -98,6 +98,14 @@ export function pushRouteBom(rooteProdet, groupName) {
|
||||
params: { rooteProdet, groupName },
|
||||
});
|
||||
}
|
||||
|
||||
export function approveRoute(rooteProdet) {
|
||||
return request({
|
||||
url: '/system/route/approveRoute',
|
||||
method: 'post',
|
||||
params: { rooteProdet },
|
||||
});
|
||||
}
|
||||
export function updateProcessPlan(rooteProdet, ids) {
|
||||
return request({
|
||||
url: '/system/route/updateProcessPlan',
|
||||
|
||||
54
src/api/system/workAlertPushConfig.js
Normal file
@ -0,0 +1,54 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function listWorkAlertPushConfig(query) {
|
||||
return request({
|
||||
url: '/system/workAlertPushConfig/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
export function getWorkAlertPushConfig(id) {
|
||||
return request({
|
||||
url: '/system/workAlertPushConfig/' + id,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function listPushTypes() {
|
||||
return request({
|
||||
url: '/system/workAlertPushConfig/pushTypes',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function addWorkAlertPushConfig(data) {
|
||||
return request({
|
||||
url: '/system/workAlertPushConfig',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function updateWorkAlertPushConfig(data) {
|
||||
return request({
|
||||
url: '/system/workAlertPushConfig',
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function delWorkAlertPushConfig(id) {
|
||||
const ids = Array.isArray(id) ? id.join(',') : id
|
||||
return request({
|
||||
url: '/system/workAlertPushConfig/' + ids,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
export function triggerWorkAlertPush(id) {
|
||||
return request({
|
||||
url: '/system/workAlertPushConfig/trigger/' + id,
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
@ -42,3 +42,131 @@ export function delWorkCenterData(id) {
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
// 生产管理看板数据(按工段 chejian,免登录)
|
||||
export function getProduceDataKanban(chejian) {
|
||||
return request({
|
||||
url: '/system/workCenterData/produceDataKanban',
|
||||
method: 'get',
|
||||
params: { chejian },
|
||||
headers: { isToken: false }
|
||||
})
|
||||
}
|
||||
|
||||
// 预警数据 - 安全库存
|
||||
export function getAlertSafetyStock() {
|
||||
return request({
|
||||
url: '/system/workCenterData/alertSafetyStock',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 预警数据 - 生产延期
|
||||
export function getAlertDelayData(workCenter) {
|
||||
return request({
|
||||
url: '/system/workCenterData/alertDelayData',
|
||||
method: 'get',
|
||||
params: { workCenter }
|
||||
})
|
||||
}
|
||||
|
||||
// 预警数据 - 采购订单临期
|
||||
export function getAlertPurchaseOrder() {
|
||||
return request({
|
||||
url: '/system/workCenterData/alertPurchaseOrder',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 预警数据 - 采购订单审批中 / 重新审核
|
||||
export function getAlertPurchaseOrderApproval() {
|
||||
return request({
|
||||
url: '/system/workCenterData/alertPurchaseOrderApproval',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 预警数据 - 采购申请单临期
|
||||
export function getAlertPurchaseRequest() {
|
||||
return request({
|
||||
url: '/system/workCenterData/alertPurchaseRequest',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 预警数据 - 采购申请临期且未下推采购订单
|
||||
export function getAlertPurchaseRequestWithoutPo() {
|
||||
return request({
|
||||
url: '/system/workCenterData/alertPurchaseRequestWithoutPo',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 预警数据 - 工序汇报未质检
|
||||
export function getAlertProcessReportNotInspected() {
|
||||
return request({
|
||||
url: '/system/workCenterData/alertProcessReportNotInspected',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 预警数据 - 工序汇报未入库
|
||||
export function getAlertProcessReport() {
|
||||
return request({
|
||||
url: '/system/workCenterData/alertProcessReport',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 预警数据 - 已转入未转出 / 部分转出
|
||||
export function getAlertTransInPendingOut(workCenter) {
|
||||
return request({
|
||||
url: '/system/workCenterData/alertTransInPendingOut',
|
||||
method: 'get',
|
||||
params: { workCenter }
|
||||
})
|
||||
}
|
||||
|
||||
// 预警数据 - 开工未转入
|
||||
export function getAlertStartedNotTransIn(workCenter) {
|
||||
return request({
|
||||
url: '/system/workCenterData/alertStartedNotTransIn',
|
||||
method: 'get',
|
||||
params: { workCenter }
|
||||
})
|
||||
}
|
||||
|
||||
// 预警数据 - 工序转移未及时
|
||||
export function getAlertProcessTransfer() {
|
||||
return request({
|
||||
url: '/system/workCenterData/alertProcessTransfer',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 预警数据 - 委外实际发出 / 接收记录
|
||||
export function getAlertSubcontractTransfer(workCenter, returnStatus) {
|
||||
return request({
|
||||
url: '/system/workCenterData/alertSubcontractTransfer',
|
||||
method: 'get',
|
||||
params: { workCenter, returnStatus }
|
||||
})
|
||||
}
|
||||
|
||||
// 按生产订单号查询委外发出 / 接收明细
|
||||
export function getSubcontractTransfersByMo(moBillNo) {
|
||||
return request({
|
||||
url: '/system/workCenterData/subcontractTransfersByMo',
|
||||
method: 'get',
|
||||
params: { moBillNo }
|
||||
})
|
||||
}
|
||||
|
||||
// 生产提醒:机加/装配/铆焊委外延期 + 委外发出/接收(含供应商)
|
||||
export function getJjSubcontractDelayRemind() {
|
||||
return request({
|
||||
url: '/system/workCenterData/jjSubcontractDelayRemind',
|
||||
method: 'get',
|
||||
timeout: 180000
|
||||
})
|
||||
}
|
||||
|
||||
@ -1 +1 @@
|
||||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M121.718 73.272v9.953c3.957-7.584 6.199-16.05 6.199-24.995C127.917 26.079 99.273 0 63.958 0 28.644 0 0 26.079 0 58.23c0 .403.028.806.028 1.21l22.97-25.953h13.34l-19.76 27.187h6.42V53.77l13.728-19.477v49.361H22.998V73.272H2.158c5.951 20.284 23.608 36.208 45.998 41.399-1.44 3.3-5.618 11.263-12.565 12.674-8.607 1.764 23.358.428 46.163-13.178 17.519-4.611 31.938-15.849 39.77-30.513h-13.506V73.272H85.02V59.464l22.998-25.977h13.008l-19.429 27.187h6.421v-7.433l13.727-19.402v39.433h-.027zm-78.24 2.822a10.516 10.516 0 0 1-.996-4.535V44.548c0-1.613.332-3.124.996-4.535a11.66 11.66 0 0 1 2.713-3.68c1.134-1.032 2.49-1.864 4.04-2.468 1.55-.605 3.21-.908 4.982-.908h11.292c1.77 0 3.431.303 4.981.908 1.522.604 2.85 1.41 3.986 2.418l-12.26 16.303v-2.898a1.96 1.96 0 0 0-.665-1.512c-.443-.403-.996-.604-1.66-.604-.665 0-1.218.201-1.661.604a1.96 1.96 0 0 0-.664 1.512v9.071L44.364 77.606a10.556 10.556 0 0 1-.886-1.512zm35.73-4.535c0 1.613-.332 3.124-.997 4.535a11.66 11.66 0 0 1-2.712 3.68c-1.134 1.032-2.49 1.864-4.04 2.469-1.55.604-3.21.907-4.982.907H55.185c-1.77 0-3.431-.303-4.981-.907-1.55-.605-2.906-1.437-4.041-2.47a12.49 12.49 0 0 1-1.384-1.512l13.727-18.217v6.375c0 .605.222 1.109.665 1.512.442.403.996.604 1.66.604.664 0 1.218-.201 1.66-.604a1.96 1.96 0 0 0 .665-1.512V53.87L75.97 36.838c.913.932 1.66 1.99 2.214 3.175.664 1.41.996 2.922.996 4.535v27.011h.028z"/></svg>
|
||||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M121.718 73.272v9.953c3.957-7.584 6.199-16.05 6.199-24.995C127.917 26.079 99.273 0 63.958 0 28.644 0 0 26.079 0 58.23c0 .403.028.806.028 1.21l22.97-25.953h13.34l-19.76 27.187h6.42V53.77l13.728-19.477v49.361H22.998V73.272H2.158c5.951 20.284 23.608 36.208 45.998 41.399-1.44 3.3-5.618 11.263-12.565 12.674-8.607 1.764 23.358.428 46.163-13.178 17.519-4.611 31.938-15.849 39.77-30.513h-13.506V73.272H85.02V59.464l22.998-25.977h13.008l-19.429 27.187h6.421v-7.433l13.727-19.402v39.433h-.027zm-78.24 2.822a10.516 10.516 0 0 1-.996-4.535V44.548c0-1.613.332-3.124.996-4.535a11.66 11.66 0 0 1 2.713-3.68c1.134-1.032 2.49-1.864 4.04-2.468 1.55-.605 3.21-.908 4.982-.908h11.292c1.77 0 3.431.303 4.981.908 1.522.604 2.85 1.41 3.986 2.418l-12.26 16.303v-2.898a1.96 1.96 0 0 0-.665-1.512c-.443-.403-.996-.604-1.66-.604-.665 0-1.218.201-1.661.604a1.96 1.96 0 0 0-.664 1.512v9.071L44.364 77.606a10.556 10.556 0 0 1-.886-1.512zm35.73-4.535c0 1.613-.332 3.124-.997 4.535a11.66 11.66 0 0 1-2.712 3.68c-1.134 1.032-2.49 1.864-4.04 2.469-1.55.604-3.21.907-4.982.907H514.2185c-1.77 0-3.431-.303-4.981-.907-1.55-.605-2.906-1.437-4.041-2.47a12.49 12.49 0 0 1-1.384-1.512l13.727-18.217v6.375c0 .605.222 1.109.665 1.512.442.403.996.604 1.66.604.664 0 1.218-.201 1.66-.604a1.96 1.96 0 0 0 .665-1.512V53.87L75.97 36.838c.913.932 1.66 1.99 2.214 3.175.664 1.41.996 2.922.996 4.535v27.011h.028z"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
@ -1 +1 @@
|
||||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M127.88 73.143c0 1.412-.506 2.635-1.518 3.669-1.011 1.033-2.209 1.55-3.592 1.55h-17.887c0 9.296-1.783 17.178-5.35 23.645l16.609 17.044c1.011 1.034 1.517 2.257 1.517 3.67 0 1.412-.506 2.635-1.517 3.668-.958 1.033-2.155 1.55-3.593 1.55-1.438 0-2.635-.517-3.593-1.55l-15.811-16.063a15.49 15.49 0 0 1-1.196 1.06c-.532.434-1.65 1.208-3.353 2.322a50.104 50.104 0 0 1-5.192 2.974c-1.758.87-3.94 1.658-6.546 2.364-2.607.706-5.189 1.06-7.748 1.06V47.044H58.89v73.062c-2.716 0-5.417-.367-8.106-1.102-2.688-.734-5.003-1.631-6.945-2.692a66.769 66.769 0 0 1-5.268-3.179c-1.571-1.057-2.73-1.94-3.476-2.65L33.9 109.34l-14.611 16.877c-1.066 1.14-2.344 1.711-3.833 1.711-1.277 0-2.422-.434-3.434-1.304-1.012-.978-1.557-2.187-1.635-3.627-.079-1.44.333-2.705 1.236-3.794l16.129-18.51c-3.087-6.197-4.63-13.644-4.63-22.342H5.235c-1.383 0-2.58-.517-3.592-1.55S.125 74.545.125 73.132c0-1.412.506-2.635 1.518-3.668 1.012-1.034 2.21-1.55 3.592-1.55h17.887V43.939L9.308 29.833c-1.012-1.033-1.517-2.256-1.517-3.669 0-1.412.505-2.635 1.517-3.668 1.012-1.034 2.21-1.55 3.593-1.55s2.58.516 3.593 1.55l13.813 14.106h67.396l13.814-14.106c1.012-1.034 2.21-1.55 3.592-1.55 1.384 0 2.581.516 3.593 1.55 1.012 1.033 1.518 2.256 1.518 3.668 0 1.413-.506 2.636-1.518 3.67l-13.814 14.105v23.975h17.887c1.383 0 2.58.516 3.593 1.55 1.011 1.033 1.517 2.256 1.517 3.668l-.005.01zM89.552 26.175H38.448c0-7.23 2.489-13.386 7.466-18.469C50.892 2.623 56.92.082 64 .082c7.08 0 13.108 2.541 18.086 7.624 4.977 5.083 7.466 11.24 7.466 18.469z"/></svg>
|
||||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M127.88 73.143c0 1.412-.506 2.635-1.518 3.669-1.011 1.033-2.209 1.55-3.592 1.55h-17.887c0 9.296-1.783 17.178-5.35 23.645l16.609 17.044c1.011 1.034 1.517 2.257 1.517 3.67 0 1.412-.506 2.635-1.517 3.668-.958 1.033-2.155 1.55-3.593 1.55-1.438 0-2.635-.517-3.593-1.55l-15.811-16.063a15.49 15.49 0 0 1-1.196 1.06c-.532.434-1.65 1.208-3.353 2.322a50.104 50.104 0 0 1-5.192 2.974c-1.758.87-3.94 1.658-6.546 2.364-2.607.706-14.2189 1.06-7.748 1.06V47.044H58.89v73.062c-2.716 0-5.417-.367-8.106-1.102-2.688-.734-5.003-1.631-6.945-2.692a66.769 66.769 0 0 1-5.268-3.179c-1.571-1.057-2.73-1.94-3.476-2.65L33.9 109.34l-14.611 16.877c-1.066 1.14-2.344 1.711-3.833 1.711-1.277 0-2.422-.434-3.434-1.304-1.012-.978-1.557-2.187-1.635-3.627-.079-1.44.333-2.705 1.236-3.794l16.129-18.51c-3.087-6.197-4.63-13.644-4.63-22.342H5.235c-1.383 0-2.58-.517-3.592-1.55S.125 74.545.125 73.132c0-1.412.506-2.635 1.518-3.668 1.012-1.034 2.21-1.55 3.592-1.55h17.887V43.939L9.308 29.833c-1.012-1.033-1.517-2.256-1.517-3.669 0-1.412.505-2.635 1.517-3.668 1.012-1.034 2.21-1.55 3.593-1.55s2.58.516 3.593 1.55l13.813 14.106h67.396l13.814-14.106c1.012-1.034 2.21-1.55 3.592-1.55 1.384 0 2.581.516 3.593 1.55 1.012 1.033 1.518 2.256 1.518 3.668 0 1.413-.506 2.636-1.518 3.67l-13.814 14.105v23.975h17.887c1.383 0 2.58.516 3.593 1.55 1.011 1.033 1.517 2.256 1.517 3.668l-.005.01zM89.552 26.175H38.448c0-7.23 2.489-13.386 7.466-18.469C50.892 2.623 56.92.082 64 .082c7.08 0 13.108 2.541 18.086 7.624 4.977 5.083 7.466 11.24 7.466 18.469z"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
@ -1 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1566035680909" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3601" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M1002.0848 744.672l-33.568 10.368c0.96 7.264 2.144 14.304 2.144 21.76 0 7.328-1.184 14.432-2.368 21.568l33.792 10.56c7.936 2.24 14.496 7.616 18.336 14.752 3.84 7.328 4.672 15.808 1.952 23.552-5.376 16-23.168 24.672-39.936 19.68l-34.176-10.624c-7.136 12.8-15.776 24.672-26.208 35.2l20.8 27.488a28.96 28.96 0 0 1 5.824 22.816 29.696 29.696 0 0 1-12.704 19.616 32.544 32.544 0 0 1-44.416-6.752l-20.8-27.552c-13.696 6.56-28.192 11.2-43.008 13.888v33.632c0 16.736-14.112 30.432-31.648 30.432-17.6 0-31.872-13.696-31.872-30.432v-33.632a167.616 167.616 0 0 1-42.88-13.888l-20.928 27.552c-10.72 13.76-30.08 16.64-44.288 6.752a29.632 29.632 0 0 1-12.704-19.616 29.28 29.28 0 0 1 5.696-22.816l20.896-27.808a166.72 166.72 0 0 1-27.008-34.688l-33.376 10.432c-16.8 5.184-34.56-3.552-39.936-19.616a29.824 29.824 0 0 1 20.224-38.24l33.472-10.432c-0.8-7.264-2.016-14.304-2.016-21.824 0-7.36 1.184-14.496 2.304-21.632l-33.792-10.368c-16.672-5.376-25.632-22.496-20.224-38.432 5.376-16 23.136-24.672 39.936-19.68l34.016 10.752c7.328-12.672 15.84-24.8 26.336-35.328l-20.8-27.552a29.44 29.44 0 0 1 6.944-42.432 32.704 32.704 0 0 1 44.384 6.752l20.832 27.616c13.696-6.432 28.224-11.2 43.104-13.952v-33.568c0-16.736 14.048-30.432 31.648-30.432 17.536 0 31.808 13.568 31.808 30.432v33.504c15.072 2.688 29.344 7.808 42.848 14.016l20.992-27.616a32.48 32.48 0 0 1 44.224-6.752 29.568 29.568 0 0 1 7.136 42.432l-21.024 27.808c10.432 10.432 19.872 21.888 27.04 34.752l33.376-10.432c16.768-5.12 34.56 3.68 39.936 19.68 5.536 15.936-3.712 33.056-20.32 38.304z m-206.016-74.432c-61.344 0-111.136 47.808-111.136 106.56 0 58.88 49.792 106.496 111.136 106.496 61.312 0 111.104-47.616 111.104-106.496 0-58.752-49.792-106.56-111.104-106.56z" p-id="3602"></path><path d="M802.7888 57.152h-76.448c0-22.08-21.024-38.24-42.848-38.24H39.3968a39.68 39.68 0 0 0-39.36 40.032v795.616s41.888 120.192 110.752 120.192H673.2848a227.488 227.488 0 0 1-107.04-97.44H117.6368s-40.608-13.696-40.608-41.248l470.304-0.256 1.664 3.36a227.68 227.68 0 0 1-12.64-73.632c0-60.576 24-118.624 66.88-161.44a228.352 228.352 0 0 1 123.552-63.392l-3.2 0.288 2.144-424.672h38.208l0.576 421.024c27.04 0 52.672 4.8 76.64 13.344V101.536c0.032 0-6.304-44.384-38.368-44.384zM149.7648 514.336H72.3888v-77.408H149.7648v77.408z m0-144.32H72.3888v-77.44H149.7648v77.44z m0-137.248H72.3888v-77.44H149.7648v77.44z m501.856 281.568H206.0848v-77.408h445.536v77.408z m0-144.32H206.0848v-77.44h445.536v77.44z m0-137.248H206.0848v-77.44h445.536v77.44z" p-id="3603"></path></svg>
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1566035680909" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3601" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M1002.0848 744.672l-33.568 10.368c0.96 7.264 2.144 14.304 2.144 21.76 0 7.328-1.184 14.432-2.368 21.568l33.792 10.56c7.936 2.24 14.496 7.616 18.336 14.752 3.84 7.328 4.672 15.808 1.952 23.552-5.376 16-23.168 24.672-39.936 19.68l-34.176-10.624c-7.136 12.8-15.776 24.672-26.208 35.2l20.8 27.488a28.96 28.96 0 0 1 5.824 22.816 29.696 29.696 0 0 1-12.704 19.616 32.544 32.544 0 0 1-44.416-6.752l-20.8-27.552c-13.696 6.56-28.192 11.2-43.008 13.888v33.632c0 16.736-14.112 30.432-31.648 30.432-17.6 0-31.872-13.696-31.872-30.432v-33.632a167.616 167.616 0 0 1-42.88-13.888l-20.928 27.552c-10.72 13.76-30.08 16.64-44.288 6.752a29.632 29.632 0 0 1-12.704-19.616 29.28 29.28 0 0 1 5.696-22.816l20.896-27.808a166.72 166.72 0 0 1-27.008-34.688l-33.376 10.432c-16.8 14.2184-34.56-3.552-39.936-19.616a29.824 29.824 0 0 1 20.224-38.24l33.472-10.432c-0.8-7.264-2.016-14.304-2.016-21.824 0-7.36 1.184-14.496 2.304-21.632l-33.792-10.368c-16.672-5.376-25.632-22.496-20.224-38.432 5.376-16 23.136-24.672 39.936-19.68l34.016 10.752c7.328-12.672 15.84-24.8 26.336-35.328l-20.8-27.552a29.44 29.44 0 0 1 6.944-42.432 32.704 32.704 0 0 1 44.384 6.752l20.832 27.616c13.696-6.432 28.224-11.2 43.104-13.952v-33.568c0-16.736 14.048-30.432 31.648-30.432 17.536 0 31.808 13.568 31.808 30.432v33.504c15.072 2.688 29.344 7.808 42.848 14.016l20.992-27.616a32.48 32.48 0 0 1 44.224-6.752 29.568 29.568 0 0 1 7.136 42.432l-21.024 27.808c10.432 10.432 19.872 21.888 27.04 34.752l33.376-10.432c16.768-5.12 34.56 3.68 39.936 19.68 5.536 15.936-3.712 33.056-20.32 38.304z m-206.016-74.432c-61.344 0-111.136 47.808-111.136 106.56 0 58.88 49.792 106.496 111.136 106.496 61.312 0 111.104-47.616 111.104-106.496 0-58.752-49.792-106.56-111.104-106.56z" p-id="3602"></path><path d="M802.7888 57.152h-76.448c0-22.08-21.024-38.24-42.848-38.24H39.3968a39.68 39.68 0 0 0-39.36 40.032v795.616s41.888 120.192 110.752 120.192H673.2848a227.488 227.488 0 0 1-107.04-97.44H117.6368s-40.608-13.696-40.608-41.248l470.304-0.256 1.664 3.36a227.68 227.68 0 0 1-12.64-73.632c0-60.576 24-118.624 66.88-161.44a228.352 228.352 0 0 1 123.552-63.392l-3.2 0.288 2.144-424.672h38.208l0.576 421.024c27.04 0 52.672 4.8 76.64 13.344V101.536c0.032 0-6.304-44.384-38.368-44.384zM149.7648 514.336H72.3888v-77.408H149.7648v77.408z m0-144.32H72.3888v-77.44H149.7648v77.44z m0-137.248H72.3888v-77.44H149.7648v77.44z m501.856 281.568H206.0848v-77.408h445.536v77.408z m0-144.32H206.0848v-77.44h445.536v77.44z m0-137.248H206.0848v-77.44h445.536v77.44z" p-id="3603"></path></svg>
|
||||
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
@ -1 +1 @@
|
||||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M84.742 36.8c2.398 7.2 5.595 12.8 11.19 18.4 4.795-4.8 7.992-11.2 10.39-18.4h-21.58zm-52.748 40h20.78l-10.39-28-10.39 28z"/><path d="M111.916 0H16.009C7.218 0 .025 7.2.025 16v96c0 8.8 7.193 16 15.984 16h95.907c8.791 0 15.984-7.2 15.984-16V16c0-8.8-6.394-16-15.984-16zM72.754 103.2c-1.598 1.6-3.197 1.6-4.795 1.6-.8 0-2.398 0-3.197-.8-.8-.8-1.599 0-1.599-.8s-.799-1.6-1.598-3.2c-.8-1.6-.8-2.4-1.599-4l-3.196-8.8H28.797L25.6 96c-1.598 3.2-2.398 5.6-3.197 7.2-.8 1.6-2.398 1.6-4.795 1.6-1.599 0-3.197-.8-4.796-1.6-1.598-1.6-2.397-2.4-2.397-4 0-.8 0-1.6.799-3.2.8-1.6.8-2.4 1.598-4l17.583-44.8c.8-1.6.8-3.2 1.599-4.8.799-1.6 1.598-3.2 2.397-4 .8-.8 1.599-2.4 3.197-3.2 1.599-.8 3.197-.8 4.796-.8 1.598 0 3.196 0 4.795.8 1.598.8 2.398 1.6 3.197 3.2.799.8 1.598 2.4 2.397 4 .8 1.6 1.599 3.2 2.398 5.6l17.583 44c1.598 3.2 2.398 5.6 2.398 7.2-.8.8-1.599 2.4-2.398 4zM116.711 72c-8.791-3.2-15.185-7.2-20.78-12-5.594 5.6-12.787 9.6-21.579 12l-2.397-4c8.791-2.4 15.984-5.6 21.579-11.2C87.939 51.2 83.144 44 81.545 36h-7.992v-3.2h21.58c-1.6-2.4-3.198-5.6-4.796-8l2.397-.8c1.599 2.4 3.997 5.6 5.595 8.8h19.98v4h-7.992c-2.397 8-6.393 15.2-11.189 20 5.595 4.8 11.988 8.8 20.78 11.2l-3.197 4z"/></svg>
|
||||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M84.742 36.8c2.398 7.2 5.595 12.8 11.19 18.4 4.795-4.8 7.992-11.2 10.39-18.4h-21.58zm-52.748 40h20.78l-10.39-28-10.39 28z"/><path d="M111.916 0H16.009C7.218 0 .025 7.2.025 16v96c0 8.8 7.193 16 15.984 16h95.907c8.791 0 15.984-7.2 15.984-16V16c0-8.8-6.394-16-15.984-16zM72.754 103.2c-1.598 1.6-3.197 1.6-4.795 1.6-.8 0-2.398 0-3.197-.8-.8-.8-1.599 0-1.599-.8s-.799-1.6-1.598-3.2c-.8-1.6-.8-2.4-1.599-4l-3.196-8.8H28.797L25.6 96c-1.598 3.2-2.398 5.6-3.197 7.2-.8 1.6-2.398 1.6-4.795 1.6-1.599 0-3.197-.8-4.796-1.6-1.598-1.6-2.397-2.4-2.397-4 0-.8 0-1.6.799-3.2.8-1.6.8-2.4 1.598-4l17.583-44.8c.8-1.6.8-3.2 1.599-4.8.799-1.6 1.598-3.2 2.397-4 .8-.8 1.599-2.4 3.197-3.2 1.599-.8 3.197-.8 4.796-.8 1.598 0 3.196 0 4.795.8 1.598.8 2.398 1.6 3.197 3.2.799.8 1.598 2.4 2.397 4 .8 1.6 1.599 3.2 2.398 5.6l17.583 44c1.598 3.2 2.398 5.6 2.398 7.2-.8.8-1.599 2.4-2.398 4zM116.711 72c-8.791-3.2-114.2185-7.2-20.78-12-5.594 5.6-12.787 9.6-21.579 12l-2.397-4c8.791-2.4 15.984-5.6 21.579-11.2C87.939 51.2 83.144 44 81.545 36h-7.992v-3.2h21.58c-1.6-2.4-3.198-5.6-4.796-8l2.397-.8c1.599 2.4 3.997 5.6 5.595 8.8h19.98v4h-7.992c-2.397 8-6.393 15.2-11.189 20 5.595 4.8 11.988 8.8 20.78 11.2l-3.197 4z"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
@ -1 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1579774825624" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1248" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M498.595712 482.290351 345.420077 482.290351l0 57.307194 210.477712 0L555.897789 274.196942l-57.301054 0L498.596735 482.290351zM498.595712 482.290351" p-id="1249"></path><path d="M577.685002 644.98478l379.879913 0 0 57.302077L577.685002 702.286858 577.685002 644.98478 577.685002 644.98478zM577.685002 644.98478" p-id="1250"></path><path d="M577.685002 773.764795l379.879913 0 0 57.307194L577.685002 831.071989 577.685002 773.764795 577.685002 773.764795zM577.685002 773.764795" p-id="1251"></path><path d="M577.685002 902.549927l379.879913 0 0 57.307194L577.685002 959.857121 577.685002 902.549927 577.685002 902.549927zM577.685002 902.549927" p-id="1252"></path><path d="M102.523001 382.290823c4.450359 2.615571 9.470699 3.954055 14.530948 3.954055 2.969635 0 5.952572-0.461511 8.836249-1.394766l190.809767-61.886489c15.052834-4.882194 23.297612-21.040199 18.415418-36.08894-4.882194-15.052834-21.040199-23.297612-36.093033-18.415418L175.676092 308.458257c15.994276-26.115797 35.170011-50.537 57.370639-72.743768 73.767074-73.767074 171.845857-114.388237 276.16783-114.388237 104.32095 0 202.39564 40.622186 276.16169 114.388237s114.393353 171.845857 114.393353 276.16783c0 26.427906-2.615571 52.449559-7.709589 77.780481l58.302871 0c4.464685-25.499767 6.708795-51.470255 6.708795-77.780481 0-60.449767-11.845793-119.102608-35.204803-174.336584-22.559808-53.334719-54.850236-101.226472-95.968725-142.349055-41.122583-41.122583-89.017406-73.408917-142.348032-95.968725C628.317169 75.866898 569.659211 64.021106 509.215584 64.021106c-60.448744 0-119.106702 11.845793-174.336584 35.207873-53.334719 22.559808-101.230566 54.846142-142.349055 95.968725-23.980157 23.980157-44.934398 50.278103-62.727647 78.601172l-20.738323-105.655342c-3.043313-15.527648-18.105357-25.642007-33.631982-22.599717-15.527648 3.048429-25.64303 18.105357-22.599717 33.637098l36.102243 183.932126C90.51348 371.153158 95.460142 378.13313 102.523001 382.290823L102.523001 382.290823zM102.523001 382.290823" p-id="1253"></path><path d="M126.020158 587.9416 67.768453 587.9416c5.759167 33.679054 15.368012 66.544579 28.789697 98.278327 22.559808 53.333696 54.850236 101.225449 95.971795 142.348032 41.122583 41.122583 89.014336 73.408917 142.349055 95.968725 54.112432 22.88829 111.517863 34.71157 170.668031 35.18229L505.547031 902.395408c-102.94972-0.941442-199.594851-41.445948-272.499277-114.349351C177.545672 732.543975 140.810003 663.275355 126.020158 587.9416L126.020158 587.9416zM126.020158 587.9416" p-id="1254"></path></svg>
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1579774825624" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1248" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M498.595712 482.290351 345.420077 482.290351l0 57.307194 210.477712 0L555.897789 274.196942l-57.301054 0L498.596735 482.290351zM498.595712 482.290351" p-id="1249"></path><path d="M577.685002 644.98478l379.879913 0 0 57.302077L577.685002 702.286858 577.685002 644.98478 577.685002 644.98478zM577.685002 644.98478" p-id="1250"></path><path d="M577.685002 773.764795l379.879913 0 0 57.307194L577.685002 831.071989 577.685002 773.764795 577.685002 773.764795zM577.685002 773.764795" p-id="1251"></path><path d="M577.685002 902.549927l379.879913 0 0 57.307194L577.685002 959.857121 577.685002 902.549927 577.685002 902.549927zM577.685002 902.549927" p-id="1252"></path><path d="M102.523001 382.290823c4.450359 2.615571 9.470699 3.954055 14.530948 3.954055 2.969635 0 5.952572-0.461511 8.836249-1.394766l190.809767-61.886489c15.052834-4.882194 23.297612-21.040199 18.415418-36.08894-4.882194-15.052834-21.040199-23.297612-36.093033-18.415418L175.676092 308.458257c15.994276-26.115797 35.170011-50.537 57.370639-72.743768 73.767074-73.767074 171.845857-114.388237 276.16783-114.388237 104.32095 0 202.39564 40.622186 276.16169 114.388237s114.393353 171.845857 114.393353 276.16783c0 26.427906-2.615571 52.449559-7.709589 77.780481l58.302871 0c4.464685-25.499767 6.708795-51.470255 6.708795-77.780481 0-60.449767-11.845793-119.102608-35.204803-174.336584-22.559808-53.334719-54.850236-101.226472-95.968725-142.349055-41.122583-41.122583-89.017406-73.408917-142.348032-95.968725C628.317169 75.866898 569.659211 64.021106 509.215584 64.021106c-60.448744 0-119.106702 11.845793-174.336584 35.207873-53.334719 22.559808-101.230566 54.846142-142.349055 95.968725-23.980157 23.980157-44.934398 50.278103-62.727647 78.601172l-20.738323-105.655342c-3.043313-15.527648-18.105357-25.642007-33.631982-22.599717-15.527648 3.048429-25.64303 18.105357-22.599717 33.637098l36.102243 183.932126C90.51348 371.153158 95.460142 378.13313 102.523001 382.290823L102.523001 382.290823zM102.523001 382.290823" p-id="1253"></path><path d="M126.020158 587.9416 67.768453 587.9416c5.759167 33.679054 15.368012 66.544579 28.789697 98.278327 22.559808 53.333696 54.850236 101.225449 95.971795 142.348032 41.122583 41.122583 89.014336 73.408917 142.349055 95.968725 54.112432 22.88829 111.517863 34.71157 170.668031 314.218229L505.547031 902.395408c-102.94972-0.941442-199.594851-41.445948-272.499277-114.349351C177.545672 732.543975 140.810003 663.275355 126.020158 587.9416L126.020158 587.9416zM126.020158 587.9416" p-id="1254"></path></svg>
|
||||
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
@ -1 +1 @@
|
||||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M126.713 90.023c.858.985 1.287 2.134 1.287 3.447v29.553c0 1.423-.429 2.6-1.287 3.53-.858.93-1.907 1.395-3.146 1.395H97.824c-1.145 0-2.146-.465-3.004-1.395-.858-.93-1.287-2.107-1.287-3.53V93.47c0-.875.19-1.696.572-2.462.382-.766.906-1.368 1.573-1.806a3.84 3.84 0 0 1 2.146-.657h9.725V69.007a3.84 3.84 0 0 0-.43-1.806 3.569 3.569 0 0 0-1.143-1.313 2.714 2.714 0 0 0-1.573-.492h-36.47v23.149h9.725c1.144 0 2.145.492 3.004 1.478.858.985 1.287 2.134 1.287 3.447v29.553c0 .876-.191 1.696-.573 2.463-.38.766-.905 1.368-1.573 1.806a3.84 3.84 0 0 1-2.145.656H51.915a3.84 3.84 0 0 1-2.145-.656c-.668-.438-1.216-1.04-1.645-1.806a4.96 4.96 0 0 1-.644-2.463V93.47c0-1.313.43-2.462 1.288-3.447.858-.986 1.907-1.478 3.146-1.478h9.582v-23.15h-37.9c-.953 0-1.74.356-2.359 1.068-.62.711-.93 1.56-.93 2.544v19.538h9.726c1.239 0 2.264.492 3.074 1.478.81.985 1.216 2.134 1.216 3.447v29.553c0 1.423-.405 2.6-1.216 3.53-.81.93-1.835 1.395-3.074 1.395H4.29c-.476 0-.93-.082-1.358-.246a4.1 4.1 0 0 1-1.144-.657 4.658 4.658 0 0 1-.93-1.067 5.186 5.186 0 0 1-.643-1.395 5.566 5.566 0 0 1-.215-1.56V93.47c0-.437.048-.875.143-1.313a3.95 3.95 0 0 1 .429-1.15c.19-.328.429-.656.715-.984.286-.329.572-.602.858-.821.286-.22.62-.383 1.001-.493.382-.11.763-.164 1.144-.164h9.726V61.619c0-.985.31-1.833.93-2.544.619-.712 1.358-1.068 2.216-1.068h44.335V39.62h-9.582c-1.24 0-2.288-.492-3.146-1.477a5.09 5.09 0 0 1-1.287-3.448V5.14c0-1.423.429-2.627 1.287-3.612.858-.985 1.907-1.477 3.146-1.477h25.743c.763 0 1.478.246 2.145.739a5.17 5.17 0 0 1 1.573 1.888c.382.766.573 1.587.573 2.462v29.553c0 1.313-.43 2.463-1.287 3.448-.859.985-1.86 1.477-3.004 1.477h-9.725v18.389h42.762c.954 0 1.74.355 2.36 1.067.62.711.93 1.56.93 2.545v26.925h9.582c1.239 0 2.288.492 3.146 1.478z"/></svg>
|
||||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M126.713 90.023c.858.985 1.287 2.134 1.287 3.447v29.553c0 1.423-.429 2.6-1.287 3.53-.858.93-1.907 1.395-3.146 1.395H97.824c-1.145 0-2.146-.465-3.004-1.395-.858-.93-1.287-2.107-1.287-3.53V93.47c0-.875.19-1.696.572-2.462.382-.766.906-1.368 1.573-1.806a3.84 3.84 0 0 1 2.146-.657h9.725V69.007a3.84 3.84 0 0 0-.43-1.806 3.569 3.569 0 0 0-1.143-1.313 2.714 2.714 0 0 0-1.573-.492h-36.47v23.149h9.725c1.144 0 2.145.492 3.004 1.478.858.985 1.287 2.134 1.287 3.447v29.553c0 .876-.191 1.696-.573 2.463-.38.766-.905 1.368-1.573 1.806a3.84 3.84 0 0 1-2.145.656H51.915a3.84 3.84 0 0 1-2.145-.656c-.668-.438-1.216-1.04-1.645-1.806a4.96 4.96 0 0 1-.644-2.463V93.47c0-1.313.43-2.462 1.288-3.447.858-.986 1.907-1.478 3.146-1.478h9.582v-23.15h-37.9c-.953 0-1.74.356-2.359 1.068-.62.711-.93 1.56-.93 2.544v19.538h9.726c1.239 0 2.264.492 3.074 1.478.81.985 1.216 2.134 1.216 3.447v29.553c0 1.423-.405 2.6-1.216 3.53-.81.93-1.835 1.395-3.074 1.395H4.29c-.476 0-.93-.082-1.358-.246a4.1 4.1 0 0 1-1.144-.657 4.658 4.658 0 0 1-.93-1.067 14.2186 14.2186 0 0 1-.643-1.395 5.566 5.566 0 0 1-.215-1.56V93.47c0-.437.048-.875.143-1.313a3.95 3.95 0 0 1 .429-1.15c.19-.328.429-.656.715-.984.286-.329.572-.602.858-.821.286-.22.62-.383 1.001-.493.382-.11.763-.164 1.144-.164h9.726V61.619c0-.985.31-1.833.93-2.544.619-.712 1.358-1.068 2.216-1.068h44.335V39.62h-9.582c-1.24 0-2.288-.492-3.146-1.477a5.09 5.09 0 0 1-1.287-3.448V5.14c0-1.423.429-2.627 1.287-3.612.858-.985 1.907-1.477 3.146-1.477h25.743c.763 0 1.478.246 2.145.739a5.17 5.17 0 0 1 1.573 1.888c.382.766.573 1.587.573 2.462v29.553c0 1.313-.43 2.463-1.287 3.448-.859.985-1.86 1.477-3.004 1.477h-9.725v18.389h42.762c.954 0 1.74.355 2.36 1.067.62.711.93 1.56.93 2.545v26.925h9.582c1.239 0 2.288.492 3.146 1.478z"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
76
src/background.js
Normal file
@ -0,0 +1,76 @@
|
||||
'use strict'
|
||||
|
||||
import { app, protocol, BrowserWindow } from 'electron'
|
||||
import { createProtocol } from 'vue-cli-plugin-electron-builder/lib'
|
||||
|
||||
const isDevelopment = process.env.NODE_ENV !== 'production'
|
||||
|
||||
// Scheme must be registered before the app is ready
|
||||
protocol.registerSchemesAsPrivileged([
|
||||
{ scheme: 'app', privileges: { secure: true, standard: true } }
|
||||
])
|
||||
|
||||
async function createWindow() {
|
||||
// Create the browser window.
|
||||
const win = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 800,
|
||||
webPreferences: {
|
||||
// Use pluginOptions.nodeIntegration, leave this alone
|
||||
// See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more info
|
||||
nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION,
|
||||
contextIsolation: !process.env.ELECTRON_NODE_INTEGRATION,
|
||||
webSecurity: false // Disable web security for local file loading if necessary
|
||||
}
|
||||
})
|
||||
|
||||
// Set window title
|
||||
win.setTitle('生产预警中心桌面端')
|
||||
|
||||
if (process.env.WEBPACK_DEV_SERVER_URL) {
|
||||
// Load the url of the dev server if in development mode
|
||||
await win.loadURL(process.env.WEBPACK_DEV_SERVER_URL)
|
||||
if (!process.env.IS_TEST) win.webContents.openDevTools()
|
||||
} else {
|
||||
createProtocol('app')
|
||||
// Load the index.html when not in development
|
||||
win.loadURL('app://./index.html')
|
||||
}
|
||||
}
|
||||
|
||||
// Quit when all windows are closed.
|
||||
app.on('window-all-closed', () => {
|
||||
// On macOS it is common for applications and their menu bar
|
||||
// to stay active until the user quits explicitly with Cmd + Q
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit()
|
||||
}
|
||||
})
|
||||
|
||||
app.on('activate', () => {
|
||||
// On macOS it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
||||
})
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
app.on('ready', async () => {
|
||||
createWindow()
|
||||
})
|
||||
|
||||
// Exit cleanly on request from parent process in development mode.
|
||||
if (isDevelopment) {
|
||||
if (process.platform === 'win32') {
|
||||
process.on('message', (data) => {
|
||||
if (data === 'graceful-exit') {
|
||||
app.quit()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
process.on('SIGTERM', () => {
|
||||
app.quit()
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -5,7 +5,7 @@
|
||||
:current-page="currentPage"
|
||||
:page-size="pageSize"
|
||||
:layout="layout"
|
||||
:page-sizes="pageSizes"
|
||||
:page-sizes="normalizedPageSizes"
|
||||
:pager-count="pagerCount"
|
||||
:total="total"
|
||||
v-bind="$attrs"
|
||||
@ -31,12 +31,12 @@ export default {
|
||||
},
|
||||
limit: {
|
||||
type: Number,
|
||||
default: 20
|
||||
default: 10
|
||||
},
|
||||
pageSizes: {
|
||||
type: Array,
|
||||
default() {
|
||||
return [100, 200, 300, 500]
|
||||
return [10, 20, 50, 100]
|
||||
}
|
||||
},
|
||||
// 移动端页码按钮的数量端默认值5
|
||||
@ -81,15 +81,22 @@ export default {
|
||||
set(val) {
|
||||
this.$emit('update:limit', val)
|
||||
}
|
||||
},
|
||||
/** 确保当前 pageSize 在可选列表中,避免下拉显示与真实条数不一致 */
|
||||
normalizedPageSizes() {
|
||||
const sizes = [...this.pageSizes]
|
||||
const limit = Number(this.limit)
|
||||
if (limit > 0 && !sizes.includes(limit)) {
|
||||
sizes.push(limit)
|
||||
}
|
||||
return sizes.sort((a, b) => a - b)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleSizeChange(val) {
|
||||
this.pageSize = val
|
||||
if (this.currentPage * val > this.total) {
|
||||
this.currentPage = 1
|
||||
}
|
||||
this.$emit('pagination', { page: this.currentPage, limit: val })
|
||||
this.$emit('update:limit', val)
|
||||
this.$emit('update:page', 1)
|
||||
this.$emit('pagination', { page: 1, limit: val })
|
||||
if (this.autoScroll) {
|
||||
scrollTo(0, 800)
|
||||
}
|
||||
|
||||
@ -8,10 +8,26 @@ import { isRelogin } from '@/utils/request'
|
||||
|
||||
NProgress.configure({ showSpinner: false })
|
||||
|
||||
const whiteList = ['/login', '/register', '/index', '/indexDaping', '/mrp2', '/system/cost']
|
||||
const whiteList = ['/login', '/register', '/index', '/indexDaping', '/mrp2', '/system/cost', '/DataBoard/ProduceKanBan/index']
|
||||
|
||||
function isWhiteList(path) {
|
||||
return whiteList.indexOf(path) !== -1
|
||||
}
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
NProgress.start()
|
||||
|
||||
// 白名单页面无需登录,直接访问(含无效/过期 token 场景)
|
||||
if (isWhiteList(to.path)) {
|
||||
if (to.path === '/login' && getToken()) {
|
||||
next({ path: '/' })
|
||||
NProgress.done()
|
||||
return
|
||||
}
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
if (getToken()) {
|
||||
to.meta.title && store.dispatch('settings/setTitle', to.meta.title)
|
||||
/* has token*/
|
||||
@ -42,13 +58,8 @@ router.beforeEach((to, from, next) => {
|
||||
} else {
|
||||
|
||||
// 没有token
|
||||
if (whiteList.indexOf(to.path) !== -1) {
|
||||
// 在免登录白名单,直接进入
|
||||
next()
|
||||
} else {
|
||||
next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页
|
||||
NProgress.done()
|
||||
}
|
||||
next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页
|
||||
NProgress.done()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@ -84,11 +84,6 @@ export const constantRoutes = [
|
||||
component: () => import('@/views/login'),
|
||||
hidden: true
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
component: () => import('@/views/'),
|
||||
hidden: true
|
||||
},
|
||||
{
|
||||
path: '/register',
|
||||
component: () => import('@/views/register'),
|
||||
@ -140,6 +135,12 @@ export const constantRoutes = [
|
||||
component: () => import('@/views/indexDaping'),
|
||||
hidden: true
|
||||
},
|
||||
{
|
||||
path: '/DataBoard/ProduceKanBan/index',
|
||||
component: () => import('@/views/system/produceKanban/index'),
|
||||
hidden: true,
|
||||
meta: { title: '生产管理看板' }
|
||||
},
|
||||
{
|
||||
path: '/mrp2',
|
||||
component: () => import('@/views/system/mrp2/index'),
|
||||
@ -153,7 +154,87 @@ export const constantRoutes = [
|
||||
name: 'CostPublic',
|
||||
hidden: true,
|
||||
meta: { title: '零件成本查询' }
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/system/operationPlanningMain',
|
||||
component: Layout,
|
||||
hidden: true,
|
||||
redirect: '/system/operationPlanningMain/index',
|
||||
children: [
|
||||
{
|
||||
path: 'index',
|
||||
component: () => import('@/views/system/operationPlanningMain/index.vue'),
|
||||
name: 'OperationPlanningMain',
|
||||
meta: { title: '工序计划', activeMenu: '/system/operationPlanningMain' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/system/capacity-analysis',
|
||||
component: Layout,
|
||||
hidden: true,
|
||||
children: [
|
||||
{
|
||||
path: 'index',
|
||||
component: () => import('@/views/system/operationPlanningMain/CapacityAnalysis.vue'),
|
||||
name: 'CapacityAnalysis',
|
||||
meta: { title: '预配套查询', activeMenu: '/system/operationPlanningMain' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/system/productionBoard',
|
||||
component: Layout,
|
||||
hidden: true,
|
||||
children: [
|
||||
{
|
||||
path: 'index',
|
||||
component: () => import('@/views/system/productionBoard/index.vue'),
|
||||
name: 'ProductionBoard',
|
||||
meta: { title: '生产综合看板', activeMenu: '/system/productionBoard' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/system/materialDocHub',
|
||||
component: Layout,
|
||||
hidden: true,
|
||||
children: [
|
||||
{
|
||||
path: 'index',
|
||||
component: () => import('@/views/system/materialDocHub/index.vue'),
|
||||
name: 'MaterialDocHub',
|
||||
meta: { title: '金蝶单据整合', activeMenu: '/system/materialDocHub' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/system/productionRemind',
|
||||
component: Layout,
|
||||
hidden: true,
|
||||
children: [
|
||||
{
|
||||
path: 'index',
|
||||
component: () => import('@/views/system/productionRemind/index.vue'),
|
||||
name: 'ProductionRemind',
|
||||
meta: { title: '委外延期生产提醒', activeMenu: '/system/productionRemind' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/system/nd3PhasedProduct',
|
||||
component: Layout,
|
||||
hidden: true,
|
||||
children: [
|
||||
{
|
||||
path: 'index',
|
||||
component: () => import('@/views/system/nd3PhasedProduct/index.vue'),
|
||||
name: 'Nd3PhasedProduct',
|
||||
meta: { title: 'ND3阶段性产品报表', activeMenu: '/system/nd3PhasedProduct' }
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// {
|
||||
// path: '/',
|
||||
// component: Layout,
|
||||
@ -256,7 +337,8 @@ Router.prototype.replace = function push(location) {
|
||||
|
||||
export default new Router({
|
||||
base: process.env.VUE_APP_CONTEXT_PATH,
|
||||
mode: 'history', // 去掉url中的#
|
||||
// Electron 使用 app:// 协议加载页面,history 模式会导致路由跳转后找不到资源(全站 404)
|
||||
mode: process.env.IS_ELECTRON ? 'hash' : 'history',
|
||||
scrollBehavior: () => ({ y: 0 }),
|
||||
routes: constantRoutes
|
||||
})
|
||||
|
||||
@ -79,7 +79,8 @@
|
||||
</el-col>
|
||||
</el-row> -->
|
||||
<el-form
|
||||
v-for="(n,i) in list"
|
||||
v-for="(n, i) in list"
|
||||
:key="i"
|
||||
:inline="true"
|
||||
:model="n"
|
||||
ref="ruleForm"
|
||||
|
||||
840
src/views/system/dingSchemeApplication/index.vue
Normal file
@ -0,0 +1,840 @@
|
||||
<template>
|
||||
<div class="ding-scheme-page app-container">
|
||||
<section class="page-hero">
|
||||
<div class="page-hero__main">
|
||||
<div class="page-hero__icon">
|
||||
<i class="el-icon-s-order"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="page-hero__title">技术方案申请归档</h1>
|
||||
<p class="page-hero__desc">查看钉钉审批通过的技术方案申请,追溯 PLM 推送状态与表单明细。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-hero__stats">
|
||||
<div class="stat-chip">
|
||||
<span class="stat-chip__label">当前页</span>
|
||||
<span class="stat-chip__value">{{ applicationList.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="96px"
|
||||
class="search-form"
|
||||
>
|
||||
<el-form-item label="方案申请编号" prop="schemeApplyNo">
|
||||
<el-input
|
||||
v-model="queryParams.schemeApplyNo"
|
||||
placeholder="模糊匹配"
|
||||
clearable
|
||||
style="width: 180px"
|
||||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="概述名称" prop="overviewName">
|
||||
<el-input
|
||||
v-model="queryParams.overviewName"
|
||||
placeholder="模糊匹配"
|
||||
clearable
|
||||
style="width: 180px"
|
||||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="填表人" prop="fillerName">
|
||||
<el-input
|
||||
v-model="queryParams.fillerName"
|
||||
placeholder="模糊匹配"
|
||||
clearable
|
||||
style="width: 140px"
|
||||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="客户全称" prop="customerFullName">
|
||||
<el-input
|
||||
v-model="queryParams.customerFullName"
|
||||
placeholder="模糊匹配"
|
||||
clearable
|
||||
style="width: 180px"
|
||||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="产品名称" prop="productName">
|
||||
<el-input
|
||||
v-model="queryParams.productName"
|
||||
placeholder="模糊匹配"
|
||||
clearable
|
||||
style="width: 160px"
|
||||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="PLM 推送" prop="plmPushStatus">
|
||||
<el-select v-model="queryParams.plmPushStatus" placeholder="全部" clearable style="width: 130px">
|
||||
<el-option v-for="item in plmStatusOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</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="applicationList"
|
||||
:header-cell-style="headerCellStyle"
|
||||
:row-class-name="tableRowClassName"
|
||||
stripe
|
||||
>
|
||||
<el-table-column type="index" label="#" width="52" align="center" />
|
||||
<el-table-column label="方案申请编号" min-width="148" show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
<span class="scheme-no">{{ scope.row.schemeApplyNo || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="概述名称" min-width="160" show-overflow-tooltip prop="overviewName" />
|
||||
<el-table-column label="填表人" width="96" align="center" prop="fillerName" />
|
||||
<el-table-column label="客户全称" min-width="160" show-overflow-tooltip prop="customerFullName" />
|
||||
<el-table-column label="产品名称" min-width="120" show-overflow-tooltip prop="productName" />
|
||||
<el-table-column label="需求日期" width="108" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span class="time-text">{{ parseTime(scope.row.needDate, '{y}-{m}-{d}') || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="审批状态" width="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag size="mini" :type="dingStatusTag(scope.row.dingProcessStatus)" effect="plain">
|
||||
{{ formatDingStatus(scope.row.dingProcessStatus) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="PLM 推送" width="108" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag size="mini" :type="plmStatusTag(scope.row.plmPushStatus)" effect="dark">
|
||||
{{ formatPlmStatus(scope.row.plmPushStatus) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="审批完成" width="168" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span class="time-text">{{ parseTime(scope.row.dingInstanceFinishTime, '{y}-{m}-{d} {h}:{i}:{s}') || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="148" align="center" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-view"
|
||||
v-hasPermi="['system:dingSchemeApplication:query']"
|
||||
@click="handleDetail(scope.row)"
|
||||
>详情</el-button>
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-upload2"
|
||||
:loading="scope.row._pushing"
|
||||
:disabled="scope.row.plmPushStatus === 'SUCCESS'"
|
||||
@click="handlePushPlm(scope.row)"
|
||||
>推送 PLM</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="62%"
|
||||
:append-to-body="true"
|
||||
custom-class="ding-scheme-drawer"
|
||||
@closed="resetDetail"
|
||||
>
|
||||
<div v-loading="detailLoading" class="drawer-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 scheme-no">{{ detail.schemeApplyNo || '—' }}</span>
|
||||
</div>
|
||||
<div class="detail-summary__item">
|
||||
<span class="detail-summary__label">PLM 推送</span>
|
||||
<el-tag size="small" :type="plmStatusTag(detail.plmPushStatus)" effect="dark">
|
||||
{{ formatPlmStatus(detail.plmPushStatus) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="detail-summary__item detail-summary__item--wide">
|
||||
<span class="detail-summary__label">推送说明</span>
|
||||
<span class="detail-summary__value">{{ detail.plmPushMessage || '—' }}</span>
|
||||
</div>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
icon="el-icon-upload2"
|
||||
:loading="pushLoading"
|
||||
:disabled="detail.plmPushStatus === 'SUCCESS'"
|
||||
@click="handlePushPlm(detail, true)"
|
||||
>手动推送 PLM</el-button>
|
||||
</div>
|
||||
|
||||
<el-tabs v-model="detailTab" class="detail-tabs">
|
||||
<el-tab-pane label="业务概要" name="overview">
|
||||
<el-descriptions :column="2" border size="small" class="overview-desc">
|
||||
<el-descriptions-item label="概述名称">{{ detail.overviewName || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="填表人">{{ detail.fillerName || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="填表日期">{{ parseTime(detail.fillDate, '{y}-{m}-{d}') || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="需求日期">{{ parseTime(detail.needDate, '{y}-{m}-{d}') || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="客户全称">{{ detail.customerFullName || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="联系人及电话">{{ detail.contactAndPhone || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="项目分类">{{ detail.projectCategory || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="产品名称">{{ detail.productName || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="方案提交日期">{{ parseTime(detail.schemeSubmitDate, '{y}-{m}-{d}') || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="方案已接收">{{ detail.schemeReceived || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="审批标题" :span="2">{{ detail.title || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="审批结果">{{ detail.instanceResult || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="审批状态">
|
||||
<el-tag size="mini" :type="dingStatusTag(detail.dingProcessStatus)" effect="plain">
|
||||
{{ formatDingStatus(detail.dingProcessStatus) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="发起部门">{{ detail.originatorDeptName || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="流程实例 ID">
|
||||
<code class="mono-text">{{ detail.processInstanceId || '—' }}</code>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">{{ parseTime(detail.dingInstanceCreateTime, '{y}-{m}-{d} {h}:{i}:{s}') || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="完成时间">{{ parseTime(detail.dingInstanceFinishTime, '{y}-{m}-{d} {h}:{i}:{s}') || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="PLM 推送时间">{{ parseTime(detail.plmPushTime, '{y}-{m}-{d} {h}:{i}:{s}') || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="触发事件">{{ detail.triggerEventType || '—' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane :label="'表单字段 (' + formFieldRows.length + ')'" name="form">
|
||||
<el-table
|
||||
v-if="formFieldRows.length"
|
||||
:data="formFieldRows"
|
||||
size="small"
|
||||
:header-cell-style="headerCellStyle"
|
||||
max-height="520"
|
||||
stripe
|
||||
>
|
||||
<el-table-column type="index" label="#" width="48" align="center" />
|
||||
<el-table-column label="字段" min-width="200" prop="label" show-overflow-tooltip />
|
||||
<el-table-column label="填写内容" min-width="320" show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
<span class="field-value">{{ scope.row.value || '—' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-else description="暂无表单字段数据" />
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="审批任务" name="task">
|
||||
<pre v-if="formattedTaskDetail" class="json-block">{{ formattedTaskDetail }}</pre>
|
||||
<el-empty v-else description="暂无任务明细" />
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="原始 JSON" name="raw">
|
||||
<el-tabs type="border-card" class="raw-tabs">
|
||||
<el-tab-pane label="wfValuesJson">
|
||||
<div class="raw-toolbar">
|
||||
<el-button size="mini" plain icon="el-icon-document-copy" @click="copyText(formattedWfValues)">复制</el-button>
|
||||
</div>
|
||||
<pre class="json-block">{{ formattedWfValues }}</pre>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="formJson">
|
||||
<div class="raw-toolbar">
|
||||
<el-button size="mini" plain icon="el-icon-document-copy" @click="copyText(formattedFormJson)">复制</el-button>
|
||||
</div>
|
||||
<pre class="json-block">{{ formattedFormJson }}</pre>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="formLabelMap">
|
||||
<div class="raw-toolbar">
|
||||
<el-button size="mini" plain icon="el-icon-document-copy" @click="copyText(formattedFormLabelMap)">复制</el-button>
|
||||
</div>
|
||||
<pre class="json-block">{{ formattedFormLabelMap }}</pre>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</template>
|
||||
<el-empty v-else-if="!detailLoading" description="暂无详情数据" />
|
||||
</div>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
listDingSchemeApplication,
|
||||
getDingSchemeApplication,
|
||||
pushDingSchemePlm
|
||||
} from '@/api/system/dingSchemeApplication'
|
||||
|
||||
const PLM_STATUS_MAP = {
|
||||
PENDING: '待推送',
|
||||
SUCCESS: '已成功',
|
||||
FAIL: '失败',
|
||||
SKIP: '跳过'
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'DingSchemeApplication',
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
showSearch: true,
|
||||
total: 0,
|
||||
applicationList: [],
|
||||
dateRange: [],
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
schemeApplyNo: undefined,
|
||||
overviewName: undefined,
|
||||
fillerName: undefined,
|
||||
customerFullName: undefined,
|
||||
productName: undefined,
|
||||
plmPushStatus: undefined
|
||||
},
|
||||
plmStatusOptions: [
|
||||
{ value: 'PENDING', label: '待推送' },
|
||||
{ value: 'SUCCESS', label: '已成功' },
|
||||
{ value: 'FAIL', label: '失败' },
|
||||
{ value: 'SKIP', label: '跳过' }
|
||||
],
|
||||
detailOpen: false,
|
||||
detailLoading: false,
|
||||
pushLoading: false,
|
||||
detail: null,
|
||||
detailTab: 'overview'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
pageSuccessCount() {
|
||||
return this.applicationList.filter(row => row.plmPushStatus === 'SUCCESS').length
|
||||
},
|
||||
detailTitle() {
|
||||
if (!this.detail) return '技术方案申请详情'
|
||||
const no = this.detail.schemeApplyNo || ''
|
||||
return no ? '申请详情 · ' + no : '技术方案申请详情'
|
||||
},
|
||||
formFieldRows() {
|
||||
return this.buildFormFieldRows(this.detail)
|
||||
},
|
||||
formattedTaskDetail() {
|
||||
return this.formatJsonBlock(this.detail && this.detail.taskDetail)
|
||||
},
|
||||
formattedWfValues() {
|
||||
return this.formatJsonBlock(this.detail && this.detail.wfValuesJson)
|
||||
},
|
||||
formattedFormJson() {
|
||||
return this.formatJsonBlock(this.detail && this.detail.formJson)
|
||||
},
|
||||
formattedFormLabelMap() {
|
||||
return this.formatJsonBlock(this.detail && this.detail.formLabelMap)
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getList()
|
||||
},
|
||||
methods: {
|
||||
headerCellStyle() {
|
||||
return {
|
||||
background: '#eef2fb',
|
||||
color: '#1e2a4a',
|
||||
fontWeight: '600',
|
||||
fontSize: '13px'
|
||||
}
|
||||
},
|
||||
tableRowClassName({ rowIndex }) {
|
||||
return rowIndex % 2 === 0 ? 'row-even' : 'row-odd'
|
||||
},
|
||||
formatPlmStatus(status) {
|
||||
if (!status) return '—'
|
||||
return PLM_STATUS_MAP[status] || status
|
||||
},
|
||||
plmStatusTag(status) {
|
||||
const map = {
|
||||
PENDING: 'info',
|
||||
SUCCESS: 'success',
|
||||
FAIL: 'danger',
|
||||
SKIP: 'warning'
|
||||
}
|
||||
return map[status] || ''
|
||||
},
|
||||
formatDingStatus(status) {
|
||||
if (!status) return '—'
|
||||
const map = {
|
||||
COMPLETED: '已完成',
|
||||
RUNNING: '审批中',
|
||||
TERMINATED: '已终止',
|
||||
CANCELED: '已撤销'
|
||||
}
|
||||
return map[status] || status
|
||||
},
|
||||
dingStatusTag(status) {
|
||||
const map = {
|
||||
COMPLETED: 'success',
|
||||
RUNNING: 'warning',
|
||||
TERMINATED: 'danger',
|
||||
CANCELED: 'info'
|
||||
}
|
||||
return map[status] || ''
|
||||
},
|
||||
buildQueryParams() {
|
||||
let params = { ...this.queryParams }
|
||||
params = this.addDateRange(params, this.dateRange, 'DingInstanceFinishTime')
|
||||
return params
|
||||
},
|
||||
getList() {
|
||||
this.loading = true
|
||||
listDingSchemeApplication(this.buildQueryParams())
|
||||
.then(response => {
|
||||
this.applicationList = (response.rows || []).map(row => ({ ...row, _pushing: false }))
|
||||
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()
|
||||
},
|
||||
handleDetail(row) {
|
||||
if (!row || row.id == null) return
|
||||
this.detailOpen = true
|
||||
this.detailLoading = true
|
||||
this.detailTab = 'overview'
|
||||
getDingSchemeApplication(row.id)
|
||||
.then(response => {
|
||||
this.detail = response.data || null
|
||||
})
|
||||
.finally(() => {
|
||||
this.detailLoading = false
|
||||
})
|
||||
},
|
||||
resetDetail() {
|
||||
this.detail = null
|
||||
this.detailTab = 'overview'
|
||||
},
|
||||
handlePushPlm(row, fromDrawer) {
|
||||
if (!row || row.id == null) return
|
||||
const isSuccess = row.plmPushStatus === 'SUCCESS'
|
||||
const title = isSuccess ? '强制重新推送 PLM' : '推送 PLM'
|
||||
const message = isSuccess
|
||||
? '该记录已推送成功,是否强制重新推送?'
|
||||
: '确认将本条技术方案申请推送到 PLM?'
|
||||
|
||||
this.$confirm(message, title, {
|
||||
confirmButtonText: isSuccess ? '强制推送' : '确认推送',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
const force = isSuccess
|
||||
if (fromDrawer) {
|
||||
this.pushLoading = true
|
||||
} else {
|
||||
this.$set(row, '_pushing', true)
|
||||
}
|
||||
pushDingSchemePlm(row.id, force)
|
||||
.then(response => {
|
||||
if (response.code === 200) {
|
||||
this.$modal.msgSuccess('推送请求已提交')
|
||||
this.getList()
|
||||
if (fromDrawer && this.detail && this.detail.id === row.id) {
|
||||
return getDingSchemeApplication(row.id).then(res => {
|
||||
this.detail = res.data || this.detail
|
||||
})
|
||||
}
|
||||
} else {
|
||||
this.$modal.msgError(response.msg || '推送失败')
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
if (fromDrawer) {
|
||||
this.pushLoading = false
|
||||
} else {
|
||||
this.$set(row, '_pushing', false)
|
||||
}
|
||||
})
|
||||
}).catch(() => {})
|
||||
},
|
||||
parseJsonSafe(text, fallback) {
|
||||
if (!text) return fallback
|
||||
try {
|
||||
return JSON.parse(text)
|
||||
} catch (e) {
|
||||
return fallback
|
||||
}
|
||||
},
|
||||
formatJsonBlock(text) {
|
||||
if (!text) return '—'
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(text), null, 2)
|
||||
} catch (e) {
|
||||
return text
|
||||
}
|
||||
},
|
||||
formatWfCell(cell) {
|
||||
if (!cell || typeof cell !== 'object') return ''
|
||||
const ext = cell.ext
|
||||
if (ext) {
|
||||
const names = this.extractInnerContactNames(ext)
|
||||
if (names) return names
|
||||
}
|
||||
const value = cell.value
|
||||
if (value == null) return ''
|
||||
return String(value).trim()
|
||||
},
|
||||
extractInnerContactNames(ext) {
|
||||
if (!ext) return ''
|
||||
try {
|
||||
const arr = typeof ext === 'string' ? JSON.parse(ext) : ext
|
||||
if (!Array.isArray(arr) || !arr.length) return ''
|
||||
const names = []
|
||||
arr.forEach(item => {
|
||||
if (!item) return
|
||||
const name = item.name || item.nick || item.orgUserName
|
||||
if (name) names.push(String(name).trim())
|
||||
})
|
||||
return names.length ? names.join(',') : ''
|
||||
} catch (e) {
|
||||
return ''
|
||||
}
|
||||
},
|
||||
buildFormFieldRows(detail) {
|
||||
if (!detail) return []
|
||||
const labelMap = this.parseJsonSafe(detail.formLabelMap, {})
|
||||
const wfValues = this.parseJsonSafe(detail.wfValuesJson, {})
|
||||
const rows = []
|
||||
const seen = {}
|
||||
|
||||
Object.keys(labelMap).forEach(fieldId => {
|
||||
const label = labelMap[fieldId]
|
||||
if (!label || label === '分栏') return
|
||||
const cell = wfValues[fieldId]
|
||||
const value = this.formatWfCell(cell)
|
||||
if (!value) return
|
||||
seen[fieldId] = true
|
||||
rows.push({ fieldId: fieldId, label: label, value: value })
|
||||
})
|
||||
|
||||
Object.keys(wfValues).forEach(fieldId => {
|
||||
if (seen[fieldId]) return
|
||||
const value = this.formatWfCell(wfValues[fieldId])
|
||||
if (!value) return
|
||||
rows.push({ fieldId: fieldId, label: fieldId, value: value })
|
||||
})
|
||||
|
||||
return rows
|
||||
},
|
||||
copyText(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>
|
||||
.ding-scheme-page {
|
||||
--ding-blue: #3b6cf5;
|
||||
--ding-blue-dark: #2a4fc0;
|
||||
--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: 520px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.search-card,
|
||||
.table-card {
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--ding-border);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.search-card ::v-deep .el-card__body {
|
||||
padding: 16px 16px 4px;
|
||||
}
|
||||
|
||||
.toolbar-row {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.scheme-no {
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-weight: 600;
|
||||
color: var(--ding-blue-dark);
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.time-text {
|
||||
font-size: 12px;
|
||||
color: #5a6478;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.mono-text {
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-size: 12px;
|
||||
color: #4a5568;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.field-value {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.drawer-body {
|
||||
padding: 0 4px 24px;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.detail-summary {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 16px 24px;
|
||||
padding: 14px 16px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(135deg, #eef2fb 0%, #f8faff 100%);
|
||||
border: 1px solid var(--ding-border);
|
||||
}
|
||||
|
||||
.detail-summary__item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
&--wide {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-summary__label {
|
||||
font-size: 11px;
|
||||
color: #7a8499;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.detail-summary__value {
|
||||
font-size: 13px;
|
||||
color: var(--ding-indigo);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.detail-tabs ::v-deep .el-tabs__header {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.overview-desc {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.json-block {
|
||||
margin: 0;
|
||||
padding: 14px 16px;
|
||||
border-radius: 8px;
|
||||
background: #1e2433;
|
||||
color: #c8d4e8;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
overflow: auto;
|
||||
max-height: 480px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.raw-toolbar {
|
||||
margin-bottom: 8px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.raw-tabs {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
::v-deep .ding-scheme-drawer .el-drawer__header {
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 14px;
|
||||
border-bottom: 1px solid var(--ding-border);
|
||||
color: var(--ding-indigo);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
::v-deep .el-table .row-even td {
|
||||
background: #fafbfd;
|
||||
}
|
||||
</style>
|
||||
71
src/views/system/materialDocHub/MaterialTableBlock.vue
Normal file
@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<el-table
|
||||
:data="rows"
|
||||
border
|
||||
size="small"
|
||||
:empty-text="emptyText"
|
||||
highlight-current-row
|
||||
@row-click="onRowClick"
|
||||
>
|
||||
<el-table-column type="index" label="#" width="50" align="center" />
|
||||
<el-table-column label="物料编码" prop="materialCode" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="物料名称" prop="materialName" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="需求" prop="requiredQty" width="72" align="center" />
|
||||
<el-table-column label="即时库存" width="88" align="center">
|
||||
<template slot-scope="scope">{{ scope.row.stock ? scope.row.stock.onHandQty : '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="预计可用" width="88" align="center">
|
||||
<template slot-scope="scope">{{ stockFinal(scope.row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="MO" width="56" align="center">
|
||||
<template slot-scope="scope">{{ (scope.row.moList || []).length }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="PO" width="56" align="center">
|
||||
<template slot-scope="scope">{{ (scope.row.poList || []).length }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="汇报" width="64" align="center">
|
||||
<template slot-scope="scope">{{ (scope.row.operationReports || []).length }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="72" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag size="mini" :type="statusType(scope.row)">{{ statusText(scope.row) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="72" align="center" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" size="mini" @click.stop="onDetail(scope.row)">详情</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'MaterialTableBlock',
|
||||
props: {
|
||||
rows: { type: Array, default: () => [] },
|
||||
emptyText: { type: String, default: '暂无数据' }
|
||||
},
|
||||
methods: {
|
||||
onRowClick(row) {
|
||||
this.$emit('detail', row)
|
||||
},
|
||||
onDetail(row) {
|
||||
this.$emit('detail', row)
|
||||
},
|
||||
stockFinal(row) {
|
||||
return row && row.calc ? row.calc.finalAvailableQty : '-'
|
||||
},
|
||||
statusType(row) {
|
||||
return this.isShortage(row) ? 'danger' : 'success'
|
||||
},
|
||||
statusText(row) {
|
||||
return this.isShortage(row) ? '缺料' : '充足'
|
||||
},
|
||||
isShortage(row) {
|
||||
const s = row && row.status
|
||||
return s === 'SHORTAGE' || (s && s.name === 'SHORTAGE')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
611
src/views/system/materialDocHub/index.vue
Normal file
@ -0,0 +1,611 @@
|
||||
<template>
|
||||
<div class="app-container material-doc-hub">
|
||||
<div class="search-panel">
|
||||
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" label-width="76px">
|
||||
<el-form-item label="生产令号" prop="sclh">
|
||||
<el-input
|
||||
v-model="queryParams.sclh"
|
||||
placeholder="留空查全部,可输入令号筛选"
|
||||
clearable
|
||||
style="width: 280px"
|
||||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<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-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<el-card v-if="total > 0 || loaded" class="summary-card" shadow="never">
|
||||
<div class="summary-row">
|
||||
<span v-if="queryParams.sclh">令号:<b>{{ queryParams.sclh }}</b></span>
|
||||
<span v-else>范围:<b>全部工单</b></span>
|
||||
<el-tag size="mini">共 {{ total }} 条</el-tag>
|
||||
<el-tag size="mini" type="info">当前页 {{ projectList.length }} 条</el-tag>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
class="project-table"
|
||||
:data="projectList"
|
||||
row-key="projectId"
|
||||
ref="projectTable"
|
||||
border
|
||||
size="small"
|
||||
:empty-text="emptyTableText"
|
||||
>
|
||||
<el-table-column type="expand">
|
||||
<template slot-scope="projectScope">
|
||||
<div class="kanban-wrap">
|
||||
<div v-if="!projectScope.row._loaded && !projectScope.row._loading" class="kanban-load-bar">
|
||||
<span class="kanban-load-hint">共 {{ projectScope.row.processCount || 0 }} 道工序,点击下方按钮加载</span>
|
||||
<el-button type="primary" plain size="mini" @click="loadProjectDetail(projectScope.row)">加载工序计划</el-button>
|
||||
</div>
|
||||
<div v-else v-loading="projectScope.row._loading" class="kanban-body">
|
||||
<div v-if="!projectScope.row._loading && !(projectScope.row.processes || []).length" class="kanban-empty">
|
||||
暂无工序计划
|
||||
</div>
|
||||
<div v-else class="kanban-scroll">
|
||||
<div
|
||||
v-for="stage in projectScope.row.processes"
|
||||
:key="stage.stageKey"
|
||||
class="kanban-col"
|
||||
>
|
||||
<div class="kanban-col__head">
|
||||
<div class="kanban-col__head-main">
|
||||
<div class="kanban-col__title">
|
||||
<span class="kanban-col__oper">工序 {{ stage.operNo }}</span>
|
||||
{{ stage.processName || '-' }}
|
||||
</div>
|
||||
<div class="kanban-col__wc">{{ stage.workCenter || '工作中心未知' }}</div>
|
||||
</div>
|
||||
<el-button type="text" size="mini" class="kanban-col__action" @click.stop="openProcessDetail(projectScope.row, stage)">详情</el-button>
|
||||
</div>
|
||||
<div class="kanban-card" @click="openProcessDetail(projectScope.row, stage)">
|
||||
<div class="kanban-card__top">
|
||||
<div class="kanban-progress">
|
||||
<el-progress
|
||||
:percentage="stage.progressPct"
|
||||
type="circle"
|
||||
:width="52"
|
||||
:stroke-width="6"
|
||||
:color="stage.progressColor"
|
||||
:show-text="false"
|
||||
/>
|
||||
<span class="kanban-progress__text">{{ stage.progressPct }}%</span>
|
||||
</div>
|
||||
<div class="kanban-card__meta">
|
||||
<div class="kanban-kv"><span class="kanban-k">计划</span><span class="kanban-v">{{ displayVal(stage.planQty) }}</span></div>
|
||||
<div class="kanban-kv"><span class="kanban-k">转入</span><span class="kanban-v">{{ displayVal(stage.inQty) }}</span></div>
|
||||
<div class="kanban-kv"><span class="kanban-k">转出</span><span class="kanban-v">{{ displayVal(stage.transOutQty) }}</span></div>
|
||||
<div class="kanban-kv"><span class="kanban-k">汇报</span><span class="kanban-v">{{ displayVal(stage.reportQty) }}</span></div>
|
||||
<div class="kanban-kv">
|
||||
<span class="kanban-k">状态</span>
|
||||
<el-tag size="mini" effect="plain" :type="processStatusType(stage.materialStatus)">{{ stage.materialStatus || '-' }}</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kanban-card__bottom">
|
||||
<span class="kanban-time">{{ stage.operPlanStartTime || '-' }} ~ {{ stage.operPlanFinishTime || '-' }}</span>
|
||||
<span v-if="stage.delayDays" class="kanban-delay">逾期 {{ stage.delayDays }} 天</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column type="index" label="#" width="50" align="center" :index="indexMethod" />
|
||||
<el-table-column label="生产令号" prop="moOrderNo" min-width="130" show-overflow-tooltip />
|
||||
<el-table-column label="产品 / 工单" min-width="260" show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
<div class="proj-main">
|
||||
<div class="proj-title">
|
||||
<span class="proj-name">{{ scope.row.productName || '-' }}</span>
|
||||
<span class="proj-code">{{ scope.row.productCode || '-' }}</span>
|
||||
</div>
|
||||
<div class="proj-sub">
|
||||
<el-tag size="mini" type="info" effect="plain">MO {{ scope.row.moBillNo || '-' }}</el-tag>
|
||||
<el-tag size="mini" effect="plain">{{ scope.row.processCount || 0 }} 道工序</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="数量" prop="qty" width="80" align="center" />
|
||||
<el-table-column label="工序" min-width="120" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span v-if="scope.row.processCount" class="cell-link">{{ scope.row.processCount }} 道 · 展开查看</span>
|
||||
<span v-else class="cell-sub">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="当前阶段" min-width="100" show-overflow-tooltip>
|
||||
<template slot-scope="scope">{{ currentStageLabel(scope.row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="80" fixed="right" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" size="mini" @click="toggleRowExpand(scope.row)">展开</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination
|
||||
v-show="total > 0"
|
||||
:total="total"
|
||||
:page.sync="queryParams.pageNum"
|
||||
:limit.sync="queryParams.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
@pagination="getList"
|
||||
/>
|
||||
|
||||
<el-drawer class="hub-drawer" :visible.sync="drawerOpen" direction="btt" size="62%" append-to-body>
|
||||
<template slot="title">
|
||||
<div class="hub-drawer__title-line">
|
||||
<span class="hub-drawer__title-main">工序汇报</span>
|
||||
<span class="hub-drawer__title-kv">{{ processContext.moOrderNo }}</span>
|
||||
<span class="hub-drawer__title-kv">MO {{ processContext.moBillNo }}</span>
|
||||
<span class="hub-drawer__title-kv">工序 {{ processContext.operNo }} · {{ processContext.processName }}</span>
|
||||
<span class="hub-drawer__title-kv hub-drawer__title-product">{{ processContext.productName }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="hub-drawer__summary">
|
||||
<span>工序号 {{ processContext.operNo }}</span>
|
||||
<span>工作中心 {{ processContext.workCenter || '-' }}</span>
|
||||
<span>计划 {{ displayVal(processContext.planQty) }}</span>
|
||||
<span>汇报 {{ displayVal(processContext.reportQty) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="hub-drawer__actions">
|
||||
<el-button size="mini" type="primary" plain :loading="reportsLoading" @click="loadReports">加载工序汇报</el-button>
|
||||
<el-button size="mini" type="warning" plain :loading="instockLoading" @click="loadInstockQc">加载入库/质检</el-button>
|
||||
<el-button size="mini" type="success" plain :loading="subcontractLoading" @click="loadSubcontract">加载委外 C/G</el-button>
|
||||
</div>
|
||||
|
||||
<el-tabs v-model="drawerTab" type="card">
|
||||
<el-tab-pane label="工序数量" name="qty">
|
||||
<el-descriptions :column="3" border size="small">
|
||||
<el-descriptions-item label="转入数量">{{ displayVal(processContext.inQty) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="转出数量">{{ displayVal(processContext.transOutQty) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="汇报数量">{{ displayVal(processContext.reportQty) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="工废数量">{{ displayVal(processContext.scrapQty) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="工序状态">{{ processContext.operStatus || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="物料状态">{{ processContext.materialStatus || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="计划开始">{{ processContext.operPlanStartTime || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="计划完成">{{ processContext.operPlanFinishTime || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="逾期天数">{{ processContext.delayDays || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="'工序汇报 (' + reportList.length + ')'" name="reports">
|
||||
<el-table :data="reportList" size="mini" border height="calc(62vh - 220px)" empty-text="点击上方「加载工序汇报」查询">
|
||||
<el-table-column label="汇报单号" prop="FBillNo" min-width="130" show-overflow-tooltip />
|
||||
<el-table-column label="工序号" prop="FOperNumber" width="72" align="center" />
|
||||
<el-table-column label="工序说明" prop="FOperDescription" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="完工" prop="FFinishQty" width="72" align="center" />
|
||||
<el-table-column label="合格" prop="FQuaQty" width="72" align="center" />
|
||||
<el-table-column label="工废" prop="FProcessFailQty" width="72" align="center" />
|
||||
<el-table-column label="料废" prop="FMaterialFailQty" width="72" align="center" />
|
||||
<el-table-column label="单据日期" prop="FDate" width="100" show-overflow-tooltip />
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="'入库/质检 (' + instockList.length + ')'" name="instock">
|
||||
<el-table :data="instockList" size="mini" border height="calc(62vh - 220px)" empty-text="点击上方「加载入库/质检」查询(仅入库点工序)">
|
||||
<el-table-column label="汇报单号" prop="FBillNo" min-width="130" show-overflow-tooltip />
|
||||
<el-table-column label="合格" prop="FQuaQty" width="72" align="center" />
|
||||
<el-table-column label="质检状态" width="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag size="mini" :type="isNotInspected(scope.row) ? 'danger' : 'success'">
|
||||
{{ isNotInspected(scope.row) ? '未质检' : '已质检' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="生产入库单" prop="instockBillNo" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="入库状态" prop="instockDocumentStatus" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="提示" prop="instockAlertReason" min-width="160" show-overflow-tooltip />
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="'委外 C/G (' + transferList.length + ')'" name="transfer">
|
||||
<el-table :data="transferList" size="mini" border height="calc(62vh - 220px)" empty-text="点击上方「加载委外 C/G」查询">
|
||||
<el-table-column label="转移单号" prop="transferBillNo" min-width="130" show-overflow-tooltip />
|
||||
<el-table-column label="类型" width="80" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag size="mini" :type="scope.row.transferType === 'C' ? 'warning' : 'success'">
|
||||
{{ scope.row.transferType === 'C' ? '发出' : scope.row.transferType === 'G' ? '接收' : scope.row.transferType }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="转移数量" prop="operTransferQty" width="88" align="center" />
|
||||
<el-table-column label="合格数量" prop="operQualifiedQty" width="88" align="center" />
|
||||
<el-table-column label="工废" prop="operProFailQty" width="72" align="center" />
|
||||
<el-table-column label="料废" prop="operMatFailQty" width="72" align="center" />
|
||||
<el-table-column label="单据日期" width="110">
|
||||
<template slot-scope="scope">{{ formatDate(scope.row.billDate) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
queryMaterialDocHubBoardPage,
|
||||
queryMaterialDocHubBoardDetail,
|
||||
queryMaterialDocHubReports,
|
||||
queryMaterialDocHubInstockQc,
|
||||
queryMaterialDocHubSubcontract
|
||||
} from '@/api/system/materialDocHub'
|
||||
|
||||
export default {
|
||||
name: 'MaterialDocHub',
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
loaded: false,
|
||||
total: 0,
|
||||
projectList: [],
|
||||
queryParams: {
|
||||
sclh: undefined,
|
||||
pageNum: 1,
|
||||
pageSize: 20
|
||||
},
|
||||
drawerOpen: false,
|
||||
drawerTab: 'qty',
|
||||
processContext: {},
|
||||
reportList: [],
|
||||
instockList: [],
|
||||
transferList: [],
|
||||
reportsLoading: false,
|
||||
instockLoading: false,
|
||||
subcontractLoading: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
emptyTableText() {
|
||||
return this.loaded ? '暂无工序计划数据' : '请点击查询加载数据'
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.initFromRoute()
|
||||
},
|
||||
methods: {
|
||||
initFromRoute() {
|
||||
const q = (this.$route && this.$route.query) || {}
|
||||
const sclh = q.sclh || q.hbytSclh || q.productionOrderNo
|
||||
if (sclh) {
|
||||
this.queryParams.sclh = sclh
|
||||
}
|
||||
this.getList()
|
||||
},
|
||||
handleQuery() {
|
||||
this.queryParams.pageNum = 1
|
||||
this.getList()
|
||||
},
|
||||
resetQuery() {
|
||||
this.queryParams.sclh = undefined
|
||||
this.queryParams.pageNum = 1
|
||||
this.getList()
|
||||
},
|
||||
getList() {
|
||||
this.loading = true
|
||||
const params = {
|
||||
pageNum: this.queryParams.pageNum,
|
||||
pageSize: this.queryParams.pageSize
|
||||
}
|
||||
const sclh = (this.queryParams.sclh || '').trim()
|
||||
if (sclh) {
|
||||
params.sclh = sclh
|
||||
}
|
||||
queryMaterialDocHubBoardPage(params)
|
||||
.then(res => {
|
||||
const data = res || {}
|
||||
const rows = data.rows || []
|
||||
this.total = data.total || 0
|
||||
this.projectList = rows.map(r => this.mapProjectRow(r))
|
||||
this.loaded = true
|
||||
})
|
||||
.catch(() => {
|
||||
this.projectList = []
|
||||
this.total = 0
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
mapProjectRow(r) {
|
||||
return {
|
||||
projectId: r.projectId || (r.moBillNo + '@@' + r.materialNumber),
|
||||
moBillNo: r.moBillNo,
|
||||
moOrderNo: r.moOrderNo,
|
||||
productCode: r.materialNumber,
|
||||
productName: r.materialName,
|
||||
qty: r.operQty,
|
||||
processCount: r.processCount,
|
||||
processes: [],
|
||||
_loaded: false,
|
||||
_loading: false
|
||||
}
|
||||
},
|
||||
loadProjectDetail(row) {
|
||||
if (!row || row._loading || row._loaded) return
|
||||
row._loading = true
|
||||
queryMaterialDocHubBoardDetail(row.moBillNo, row.productCode)
|
||||
.then(res => {
|
||||
const data = (res && res.data !== undefined) ? res.data : res
|
||||
row.processes = this.buildProcesses(data.processes || [])
|
||||
row._loaded = true
|
||||
row.currentStage = this.resolveCurrentStage(row.processes)
|
||||
})
|
||||
.catch(() => {
|
||||
row.processes = []
|
||||
})
|
||||
.finally(() => {
|
||||
row._loading = false
|
||||
})
|
||||
},
|
||||
buildProcesses(rows) {
|
||||
const list = (rows || []).map(r => {
|
||||
const planQty = Number(r.operQty || 0)
|
||||
const transOut = Number(r.transOutQty || 0)
|
||||
const pct = planQty > 0 ? Math.round((transOut / planQty) * 100) : 0
|
||||
return {
|
||||
stageKey: `${r.operNumber}@@${r.processName}`,
|
||||
operNo: Number(r.operNumber),
|
||||
processName: r.processName,
|
||||
workCenter: r.workCenterName,
|
||||
planQty: r.operQty,
|
||||
inQty: r.transInQty,
|
||||
transOutQty: r.transOutQty,
|
||||
reportQty: r.reportQty,
|
||||
scrapQty: r.scrapQty,
|
||||
operStatus: r.operStatus,
|
||||
materialStatus: r.materialStatus,
|
||||
operPlanStartTime: r.operPlanStartTime2 || r.operPlanStartTime,
|
||||
operPlanFinishTime: r.operPlanFinishTime2 || r.operPlanFinishTime,
|
||||
delayDays: r.delayDays,
|
||||
progressPct: Math.max(0, Math.min(100, pct)),
|
||||
progressColor: pct >= 100 ? '#67c23a' : (pct > 0 ? '#7c3aed' : '#c0c4cc')
|
||||
}
|
||||
})
|
||||
return list.sort((a, b) => (a.operNo || 0) - (b.operNo || 0))
|
||||
},
|
||||
openProcessDetail(project, stage) {
|
||||
if (!stage) return
|
||||
this.processContext = {
|
||||
moOrderNo: project.moOrderNo,
|
||||
moBillNo: project.moBillNo,
|
||||
materialNumber: project.productCode,
|
||||
productName: project.productName,
|
||||
operNo: stage.operNo,
|
||||
processName: stage.processName,
|
||||
workCenter: stage.workCenter,
|
||||
planQty: stage.planQty,
|
||||
inQty: stage.inQty,
|
||||
transOutQty: stage.transOutQty,
|
||||
reportQty: stage.reportQty,
|
||||
scrapQty: stage.scrapQty,
|
||||
operStatus: stage.operStatus,
|
||||
materialStatus: stage.materialStatus,
|
||||
operPlanStartTime: stage.operPlanStartTime,
|
||||
operPlanFinishTime: stage.operPlanFinishTime,
|
||||
delayDays: stage.delayDays
|
||||
}
|
||||
this.reportList = []
|
||||
this.instockList = []
|
||||
this.transferList = []
|
||||
this.drawerTab = 'qty'
|
||||
this.drawerOpen = true
|
||||
},
|
||||
loadReports() {
|
||||
const ctx = this.processContext
|
||||
if (!ctx.moBillNo) return
|
||||
this.reportsLoading = true
|
||||
queryMaterialDocHubReports(ctx.moBillNo, ctx.materialNumber, String(ctx.operNo))
|
||||
.then(res => {
|
||||
this.reportList = (res && res.data !== undefined) ? res.data : (res || [])
|
||||
})
|
||||
.finally(() => { this.reportsLoading = false })
|
||||
},
|
||||
loadInstockQc() {
|
||||
const ctx = this.processContext
|
||||
if (!ctx.moBillNo) return
|
||||
this.instockLoading = true
|
||||
queryMaterialDocHubInstockQc(ctx.moBillNo, ctx.materialNumber, String(ctx.operNo))
|
||||
.then(res => {
|
||||
this.instockList = (res && res.data !== undefined) ? res.data : (res || [])
|
||||
})
|
||||
.finally(() => { this.instockLoading = false })
|
||||
},
|
||||
loadSubcontract() {
|
||||
const ctx = this.processContext
|
||||
if (!ctx.moBillNo) return
|
||||
this.subcontractLoading = true
|
||||
queryMaterialDocHubSubcontract(ctx.moBillNo)
|
||||
.then(res => {
|
||||
this.transferList = (res && res.data !== undefined) ? res.data : (res || [])
|
||||
})
|
||||
.finally(() => { this.subcontractLoading = false })
|
||||
},
|
||||
isNotInspected(row) {
|
||||
return this.isZero(row.FQuaQty) && this.isZero(row.FProcessFailQty)
|
||||
&& this.isZero(row.FMaterialFailQty) && this.isZero(row.FReworkQty)
|
||||
},
|
||||
isZero(v) {
|
||||
if (v == null || v === '') return true
|
||||
const n = Number(v)
|
||||
return !Number.isFinite(n) || n === 0
|
||||
},
|
||||
currentStageLabel(project) {
|
||||
if (project.currentStage) return project.currentStage
|
||||
if (project.processCount) return project.processCount + ' 道工序'
|
||||
return '-'
|
||||
},
|
||||
resolveCurrentStage(processes) {
|
||||
const stages = processes || []
|
||||
if (!stages.length) return '-'
|
||||
const cur = stages.find(s => this.processPhase(s.materialStatus) !== 'done') || stages[stages.length - 1]
|
||||
return cur && cur.processName ? cur.processName : '-'
|
||||
},
|
||||
indexMethod(index) {
|
||||
return (this.queryParams.pageNum - 1) * this.queryParams.pageSize + index + 1
|
||||
},
|
||||
toggleRowExpand(row) {
|
||||
if (this.$refs && this.$refs.projectTable) {
|
||||
this.$refs.projectTable.toggleRowExpansion(row)
|
||||
}
|
||||
},
|
||||
processPhase(status) {
|
||||
const s = (status || '').toString()
|
||||
if (s.includes('已转出')) return 'done'
|
||||
if (s.includes('未转出')) return 'active'
|
||||
return 'idle'
|
||||
},
|
||||
processStatusType(status) {
|
||||
const phase = this.processPhase(status)
|
||||
if (phase === 'done') return 'success'
|
||||
if (phase === 'active') return 'warning'
|
||||
return 'info'
|
||||
},
|
||||
displayVal(v) {
|
||||
return v == null || v === '' ? '-' : v
|
||||
},
|
||||
formatDate(d) {
|
||||
if (!d) return '-'
|
||||
if (typeof d === 'string') return d.substring(0, 10)
|
||||
try {
|
||||
return new Date(d).toISOString().substring(0, 10)
|
||||
} catch (e) {
|
||||
return '-'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.search-panel {
|
||||
margin-bottom: 12px;
|
||||
padding: 12px 14px;
|
||||
background: #fafafa;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.summary-card { margin-bottom: 12px; }
|
||||
.summary-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.project-table ::v-deep .el-table__expanded-cell {
|
||||
padding: 12px 16px;
|
||||
background: #fafbfc;
|
||||
}
|
||||
.proj-main { display: flex; flex-direction: column; gap: 4px; }
|
||||
.proj-title { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px; }
|
||||
.proj-name { font-weight: 600; color: #303133; }
|
||||
.proj-code { font-size: 12px; color: #909399; }
|
||||
.proj-sub { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.kanban-wrap { width: 100%; min-height: 40px; }
|
||||
.kanban-load-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
padding: 16px 0;
|
||||
}
|
||||
.kanban-load-hint { font-size: 12px; color: #909399; }
|
||||
.kanban-body { min-height: 40px; }
|
||||
.kanban-empty { padding: 10px 0; font-size: 12px; color: #c0c4cc; }
|
||||
.kanban-scroll { display: flex; gap: 10px; overflow-x: auto; padding-bottom: 6px; }
|
||||
.kanban-col { flex: 0 0 220px; background: #fff; border: 1px solid #ebeef5; border-radius: 6px; overflow: hidden; }
|
||||
.kanban-col__head { display: flex; align-items: flex-start; justify-content: space-between; padding: 8px 10px; background: #7c3aed; color: #fff; gap: 6px; }
|
||||
.kanban-col__head-main { flex: 1; min-width: 0; }
|
||||
.kanban-col__title { font-size: 13px; font-weight: 600; line-height: 1.3; }
|
||||
.kanban-col__oper {
|
||||
display: inline-block;
|
||||
margin-right: 4px;
|
||||
padding: 0 4px;
|
||||
border-radius: 3px;
|
||||
background: rgba(255,255,255,0.2);
|
||||
font-size: 11px;
|
||||
}
|
||||
.kanban-col__wc { margin-top: 2px; font-size: 11px; opacity: 0.9; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.kanban-col__action { color: rgba(255,255,255,0.9); padding: 0; flex-shrink: 0; }
|
||||
.kanban-card { padding: 10px; cursor: pointer; }
|
||||
.kanban-card__top { display: flex; gap: 10px; align-items: center; }
|
||||
.kanban-progress {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.kanban-progress ::v-deep .el-progress { line-height: 1; }
|
||||
.kanban-progress ::v-deep .el-progress-circle {
|
||||
width: 52px !important;
|
||||
height: 52px !important;
|
||||
}
|
||||
.kanban-progress__text {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #606266;
|
||||
line-height: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
.kanban-card__meta {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
justify-content: center;
|
||||
}
|
||||
.kanban-kv {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.kanban-k { color: #909399; flex-shrink: 0; }
|
||||
.kanban-v {
|
||||
color: #303133;
|
||||
font-variant-numeric: tabular-nums;
|
||||
text-align: right;
|
||||
min-width: 44px;
|
||||
}
|
||||
.kanban-card__bottom {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
font-size: 11px;
|
||||
color: #909399;
|
||||
}
|
||||
.kanban-delay { color: #f56c6c; }
|
||||
.cell-sub { font-size: 12px; color: #c0c4cc; }
|
||||
.cell-link { font-size: 12px; color: #409eff; }
|
||||
.hub-drawer ::v-deep .el-drawer__header { margin-bottom: 0; padding: 14px 16px; border-bottom: 1px solid #ebeef5; }
|
||||
.hub-drawer ::v-deep .el-drawer__body { padding: 12px 16px; }
|
||||
.hub-drawer__title-line { display: flex; align-items: center; gap: 12px; overflow: hidden; white-space: nowrap; }
|
||||
.hub-drawer__title-main { font-size: 16px; font-weight: 500; }
|
||||
.hub-drawer__title-kv { font-size: 13px; color: #909399; }
|
||||
.hub-drawer__title-product { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
||||
.hub-drawer__summary { display: flex; flex-wrap: wrap; gap: 16px; margin-bottom: 10px; font-size: 13px; color: #606266; }
|
||||
.hub-drawer__actions { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }
|
||||
</style>
|
||||
739
src/views/system/nd3PhasedProduct/index.vue
Normal file
@ -0,0 +1,739 @@
|
||||
<template>
|
||||
<div class="app-container nd3-phased-product">
|
||||
<!-- 顶部查询 -->
|
||||
<el-form ref="queryForm" :model="queryParams" size="small" :inline="true" label-width="90px">
|
||||
<el-form-item label="查询模式">
|
||||
<el-radio-group v-model="queryMode" size="small">
|
||||
<el-radio-button label="single">单主产品</el-radio-button>
|
||||
<el-radio-button label="multi">多主产品联合试算</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="项目检索" prop="keyword">
|
||||
<el-select
|
||||
v-model="queryParams.keyword"
|
||||
filterable
|
||||
remote
|
||||
reserve-keyword
|
||||
placeholder="请输入关键字检索"
|
||||
:remote-method="querySearch"
|
||||
:loading="searchLoading"
|
||||
clearable
|
||||
style="width: 260px"
|
||||
@change="handleSelect"
|
||||
>
|
||||
<el-option
|
||||
v-for="(item, index) in searchOptions"
|
||||
:key="index"
|
||||
:label="item.productionOrderNo ? (item.productionOrderNo + ' - ' + item.productName) : item.productName"
|
||||
:value="index"
|
||||
>
|
||||
<div style="line-height: 1.4; padding: 4px 0;">
|
||||
<div>{{ item.productionOrderNo || '无令号' }}</div>
|
||||
<div style="font-size: 12px; color: #999;">
|
||||
{{ item.productName }} <span v-if="item.productCode">({{ item.productCode }})</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="当前令号" prop="productionOrderNo">
|
||||
<el-input
|
||||
v-model="queryParams.productionOrderNo"
|
||||
placeholder="生产令号(与图号二选一)"
|
||||
clearable
|
||||
style="width: 180px"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="queryMode === 'single'" label="主产品图号" prop="productCode">
|
||||
<el-input
|
||||
v-model="queryParams.productCode"
|
||||
placeholder="主产品图号(与令号二选一)"
|
||||
clearable
|
||||
style="width: 180px"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="queryMode === 'single'" label="投产量" prop="inputQty">
|
||||
<el-input-number
|
||||
v-model="queryParams.inputQty"
|
||||
:min="1"
|
||||
placeholder="试算投产量"
|
||||
controls-position="right"
|
||||
style="width: 130px"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" size="mini" :loading="loading" @click="handleQuery">查询</el-button>
|
||||
<el-button
|
||||
v-if="queryMode === 'multi'"
|
||||
type="warning"
|
||||
plain
|
||||
icon="el-icon-download"
|
||||
size="mini"
|
||||
:loading="exportLoading"
|
||||
@click="handleExportCustomKit"
|
||||
>导出</el-button>
|
||||
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 多主产品投单量 -->
|
||||
<el-card v-if="queryMode === 'multi'" shadow="never" class="mb-20">
|
||||
<div slot="header" class="parent-header">
|
||||
<span style="font-weight: bold;">销售投单量(可添加多个相似 BOM 主产品)</span>
|
||||
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="addParentRow">添加主产品</el-button>
|
||||
</div>
|
||||
<el-table :data="parentInputs" border size="small">
|
||||
<el-table-column label="物料编码" min-width="180">
|
||||
<template slot-scope="scope">
|
||||
<el-input v-model="scope.row.materialCode" placeholder="如 LTBZF.0" clearable size="small" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="投单量" width="160" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-input-number v-model="scope.row.inputQty" :min="1" controls-position="right" size="small" style="width: 120px" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="80" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" size="mini" :disabled="parentInputs.length <= 1" @click="removeParentRow(scope.$index)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<div v-if="reportData && queryMode === 'single'" v-loading="loading">
|
||||
<div style="margin-bottom: 15px; font-size: 14px; color: #606266;">
|
||||
<strong>可生产套数 (BOM齐套):</strong>
|
||||
<span style="font-size: 18px; font-weight: bold; color: #E6A23C;">{{ reportData.maxBuildQty || 0 }}</span>
|
||||
</div>
|
||||
|
||||
<!-- BOM 头 -->
|
||||
<el-descriptions v-if="reportData.bomHeader" title="BOM" :column="3" border class="mb-20">
|
||||
<el-descriptions-item label="版本">{{ reportData.bomHeader.bomNumber }}</el-descriptions-item>
|
||||
<el-descriptions-item label="父项">{{ reportData.bomHeader.materialCode }}</el-descriptions-item>
|
||||
<el-descriptions-item label="名称">{{ reportData.bomHeader.materialName }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<!-- 主产品区 -->
|
||||
<el-card v-if="reportData.mainProduct" shadow="never" class="mb-20">
|
||||
<div slot="header" style="font-weight: bold; font-size: 15px;">
|
||||
主产品:{{ reportData.mainProduct.productName || '-' }} ({{ reportData.mainProduct.productCode || '-' }})
|
||||
</div>
|
||||
<el-space class="ml-4">
|
||||
<el-tag type="info" style="cursor: pointer" @click="openDrill('库存', reportData.mainProduct.stockDrillRows, reportData.mainProduct.productCode)">
|
||||
库存 {{ reportData.mainProduct.stockQty || 0 }}
|
||||
</el-tag>
|
||||
<el-tag type="success" style="cursor: pointer; margin-left: 10px;" @click="openDrill('发货', reportData.mainProduct.deliverDrillRows, reportData.mainProduct.productCode)">
|
||||
发货 {{ reportData.mainProduct.deliverQty || 0 }}
|
||||
</el-tag>
|
||||
<el-tag type="warning" style="cursor: pointer; margin-left: 10px;" @click="openDrill('在途', reportData.mainProduct.transitMoRows, reportData.mainProduct.productCode)">
|
||||
在途 {{ reportData.mainProduct.transitQty || 0 }}
|
||||
</el-tag>
|
||||
</el-space>
|
||||
</el-card>
|
||||
|
||||
<!-- 子项区 -->
|
||||
<el-tabs v-model="activeTab" type="border-card" class="mb-20">
|
||||
<el-tab-pane :label="`自制子项 (${reportData.subItems && reportData.subItems.selfMade ? reportData.subItems.selfMade.length : 0})`" name="selfMade">
|
||||
<el-table :data="reportData.subItems && reportData.subItems.selfMade ? reportData.subItems.selfMade : []" border size="small" max-height="600">
|
||||
<el-table-column prop="partCode" label="部件图号" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="partName" label="部件名称" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="materialCode" label="子项编码" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="materialName" label="子项名称" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="materialType" label="类型" width="70" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag size="mini" type="primary">{{ scope.row.materialType }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="requiredQty" label="单台用量" width="80" align="right" />
|
||||
<el-table-column prop="stockQty" label="库存" width="90" align="right">
|
||||
<template slot-scope="scope">
|
||||
<el-link type="primary" :underline="false" style="color: #606266; text-decoration: underline;" @click="openDrill('库存', scope.row.stockDrillRows, scope.row.materialCode)">
|
||||
{{ scope.row.stockQty || 0 }}
|
||||
</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="noPickedQty" label="未领料" width="90" align="right" />
|
||||
<el-table-column label="在途" align="center">
|
||||
<el-table-column prop="transitMoQty" label="MO在途" width="90" align="right">
|
||||
<template slot-scope="scope">
|
||||
<el-popover v-if="scope.row.transitMoRows && scope.row.transitMoRows.length > 0" trigger="hover" placement="top" width="250">
|
||||
<div style="max-height: 200px; overflow-y: auto;">
|
||||
<div style="font-weight: bold; margin-bottom: 5px;">生产在途明细:</div>
|
||||
<div v-for="(item, idx) in scope.row.transitMoRows" :key="idx" style="margin-top: 5px; font-size: 13px; border-bottom: 1px solid #ebeef5; padding-bottom: 4px;">
|
||||
令号:<span>{{ item.sclh || '(无令号)' }}</span><br>
|
||||
单号:<span style="color: #409EFF">{{ item.billNo || item.orderNo || '-' }}</span><br>
|
||||
未入库:<span style="color: #E6A23C">{{ item.notInstockQty != null ? item.notInstockQty : (item.qty || item.orderQty || 0) }}</span> |
|
||||
交期:{{ item.planFinishDate ? item.planFinishDate.substring(0, 10) : '-' }}
|
||||
</div>
|
||||
</div>
|
||||
<div slot="reference" style="cursor: pointer; color: #67c23a; text-decoration: underline;">
|
||||
{{ scope.row.transitMoQty || 0 }}
|
||||
</div>
|
||||
</el-popover>
|
||||
<span v-else>{{ scope.row.transitMoQty || 0 }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="transitPoQty" label="PO在途" width="90" align="right">
|
||||
<template slot-scope="scope">
|
||||
<el-popover v-if="scope.row.transitPoRows && scope.row.transitPoRows.length > 0" trigger="hover" placement="top" width="250">
|
||||
<div style="max-height: 200px; overflow-y: auto;">
|
||||
<div style="font-weight: bold; margin-bottom: 5px;">采购在途明细:</div>
|
||||
<div v-for="(item, idx) in scope.row.transitPoRows" :key="idx" style="margin-top: 5px; font-size: 13px; border-bottom: 1px solid #ebeef5; padding-bottom: 4px;">
|
||||
令号:<span>{{ item.sclh || '(无令号)' }}</span><br>
|
||||
单号:<span style="color: #409EFF">{{ item.billNo || item.orderNo || '-' }}</span><br>
|
||||
未入库:<span style="color: #E6A23C">{{ item.notInstockQty != null ? item.notInstockQty : (item.qty || item.orderQty || 0) }}</span> |
|
||||
交期:{{ item.planFinishDate ? item.planFinishDate.substring(0, 10) : '-' }}
|
||||
</div>
|
||||
</div>
|
||||
<div slot="reference" style="cursor: pointer; color: #409eff; text-decoration: underline;">
|
||||
{{ scope.row.transitPoQty || 0 }}
|
||||
</div>
|
||||
</el-popover>
|
||||
<span v-else>{{ scope.row.transitPoQty || 0 }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table-column>
|
||||
<el-table-column prop="expectedAvailableQty" label="预计可用" width="90" align="right">
|
||||
<template slot-scope="scope">
|
||||
<span :style="{ color: scope.row.expectedAvailableQty < 0 ? '#f56c6c' : '#67c23a', fontWeight: 'bold' }">
|
||||
{{ scope.row.expectedAvailableQty || 0 }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane :label="`外购子项 (${reportData.subItems && reportData.subItems.purchase ? reportData.subItems.purchase.length : 0})`" name="purchase">
|
||||
<el-table :data="reportData.subItems && reportData.subItems.purchase ? reportData.subItems.purchase : []" border size="small" max-height="600">
|
||||
<el-table-column prop="partCode" label="部件图号" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="partName" label="部件名称" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="materialCode" label="子项编码" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="materialName" label="子项名称" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="materialType" label="类型" width="70" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag size="mini" type="warning">{{ scope.row.materialType }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="requiredQty" label="单台用量" width="80" align="right" />
|
||||
<el-table-column prop="stockQty" label="库存" width="90" align="right">
|
||||
<template slot-scope="scope">
|
||||
<el-link type="primary" :underline="false" style="color: #606266; text-decoration: underline;" @click="openDrill('库存', scope.row.stockDrillRows, scope.row.materialCode)">
|
||||
{{ scope.row.stockQty || 0 }}
|
||||
</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="noPickedQty" label="未领料" width="90" align="right" />
|
||||
<el-table-column label="在途" align="center">
|
||||
<el-table-column prop="transitMoQty" label="MO在途" width="90" align="right">
|
||||
<template slot-scope="scope">
|
||||
<el-popover v-if="scope.row.transitMoRows && scope.row.transitMoRows.length > 0" trigger="hover" placement="top" width="250">
|
||||
<div style="max-height: 200px; overflow-y: auto;">
|
||||
<div style="font-weight: bold; margin-bottom: 5px;">生产在途明细:</div>
|
||||
<div v-for="(item, idx) in scope.row.transitMoRows" :key="idx" style="margin-top: 5px; font-size: 13px; border-bottom: 1px solid #ebeef5; padding-bottom: 4px;">
|
||||
令号:<span>{{ item.sclh || '(无令号)' }}</span><br>
|
||||
单号:<span style="color: #409EFF">{{ item.billNo || item.orderNo || '-' }}</span><br>
|
||||
未入库:<span style="color: #E6A23C">{{ item.notInstockQty != null ? item.notInstockQty : (item.qty || item.orderQty || 0) }}</span> |
|
||||
交期:{{ item.planFinishDate ? item.planFinishDate.substring(0, 10) : '-' }}
|
||||
</div>
|
||||
</div>
|
||||
<div slot="reference" style="cursor: pointer; color: #67c23a; text-decoration: underline;">
|
||||
{{ scope.row.transitMoQty || 0 }}
|
||||
</div>
|
||||
</el-popover>
|
||||
<span v-else>{{ scope.row.transitMoQty || 0 }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="transitPoQty" label="PO在途" width="90" align="right">
|
||||
<template slot-scope="scope">
|
||||
<el-popover v-if="scope.row.transitPoRows && scope.row.transitPoRows.length > 0" trigger="hover" placement="top" width="250">
|
||||
<div style="max-height: 200px; overflow-y: auto;">
|
||||
<div style="font-weight: bold; margin-bottom: 5px;">采购在途明细:</div>
|
||||
<div v-for="(item, idx) in scope.row.transitPoRows" :key="idx" style="margin-top: 5px; font-size: 13px; border-bottom: 1px solid #ebeef5; padding-bottom: 4px;">
|
||||
令号:<span>{{ item.sclh || '(无令号)' }}</span><br>
|
||||
单号:<span style="color: #409EFF">{{ item.billNo || item.orderNo || '-' }}</span><br>
|
||||
未入库:<span style="color: #E6A23C">{{ item.notInstockQty != null ? item.notInstockQty : (item.qty || item.orderQty || 0) }}</span> |
|
||||
交期:{{ item.planFinishDate ? item.planFinishDate.substring(0, 10) : '-' }}
|
||||
</div>
|
||||
</div>
|
||||
<div slot="reference" style="cursor: pointer; color: #409eff; text-decoration: underline;">
|
||||
{{ scope.row.transitPoQty || 0 }}
|
||||
</div>
|
||||
</el-popover>
|
||||
<span v-else>{{ scope.row.transitPoQty || 0 }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table-column>
|
||||
<el-table-column prop="expectedAvailableQty" label="预计可用" width="90" align="right">
|
||||
<template slot-scope="scope">
|
||||
<span :style="{ color: scope.row.expectedAvailableQty < 0 ? '#f56c6c' : '#67c23a', fontWeight: 'bold' }">
|
||||
{{ scope.row.expectedAvailableQty || 0 }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<!-- 投产量试算区 -->
|
||||
<el-card v-if="reportData.deployRows && reportData.deployRows.length > 0" shadow="never" class="mb-20">
|
||||
<div slot="header" style="font-weight: bold;">
|
||||
投产量试算
|
||||
<el-tag size="mini" type="success" style="margin-left: 10px;">试算数量: {{ queryParams.inputQty }}</el-tag>
|
||||
<span style="font-size: 12px; color: #909399; font-weight: normal; margin-left: 15px;">
|
||||
计算说明:<span style="color: #606266">需投数量 = 试算数量 × 单台用量</span> | <span style="color: #606266">投后余量 = 可用数量 - 需投数量</span> | <span style="color: #606266">仍须补投 = 投后余量 < 0 时的缺口</span>
|
||||
</span>
|
||||
</div>
|
||||
<el-table :data="reportData.deployRows" border size="small" max-height="400">
|
||||
<el-table-column prop="materialCode" label="物料编码" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="materialName" label="物料名称" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="needQty" label="需投数量" width="100" align="right" />
|
||||
<el-table-column label="投后余量" width="100" align="right">
|
||||
<template slot="header">
|
||||
投后余量
|
||||
<el-tooltip class="item" effect="dark" content="投后余量 = 可用数量 - 需投数量" placement="top">
|
||||
<i class="el-icon-question" style="color: #909399; margin-left: 2px;"></i>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<template slot-scope="scope">
|
||||
<span :style="{ color: getBalanceQty(scope.row) < 0 ? '#f56c6c' : '#67c23a', fontWeight: 'bold' }">
|
||||
{{ getBalanceQty(scope.row) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remainDeployQty" label="仍须补投" width="100" align="right">
|
||||
<template slot-scope="scope">
|
||||
<span :style="{ color: scope.row.remainDeployQty > 0 ? '#f56c6c' : 'inherit', fontWeight: scope.row.remainDeployQty > 0 ? 'bold' : 'normal' }">
|
||||
{{ scope.row.remainDeployQty || 0 }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 多主产品联合试算结果 -->
|
||||
<div v-if="reportData && queryMode === 'multi'" v-loading="loading">
|
||||
<el-card v-if="reportData.parentProducts && reportData.parentProducts.length" shadow="never" class="mb-20">
|
||||
<div slot="header" style="font-weight: bold;">主产品投单汇总</div>
|
||||
<el-table :data="reportData.parentProducts" border size="small">
|
||||
<el-table-column prop="materialCode" label="物料编码" min-width="160" />
|
||||
<el-table-column prop="materialName" label="物料名称" min-width="220" show-overflow-tooltip />
|
||||
<el-table-column prop="inputQty" label="投单量" width="100" align="right" />
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-card v-if="reportData.customKitRows && reportData.customKitRows.length" shadow="never" class="mb-20">
|
||||
<div slot="header">
|
||||
<span style="font-weight: bold;">联合齐套试算</span>
|
||||
<span style="font-size: 12px; color: #909399; font-weight: normal; margin-left: 15px;">
|
||||
本批需求 = Σ(各主产品单台用量 × 投单量) | 差值 = 本批需求 − 预计可用 | 本次投单 = max(0, 差值)
|
||||
</span>
|
||||
</div>
|
||||
<el-table :data="reportData.customKitRows" border size="small" max-height="650">
|
||||
<el-table-column type="index" label="序号" width="55" align="center" />
|
||||
<el-table-column prop="materialCode" label="子项编码" min-width="130" show-overflow-tooltip />
|
||||
<el-table-column prop="materialName" label="子项名称" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="materialType" label="类型" width="70" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag size="mini" :type="scope.row.materialType === '外购' ? 'warning' : 'primary'">{{ scope.row.materialType }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-for="parent in reportData.parentProducts"
|
||||
:key="parent.materialCode"
|
||||
:label="parent.materialCode + ' 单台用量'"
|
||||
width="115"
|
||||
align="right"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
{{ getParentUnitQty(scope.row, parent.materialCode) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="stockQty" label="库存" width="90" align="right">
|
||||
<template slot-scope="scope">
|
||||
<el-link type="primary" :underline="false" style="color: #606266; text-decoration: underline;" @click="openDrill('库存', scope.row.stockDrillRows, scope.row.materialCode)">
|
||||
{{ scope.row.stockQty || 0 }}
|
||||
</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="noPickedQty" label="未领料" width="90" align="right" />
|
||||
<el-table-column label="在途" align="center">
|
||||
<el-table-column prop="transitMoQty" label="MO在途" width="90" align="right">
|
||||
<template slot-scope="scope">
|
||||
<el-popover v-if="scope.row.transitMoRows && scope.row.transitMoRows.length > 0" trigger="hover" placement="top" width="250">
|
||||
<div style="max-height: 200px; overflow-y: auto;">
|
||||
<div style="font-weight: bold; margin-bottom: 5px;">生产在途明细:</div>
|
||||
<div v-for="(item, idx) in scope.row.transitMoRows" :key="idx" style="margin-top: 5px; font-size: 13px; border-bottom: 1px solid #ebeef5; padding-bottom: 4px;">
|
||||
令号:<span>{{ item.sclh || '(无令号)' }}</span><br>
|
||||
单号:<span style="color: #409EFF">{{ item.billNo || item.orderNo || '-' }}</span><br>
|
||||
未入库:<span style="color: #E6A23C">{{ item.notInstockQty != null ? item.notInstockQty : (item.qty || item.orderQty || 0) }}</span> |
|
||||
交期:{{ item.planFinishDate ? item.planFinishDate.substring(0, 10) : '-' }}
|
||||
</div>
|
||||
</div>
|
||||
<div slot="reference" style="cursor: pointer; color: #67c23a; text-decoration: underline;">
|
||||
{{ scope.row.transitMoQty || 0 }}
|
||||
</div>
|
||||
</el-popover>
|
||||
<span v-else>{{ scope.row.transitMoQty || 0 }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="transitPoQty" label="PO在途" width="90" align="right">
|
||||
<template slot-scope="scope">
|
||||
<el-popover v-if="scope.row.transitPoRows && scope.row.transitPoRows.length > 0" trigger="hover" placement="top" width="250">
|
||||
<div style="max-height: 200px; overflow-y: auto;">
|
||||
<div style="font-weight: bold; margin-bottom: 5px;">采购在途明细:</div>
|
||||
<div v-for="(item, idx) in scope.row.transitPoRows" :key="idx" style="margin-top: 5px; font-size: 13px; border-bottom: 1px solid #ebeef5; padding-bottom: 4px;">
|
||||
令号:<span>{{ item.sclh || '(无令号)' }}</span><br>
|
||||
单号:<span style="color: #409EFF">{{ item.billNo || item.orderNo || '-' }}</span><br>
|
||||
未入库:<span style="color: #E6A23C">{{ item.notInstockQty != null ? item.notInstockQty : (item.qty || item.orderQty || 0) }}</span> |
|
||||
交期:{{ item.planFinishDate ? item.planFinishDate.substring(0, 10) : '-' }}
|
||||
</div>
|
||||
</div>
|
||||
<div slot="reference" style="cursor: pointer; color: #409eff; text-decoration: underline;">
|
||||
{{ scope.row.transitPoQty || 0 }}
|
||||
</div>
|
||||
</el-popover>
|
||||
<span v-else>{{ scope.row.transitPoQty || 0 }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table-column>
|
||||
<el-table-column prop="expectedAvailableQty" label="预计可用" width="90" align="right">
|
||||
<template slot-scope="scope">
|
||||
<span :style="{ color: scope.row.expectedAvailableQty < 0 ? '#f56c6c' : '#67c23a', fontWeight: 'bold' }">
|
||||
{{ scope.row.expectedAvailableQty != null ? scope.row.expectedAvailableQty : (scope.row.availableQty || 0) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="batchNeedQty" label="本批需求数量" width="110" align="right">
|
||||
<template slot-scope="scope">
|
||||
<span style="font-weight: bold;">{{ scope.row.batchNeedQty || 0 }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="diffQty" label="差值" width="90" align="right">
|
||||
<template slot-scope="scope">
|
||||
<span :style="{ color: scope.row.diffQty < 0 ? '#67c23a' : (scope.row.diffQty > 0 ? '#f56c6c' : '#606266'), fontWeight: 'bold' }">
|
||||
{{ scope.row.diffQty || 0 }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="deployQty" label="本次投单量" width="100" align="right">
|
||||
<template slot-scope="scope">
|
||||
<span :style="{ color: scope.row.deployQty > 0 ? '#f56c6c' : 'inherit', fontWeight: scope.row.deployQty > 0 ? 'bold' : 'normal' }">
|
||||
{{ scope.row.deployQty || 0 }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<el-empty v-else-if="!loading" description="请输入查询条件并点击查询" />
|
||||
|
||||
<!-- 下钻弹窗 -->
|
||||
<el-dialog :title="drillTitle" :visible.sync="drillVisible" width="900px" append-to-body>
|
||||
<el-table :data="drillData" border size="small" max-height="500">
|
||||
<el-table-column prop="sclh" label="令号" min-width="150" show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
{{ scope.row.sclh || '(无令号)' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="billNo" label="单据号" min-width="130" show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
{{ scope.row.billNo || scope.row.orderNo || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sourceType" label="类型" width="80" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag size="mini" :type="getSourceTypeTag(scope.row.sourceType)">
|
||||
{{ scope.row.sourceType }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="数量" width="80" align="right">
|
||||
<template slot-scope="scope">
|
||||
{{ scope.row.notInstockQty !== null && scope.row.notInstockQty !== undefined ? scope.row.notInstockQty : (scope.row.qty || scope.row.orderQty || 0) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="instockQty" label="已入库" width="80" align="right">
|
||||
<template slot-scope="scope">
|
||||
{{ scope.row.instockQty != null ? scope.row.instockQty : '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="planStartDate" label="下达时间" width="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
{{ scope.row.planStartDate ? scope.row.planStartDate.substring(0, 10) : '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="planFinishDate" label="完成/交期" width="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
{{ scope.row.planFinishDate ? scope.row.planFinishDate.substring(0, 10) : '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { searchNd3Project, getNd3Report, getNd3CustomKit, exportNd3CustomKit } from '@/api/system/nd3PhasedProduct'
|
||||
import { blobValidate } from '@/utils/ruoyi'
|
||||
import { saveAs } from 'file-saver'
|
||||
|
||||
export default {
|
||||
name: 'Nd3PhasedProduct',
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
exportLoading: false,
|
||||
searchLoading: false,
|
||||
queryMode: 'single',
|
||||
searchOptions: [],
|
||||
parentInputs: [
|
||||
{ materialCode: '', inputQty: 1 },
|
||||
{ materialCode: '', inputQty: 1 },
|
||||
{ materialCode: '', inputQty: 1 }
|
||||
],
|
||||
queryParams: {
|
||||
keyword: undefined,
|
||||
productionOrderNo: undefined,
|
||||
productCode: undefined,
|
||||
inputQty: undefined
|
||||
},
|
||||
reportData: null,
|
||||
activeTab: 'selfMade',
|
||||
|
||||
// 下钻弹窗
|
||||
drillVisible: false,
|
||||
drillTitle: '',
|
||||
drillData: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 自动补全搜索
|
||||
querySearch(query) {
|
||||
if (query !== '') {
|
||||
this.searchLoading = true
|
||||
searchNd3Project({ keyword: query }).then(res => {
|
||||
this.searchOptions = Array.isArray(res.data) ? res.data : []
|
||||
}).catch(() => {
|
||||
this.searchOptions = []
|
||||
}).finally(() => {
|
||||
this.searchLoading = false
|
||||
})
|
||||
} else {
|
||||
this.searchOptions = []
|
||||
}
|
||||
},
|
||||
// 选择搜索结果
|
||||
handleSelect(index) {
|
||||
if (index !== undefined && index !== null && this.searchOptions[index]) {
|
||||
const item = this.searchOptions[index]
|
||||
this.queryParams.productionOrderNo = item.productionOrderNo
|
||||
this.queryParams.productCode = item.productCode
|
||||
}
|
||||
},
|
||||
// 提交查询
|
||||
handleQuery() {
|
||||
if (this.queryMode === 'multi') {
|
||||
this.handleCustomKitQuery()
|
||||
return
|
||||
}
|
||||
const { productionOrderNo, productCode, inputQty } = this.queryParams
|
||||
if (!productionOrderNo && !productCode) {
|
||||
this.$modal.msgWarning('请填写主产品图号或生产令号')
|
||||
return
|
||||
}
|
||||
this.loading = true
|
||||
|
||||
const params = {}
|
||||
if (productionOrderNo) {
|
||||
params.productionOrderNo = productionOrderNo
|
||||
}
|
||||
if (productCode) {
|
||||
params.productCode = productCode
|
||||
// 兼容后端强制要求 materialCode 的情况
|
||||
params.materialCode = productCode
|
||||
}
|
||||
if (inputQty > 0) {
|
||||
params.inputQty = inputQty
|
||||
}
|
||||
|
||||
getNd3Report(params).then(res => {
|
||||
this.reportData = res.data || null
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
handleCustomKitQuery() {
|
||||
const payload = this.buildCustomKitPayload()
|
||||
if (!payload) {
|
||||
return
|
||||
}
|
||||
this.loading = true
|
||||
getNd3CustomKit(payload).then(res => {
|
||||
this.reportData = res.data || null
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
buildCustomKitPayload() {
|
||||
const parents = this.parentInputs
|
||||
.filter(p => p.materialCode && String(p.materialCode).trim() && p.inputQty > 0)
|
||||
.map(p => ({
|
||||
materialCode: String(p.materialCode).trim(),
|
||||
inputQty: p.inputQty
|
||||
}))
|
||||
if (parents.length === 0) {
|
||||
this.$modal.msgWarning('请至少填写一个主产品编码及投单量')
|
||||
return null
|
||||
}
|
||||
const payload = { parents }
|
||||
if (this.queryParams.productionOrderNo) {
|
||||
payload.productionOrderNo = this.queryParams.productionOrderNo
|
||||
}
|
||||
return payload
|
||||
},
|
||||
handleExportCustomKit() {
|
||||
const payload = this.buildCustomKitPayload()
|
||||
if (!payload) {
|
||||
return
|
||||
}
|
||||
this.exportLoading = true
|
||||
exportNd3CustomKit(payload).then(async (data) => {
|
||||
const isBlob = blobValidate(data)
|
||||
if (isBlob) {
|
||||
saveAs(new Blob([data]), `多主产品联合齐套试算_${new Date().getTime()}.xlsx`)
|
||||
} else {
|
||||
const resText = await data.text()
|
||||
const rspObj = JSON.parse(resText)
|
||||
this.$modal.msgError(rspObj.msg || '导出失败')
|
||||
}
|
||||
}).catch(() => {
|
||||
this.$modal.msgError('导出失败,请稍后重试')
|
||||
}).finally(() => {
|
||||
this.exportLoading = false
|
||||
})
|
||||
},
|
||||
addParentRow() {
|
||||
this.parentInputs.push({ materialCode: '', inputQty: 1 })
|
||||
},
|
||||
removeParentRow(index) {
|
||||
if (this.parentInputs.length <= 1) return
|
||||
this.parentInputs.splice(index, 1)
|
||||
},
|
||||
getParentUnitQty(row, parentMaterialCode) {
|
||||
const list = row.parentUnitQtys || []
|
||||
const hit = list.find(u => u.parentMaterialCode === parentMaterialCode)
|
||||
return hit && hit.unitQty != null ? hit.unitQty : 0
|
||||
},
|
||||
// 重置
|
||||
resetQuery() {
|
||||
this.queryMode = 'single'
|
||||
this.parentInputs = [
|
||||
{ materialCode: '', inputQty: 1 },
|
||||
{ materialCode: '', inputQty: 1 },
|
||||
{ materialCode: '', inputQty: 1 }
|
||||
]
|
||||
this.queryParams = {
|
||||
keyword: undefined,
|
||||
productionOrderNo: undefined,
|
||||
materialCode: undefined,
|
||||
inputQty: undefined
|
||||
}
|
||||
this.searchOptions = []
|
||||
this.reportData = null
|
||||
this.activeTab = 'selfMade'
|
||||
},
|
||||
// 打开下钻弹窗
|
||||
openDrill(typeLabel, rows, code) {
|
||||
this.drillData = rows || []
|
||||
// 根据点击类型补充 sourceType (例如:MO在途、PO在途的明细里可能没有带这个字段)
|
||||
if (this.drillData.length > 0) {
|
||||
this.drillData.forEach(row => {
|
||||
if (!row.sourceType) {
|
||||
if (typeLabel.includes('MO')) {
|
||||
row.sourceType = 'MO'
|
||||
} else if (typeLabel.includes('PO')) {
|
||||
row.sourceType = 'PO'
|
||||
} else if (typeLabel.includes('发货')) {
|
||||
row.sourceType = 'DELIVER'
|
||||
} else {
|
||||
row.sourceType = 'STOCK'
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
this.drillTitle = `${typeLabel}明细 - ${code || ''}`
|
||||
this.drillVisible = true
|
||||
},
|
||||
// 获取行标签颜色
|
||||
getSourceTypeTag(type) {
|
||||
const map = {
|
||||
'STOCK': 'info',
|
||||
'DELIVER': 'success',
|
||||
'WIP': 'primary',
|
||||
'MO': 'primary',
|
||||
'PO': 'warning'
|
||||
}
|
||||
return map[type] || 'default'
|
||||
},
|
||||
// 计算投后余量
|
||||
getBalanceQty(row) {
|
||||
if (row.balanceQty != null) return row.balanceQty
|
||||
if (row.remainStockQty != null) return row.remainStockQty
|
||||
return (row.availableQty || 0) - (row.needQty || 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.nd3-phased-product .mb-20 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* 主产品 KPI 卡片样式 */
|
||||
.kpi-card {
|
||||
padding: 12px 15px;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
border: 1px solid #ebeef5;
|
||||
}
|
||||
.kpi-card.kpi-stock {
|
||||
border-left: 4px solid #909399;
|
||||
background-color: #f4f4f5;
|
||||
}
|
||||
.kpi-card.kpi-deliver {
|
||||
border-left: 4px solid #67c23a;
|
||||
background-color: #f0f9eb;
|
||||
}
|
||||
.kpi-card.kpi-wip {
|
||||
border-left: 4px solid #409eff;
|
||||
background-color: #ecf5ff;
|
||||
}
|
||||
|
||||
.kpi-label {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.kpi-value {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.kpi-value ::v-deep .el-link {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.parent-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
||||
570
src/views/system/operationPlanningMain/CapacityAnalysis.vue
Normal file
@ -0,0 +1,570 @@
|
||||
<template>
|
||||
<div class="capacity-analysis">
|
||||
<!-- 搜索区域 -->
|
||||
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" label-width="100px">
|
||||
<el-form-item label="生产令号" prop="productionOrderNo" v-hasPermi="['system:plan:productionOrderNoOptions']">
|
||||
<el-select
|
||||
v-model="queryParams.productionOrderNo"
|
||||
filterable
|
||||
remote
|
||||
reserve-keyword
|
||||
placeholder="请输入生产令号模糊查询"
|
||||
:remote-method="searchProductionOrder"
|
||||
:loading="searchingOptions"
|
||||
@change="handleOrderSelect"
|
||||
style="width: 280px"
|
||||
clearable
|
||||
>
|
||||
<el-option
|
||||
v-for="(item, index) in productionOptions"
|
||||
:key="index"
|
||||
:label="item"
|
||||
:value="item"
|
||||
>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="主产品图号" prop="productCode">
|
||||
<el-input
|
||||
v-model="queryParams.productCode"
|
||||
placeholder="自动带出"
|
||||
disabled
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="主产品名称" prop="productName">
|
||||
<el-input
|
||||
v-model="queryParams.productName"
|
||||
placeholder="自动带出"
|
||||
disabled
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" size="mini" v-hasPermi="['system:material:buildCapacity']" @click="handleQuery">分析</el-button>
|
||||
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||
<el-button
|
||||
type="warning"
|
||||
icon="el-icon-download"
|
||||
size="mini"
|
||||
:disabled="!hasSearched || !analysisData"
|
||||
v-hasPermi="['system:material:exportBuildCapacity']"
|
||||
@click="handleExport"
|
||||
>导出</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div v-if="loading" class="loading-wrapper">
|
||||
<el-spinner />
|
||||
<span class="loading-text">正在分析预配套数据,请稍候...</span>
|
||||
</div>
|
||||
|
||||
<!-- 分析结果区域 -->
|
||||
<template v-else-if="analysisData && (analysisData.selfMadeMaterials || analysisData.purchaseMaterials)">
|
||||
<!-- 顶部卡片 -->
|
||||
<el-card class="top-card" shadow="never">
|
||||
<div class="header-info">
|
||||
<div class="info-item">
|
||||
<span class="label">产品:</span>
|
||||
<span class="value">{{ analysisData.productName }} ({{ analysisData.productCode }})</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">可生产:</span>
|
||||
<span class="value highlight">{{ analysisData.maxBuildQty }} 台</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 自制件表格 -->
|
||||
<el-card class="table-card" shadow="never" header="自制件">
|
||||
<el-table
|
||||
:data="analysisData.selfMadeMaterials || []"
|
||||
border
|
||||
style="width: 100%"
|
||||
:header-cell-style="{ background: '#f0f2f5', color: '#303133', fontWeight: 'bold' }"
|
||||
>
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
|
||||
<el-table-column prop="materialCode" label="物料编码" width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="materialName" label="物料名称" min-width="160" show-overflow-tooltip />
|
||||
|
||||
<!-- 物料基本属性组 -->
|
||||
<el-table-column label="物料属性" align="center">
|
||||
<el-table-column prop="material" label="材质" width="100" align="center" show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
<span>{{ scope.row.material || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="singleWeight" label="单重(kg)" width="80" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ scope.row.singleWeight != null ? scope.row.singleWeight : '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="requiredQty" label="单台用量" width="80" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ scope.row.requiredQty != null ? scope.row.requiredQty : '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="totalWeight" label="总重(kg)" width="80" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ scope.row.totalWeight != null ? scope.row.totalWeight : '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 库存信息组 -->
|
||||
<el-table-column label="库存信息" align="center">
|
||||
<el-table-column prop="stock.onHandQty" label="即时库存" width="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ scope.row.stock ? scope.row.stock.onHandQty : '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="stock.poNotInQty" label="采购未入库" width="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ scope.row.stock ? scope.row.stock.poNotInQty : '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="stock.moNotInQty" label="生产未入库" width="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ scope.row.stock ? scope.row.stock.moNotInQty : '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="stock.moNotPickQty" label="未领料(占用)" width="110" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ scope.row.stock ? scope.row.stock.moNotPickQty : '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 计算结果组 -->
|
||||
<el-table-column label="计算" align="center">
|
||||
<el-table-column prop="calc.finalAvailableQty" label="最终可用" width="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span class="calc-value">{{ scope.row.calc ? scope.row.calc.finalAvailableQty : '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="calc.canSupportQty" label="可生产数" width="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span class="calc-value highlight">{{ scope.row.calc ? scope.row.calc.canSupportQty : '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 状态列 -->
|
||||
<el-table-column prop="status" label="状态" width="100" align="center" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.status" :type="getStatusTagType(scope.row.status)" effect="plain" size="small">
|
||||
{{ formatStatus(scope.row.status) }}
|
||||
</el-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- 采购件表格 -->
|
||||
<el-card class="table-card" shadow="never" header="采购件">
|
||||
<el-table
|
||||
:data="analysisData.purchaseMaterials || []"
|
||||
border
|
||||
style="width: 100%"
|
||||
:header-cell-style="{ background: '#f0f2f5', color: '#303133', fontWeight: 'bold' }"
|
||||
>
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
|
||||
<el-table-column prop="materialCode" label="物料编码" width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="materialName" label="物料名称" min-width="160" show-overflow-tooltip />
|
||||
|
||||
<!-- 物料基本属性组 -->
|
||||
<el-table-column label="物料属性" align="center">
|
||||
<el-table-column prop="material" label="材质" width="100" align="center" show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
<span>{{ scope.row.material || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="singleWeight" label="单重(kg)" width="80" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ scope.row.singleWeight != null ? scope.row.singleWeight : '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="requiredQty" label="单台用量" width="80" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ scope.row.requiredQty != null ? scope.row.requiredQty : '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="totalWeight" label="总重(kg)" width="80" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ scope.row.totalWeight != null ? scope.row.totalWeight : '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 库存信息组 -->
|
||||
<el-table-column label="库存信息" align="center">
|
||||
<el-table-column prop="stock.onHandQty" label="即时库存" width="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ scope.row.stock ? scope.row.stock.onHandQty : '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="stock.poNotInQty" label="采购未入库" width="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ scope.row.stock ? scope.row.stock.poNotInQty : '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="stock.moNotInQty" label="生产未入库" width="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ scope.row.stock ? scope.row.stock.moNotInQty : '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="stock.moNotPickQty" label="未领料(占用)" width="110" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ scope.row.stock ? scope.row.stock.moNotPickQty : '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 计算结果组 -->
|
||||
<el-table-column label="计算" align="center">
|
||||
<el-table-column prop="calc.finalAvailableQty" label="最终可用" width="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span class="calc-value">{{ scope.row.calc ? scope.row.calc.finalAvailableQty : '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="calc.canSupportQty" label="可生产数" width="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span class="calc-value highlight">{{ scope.row.calc ? scope.row.calc.canSupportQty : '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 状态列 -->
|
||||
<el-table-column prop="status" label="状态" width="100" align="center" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.status" :type="getStatusTagType(scope.row.status)" effect="plain" size="small">
|
||||
{{ formatStatus(scope.row.status) }}
|
||||
</el-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</template>
|
||||
<el-empty v-else-if="!loading && hasSearched" description="暂无产能分析数据" />
|
||||
|
||||
<!-- 多产品选择弹窗 -->
|
||||
<el-dialog title="选择主产品" :visible.sync="productSelectVisible" width="800px" append-to-body>
|
||||
<el-table :data="productOptions" border style="width: 100%" v-loading="productSelectLoading">
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
<el-table-column prop="parentDrawingNo" label="主产品图号" min-width="150" />
|
||||
<el-table-column prop="parentPart" label="主产品名称" min-width="150" />
|
||||
<el-table-column prop="drawingNo" label="组件图号" min-width="150" />
|
||||
<el-table-column prop="quantity" label="数量" width="80" align="center" />
|
||||
<el-table-column label="操作" width="100" align="center" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" size="mini" @click="confirmProductSelect(scope.row)">选择</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-dialog>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getCapacityAnalysisApi, selectByProCode, getProductionOrderNoOptions } from "@/api/system/operationPlanningMain";
|
||||
|
||||
export default {
|
||||
name: 'CapacityAnalysis',
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
hasSearched: false,
|
||||
queryParams: {
|
||||
productionOrderNo: '',
|
||||
productCode: '',
|
||||
productName: ''
|
||||
},
|
||||
analysisData: null,
|
||||
// 下拉框数据
|
||||
productionOptions: [],
|
||||
searchingOptions: false,
|
||||
|
||||
// 选中令号后的关联图号列表对话框
|
||||
productSelectVisible: false,
|
||||
productSelectLoading: false,
|
||||
productOptions: []
|
||||
};
|
||||
},
|
||||
created() {
|
||||
// 初始加载生产令号下拉列表
|
||||
this.searchProductionOrder('');
|
||||
},
|
||||
methods: {
|
||||
searchProductionOrder(query) {
|
||||
this.searchingOptions = true;
|
||||
getProductionOrderNoOptions({ keyword: query, limit: 200 }).then(res => {
|
||||
this.searchingOptions = false;
|
||||
if (res.code === 200) {
|
||||
// 后端返回的是 List<String>
|
||||
this.productionOptions = res.data || [];
|
||||
} else {
|
||||
this.productionOptions = [];
|
||||
}
|
||||
}).catch(() => {
|
||||
this.searchingOptions = false;
|
||||
this.productionOptions = [];
|
||||
});
|
||||
},
|
||||
handleOrderSelect(val) {
|
||||
if (!val) {
|
||||
this.queryParams.productCode = '';
|
||||
this.queryParams.productName = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// 选中生产令号后,去后端查询对应的产品信息列表
|
||||
this.productSelectLoading = true;
|
||||
selectByProCode(val).then(res => {
|
||||
this.productSelectLoading = false;
|
||||
if (res.code === 200 && res.data && res.data.length > 0) {
|
||||
if (res.data.length === 1) {
|
||||
// 只有一个产品直接带出
|
||||
this.queryParams.productCode = res.data[0].parentDrawingNo;
|
||||
this.queryParams.productName = res.data[0].parentPart;
|
||||
} else {
|
||||
// 多个产品,弹窗让用户选
|
||||
this.productOptions = res.data;
|
||||
this.productSelectVisible = true;
|
||||
}
|
||||
} else {
|
||||
this.$message.warning('该生产令号未查到关联产品');
|
||||
this.queryParams.productCode = '';
|
||||
this.queryParams.productName = '';
|
||||
}
|
||||
}).catch(() => {
|
||||
this.productSelectLoading = false;
|
||||
this.$message.error('获取关联产品失败');
|
||||
});
|
||||
},
|
||||
confirmProductSelect(row) {
|
||||
this.queryParams.productCode = row.parentDrawingNo;
|
||||
this.queryParams.productName = row.parentPart;
|
||||
this.productSelectVisible = false;
|
||||
},
|
||||
handleQuery() {
|
||||
if (!this.queryParams.productionOrderNo) {
|
||||
this.$message.warning('请输入生产令号');
|
||||
return;
|
||||
}
|
||||
if (!this.queryParams.productCode) {
|
||||
this.$message.warning('请输入主产品图号');
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
this.hasSearched = true;
|
||||
|
||||
getCapacityAnalysisApi(this.queryParams).then(res => {
|
||||
if (res.code === 200) {
|
||||
this.analysisData = res.data || res;
|
||||
} else {
|
||||
this.$message.error(res.msg || '产能分析查询失败');
|
||||
}
|
||||
}).catch(() => {
|
||||
this.$message.error('产能分析请求异常');
|
||||
}).finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
handleExport() {
|
||||
this.$confirm('是否确认导出产能分析数据项?', "警告", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
}).then(() => {
|
||||
this.download('/system/material/buildCapacity/export', {
|
||||
...this.queryParams
|
||||
}, `capacity_${new Date().getTime()}.xlsx`);
|
||||
}).catch(() => {});
|
||||
},
|
||||
resetQuery() {
|
||||
this.queryParams = {
|
||||
productionOrderNo: '',
|
||||
productCode: '',
|
||||
productName: ''
|
||||
};
|
||||
this.analysisData = null;
|
||||
this.hasSearched = false;
|
||||
},
|
||||
buildTree(list) {
|
||||
const map = {};
|
||||
const roots = [];
|
||||
|
||||
// 先将所有节点放入 map 中
|
||||
list.forEach(item => {
|
||||
map[item.rowId] = { ...item, children: [] };
|
||||
});
|
||||
|
||||
// 构建树结构
|
||||
list.forEach(item => {
|
||||
const node = map[item.rowId];
|
||||
// 如果 parentRowId 为 null 或者在 map 中找不到其父节点,则视为根节点
|
||||
if (item.parentRowId === null || !map[item.parentRowId]) {
|
||||
roots.push(node);
|
||||
} else {
|
||||
map[item.parentRowId].children.push(node);
|
||||
}
|
||||
});
|
||||
|
||||
// 清理空的 children 数组,避免展开箭头错乱
|
||||
const cleanEmptyChildren = (nodes) => {
|
||||
nodes.forEach(node => {
|
||||
if (node.children.length === 0) {
|
||||
delete node.children;
|
||||
} else {
|
||||
cleanEmptyChildren(node.children);
|
||||
}
|
||||
});
|
||||
};
|
||||
cleanEmptyChildren(roots);
|
||||
|
||||
return roots;
|
||||
},
|
||||
// 为瓶颈行添加红色高亮 class
|
||||
tableRowClassName({ row }) {
|
||||
return row.bottleneck ? 'bottleneck-row' : '';
|
||||
},
|
||||
// 根据状态返回 Element UI 标签颜色类型
|
||||
getStatusTagType(status) {
|
||||
if (status === 'SHORTAGE') return 'danger'; // 红色
|
||||
if (status === 'WARNING') return 'warning'; // 橙色
|
||||
return 'success'; // 绿色
|
||||
},
|
||||
// 将英文状态格式化为中文
|
||||
formatStatus(status) {
|
||||
if (status === 'SHORTAGE') return '短缺';
|
||||
if (status === 'WARNING') return '预警';
|
||||
if (status === 'OK') return '正常';
|
||||
return status;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.capacity-analysis {
|
||||
padding: 16px;
|
||||
background-color: #f5f7fa; /* 工业风浅灰背景 */
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.top-card {
|
||||
margin-bottom: 16px;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.header-info {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.info-item .label {
|
||||
color: #606266;
|
||||
margin-right: 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.info-item .value {
|
||||
color: #303133;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.info-item .value.highlight {
|
||||
color: #409EFF;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.bottleneck-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tags {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.bottleneck-tag {
|
||||
font-size: 14px;
|
||||
padding: 0 12px;
|
||||
height: 32px;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.no-bottleneck {
|
||||
color: #67c23a;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 表格标题列及基础边框工业风微调 */
|
||||
::v-deep .el-table {
|
||||
border: 1px solid #e4e7ed;
|
||||
}
|
||||
|
||||
.bottleneck-icon {
|
||||
margin-right: 6px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: #F56C6C;
|
||||
}
|
||||
|
||||
/* 计算字段强调 */
|
||||
.calc-value {
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.calc-value.highlight {
|
||||
color: #409EFF;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.loading-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 200px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
margin-top: 15px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 瓶颈行整行标红的背景覆盖 */
|
||||
::v-deep .el-table .bottleneck-row {
|
||||
background-color: #fef0f0 !important;
|
||||
}
|
||||
|
||||
/* 鼠标悬停在瓶颈行时加深红色 */
|
||||
::v-deep .el-table .bottleneck-row:hover > td {
|
||||
background-color: #fde2e2 !important;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<div class="capacity-analysis-page">
|
||||
<div v-if="loading" class="loading-wrapper">
|
||||
<el-spinner />
|
||||
<span class="loading-text">正在分析预配套数据,请稍候...</span>
|
||||
</div>
|
||||
<capacity-analysis v-else-if="capacityAnalysisData" :analysis-data="capacityAnalysisData" />
|
||||
<el-empty v-else description="暂无产能分析数据" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CapacityAnalysis from "./CapacityAnalysis.vue";
|
||||
import { getCapacityAnalysisApi } from "@/api/system/operationPlanningMain";
|
||||
|
||||
export default {
|
||||
name: "CapacityAnalysisPage",
|
||||
components: {
|
||||
CapacityAnalysis
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
capacityAnalysisData: null
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.fetchCapacityData();
|
||||
},
|
||||
methods: {
|
||||
fetchCapacityData() {
|
||||
// 获取通过路由传递过来的参数
|
||||
const { productionOrderNo, productCode, productName } = this.$route.query;
|
||||
|
||||
if (!productionOrderNo) {
|
||||
this.loading = false;
|
||||
this.$message.error('未获取到生产订单号');
|
||||
return;
|
||||
}
|
||||
if (!productCode) {
|
||||
this.loading = false;
|
||||
this.$message.error('未获取到产品编码');
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
|
||||
// 调用后端真实接口
|
||||
getCapacityAnalysisApi({ productionOrderNo, productCode, productName }).then(res => {
|
||||
// 如果后端返回的外层包装是 res.data
|
||||
if (res.code === 200) {
|
||||
this.capacityAnalysisData = res.data || res;
|
||||
} else {
|
||||
this.$message.error(res.msg || '产能分析查询失败');
|
||||
}
|
||||
}).catch(() => {
|
||||
this.$message.error('产能分析请求异常');
|
||||
}).finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.capacity-analysis-page {
|
||||
height: calc(100vh - 84px); /* 减去顶部导航栏高度 */
|
||||
overflow-y: auto;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.loading-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
margin-top: 15px;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
@ -106,7 +106,7 @@
|
||||
<el-table-column label="上传图纸" align="center" prop="uploadDrawing"/>
|
||||
|
||||
<el-table-column label="图纸预览" align="center" prop="drawingPreview" width="200">
|
||||
<template scope="scope">
|
||||
<template slot-scope="scope">
|
||||
<ImagePreview v-if="previewListResource && checkFileSuffix(scope.row.drawingPreview)" :width=100 :height=100
|
||||
:src="scope.row.drawingPreview" :preview-src-list="[scope.row.drawingPreview]"/>
|
||||
<span v-text="scope.row.drawingPreview"
|
||||
|
||||
706
src/views/system/plmPdfZipPush/index.vue
Normal file
@ -0,0 +1,706 @@
|
||||
<template>
|
||||
<div class="plm-push-page app-container">
|
||||
<section class="page-hero">
|
||||
<div class="page-hero__main">
|
||||
<div class="page-hero__icon">
|
||||
<i class="el-icon-folder-opened"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="page-hero__title">PLM 图纸推送接收记录</h1>
|
||||
<p class="page-hero__desc">追踪 PLM 系统推送的 PDF/DWG 接收情况,查看归档目录与图纸明细。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-hero__stats">
|
||||
<div class="stat-chip">
|
||||
<span class="stat-chip__label">当前页记录</span>
|
||||
<span class="stat-chip__value">{{ pushList.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">{{ pageItemCount }}</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="archiveName">
|
||||
<el-input
|
||||
v-model="queryParams.archiveName"
|
||||
placeholder="模糊匹配压缩包名"
|
||||
clearable
|
||||
style="width: 220px"
|
||||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
</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="pushList"
|
||||
:header-cell-style="headerCellStyle"
|
||||
:row-class-name="tableRowClassName"
|
||||
stripe
|
||||
>
|
||||
<el-table-column type="index" label="#" width="52" align="center" />
|
||||
<el-table-column label="生产令号" min-width="168" show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
<span class="order-no">{{ scope.row.productionOrderNo || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="压缩包" min-width="200" show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
<div class="archive-cell">
|
||||
<i class="el-icon-document archive-cell__icon"></i>
|
||||
<span>{{ scope.row.archiveName || '-' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="明细数量" width="96" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag size="mini" effect="plain" type="info">{{ scope.row.itemCount != null ? scope.row.itemCount : 0 }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="保存目录" min-width="260" show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
<code class="path-text">{{ scope.row.savedDir || '-' }}</code>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="接收时间" width="168" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span class="time-text">{{ parseTime(scope.row.receiveTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="96" 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:plmPdfZipPush: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
|
||||
custom-class="plm-detail-drawer"
|
||||
@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">{{ detail.archiveName || '-' }}</span>
|
||||
</div>
|
||||
<div class="detail-summary__item">
|
||||
<span class="detail-summary__label">明细数量</span>
|
||||
<span class="detail-summary__value">{{ detail.itemCount != null ? detail.itemCount : 0 }}</span>
|
||||
</div>
|
||||
<div class="detail-summary__item detail-summary__item--wide">
|
||||
<span class="detail-summary__label">保存目录</span>
|
||||
<code class="detail-summary__path">{{ detail.savedDir || '-' }}</code>
|
||||
</div>
|
||||
<div class="detail-summary__item">
|
||||
<span class="detail-summary__label">接收时间</span>
|
||||
<span class="detail-summary__value">{{ parseTime(detail.receiveTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-tabs v-model="detailTab" class="detail-tabs">
|
||||
<el-tab-pane label="图纸明细" name="items">
|
||||
<el-table
|
||||
:data="detail.items || []"
|
||||
size="small"
|
||||
border
|
||||
max-height="360"
|
||||
empty-text="暂无图纸明细"
|
||||
>
|
||||
<el-table-column type="index" label="#" width="48" align="center" />
|
||||
<el-table-column label="物料编码" prop="materialCode" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="物料名称" prop="materialName" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="单台数量" prop="unitQty" width="96" align="center" />
|
||||
<el-table-column label="本批数量" prop="batchQty" width="96" align="center" />
|
||||
<el-table-column label="变更图纸" width="88" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag
|
||||
v-if="scope.row.isDrawingChanged === true || scope.row.isDrawingChanged === 1"
|
||||
size="mini"
|
||||
type="warning"
|
||||
>是</el-tag>
|
||||
<el-tag
|
||||
v-else-if="scope.row.isDrawingChanged === false || scope.row.isDrawingChanged === 0"
|
||||
size="mini"
|
||||
type="info"
|
||||
>否</el-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="PDF文件名" prop="savedFileName" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="DWG文件名" prop="savedDwgFileName" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="PDF包内路径" prop="pdfPath" min-width="120" show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
<code class="path-text path-text--sm">{{ scope.row.pdfPath || '-' }}</code>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="DWG包内路径" prop="dwgPath" min-width="120" show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
<code class="path-text path-text--sm">{{ scope.row.dwgPath || '-' }}</code>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="PDF本地路径" prop="savedPath" min-width="180" show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
<code class="path-text path-text--sm">{{ scope.row.savedPath || '-' }}</code>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="DWG本地路径" prop="savedDwgPath" min-width="220" show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
<div class="dwg-path-cell">
|
||||
<code class="path-text path-text--sm">{{ scope.row.savedDwgPath || '-' }}</code>
|
||||
<el-button
|
||||
v-if="scope.row.savedDwgPath || scope.row.savedDwgFileName"
|
||||
type="text"
|
||||
size="mini"
|
||||
icon="el-icon-download"
|
||||
:loading="downloadingItemId === scope.row.id"
|
||||
@click="downloadDwgItem(scope.row)"
|
||||
v-hasPermi="['system:plmPdfZipPush:query']"
|
||||
>下载</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="Manifest JSON" name="manifest">
|
||||
<div class="manifest-toolbar">
|
||||
<el-button size="mini" plain icon="el-icon-document-copy" @click="copyManifest">复制 JSON</el-button>
|
||||
</div>
|
||||
<pre class="manifest-block">{{ formattedManifest }}</pre>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</template>
|
||||
<el-empty v-else-if="!detailLoading" description="暂无详情数据" />
|
||||
</div>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listPlmPdfZipPush, getPlmPdfZipPush, downloadPlmDwgItem } from '@/api/system/plmPdfZipPush'
|
||||
|
||||
export default {
|
||||
name: 'PlmPdfZipPush',
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
showSearch: true,
|
||||
total: 0,
|
||||
pushList: [],
|
||||
dateRange: [],
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
productionOrderNo: undefined,
|
||||
archiveName: undefined
|
||||
},
|
||||
detailOpen: false,
|
||||
detailLoading: false,
|
||||
detail: null,
|
||||
detailTab: 'items',
|
||||
downloadingItemId: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
pageItemCount() {
|
||||
return this.pushList.reduce((sum, row) => sum + (Number(row.itemCount) || 0), 0)
|
||||
},
|
||||
detailTitle() {
|
||||
if (!this.detail) return '推送记录详情'
|
||||
const orderNo = this.detail.productionOrderNo || ''
|
||||
return orderNo ? `推送详情 · ${orderNo}` : '推送记录详情'
|
||||
},
|
||||
formattedManifest() {
|
||||
if (!this.detail || !this.detail.manifestJson) return '—'
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(this.detail.manifestJson), null, 2)
|
||||
} catch (e) {
|
||||
return this.detail.manifestJson
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getList()
|
||||
},
|
||||
methods: {
|
||||
headerCellStyle() {
|
||||
return {
|
||||
background: '#f0f5f4',
|
||||
color: '#1a3a36',
|
||||
fontWeight: '600',
|
||||
fontSize: '13px'
|
||||
}
|
||||
},
|
||||
tableRowClassName({ rowIndex }) {
|
||||
return rowIndex % 2 === 0 ? 'row-even' : 'row-odd'
|
||||
},
|
||||
buildQueryParams() {
|
||||
let params = { ...this.queryParams }
|
||||
params = this.addDateRange(params, this.dateRange, 'ReceiveTime')
|
||||
return params
|
||||
},
|
||||
getList() {
|
||||
this.loading = true
|
||||
listPlmPdfZipPush(this.buildQueryParams())
|
||||
.then(response => {
|
||||
this.pushList = 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()
|
||||
},
|
||||
handleDetail(row) {
|
||||
if (!row || row.id == null) return
|
||||
this.detailOpen = true
|
||||
this.detailLoading = true
|
||||
this.detailTab = 'items'
|
||||
getPlmPdfZipPush(row.id)
|
||||
.then(response => {
|
||||
this.detail = response.data || null
|
||||
})
|
||||
.finally(() => {
|
||||
this.detailLoading = false
|
||||
})
|
||||
},
|
||||
resetDetail() {
|
||||
this.detail = null
|
||||
this.detailTab = 'items'
|
||||
},
|
||||
copyManifest() {
|
||||
if (!this.formattedManifest || this.formattedManifest === '—') {
|
||||
this.$modal.msgWarning('暂无可复制的 JSON')
|
||||
return
|
||||
}
|
||||
const textarea = document.createElement('textarea')
|
||||
textarea.value = this.formattedManifest
|
||||
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)
|
||||
},
|
||||
async downloadDwgItem(row) {
|
||||
if (!row || row.id == null) return
|
||||
this.downloadingItemId = row.id
|
||||
try {
|
||||
const response = await downloadPlmDwgItem(row.id)
|
||||
if (!(response instanceof Blob)) {
|
||||
this.$modal.msgError('下载失败:服务器返回异常')
|
||||
return
|
||||
}
|
||||
const contentType = (response.type || '').toLowerCase()
|
||||
if (contentType.includes('application/json')) {
|
||||
const text = await response.text()
|
||||
try {
|
||||
const json = JSON.parse(text)
|
||||
this.$modal.msgError(json.msg || '下载失败')
|
||||
} catch (_) {
|
||||
this.$modal.msgError(text || '下载失败')
|
||||
}
|
||||
return
|
||||
}
|
||||
if (response.size === 0) {
|
||||
this.$modal.msgError('下载失败:文件为空')
|
||||
return
|
||||
}
|
||||
const filename = row.savedDwgFileName || `${row.materialCode || 'drawing'}.dwg`
|
||||
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()
|
||||
window.URL.revokeObjectURL(url)
|
||||
this.$modal.msgSuccess('DWG下载成功')
|
||||
} catch (e) {
|
||||
let msg = '下载失败'
|
||||
try {
|
||||
const data = e && e.response && e.response.data
|
||||
if (data instanceof Blob) {
|
||||
const text = await data.text()
|
||||
const json = JSON.parse(text)
|
||||
msg = json.msg || msg
|
||||
} else if (e && e.message) {
|
||||
msg = e.message
|
||||
}
|
||||
} catch (_) {
|
||||
// ignore
|
||||
}
|
||||
this.$modal.msgError(msg)
|
||||
} finally {
|
||||
this.downloadingItemId = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.plm-push-page {
|
||||
--plm-teal: #00b4ad;
|
||||
--plm-teal-dark: #008f89;
|
||||
--plm-slate: #1a3a36;
|
||||
--plm-bg: #f4f8f7;
|
||||
--plm-border: #d8e8e5;
|
||||
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);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: -40px;
|
||||
top: -40px;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, rgba(0, 180, 173, 0.25) 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(0, 180, 173, 0.2);
|
||||
border: 1px solid rgba(0, 180, 173, 0.35);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #7ee8e3;
|
||||
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.6;
|
||||
max-width: 520px;
|
||||
}
|
||||
|
||||
.page-hero__stats {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.stat-chip {
|
||||
min-width: 96px;
|
||||
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.18);
|
||||
border-color: rgba(0, 180, 173, 0.35);
|
||||
}
|
||||
}
|
||||
|
||||
.stat-chip__label {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
margin-bottom: 4px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.stat-chip__value {
|
||||
display: block;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.search-card,
|
||||
.table-card {
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--plm-border);
|
||||
margin-bottom: 12px;
|
||||
|
||||
::v-deep .el-card__body {
|
||||
padding: 16px 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-row {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.order-no {
|
||||
font-weight: 600;
|
||||
color: var(--plm-slate);
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.archive-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.archive-cell__icon {
|
||||
color: var(--plm-teal);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.path-text {
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-size: 12px;
|
||||
color: #5a6b68;
|
||||
background: #eef5f4;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
|
||||
&--sm {
|
||||
font-size: 11px;
|
||||
padding: 1px 4px;
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.dwg-path-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
|
||||
.path-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.time-text {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.detail-body {
|
||||
min-height: 200px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.detail-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
margin-bottom: 18px;
|
||||
padding: 16px;
|
||||
border-radius: 10px;
|
||||
background: #f0f7f6;
|
||||
border: 1px solid var(--plm-border);
|
||||
}
|
||||
|
||||
.detail-summary__item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
&--wide {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-summary__label {
|
||||
font-size: 11px;
|
||||
color: #8a9a97;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.detail-summary__value {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.detail-summary__path {
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-size: 12px;
|
||||
color: #5a6b68;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.detail-tabs {
|
||||
::v-deep .el-tabs__header {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
::v-deep .el-tabs__item.is-active {
|
||||
color: var(--plm-teal-dark);
|
||||
}
|
||||
|
||||
::v-deep .el-tabs__active-bar {
|
||||
background-color: var(--plm-teal);
|
||||
}
|
||||
}
|
||||
|
||||
.manifest-toolbar {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.manifest-block {
|
||||
margin: 0;
|
||||
padding: 14px 16px;
|
||||
max-height: 420px;
|
||||
overflow: auto;
|
||||
border-radius: 8px;
|
||||
background: #1e2d2b;
|
||||
color: #a8e6cf;
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.65;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
border: 1px solid #2d4a46;
|
||||
}
|
||||
|
||||
::v-deep .plm-detail-drawer .el-drawer__header {
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid var(--plm-border);
|
||||
font-weight: 700;
|
||||
color: var(--plm-slate);
|
||||
}
|
||||
|
||||
::v-deep .row-even td {
|
||||
background: #fafcfb !important;
|
||||
}
|
||||
</style>
|
||||
586
src/views/system/plmReceiveLog/index.vue
Normal file
@ -0,0 +1,586 @@
|
||||
<template>
|
||||
<div class="plm-recv-page app-container">
|
||||
<section class="page-hero">
|
||||
<div class="page-hero__main">
|
||||
<div class="page-hero__icon">
|
||||
<i class="el-icon-document-checked"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="page-hero__title">PLM 工艺 / 总装 BOM 接收留痕</h1>
|
||||
<p class="page-hero__desc">记录 PLM 推送的完整报文,防先删后插后扯皮;可按令号、类型、状态追溯。</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: 160px">
|
||||
<el-option label="总装配 BOM" value="ASSEMBLY_BOM" />
|
||||
<el-option label="工艺路线" value="PROCESS_ROUTE" />
|
||||
</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-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="160" 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="scope.row.bizType === 'ASSEMBLY_BOM' ? '' : 'warning'" effect="plain">
|
||||
{{ bizTypeLabel(scope.row.bizType) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="88" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag size="mini" :type="scope.row.status === 'SUCCESS' ? 'success' : 'danger'">
|
||||
{{ scope.row.status === 'SUCCESS' ? '成功' : '失败' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="行数" width="72" align="center" prop="lineCount" />
|
||||
<el-table-column label="入库数" width="80" align="center" prop="savedCount" />
|
||||
<el-table-column label="覆盖" width="72" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ scope.row.replaced === true ? '是' : (scope.row.replaced === false ? '否' : '-') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="报文大小" width="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
{{ formatSize(scope.row.payloadSize) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="客户端IP" width="120" align="center" show-overflow-tooltip prop="clientIp" />
|
||||
<el-table-column label="接收时间" width="168" align="center">
|
||||
<template slot-scope="scope">
|
||||
{{ parseTime(scope.row.receiveTime, '{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="210" 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:plmReceiveLog:query']"
|
||||
>详情</el-button>
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-upload2"
|
||||
:disabled="!scope.row.productionOrderNo"
|
||||
@click="handleSyncBomDetails(scope.row)"
|
||||
v-hasPermi="['system:plmReceiveLog:syncBomDetails']"
|
||||
>写入BOM明细</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="780px"
|
||||
append-to-body
|
||||
custom-class="plm-recv-drawer"
|
||||
@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>
|
||||
<el-tag size="mini" :type="detail.status === 'SUCCESS' ? 'success' : 'danger'">
|
||||
{{ detail.status === 'SUCCESS' ? '成功' : '失败' }}
|
||||
</el-tag>
|
||||
</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.lineCount != null ? detail.lineCount : '-' }} / {{ detail.savedCount != null ? detail.savedCount : '-' }}</span>
|
||||
</div>
|
||||
<div class="detail-summary__item">
|
||||
<span class="detail-summary__label">报文大小</span>
|
||||
<span class="detail-summary__value">{{ formatSize(detail.payloadSize) }}</span>
|
||||
</div>
|
||||
<div class="detail-summary__item detail-summary__item--wide">
|
||||
<span class="detail-summary__label">SHA-256</span>
|
||||
<code class="path-text">{{ detail.payloadSha256 || '-' }}</code>
|
||||
</div>
|
||||
<div class="detail-summary__item">
|
||||
<span class="detail-summary__label">接收时间</span>
|
||||
<span class="detail-summary__value">{{ parseTime(detail.receiveTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
|
||||
</div>
|
||||
<div class="detail-summary__item" v-if="detail.errorMsg">
|
||||
<span class="detail-summary__label">失败原因</span>
|
||||
<span class="detail-summary__value error-text">{{ detail.errorMsg }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-actions" v-hasPermi="['system:plmReceiveLog:syncBomDetails']">
|
||||
<el-button
|
||||
size="mini"
|
||||
type="primary"
|
||||
plain
|
||||
icon="el-icon-upload2"
|
||||
:disabled="!detail.productionOrderNo"
|
||||
:loading="syncLoading"
|
||||
@click="handleSyncBomDetails(detail)"
|
||||
>写入 BOM 明细</el-button>
|
||||
<span class="detail-actions__hint">从 production_order 同步总装行到 bom_details(覆盖 bomType=0)</span>
|
||||
</div>
|
||||
|
||||
<el-tabs v-model="detailTab" class="detail-tabs">
|
||||
<el-tab-pane label="请求报文 JSON" name="payload">
|
||||
<div class="json-toolbar">
|
||||
<el-button size="mini" type="primary" plain icon="el-icon-document-copy" @click="copyJson(formattedPayload)">复制报文</el-button>
|
||||
</div>
|
||||
<pre class="json-block">{{ formattedPayload }}</pre>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="摘要 JSON" name="summary">
|
||||
<div class="json-toolbar">
|
||||
<el-button size="mini" type="primary" plain icon="el-icon-document-copy" @click="copyJson(formattedSummary)">复制摘要</el-button>
|
||||
</div>
|
||||
<pre class="json-block">{{ formattedSummary }}</pre>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</template>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listPlmReceiveLog, getPlmReceiveLog, syncPlmReceiveLogBomDetails } from '@/api/system/plmReceiveLog'
|
||||
|
||||
export default {
|
||||
name: 'PlmReceiveLog',
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
showSearch: true,
|
||||
total: 0,
|
||||
logList: [],
|
||||
dateRange: [],
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
productionOrderNo: undefined,
|
||||
bizType: undefined,
|
||||
status: undefined
|
||||
},
|
||||
detailOpen: false,
|
||||
detailLoading: false,
|
||||
syncLoading: false,
|
||||
detail: null,
|
||||
detailTab: 'payload'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
pageSuccessCount() {
|
||||
return this.logList.filter(r => r.status === 'SUCCESS').length
|
||||
},
|
||||
detailTitle() {
|
||||
if (!this.detail) return '接收记录详情'
|
||||
const orderNo = this.detail.productionOrderNo || ''
|
||||
return orderNo ? `接收详情 · ${orderNo}` : '接收记录详情'
|
||||
},
|
||||
formattedPayload() {
|
||||
return this.prettyJson(this.detail && this.detail.payloadJson)
|
||||
},
|
||||
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) {
|
||||
if (type === 'ASSEMBLY_BOM') return '总装配 BOM'
|
||||
if (type === 'PROCESS_ROUTE') return '工艺路线'
|
||||
return type || '-'
|
||||
},
|
||||
formatSize(size) {
|
||||
if (size == null) return '-'
|
||||
if (size < 1024) return size + ' B'
|
||||
if (size < 1024 * 1024) return (size / 1024).toFixed(1) + ' KB'
|
||||
return (size / 1024 / 1024).toFixed(2) + ' MB'
|
||||
},
|
||||
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, 'ReceiveTime')
|
||||
return params
|
||||
},
|
||||
getList() {
|
||||
this.loading = true
|
||||
listPlmReceiveLog(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()
|
||||
},
|
||||
handleDetail(row) {
|
||||
if (!row || row.id == null) return
|
||||
this.detailOpen = true
|
||||
this.detailLoading = true
|
||||
this.detailTab = 'payload'
|
||||
getPlmReceiveLog(row.id)
|
||||
.then(response => {
|
||||
this.detail = response.data || null
|
||||
})
|
||||
.finally(() => {
|
||||
this.detailLoading = false
|
||||
})
|
||||
},
|
||||
handleSyncBomDetails(row) {
|
||||
if (!row || row.id == null) return
|
||||
const orderNo = row.productionOrderNo || ''
|
||||
if (!orderNo) {
|
||||
this.$modal.msgWarning('该记录无生产令号')
|
||||
return
|
||||
}
|
||||
this.$modal.confirm(
|
||||
'确认将令号「' + orderNo + '」在 production_order 中的总装行同步到 bom_details?\n(覆盖同令号旧总装行 bomType=0,保留材料 BOM)'
|
||||
).then(() => {
|
||||
this.syncLoading = true
|
||||
return syncPlmReceiveLogBomDetails(row.id)
|
||||
}).then(res => {
|
||||
const count = res && res.data ? res.data.savedCount : null
|
||||
const msg = res && res.msg
|
||||
? res.msg
|
||||
: ('同步成功' + (count != null ? ',共 ' + count + ' 条' : ''))
|
||||
this.$modal.msgSuccess(msg)
|
||||
}).catch(() => {}).finally(() => {
|
||||
this.syncLoading = false
|
||||
})
|
||||
},
|
||||
resetDetail() {
|
||||
this.detail = null
|
||||
this.detailTab = 'payload'
|
||||
},
|
||||
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>
|
||||
.plm-recv-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-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
background: #eef8f6;
|
||||
border: 1px solid #cfe8e3;
|
||||
}
|
||||
|
||||
.detail-actions__hint {
|
||||
font-size: 12px;
|
||||
color: #6a8682;
|
||||
}
|
||||
|
||||
.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>
|
||||
607
src/views/system/produceKanban/index.vue
Normal file
@ -0,0 +1,607 @@
|
||||
<template>
|
||||
<div class="produce-kanban">
|
||||
<div class="kanban-bg-grid" aria-hidden="true"></div>
|
||||
<div class="kanban-scanline" aria-hidden="true"></div>
|
||||
|
||||
<header class="kanban-header">
|
||||
<div class="kanban-header__brand">
|
||||
<div class="brand-logo">
|
||||
<span class="brand-logo__cn">伊特</span>
|
||||
<span class="brand-logo__en">Evo-Tech</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kanban-header__center">
|
||||
<h1 class="kanban-title">河北伊特机械设备制造有限公司 生产管理看板</h1>
|
||||
<p class="kanban-subtitle">工作中心:{{ chejian || '—' }}</p>
|
||||
</div>
|
||||
<div class="kanban-header__clock">
|
||||
<span class="clock-date">{{ clockDate }}</span>
|
||||
<span class="clock-time">{{ clockTime }}</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="kanban-table-wrap">
|
||||
<div class="kanban-table-head">
|
||||
<table>
|
||||
<colgroup>
|
||||
<col style="width: 4%" />
|
||||
<col style="width: 11%" />
|
||||
<col style="width: 14%" />
|
||||
<col style="width: 12%" />
|
||||
<col style="width: 5%" />
|
||||
<col style="width: 5%" />
|
||||
<col style="width: 5%" />
|
||||
<col style="width: 6%" />
|
||||
<col style="width: 10%" />
|
||||
<col style="width: 8%" />
|
||||
<col style="width: 8%" />
|
||||
<col style="width: 6%" />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>序号</th>
|
||||
<th>生产令号</th>
|
||||
<th>产品编码</th>
|
||||
<th>产品名称</th>
|
||||
<th>数量</th>
|
||||
<th>转入</th>
|
||||
<th>转出</th>
|
||||
<th>工序号</th>
|
||||
<th>作业</th>
|
||||
<th>计划开始时间</th>
|
||||
<th>计划结束时间</th>
|
||||
<th>延期</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="kanban-page-viewport"
|
||||
ref="viewport"
|
||||
@mouseenter="pagePaused = true"
|
||||
@mouseleave="pagePaused = false"
|
||||
>
|
||||
<div v-if="loading" class="kanban-loading">
|
||||
<i class="el-icon-loading"></i>
|
||||
<span>数据加载中…</span>
|
||||
</div>
|
||||
<div v-else-if="!displayRows.length" class="kanban-empty">暂无生产数据</div>
|
||||
<transition v-else name="kanban-page" mode="out-in">
|
||||
<table :key="currentPage" class="kanban-data-table">
|
||||
<colgroup>
|
||||
<col style="width: 4%" />
|
||||
<col style="width: 11%" />
|
||||
<col style="width: 14%" />
|
||||
<col style="width: 12%" />
|
||||
<col style="width: 5%" />
|
||||
<col style="width: 5%" />
|
||||
<col style="width: 5%" />
|
||||
<col style="width: 6%" />
|
||||
<col style="width: 10%" />
|
||||
<col style="width: 8%" />
|
||||
<col style="width: 8%" />
|
||||
<col style="width: 6%" />
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="row in pagedRows"
|
||||
:key="row._key"
|
||||
:class="{ 'row-delayed': row._delayed }"
|
||||
>
|
||||
<td>{{ row._index }}</td>
|
||||
<td>{{ row.moOrderNo || '—' }}</td>
|
||||
<td :title="row.materialNumber">{{ row.materialNumber || '—' }}</td>
|
||||
<td :title="row.materialName">{{ row.materialName || '—' }}</td>
|
||||
<td>{{ formatQty(row.operQty) }}</td>
|
||||
<td>{{ formatQty(row.transInQty) }}</td>
|
||||
<td>{{ formatQty(row.transOutQty) }}</td>
|
||||
<td>{{ row.operNumber != null && row.operNumber !== '' ? row.operNumber : '—' }}</td>
|
||||
<td :title="row.processName">{{ row.processName || '—' }}</td>
|
||||
<td>{{ formatPlanDate(row.operPlanStartTime) }}</td>
|
||||
<td>{{ formatPlanDate(row.operPlanFinishTime) }}</td>
|
||||
<td>{{ row._delayText }}</td>
|
||||
</tr>
|
||||
<tr v-for="n in emptyRowCount" :key="`empty-${n}`" class="row-placeholder">
|
||||
<td v-for="c in 12" :key="c"> </td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<footer class="kanban-footer">
|
||||
<span>共 {{ displayRows.length }} 条</span>
|
||||
<span class="kanban-footer__sep">|</span>
|
||||
<span>第 {{ currentPage }} / {{ totalPages }} 页</span>
|
||||
<span class="kanban-footer__sep">|</span>
|
||||
<span>每页 {{ pageSize }} 条</span>
|
||||
<span class="kanban-footer__sep">|</span>
|
||||
<span>{{ refreshHint }}</span>
|
||||
<div v-if="totalPages > 1 && totalPages <= 20" class="kanban-pager-dots">
|
||||
<span
|
||||
v-for="p in totalPages"
|
||||
:key="p"
|
||||
class="pager-dot"
|
||||
:class="{ 'is-active': p === currentPage }"
|
||||
></span>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getProduceDataKanban } from '@/api/system/workCenterData'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const DEFAULT_CHEJIAN = '机一工段'
|
||||
const REFRESH_MS = 10 * 60 * 1000
|
||||
const PAGE_FLIP_SEC = 30
|
||||
const DEFAULT_PAGE_SIZE = 18
|
||||
const ROW_HEIGHT = 42
|
||||
|
||||
export default {
|
||||
name: 'ProduceKanban',
|
||||
data() {
|
||||
return {
|
||||
chejian: DEFAULT_CHEJIAN,
|
||||
loading: false,
|
||||
rawList: [],
|
||||
clockDate: '',
|
||||
clockTime: '',
|
||||
pagePaused: false,
|
||||
pageSize: DEFAULT_PAGE_SIZE,
|
||||
currentPage: 1,
|
||||
pageFlipCountdown: PAGE_FLIP_SEC,
|
||||
refreshHint: '',
|
||||
clockTimer: null,
|
||||
refreshTimer: null,
|
||||
pageFlipTimer: null,
|
||||
countdownTimer: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
displayRows() {
|
||||
return this.rawList.map((item, index) => this.normalizeRow(item, index))
|
||||
},
|
||||
totalPages() {
|
||||
if (!this.displayRows.length) return 1
|
||||
return Math.ceil(this.displayRows.length / this.pageSize)
|
||||
},
|
||||
pagedRows() {
|
||||
const start = (this.currentPage - 1) * this.pageSize
|
||||
return this.displayRows.slice(start, start + this.pageSize)
|
||||
},
|
||||
emptyRowCount() {
|
||||
return Math.max(0, this.pageSize - this.pagedRows.length)
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'$route.query.chejian': {
|
||||
immediate: true,
|
||||
handler(val) {
|
||||
this.chejian = val || DEFAULT_CHEJIAN
|
||||
this.fetchData()
|
||||
},
|
||||
},
|
||||
totalPages(val) {
|
||||
if (this.currentPage > val) {
|
||||
this.currentPage = 1
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
document.title = '生产管理看板'
|
||||
this.tickClock()
|
||||
this.clockTimer = setInterval(this.tickClock, 1000)
|
||||
this.refreshTimer = setInterval(this.fetchData, REFRESH_MS)
|
||||
window.addEventListener('resize', this.handleResize)
|
||||
this.$nextTick(() => {
|
||||
this.calcPageSize()
|
||||
this.startPageFlip()
|
||||
})
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearInterval(this.clockTimer)
|
||||
clearInterval(this.refreshTimer)
|
||||
this.stopPageFlip()
|
||||
window.removeEventListener('resize', this.handleResize)
|
||||
},
|
||||
methods: {
|
||||
tickClock() {
|
||||
const now = dayjs()
|
||||
this.clockDate = now.format('YYYY-MM-DD')
|
||||
this.clockTime = now.format('HH:mm:ss')
|
||||
},
|
||||
handleResize() {
|
||||
this.calcPageSize()
|
||||
},
|
||||
calcPageSize() {
|
||||
const viewport = this.$refs.viewport
|
||||
if (!viewport || !viewport.clientHeight) {
|
||||
this.pageSize = DEFAULT_PAGE_SIZE
|
||||
return
|
||||
}
|
||||
const size = Math.floor(viewport.clientHeight / ROW_HEIGHT)
|
||||
this.pageSize = Math.max(10, Math.min(30, size || DEFAULT_PAGE_SIZE))
|
||||
},
|
||||
startPageFlip() {
|
||||
this.stopPageFlip()
|
||||
this.pageFlipCountdown = PAGE_FLIP_SEC
|
||||
this.countdownTimer = setInterval(() => {
|
||||
if (this.pagePaused || this.loading || this.totalPages <= 1) {
|
||||
return
|
||||
}
|
||||
if (this.pageFlipCountdown > 1) {
|
||||
this.pageFlipCountdown -= 1
|
||||
return
|
||||
}
|
||||
this.nextPage()
|
||||
this.pageFlipCountdown = PAGE_FLIP_SEC
|
||||
}, 1000)
|
||||
},
|
||||
stopPageFlip() {
|
||||
if (this.pageFlipTimer) {
|
||||
clearInterval(this.pageFlipTimer)
|
||||
this.pageFlipTimer = null
|
||||
}
|
||||
if (this.countdownTimer) {
|
||||
clearInterval(this.countdownTimer)
|
||||
this.countdownTimer = null
|
||||
}
|
||||
},
|
||||
nextPage() {
|
||||
if (this.totalPages <= 1) return
|
||||
this.currentPage = this.currentPage >= this.totalPages ? 1 : this.currentPage + 1
|
||||
},
|
||||
resetPagination() {
|
||||
this.currentPage = 1
|
||||
this.pageFlipCountdown = PAGE_FLIP_SEC
|
||||
this.$nextTick(() => {
|
||||
this.calcPageSize()
|
||||
this.startPageFlip()
|
||||
})
|
||||
},
|
||||
async fetchData() {
|
||||
if (!this.chejian) return
|
||||
this.loading = true
|
||||
try {
|
||||
const res = await getProduceDataKanban(this.chejian)
|
||||
this.rawList = this.parseList(res)
|
||||
this.refreshHint = `更新于 ${dayjs().format('HH:mm:ss')}`
|
||||
this.resetPagination()
|
||||
} catch (e) {
|
||||
this.rawList = []
|
||||
this.refreshHint = '数据加载失败'
|
||||
this.stopPageFlip()
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
parseList(res) {
|
||||
if (Array.isArray(res)) return res
|
||||
if (Array.isArray(res && res.rows)) return res.rows
|
||||
if (Array.isArray(res && res.data)) return res.data
|
||||
return []
|
||||
},
|
||||
normalizeRow(item, index) {
|
||||
const delayDays = this.pickNumber(item, ['delayDays', 'delayDay', 'delay'])
|
||||
const delayed = delayDays != null && delayDays > 0
|
||||
return {
|
||||
...item,
|
||||
moOrderNo: this.pickField(item, ['moOrderNo', 'productionOrderNo', 'sclh']),
|
||||
materialNumber: this.pickField(item, ['materialNumber', 'productCode']),
|
||||
materialName: this.pickField(item, ['materialName', 'productName']),
|
||||
operQty: this.pickField(item, ['operQty', 'qty', 'quantity']),
|
||||
transInQty: this.pickField(item, ['transInQty', 'transferIn']),
|
||||
transOutQty: this.pickField(item, ['transOutQty', 'transferOut']),
|
||||
operNumber: this.pickField(item, ['operNumber', 'processNo', 'operNo']),
|
||||
processName: this.pickField(item, ['processName', 'operation', 'workName']),
|
||||
operPlanStartTime: this.pickField(item, ['operPlanStartTime', 'planStartDate', 'FPlanStartDate']),
|
||||
operPlanFinishTime: this.pickField(item, ['operPlanFinishTime', 'planFinishDate', 'FPlanFinishDate']),
|
||||
_index: index + 1,
|
||||
_key: item.id != null ? item.id : `${index}-${item.moOrderNo || ''}`,
|
||||
_delayed: delayed,
|
||||
_delayText: delayed ? String(delayDays) : '--',
|
||||
}
|
||||
},
|
||||
pickField(obj, keys) {
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const v = obj[keys[i]]
|
||||
if (v !== undefined && v !== null && v !== '') return v
|
||||
}
|
||||
return ''
|
||||
},
|
||||
pickNumber(obj, keys) {
|
||||
const v = this.pickField(obj, keys)
|
||||
if (v === '') return null
|
||||
const n = Number(v)
|
||||
return Number.isFinite(n) ? n : null
|
||||
},
|
||||
formatQty(val) {
|
||||
if (val === '' || val == null) return '—'
|
||||
const n = Number(val)
|
||||
return Number.isFinite(n) ? n : val
|
||||
},
|
||||
formatPlanDate(val) {
|
||||
if (!val) return '—'
|
||||
const d = dayjs(val)
|
||||
if (d.isValid()) return d.format('MM-DD')
|
||||
const str = String(val)
|
||||
if (str.length >= 10) return str.slice(5, 10)
|
||||
return str
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.produce-kanban {
|
||||
--kb-bg: #07101f;
|
||||
--kb-bg-panel: rgba(8, 22, 42, 0.92);
|
||||
--kb-border: rgba(64, 158, 255, 0.35);
|
||||
--kb-border-strong: rgba(64, 158, 255, 0.55);
|
||||
--kb-text: #eef4ff;
|
||||
--kb-text-dim: rgba(200, 220, 255, 0.72);
|
||||
--kb-delay: #ff4d4f;
|
||||
--kb-header-h: 88px;
|
||||
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
background: radial-gradient(ellipse 120% 80% at 50% -10%, #0f2847 0%, var(--kb-bg) 55%);
|
||||
color: var(--kb-text);
|
||||
font-family: "Microsoft YaHei", "PingFang SC", "Helvetica Neue", sans-serif;
|
||||
}
|
||||
|
||||
.kanban-bg-grid {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image:
|
||||
linear-gradient(rgba(64, 158, 255, 0.04) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(64, 158, 255, 0.04) 1px, transparent 1px);
|
||||
background-size: 32px 32px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.kanban-scanline {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: repeating-linear-gradient(
|
||||
0deg,
|
||||
transparent,
|
||||
transparent 2px,
|
||||
rgba(0, 0, 0, 0.03) 2px,
|
||||
rgba(0, 0, 0, 0.03) 4px
|
||||
);
|
||||
pointer-events: none;
|
||||
opacity: 0.35;
|
||||
}
|
||||
|
||||
.kanban-header {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: grid;
|
||||
grid-template-columns: 200px 1fr 200px;
|
||||
align-items: center;
|
||||
height: var(--kb-header-h);
|
||||
padding: 0 28px;
|
||||
border-bottom: 2px solid var(--kb-border-strong);
|
||||
background: linear-gradient(180deg, rgba(12, 32, 58, 0.95) 0%, rgba(8, 20, 38, 0.88) 100%);
|
||||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.brand-logo__cn {
|
||||
font-size: 28px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.brand-logo__en {
|
||||
margin-top: 2px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.12em;
|
||||
color: #5dade2;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.kanban-header__center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.kanban-title {
|
||||
margin: 0;
|
||||
font-size: clamp(22px, 2.2vw, 34px);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
color: #fff;
|
||||
text-shadow: 0 0 20px rgba(64, 158, 255, 0.25);
|
||||
}
|
||||
|
||||
.kanban-subtitle {
|
||||
margin: 6px 0 0;
|
||||
font-size: clamp(14px, 1.2vw, 20px);
|
||||
font-weight: 600;
|
||||
color: #7ec8ff;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.kanban-header__clock {
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.clock-date {
|
||||
display: block;
|
||||
font-size: 16px;
|
||||
color: var(--kb-text-dim);
|
||||
}
|
||||
|
||||
.clock-time {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.kanban-table-wrap {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - var(--kb-header-h));
|
||||
padding: 10px 16px 12px;
|
||||
}
|
||||
|
||||
.kanban-table-head {
|
||||
flex-shrink: 0;
|
||||
border: 1px solid var(--kb-border-strong);
|
||||
border-bottom: none;
|
||||
background: rgba(15, 45, 80, 0.85);
|
||||
}
|
||||
|
||||
.kanban-table-head table,
|
||||
.kanban-data-table {
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.kanban-table-head th {
|
||||
padding: 12px 6px;
|
||||
font-size: clamp(13px, 1vw, 16px);
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
border-right: 1px solid rgba(64, 158, 255, 0.2);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.kanban-table-head th:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.kanban-page-viewport {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--kb-border);
|
||||
background: var(--kb-bg-panel);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.kanban-data-table {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.kanban-data-table td {
|
||||
padding: 10px 6px;
|
||||
height: 42px;
|
||||
box-sizing: border-box;
|
||||
font-size: clamp(13px, 0.95vw, 17px);
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
color: var(--kb-text);
|
||||
border-bottom: 1px solid rgba(64, 158, 255, 0.12);
|
||||
border-right: 1px solid rgba(64, 158, 255, 0.08);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.kanban-data-table td:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.kanban-data-table tbody tr:nth-child(even):not(.row-placeholder) td {
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
|
||||
.kanban-data-table tbody tr.row-delayed td {
|
||||
color: var(--kb-delay);
|
||||
text-shadow: 0 0 8px rgba(255, 77, 79, 0.35);
|
||||
}
|
||||
|
||||
.kanban-data-table tbody tr.row-placeholder td {
|
||||
border-bottom-color: rgba(64, 158, 255, 0.06);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.kanban-page-enter-active,
|
||||
.kanban-page-leave-active {
|
||||
transition: opacity 0.45s ease, transform 0.45s ease;
|
||||
}
|
||||
|
||||
.kanban-page-enter {
|
||||
opacity: 0;
|
||||
transform: translateY(12px);
|
||||
}
|
||||
|
||||
.kanban-page-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-12px);
|
||||
}
|
||||
|
||||
.kanban-loading,
|
||||
.kanban-empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
height: 100%;
|
||||
font-size: 18px;
|
||||
color: var(--kb-text-dim);
|
||||
}
|
||||
|
||||
.kanban-loading i {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.kanban-footer {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-top: 8px;
|
||||
font-size: 14px;
|
||||
color: var(--kb-text-dim);
|
||||
}
|
||||
|
||||
.kanban-footer__sep {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.kanban-pager-dots {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.pager-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
transition: background 0.3s, transform 0.3s;
|
||||
}
|
||||
|
||||
.pager-dot.is-active {
|
||||
background: #409eff;
|
||||
transform: scale(1.25);
|
||||
box-shadow: 0 0 8px rgba(64, 158, 255, 0.6);
|
||||
}
|
||||
</style>
|
||||
1231
src/views/system/productionBoard/index.vue
Normal file
567
src/views/system/productionRemind/index.vue
Normal file
@ -0,0 +1,567 @@
|
||||
<template>
|
||||
<div class="remind-page" :class="{ embedded: embedded }">
|
||||
<header v-if="!embedded" class="remind-header">
|
||||
<div class="brand">
|
||||
<div class="brand-mark"><i class="el-icon-bell" /></div>
|
||||
<div>
|
||||
<h1>委外延期 · 生产提醒</h1>
|
||||
<p>机加 / 装配 / 铆焊委外延期 · 发出 / 接收记录</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<span class="sync-time" v-if="lastSyncTime"><i class="el-icon-time" /> {{ lastSyncTime }}</span>
|
||||
<el-button type="primary" size="mini" icon="el-icon-refresh" :loading="loading" @click="loadData">
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="transfer-panel">
|
||||
<div class="panel-head">
|
||||
<div class="head-left">
|
||||
<h2>委外发出 / 接收汇总(按订单合并)</h2>
|
||||
</div>
|
||||
<div class="toolbar">
|
||||
<el-select
|
||||
v-model="supplierFilter"
|
||||
size="mini"
|
||||
clearable
|
||||
filterable
|
||||
placeholder="转出供应商"
|
||||
class="supplier-select"
|
||||
>
|
||||
<el-option
|
||||
v-for="name in supplierOptions"
|
||||
:key="name"
|
||||
:label="name"
|
||||
:value="name"
|
||||
/>
|
||||
<el-option label="(无供应商)" value="__EMPTY__" />
|
||||
</el-select>
|
||||
<el-select v-model="gapFilter" size="mini" clearable placeholder="差额筛选" class="type-select">
|
||||
<el-option label="本批未发完" value="qtyDiff" />
|
||||
<el-option label="发出未回完" value="returnDiff" />
|
||||
</el-select>
|
||||
<el-input
|
||||
v-model="keyword"
|
||||
size="mini"
|
||||
clearable
|
||||
placeholder="令号 / 订单 / 产品 / 供应商"
|
||||
prefix-icon="el-icon-search"
|
||||
class="search-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-wrap" ref="tableWrap">
|
||||
<el-table
|
||||
ref="remindTable"
|
||||
v-loading="loading"
|
||||
:data="pagedTransfers"
|
||||
border
|
||||
stripe
|
||||
size="mini"
|
||||
:height="tableHeight"
|
||||
class="remind-table"
|
||||
:header-cell-style="headerCellStyle"
|
||||
empty-text="暂无委外发出 / 接收汇总数据"
|
||||
>
|
||||
<el-table-column type="index" label="#" width="44" align="center" fixed :index="transferIndex" />
|
||||
<el-table-column prop="moOrderNo" label="生产令号" min-width="120" show-overflow-tooltip fixed>
|
||||
<template slot-scope="{ row }">{{ row.moOrderNo || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="moBillNo" label="生产订单号" min-width="112" show-overflow-tooltip fixed />
|
||||
<el-table-column prop="outSupplierName" label="转出供应商" min-width="150" show-overflow-tooltip>
|
||||
<template slot-scope="{ row }">{{ row.outSupplierName || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="inSupplierName" label="接收供应商" min-width="150" show-overflow-tooltip>
|
||||
<template slot-scope="{ row }">{{ row.inSupplierName || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="materialNumber" label="产品编码" min-width="110" show-overflow-tooltip />
|
||||
<el-table-column prop="materialName" label="产品名称" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="operQty" label="本批数量" width="88" align="right">
|
||||
<template slot-scope="{ row }">{{ row.operQty == null || row.operQty === '' ? '—' : formatQtyVal(row.operQty) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="operTransferQty" label="发出数量" width="88" align="right">
|
||||
<template slot-scope="{ row }">{{ formatQtyVal(row.operTransferQty) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="receiveTransferQty" label="接收数量" width="88" align="right">
|
||||
<template slot-scope="{ row }">{{ formatQtyVal(row.receiveTransferQty) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="qtyDiff" label="本批−发出" width="96" align="right">
|
||||
<template slot-scope="{ row }">
|
||||
<span :class="diffClass(row.qtyDiff)">{{ formatQtyVal(row.qtyDiff) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="returnDiff" label="发出−接收" width="96" align="right">
|
||||
<template slot-scope="{ row }">
|
||||
<span :class="diffClass(row.returnDiff)">{{ formatQtyVal(row.returnDiff) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最新日期" width="108">
|
||||
<template slot-scope="{ row }">{{ formatDate(row.billDate) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="inOperNumber" label="委外工序" width="80" align="center">
|
||||
<template slot-scope="{ row }">{{ formatOperNo(row.inOperNumber) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="processName" label="工序" min-width="110" show-overflow-tooltip />
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<div class="panel-footer">
|
||||
<span class="hint">
|
||||
共 <b>{{ filteredTransfers.length }}</b> 条
|
||||
<template v-if="supplierFilter && supplierFilter !== '__EMPTY__'">
|
||||
· 供应商「{{ supplierFilter }}」
|
||||
</template>
|
||||
</span>
|
||||
<el-pagination
|
||||
background
|
||||
small
|
||||
layout="sizes, prev, pager, next"
|
||||
:current-page.sync="transferPage"
|
||||
:page-size.sync="transferPageSize"
|
||||
:page-sizes="[20, 50, 100]"
|
||||
:total="filteredTransfers.length"
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getJjSubcontractDelayRemind } from '@/api/system/workCenterData'
|
||||
|
||||
export default {
|
||||
name: 'ProductionRemind',
|
||||
props: {
|
||||
/** 嵌入生产提醒(workCenterAlert)时隐藏独立顶栏 */
|
||||
embedded: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
lastSyncTime: '',
|
||||
transferRows: [],
|
||||
delayMoCount: 0,
|
||||
delayRowCount: 0,
|
||||
keyword: '',
|
||||
gapFilter: '',
|
||||
supplierFilter: '',
|
||||
transferPage: 1,
|
||||
transferPageSize: 50,
|
||||
tableHeight: 420,
|
||||
headerCellStyle: {
|
||||
background: '#1a2332',
|
||||
color: '#e8e4dc',
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
borderColor: '#243044'
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
supplierOptions() {
|
||||
const set = new Set()
|
||||
;(this.transferRows || []).forEach(r => {
|
||||
this.splitSuppliers(r && r.outSupplierName).forEach(name => set.add(name))
|
||||
})
|
||||
return Array.from(set).sort((a, b) => a.localeCompare(b, 'zh-CN'))
|
||||
},
|
||||
filteredTransfers() {
|
||||
let list = this.transferRows || []
|
||||
if (this.supplierFilter === '__EMPTY__') {
|
||||
list = list.filter(r => this.splitSuppliers(r && r.outSupplierName).length === 0)
|
||||
} else if (this.supplierFilter) {
|
||||
const selected = this.supplierFilter
|
||||
list = list.filter(r => this.splitSuppliers(r && r.outSupplierName).includes(selected))
|
||||
}
|
||||
if (this.gapFilter === 'qtyDiff') {
|
||||
list = list.filter(r => Number(r && r.qtyDiff) > 0)
|
||||
} else if (this.gapFilter === 'returnDiff') {
|
||||
list = list.filter(r => Number(r && r.returnDiff) > 0)
|
||||
}
|
||||
const kw = (this.keyword || '').trim().toLowerCase()
|
||||
if (kw) {
|
||||
list = list.filter(r => {
|
||||
const blob = [
|
||||
r.moOrderNo, r.moBillNo, r.outSupplierName, r.inSupplierName,
|
||||
r.materialNumber, r.materialName, r.processName
|
||||
].map(v => (v == null ? '' : String(v).toLowerCase())).join('|')
|
||||
return blob.includes(kw)
|
||||
})
|
||||
}
|
||||
return list
|
||||
},
|
||||
pagedTransfers() {
|
||||
const start = (this.transferPage - 1) * this.transferPageSize
|
||||
return this.filteredTransfers.slice(start, start + this.transferPageSize)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
keyword() { this.transferPage = 1 },
|
||||
gapFilter() { this.transferPage = 1 },
|
||||
supplierFilter() { this.transferPage = 1 },
|
||||
filteredTransfers() {
|
||||
this.$nextTick(() => this.scheduleTableLayout())
|
||||
},
|
||||
pagedTransfers() {
|
||||
this.$nextTick(() => this.scheduleTableLayout())
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if (!this.embedded) {
|
||||
this.loadData()
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.updateTableHeight()
|
||||
window.addEventListener('resize', this.updateTableHeight)
|
||||
},
|
||||
activated() {
|
||||
this.updateTableHeight()
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.updateTableHeight)
|
||||
if (this._tableLayoutTimer) {
|
||||
clearTimeout(this._tableLayoutTimer)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
scheduleTableLayout() {
|
||||
if (this._tableLayoutTimer) {
|
||||
clearTimeout(this._tableLayoutTimer)
|
||||
}
|
||||
this._tableLayoutTimer = setTimeout(() => {
|
||||
this.updateTableHeight()
|
||||
}, 50)
|
||||
},
|
||||
updateTableHeight() {
|
||||
this.$nextTick(() => {
|
||||
const wrap = this.$refs.tableWrap
|
||||
if (!wrap) return
|
||||
const next = Math.max(wrap.clientHeight || 0, 240)
|
||||
if (Math.abs(next - this.tableHeight) > 2) {
|
||||
this.tableHeight = next
|
||||
}
|
||||
const table = this.$refs.remindTable
|
||||
if (table && typeof table.doLayout === 'function') {
|
||||
table.doLayout()
|
||||
}
|
||||
})
|
||||
},
|
||||
normSupplier(val) {
|
||||
if (val == null || val === '') return ''
|
||||
return String(val).replace(/\u00a0/g, ' ').trim()
|
||||
},
|
||||
splitSuppliers(val) {
|
||||
const raw = this.normSupplier(val)
|
||||
if (!raw) return []
|
||||
return raw.split(/[、,,;/|]+/).map(s => s.trim()).filter(Boolean)
|
||||
},
|
||||
normType(val) {
|
||||
if (val == null || val === '') return ''
|
||||
return String(val).trim().toUpperCase()
|
||||
},
|
||||
loadData() {
|
||||
this.loading = true
|
||||
return getJjSubcontractDelayRemind()
|
||||
.then(res => {
|
||||
const data = (res && res.data) || {}
|
||||
this.transferRows = (data.transferRows || []).map(row => ({
|
||||
...row,
|
||||
transferType: this.normType(row.transferType) || 'SUM',
|
||||
outSupplierName: this.normSupplier(row.outSupplierName) || null,
|
||||
inSupplierName: this.normSupplier(row.inSupplierName) || null,
|
||||
moBillNo: row.moBillNo != null ? String(row.moBillNo).trim() : row.moBillNo
|
||||
}))
|
||||
this.delayMoCount = data.delayMoCount || 0
|
||||
this.delayRowCount = data.delayRowCount || 0
|
||||
this.transferPage = 1
|
||||
this.supplierFilter = ''
|
||||
this.gapFilter = ''
|
||||
this.touchSyncTime()
|
||||
this.$emit('loaded', {
|
||||
delayRowCount: this.delayRowCount,
|
||||
delayMoCount: this.delayMoCount,
|
||||
transferCount: this.transferRows.length
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
this.transferRows = []
|
||||
this.delayMoCount = 0
|
||||
this.delayRowCount = 0
|
||||
this.$emit('loaded', { delayRowCount: 0, delayMoCount: 0, transferCount: 0 })
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false
|
||||
this.$nextTick(() => this.updateTableHeight())
|
||||
})
|
||||
},
|
||||
transferIndex(index) {
|
||||
return (this.transferPage - 1) * this.transferPageSize + index + 1
|
||||
},
|
||||
formatQtyVal(val) {
|
||||
if (val == null || val === '') return '0'
|
||||
const n = Number(val)
|
||||
if (!Number.isFinite(n)) return '0'
|
||||
return parseFloat(n.toFixed(2))
|
||||
},
|
||||
diffClass(val) {
|
||||
const n = Number(val)
|
||||
if (!Number.isFinite(n) || n === 0) return 'diff-zero'
|
||||
return n > 0 ? 'diff-pos' : 'diff-neg'
|
||||
},
|
||||
formatOperNo(val) {
|
||||
if (val == null || val === '') return '—'
|
||||
const s = String(val)
|
||||
return s.endsWith('.0') ? s.slice(0, -2) : s
|
||||
},
|
||||
formatDate(val) {
|
||||
if (val == null || val === '') return '—'
|
||||
const s = String(val)
|
||||
if (s.includes('T')) return s.substring(0, 10)
|
||||
return s.length >= 10 ? s.substring(0, 10) : s
|
||||
},
|
||||
touchSyncTime() {
|
||||
const d = new Date()
|
||||
const pad = n => String(n).padStart(2, '0')
|
||||
this.lastSyncTime = `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())} 已同步`
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Barlow:wght@400;500;600;700&family=Barlow+Condensed:wght@600;700&display=swap');
|
||||
|
||||
.remind-page {
|
||||
--bg: #eceae4;
|
||||
--panel: #faf9f6;
|
||||
--dark: #1a2332;
|
||||
--muted: #5c6573;
|
||||
--amber: #d4820a;
|
||||
--teal: #1a7a6d;
|
||||
--red: #c0392b;
|
||||
--border: #c8c4ba;
|
||||
height: calc(100vh - 84px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 4px 8px;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
font-family: 'Barlow', 'Microsoft YaHei', sans-serif;
|
||||
color: var(--dark);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(26, 35, 50, 0.03) 25%, transparent 25%) -20px 0 / 40px 40px,
|
||||
var(--bg);
|
||||
}
|
||||
|
||||
.remind-page.embedded {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.remind-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
padding: 8px 14px;
|
||||
margin-bottom: 8px;
|
||||
background: var(--dark);
|
||||
border-radius: 4px;
|
||||
color: #f0ebe3;
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.brand-mark {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
background: var(--amber);
|
||||
color: var(--dark);
|
||||
border-radius: 3px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.brand h1 {
|
||||
margin: 0;
|
||||
font-family: 'Barlow Condensed', sans-serif;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.03em;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.brand p {
|
||||
margin: 1px 0 0;
|
||||
font-size: 11px;
|
||||
color: rgba(240, 235, 227, 0.5);
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sync-time {
|
||||
font-size: 11px;
|
||||
color: rgba(240, 235, 227, 0.5);
|
||||
}
|
||||
|
||||
.transfer-panel {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.panel-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.head-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.panel-head h2 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-family: 'Barlow Condensed', sans-serif;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.head-hint {
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.search-input { width: 200px; }
|
||||
.type-select { width: 100px; }
|
||||
.supplier-select { width: 200px; }
|
||||
|
||||
.table-wrap {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: 0; /* 配合 flex:1,让子表拿到真实高度以冻结表头 */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.remind-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.remind-table >>> .el-table__fixed,
|
||||
.remind-table >>> .el-table__fixed-right {
|
||||
box-shadow: 6px 0 10px rgba(26, 35, 50, 0.08);
|
||||
}
|
||||
|
||||
.remind-table >>> .el-table__header th {
|
||||
padding: 6px 0 !important;
|
||||
}
|
||||
|
||||
.remind-table >>> .el-table__header th .cell {
|
||||
color: #e8e4dc !important;
|
||||
line-height: 1.3;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.remind-table >>> .el-table__body td {
|
||||
padding: 6px 0 !important;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.remind-table >>> .el-table__body td .cell {
|
||||
line-height: 1.35;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.remind-table >>> .el-table__body tr:hover > td {
|
||||
background: rgba(212, 130, 10, 0.06) !important;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-variant-numeric: tabular-nums;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.diff-pos {
|
||||
color: var(--red);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.diff-neg {
|
||||
color: var(--teal);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.diff-zero {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.panel-footer {
|
||||
flex-shrink: 0;
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.hint b {
|
||||
color: var(--dark);
|
||||
font-family: 'Barlow Condensed', sans-serif;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.panel-head {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
.toolbar { justify-content: flex-start; }
|
||||
}
|
||||
</style>
|
||||
185
src/views/system/ratio/index.vue
Normal file
@ -0,0 +1,185 @@
|
||||
<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)"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="输入数量" prop="qty">
|
||||
<el-input-number
|
||||
v-model="queryParams.qty"
|
||||
:min="1"
|
||||
placeholder="齐套数量"
|
||||
controls-position="right"
|
||||
style="width: 150px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<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-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">
|
||||
<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="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">
|
||||
<template slot="header">
|
||||
<span>可用</span>
|
||||
<el-tooltip class="item" effect="dark" content="不包含不良品和废品" placement="top">
|
||||
<i class="el-icon-question" style="margin-left: 4px; color: #909399; cursor: pointer;" />
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<template slot-scope="scope">
|
||||
<span style="font-weight: bold;">{{ scope.row.availableQty || 0 }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="在途" align="center">
|
||||
<el-table-column label="采购在途" prop="poNotInQty" width="80" 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;">
|
||||
<strong>采购在途明细:</strong>
|
||||
<div v-for="(item, idx) in scope.row.poInTransit" :key="idx" style="margin-top: 5px; font-size: 12px; border-bottom: 1px solid #ebeef5; padding-bottom: 4px;">
|
||||
单号: <span style="color: #409EFF">{{ item.billNo }}</span><br>
|
||||
未入库: <span style="color: #E6A23C">{{ item.notInstockQty }}</span> |
|
||||
交期: {{ item.deliveryDate ? item.deliveryDate.substring(0, 10) : '-' }}
|
||||
</div>
|
||||
</div>
|
||||
<div slot="reference" style="cursor: pointer; color: #409EFF; font-weight: bold; text-decoration: underline;">
|
||||
{{ scope.row.poNotInQty || 0 }}
|
||||
</div>
|
||||
</el-popover>
|
||||
<span v-else>{{ scope.row.poNotInQty || 0 }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="生产在途" prop="moNotInQty" width="80" 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;">
|
||||
<strong>生产在途明细:</strong>
|
||||
<div v-for="(item, idx) in scope.row.moInTransit" :key="idx" style="margin-top: 5px; font-size: 12px; border-bottom: 1px solid #ebeef5; padding-bottom: 4px;">
|
||||
单号: <span style="color: #67C23A">{{ item.billNo }}</span><br>
|
||||
未入库: <span style="color: #E6A23C">{{ item.notInstockQty }}</span> |
|
||||
计划完工: {{ item.planFinishDate ? item.planFinishDate.substring(0, 10) : '-' }}
|
||||
</div>
|
||||
</div>
|
||||
<div slot="reference" style="cursor: pointer; color: #67C23A; font-weight: bold; text-decoration: underline;">
|
||||
{{ scope.row.moNotInQty || 0 }}
|
||||
</div>
|
||||
</el-popover>
|
||||
<span v-else>{{ scope.row.moNotInQty || 0 }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="最终可用" prop="finalAvailableQty" width="80" 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 v-if="queryParams.qty" label="齐套补差" prop="shortageQty" width="80" align="right" fixed="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>
|
||||
</div>
|
||||
|
||||
<el-empty v-else description="请输入速比并点击查询获取数据" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getRatioBuildCapacity } from '@/api/system/ratio'
|
||||
|
||||
export default {
|
||||
name: 'RatioBuildCapacity',
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
hasSearched: false,
|
||||
queryParams: {
|
||||
ratioValue: undefined,
|
||||
qty: undefined
|
||||
},
|
||||
reportData: null,
|
||||
dataList: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleQuery() {
|
||||
if (!this.queryParams.ratioValue) {
|
||||
this.$modal.msgWarning('请输入速比')
|
||||
return
|
||||
}
|
||||
this.hasSearched = true
|
||||
this.loading = true
|
||||
getRatioBuildCapacity(this.queryParams).then(res => {
|
||||
if (res.data) {
|
||||
this.reportData = res.data
|
||||
this.dataList = res.data.items || []
|
||||
} else {
|
||||
this.reportData = null
|
||||
this.dataList = []
|
||||
}
|
||||
}).catch(() => {
|
||||
this.reportData = null
|
||||
this.dataList = []
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
resetQuery() {
|
||||
this.resetForm('queryForm')
|
||||
this.reportData = null
|
||||
this.dataList = []
|
||||
this.hasSearched = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.box-card {
|
||||
margin-top: 15px;
|
||||
}
|
||||
</style>
|
||||
@ -2,6 +2,7 @@
|
||||
<div>
|
||||
<el-form
|
||||
v-for="(n,i) in list"
|
||||
:key="i"
|
||||
:inline="true"
|
||||
:model="n"
|
||||
ref="ruleForm"
|
||||
|
||||
@ -41,6 +41,11 @@
|
||||
v-hasPermi="['system:route:pushRouteBom']">推送工艺
|
||||
</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<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>
|
||||
@ -154,10 +159,20 @@
|
||||
<el-table-column label="工序控制码" width="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-form-item v-if="scope.row.isEdit" :prop="'routeList.' + scope.$index + '.processControl'">
|
||||
<el-select v-model="scope.row.processControl" placeholder="请选择工序控制码" clearable>
|
||||
<el-option label="汇报+免检" value="汇报+免检"></el-option>
|
||||
<el-option label="委外+质量" value="委外+质量"></el-option>
|
||||
<el-option label="汇报+质量" value="汇报+质量"></el-option>
|
||||
<el-select
|
||||
v-model="scope.row.processControl"
|
||||
placeholder="请选择工序控制码"
|
||||
clearable
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
>
|
||||
<el-option
|
||||
v-for="item in processControlOptions"
|
||||
:key="item"
|
||||
:label="item"
|
||||
:value="item"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<span v-else>{{ scope.row.processControl }}</span>
|
||||
@ -323,10 +338,10 @@
|
||||
<el-select v-model="form.workCenter" placeholder="请选择工作中心" clearable>
|
||||
<el-option label="机一工段" value="机一工段"></el-option>
|
||||
<el-option label="机二工段" value="机二工段"></el-option>
|
||||
<el-option label="机三工段" value="机三工段"></el-option>
|
||||
<el-option label="焊接工段" value="焊接工段"></el-option>
|
||||
<el-option label="装一工段" value="装一工段"></el-option>
|
||||
<el-option label="装二工段" value="装二工段"></el-option>
|
||||
<el-option label="铆焊工段" value="铆焊工段"></el-option>
|
||||
<el-option label="下料工段" value="下料工段"></el-option>
|
||||
<el-option label="电钳工段" value="电钳工段"></el-option>
|
||||
<el-option label="委外中心" value="委外中心"></el-option>
|
||||
</el-select>
|
||||
@ -353,10 +368,20 @@
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="工序控制" prop="processControl">
|
||||
<el-select v-model="form.processControl" placeholder="请选择工序控制码" clearable>
|
||||
<el-option label="汇报+免检" value="汇报+免检"></el-option>
|
||||
<el-option label="委外+质量" value="委外+质量"></el-option>
|
||||
<el-option label="汇报+质量" value="汇报+质量"></el-option>
|
||||
<el-select
|
||||
v-model="form.processControl"
|
||||
placeholder="请选择工序控制码"
|
||||
clearable
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
>
|
||||
<el-option
|
||||
v-for="item in processControlOptions"
|
||||
:key="item"
|
||||
:label="item"
|
||||
:value="item"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@ -609,7 +634,7 @@ import {
|
||||
import { getToken } from "../../../utils/auth";
|
||||
import Fromed from './fromed'
|
||||
import Bomfrom from './bomfrom'
|
||||
import { getProcessInfoList, pushRouteBom, onSave, getBomInfo, getProcessRouteList } from "@/api/system/route";
|
||||
import { getProcessInfoList, pushRouteBom, approveRoute, onSave, getBomInfo, getProcessRouteList } from "@/api/system/route";
|
||||
import { listMaterial } from "@/api/system/material";
|
||||
|
||||
import dict from "@/utils/dict";
|
||||
@ -647,6 +672,12 @@ export default {
|
||||
url: process.env.VUE_APP_BASE_API + "/system/route/importDataTime"
|
||||
},
|
||||
materialOptions: [], // 用于存储物料信息
|
||||
processControlOptions: [
|
||||
'汇报+免检',
|
||||
'汇报+质量',
|
||||
'委外+质量',
|
||||
'委外+汇报'
|
||||
],
|
||||
showtable: true,
|
||||
expand: true,
|
||||
bomData: [],
|
||||
@ -718,14 +749,14 @@ export default {
|
||||
},
|
||||
// 对应关系映射:工序名称 -> 工作中心
|
||||
processToWorkCenterMap: {
|
||||
// 铆焊工段
|
||||
'数控火焰下料': '铆焊工段',
|
||||
'数控激光下料': '铆焊工段',
|
||||
'数控切管下料': '铆焊工段',
|
||||
'水虎鱼剪折冲': '铆焊工段',
|
||||
'锯床下料': '铆焊工段',
|
||||
'抛丸': '铆焊工段',
|
||||
'组焊': '铆焊工段',
|
||||
// 下料工段
|
||||
'数控火焰下料': '下料工段',
|
||||
'数控激光下料': '下料工段',
|
||||
'数控切管下料': '下料工段',
|
||||
'水虎鱼剪折冲': '下料工段',
|
||||
'锯床下料': '下料工段',
|
||||
'抛丸': '下料工段',
|
||||
'组焊': '下料工段',
|
||||
'普车': '机一工段',
|
||||
'普铣': '机一工段',
|
||||
'龙门铣': '机一工段',
|
||||
@ -738,14 +769,14 @@ export default {
|
||||
'双主轴': '机二工段',
|
||||
'车铣复合': '机二工段',
|
||||
'线切割': '机二工段',
|
||||
'卧加63': '机三工段',
|
||||
'5032龙门': '机三工段',
|
||||
'3018龙门': '机三工段',
|
||||
'2013龙门': '机三工段',
|
||||
'立加1370': '机三工段',
|
||||
'立加1177': '机三工段',
|
||||
'立加850': '机三工段',
|
||||
'数插5035': '机三工段',
|
||||
'卧加63': '焊接工段',
|
||||
'5032龙门': '焊接工段',
|
||||
'3018龙门': '焊接工段',
|
||||
'2013龙门': '焊接工段',
|
||||
'立加1370': '焊接工段',
|
||||
'立加1177': '焊接工段',
|
||||
'立加850': '焊接工段',
|
||||
'数插5035': '焊接工段',
|
||||
'装配1': '装一工段',
|
||||
'装配2': '装二工段',
|
||||
'电装': '电钳工段',
|
||||
@ -1014,98 +1045,12 @@ export default {
|
||||
},
|
||||
|
||||
|
||||
// 处理工作中心变化
|
||||
handleWorkCenterChange(scope) {
|
||||
const { row } = scope;
|
||||
const currentIndex = this.form.routeList.findIndex(item => item === row);
|
||||
|
||||
// 移除自动设置工序控制码的逻辑
|
||||
// 只需记录工作中心的变化,不再自动填写
|
||||
console.log("工作中心变化:", row.workCenter);
|
||||
},
|
||||
|
||||
// 验证工序控制
|
||||
validateProcessControl(row, index) {
|
||||
// 验证逻辑保持不变
|
||||
if (row.workCenter === '委外中心' && row.processControl !== '委外+质量') {
|
||||
return {
|
||||
valid: false,
|
||||
message: '委外中心的工序控制必须为"委外+质量"',
|
||||
correctValue: '委外+质量'
|
||||
};
|
||||
}
|
||||
|
||||
if (index > 0) {
|
||||
const previousRow = this.form.routeList[index - 1];
|
||||
if (previousRow.workCenter === row.workCenter &&
|
||||
previousRow.workCenter !== '委外中心' &&
|
||||
row.processControl !== '汇报+免检') {
|
||||
return {
|
||||
valid: false,
|
||||
message: '与上道工序工作中心相同时,工序控制应为"汇报+免检"',
|
||||
correctValue: '汇报+免检'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
},
|
||||
|
||||
// 开始编辑行
|
||||
handleRowEdit(scope) {
|
||||
// 保存原始数据
|
||||
scope.row.originalData = JSON.parse(JSON.stringify(scope.row));
|
||||
// 设置编辑状态
|
||||
this.$set(scope.row, 'isEdit', true);
|
||||
|
||||
// 如果是委外中心,确保工序控制正确
|
||||
if (scope.row.workCenter === '委外中心') {
|
||||
this.$set(scope.row, 'processControl', '委外+质量');
|
||||
}
|
||||
},
|
||||
|
||||
// 判断工序控制是否应该禁用
|
||||
isProcessControlDisabled(row, index) {
|
||||
return row.workCenter === '委外中心' || this.shouldBeAutoFreeInspection(row, index);
|
||||
},
|
||||
|
||||
// 设置工序控制
|
||||
setProcessControl(row, index) {
|
||||
console.log('进入setProcessControl方法'); // 添加日志
|
||||
if (row.workCenter === '委外中心') {
|
||||
console.log('设置委外+质量'); // 添加日志
|
||||
this.$set(row, 'processControl', '委外+质量');
|
||||
} else if (this.shouldBeAutoFreeInspection(row, index)) {
|
||||
console.log('设置汇报+免检'); // 添加日志
|
||||
this.$set(row, 'processControl', '汇报+免检');
|
||||
}
|
||||
},
|
||||
// 验证工序控制
|
||||
|
||||
// 判断是否应该自动设置为免检
|
||||
shouldBeAutoFreeInspection(row, index) {
|
||||
if (!row.parentId || index === 0 || row.workCenter === '委外中心') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 获取上一道工序
|
||||
const parent = this.form.routeList.find(item => item.id === row.parentId);
|
||||
if (!parent || !parent.children) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentIndex = parent.children.findIndex(item => item === row);
|
||||
if (currentIndex <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const prevProcess = parent.children[currentIndex - 1];
|
||||
|
||||
|
||||
return prevProcess && prevProcess.workCenter === row.workCenter;
|
||||
},
|
||||
|
||||
|
||||
/** 导入时间按钮操作 */
|
||||
handleImportTime() {
|
||||
this.timeUpload.title = "导入时间";
|
||||
@ -1268,14 +1213,6 @@ export default {
|
||||
};
|
||||
routeList.push(newRow);
|
||||
this.showParentSaveBtn = props.row.id;
|
||||
|
||||
// 添加新行后,监听工作中心变化
|
||||
this.$nextTick(() => {
|
||||
if (newRow.workCenter) {
|
||||
this.setProcessControl(newRow, routeList.length - 1);
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
// 删除子集
|
||||
@ -1479,9 +1416,8 @@ export default {
|
||||
},
|
||||
|
||||
|
||||
/**推送工艺 */
|
||||
async pushRouteBom() {
|
||||
// 获取生产订单号
|
||||
/** 推送工艺 / 审核并推送 共用逻辑 */
|
||||
async executeRoutePush({ confirmMessage, loadingText, apiFn, errorTitle, successTitle }) {
|
||||
const productionOrderNo = this.params.productionOrderNo;
|
||||
if (!productionOrderNo) {
|
||||
this.$modal.msgError("未获取到生产订单号,请检查!");
|
||||
@ -1491,47 +1427,36 @@ export default {
|
||||
this.$modal.msgError("请填写分组名称!");
|
||||
return;
|
||||
}
|
||||
let loading = null;
|
||||
try {
|
||||
// 显示确认对话框
|
||||
await this.$confirm(
|
||||
'推送工艺可能需要较长时间,请耐心等待。是否继续?',
|
||||
'提示',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
);
|
||||
// 显示全屏 loading
|
||||
const loading = this.$loading({
|
||||
await this.$confirm(confirmMessage, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
});
|
||||
loading = this.$loading({
|
||||
lock: true,
|
||||
text: '正在推送工艺,请稍候...',
|
||||
text: loadingText,
|
||||
spinner: 'el-icon-loading',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
});
|
||||
// 重置表单状态
|
||||
this.reset();
|
||||
// 设置超时检测
|
||||
const timeout = 600000; // 10分钟超时
|
||||
const timeout = 600000;
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(new Error('推送操作超时,请检查后台处理状态'));
|
||||
}, timeout);
|
||||
});
|
||||
// 调用推送 API 并设置超时
|
||||
const response = await Promise.race([
|
||||
pushRouteBom(productionOrderNo, this.routeGroupName.trim()),
|
||||
apiFn(productionOrderNo, this.routeGroupName.trim()),
|
||||
timeoutPromise
|
||||
]);
|
||||
// 处理后端返回的数据 - 适配新的返回结构 R<ProcessRoutePushResultDTO>
|
||||
const { duplicateRoutes, failedRoutes, successfulRoutes } = response.data;
|
||||
// 提取物料编码和错误信息
|
||||
const duplicateCodes = duplicateRoutes.join(', ');
|
||||
const failedInfo = failedRoutes.map(route =>
|
||||
`${route.materialCode} (${route.errorMessage || '未知错误'})`
|
||||
).join(', ');
|
||||
const successfulCodes = successfulRoutes.map(route => route.materialCode).join(', ');
|
||||
// 显示成功消息
|
||||
this.$alert(`
|
||||
<div><strong>成功 (${successfulRoutes.length}):</strong></div>
|
||||
<div style="margin-left: 20px;">${successfulCodes || '无'}</div>
|
||||
@ -1539,38 +1464,71 @@ export default {
|
||||
<div style="margin-left: 20px;">${failedInfo || '无'}</div>
|
||||
<div><strong>重复 (${duplicateRoutes.length}):</strong></div>
|
||||
<div style="margin-left: 20px;">${duplicateCodes || '无'}</div>
|
||||
`, '推送完成', {
|
||||
`, successTitle || '推送完成', {
|
||||
dangerouslyUseHTMLString: true,
|
||||
confirmButtonText: '确定'
|
||||
});
|
||||
|
||||
// 刷新列表
|
||||
await this.getList();
|
||||
|
||||
// 关闭 loading
|
||||
loading.close();
|
||||
|
||||
} catch (error) {
|
||||
// 错误处理
|
||||
console.error('[推送工艺失败]:', error);
|
||||
|
||||
// 显示错误通知
|
||||
console.error(`[${errorTitle}]:`, error);
|
||||
this.$notify({
|
||||
title: '推送工艺失败',
|
||||
message: error.message || '推送工艺失败,请重试!',
|
||||
title: errorTitle,
|
||||
message: error.message || `${errorTitle},请重试!`,
|
||||
type: 'error',
|
||||
duration: 0,
|
||||
showClose: true
|
||||
});
|
||||
|
||||
} finally {
|
||||
// 确保 loading 被关闭
|
||||
if (this.$loading) {
|
||||
this.$loading().close();
|
||||
if (loading) {
|
||||
loading.close();
|
||||
}
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**推送工艺 */
|
||||
pushRouteBom() {
|
||||
return this.executeRoutePush({
|
||||
confirmMessage: '推送工艺可能需要较长时间,请耐心等待。是否继续?',
|
||||
loadingText: '正在推送工艺,请稍候...',
|
||||
apiFn: pushRouteBom,
|
||||
errorTitle: '推送工艺失败'
|
||||
});
|
||||
},
|
||||
|
||||
/** 审核通过:更新状态并发送钉钉通知,不推送金蝶、不发送工艺计划表 */
|
||||
async approveRoute() {
|
||||
const productionOrderNo = this.params.productionOrderNo;
|
||||
if (!productionOrderNo) {
|
||||
this.$modal.msgError("未获取到生产订单号,请检查!");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.$confirm(
|
||||
'将通过工艺审核并发送钉钉通知,不会推送金蝶。是否继续?',
|
||||
'提示',
|
||||
{ confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }
|
||||
);
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
text: '正在审核并发送通知...',
|
||||
spinner: 'el-icon-loading',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
});
|
||||
try {
|
||||
const res = await approveRoute(productionOrderNo);
|
||||
this.$modal.msgSuccess((res && res.msg) || '审核通过');
|
||||
await this.getList();
|
||||
} finally {
|
||||
loading.close();
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
this.$modal.msgError(error.message || '审核失败');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/** 更新计划时间间*/
|
||||
updateProcessPlan(row) {
|
||||
// 若未勾选任何行,则提示并返回
|
||||
@ -1704,8 +1662,6 @@ export default {
|
||||
}
|
||||
});
|
||||
},
|
||||
// 验证所有工序控制
|
||||
|
||||
/** 删除按钮操作 */
|
||||
handleDelete(scope) {
|
||||
const ids = [scope.row.id];
|
||||
@ -1847,8 +1803,6 @@ export default {
|
||||
this.loading = true;
|
||||
// 获取行数据并创建副本
|
||||
const row = scope.row;
|
||||
// 验证委外中心的工序控制
|
||||
|
||||
const saveData = {
|
||||
id: row.id,
|
||||
parentId: row.parentId,
|
||||
|
||||
@ -345,10 +345,10 @@
|
||||
<el-select v-model="form.workCenter" placeholder="请选择工作中心" clearable>
|
||||
<el-option label="机一工段" value="机一工段"></el-option>
|
||||
<el-option label="机二工段" value="机二工段"></el-option>
|
||||
<el-option label="机三工段" value="机三工段"></el-option>
|
||||
<el-option label="焊接工段" value="焊接工段"></el-option>
|
||||
<el-option label="装一工段" value="装一工段"></el-option>
|
||||
<el-option label="装二工段" value="装二工段"></el-option>
|
||||
<el-option label="铆焊工段" value="铆焊工段"></el-option>
|
||||
<el-option label="下料工段" value="下料工段"></el-option>
|
||||
<el-option label="电钳工段" value="电钳工段"></el-option>
|
||||
<el-option label="委外中心" value="委外中心"></el-option>
|
||||
</el-select>
|
||||
@ -742,14 +742,14 @@ export default {
|
||||
},
|
||||
// 对应关系映射:工序名称 -> 工作中心
|
||||
processToWorkCenterMap: {
|
||||
// 铆焊工段
|
||||
'数控火焰下料': '铆焊工段',
|
||||
'数控激光下料': '铆焊工段',
|
||||
'数控切管下料': '铆焊工段',
|
||||
'水虎鱼剪折冲': '铆焊工段',
|
||||
'锯床下料': '铆焊工段',
|
||||
'抛丸': '铆焊工段',
|
||||
'组焊': '铆焊工段',
|
||||
// 下料工段
|
||||
'数控火焰下料': '下料工段',
|
||||
'数控激光下料': '下料工段',
|
||||
'数控切管下料': '下料工段',
|
||||
'水虎鱼剪折冲': '下料工段',
|
||||
'锯床下料': '下料工段',
|
||||
'抛丸': '下料工段',
|
||||
'组焊': '下料工段',
|
||||
'普车': '机一工段',
|
||||
'普铣': '机一工段',
|
||||
'龙门铣': '机一工段',
|
||||
@ -762,14 +762,14 @@ export default {
|
||||
'双主轴': '机二工段',
|
||||
'车铣复合': '机二工段',
|
||||
'线切割': '机二工段',
|
||||
'卧加63': '机三工段',
|
||||
'5032龙门': '机三工段',
|
||||
'3018龙门': '机三工段',
|
||||
'2013龙门': '机三工段',
|
||||
'立加1370': '机三工段',
|
||||
'立加1177': '机三工段',
|
||||
'立加850': '机三工段',
|
||||
'数插5035': '机三工段',
|
||||
'卧加63': '焊接工段',
|
||||
'5032龙门': '焊接工段',
|
||||
'3018龙门': '焊接工段',
|
||||
'2013龙门': '焊接工段',
|
||||
'立加1370': '焊接工段',
|
||||
'立加1177': '焊接工段',
|
||||
'立加850': '焊接工段',
|
||||
'数插5035': '焊接工段',
|
||||
'装配1': '装一工段',
|
||||
'装配2': '装二工段',
|
||||
'电装': '电钳工段',
|
||||
|
||||
424
src/views/system/workAlertPushConfig/index.vue
Normal file
@ -0,0 +1,424 @@
|
||||
<template>
|
||||
<div class="app-container push-config-page">
|
||||
<el-alert
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="tip-banner"
|
||||
title="① 已转入未转出 / 开工未转入:工段矩阵 → 各工段长收单 Sheet;「汇总接收人」→ 收全工段多 Sheet Excel(留空则发给矩阵里所有人)。② 汇报未质检 / 质检未入库:填钉钉用户ID。③ 其他:填用户ID私发,留空发群。"
|
||||
/>
|
||||
|
||||
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="80px">
|
||||
<el-form-item label="推送名称" prop="pushName">
|
||||
<el-input v-model="queryParams.pushName" placeholder="推送名称" clearable @keyup.enter.native="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="enabled">
|
||||
<el-select v-model="queryParams.enabled" placeholder="全部" clearable style="width: 120px">
|
||||
<el-option label="启用" value="1" />
|
||||
<el-option label="停用" value="0" />
|
||||
</el-select>
|
||||
</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:workAlertPushConfig:add']">新增</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="success" plain icon="el-icon-edit" size="mini" :disabled="single" @click="handleUpdate" v-hasPermi="['system:workAlertPushConfig:edit']">修改</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:workAlertPushConfig:remove']">删除</el-button>
|
||||
</el-col>
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList" />
|
||||
</el-row>
|
||||
|
||||
<div class="table-wrap">
|
||||
<el-table
|
||||
ref="configTable"
|
||||
v-loading="loading"
|
||||
:data="configList"
|
||||
row-key="id"
|
||||
border
|
||||
stripe
|
||||
style="width: 100%"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="48" align="center" fixed="left" />
|
||||
<el-table-column label="推送名称" prop="pushName" min-width="130" show-overflow-tooltip fixed="left" />
|
||||
<el-table-column label="任务编码" prop="pushCode" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="推送时间" prop="pushTime" width="88" align="center" />
|
||||
<el-table-column label="启用" prop="enabled" width="72" 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="dingUserIds" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="分对象" prop="recipientRules" min-width="100" show-overflow-tooltip>
|
||||
<template slot-scope="{ row }">
|
||||
<span v-if="row.recipientRules">{{ formatRecipientSummary(row.recipientRules) }}</span>
|
||||
<span v-else class="text-muted">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="上次推送" prop="lastPushDate" width="108" align="center">
|
||||
<template slot-scope="{ row }">{{ row.lastPushDate || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" prop="remark" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="220" align="center" class-name="action-col">
|
||||
<template slot-scope="{ row }">
|
||||
<div class="action-cell">
|
||||
<el-button size="mini" type="text" icon="el-icon-video-play" @click="handleTrigger(row)" v-hasPermi="['system:workAlertPushConfig:trigger']">立即推送</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(row)" v-hasPermi="['system:workAlertPushConfig:edit']">编辑</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(row)" v-hasPermi="['system:workAlertPushConfig:remove']">删除</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<pagination
|
||||
v-show="total > 0"
|
||||
:total="total"
|
||||
:page.sync="queryParams.pageNum"
|
||||
:limit.sync="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
|
||||
<el-dialog :title="title" :visible.sync="open" width="720px" append-to-body @close="cancel">
|
||||
<el-form ref="form" :model="form" :rules="rules" label-width="110px">
|
||||
<el-form-item label="推送任务" prop="pushCode">
|
||||
<el-select v-model="form.pushCode" placeholder="选择推送任务" filterable style="width: 100%" :disabled="!!form.id" @change="onPushCodeChange">
|
||||
<el-option v-for="item in pushTypeOptions" :key="item.code" :label="item.name + ' (' + item.code + ')'" :value="item.code">
|
||||
<span>{{ item.name }}</span>
|
||||
<span style="float: right; color: #8492a6; font-size: 12px">{{ item.code }}</span>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="推送名称" prop="pushName">
|
||||
<el-input v-model="form.pushName" placeholder="显示名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="每日推送时间" prop="pushTime">
|
||||
<el-time-select
|
||||
v-model="form.pushTime"
|
||||
:picker-options="{ start: '06:00', step: '00:05', end: '22:00' }"
|
||||
placeholder="选择时间"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="isPerWorkCenterMode" label="工段接收人">
|
||||
<div class="wc-recipient-block">
|
||||
<p class="wc-tip">各工段长 StaffId(逗号分隔)。有数据且已填写的工段才会收到本工段单 Sheet Excel。</p>
|
||||
<el-table :data="workCenterRows" border size="mini" max-height="280">
|
||||
<el-table-column label="工段" prop="name" width="100" />
|
||||
<el-table-column label="StaffId">
|
||||
<template slot-scope="{ row }">
|
||||
<el-input v-model="row.userIds" size="mini" placeholder="工段长 StaffId" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="isPerWorkCenterMode" label="汇总接收人">
|
||||
<el-input
|
||||
v-model="form.dingUserIds"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="收全工段多 Sheet 汇总 Excel 的人员 StaffId(如生产主管);留空则发给工段矩阵里全部人员"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item v-else label="钉钉用户ID" prop="dingUserIds">
|
||||
<el-input
|
||||
v-model="form.dingUserIds"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
:placeholder="roleRecipientPlaceholder"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="群会话ID">
|
||||
<el-input v-model="form.openConversationId" placeholder="仅群发模式需要;私发模式可留空" />
|
||||
</el-form-item>
|
||||
<el-form-item label="机器人Code">
|
||||
<el-input v-model="form.robotCode" placeholder="留空使用 dingtalk.robot-code(需与机器人应用一致)" />
|
||||
</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-item label="排序">
|
||||
<el-input-number v-model="form.sortOrder" :min="0" :max="9999" controls-position="right" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="备注说明" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
listWorkAlertPushConfig,
|
||||
getWorkAlertPushConfig,
|
||||
addWorkAlertPushConfig,
|
||||
updateWorkAlertPushConfig,
|
||||
delWorkAlertPushConfig,
|
||||
listPushTypes,
|
||||
triggerWorkAlertPush
|
||||
} from '@/api/system/workAlertPushConfig'
|
||||
|
||||
const WORK_CENTERS = [
|
||||
'机一工段', '机二工段', '焊接工段', '装一工段', '装二工段', '机加委外', '电钳工段', '下料工段'
|
||||
]
|
||||
|
||||
const PER_WORK_CENTER_CODES = ['getMassageTransInPendingOut', 'getMassageStartedNotTransIn']
|
||||
|
||||
export default {
|
||||
name: 'WorkAlertPushConfig',
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
buttonLoading: false,
|
||||
ids: [],
|
||||
single: true,
|
||||
multiple: true,
|
||||
showSearch: true,
|
||||
total: 0,
|
||||
configList: [],
|
||||
pushTypeOptions: [],
|
||||
title: '',
|
||||
open: false,
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pushName: undefined,
|
||||
enabled: undefined
|
||||
},
|
||||
form: {},
|
||||
workCenterRows: [],
|
||||
rules: {
|
||||
pushCode: [{ required: true, message: '请选择推送任务', trigger: 'change' }],
|
||||
pushName: [{ required: true, message: '推送名称不能为空', trigger: 'blur' }],
|
||||
pushTime: [{ required: true, message: '请选择推送时间', trigger: 'change' }],
|
||||
enabled: [{ required: true, message: '请选择启用状态', trigger: 'change' }]
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isPerWorkCenterMode() {
|
||||
return PER_WORK_CENTER_CODES.includes(this.form.pushCode)
|
||||
},
|
||||
roleRecipientPlaceholder() {
|
||||
if (this.form.pushCode === 'getProcessReportNotInspected') {
|
||||
return '质检人员 StaffId,多个英文逗号分隔(私发单 Sheet「汇报未质检」)'
|
||||
}
|
||||
if (this.form.pushCode === 'getProcessReportNotStockedIn') {
|
||||
return '库房人员 StaffId,多个英文逗号分隔(私发单 Sheet「质检未入库」)'
|
||||
}
|
||||
return '私发接收人 StaffId,多个英文逗号分隔;留空则发默认群'
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadPushTypes()
|
||||
this.getList()
|
||||
},
|
||||
methods: {
|
||||
loadPushTypes() {
|
||||
listPushTypes().then(res => {
|
||||
this.pushTypeOptions = res.data || []
|
||||
})
|
||||
},
|
||||
getList() {
|
||||
this.loading = true
|
||||
listWorkAlertPushConfig(this.queryParams).then(response => {
|
||||
this.configList = response.rows
|
||||
this.total = response.total
|
||||
this.loading = false
|
||||
this.layoutTable()
|
||||
})
|
||||
},
|
||||
layoutTable() {
|
||||
this.$nextTick(() => {
|
||||
const table = this.$refs.configTable
|
||||
if (table && table.doLayout) {
|
||||
table.doLayout()
|
||||
}
|
||||
})
|
||||
},
|
||||
onPushCodeChange(code) {
|
||||
const item = this.pushTypeOptions.find(p => p.code === code)
|
||||
if (item && !this.form.pushName) {
|
||||
this.form.pushName = item.name
|
||||
this.form.remark = item.remark
|
||||
}
|
||||
if (PER_WORK_CENTER_CODES.includes(code)) {
|
||||
this.initWorkCenterRows(this.form.recipientRules)
|
||||
}
|
||||
},
|
||||
initWorkCenterRows(recipientRulesJson) {
|
||||
this.workCenterRows = WORK_CENTERS.map(name => ({ name, userIds: '' }))
|
||||
if (!recipientRulesJson) return
|
||||
try {
|
||||
const obj = JSON.parse(recipientRulesJson)
|
||||
this.workCenterRows.forEach(row => {
|
||||
if (obj[row.name]) {
|
||||
row.userIds = obj[row.name]
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
/* ignore invalid json */
|
||||
}
|
||||
},
|
||||
buildRecipientRulesJson() {
|
||||
if (!this.isPerWorkCenterMode) return undefined
|
||||
const obj = {}
|
||||
this.workCenterRows.forEach(row => {
|
||||
if (row.userIds && row.userIds.trim()) {
|
||||
obj[row.name] = row.userIds.trim()
|
||||
}
|
||||
})
|
||||
return Object.keys(obj).length ? JSON.stringify(obj) : undefined
|
||||
},
|
||||
formatRecipientSummary(json) {
|
||||
try {
|
||||
const obj = JSON.parse(json)
|
||||
const keys = Object.keys(obj)
|
||||
return keys.length ? keys.length + ' 个工段' : '—'
|
||||
} catch (e) {
|
||||
return '已配置'
|
||||
}
|
||||
},
|
||||
cancel() {
|
||||
this.open = false
|
||||
this.reset()
|
||||
},
|
||||
reset() {
|
||||
this.form = {
|
||||
id: undefined,
|
||||
pushCode: undefined,
|
||||
pushName: undefined,
|
||||
pushTime: '08:30',
|
||||
dingUserIds: undefined,
|
||||
recipientRules: undefined,
|
||||
openConversationId: undefined,
|
||||
robotCode: undefined,
|
||||
enabled: '0',
|
||||
sortOrder: 0,
|
||||
remark: undefined
|
||||
}
|
||||
this.workCenterRows = []
|
||||
this.resetForm('form')
|
||||
},
|
||||
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 = '新增预警推送配置'
|
||||
},
|
||||
handleUpdate(row) {
|
||||
this.reset()
|
||||
const id = row.id || this.ids[0]
|
||||
getWorkAlertPushConfig(id).then(response => {
|
||||
this.form = response.data
|
||||
this.initWorkCenterRows(this.form.recipientRules)
|
||||
this.open = true
|
||||
this.title = '修改预警推送配置'
|
||||
})
|
||||
},
|
||||
submitForm() {
|
||||
this.$refs.form.validate(valid => {
|
||||
if (!valid) return
|
||||
this.buttonLoading = true
|
||||
const payload = { ...this.form }
|
||||
if (this.isPerWorkCenterMode) {
|
||||
payload.recipientRules = this.buildRecipientRulesJson()
|
||||
} else {
|
||||
payload.recipientRules = undefined
|
||||
}
|
||||
const req = payload.id ? updateWorkAlertPushConfig(payload) : addWorkAlertPushConfig(payload)
|
||||
req.then(() => {
|
||||
this.$modal.msgSuccess(this.form.id ? '修改成功' : '新增成功')
|
||||
this.open = false
|
||||
this.getList()
|
||||
}).finally(() => {
|
||||
this.buttonLoading = false
|
||||
})
|
||||
})
|
||||
},
|
||||
handleDelete(row) {
|
||||
const ids = row.id ? [row.id] : this.ids
|
||||
this.$modal.confirm('是否确认删除选中的配置?').then(() => {
|
||||
return delWorkAlertPushConfig(ids.join(','))
|
||||
}).then(() => {
|
||||
this.getList()
|
||||
this.$modal.msgSuccess('删除成功')
|
||||
}).catch(() => {})
|
||||
},
|
||||
handleTrigger(row) {
|
||||
this.$modal.confirm('确认立即执行「' + row.pushName + '」推送?').then(() => {
|
||||
return triggerWorkAlertPush(row.id)
|
||||
}).then(() => {
|
||||
this.$modal.msgSuccess('推送任务已触发')
|
||||
this.getList()
|
||||
}).catch(() => {})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.push-config-page .tip-banner {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.wc-recipient-block .wc-tip {
|
||||
margin: 0 0 8px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.table-wrap {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.action-cell {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px 4px;
|
||||
min-height: 28px;
|
||||
}
|
||||
|
||||
.push-config-page ::v-deep .action-col .cell {
|
||||
overflow: visible;
|
||||
white-space: normal;
|
||||
}
|
||||
</style>
|
||||
1827
src/views/system/workCenterAlert/index.vue
Normal file
@ -31,8 +31,16 @@ module.exports = {
|
||||
devServer: {
|
||||
host: '0.0.0.0',
|
||||
port: port,
|
||||
open: true,
|
||||
open: false,
|
||||
proxy: {
|
||||
// 出图状态接口的跨域代理
|
||||
'/plot-api': {
|
||||
target: 'http://192.168.5.18:9000',
|
||||
changeOrigin: true,
|
||||
pathRewrite: {
|
||||
'^/plot-api': ''
|
||||
}
|
||||
},
|
||||
// detail: https://cli.vuejs.org/config/#devserver-proxy
|
||||
[process.env.VUE_APP_BASE_API]: {
|
||||
target: `http://127.0.0.1:8033`,
|
||||
@ -144,5 +152,10 @@ module.exports = {
|
||||
transpileDependencies: [
|
||||
'vue-echarts',
|
||||
'resize-detector'
|
||||
]
|
||||
],
|
||||
pluginOptions: {
|
||||
electronBuilder: {
|
||||
nodeIntegration: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||