feat: 多个页面功能优化与新增

1.  异议页面:新增状态为4时的导出PDF按钮,替换原有注释的打印按钮
2.  流程图页面:重构流程图组件,支持多流程切换、节点点击事件,优化主题配置和渲染逻辑
3.  钢卷待领页面:新增钢卷数据修正功能,新增操作列按钮和对应弹窗表单,注释绑定组件
4.  导出PDF弹窗:优化多钢卷数据展示,拆分合并附件排版,优化导出样式和分页逻辑
This commit is contained in:
2026-06-30 11:42:03 +08:00
parent 89773b273b
commit 81dec034b3
4 changed files with 873 additions and 330 deletions

View File

@@ -27,7 +27,7 @@
</div>
<div v-loading="loading" class="flow-content">
<div ref="diagram" class="flow-diagram" v-html="currentSvg" @click="onNodeClick"></div>
<div ref="diagram" class="flow-diagram" v-html="currentSvg"></div>
</div>
</div>
</template>
@@ -35,7 +35,7 @@
<script>
import { renderMermaidSVG, THEMES } from 'beautiful-mermaid'
const theme = {
const MERMAID_THEME = {
...THEMES['zinc-light'],
padding: 24,
nodeSpacing: 28,
@@ -43,35 +43,75 @@ const theme = {
font: 'Inter, "Microsoft YaHei", sans-serif',
}
const diagrams = {
afterSales: `
graph TD
A["<b>创建售后单</b><br/>填写基本信息<br/>选择需售后处理的钢卷"]:::step
A --> B["<b>多部门并行处理</b>"]:::fork
B --> C["<b>生产部</b><br/>出具处理意见"]:::dept1
B --> D["<b>质量部</b><br/>出具处理意见"]:::dept2
B --> E["<b>销售部</b><br/>出具处理意见"]:::dept3
C --> F{"三个部门<br/>全部提交?"}:::decision
D --> F
E --> F
F -->|已全部提交| G["<b>售后负责人</b><br/>汇总各部门意见<br/>形成最终处理方案<br/>指定执行部门"]:::approve
G --> H["<b>部门执行</b><br/>执行处理方案"]:::execute
H --> I["<b>返回执行结果</b><br/>提交执行报告"]:::result
I --> J(["<b>售后单封存</b><br/>流程结束"]):::end
const TAB_LIST = [
{ key: 'steelFullChain', label: '生产全链路流程', icon: 'el-icon-s-operation' },
{ key: 'afterSales', label: '售后处理流程', icon: 'el-icon-s-claim' },
{ key: 'inventoryCheck', label: '盘库流程', icon: 'el-icon-s-check' },
{ key: 'productionSchedule', label: '排产流程', icon: 'el-icon-s-order' },
{ key: 'equipmentRepair', label: '设备维修流程', icon: 'el-icon-s-tools' },
]
classDef step fill:#409eff,stroke:#337ecc,color:#fff,stroke-width:2px
classDef fork fill:#f0f5ff,stroke:#409eff,color:#303133,stroke-width:2px
classDef dept1 fill:#e6fffa,stroke:#00b4a0,color:#303133,stroke-width:2px
classDef dept2 fill:#fff7e6,stroke:#fa8c16,color:#303133,stroke-width:2px
classDef dept3 fill:#fff0f6,stroke:#eb2f96,color:#303133,stroke-width:2px
classDef decision fill:#f9f0ff,stroke:#722ed1,color:#303133,stroke-width:2px
classDef approve fill:#e6f7ff,stroke:#1890ff,color:#303133,stroke-width:2px
classDef execute fill:#fffbe6,stroke:#fadb14,color:#303133,stroke-width:2px
classDef result fill:#f6ffed,stroke:#52c41a,color:#303133,stroke-width:2px
classDef end fill:#dcf7e8,stroke:#52c41a,color:#303133,stroke-width:2px,rx:10,ry:10
linkStyle default stroke:#bfbfbf,stroke-width:2px
`,
const NODE_EVENT_CONFIG = {
steelFullChain: [
{ id: 'A', label: '销售部创建合同', handler: 'handleClick', params: { action: 'openContract' } },
{ id: 'B', label: '原料卷到货', handler: 'handleClick', params: { action: 'openRawMaterial' } },
{ id: 'C', label: '入库验收', handler: 'handleClick', params: { action: 'openWarehouse' } },
{ id: 'D', label: '登记原料库存', handler: 'handleClick', params: { action: 'openWarehouse' } },
{ id: 'E', label: '成品钢卷加工', handler: 'handleClick', params: { action: 'openProduction' } },
{ id: 'I', label: '登记质量缺陷', handler: 'handleClick', params: { action: 'openQuality' } },
{ id: 'J', label: '质量等级判定', handler: 'handleClick', params: { action: 'openQuality' } },
{ id: 'K', label: '单卷档案', handler: 'handleClick', params: { action: 'openArchive' } },
{ id: 'L', label: '编排发货计划', handler: 'handleClick', params: { action: 'openShipping' } },
{ id: 'M', label: '生成发货单', handler: 'handleClick', params: { action: 'openShipping' } },
{ id: 'N', label: '发货质量校验', handler: 'handleClick', params: { action: 'openQuality' } },
{ id: 'O', label: '出库发货', handler: 'handleClick', params: { action: 'openShipping' } },
{ id: 'P', label: '禁止出库', handler: 'handleClick', params: { action: 'openShipping' } },
{ id: 'Q', label: '钢卷库存管理', handler: 'handleClick', params: { action: 'openInventory' } },
{ id: 'V', label: '生产过程数据异常检测', handler: 'handleClick', params: { action: 'openAlert' } },
{ id: 'W', label: '自动触发告警', handler: 'handleClick', params: { action: 'openAlert' } },
{ id: 'X', label: '生产数据报表统计', handler: 'handleClick', params: { action: 'openReport' } },
],
afterSales: [
{ id: 'A', label: '创建售后单', handler: 'handleClick', params: { action: 'openAfterSalesCreate' } },
{ id: 'C', label: '生产部出具处理意见', handler: 'handleClick', params: { action: 'openAfterSalesDept', dept: 'production' } },
{ id: 'D', label: '质量部出具处理意见', handler: 'handleClick', params: { action: 'openAfterSalesDept', dept: 'quality' } },
{ id: 'E', label: '销售部出具处理意见', handler: 'handleClick', params: { action: 'openAfterSalesDept', dept: 'sales' } },
{ id: 'G', label: '售后负责人汇总', handler: 'handleClick', params: { action: 'openAfterSalesSummary' } },
],
inventoryCheck: [
{ id: 'A', label: '创建盘库计划', handler: 'handleClick', params: { action: 'openInventoryPlan' } },
{ id: 'B', label: '创建计划明细', handler: 'handleClick', params: { action: 'openInventoryPlanDetail' } },
{ id: 'C', label: '选择库区', handler: 'handleClick', params: { action: 'openInventoryPlanDetail' } },
{ id: 'D', label: '提交审批', handler: 'handleClick', params: { action: 'submitApproval' } },
{ id: 'G', label: '生成系统库存快照', handler: 'handleClick', params: { action: 'openInventorySnapshot' } },
{ id: 'H', label: '上传实盘库存Excel', handler: 'handleClick', params: { action: 'openInventoryUpload' } },
{ id: 'I', label: '执行对比', handler: 'handleClick', params: { action: 'openInventoryCompare' } },
{ id: 'J', label: '查看差异明细', handler: 'handleClick', params: { action: 'openInventoryDiff' } },
{ id: 'K', label: '再次提交审批', handler: 'handleClick', params: { action: 'submitApproval' } },
{ id: 'N', label: '执行处理差异', handler: 'handleClick', params: { action: 'openInventoryExecute' } },
],
productionSchedule: [
{ id: 'A', label: '创建需求单', handler: 'handleClick', params: { action: 'openScheduleCreate' } },
{ id: 'B', label: '选择合同', handler: 'handleClick', params: { action: 'openScheduleContract' } },
{ id: 'C', label: '自动获取需求明细', handler: 'handleClick', params: { action: 'openScheduleDetail' } },
{ id: 'D', label: '调整需求明细', handler: 'handleClick', params: { action: 'openScheduleDetail' } },
{ id: 'E', label: '提交审批', handler: 'handleClick', params: { action: 'submitApproval' } },
{ id: 'H', label: '转化为排产单', handler: 'handleClick', params: { action: 'openScheduleConvert' } },
{ id: 'I', label: '排产单', handler: 'handleClick', params: { action: 'openScheduleEdit' } },
{ id: 'J', label: '再次提交审批', handler: 'handleClick', params: { action: 'submitApproval' } },
{ id: 'M', label: '提交给车间', handler: 'handleClick', params: { action: 'submitApproval' } },
{ id: 'N', label: '车间绑定钢卷', handler: 'handleClick', params: { action: 'openScheduleBind' } },
{ id: 'O', label: '执行生产', handler: 'handleClick', params: { action: 'openScheduleExecute' } },
],
equipmentRepair: [
{ id: 'A', label: '创建维修计划', handler: 'handleClick', params: { action: 'openRepairCreate' } },
{ id: 'A1', label: '点选异常巡检记录', handler: 'handleClick', params: { action: 'openRepairSelect' } },
{ id: 'B', label: '提交审批', handler: 'handleClick', params: { action: 'submitApproval' } },
{ id: 'E', label: '逐设备维修记录', handler: 'handleClick', params: { action: 'openRepairExecute' } },
],
}
const DIAGRAMS = {
steelFullChain: `
graph TD
A["<b>销售部创建合同</b><br/>录入产品所需内容<br/>规格/数量/技术标准"]:::s1
@@ -121,38 +161,66 @@ graph TD
linkStyle default stroke:#bfbfbf,stroke-width:2px
`,
afterSales: `
graph TD
A["<b>创建售后单</b><br/>填写基本信息<br/>选择需售后处理的钢卷"]:::step
A --> B["<b>多部门并行处理</b>"]:::fork
B --> C["<b>生产部</b><br/>出具处理意见"]:::dept1
B --> D["<b>质量部</b><br/>出具处理意见"]:::dept2
B --> E["<b>销售部</b><br/>出具处理意见"]:::dept3
C --> F{"三个部门<br/>全部提交?"}:::decision
D --> F
E --> F
F -->|已全部提交| G["<b>售后负责人</b><br/>汇总各部门意见<br/>直接办结归档"]:::approve
G --> J(["<b>售后单封存</b><br/>流程结束"]):::end
classDef step fill:#409eff,stroke:#337ecc,color:#fff,stroke-width:2px
classDef fork fill:#f0f5ff,stroke:#409eff,color:#303133,stroke-width:2px
classDef dept1 fill:#e6fffa,stroke:#00b4a0,color:#303133,stroke-width:2px
classDef dept2 fill:#fff7e6,stroke:#fa8c16,color:#303133,stroke-width:2px
classDef dept3 fill:#fff0f6,stroke:#eb2f96,color:#303133,stroke-width:2px
classDef decision fill:#f9f0ff,stroke:#722ed1,color:#303133,stroke-width:2px
classDef approve fill:#e6f7ff,stroke:#1890ff,color:#303133,stroke-width:2px
classDef end fill:#dcf7e8,stroke:#52c41a,color:#303133,stroke-width:2px,rx:10,ry:10
linkStyle default stroke:#bfbfbf,stroke-width:2px
`,
inventoryCheck: `
graph TD
A["<b>创建盘库计划</b><br/>填写基本信息"]:::c1
A --> B["<b>创建计划明细</b><br/>可创建多个明细"]:::c2
B --> C["<b>选择库区</b><br/>逻辑库 / 实际库<br/>至少选一个"]:::c3
C --> D["<b>生成系统库存快照</b>"]:::c4
D --> E["<b>上传实盘库存Excel</b>"]:::c5
E --> F["<b>执行对比</b><br/>快照 vs 实盘<br/>自动计算差异"]:::c6
C --> D["<b>提交审批</b>"]:::c4
F --> G["<b>查看差异明细</b><br/>保存差异并填写处理方式"]:::c7
G --> H["<b>提交送审</b>"]:::c8
D --> E{"审批"}:::dec
E -->|不通过| F["<b>退回修改</b>"]:::c5
F --> B
E -->|通过| G["<b>生成系统库存快照</b>"]:::c6
H --> I{"审批"}:::dec
I -->|不通过| J["<b>退回修改</b>"]:::c7
J --> G
I -->|通过| K["<b>开始处理差异</b><br/>逐项执行处理方式"]:::c9
G --> H["<b>上传实盘库存Excel</b>"]:::c7
H --> I["<b>执行对比</b><br/>快照 vs 实盘<br/>自动计算差异"]:::c8
I --> J["<b>查看差异明细</b><br/>保存差异并填写处理方式"]:::c9
J --> K["<b>再次提交审批</b>"]:::c10
K --> L{"所有差异<br/>处理完成?"}:::dec
L -->|否| K
L -->|是| M(["<b>完结流程</b><br/>盘库结束"]):::cend
K --> L{"审批"}:::dec
L -->|不通过| M["<b>退回修改</b>"]:::c11
M --> J
L -->|通过| N["<b>执行处理差异</b><br/>逐项执行并填写执行结果"]:::c12
N --> O(["<b>归档办结</b><br/>盘库结束"]):::cend
classDef c1 fill:#409eff,stroke:#337ecc,color:#fff,stroke-width:2px
classDef c2 fill:#e6fffa,stroke:#13c2c2,color:#303133,stroke-width:2px
classDef c3 fill:#fff7e6,stroke:#fa8c16,color:#303133,stroke-width:2px
classDef c4 fill:#f0f5ff,stroke:#597ef7,color:#303133,stroke-width:2px
classDef c5 fill:#e6f7ff,stroke:#1890ff,color:#303133,stroke-width:2px
classDef c6 fill:#fffbe6,stroke:#fadb14,color:#606266,stroke-width:2px
classDef c5 fill:#fff1f0,stroke:#f5222d,color:#303133,stroke-width:2px
classDef c6 fill:#e6f7ff,stroke:#1890ff,color:#303133,stroke-width:2px
classDef c7 fill:#f0f5ff,stroke:#597ef7,color:#303133,stroke-width:2px
classDef c8 fill:#fff0f6,stroke:#eb2f96,color:#303133,stroke-width:2px
classDef c9 fill:#f6ffed,stroke:#52c41a,color:#303133,stroke-width:2px
classDef c8 fill:#fffbe6,stroke:#fadb14,color:#606266,stroke-width:2px
classDef c9 fill:#fff0f6,stroke:#eb2f96,color:#303133,stroke-width:2px
classDef c10 fill:#f0f5ff,stroke:#597ef7,color:#303133,stroke-width:2px
classDef c11 fill:#fff1f0,stroke:#f5222d,color:#303133,stroke-width:2px
classDef c12 fill:#f6ffed,stroke:#52c41a,color:#303133,stroke-width:2px
classDef dec fill:#f9f0ff,stroke:#722ed1,color:#303133,stroke-width:2px
classDef cend fill:#dcf7e8,stroke:#52c41a,color:#303133,stroke-width:2px,rx:10,ry:10
linkStyle default stroke:#bfbfbf,stroke-width:2px
@@ -226,22 +294,42 @@ graph TD
export default {
name: 'FlowChart',
data() {
return {
loading: false,
activeTab: 'steelFullChain',
tabs: [
{ key: 'steelFullChain', label: '生产全链路流程', icon: 'el-icon-s-operation' },
{ key: 'afterSales', label: '售后处理流程', icon: 'el-icon-s-claim' },
{ key: 'inventoryCheck', label: '盘库流程', icon: 'el-icon-s-check' },
{ key: 'productionSchedule', label: '排产流程', icon: 'el-icon-s-order' },
{ key: 'equipmentRepair', label: '设备维修流程', icon: 'el-icon-s-tools' },
],
tabs: TAB_LIST,
svgCache: {},
selectedNode: null,
downloadLoading: false,
}
},
computed: {
currentSvg() {
const code = DIAGRAMS[this.activeTab]
if (!code) return ''
if (!this.svgCache[this.activeTab]) {
try {
this.svgCache[this.activeTab] = renderMermaidSVG(code, MERMAID_THEME)
} catch (e) {
console.error('Mermaid render error:', e)
return '<p style="color:#f5222d;text-align:center;">流程图渲染失败: ' + e.message + '</p>'
}
}
return this.svgCache[this.activeTab]
},
},
mounted() {
this.$nextTick(() => {
this.bindNodeEvents()
})
},
watch: {
currentSvg() {
this.$nextTick(() => {
@@ -249,22 +337,41 @@ export default {
})
},
},
computed: {
currentSvg() {
const code = diagrams[this.activeTab]
if (!code) return ''
if (!this.svgCache[this.activeTab]) {
try {
this.svgCache[this.activeTab] = renderMermaidSVG(code, theme)
} catch (e) {
console.error('Mermaid render error:', e)
return '<p style="color:#f5222d;text-align:center;">流程图渲染失败: ' + e.message + '</p>'
}
}
return this.svgCache[this.activeTab]
},
},
methods: {
switchTab(key) {
this.activeTab = key
this.selectedNode = null
},
bindNodeEvents() {
const el = this.$refs.diagram
if (!el) return
const svg = el.querySelector('svg')
if (!svg) return
const nodeConfigs = NODE_EVENT_CONFIG[this.activeTab] || []
nodeConfigs.forEach(config => {
const node = svg.querySelector(`g[data-id="${config.id}"]`)
if (!node) return
node.style.cursor = 'pointer'
node.addEventListener('click', (e) => {
e.stopPropagation()
const textEl = node.querySelector('text, span')
const label = textEl ? textEl.textContent.replace(/\s+/g, ' ').trim() : config.label
this.selectedNode = { dataId: config.id, label, handler: config.handler, params: config.params }
if (typeof this[config.handler] === 'function') {
this[config.handler]({ id: config.id, label, node, params: config.params })
} else {
console.warn(`[FlowChart] Handler "${config.handler}" not found`)
}
})
})
},
handleDownload(format) {
if (format === 'svg') {
this.downloadSvg()
@@ -272,25 +379,30 @@ export default {
this.downloadPng()
}
},
downloadSvg() {
const svgEl = this.$refs.diagram?.querySelector('svg')
if (!svgEl) {
this.$message.warning('流程图尚未渲染完成')
return
}
const clone = svgEl.cloneNode(true)
const serializer = new XMLSerializer()
const source = serializer.serializeToString(clone)
const blob = new Blob([source], { type: 'image/svg+xml;charset=utf-8' })
this.triggerDownload(URL.createObjectURL(blob), `${this.activeTab}.svg`)
this.$message.success('SVG 已下载')
},
downloadPng() {
const svgEl = this.$refs.diagram?.querySelector('svg')
if (!svgEl) {
this.$message.warning('流程图尚未渲染完成')
return
}
this.downloadLoading = true
const clone = svgEl.cloneNode(true)
@@ -308,10 +420,13 @@ export default {
const scale = 2
canvas.width = rect.width * scale
canvas.height = rect.height * scale
const ctx = canvas.getContext('2d')
ctx.scale(scale, scale)
ctx.drawImage(img, 0, 0, rect.width, rect.height)
URL.revokeObjectURL(url)
canvas.toBlob(blob => {
this.downloadLoading = false
if (blob) {
@@ -320,12 +435,15 @@ export default {
}
}, 'image/png')
}
img.onerror = () => {
this.downloadLoading = false
this.$message.error('PNG 导出失败')
}
img.src = url
},
triggerDownload(url, filename) {
const a = document.createElement('a')
a.href = url
@@ -335,40 +453,10 @@ export default {
document.body.removeChild(a)
URL.revokeObjectURL(url)
},
switchTab(key) {
this.activeTab = key
this.selectedNode = null
},
bindNodeEvents() {
const el = this.$refs.diagram
if (!el) return
const svg = el.querySelector('svg')
if (!svg) return
const groups = svg.querySelectorAll('g:not([class*="edge"])')
groups.forEach(g => {
const hasRect = g.querySelector('rect, path, ellipse, polygon')
const text = g.querySelector('text, span')
if (hasRect && text) {
g.style.cursor = 'pointer'
}
})
},
onNodeClick(e) {
let target = e.target
while (target && target !== e.currentTarget) {
if (target.tagName === 'g') {
const textEl = target.querySelector('text, span')
if (textEl) {
const label = textEl.textContent.replace(/\s+/g, ' ').trim()
if (label) {
this.selectedNode = label
this.$message({ message: label, type: 'info', duration: 1500 })
return
}
}
}
target = target.parentElement
}
handleClick({ id, label, params }) {
console.log('[FlowChart] Node clicked:', { id, label, params })
this.$message({ message: `${label}`, type: 'info', duration: 1500 })
},
},
}

View File

@@ -4,7 +4,7 @@
<div v-loading="loading" class="export-preview">
<div ref="pdfContent" class="pdf-content">
<!-- ========== 附件一质量异议反馈单 ========== -->
<div class="attachment-section">
<div ref="feedbackFormSection" class="attachment-section">
<h2 class="doc-title">质量异议反馈单</h2>
<table class="form-table">
@@ -24,19 +24,19 @@
<tr>
<td class="label" style="width:60px;vertical-align:middle" :rowspan="8">情况概述<br>用户提供</td>
<td class="label" style="width:120px">产品名称</td>
<td style="width:180px">{{ firstCoil.itemName || '' }}</td>
<td style="width:180px">{{ allItemNames }}</td>
<td class="label" style="width:110px">异议合同号</td>
<td>{{ firstContract.contractCode || '' }}</td>
<td>{{ allContractCodes }}</td>
</tr>
<tr>
<td class="label">合同交货量</td>
<td>{{ firstContract.orderAmount || '' }}</td>
<td>{{ allOrderAmounts }}</td>
<td class="label">牌号/钢种</td>
<td>{{ firstCoil.material || '' }}</td>
<td>{{ allMaterials }}</td>
</tr>
<tr>
<td class="label">规格</td>
<td>{{ firstCoil.specification || '' }}</td>
<td>{{ allSpecifications }}</td>
<td class="label">异议量</td>
<td>{{ coilWeightSummary }}</td>
</tr>
@@ -44,7 +44,7 @@
<td class="label">采购日期</td>
<td>{{ detail.complaintDate | formatDate }}</td>
<td class="label">使用日期</td>
<td>{{ firstCoil.exportTime | formatDate }}</td>
<td>{{ allExportTimes }}</td>
</tr>
<tr>
<td class="label">产品卷号</td>
@@ -52,7 +52,7 @@
</tr>
<tr>
<td class="label">产品生产日期</td>
<td colspan="3">{{ firstCoil.createTime | formatDate }}</td>
<td colspan="3">{{ allCreateTimes }}</td>
</tr>
<tr>
<td class="label">下游使用用户名称</td>
@@ -95,174 +95,176 @@
</div>
</div>
<!-- ========== 附件二质量投诉立案确认及处置单 ========== -->
<div class="page-break"></div>
<div class="attachment-section">
<h2 class="doc-title">附件 2</h2>
<h3 class="sub-title">质量投诉立案确认及处置单</h3>
<!-- ========== 附件二 + 附件三 ========== -->
<div ref="restContentSection">
<div class="page-break"></div>
<div class="attachment-section">
<h2 class="doc-title">附件 2</h2>
<h3 class="sub-title">质量投诉立案确认及处置单</h3>
<table class="form-table">
<tr>
<td class="label" style="width:120px">收到日期</td>
<td style="width:180px">{{ detail.complaintDate | formatDate }}</td>
<td class="label" style="width:100px">投诉单编号</td>
<td>{{ detail.complaintNo || '' }}</td>
</tr>
<tr>
<td class="label">异议反馈单位</td>
<td>{{ customer.companyName || '' }}</td>
<td class="label">反馈人姓名</td>
<td>{{ customer.contactPerson || '' }}</td>
</tr>
<tr>
<td class="label">信息接收人</td>
<td>{{ detail.auditOpinion || '' }}</td>
<td class="label">联系方式</td>
<td>{{ customer.contactWay || '' }}</td>
</tr>
<tr>
<td class="label">订货用户及使用<br>用户名称及地址</td>
<td colspan="3">{{ customer.companyName || '' }} {{ customer.address || '' }}</td>
</tr>
<tr>
<td class="label">产品名称</td>
<td>{{ firstCoil.itemName || '' }}</td>
<td class="label">合同号</td>
<td>{{ firstContract.contractCode || '' }}</td>
</tr>
<tr>
<td class="label">合同交货量</td>
<td>{{ firstContract.orderAmount || '' }}</td>
<td class="label">未出库量</td>
<td></td>
</tr>
<tr>
<td class="label">规格mm</td>
<td>{{ firstCoil.specification || '' }}</td>
<td class="label">产品钢种/牌号</td>
<td>{{ firstCoil.material || '' }}</td>
</tr>
<tr>
<td class="label">异议量</td>
<td>{{ coilWeightSummary }}</td>
<td class="label">产品用途</td>
<td>{{ detail.productUsage || '' }}</td>
</tr>
</table>
<table class="form-table">
<tr>
<td class="label" style="width:120px">收到日期</td>
<td style="width:180px">{{ detail.complaintDate | formatDate }}</td>
<td class="label" style="width:100px">投诉单编号</td>
<td>{{ detail.complaintNo || '' }}</td>
</tr>
<tr>
<td class="label">异议反馈单位</td>
<td>{{ customer.companyName || '' }}</td>
<td class="label">反馈人姓名</td>
<td>{{ customer.contactPerson || '' }}</td>
</tr>
<tr>
<td class="label">信息接收人</td>
<td>{{ detail.auditOpinion || '' }}</td>
<td class="label">联系方式</td>
<td>{{ customer.contactWay || '' }}</td>
</tr>
<tr>
<td class="label">订货用户及使用<br>用户名称及地址</td>
<td colspan="3">{{ customer.companyName || '' }} {{ customer.address || '' }}</td>
</tr>
<tr>
<td class="label">产品名称</td>
<td>{{ allItemNames }}</td>
<td class="label">合同号</td>
<td>{{ allContractCodes }}</td>
</tr>
<tr>
<td class="label">合同交货量</td>
<td>{{ allOrderAmounts }}</td>
<td class="label">未出库量</td>
<td></td>
</tr>
<tr>
<td class="label">规格mm</td>
<td>{{ allSpecifications }}</td>
<td class="label">产品钢种/牌号</td>
<td>{{ allMaterials }}</td>
</tr>
<tr>
<td class="label">异议量</td>
<td>{{ coilWeightSummary }}</td>
<td class="label">产品用途</td>
<td>{{ detail.productUsage || '' }}</td>
</tr>
</table>
<div class="complaint-desc">
<div class="desc-label">问题描述产品卷号产品生产日期缺陷名称缺陷描述缺陷程度缺陷数量发生问题的生产工序等以及附加缺陷照片</div>
<div class="desc-content">客户反馈{{ detail.complaintContent || '' }}</div>
</div>
<table class="form-table">
<tr>
<td class="label" style="width:100px;vertical-align:middle" :rowspan="2">问题内/<br>外部调<br><br>确认</td>
<td class="label" style="width:60px">内部</td>
<td>
<div>{{ prodOpinion.internalInvestigation || '' }}</div>
<div style="text-align:right;margin-top:8px">
确认人{{ prodOpinion.internalConfirmer || '' }} 日期{{ prodOpinion.internalConfirmDate || '' }}
</div>
</td>
</tr>
<tr>
<td class="label">外部</td>
<td>
<div>{{ prodOpinion.externalInvestigation || '' }}</div>
<div style="text-align:right;margin-top:8px">
确认人{{ prodOpinion.externalConfirmer || '' }} 日期{{ prodOpinion.externalConfirmDate || '' }}
</div>
</td>
</tr>
</table>
<table class="form-table" style="margin-top:-1px">
<tr>
<td class="label" style="width:100px">责任单位</td>
<td style="width:200px">{{ prodOpinion.responsibleUnit || '' }}</td>
<td class="label" style="width:100px">接收人/日期</td>
<td>{{ prodOpinion.receiver || '' }} {{ prodOpinion.acceptDate || '' }}</td>
</tr>
</table>
<table class="form-table" style="margin-top:-1px">
<tr>
<td class="label" style="width:100px;vertical-align:middle" :rowspan="2">原因分析</td>
<td class="label" style="width:70px">产生原因</td>
<td>{{ qualityOpinion.cause || '' }}</td>
</tr>
<tr>
<td class="label">流出原因</td>
<td>{{ qualityOpinion.escapeReason || '' }}</td>
</tr>
</table>
<table class="form-table" style="margin-top:-1px">
<tr>
<td class="label" style="width:100px">纠正/预防措施</td>
<td>{{ qualityOpinion.correctiveAction || '' }}</td>
</tr>
</table>
<table class="form-table" style="margin-top:-1px">
<tr>
<td class="label" style="width:100px">整改计划完成时间</td>
<td style="width:200px">{{ qualityOpinion.rectifyDate || '' }}</td>
<td class="label" style="width:100px">责任人签字/日期</td>
<td></td>
</tr>
</table>
<table class="form-table" style="margin-top:-1px">
<tr>
<td class="label" style="width:120px">品质部主管审核</td>
<td>{{ qualityOpinion.supervisorOpinion || '' }}</td>
</tr>
</table>
<table class="form-table" style="margin-top:-1px">
<tr>
<td class="label" style="width:70px;vertical-align:middle">意见</td>
<td>
<div style="text-align:right">签字{{ qualityOpinion.internalConfirmer || '' }} 日期{{ qualityOpinion.internalConfirmDate || '' }}</div>
</td>
</tr>
<tr>
<td class="label" style="vertical-align:middle">商务<br>处置</td>
<td>
<table class="inner-table">
<tr>
<td class="label" style="width:100px">销售人员处<br>意见</td>
<td>{{ salesOpinion.handlingOpinion || '' }}</td>
</tr>
<tr>
<td class="label">销售部领导<br>审核意见</td>
<td>{{ salesOpinion.leaderOpinion || '' }}</td>
</tr>
</table>
</td>
</tr>
<tr>
<td class="label" style="vertical-align:middle">公司领导审批意<br></td>
<td>
<div style="min-height:40px">{{ detail.planContent || '' }}</div>
<div style="text-align:right;margin-top:8px">签字  日期</div>
</td>
</tr>
</table>
</div>
<!-- ========== 附件三缺陷图片 ========== -->
<div class="page-break"></div>
<div class="attachment-section">
<h2 class="doc-title">附缺陷照片</h2>
<div v-if="defectImages.length > 0" class="defect-grid">
<div v-for="(img, idx) in defectImages" :key="idx" class="defect-item">
<img :src="img" class="defect-img" />
<div class="complaint-desc">
<div class="desc-label">问题描述产品卷号产品生产日期缺陷名称缺陷描述缺陷程度缺陷数量发生问题的生产工序等以及附加缺陷照片</div>
<div class="desc-content">客户反馈{{ detail.complaintContent || '' }}</div>
</div>
<table class="form-table">
<tr>
<td class="label" style="width:100px;vertical-align:middle" :rowspan="2">问题内/<br>外部调<br><br>确认</td>
<td class="label" style="width:60px">内部</td>
<td>
<div>{{ prodOpinion.internalInvestigation || '' }}</div>
<div style="text-align:right;margin-top:8px">
确认人{{ prodOpinion.internalConfirmer || '' }} 日期{{ prodOpinion.internalConfirmDate || '' }}
</div>
</td>
</tr>
<tr>
<td class="label">外部</td>
<td>
<div>{{ prodOpinion.externalInvestigation || '' }}</div>
<div style="text-align:right;margin-top:8px">
确认人{{ prodOpinion.externalConfirmer || '' }} 日期{{ prodOpinion.externalConfirmDate || '' }}
</div>
</td>
</tr>
</table>
<table class="form-table" style="margin-top:-1px">
<tr>
<td class="label" style="width:100px">责任单位</td>
<td style="width:200px">{{ prodOpinion.responsibleUnit || '' }}</td>
<td class="label" style="width:100px">接收人/日期</td>
<td>{{ prodOpinion.receiver || '' }} {{ prodOpinion.acceptDate || '' }}</td>
</tr>
</table>
<table class="form-table" style="margin-top:-1px">
<tr>
<td class="label" style="width:100px;vertical-align:middle" :rowspan="2">原因分析</td>
<td class="label" style="width:70px">产生原因</td>
<td>{{ qualityOpinion.cause || '' }}</td>
</tr>
<tr>
<td class="label">流出原因</td>
<td>{{ qualityOpinion.escapeReason || '' }}</td>
</tr>
</table>
<table class="form-table" style="margin-top:-1px">
<tr>
<td class="label" style="width:100px">纠正/预防措施</td>
<td>{{ qualityOpinion.correctiveAction || '' }}</td>
</tr>
</table>
<table class="form-table" style="margin-top:-1px">
<tr>
<td class="label" style="width:100px">整改计划完成时间</td>
<td style="width:200px">{{ qualityOpinion.rectifyDate || '' }}</td>
<td class="label" style="width:100px">责任人签字/日期</td>
<td></td>
</tr>
</table>
<table class="form-table" style="margin-top:-1px">
<tr>
<td class="label" style="width:120px">品质部主管审核</td>
<td>{{ qualityOpinion.supervisorOpinion || '' }}</td>
</tr>
</table>
<table class="form-table" style="margin-top:-1px">
<tr>
<td class="label" style="width:70px;vertical-align:middle">意见</td>
<td>
<div style="text-align:right">签字{{ qualityOpinion.internalConfirmer || '' }} 日期{{ qualityOpinion.internalConfirmDate || '' }}</div>
</td>
</tr>
<tr>
<td class="label" style="vertical-align:middle">商务<br>处置</td>
<td>
<table class="inner-table">
<tr>
<td class="label" style="width:100px">销售人员处<br>意见</td>
<td>{{ salesOpinion.handlingOpinion || '' }}</td>
</tr>
<tr>
<td class="label">销售部领导<br>审核意见</td>
<td>{{ salesOpinion.leaderOpinion || '' }}</td>
</tr>
</table>
</td>
</tr>
<tr>
<td class="label" style="vertical-align:middle">公司领导审批意<br></td>
<td>
<div style="min-height:40px">{{ detail.planContent || '' }}</div>
<div style="text-align:right;margin-top:8px">签字  日期</div>
</td>
</tr>
</table>
</div>
<!-- ========== 附件三缺陷图片 ========== -->
<div class="page-break"></div>
<div class="attachment-section">
<h2 class="doc-title">附缺陷照片</h2>
<div v-if="defectImages.length > 0" class="defect-grid">
<div v-for="(img, idx) in defectImages" :key="idx" class="defect-item">
<img :src="img" class="defect-img" />
</div>
</div>
<div v-else class="empty-tip">暂无缺陷照片</div>
</div>
<div v-else class="empty-tip">暂无缺陷照片</div>
</div>
</div>
</div>
@@ -322,16 +324,42 @@ export default {
const day = String(d.getDate()).padStart(2, '0');
return `${y}.${m}.${day}`;
},
firstCoil() {
if (this.coilList.length === 0) return {};
const rel = this.coilList[0];
return rel.coilInfo || {};
allItemNames() {
return [...new Set(this.coilList.map(rel => (rel.coilInfo && rel.coilInfo.itemName) || '').filter(Boolean))].join('、');
},
firstContract() {
if (this.coilList.length === 0) return {};
const rel = this.coilList[0];
const orders = (rel.coilInfo && rel.coilInfo.orderList) || [];
return orders[0] || {};
allMaterials() {
return [...new Set(this.coilList.map(rel => (rel.coilInfo && rel.coilInfo.material) || '').filter(Boolean))].join('、');
},
allSpecifications() {
return [...new Set(this.coilList.map(rel => (rel.coilInfo && rel.coilInfo.specification) || '').filter(Boolean))].join('、');
},
allContractCodes() {
const codes = [];
this.coilList.forEach(rel => {
const orders = (rel.coilInfo && rel.coilInfo.orderList) || [];
orders.forEach(o => { if (o.contractCode) codes.push(o.contractCode); });
});
return [...new Set(codes)].join('、');
},
allOrderAmounts() {
const amounts = [];
this.coilList.forEach(rel => {
const orders = (rel.coilInfo && rel.coilInfo.orderList) || [];
orders.forEach(o => { if (o.orderAmount != null) amounts.push(o.orderAmount); });
});
return [...new Set(amounts)].join('、');
},
allExportTimes() {
return this.coilList
.map(rel => this.fmtDate((rel.coilInfo && rel.coilInfo.exportTime) || ''))
.filter(Boolean)
.join('、');
},
allCreateTimes() {
return this.coilList
.map(rel => this.fmtDate((rel.coilInfo && rel.coilInfo.createTime) || ''))
.filter(Boolean)
.join('、');
},
coilNoSummary() {
return this.coilList
@@ -417,43 +445,35 @@ export default {
this.taskList = [];
this.defectImages = [];
},
fmtDate(val) {
if (!val) return '';
const d = new Date(val);
if (isNaN(d.getTime())) return val;
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${y}.${m}.${day}`;
},
async exportPdf() {
const element = this.$refs.pdfContent;
if (!element) return;
const feedbackEl = this.$refs.feedbackFormSection;
const restEl = this.$refs.restContentSection;
if (!feedbackEl || !restEl) return;
this.exporting = true;
try {
const canvas = await html2canvas(element, {
scale: 2,
useCORS: true,
logging: false,
backgroundColor: '#ffffff',
windowWidth: 794
});
const imgData = canvas.toDataURL('image/png');
const pdf = new jsPDF('p', 'mm', 'a4');
const pdfWidth = pdf.internal.pageSize.getWidth();
const pageHeight = pdf.internal.pageSize.getHeight();
const imgWidth = pdfWidth;
const imgHeight = (canvas.height * imgWidth) / canvas.width;
const pageWidth = pdf.internal.pageSize.getWidth(); // 210
const pageHeight = pdf.internal.pageSize.getHeight(); // 297
const margin = 10; // mm
const contentWidth = pageWidth - margin * 2; // 190
const contentHeight = pageHeight - margin * 2; // 277
// 附件一:质量异议反馈单 — 独立起始
await this._renderSection(pdf, feedbackEl, pageWidth, contentWidth, contentHeight, margin);
// 附件二 + 附件三 — 从新页开始
pdf.addPage();
await this._renderSection(pdf, restEl, pageWidth, contentWidth, contentHeight, margin);
if (imgHeight <= pageHeight) {
pdf.addImage(imgData, 'PNG', 0, 0, imgWidth, imgHeight);
} else {
let posY = 0;
const ratio = pdfWidth / canvas.width;
const pageCanvasHeight = pageHeight / ratio;
while (posY < canvas.height) {
if (posY > 0) pdf.addPage();
const pieceCanvas = document.createElement('canvas');
pieceCanvas.width = canvas.width;
pieceCanvas.height = Math.min(pageCanvasHeight, canvas.height - posY);
const ctx = pieceCanvas.getContext('2d');
ctx.drawImage(canvas, 0, posY, canvas.width, pieceCanvas.height, 0, 0, canvas.width, pieceCanvas.height);
const pieceData = pieceCanvas.toDataURL('image/png');
pdf.addImage(pieceData, 'PNG', 0, 0, imgWidth, pieceCanvas.height * ratio);
posY += pageCanvasHeight;
}
}
pdf.save(`${this.detail.complaintNo || '售后单'}_质量异议全套资料.pdf`);
this.$modal.msgSuccess('PDF导出成功');
this.visible = false;
@@ -463,6 +483,38 @@ export default {
} finally {
this.exporting = false;
}
},
async _renderSection(pdf, el, pageWidth, contentWidth, contentHeight, margin) {
const canvas = await html2canvas(el, {
scale: 2,
useCORS: true,
logging: false,
backgroundColor: '#ffffff',
windowWidth: 794
});
const ratio = contentWidth / canvas.width;
const imgFullHeight = canvas.height * ratio;
if (imgFullHeight <= contentHeight) {
// 单页:居中放置
pdf.addImage(canvas.toDataURL('image/png'), 'PNG', margin, margin, contentWidth, imgFullHeight);
return;
}
// 多页分割
let posY = 0;
const pageCanvasHeight = contentHeight / ratio;
while (posY < canvas.height) {
if (posY > 0) pdf.addPage();
const pieceHeight = Math.min(pageCanvasHeight, canvas.height - posY);
const pieceCanvas = document.createElement('canvas');
pieceCanvas.width = canvas.width;
pieceCanvas.height = pieceHeight;
const ctx = pieceCanvas.getContext('2d');
ctx.drawImage(canvas, 0, posY, canvas.width, pieceHeight, 0, 0, canvas.width, pieceHeight);
pdf.addImage(pieceCanvas.toDataURL('image/png'), 'PNG', margin, margin, contentWidth, pieceHeight * ratio);
posY += pageCanvasHeight;
}
}
}
};
@@ -472,7 +524,7 @@ export default {
.export-preview {
max-height: 70vh;
overflow-y: auto;
background: #f5f5f5;
background: #ffffff;
padding: 16px;
}
@@ -493,7 +545,7 @@ export default {
break-after: page;
height: 1px;
margin: 20px 0;
border-top: 1px dashed #ccc;
border-top: 1px dashed #000;
}
.doc-title {
@@ -530,7 +582,6 @@ export default {
}
.form-table .label {
background-color: #f5f5f5;
font-weight: 500;
text-align: center;
vertical-align: middle;
@@ -558,7 +609,7 @@ export default {
.desc-label {
font-size: 12px;
color: #333;
color: #000;
margin-bottom: 8px;
}
@@ -581,7 +632,6 @@ export default {
}
.company-header {
background-color: #f5f5f5;
text-align: center;
font-weight: 700;
font-size: 14px;
@@ -597,7 +647,7 @@ export default {
.defect-item {
flex: 0 0 calc(50% - 6px);
border: 1px solid #ddd;
border: 1px solid #000;
padding: 4px;
}
@@ -610,7 +660,7 @@ export default {
.empty-tip {
text-align: center;
color: #999;
color: #000;
padding: 40px;
font-size: 14px;
}

View File

@@ -70,8 +70,8 @@
<HeaderControlSection :complaintNo="currentRow.complaintNo" :flowStatus="currentRow.flowStatus"
:meta="currentRow">
<template #actions>
<!-- <el-button :loading="pdfLoading" size="mini" type="text" icon="el-icon-printer" @click="handlePrint"
:disabled="pdfLoading" title="导出PDF">导出PDF</el-button> -->
<el-button v-if="currentRow.flowStatus === 4" size="mini" type="text" icon="el-icon-download"
@click="handleExportPdf(currentRow)" title="导出PDF">导出</el-button>
<el-button v-if="currentRow.flowStatus === 1" size="mini" type="primary" plain
icon="el-icon-s-promotion" @click="handleOpinionDispatch">意见下发</el-button>
<el-button v-if="false" size="mini" type="warning" plain