This commit is contained in:
tzy 2026-08-11 14:46:49 +08:00
parent 29566db751
commit b82ec60b61
47 changed files with 14629 additions and 1506 deletions

35
.gitignore vendored
View File

@ -1,3 +1,36 @@
# Dependencies
node_modules/
package-lock.json package-lock.json
yarn.lock 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

View File

@ -5,10 +5,12 @@
"author": "tzy", "author": "tzy",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"dev": "vue-cli-service serve", "dev": "set NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve",
"build:prod": "vue-cli-service build", "build:prod": "set NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service build",
"preview": "node build/index.js --preview", "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": { "husky": {
"hooks": { "hooks": {
@ -82,6 +84,7 @@
"compression-webpack-plugin": "5.0.2", "compression-webpack-plugin": "5.0.2",
"connect": "3.6.6", "connect": "3.6.6",
"echarts": "^5.6.0", "echarts": "^5.6.0",
"electron": "^13.0.0",
"eslint": "^6.0.1", "eslint": "^6.0.1",
"eslint-plugin-vue": "7.2.0", "eslint-plugin-vue": "7.2.0",
"less": "^4.2.0", "less": "^4.2.0",
@ -92,6 +95,7 @@
"sass-loader": "10.1.1", "sass-loader": "10.1.1",
"script-ext-html-webpack-plugin": "2.1.5", "script-ext-html-webpack-plugin": "2.1.5",
"svg-sprite-loader": "5.1.1", "svg-sprite-loader": "5.1.1",
"vue-cli-plugin-electron-builder": "^2.1.1",
"vue-template-compiler": "2.6.12" "vue-template-compiler": "2.6.12"
}, },
"engines": { "engines": {

View 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 }
})
}

View 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
})
}

View 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'
})
}

View File

@ -74,3 +74,40 @@ export function queryFromK3OperationPlanningMain(query) {
params: 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 }
})
}

View File

@ -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) { export function exportRoute(id) {
return request({ return request({
@ -156,3 +165,61 @@ export function exportRoute3(id) {
responseType: 'blob' 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'
})
}

View 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'
})
}

View 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'
})
}

View 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
View 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
}
})
}

View File

@ -98,6 +98,14 @@ export function pushRouteBom(rooteProdet, groupName) {
params: { rooteProdet, groupName }, params: { rooteProdet, groupName },
}); });
} }
export function approveRoute(rooteProdet) {
return request({
url: '/system/route/approveRoute',
method: 'post',
params: { rooteProdet },
});
}
export function updateProcessPlan(rooteProdet, ids) { export function updateProcessPlan(rooteProdet, ids) {
return request({ return request({
url: '/system/route/updateProcessPlan', url: '/system/route/updateProcessPlan',

View 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'
})
}

View File

@ -42,3 +42,131 @@ export function delWorkCenterData(id) {
method: 'delete' 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
})
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
View 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()
})
}
}

View File

@ -5,7 +5,7 @@
:current-page="currentPage" :current-page="currentPage"
:page-size="pageSize" :page-size="pageSize"
:layout="layout" :layout="layout"
:page-sizes="pageSizes" :page-sizes="normalizedPageSizes"
:pager-count="pagerCount" :pager-count="pagerCount"
:total="total" :total="total"
v-bind="$attrs" v-bind="$attrs"
@ -31,12 +31,12 @@ export default {
}, },
limit: { limit: {
type: Number, type: Number,
default: 20 default: 10
}, },
pageSizes: { pageSizes: {
type: Array, type: Array,
default() { default() {
return [100, 200, 300, 500] return [10, 20, 50, 100]
} }
}, },
// 5 // 5
@ -81,15 +81,22 @@ export default {
set(val) { set(val) {
this.$emit('update:limit', 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: { methods: {
handleSizeChange(val) { handleSizeChange(val) {
this.pageSize = val this.$emit('update:limit', val)
if (this.currentPage * val > this.total) { this.$emit('update:page', 1)
this.currentPage = 1 this.$emit('pagination', { page: 1, limit: val })
}
this.$emit('pagination', { page: this.currentPage, limit: val })
if (this.autoScroll) { if (this.autoScroll) {
scrollTo(0, 800) scrollTo(0, 800)
} }

View File

@ -8,10 +8,26 @@ import { isRelogin } from '@/utils/request'
NProgress.configure({ showSpinner: false }) 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) => { router.beforeEach((to, from, next) => {
NProgress.start() NProgress.start()
// 白名单页面无需登录,直接访问(含无效/过期 token 场景)
if (isWhiteList(to.path)) {
if (to.path === '/login' && getToken()) {
next({ path: '/' })
NProgress.done()
return
}
next()
return
}
if (getToken()) { if (getToken()) {
to.meta.title && store.dispatch('settings/setTitle', to.meta.title) to.meta.title && store.dispatch('settings/setTitle', to.meta.title)
/* has token*/ /* has token*/
@ -42,14 +58,9 @@ router.beforeEach((to, from, next) => {
} else { } else {
// 没有token // 没有token
if (whiteList.indexOf(to.path) !== -1) {
// 在免登录白名单,直接进入
next()
} else {
next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页 next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页
NProgress.done() NProgress.done()
} }
}
}) })
router.afterEach(() => { router.afterEach(() => {

View File

@ -84,11 +84,6 @@ export const constantRoutes = [
component: () => import('@/views/login'), component: () => import('@/views/login'),
hidden: true hidden: true
}, },
{
path: '/login',
component: () => import('@/views/'),
hidden: true
},
{ {
path: '/register', path: '/register',
component: () => import('@/views/register'), component: () => import('@/views/register'),
@ -140,6 +135,12 @@ export const constantRoutes = [
component: () => import('@/views/indexDaping'), component: () => import('@/views/indexDaping'),
hidden: true hidden: true
}, },
{
path: '/DataBoard/ProduceKanBan/index',
component: () => import('@/views/system/produceKanban/index'),
hidden: true,
meta: { title: '生产管理看板' }
},
{ {
path: '/mrp2', path: '/mrp2',
component: () => import('@/views/system/mrp2/index'), component: () => import('@/views/system/mrp2/index'),
@ -153,7 +154,87 @@ export const constantRoutes = [
name: 'CostPublic', name: 'CostPublic',
hidden: true, hidden: true,
meta: { title: '零件成本查询' } 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: '/', // path: '/',
// component: Layout, // component: Layout,
@ -256,7 +337,8 @@ Router.prototype.replace = function push(location) {
export default new Router({ export default new Router({
base: process.env.VUE_APP_CONTEXT_PATH, base: process.env.VUE_APP_CONTEXT_PATH,
mode: 'history', // 去掉url中的# // Electron 使用 app:// 协议加载页面history 模式会导致路由跳转后找不到资源(全站 404
mode: process.env.IS_ELECTRON ? 'hash' : 'history',
scrollBehavior: () => ({ y: 0 }), scrollBehavior: () => ({ y: 0 }),
routes: constantRoutes routes: constantRoutes
}) })

View File

@ -80,6 +80,7 @@
</el-row> --> </el-row> -->
<el-form <el-form
v-for="(n, i) in list" v-for="(n, i) in list"
:key="i"
:inline="true" :inline="true"
:model="n" :model="n"
ref="ruleForm" ref="ruleForm"

File diff suppressed because it is too large Load Diff

View 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>

View 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>

View 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>

View 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> &nbsp;&nbsp;|&nbsp;&nbsp; <span style="color: #606266">投后余量 = 可用数量 - 需投数量</span> &nbsp;&nbsp;|&nbsp;&nbsp; <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;">
本批需求 = Σ(各主产品单台用量 × 投单量) &nbsp;|&nbsp; 差值 = 本批需求 预计可用 &nbsp;|&nbsp; 本次投单 = 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 (MOPO)
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>

View 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>

View File

@ -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>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -106,7 +106,7 @@
<el-table-column label="上传图纸" align="center" prop="uploadDrawing"/> <el-table-column label="上传图纸" align="center" prop="uploadDrawing"/>
<el-table-column label="图纸预览" align="center" prop="drawingPreview" width="200"> <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 <ImagePreview v-if="previewListResource && checkFileSuffix(scope.row.drawingPreview)" :width=100 :height=100
:src="scope.row.drawingPreview" :preview-src-list="[scope.row.drawingPreview]"/> :src="scope.row.drawingPreview" :preview-src-list="[scope.row.drawingPreview]"/>
<span v-text="scope.row.drawingPreview" <span v-text="scope.row.drawingPreview"

View 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>

View 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>

View 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">&nbsp;</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>

File diff suppressed because it is too large Load Diff

View 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>

View 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>

View File

@ -2,6 +2,7 @@
<div> <div>
<el-form <el-form
v-for="(n,i) in list" v-for="(n,i) in list"
:key="i"
:inline="true" :inline="true"
:model="n" :model="n"
ref="ruleForm" ref="ruleForm"

View File

@ -41,6 +41,11 @@
v-hasPermi="['system:route:pushRouteBom']">推送工艺 v-hasPermi="['system:route:pushRouteBom']">推送工艺
</el-button> </el-button>
</el-col> </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-col :span="3">
<el-input v-model="routeGroupName" placeholder="请输入推送分组" size="mini" clearable /> <el-input v-model="routeGroupName" placeholder="请输入推送分组" size="mini" clearable />
</el-col> </el-col>
@ -154,10 +159,20 @@
<el-table-column label="工序控制码" width="100" align="center"> <el-table-column label="工序控制码" width="100" align="center">
<template slot-scope="scope"> <template slot-scope="scope">
<el-form-item v-if="scope.row.isEdit" :prop="'routeList.' + scope.$index + '.processControl'"> <el-form-item v-if="scope.row.isEdit" :prop="'routeList.' + scope.$index + '.processControl'">
<el-select v-model="scope.row.processControl" placeholder="请选择工序控制码" clearable> <el-select
<el-option label="汇报+免检" value="汇报+免检"></el-option> v-model="scope.row.processControl"
<el-option label="委外+质量" value="委外+质量"></el-option> placeholder="请选择工序控制码"
<el-option label="汇报+质量" value="汇报+质量"></el-option> clearable
filterable
allow-create
default-first-option
>
<el-option
v-for="item in processControlOptions"
:key="item"
:label="item"
:value="item"
/>
</el-select> </el-select>
</el-form-item> </el-form-item>
<span v-else>{{ scope.row.processControl }}</span> <span v-else>{{ scope.row.processControl }}</span>
@ -323,10 +338,10 @@
<el-select v-model="form.workCenter" placeholder="请选择工作中心" clearable> <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-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> </el-select>
@ -353,10 +368,20 @@
<el-row> <el-row>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="工序控制" prop="processControl"> <el-form-item label="工序控制" prop="processControl">
<el-select v-model="form.processControl" placeholder="请选择工序控制码" clearable> <el-select
<el-option label="汇报+免检" value="汇报+免检"></el-option> v-model="form.processControl"
<el-option label="委外+质量" value="委外+质量"></el-option> placeholder="请选择工序控制码"
<el-option label="汇报+质量" value="汇报+质量"></el-option> clearable
filterable
allow-create
default-first-option
>
<el-option
v-for="item in processControlOptions"
:key="item"
:label="item"
:value="item"
/>
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col> </el-col>
@ -609,7 +634,7 @@ import {
import { getToken } from "../../../utils/auth"; import { getToken } from "../../../utils/auth";
import Fromed from './fromed' import Fromed from './fromed'
import Bomfrom from './bomfrom' 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 { listMaterial } from "@/api/system/material";
import dict from "@/utils/dict"; import dict from "@/utils/dict";
@ -647,6 +672,12 @@ export default {
url: process.env.VUE_APP_BASE_API + "/system/route/importDataTime" url: process.env.VUE_APP_BASE_API + "/system/route/importDataTime"
}, },
materialOptions: [], // materialOptions: [], //
processControlOptions: [
'汇报+免检',
'汇报+质量',
'委外+质量',
'委外+汇报'
],
showtable: true, showtable: true,
expand: true, expand: true,
bomData: [], bomData: [],
@ -718,14 +749,14 @@ export default {
}, },
// -> // ->
processToWorkCenterMap: { processToWorkCenterMap: {
// //
'数控火焰下料': '铆焊工段', '数控火焰下料': '下料工段',
'数控激光下料': '铆焊工段', '数控激光下料': '下料工段',
'数控切管下料': '铆焊工段', '数控切管下料': '下料工段',
'水虎鱼剪折冲': '铆焊工段', '水虎鱼剪折冲': '下料工段',
'锯床下料': '铆焊工段', '锯床下料': '下料工段',
'抛丸': '铆焊工段', '抛丸': '下料工段',
'组焊': '铆焊工段', '组焊': '下料工段',
'普车': '机一工段', '普车': '机一工段',
'普铣': '机一工段', '普铣': '机一工段',
'龙门铣': '机一工段', '龙门铣': '机一工段',
@ -738,14 +769,14 @@ export default {
'双主轴': '机二工段', '双主轴': '机二工段',
'车铣复合': '机二工段', '车铣复合': '机二工段',
'线切割': '机二工段', '线切割': '机二工段',
'卧加63': '机三工段', '卧加63': '焊接工段',
'5032龙门': '机三工段', '5032龙门': '焊接工段',
'3018龙门': '机三工段', '3018龙门': '焊接工段',
'2013龙门': '机三工段', '2013龙门': '焊接工段',
'立加1370': '机三工段', '立加1370': '焊接工段',
'立加1177': '机三工段', '立加1177': '焊接工段',
'立加850': '机三工段', '立加850': '焊接工段',
'数插5035': '机三工段', '数插5035': '焊接工段',
'装配1': '装一工段', '装配1': '装一工段',
'装配2': '装二工段', '装配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) { handleRowEdit(scope) {
//
scope.row.originalData = JSON.parse(JSON.stringify(scope.row)); scope.row.originalData = JSON.parse(JSON.stringify(scope.row));
//
this.$set(scope.row, 'isEdit', true); 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() { handleImportTime() {
this.timeUpload.title = "导入时间"; this.timeUpload.title = "导入时间";
@ -1268,14 +1213,6 @@ export default {
}; };
routeList.push(newRow); routeList.push(newRow);
this.showParentSaveBtn = props.row.id; 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; const productionOrderNo = this.params.productionOrderNo;
if (!productionOrderNo) { if (!productionOrderNo) {
this.$modal.msgError("未获取到生产订单号,请检查!"); this.$modal.msgError("未获取到生产订单号,请检查!");
@ -1491,47 +1427,36 @@ export default {
this.$modal.msgError("请填写分组名称!"); this.$modal.msgError("请填写分组名称!");
return; return;
} }
let loading = null;
try { try {
// await this.$confirm(confirmMessage, '提示', {
await this.$confirm(
'推送工艺可能需要较长时间,请耐心等待。是否继续?',
'提示',
{
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning' type: 'warning'
} });
); loading = this.$loading({
// loading
const loading = this.$loading({
lock: true, lock: true,
text: '正在推送工艺,请稍候...', text: loadingText,
spinner: 'el-icon-loading', spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.7)' background: 'rgba(0, 0, 0, 0.7)'
}); });
//
this.reset(); this.reset();
// const timeout = 600000;
const timeout = 600000; // 10
const timeoutPromise = new Promise((_, reject) => { const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => { setTimeout(() => {
reject(new Error('推送操作超时,请检查后台处理状态')); reject(new Error('推送操作超时,请检查后台处理状态'));
}, timeout); }, timeout);
}); });
// API
const response = await Promise.race([ const response = await Promise.race([
pushRouteBom(productionOrderNo, this.routeGroupName.trim()), apiFn(productionOrderNo, this.routeGroupName.trim()),
timeoutPromise timeoutPromise
]); ]);
// - R<ProcessRoutePushResultDTO>
const { duplicateRoutes, failedRoutes, successfulRoutes } = response.data; const { duplicateRoutes, failedRoutes, successfulRoutes } = response.data;
//
const duplicateCodes = duplicateRoutes.join(', '); const duplicateCodes = duplicateRoutes.join(', ');
const failedInfo = failedRoutes.map(route => const failedInfo = failedRoutes.map(route =>
`${route.materialCode} (${route.errorMessage || '未知错误'})` `${route.materialCode} (${route.errorMessage || '未知错误'})`
).join(', '); ).join(', ');
const successfulCodes = successfulRoutes.map(route => route.materialCode).join(', '); const successfulCodes = successfulRoutes.map(route => route.materialCode).join(', ');
//
this.$alert(` this.$alert(`
<div><strong>成功 (${successfulRoutes.length}):</strong></div> <div><strong>成功 (${successfulRoutes.length}):</strong></div>
<div style="margin-left: 20px;">${successfulCodes || '无'}</div> <div style="margin-left: 20px;">${successfulCodes || '无'}</div>
@ -1539,38 +1464,71 @@ export default {
<div style="margin-left: 20px;">${failedInfo || '无'}</div> <div style="margin-left: 20px;">${failedInfo || '无'}</div>
<div><strong>重复 (${duplicateRoutes.length}):</strong></div> <div><strong>重复 (${duplicateRoutes.length}):</strong></div>
<div style="margin-left: 20px;">${duplicateCodes || '无'}</div> <div style="margin-left: 20px;">${duplicateCodes || '无'}</div>
`, '推送完成', { `, successTitle || '推送完成', {
dangerouslyUseHTMLString: true, dangerouslyUseHTMLString: true,
confirmButtonText: '确定' confirmButtonText: '确定'
}); });
//
await this.getList(); await this.getList();
// loading
loading.close();
} catch (error) { } catch (error) {
// console.error(`[${errorTitle}]:`, error);
console.error('[推送工艺失败]:', error);
//
this.$notify({ this.$notify({
title: '推送工艺失败', title: errorTitle,
message: error.message || '推送工艺失败,请重试!', message: error.message || `${errorTitle},请重试!`,
type: 'error', type: 'error',
duration: 0, duration: 0,
showClose: true showClose: true
}); });
} finally { } finally {
// loading if (loading) {
if (this.$loading) { loading.close();
this.$loading().close();
} }
this.loading = false; 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) { updateProcessPlan(row) {
// //
@ -1704,8 +1662,6 @@ export default {
} }
}); });
}, },
//
/** 删除按钮操作 */ /** 删除按钮操作 */
handleDelete(scope) { handleDelete(scope) {
const ids = [scope.row.id]; const ids = [scope.row.id];
@ -1847,8 +1803,6 @@ export default {
this.loading = true; this.loading = true;
// //
const row = scope.row; const row = scope.row;
//
const saveData = { const saveData = {
id: row.id, id: row.id,
parentId: row.parentId, parentId: row.parentId,

View File

@ -345,10 +345,10 @@
<el-select v-model="form.workCenter" placeholder="请选择工作中心" clearable> <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-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> </el-select>
@ -742,14 +742,14 @@ export default {
}, },
// -> // ->
processToWorkCenterMap: { processToWorkCenterMap: {
// //
'数控火焰下料': '铆焊工段', '数控火焰下料': '下料工段',
'数控激光下料': '铆焊工段', '数控激光下料': '下料工段',
'数控切管下料': '铆焊工段', '数控切管下料': '下料工段',
'水虎鱼剪折冲': '铆焊工段', '水虎鱼剪折冲': '下料工段',
'锯床下料': '铆焊工段', '锯床下料': '下料工段',
'抛丸': '铆焊工段', '抛丸': '下料工段',
'组焊': '铆焊工段', '组焊': '下料工段',
'普车': '机一工段', '普车': '机一工段',
'普铣': '机一工段', '普铣': '机一工段',
'龙门铣': '机一工段', '龙门铣': '机一工段',
@ -762,14 +762,14 @@ export default {
'双主轴': '机二工段', '双主轴': '机二工段',
'车铣复合': '机二工段', '车铣复合': '机二工段',
'线切割': '机二工段', '线切割': '机二工段',
'卧加63': '机三工段', '卧加63': '焊接工段',
'5032龙门': '机三工段', '5032龙门': '焊接工段',
'3018龙门': '机三工段', '3018龙门': '焊接工段',
'2013龙门': '机三工段', '2013龙门': '焊接工段',
'立加1370': '机三工段', '立加1370': '焊接工段',
'立加1177': '机三工段', '立加1177': '焊接工段',
'立加850': '机三工段', '立加850': '焊接工段',
'数插5035': '机三工段', '数插5035': '焊接工段',
'装配1': '装一工段', '装配1': '装一工段',
'装配2': '装二工段', '装配2': '装二工段',
'电装': '电钳工段', '电装': '电钳工段',

View 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>

File diff suppressed because it is too large Load Diff

View File

@ -31,8 +31,16 @@ module.exports = {
devServer: { devServer: {
host: '0.0.0.0', host: '0.0.0.0',
port: port, port: port,
open: true, open: false,
proxy: { proxy: {
// 出图状态接口的跨域代理
'/plot-api': {
target: 'http://192.168.5.18:9000',
changeOrigin: true,
pathRewrite: {
'^/plot-api': ''
}
},
// detail: https://cli.vuejs.org/config/#devserver-proxy // detail: https://cli.vuejs.org/config/#devserver-proxy
[process.env.VUE_APP_BASE_API]: { [process.env.VUE_APP_BASE_API]: {
target: `http://127.0.0.1:8033`, target: `http://127.0.0.1:8033`,
@ -144,5 +152,10 @@ module.exports = {
transpileDependencies: [ transpileDependencies: [
'vue-echarts', 'vue-echarts',
'resize-detector' 'resize-detector'
] ],
pluginOptions: {
electronBuilder: {
nodeIntegration: true
}
}
} }