Merge remote-tracking branch 'origin/0.8.X' into 0.8.X

This commit is contained in:
朱昊天
2026-07-06 16:04:31 +08:00
117 changed files with 6077 additions and 1170 deletions

View File

@@ -0,0 +1,44 @@
import request from '@/utils/request'
// 查询工艺列表
export function listProcess(query) {
return request({
url: '/flow/prodProcess/list',
method: 'get',
params: query
})
}
// 查询工艺详细
export function getProcess(processId) {
return request({
url: '/flow/prodProcess/' + processId,
method: 'get'
})
}
// 新增工艺
export function addProcess(data) {
return request({
url: '/flow/prodProcess',
method: 'post',
data: data
})
}
// 修改工艺
export function updateProcess(data) {
return request({
url: '/flow/prodProcess',
method: 'put',
data: data
})
}
// 删除工艺
export function delProcess(processIds) {
return request({
url: '/flow/prodProcess/' + processIds,
method: 'delete'
})
}

View File

@@ -0,0 +1,44 @@
import request from '@/utils/request'
// 查询工艺步骤列表
export function listProcessStep(query) {
return request({
url: '/flow/prodProcessStep/list',
method: 'get',
params: query
})
}
// 查询工艺步骤详细
export function getProcessStep(stepId) {
return request({
url: '/flow/prodProcessStep/' + stepId,
method: 'get'
})
}
// 新增工艺步骤
export function addProcessStep(data) {
return request({
url: '/flow/prodProcessStep',
method: 'post',
data: data
})
}
// 修改工艺步骤
export function updateProcessStep(data) {
return request({
url: '/flow/prodProcessStep',
method: 'put',
data: data
})
}
// 删除工艺步骤
export function delProcessStep(stepIds) {
return request({
url: '/flow/prodProcessStep/' + stepIds,
method: 'delete'
})
}

View File

@@ -73,12 +73,12 @@ export function delScheduleItem(ids) {
// ====== 排产单明细项 接收/合并 ======
// 接收产需单(后端全字段复制
export function receiveScheduleItem(scheduleId) {
// 接收产需单(根据配置的工序步骤生成排产明细
export function receiveScheduleItem(data) {
return request({
url: '/flow/prodScheduleItem/receive',
method: 'post',
data: { scheduleId }
data: data
})
}

View File

@@ -51,12 +51,11 @@ export function submitQualityReview(reviewId) {
})
}
// 审批通过
export function approveQualityReview(data) {
// 审批通过(创建时已填好内容,领导仅确认)
export function approveQualityReview(reviewId) {
return request({
url: '/qc/qualityReview/approve',
method: 'post',
data: data
url: '/qc/qualityReview/approve/' + reviewId,
method: 'post'
})
}

View File

@@ -63,6 +63,19 @@ export function exportAttendanceReport(params) {
url: '/wms/attendanceRecords/exportReport',
method: 'post',
params: params,
responseType: 'blob'
responseType: 'blob',
// enames 是数组,需要特殊序列化避免逗号被二次编码
paramsSerializer: params => {
const searchParams = new URLSearchParams();
Object.keys(params).forEach(key => {
const val = params[key];
if (Array.isArray(val)) {
searchParams.append(key, val.join(','));
} else if (val !== undefined && val !== null && val !== '') {
searchParams.append(key, val);
}
});
return searchParams.toString();
}
})
}

View File

@@ -42,3 +42,20 @@ export function delEmployeeInfo(infoId) {
method: 'delete'
})
}
// 获取所有不重复的部门名称
export function getDepts() {
return request({
url: '/wms/employeeInfo/depts',
method: 'get'
})
}
// 根据部门查询在职员工
export function getEmployeesByDept(dept) {
return request({
url: '/wms/employeeInfo/employeesByDept',
method: 'get',
params: { dept }
})
}

View File

@@ -0,0 +1,112 @@
<template>
<div class="process-select">
<el-select
v-model="processId"
:placeholder="placeholder"
:clearable="clearable"
:disabled="disabled"
filterable
class="process-select-input"
@clear="handleClear"
>
<el-option
v-for="item in processList"
:key="item.processId"
:label="item.processName"
:value="item.processId"
>
<span class="process-option-name">{{ item.processName }}</span>
<span v-if="item._steps" class="process-option-steps">{{ item._steps }}</span>
</el-option>
</el-select>
</div>
</template>
<script>
import { listProcess } from '@/api/aps/process'
export default {
name: 'ProcessSelect',
props: {
value: {
type: [String, Number, undefined],
default: undefined
},
placeholder: {
type: String,
default: '请选择工序'
},
clearable: {
type: Boolean,
default: true
},
disabled: {
type: Boolean,
default: false
}
},
data() {
return {
processList: []
}
},
computed: {
processId: {
get() {
return this.value
},
set(val) {
const process = this.processList.find(item => item.processId === val) || null
this.$emit('input', val)
this.$emit('change', val, process)
}
}
},
created() {
this.getProcessList()
},
methods: {
getProcessList() {
listProcess({ pageNum: 1, pageSize: 1000 }).then(res => {
const rows = res.rows || []
this.processList = rows.map(item => ({
...item,
_steps: (item.stepList && item.stepList.length > 0)
? item.stepList.map(s => s.stepName).join(' → ')
: ''
}))
}).catch(() => {
this.processList = []
})
},
handleClear() {
this.$emit('input', undefined)
this.$emit('change', undefined, null)
}
}
}
</script>
<style scoped lang="scss">
.process-select {
width: 100%;
}
.process-select-input {
width: 100%;
}
.process-option-name {
font-size: 14px;
font-weight: 500;
color: #303133;
}
.process-option-steps {
display: block;
margin-top: 4px;
font-size: 11px;
color: #909399;
line-height: 1.4;
}
</style>

View File

@@ -20,9 +20,6 @@ export default {
const { icon, title, hasChildren } = context.props
// 函数式组件中 :style 绑定不会进 props需要从 context.data.style 读取
const customStyle = context.data.style || {}
if (Object.keys(customStyle).length > 0) {
console.log('[Item] ✅ 收到 style:', customStyle, '| title:', title)
}
const vnodes = []
if (icon) {

View File

@@ -30,9 +30,9 @@
</span>
</div>
<!-- <el-alert :title="'已配置'+allCols.length+'个列'" type="info" :closable="false" show-icon style="margin-bottom:8px" /> -->
<el-table v-loading="gridLoading" height="calc(100vh - 260px)" :data="gridRows" border stripe size="mini" style="width:100%" :header-cell-style="headerStyle" :key="'tbl-'+inputMode">
<el-table v-loading="gridLoading" height="calc(100vh - 260px)" :data="tableData" border stripe size="mini" style="width:100%" :header-cell-style="headerStyle" :row-class-name="rowClassName" :key="'tbl-'+inputMode">
<el-table-column label="日期" width="135" fixed>
<template slot-scope="s"><el-date-picker v-model="s.row.detailDate" type="date" value-format="yyyy-MM-dd" size="mini" style="width:124px" @change="sortGrid" /></template>
<template slot-scope="s"><span v-if="s.row.$isSummary" class="summary-label">{{ s.row.detailDate }}</span><el-date-picker v-else v-model="s.row.detailDate" type="date" value-format="yyyy-MM-dd" size="mini" style="width:124px" @change="sortGrid" /></template>
</el-table-column>
<template v-for="col in displayCols">
<el-table-column v-if="col.$type==='detail' && !col.isShift" :key="'d'+col.itemId" align="center" width="130">
@@ -40,7 +40,8 @@
<div class="col-hd">{{ col.itemName }}{{ col.unit ? '('+col.unit+')' : '' }}</div>
</template>
<template slot-scope="s">
<el-input v-model="s.row['q'+col.itemId]" size="mini" @input="recalcAll" :class="{'anomaly-input': isAnomalyCell(s.row.detailDate, col.itemId)}">
<span v-if="s.row.$isSummary" class="summary-val">{{ s.row['q'+col.itemId] || '-' }}</span>
<el-input v-else v-model="s.row['q'+col.itemId]" size="mini" @input="recalcAll" :class="{'anomaly-input': isAnomalyCell(s.row.detailDate, col.itemId)}">
<!-- <span slot="suffix" v-if="col.queryCondition" class="input-suffix-actions">
<i title="反填" v-if="col.category==='辅料'||col.category==='能耗'" :class="backfillLoading[col.itemId]?'el-icon-loading':'el-icon-upload2'" class="ica ica-backfill" @click.stop="backfillCell(col, s.row)" />
<i title="自动获取" :class="autoLoading[col.itemId]?'el-icon-loading':'el-icon-refresh'" class="ica ica-fetch" @click.stop="fetchAutoData(col, s.row)" />
@@ -53,27 +54,39 @@
<div class="col-hd">{{ col.itemName }}{{ col.unit ? '('+col.unit+')' : '' }}</div>
</template>
<template slot-scope="s">
<div class="shift-cell">
<span class="shift-tag"></span>
<el-input v-model="s.row['q'+col.itemId+'_1']" size="mini" class="shift-input" @input="recalcAll" :class="{'anomaly-input': isAnomalyCell(s.row.detailDate, col.itemId)}">
<!-- <span slot="suffix" v-if="col.queryCondition" class="input-suffix-actions"><i v-if="col.category==='辅料'||col.category==='能耗'" :class="backfillLoading[col.itemId]?'el-icon-loading':'el-icon-upload2'" class="ica ica-backfill" @click.stop="backfillCell(col, s.row, '1')" /><i :class="autoLoading[col.itemId]?'el-icon-loading':'el-icon-refresh'" class="ica ica-fetch" @click.stop="fetchAutoData(col, s.row, '1')" /></span> -->
</el-input>
</div>
<div class="shift-cell">
<span class="shift-tag"></span>
<el-input v-model="s.row['q'+col.itemId+'_2']" size="mini" class="shift-input" @input="recalcAll" :class="{'anomaly-input': isAnomalyCell(s.row.detailDate, col.itemId)}">
<!-- <span slot="suffix" v-if="col.queryCondition" class="input-suffix-actions"><i v-if="col.category==='辅料'||col.category==='能耗'" :class="backfillLoading[col.itemId]?'el-icon-loading':'el-icon-upload2'" class="ica ica-backfill" @click.stop="backfillCell(col, s.row, '2')" /><i :class="autoLoading[col.itemId]?'el-icon-loading':'el-icon-refresh'" class="ica ica-fetch" @click.stop="fetchAutoData(col, s.row, '2')" /></span> -->
</el-input>
</div>
<template v-if="s.row.$isSummary">
<div class="shift-cell"><span class="shift-tag"></span><span class="summary-val">{{ s.row['q'+col.itemId+'_1'] || '-' }}</span></div>
<div class="shift-cell"><span class="shift-tag"></span><span class="summary-val">{{ s.row['q'+col.itemId+'_2'] || '-' }}</span></div>
</template>
<template v-else>
<div class="shift-cell">
<span class="shift-tag"></span>
<el-input v-model="s.row['q'+col.itemId+'_1']" size="mini" class="shift-input" @input="recalcAll" :class="{'anomaly-input': isAnomalyCell(s.row.detailDate, col.itemId)}">
<!-- <span slot="suffix" v-if="col.queryCondition" class="input-suffix-actions"><i v-if="col.category==='辅料'||col.category==='能耗'" :class="backfillLoading[col.itemId]?'el-icon-loading':'el-icon-upload2'" class="ica ica-backfill" @click.stop="backfillCell(col, s.row, '1')" /><i :class="autoLoading[col.itemId]?'el-icon-loading':'el-icon-refresh'" class="ica ica-fetch" @click.stop="fetchAutoData(col, s.row, '1')" /></span> -->
</el-input>
</div>
<div class="shift-cell">
<span class="shift-tag"></span>
<el-input v-model="s.row['q'+col.itemId+'_2']" size="mini" class="shift-input" @input="recalcAll" :class="{'anomaly-input': isAnomalyCell(s.row.detailDate, col.itemId)}">
<!-- <span slot="suffix" v-if="col.queryCondition" class="input-suffix-actions"><i v-if="col.category==='辅料'||col.category==='能耗'" :class="backfillLoading[col.itemId]?'el-icon-loading':'el-icon-upload2'" class="ica ica-backfill" @click.stop="backfillCell(col, s.row, '2')" /><i :class="autoLoading[col.itemId]?'el-icon-loading':'el-icon-refresh'" class="ica ica-fetch" @click.stop="fetchAutoData(col, s.row, '2')" /></span> -->
</el-input>
</div>
</template>
</template>
</el-table-column>
<el-table-column v-else-if="col.$type==='metric' && !col.isShift" :key="'m'+col.mIdx" :label="col.metricName+(col.unit?'('+col.unit+')':'')" width="85" align="center">
<template slot-scope="s">{{ s.row['mv'+col.mIdx]!=null ? s.row['mv'+col.mIdx] : '-' }}</template>
<template slot-scope="s"><span v-if="s.row.$isSummary" class="summary-val">{{ s.row['mv'+col.mIdx]!=null ? s.row['mv'+col.mIdx] : '-' }}</span><template v-else>{{ s.row['mv'+col.mIdx]!=null ? s.row['mv'+col.mIdx] : '-' }}</template></template>
</el-table-column>
<el-table-column v-else-if="col.$type==='metric' && col.isShift" :key="'ms'+col.mIdx" :label="col.metricName+(col.unit?'('+col.unit+')':'')" width="95" align="center">
<template slot-scope="s">
<div class="shift-metric"> {{ s.row['mv'+col.mIdx+'_1']!=null ? s.row['mv'+col.mIdx+'_1'] : '-' }}</div>
<div class="shift-metric"> {{ s.row['mv'+col.mIdx+'_2']!=null ? s.row['mv'+col.mIdx+'_2'] : '-' }}</div>
<template v-if="s.row.$isSummary">
<div class="shift-metric"><span class="summary-val"> {{ s.row['mv'+col.mIdx+'_1']!=null ? s.row['mv'+col.mIdx+'_1'] : '-' }}</span></div>
<div class="shift-metric"><span class="summary-val"> {{ s.row['mv'+col.mIdx+'_2']!=null ? s.row['mv'+col.mIdx+'_2'] : '-' }}</span></div>
</template>
<template v-else>
<div class="shift-metric"> {{ s.row['mv'+col.mIdx+'_1']!=null ? s.row['mv'+col.mIdx+'_1'] : '-' }}</div>
<div class="shift-metric"> {{ s.row['mv'+col.mIdx+'_2']!=null ? s.row['mv'+col.mIdx+'_2'] : '-' }}</div>
</template>
</template>
</el-table-column>
</template>
@@ -82,7 +95,7 @@
<!-- <el-button size="mini" type="text" style="padding:0 2px;font-size:11px" @click="saveRow(s.row)">保存</el-button>
<el-button size="mini" type="text" style="padding:0 2px;font-size:11px" @click="fetchRow(s.row)">抓取</el-button>
<el-button size="mini" type="text" style="padding:0 2px;font-size:11px;color:#67c23a" @click="backfillRow(s.row)">反填</el-button> -->
<el-button size="mini" type="text" style="padding:0 2px;font-size:11px;color:#f56c6c" @click="gridRows.splice(s.$index,1)">删除</el-button>
<el-button v-if="!s.row.$isSummary" size="mini" type="text" style="padding:0 2px;font-size:11px;color:#f56c6c" @click="gridRows.splice(s.$index - summaryRows.length,1)">删除</el-button>
</template>
</el-table-column>
</el-table>
@@ -494,6 +507,74 @@ export default {
})
return col && col.color ? { background: col.color, color: '#fff' } : {}
}
},
summaryRows() {
const rows = this.gridRows.filter(r => r.detailDate)
if (!rows.length) return []
const detailCols = this.allCols.filter(c => c.$type === 'detail')
const metricCols = this.allCols.filter(c => c.$type === 'metric')
const rp = this.activeReport || {}
const esc = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
const evalF = (f) => { const s = f.replace(/[^0-9+\-*/.()\s]/g,''); if(!s) return null; try { const r = new Function('return ('+s+')')(); return isFinite(r)?(Math.round(r*100)/100).toFixed(2):null } catch(e){ return null } }
const calc = (type) => {
const row = { detailDate: type === 'sum' ? '总和' : '平均', $isSummary: true, $summaryType: type }
const n = rows.length || 1
detailCols.forEach(col => {
if (col.isShift) {
const s1 = rows.reduce((a, r) => a + (parseFloat(r['q' + col.itemId + '_1']) || 0), 0)
const s2 = rows.reduce((a, r) => a + (parseFloat(r['q' + col.itemId + '_2']) || 0), 0)
row['q' + col.itemId + '_1'] = type === 'sum' ? s1.toFixed(2) : (s1 / n).toFixed(2)
row['q' + col.itemId + '_2'] = type === 'sum' ? s2.toFixed(2) : (s2 / n).toFixed(2)
row['q' + col.itemId] = type === 'sum' ? (s1 + s2).toFixed(2) : ((s1 + s2) / n).toFixed(2)
} else {
const s = rows.reduce((a, r) => a + (parseFloat(r['q' + col.itemId]) || 0), 0)
row['q' + col.itemId] = type === 'sum' ? s.toFixed(2) : (s / n).toFixed(2)
}
})
metricCols.forEach((m, mi) => {
if (!m.metricFormula) {
if (m.isShift) { row['mv'+m.mIdx+'_1'] = null; row['mv'+m.mIdx+'_2'] = null }
else row['mv'+m.mIdx] = null
return
}
const ef = (shiftId) => {
let f = m.metricFormula
for (let pmi = 0; pmi < mi; pmi++) {
const pm = metricCols[pmi]; if (!pm.metricName) continue
const pn = esc(pm.metricName)
if (pm.isShift) {
const pv1 = row['mv'+pm.mIdx+'_1']; const pv2 = row['mv'+pm.mIdx+'_2']
if (pv1 != null) f = f.replace(new RegExp('@\\{'+pn+'\\}\\.甲班','g'), pv1)
if (pv2 != null) f = f.replace(new RegExp('@\\{'+pn+'\\}\\.乙班','g'), pv2)
}
const pv = pm.isShift ? (shiftId ? row['mv'+pm.mIdx+'_'+shiftId] : null) : row['mv'+pm.mIdx]
if (pv != null) f = f.replace(new RegExp('@\\{'+pn+'\\}','g'), pv)
}
detailCols.forEach(c => {
const item = this.allItems.find(i => String(i.itemId) === String(c.itemId))
if (!item || !item.itemCode) return; const code = item.itemCode
if (c.isShift) {
const v1 = parseFloat(row['q'+c.itemId+'_1'])||0; const v2 = parseFloat(row['q'+c.itemId+'_2'])||0
f = f.replace(new RegExp('@\\{'+code+'\\}\\.甲班','g'), v1).replace(new RegExp('@\\{'+code+'\\}\\.乙班','g'), v2)
const v = shiftId ? (shiftId==='1'?v1:v2) : v1+v2
f = f.replace(new RegExp('@\\{'+code+'\\}','g'), v)
} else {
const v = parseFloat(row['q'+c.itemId])||0
f = f.replace(new RegExp('@\\{'+code+'\\}\\.甲班','g'), v).replace(new RegExp('@\\{'+code+'\\}\\.乙班','g'), v).replace(new RegExp('@\\{'+code+'\\}','g'), v)
}
})
f = f.replace(/input_weight/g, rp.inputWeight||0).replace(/output_weight/g, rp.outputWeight||0).replace(/price/g, (m.usePrice == 1) ? (m.metricValue || 0) : 0)
return evalF(f)
}
if (m.isShift) { row['mv'+m.mIdx+'_1']=ef('1'); row['mv'+m.mIdx+'_2']=ef('2') }
else row['mv'+m.mIdx]=ef(null)
})
return row
}
return [calc('sum'), calc('avg')]
},
tableData() {
return [...this.summaryRows, ...this.gridRows]
}
},
watch: { configOpen(v) { if (!v) this.rpOpen = false } },
@@ -509,6 +590,10 @@ export default {
}
},
methods: {
rowClassName({ row }) {
if (row.$isSummary) return 'summary-row'
return ''
},
/* report */
getList() {
this.loading = true
@@ -1142,4 +1227,8 @@ export default {
.ica-backfill { color: #67c23a; margin-right: 1px; }
.ica:hover { opacity: 0.7; }
/deep/ .anomaly-input .el-input__inner { background: #fef0f0 !important; border-color: #f56c6c !important; }
/deep/ .summary-row td { background: #f0f7ff !important; font-weight: bold; }
/deep/ .summary-row td .cell { color: #303133; }
.summary-label { font-weight: bold; color: #303133; padding: 0 5px; }
.summary-val { font-weight: bold; color: #303133; }
</style>

View File

@@ -1,7 +1,7 @@
<template>
<div class="app-container cost-trend-page">
<!-- 筛选区 -->
<el-card class="mb8">
<el-card class="mb8 filter-card">
<div class="filter-bar">
<el-radio-group
v-model="selectedLine"
@@ -16,7 +16,6 @@
{{ line.lineName }}
</el-radio-button>
</el-radio-group>
<el-button type="primary" size="small" icon="el-icon-search" :loading="loading" @click="fetchData">
查询
</el-button>
@@ -47,10 +46,10 @@
multiple
collapse-tags
placeholder="成本类别(全部)"
size="small"
style="width:160px"
@change="onCategoryChange"
clearable
size="small"
style="width:150px"
@change="onCategoryChange"
>
<el-option label="原料" value="原料" />
<el-option label="能耗" value="能耗" />
@@ -67,10 +66,10 @@
collapse-tags
filterable
placeholder="成本项(全部)"
size="small"
style="width:220px"
@change="onItemChange"
clearable
size="small"
style="width:200px"
@change="onItemChange"
>
<el-option
v-for="it in filteredItemOptions"
@@ -81,13 +80,21 @@
</el-select>
</div>
<!-- 图表区 -->
<div class="chart-box" v-loading="loading">
<div ref="trendChart" class="chart-body" />
</div>
<div v-if="summary.seriesCount > 15" class="legend-hint">
<i class="el-icon-info" /> 图例较多可在下方图例区滚动查看或缩小筛选范围以减少趋势线数量
<!-- 迷你图表网格 -->
<div v-loading="loading" class="mini-charts-grid">
<template v-if="seriesList.length">
<div v-for="s in seriesList" :key="s.key" class="mini-chart-card">
<div class="mini-chart-header">
<span class="mini-chart-dot" :style="{ background: s.color }" />
<span class="mini-chart-name">{{ s.name }}</span>
<span class="mini-chart-max">峰值 {{ formatMiniMax(s.maxVal) }}</span>
</div>
<div ref="miniCharts" class="mini-chart-body" />
</div>
</template>
<div v-else-if="!loading && reports.length" class="empty-hint">
暂无匹配的成本项数据
</div>
</div>
</div>
</template>
@@ -116,7 +123,8 @@ export default {
selectedItems: [],
allItems: [],
loading: false,
chart: null,
chartInstances: [],
seriesList: [],
reports: [],
cachedDetails: [],
summary: { seriesCount: 0, maxLabel: '-' }
@@ -128,10 +136,6 @@ export default {
if (!this.selectedLine || !this.productionLines.length) return null
return this.productionLines.find(l => l.lineId === this.selectedLine) || null
},
lineNameDisplay() {
const line = this.selectedLineObj
return line ? line.lineName : ''
},
filteredItemOptions() {
if (!this.selectedCategories.length) return this.allItems
return this.allItems.filter(it => this.selectedCategories.includes(it.category))
@@ -143,12 +147,12 @@ export default {
this.$nextTick(() => this.fetchData())
})
this.loadItems()
this._resizeHandler = () => { if (this.chart) this.chart.resize() }
this._resizeHandler = () => this.resizeAllCharts()
window.addEventListener('resize', this._resizeHandler)
},
beforeDestroy() {
window.removeEventListener('resize', this._resizeHandler)
if (this.chart) { this.chart.dispose(); this.chart = null }
this.disposeAllCharts()
},
methods: {
initDefaultLine() {
@@ -186,11 +190,9 @@ export default {
this.allItems = r.rows || []
} catch (e) { /* ignore */ }
},
async fetchData() {
if (this.selectedLine == null) {
return
}
if (this.selectedLine == null) return
this.loading = true
try {
@@ -204,13 +206,13 @@ export default {
const allReports = (reportRes.rows || []).sort((a, b) => {
return (a.reportDate || '').localeCompare(b.reportDate || '')
})
console.log('[CostTrend] reports:', allReports.length, allReports.map(r => ({ id: r.reportId, title: r.reportTitle, date: r.reportDate })))
if (!allReports.length) {
this.$modal.msgWarning('该产线暂无报表')
this.reports = []
this.seriesList = []
this.summary = { seriesCount: 0, maxLabel: '-' }
if (this.chart) this.chart.clear()
this.disposeAllCharts()
return
}
this.reports = allReports
@@ -224,15 +226,11 @@ export default {
}
},
// ==================== 成本项模式 ====================
async fetchDetailsAndCache(reports) {
const allDetailResults = await Promise.all(
reports.map(r =>
listProdDetail({ reportId: r.reportId, pageNum: 1, pageSize: 99999 })
.then(res => {
console.log('[CostTrend] detail for report', r.reportId, 'rows:', (res.rows || []).length)
return { reportId: r.reportId, rows: res.rows || [] }
})
.then(res => ({ reportId: r.reportId, rows: res.rows || [] }))
.catch(e => {
console.error('[CostTrend] detail fetch error for report', r.reportId, e)
return { reportId: r.reportId, rows: [] }
@@ -279,7 +277,7 @@ export default {
})
const xLabels = reports.map(r => this.reportLabel(r))
const series = []
const list = []
let globalMax = 0
Array.from(allItemIds).forEach((key, idx) => {
@@ -288,115 +286,121 @@ export default {
const data = reports.map(r => {
const sums = reportSums[r.reportId] || {}
const val = sums[key] ?? null
if (val != null && val > globalMax) globalMax = val
return val
})
if (data.every(v => v === null)) return
series.push({
const maxVal = Math.max(...data.filter(v => v != null))
if (maxVal > globalMax) globalMax = maxVal
list.push({
key,
name,
type: 'line',
color: COLORS[idx % COLORS.length],
data,
smooth: true,
symbol: 'circle',
symbolSize: 6,
lineStyle: { width: 2, color: COLORS[idx % COLORS.length] },
itemStyle: { color: COLORS[idx % COLORS.length] },
emphasis: { focus: 'series' }
xLabels,
maxVal
})
})
this.seriesList = list
this.summary = {
seriesCount: series.length,
seriesCount: list.length,
maxLabel: this.formatMoney(globalMax)
}
this.$nextTick(() => {
setTimeout(() => this.renderChart(xLabels, series, '数量'), 50)
this.$nextTick(() => this.renderMiniCharts())
},
// ==================== 迷你图表渲染 ====================
disposeAllCharts() {
this.chartInstances.forEach(c => {
try { c.dispose() } catch (e) { /* ignore */ }
})
this.chartInstances = []
},
resizeAllCharts() {
this.chartInstances.forEach(c => {
try { c.resize() } catch (e) { /* ignore */ }
})
},
// ==================== 图表渲染 ====================
renderChart(xLabels, series, yAxisName) {
console.log('[CostTrend] renderChart called, ref:', !!this.$refs.trendChart, 'series:', series.length, 'xLabels:', xLabels.length)
if (!this.$refs.trendChart) return
if (this.chart) this.chart.dispose()
this.chart = echarts.init(this.$refs.trendChart)
renderMiniCharts() {
this.disposeAllCharts()
if (!series.length) {
this.chart.setOption({
title: { text: '暂无数据', left: 'center', top: 'center', textStyle: { color: '#999', fontSize: 14 } },
xAxis: { show: false },
yAxis: { show: false },
series: []
})
return
}
const containers = this.$refs.miniCharts
if (!containers || !containers.length) return
this.chart.setOption({
tooltip: {
trigger: 'axis',
formatter: function (params) {
if (!params || !params.length) return ''
let html = '<b>' + params[0].axisValue + '</b><br/>'
params.forEach(p => {
if (p.value != null) {
html += '<span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:' +
p.color + ';margin-right:5px;"></span>' +
p.seriesName + ': <b>' +
Number(p.value).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) +
'</b><br/>'
}
})
return html
}
},
legend: {
type: 'scroll',
bottom: 0,
textStyle: { fontSize: 11 },
pageIconSize: 12,
pageTextStyle: { fontSize: 11 }
},
grid: {
left: '3%',
right: '4%',
top: '3%',
bottom: series.length > 20 ? '18%' : (series.length > 10 ? '14%' : '10%'),
containLabel: true
},
xAxis: {
type: 'category',
data: xLabels,
boundaryGap: false,
axisLabel: {
fontSize: 11,
rotate: xLabels.length > 8 ? 30 : 0
}
},
yAxis: {
type: 'value',
name: yAxisName,
axisLabel: {
fontSize: 11,
formatter: function (v) {
if (v >= 10000) return (v / 10000).toFixed(1) + '万'
return v
this.seriesList.forEach((s, idx) => {
const el = containers[idx]
if (!el) return
const chart = echarts.init(el)
chart.setOption({
tooltip: {
trigger: 'axis',
formatter: function(params) {
if (!params || !params.length) return ''
const p = params[0]
return '<b>' + p.axisValue + '</b><br/>' +
'<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:' +
p.color + ';margin-right:4px;"></span>' +
p.seriesName + ': <b>' +
Number(p.value).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) +
'</b>'
}
},
splitLine: { lineStyle: { type: 'dashed', color: '#e8e8e8' } }
},
dataZoom: xLabels.length > 10 ? [
{
type: 'slider',
bottom: series.length > 10 ? (series.length > 20 ? 60 : 50) : 35,
height: 16,
textStyle: { fontSize: 10 }
grid: {
left: 8,
right: 12,
top: 8,
bottom: 24,
containLabel: true
},
{ type: 'inside' }
] : [],
series
}, true)
xAxis: {
type: 'category',
data: s.xLabels,
boundaryGap: false,
axisLabel: {
fontSize: 10,
rotate: s.xLabels.length > 8 ? 25 : 0,
interval: s.xLabels.length > 12 ? 'auto' : 0
},
axisTick: { show: false },
axisLine: { lineStyle: { color: '#ddd' }}
},
yAxis: {
type: 'value',
axisLabel: {
fontSize: 10,
formatter: function(v) {
if (v >= 10000) return (v / 10000).toFixed(1) + '万'
return v
}
},
splitLine: { lineStyle: { type: 'dashed', color: '#eee' }},
splitNumber: 3
},
series: [{
name: s.name,
type: 'line',
data: s.data,
smooth: true,
symbol: 'none',
lineStyle: { width: 1.5, color: s.color },
itemStyle: { color: s.color },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: s.color + '30' },
{ offset: 1, color: s.color + '05' }
])
}
}]
}, true)
this.chartInstances.push(chart)
})
},
reportLabel(r) {
@@ -411,78 +415,132 @@ export default {
const num = Number(val)
if (num >= 10000) return (num / 10000).toFixed(2) + '万'
return num.toFixed(2)
},
formatMiniMax(val) {
if (val == null || isNaN(val)) return '-'
if (val >= 10000) return (val / 10000).toFixed(1) + '万'
return Number(val).toFixed(1)
}
}
}
</script>
<style scoped>
.mb8 { margin-bottom: 10px; }
.mb8 { margin-bottom: 8px; }
.filter-card {
border: none;
box-shadow: none;
background: transparent;
}
.filter-card >>> .el-card__body {
padding: 0;
}
.filter-bar {
display: flex;
align-items: center;
gap: 10px;
gap: 8px;
flex-wrap: wrap;
}
/* 统计摘要 */
.stats-row {
display: flex;
gap: 8px;
margin-bottom: 10px;
gap: 6px;
margin-bottom: 8px;
}
.stat-item {
flex: 1;
min-width: 100px;
min-width: 80px;
background: #fff;
border: 1px solid #ebeef5;
border-radius: 2px;
padding: 10px 12px;
border-radius: 4px;
padding: 8px 10px;
text-align: center;
}
.stat-val {
display: block;
font-size: 22px;
font-size: 20px;
font-weight: 600;
color: #303133;
line-height: 1.2;
}
.stat-label {
display: block;
font-size: 12px;
font-size: 11px;
color: #909399;
margin-top: 2px;
margin-top: 1px;
}
.stat-blue .stat-val { color: #409eff; }
.stat-orange .stat-val { color: #e6a23c; }
.stat-green .stat-val { color: #67c23a; }
.chart-box {
background: #fff;
border: 1px solid #ebeef5;
border-radius: 2px;
}
/* 图表筛选 */
.chart-filter-bar {
display: flex;
align-items: center;
gap: 10px;
gap: 8px;
margin-bottom: 8px;
}
.chart-filter-bar .filter-label {
font-size: 13px;
font-size: 12px;
color: #606266;
white-space: nowrap;
}
.chart-body {
width: 100%;
height: 480px;
/* 迷你图表网格 */
.mini-charts-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
gap: 10px;
}
.legend-hint {
margin-top: 8px;
font-size: 12px;
color: #909399;
.mini-chart-card {
background: #fff;
border: 1px solid #ebeef5;
border-radius: 4px;
overflow: hidden;
}
.mini-chart-header {
display: flex;
align-items: center;
gap: 4px;
gap: 6px;
padding: 8px 10px 0 10px;
font-size: 13px;
color: #303133;
}
.mini-chart-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.mini-chart-name {
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mini-chart-max {
margin-left: auto;
font-size: 11px;
color: #909399;
flex-shrink: 0;
}
.mini-chart-body {
width: 100%;
height: 220px;
}
.empty-hint {
grid-column: 1 / -1;
text-align: center;
padding: 60px 0;
font-size: 13px;
color: #909399;
}
</style>

View File

@@ -55,7 +55,7 @@
<el-table-column label="备注" align="center" prop="remark" show-overflow-tooltip />
<el-table-column label="发货状态" align="center" prop="status" width="150">
<el-table-column label="发货状态" align="center" prop="status" width="80">
<template slot-scope="scope">
<el-tag v-if="scope.row.status === 1" type="success" size="mini">已发货</el-tag>
<el-tag v-else type="info" size="mini">未发货</el-tag>
@@ -72,7 +72,7 @@
</el-button>
<el-button size="mini" type="text" icon="el-icon-search"
@click="handleDigitalCoilNo(scope.row)">数字钢卷</el-button>
<el-button size="mini" type="text" icon="el-icon-search" @click="handleTrace(scope.row)">追溯</el-button>
<el-button size="mini" type="text" icon="el-icon-search" @click="handleTrace(scope.row)">钢卷追溯</el-button>
</template>
</el-table-column>
</KLPTable>
@@ -282,4 +282,10 @@ export default {
},
}
}
</script>
</script>
<style>
.el-table .el-button + .el-button {
margin-left: 0;
}
</style>

View File

@@ -39,49 +39,49 @@
<!-- 产品行 -->
<div v-for="(item, index) in products" :key="index" class="table-row" :class="{ 'table-row-hover': !readonly }">
<div class="table-cell">
<div class="table-cell edit-cell">
<div class="serial-number">
<span>{{ index + 1 }}</span>
<el-button v-if="!readonly && products.length > 1" type="text" size="mini" icon="el-icon-delete"
class="delete-btn" @click="removeProduct(index)" />
</div>
</div>
<div class="table-cell">
<div class="table-cell edit-cell">
<el-input v-model="item.spec" placeholder="请输入规格" :readonly="readonly" size="small" />
</div>
<div class="table-cell">
<div class="table-cell edit-cell">
<el-select v-model="item.material" placeholder="请选择材质" :disabled="readonly" size="small" filterable allow-create clearable style="width:100%;">
<el-option v-for="opt in materialOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
</el-select>
</div>
<div class="table-cell">
<div class="table-cell edit-cell">
<el-input v-model="item.quantity" placeholder="请输入数量" :readonly="readonly" size="small"
@change="onQuantityChange(item)" />
</div>
<div class="table-cell">
<div class="table-cell edit-cell">
<el-input v-model="item.taxPrice" placeholder="请输入含税单价" :readonly="readonly" size="small"
@change="onTaxPriceChange(item)" />
</div>
<div class="table-cell">
<div class="table-cell edit-cell">
<el-input v-model="item.taxDivisor" placeholder="税率除数" :readonly="readonly"
size="small" />
</div>
<div class="table-cell">
<div class="table-cell edit-cell">
<el-input v-model="item.noTaxPrice" placeholder="无税单价" :readonly="readonly"
size="small" />
</div>
<div class="table-cell">
<div class="table-cell edit-cell">
<el-input v-model="item.taxTotal" placeholder="含税总额" :readonly="readonly" size="small" />
</div>
<div class="table-cell">
<div class="table-cell edit-cell">
<el-input v-model="item.noTaxTotal" placeholder="无税总额" :readonly="readonly"
size="small" />
</div>
<div class="table-cell">
<div class="table-cell edit-cell">
<el-input v-model="item.taxAmount" placeholder="税额" :readonly="readonly" size="small" />
</div>
<div class="table-cell">
<div class="table-cell edit-cell">
<el-input v-model="item.remark" placeholder="请输入备注" :readonly="readonly" size="small" />
</div>
</div>
@@ -397,6 +397,10 @@ export default {
align-items: center;
}
.edit-cell {
padding: 0px;
}
.table-cell:last-child {
border-right: none;
}

View File

@@ -66,7 +66,7 @@
<p>从左侧选择一条采购合同进行审核</p>
</div>
<div v-else class="pa-view">
<div v-else class="pa-view" v-loading="detailLoading">
<div class="pa-d-head">
<div>
<span class="pa-d-title">{{ current.planNo }}</span>
@@ -91,8 +91,9 @@
<div class="pa-section">采购要求<span class="pa-section-hint" v-if="current.items && current.items.length"> {{ current.items.length }} </span></div>
<el-table :data="current.items || []" border size="mini" max-height="320" class="pa-table">
<el-table-column label="#" type="index" width="44" align="center" />
<el-table-column label="规格" prop="spec" min-width="200" show-overflow-tooltip />
<el-table-column label="总重量(T)" prop="weight" min-width="110" align="right" />
<el-table-column label="规格" prop="spec" min-width="160" show-overflow-tooltip />
<el-table-column label="材质" prop="material" min-width="100" show-overflow-tooltip />
<el-table-column label="总重量(T)" prop="weight" min-width="100" align="right" />
<el-table-column label="厂商" prop="manufacturer" min-width="180" show-overflow-tooltip />
<template slot="empty"><span>无采购要求</span></template>
</el-table>
@@ -131,6 +132,7 @@ export default {
return {
loading: true,
buttonLoading: false,
detailLoading: false,
total: 0,
pendingCount: 0,
planList: [],
@@ -175,10 +177,11 @@ export default {
this.getList()
},
selectPlan(row) {
this.detailLoading = true
getPurchasePlan(row.planId).then(res => {
this.current = res.data || {}
this.auditOpinion = this.current.auditOpinion || ''
})
}).finally(() => { this.detailLoading = false })
},
doAudit(status) {
this.buttonLoading = true
@@ -192,10 +195,11 @@ export default {
this.getList(true) // 刷新列表但保留右侧当前计划
this.loadPendingCount()
// 右侧停留在刚审核的计划:状态/历史即时更新,驳回后可直接重新审核
this.detailLoading = true
getPurchasePlan(pid).then(res => {
this.current = res.data || {}
this.auditOpinion = ''
})
}).finally(() => { this.detailLoading = false })
}).finally(() => { this.buttonLoading = false })
},
auditText(s) {

View File

@@ -53,7 +53,7 @@
<p>从左侧选择一条采购合同</p>
</div>
<div v-else>
<div v-else v-loading="detailLoading">
<div class="pd-d-head">
<div>
<span class="pd-d-title">{{ current.planNo }}</span>
@@ -61,6 +61,7 @@
</div>
<div class="pd-head-act">
<el-button size="small" icon="el-icon-refresh" :loading="refreshing" @click="doRefreshArrival">刷新到货</el-button>
<el-button size="small" icon="el-icon-download" @click="downloadTemplate">下载模板</el-button>
<el-upload
:headers="upload.headers"
:action="uploadUrl"
@@ -90,19 +91,20 @@
</div>
<el-tabs v-model="activeTab" class="pd-tabs">
<el-tab-pane label="采购要求" name="items">
<el-table :data="current.items" border size="mini" max-height="420">
<el-tab-pane label="采购与到货" name="all">
<div class="pd-sec-title">采购要求</div>
<el-table :data="current.items" border size="mini" max-height="300">
<el-table-column label="#" type="index" width="44" align="center" />
<el-table-column label="规格" prop="spec" min-width="200" show-overflow-tooltip />
<el-table-column label="总重量(T)" prop="weight" min-width="110" align="right" />
<el-table-column label="厂商" prop="manufacturer" min-width="180" show-overflow-tooltip />
<el-table-column label="规格" prop="spec" min-width="160" show-overflow-tooltip />
<el-table-column label="材质" prop="material" min-width="100" show-overflow-tooltip />
<el-table-column label="总重量(T)" prop="weight" min-width="100" align="right" />
<el-table-column label="厂商" prop="manufacturer" min-width="150" show-overflow-tooltip />
<template slot="empty"><span>无采购要求</span></template>
</el-table>
</el-tab-pane>
<el-tab-pane :label="'到货明细(' + deliveryList.length + ''" name="delivery">
<div class="pd-sec-title" style="margin-top:16px">到货明细{{ deliveryList.length }}</div>
<el-table
:data="deliveryList" border stripe size="mini" max-height="420"
:data="deliveryList" border stripe size="mini" max-height="300"
v-loading="deliveryLoading" :row-class-name="rowClass"
>
<el-table-column label="日期" prop="arrivalDate" width="100" align="center" />
@@ -121,6 +123,32 @@
<template slot="empty"><span>暂无到货记录点右上角上传到货表格</span></template>
</el-table>
</el-tab-pane>
<el-tab-pane :label="'关联合同(' + (current.contractInfos ? current.contractInfos.length : 0) + ''" name="contracts">
<el-table :data="current.contractInfos" border size="mini" max-height="420" style="width:100%">
<el-table-column label="订单编号" prop="orderCode" width="150" show-overflow-tooltip fixed />
<el-table-column label="合同号" prop="contractCode" width="140" show-overflow-tooltip />
<el-table-column label="合同名称" prop="contractName" min-width="150" show-overflow-tooltip />
<el-table-column label="客户(需方)" prop="customer" width="130" show-overflow-tooltip />
<el-table-column label="供方" prop="supplier" width="120" show-overflow-tooltip />
<el-table-column label="金额" prop="orderAmount" width="110" align="right" />
<el-table-column label="销售员" prop="salesman" width="80" />
<el-table-column label="签订时间" prop="signTime" width="105" align="center" />
<el-table-column label="签订地点" prop="signLocation" width="110" show-overflow-tooltip />
<el-table-column label="交货日期" prop="deliveryDate" width="105" align="center" />
<el-table-column label="合同状态" width="90" align="center">
<template slot-scope="s">{{ contractStatusText(s.row.status) }}</template>
</el-table-column>
<el-table-column label="应付定金" prop="depositPayable" width="100" align="right" />
<el-table-column label="已付定金" prop="depositPaid" width="100" align="right" />
<el-table-column label="未结款" prop="unpaidAmount" width="100" align="right" />
<el-table-column label="产品内容" prop="productContent" min-width="180" show-overflow-tooltip />
<el-table-column label="供方电话" prop="supplierPhone" width="120" show-overflow-tooltip />
<el-table-column label="需方电话" prop="customerPhone" width="120" show-overflow-tooltip />
<el-table-column label="备注" prop="remark" min-width="150" show-overflow-tooltip />
<template slot="empty"><span>无关联合同</span></template>
</el-table>
</el-tab-pane>
</el-tabs>
</div>
</section>
@@ -136,6 +164,7 @@ import {
refreshArrival
} from '@/api/erp/purchasePlan'
import { getToken } from '@/utils/auth'
import * as XLSX from 'xlsx'
export default {
name: 'ErpPurchaseDelivery',
@@ -146,10 +175,11 @@ export default {
planList: [],
queryParams: { pageNum: 1, pageSize: 20, keyword: undefined },
current: {},
activeTab: 'items',
activeTab: 'all',
deliveryList: [],
deliveryLoading: false,
refreshing: false,
detailLoading: false,
upload: { headers: { Authorization: 'Bearer ' + getToken() } },
progressColor: '#5b8db8'
}
@@ -180,10 +210,12 @@ export default {
this.getList()
},
selectPlan(p) {
this.activeTab = 'items'
this.activeTab = 'all'
this.current = { ...p }
this.deliveryList = []
// 打开即静默按钢卷表复核一次到货状态
// 立即加载详情,不等待 WMS 刷新
this.refreshDetail()
// 后台静默按钢卷表复核一次到货状态
const planId = p.planId
refreshArrival(planId).catch(() => {}).finally(() => {
if (this.current.planId === planId) this.refreshDetail()
@@ -192,7 +224,8 @@ export default {
refreshDetail() {
const planId = this.current.planId
if (!planId) return
getPurchasePlan(planId).then(res => { this.current = { ...this.current, ...(res.data || {}) } })
this.detailLoading = true
getPurchasePlan(planId).then(res => { this.current = { ...this.current, ...(res.data || {}) } }).finally(() => { this.detailLoading = false })
this.deliveryLoading = true
listDelivery(planId).then(res => { this.deliveryList = res.data || [] }).finally(() => { this.deliveryLoading = false })
},
@@ -212,13 +245,11 @@ export default {
},
statusText(row) {
if (row.arrived === 1) return '已到货'
if (row.inTransit === 1) return '在途'
return '未到'
return '在途'
},
statusClass(row) {
if (row.arrived === 1) return 'yes'
if (row.inTransit === 1) return 'transit'
return 'no'
return 'transit'
},
handleUploadSuccess(res) {
if (res.code === 200) {
@@ -228,7 +259,7 @@ export default {
} else {
this.$modal.msgSuccess(res.msg || '导入成功')
}
this.activeTab = 'delivery'
this.activeTab = 'all'
this.refreshDetail()
this.getList(true)
} else {
@@ -240,6 +271,18 @@ export default {
},
fmt(v) {
return Number(v || 0).toLocaleString('zh-CN', { minimumFractionDigits: 0, maximumFractionDigits: 3 })
},
contractStatusText(v) {
return { 0: '草稿', 1: '生效', 2: '作废', 3: '已完成' }[v] || '—'
},
downloadTemplate() {
const headers = ['日期', '牌号', '规格', '卷号', '单卷重量', '车号', '数量', '件数', '销售', '钢厂到站']
const sampleRow = ['2026-01-01', 'Q235B', '1.0*1250*C', 'COIL0001', '20.50', '辽A12345', '60.8', '3', 'XS001', '沈阳站']
const wb = XLSX.utils.book_new()
const ws = XLSX.utils.aoa_to_sheet([headers, sampleRow])
ws['!cols'] = headers.map(h => ({ wch: h.length > 4 ? 14 : 10 }))
XLSX.utils.book_append_sheet(wb, ws, '到货模板')
XLSX.writeFile(wb, '到货导入模板.xlsx')
}
}
}
@@ -292,6 +335,10 @@ $sub: #909399;
&.wide { grid-column: span 2; }
}
.pd-tabs { padding: 0 18px 18px; }
.pd-sec-title {
font-size: 13px; font-weight: 600; color: $ink;
border-left: 3px solid $accent; padding-left: 8px; margin-bottom: 10px;
}
.pd-mtag {
font-size: 11px; line-height: 16px; padding: 0 6px; border-radius: 2px; border: 1px solid #dcdfe6; color: $sub;
&.yes { color: #3a8a4d; border-color: #b7d9bf; background: #f0f9f1; }

View File

@@ -124,10 +124,13 @@
</div>
<el-table :data="form.items" border size="mini" max-height="340">
<el-table-column label="#" type="index" width="44" align="center" />
<el-table-column label="规格" min-width="220">
<el-table-column label="规格" min-width="160">
<template slot-scope="s"><el-input v-model="s.row.spec" size="mini" placeholder="如 3.00×1230" /></template>
</el-table-column>
<el-table-column label="总重量(T)" min-width="120">
<el-table-column label="材质" min-width="120">
<template slot-scope="s"><el-input v-model="s.row.material" size="mini" placeholder="如 Q235B" /></template>
</el-table-column>
<el-table-column label="总重量(T)" min-width="110">
<template slot-scope="s"><el-input v-model="s.row.weight" size="mini" /></template>
</el-table-column>
<el-table-column label="厂商" min-width="200">
@@ -157,7 +160,7 @@
</div>
<!-- 查看 -->
<div v-else class="pp-view">
<div v-else class="pp-view" v-loading="detailLoading">
<div class="pp-d-head">
<div>
<span class="pp-d-title">{{ current.planNo }}</span>
@@ -212,12 +215,38 @@
<el-tab-pane label="采购要求" name="items">
<el-table :data="current.items" border size="mini" max-height="340">
<el-table-column label="#" type="index" width="44" align="center" />
<el-table-column label="规格" prop="spec" min-width="200" show-overflow-tooltip />
<el-table-column label="总重量(T)" prop="weight" min-width="110" align="right" />
<el-table-column label="厂商" prop="manufacturer" min-width="180" show-overflow-tooltip />
<el-table-column label="规格" prop="spec" min-width="160" show-overflow-tooltip />
<el-table-column label="材质" prop="material" min-width="100" show-overflow-tooltip />
<el-table-column label="总重量(T)" prop="weight" min-width="100" align="right" />
<el-table-column label="厂商" prop="manufacturer" min-width="150" show-overflow-tooltip />
<template slot="empty"><span>无采购要求</span></template>
</el-table>
</el-tab-pane>
<el-tab-pane :label="'关联合同(' + (current.contractInfos ? current.contractInfos.length : 0) + ''" name="contracts">
<el-table :data="current.contractInfos" border size="mini" max-height="340" style="width:100%">
<el-table-column label="订单编号" prop="orderCode" width="150" show-overflow-tooltip fixed />
<el-table-column label="合同号" prop="contractCode" width="140" show-overflow-tooltip />
<el-table-column label="合同名称" prop="contractName" min-width="150" show-overflow-tooltip />
<el-table-column label="客户(需方)" prop="customer" width="130" show-overflow-tooltip />
<el-table-column label="供方" prop="supplier" width="120" show-overflow-tooltip />
<el-table-column label="金额" prop="orderAmount" width="110" align="right" />
<el-table-column label="销售员" prop="salesman" width="80" />
<el-table-column label="签订时间" prop="signTime" width="105" align="center" />
<el-table-column label="签订地点" prop="signLocation" width="110" show-overflow-tooltip />
<el-table-column label="交货日期" prop="deliveryDate" width="105" align="center" />
<el-table-column label="合同状态" width="90" align="center">
<template slot-scope="s">{{ contractStatusText(s.row.status) }}</template>
</el-table-column>
<el-table-column label="应付定金" prop="depositPayable" width="100" align="right" />
<el-table-column label="已付定金" prop="depositPaid" width="100" align="right" />
<el-table-column label="未结款" prop="unpaidAmount" width="100" align="right" />
<el-table-column label="产品内容" prop="productContent" min-width="180" show-overflow-tooltip />
<el-table-column label="供方电话" prop="supplierPhone" width="120" show-overflow-tooltip />
<el-table-column label="需方电话" prop="customerPhone" width="120" show-overflow-tooltip />
<el-table-column label="备注" prop="remark" min-width="150" show-overflow-tooltip />
<template slot="empty"><span>无关联合同</span></template>
</el-table>
</el-tab-pane>
</el-tabs>
</div>
</section>
@@ -342,6 +371,7 @@ export default {
},
buttonLoading: false,
submitLoading: false,
detailLoading: false,
selectedContracts: [],
// 合同选择器
pickerOpen: false,
@@ -394,7 +424,8 @@ export default {
refreshDetail() {
const planId = this.current.planId
if (!planId) return
getPurchasePlan(planId).then(res => { this.current = { ...this.current, ...(res.data || {}) } })
this.detailLoading = true
getPurchasePlan(planId).then(res => { this.current = { ...this.current, ...(res.data || {}) } }).finally(() => { this.detailLoading = false })
},
// ---- 新增 / 编辑 ----
resetForm() {
@@ -406,6 +437,7 @@ export default {
this.mode = 'edit'
},
handleEdit() {
this.detailLoading = true
getPurchasePlan(this.current.planId).then(res => {
const d = res.data || {}
this.form = {
@@ -415,7 +447,7 @@ export default {
}
this.selectedContracts = []
this.mode = 'edit'
})
}).finally(() => { this.detailLoading = false })
},
cancelEdit() {
this.mode = this.planList.length ? 'view' : 'empty'
@@ -495,7 +527,7 @@ export default {
}).catch(() => cb([]))
},
blankItem() {
return { spec: '', weight: '', manufacturer: '' }
return { spec: '', material: '', weight: '', manufacturer: '' }
},
addItem() {
// 新行继承上一行的厂商(同批多为同厂),规格/重量留空
@@ -555,6 +587,9 @@ export default {
},
auditText(s) {
return { '0': '待审核', '1': '已通过', '2': '已驳回', '3': '待送审' }[s] || '—'
},
contractStatusText(v) {
return { 0: '草稿', 1: '生效', 2: '作废', 3: '已完成' }[v] || '—'
}
}
}

View File

@@ -70,9 +70,7 @@
<el-button v-if="currentRow.flowStatus === 1" size="mini" type="primary" plain icon="el-icon-s-promotion"
@click="handleSubmit" v-hasPermi="['qc:qualityReview:submit']">提交送审</el-button>
<!-- 待审批审批/驳回表单在下方展示顶部不再重复放置操作按钮 -->
<!-- 已通过执行改判 -->
<el-button v-if="currentRow.flowStatus === 3" size="mini" type="warning" plain icon="el-icon-setting"
@click="handleExecute" v-hasPermi="['qc:qualityReview:execute']">执行改判</el-button>
<!-- 已通过审批通过时已直接执行改判无需额外操作 -->
<!-- 已驳回重新提交 -->
<el-button v-if="currentRow.flowStatus === 4" size="mini" type="primary" plain icon="el-icon-s-promotion"
@click="handleReSubmit" v-hasPermi="['qc:qualityReview:submit']">重新提交</el-button>
@@ -105,9 +103,6 @@
<el-col :span="8">
<el-form-item label="传递日期">{{ currentRow.transmitDate }}</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="生产日期">{{ currentRow.prodDateRange }}</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
@@ -129,27 +124,29 @@
<el-tag size="mini" type="danger">{{ scope.row.beforeQuality }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="regradeQuality" label="改判后" width="110" align="center">
<el-table-column prop="regradeQuality" label="改判后" width="100" align="center">
<template slot-scope="scope">
<el-tag v-if="scope.row.regradeQuality" size="mini" type="warning">{{ scope.row.regradeQuality }}</el-tag>
<span v-else class="text-muted">待指定</span>
</template>
</el-table-column>
<el-table-column prop="beforeWarehouseName" label="当前库区" width="100">
<template slot-scope="scope">
<span>{{ scope.row.beforeWarehouseName || '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="targetWarehouseName" label="改判后库区" width="120">
<template slot-scope="scope">
<span>{{ scope.row.targetWarehouseName || '不移动' }}</span>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- ===== 品质部评审意见 ===== -->
<el-card class="detail-section" shadow="never">
<div slot="header"><span><i class="el-icon-edit-outline"></i> 品质部评审意见</span></div>
<div v-if="currentRow.flowStatus === 1 || currentRow.flowStatus === 4" class="section-editor">
<el-input type="textarea" :rows="3" v-model="opinionForm.deptOpinion" placeholder="请填写品质部评审意见..." />
<div style="margin-top:8px; display:flex; gap:8px;">
<el-input v-model="opinionForm.deptSign" placeholder="签字人" style="width:160px;" size="small" />
<el-date-picker v-model="opinionForm.deptSignDate" type="date" placeholder="签字日期" size="small" />
</div>
<el-button size="small" type="primary" @click="saveOpinion" style="margin-top:8px;">保存意见</el-button>
</div>
<div v-else class="section-display">
<div class="section-display">
<div class="opinion-text">{{ currentRow.deptOpinion || '暂未填写' }}</div>
<div class="opinion-footer" v-if="currentRow.deptOpinion">
<span>签字{{ currentRow.deptSign || '-' }}</span>
@@ -161,34 +158,57 @@
<!-- ===== 领导审批意见 ===== -->
<el-card class="detail-section" shadow="never">
<div slot="header"><span><i class="el-icon-s-check"></i> 领导审批意见</span></div>
<!-- flowStatus=2时显示审批表单权限由后端API控制前端不做限制 -->
<!-- flowStatus=2时显示审批表单创建时已填好内容领导仅确认/驳回 -->
<div v-if="currentRow.flowStatus === 2" class="section-editor">
<!-- 审批通过模式 -->
<el-radio-group v-model="approveAction" style="margin-bottom:12px;">
<el-radio-button label="approve">通过</el-radio-button>
<el-radio-button label="reject">驳回</el-radio-button>
</el-radio-group>
<!-- 领导意见只读展示 -->
<div class="approve-opinion-box">
<label>领导审批意见</label>
<div class="opinion-content">{{ currentRow.leaderOpinion || '暂未填写' }}</div>
<div class="opinion-footer" v-if="currentRow.leaderOpinion">
<span>签字{{ currentRow.leaderSign || '-' }}</span>
<span>日期{{ currentRow.leaderSignDate || '-' }}</span>
</div>
</div>
<template v-if="approveAction === 'approve'">
<el-input type="textarea" :rows="3" v-model="approveForm.leaderOpinion" placeholder="请输入领导审批意见..." />
<div style="margin:8px 0;">
<el-input v-model="approveForm.leaderSign" placeholder="签字人" style="width:160px;" size="small" />
<!-- 各钢卷改判后等级只读展示 -->
<div class="approve-coil-section">
<div class="approve-coil-title">各钢卷改判后质量状态</div>
<el-table :data="coilList" size="small" border max-height="300">
<el-table-column prop="currentCoilNo" label="产品卷号" width="140" />
<el-table-column prop="spec" label="规格(mm)" width="100" />
<el-table-column prop="defectDesc" label="缺陷描述" min-width="160" show-overflow-tooltip />
<el-table-column prop="beforeQuality" label="当前等级" width="80" align="center">
<template slot-scope="s"><el-tag size="mini" type="danger">{{ s.row.beforeQuality }}</el-tag></template>
</el-table-column>
<el-table-column prop="regradeQuality" label="改判后等级" width="100" align="center">
<template slot-scope="s"><el-tag size="mini" type="warning">{{ s.row.regradeQuality }}</el-tag></template>
</el-table-column>
<el-table-column prop="beforeWarehouseName" label="当前库区" width="100" />
<el-table-column prop="targetWarehouseName" label="改判后库区" width="120">
<template slot-scope="s">
<span>{{ s.row.targetWarehouseName || '不移动' }}</span>
</template>
</el-table-column>
</el-table>
</div>
<div class="approve-actions">
<el-radio-group v-model="approveAction" size="small">
<el-radio-button label="approve">通过</el-radio-button>
<el-radio-button label="reject">驳回</el-radio-button>
</el-radio-group>
<div class="approve-action-body">
<template v-if="approveAction === 'approve'">
<p class="approve-hint">确认审批通过后将直接执行改判钢卷质量状态和库区将同步更新</p>
<el-button type="success" @click="doApprove" :size="'small'" icon="el-icon-check" round>确认通过</el-button>
</template>
<template v-else>
<el-input type="textarea" :rows="2" v-model="rejectReason" placeholder="请输入驳回原因..." style="margin-bottom:8px;" />
<el-button type="danger" @click="doReject" :size="'small'" icon="el-icon-close" round>确认驳回</el-button>
</template>
</div>
<div class="approve-coil-section">
<div class="approve-coil-title">请为每个钢卷指定改判后质量状态</div>
<div v-for="coil in coilList" :key="coil.detailId" class="approve-coil-row">
<span class="approve-coil-label">{{ coil.currentCoilNo }}{{ coil.spec }}</span>
<el-select v-model="approveCoilMap[coil.detailId]" placeholder="请选择改判后等级" size="small" style="width:180px;">
<el-option v-for="item in dict.type.coil_quality_status" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
</div>
<el-button size="small" type="success" @click="doApprove">确认通过</el-button>
</template>
<template v-else>
<el-input type="textarea" :rows="3" v-model="rejectReason" placeholder="请输入驳回原因..." />
<el-button size="small" type="danger" @click="doReject" style="margin-top:8px;">确认驳回</el-button>
</template>
</div>
</div>
<div v-else class="section-display">
<div class="opinion-text">{{ currentRow.leaderOpinion || '暂无审批意见' }}</div>
@@ -224,8 +244,14 @@
</div>
<!-- 编辑弹窗 -->
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="900px" :close-on-click-modal="false" append-to-body>
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="1200px" :close-on-click-modal="false" append-to-body class="review-dialog">
<el-form ref="form" :model="editForm" :rules="rules" label-width="100px" size="small">
<!-- ===== 基本信息 ===== -->
<div class="dialog-section">
<div class="dialog-section-header">
<i class="el-icon-info"></i>
<span>基本信息</span>
</div>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="产品名称" prop="productName">
@@ -239,23 +265,20 @@
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-col :span="12">
<el-form-item label="传递人" prop="transmitUser">
<el-input v-model="editForm.transmitUser" />
<el-input v-model="editForm.transmitUser" placeholder="填写传递人姓名" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-col :span="12">
<el-form-item label="传递日期" prop="transmitDate">
<el-date-picker v-model="editForm.transmitDate" type="date" placeholder="选择日期" style="width:100%;" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="生产日期" prop="prodDateRange">
<el-input v-model="editForm.prodDateRange" placeholder="如14-15" />
</el-form-item>
</el-col>
</el-row>
</div>
<!-- ===== 钢卷选择 ===== -->
<div class="dialog-section">
<div class="dialog-section-header">
@@ -267,18 +290,46 @@
<el-table-column prop="supplierCoilNo" label="原料卷号" min-width="130" />
<el-table-column prop="spec" label="规格(mm)" width="100" />
<el-table-column prop="netWeight" label="卷重(t)" width="80" align="right" />
<el-table-column prop="defectDesc" label="缺陷描述" min-width="160">
<el-table-column prop="defectDesc" label="缺陷描述" min-width="150">
<template slot-scope="scope">
<el-input v-model="scope.row.defectDesc" size="mini" placeholder="输入缺陷描述" />
<el-input v-model="scope.row.defectDesc" size="mini" placeholder="输入缺陷" />
</template>
</el-table-column>
<el-table-column label="操作" width="60" align="center">
<el-table-column prop="beforeQuality" label="当前等级" width="80" align="center">
<template slot-scope="scope">
<el-tag size="mini" type="danger">{{ scope.row.beforeQuality }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="regradeQuality" label="改判后等级" width="120">
<template slot-scope="scope">
<el-select v-model="scope.row.regradeQuality" placeholder="请选择" size="mini" style="width:100%;">
<el-option v-for="item in dict.type.coil_quality_status" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</template>
</el-table-column>
<el-table-column prop="beforeWarehouseName" label="当前库区" width="100">
<template slot-scope="scope">
<span>{{ scope.row.beforeWarehouseName || '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="targetWarehouseId" label="改判后库区" width="140">
<template slot-scope="scope">
<el-select v-model="scope.row.targetWarehouseId" placeholder="请选择" size="mini" style="width:100%;" @change="onTargetWarehouseChange(scope.row)">
<el-option label="不移动" :value="null" />
<el-option v-for="w in warehouseOptions" v-if="w.warehouseType === 1" :key="w.warehouseId" :label="w.warehouseName" :value="w.warehouseId" />
</el-select>
</template>
</el-table-column>
<el-table-column label="操作" width="55" align="center">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-delete" @click="editForm.coilList.splice(scope.$index, 1)"></el-button>
</template>
</el-table-column>
</el-table>
<div v-if="editForm.coilList.length === 0" class="dialog-table-empty" style="cursor:pointer;" @click="showCoilSelector = true">请选择O级钢卷</div>
<div v-if="editForm.coilList.length === 0" class="dialog-table-empty" @click="showCoilSelector = true">
<i class="el-icon-circle-plus-outline"></i>
<span>点击选择O级钢卷</span>
</div>
</div>
<!-- ===== 钢卷选择器嵌套在弹窗内 ===== -->
@@ -288,31 +339,63 @@
:filters="{ qualityStatusCsv: 'O', status: 0 }"
@confirm="onDialogCoilConfirm" />
<el-divider />
<!-- ===== 品质部评审 ===== -->
<div class="dialog-section">
<div class="dialog-section-header">
<i class="el-icon-edit-outline"></i>
<span>品质部评审意见</span>
</div>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="评审意见" prop="deptOpinion">
<el-input type="textarea" :rows="3" v-model="editForm.deptOpinion" placeholder="请填写品质部评审意见" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="签字人">
<el-input v-model="editForm.deptSign" placeholder="签字人姓名" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="签字日期">
<el-date-picker v-model="editForm.deptSignDate" type="date" placeholder="选择日期" style="width:100%;" />
</el-form-item>
</el-col>
</el-row>
</div>
<!-- ===== 领导审批 ===== -->
<div class="dialog-section">
<div class="dialog-section-header">
<i class="el-icon-s-check"></i>
<span>领导审批意见</span>
</div>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="品质部意见" prop="deptOpinion">
<el-input type="textarea" :rows="3" v-model="editForm.deptOpinion" placeholder="请填写品质部评审意见" />
<el-form-item label="审批意见" prop="leaderOpinion">
<el-input type="textarea" :rows="3" v-model="editForm.leaderOpinion" placeholder="请填写领导审批意见" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-col :span="12">
<el-form-item label="签字人">
<el-input v-model="editForm.deptSign" />
<el-input v-model="editForm.leaderSign" placeholder="领导姓名" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-col :span="12">
<el-form-item label="签字日期">
<el-date-picker v-model="editForm.deptSignDate" type="date" placeholder="选择日期" style="width:100%;" />
<el-date-picker v-model="editForm.leaderSignDate" type="date" placeholder="选择日期" style="width:100%;" />
</el-form-item>
</el-col>
</el-row>
</div>
</el-form>
<span slot="footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveForm">确定</el-button>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false" icon="el-icon-close">取消</el-button>
<el-button type="primary" @click="saveForm" icon="el-icon-check">确定</el-button>
</span>
</el-dialog>
</div>
@@ -323,6 +406,7 @@ import { listQualityReview, getQualityReview, addQualityReview, updateQualityRev
submitQualityReview, approveQualityReview, rejectQualityReview, executeQualityReview,
listQualityReviewCoil, listQualityReviewLog } from '@/api/mes/qc/qualityReview'
import CoilSelector from '@/components/CoilSelector'
import { listWarehouse } from '@/api/wms/warehouse'
export default {
name: 'QualityReview',
@@ -349,13 +433,8 @@ export default {
// 钢卷选择
showCoilSelector: false,
warehouseOptions: [], // 库区下拉选项
// 品质部意见编辑独立于editForm避免串数据
opinionForm: {
deptOpinion: '',
deptSign: '',
deptSignDate: undefined
},
// 编辑弹窗
dialogVisible: false,
dialogTitle: '',
@@ -370,6 +449,9 @@ export default {
deptOpinion: '',
deptSign: '',
deptSignDate: undefined,
leaderOpinion: '',
leaderSign: '',
leaderSignDate: undefined,
coilList: []
},
rules: {
@@ -390,6 +472,11 @@ export default {
created() {
this.getList()
},
watch: {
dialogVisible(val) {
if (val) this.loadWarehouseOptions()
}
},
methods: {
// ===== 列表 =====
handleQuery() {
@@ -423,13 +510,6 @@ export default {
map[c.detailId] = c.regradeQuality || ''
})
this.approveCoilMap = map
// 初始化品质部意见编辑独立于editForm
this.opinionForm = {
deptOpinion: data.deptOpinion || '',
deptSign: data.deptSign || '',
deptSignDate: data.deptSignDate || undefined
}
this.approveForm.reviewId = reviewId
this.approveForm.leaderOpinion = ''
this.approveForm.leaderSign = ''
this.rejectReason = ''
@@ -456,6 +536,9 @@ export default {
deptOpinion: '',
deptSign: '',
deptSignDate: undefined,
leaderOpinion: '',
leaderSign: '',
leaderSignDate: undefined,
coilList: []
}
this.editing = true
@@ -473,6 +556,9 @@ export default {
deptOpinion: row.deptOpinion,
deptSign: row.deptSign,
deptSignDate: row.deptSignDate,
leaderOpinion: row.leaderOpinion || '',
leaderSign: row.leaderSign || '',
leaderSignDate: row.leaderSignDate || undefined,
coilList: this.coilList.map(c => ({ ...c }))
}
this.editing = true
@@ -501,23 +587,51 @@ export default {
// ===== 钢卷选择(弹窗内) =====
onDialogCoilConfirm(selected) {
if (!selected || selected.length === 0) return
const existingIds = new Set(this.editForm.coilList.map(c => c.coilId))
const newSelected = selected.filter(coil => !existingIds.has(coil.coilId))
const dupCount = selected.length - newSelected.length
if (dupCount > 0) {
this.$message.warning(`已跳过 ${dupCount} 个重复钢卷`)
}
if (newSelected.length === 0) return
const startIdx = this.editForm.coilList.length || 0
const newCoils = selected.map((coil, idx) => ({
const newCoils = newSelected.map((coil, idx) => ({
coilId: coil.coilId,
currentCoilNo: coil.currentCoilNo,
supplierCoilNo: coil.supplierCoilNo || coil.enterCoilNo,
spec: coil.specification, // CoilSelector返回的字段名为specification
spec: coil.specification,
netWeight: coil.netWeight,
defectDesc: '',
regradeQuality: '',
beforeWarehouseId: coil.warehouseId || null,
beforeWarehouseName: this.getWarehouseName(coil.warehouseId),
targetWarehouseId: null,
targetWarehouseName: '',
groupSeq: startIdx + idx + 1,
beforeQuality: coil.qualityStatus || 'O'
}))
this.editForm.coilList = [...(this.editForm.coilList || []), ...newCoils]
},
getWarehouseName(warehouseId) {
if (!warehouseId) return ''
const wh = this.warehouseOptions.find(w => w.warehouseId === warehouseId)
return wh ? wh.warehouseName : ''
},
handleRemoveCoil(index) {
this.coilList.splice(index, 1)
},
// ===== 库区选择 =====
loadWarehouseOptions() {
listWarehouse().then(res => {
this.warehouseOptions = res.data || []
})
},
onTargetWarehouseChange(row) {
const selected = this.warehouseOptions.find(w => w.warehouseId === row.targetWarehouseId)
row.targetWarehouseName = selected ? selected.warehouseName : ''
},
// ===== 提交送审 =====
handleSubmit() {
this.$confirm('确认提交送审?', '提示', { type: 'warning' }).then(() => {
@@ -538,26 +652,8 @@ export default {
// ===== 审批 =====
doApprove() {
if (!this.approveForm.leaderOpinion) {
this.$modal.msgError('请输入审批意见')
return
}
const coilRegradeList = this.coilList.map(c => ({
detailId: c.detailId,
regradeQuality: this.approveCoilMap[c.detailId]
}))
const invalid = coilRegradeList.some(r => !r.regradeQuality)
if (invalid) {
this.$modal.msgError('请为每个钢卷指定改判后质量状态')
return
}
this.$confirm('确认审批通过?', '提示', { type: 'success' }).then(() => {
approveQualityReview({
reviewId: this.currentRow.reviewId,
leaderOpinion: this.approveForm.leaderOpinion,
leaderSign: this.approveForm.leaderSign,
coilRegradeList
}).then(() => {
this.$confirm('确认审批通过?审批后将直接执行改判,钢卷质量状态将同步更新。', '提示', { type: 'success' }).then(() => {
approveQualityReview(this.currentRow.reviewId).then(() => {
this.$modal.msgSuccess('审批通过')
this.handleRefreshDetail()
})
@@ -605,20 +701,8 @@ export default {
hasPermi(permi) {
return this.$store.getters.permissions && this.$store.getters.permissions.includes(permi)
},
saveOpinion() {
if (!this.currentRow) return
updateQualityReview({
reviewId: this.currentRow.reviewId,
deptOpinion: this.opinionForm.deptOpinion,
deptSign: this.opinionForm.deptSign,
deptSignDate: this.opinionForm.deptSignDate
}).then(() => {
this.$modal.msgSuccess('保存成功')
this.handleRefreshDetail()
})
}
}
}
</script>
<style lang="scss">
@@ -782,6 +866,20 @@ export default {
font-size: 13px;
}
}
.approve-actions {
background: #fafafa;
border-radius: 4px;
padding: 16px;
margin-top: 8px;
}
.approve-actions .approve-action-body {
margin-top: 12px;
}
.approve-actions .approve-hint {
margin: 0 0 10px;
font-size: 13px;
color: #909399;
}
.text-muted { color: #c0c4cc; }
.timeline-opinion {
font-size: 13px;

View File

@@ -44,21 +44,33 @@
</el-radio-group>
<template v-if="approveAction === 'approve'">
<el-input type="textarea" :rows="3" v-model="approveForm.leaderOpinion" placeholder="请输入领导审批意见..." style="margin-bottom:12px;" />
<el-input v-model="approveForm.leaderSign" placeholder="签字人" style="width:160px; margin-bottom:12px;" size="small" />
<!-- 领导意见只读展示 -->
<div class="approve-opinion-box">
<label>领导审批意见</label>
<div class="opinion-content">{{ approveDialog.review.leaderOpinion || '暂未填写' }}</div>
<div class="opinion-footer" v-if="approveDialog.review.leaderOpinion">
<span>签字{{ approveDialog.review.leaderSign || '-' }}</span>
<span>日期{{ approveDialog.review.leaderSignDate || '-' }}</span>
</div>
</div>
<!-- 各钢卷改判后等级只读展示 -->
<div class="coil-regrade-section">
<div class="coil-regrade-title">请为以下钢卷指定改判后质量状态</div>
<div class="coil-regrade-title">各钢卷改判后质量状态</div>
<el-table :data="approveCoilList" size="small" border max-height="300">
<el-table-column prop="currentCoilNo" label="产品卷号" width="140" />
<el-table-column prop="spec" label="规格" width="100" />
<el-table-column prop="beforeQuality" label="当前等级" width="80">
<el-table-column prop="defectDesc" label="缺陷描述" min-width="160" show-overflow-tooltip />
<el-table-column prop="beforeQuality" label="当前等级" width="80" align="center">
<template slot-scope="s"><el-tag size="mini" type="danger">{{ s.row.beforeQuality }}</el-tag></template>
</el-table-column>
<el-table-column label="改判后状态" width="160">
<el-table-column prop="regradeQuality" label="改判后等级" width="100" align="center">
<template slot-scope="s"><el-tag size="mini" type="warning">{{ s.row.regradeQuality }}</el-tag></template>
</el-table-column>
<el-table-column prop="beforeWarehouseName" label="当前库区" width="100" />
<el-table-column prop="targetWarehouseName" label="改判后库区" width="120">
<template slot-scope="s">
<el-select v-model="approveCoilMap[s.row.detailId]" placeholder="请选择" size="small">
<el-option v-for="item in dict.type.coil_quality_status" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<span>{{ s.row.targetWarehouseName || '不移动' }}</span>
</template>
</el-table-column>
</el-table>
@@ -97,13 +109,7 @@ export default {
review: null
},
approveAction: 'approve',
approveForm: {
reviewId: undefined,
leaderOpinion: '',
leaderSign: ''
},
approveCoilList: [],
approveCoilMap: {},
approveLoading: false,
rejectReason: ''
}
@@ -126,19 +132,13 @@ export default {
handleQuickApprove(row) {
this.approveAction = 'approve'
this.approveDialog.review = row
this.approveForm = { reviewId: row.reviewId, leaderOpinion: '', leaderSign: '' }
this.rejectReason = ''
this.approveDialog.visible = true
// 加载明细
// 加载明细(只读展示)
getQualityReview(row.reviewId).then(res => {
const data = res.data
this.approveCoilList = data.coilList || []
const map = {}
;(data.coilList || []).forEach(c => {
map[c.detailId] = c.regradeQuality || ''
})
this.approveCoilMap = map
})
},
handleQuickReject(row) {
@@ -150,25 +150,8 @@ export default {
}).catch(() => {})
},
doApprove() {
if (!this.approveForm.leaderOpinion) {
this.$modal.msgError('请输入审批意见')
return
}
const coilRegradeList = this.approveCoilList.map(c => ({
detailId: c.detailId,
regradeQuality: this.approveCoilMap[c.detailId]
}))
if (coilRegradeList.some(r => !r.regradeQuality)) {
this.$modal.msgError('请为每个钢卷指定改判后质量状态')
return
}
this.approveLoading = true
approveQualityReview({
reviewId: this.approveDialog.review.reviewId,
leaderOpinion: this.approveForm.leaderOpinion,
leaderSign: this.approveForm.leaderSign,
coilRegradeList
}).then(() => {
approveQualityReview(this.approveDialog.review.reviewId).then(() => {
this.$modal.msgSuccess('审批通过')
this.approveDialog.visible = false
this.approveLoading = false
@@ -205,6 +188,13 @@ export default {
line-height: 1.6;
white-space: pre-wrap;
}
.opinion-footer {
margin-top: 8px;
display: flex;
gap: 20px;
color: #909399;
font-size: 13px;
}
}
.coil-regrade-section {
margin-top: 12px;

View File

@@ -56,7 +56,12 @@
<el-col :span="14">
<div class="chart-card">
<div class="chart-header">
<span class="chart-title"><i class="el-icon-data-line" /> 每日磨削趋势</span>
<span class="chart-title"><i class="el-icon-data-line" /> {{ trendChartTitle }}</span>
<el-radio-group v-model="trendDimension" size="mini" @change="onTrendDimensionChange" style="margin-left:12px">
<el-radio-button label="day">按天</el-radio-button>
<el-radio-button label="week">按周</el-radio-button>
<el-radio-button label="month">按月</el-radio-button>
</el-radio-group>
</div>
<div ref="trendChartRef" class="chart-container" style="height:340px"></div>
<div v-if="!dailyTrend.length" class="chart-empty">暂无数据</div>
@@ -131,6 +136,38 @@
<el-empty v-else description="暂无磨削记录" />
</div>
<!-- 月度产线汇总二维表 -->
<div class="table-card mt16">
<div class="card-header">
<span class="card-title"><i class="el-icon-s-data" /> 月度产线汇总</span>
<span class="card-subtitle" v-if="monthlyPivot.length">{{ monthlyPivotMonths.length }} 个月 · {{ monthlyPivot.length }} 条产线</span>
<el-button size="mini" type="primary" plain icon="el-icon-download" style="margin-left:8px" @click="exportMonthlyPivot" :disabled="!monthlyPivot.length">导出</el-button>
</div>
<div style="overflow-x:auto" v-if="monthlyPivot.length">
<el-table :data="monthlyPivot" size="small" border stripe style="min-width:100%"
:header-cell-style="{ textAlign: 'center', fontWeight: 600 }">
<el-table-column label="产线" prop="lineName" fixed="left" width="140" align="center" />
<el-table-column v-for="m in monthlyPivotMonths" :key="m" :label="m" align="center">
<el-table-column label="磨削次数" align="center" min-width="80">
<template slot-scope="{row}">{{ row[m + '_count'] || 0 }}</template>
</el-table-column>
<el-table-column label="磨削量(mm)" align="right" min-width="110">
<template slot-scope="{row}">{{ (row[m + '_amount'] || 0).toFixed(2) }}</template>
</el-table-column>
</el-table-column>
<el-table-column label="合计" align="center">
<el-table-column label="磨削次数" align="center" min-width="80">
<template slot-scope="{row}">{{ row.totalCount }}</template>
</el-table-column>
<el-table-column label="磨削量(mm)" align="right" min-width="110">
<template slot-scope="{row}">{{ row.totalAmount.toFixed(2) }}</template>
</el-table-column>
</el-table-column>
</el-table>
</div>
<el-empty v-else description="暂无磨削记录" />
</div>
</div>
</template>
@@ -139,12 +176,19 @@ import * as echarts from 'echarts'
import * as XLSX from 'xlsx'
import { getRollStats, listRollInfo } from '@/api/mes/roll/rollInfo'
import { listRollGrindAll } from '@/api/mes/roll/rollGrind'
import { mapGetters } from 'vuex'
import rollLineMixin from '../rollLineMixin'
export default {
name: 'RollReport',
mixins: [rollLineMixin],
dicts: ['mes_roll_operator'],
computed: {
...mapGetters(['productionLines']),
trendChartTitle() {
return { day: '每日磨削趋势', week: '每周磨削趋势', month: '每月磨削趋势' }[this.trendDimension] || '磨削趋势'
}
},
data() {
const now = new Date()
const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
@@ -161,7 +205,10 @@ export default {
trendChart: null,
pieChart: null,
_beginTime: fmt(sevenDaysAgo),
_endTime: fmt(now)
_endTime: fmt(now),
monthlyPivot: [],
monthlyPivotMonths: [],
trendDimension: 'day'
}
},
watch: {
@@ -197,8 +244,8 @@ export default {
this.stats = statsRes.data || {}
}
// 2. 获取全部轧辊(用于辊型映射)
const rollRes = await listRollInfo({ pageNum: 1, pageSize: 99999, lineId: this.lineId || undefined })
// 2. 获取全部轧辊(用于辊型映射和产线关联
const rollRes = await listRollInfo({ pageNum: 1, pageSize: 99999 })
const rolls = rollRes.rows || []
const rollMap = {}
rolls.forEach(r => { rollMap[r.rollId] = r })
@@ -211,10 +258,20 @@ export default {
const allRecords = grindRes.data || []
// 4. 汇总计算
this.autoDetectTrendDimension()
this.computeSummary(allRecords, rollMap)
this.computeDailyTrend(allRecords)
this.computeTrendData(allRecords)
this.computeRollTypeDist(allRecords, rollMap)
this.computeOperatorStats(allRecords, rollMap)
// 5. 月度产线汇总(跨全部产线,不受当前 lineId 限制)
await this.$store.dispatch('productionLine/getProductionLines')
const allParams = {}
if (this._beginTime) allParams.beginTime = this._beginTime
if (this._endTime) allParams.endTime = this._endTime + ' 23:59:59'
const allGrindRes = await listRollGrindAll(allParams)
this.computeMonthlyPivot(allGrindRes.data || [], rollMap)
this.updateCharts()
} catch (e) {
console.error('加载报表数据失败', e)
@@ -249,6 +306,56 @@ export default {
})
this.dailyTrend = Object.values(dateMap).sort((a, b) => a.grindDate.localeCompare(b.grindDate))
},
computeWeeklyTrend(records) {
const weekMap = {}
const pad = n => String(n).padStart(2, '0')
records.forEach(r => {
if (!r.grindTime) return
const d = new Date(r.grindTime)
const dayOfWeek = d.getDay() || 7
const monday = new Date(d)
monday.setDate(d.getDate() - dayOfWeek + 1)
const sunday = new Date(monday)
sunday.setDate(monday.getDate() + 6)
const key = `${monday.getFullYear()}-${pad(monday.getMonth() + 1)}-${pad(monday.getDate())}`
const label = `${pad(monday.getMonth() + 1)}-${pad(monday.getDate())}~${pad(sunday.getMonth() + 1)}-${pad(sunday.getDate())}`
if (!weekMap[key]) weekMap[key] = { grindDate: label, grindCount: 0, totalGrindAmount: 0, _sort: key }
weekMap[key].grindCount++
weekMap[key].totalGrindAmount += Number(r.grindAmount || 0)
})
this.dailyTrend = Object.values(weekMap).sort((a, b) => a._sort.localeCompare(b._sort))
},
computeMonthlyTrend(records) {
const monthMap = {}
records.forEach(r => {
if (!r.grindTime) return
const m = r.grindTime.substring(0, 7)
if (!monthMap[m]) monthMap[m] = { grindDate: m, grindCount: 0, totalGrindAmount: 0 }
monthMap[m].grindCount++
monthMap[m].totalGrindAmount += Number(r.grindAmount || 0)
})
this.dailyTrend = Object.values(monthMap).sort((a, b) => a.grindDate.localeCompare(b.grindDate))
},
computeTrendData(records) {
this._cachedRecords = records
if (this.trendDimension === 'week') this.computeWeeklyTrend(records)
else if (this.trendDimension === 'month') this.computeMonthlyTrend(records)
else this.computeDailyTrend(records)
},
autoDetectTrendDimension() {
const [b, e] = this.dateRange
if (!b || !e) return
const days = (new Date(e) - new Date(b)) / (24 * 60 * 60 * 1000)
if (days <= 31) this.trendDimension = 'day'
else if (days <= 90) this.trendDimension = 'week'
else this.trendDimension = 'month'
},
onTrendDimensionChange() {
if (this._cachedRecords) {
this.computeTrendData(this._cachedRecords)
this.updateTrendChart()
}
},
computeRollTypeDist(records, rollMap) {
const typeMap = {}
records.forEach(r => {
@@ -420,6 +527,79 @@ export default {
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
},
computeMonthlyPivot(records, rollMap) {
const lineMonthMap = {}
const monthSet = new Set()
records.forEach(r => {
if (!r.grindTime) return
const month = r.grindTime.substring(0, 7)
const roll = rollMap[r.rollId]
const lid = Number(roll?.lineId) || 0
if (!lineMonthMap[lid]) {
const line = this.productionLines.find(l => Number(l.lineId) === lid)
lineMonthMap[lid] = { lineId: lid, lineName: line ? line.lineName : (r.lineName || '未知产线'), months: {} }
}
if (!lineMonthMap[lid].months[month]) {
lineMonthMap[lid].months[month] = { grindCount: 0, totalGrindAmount: 0 }
}
lineMonthMap[lid].months[month].grindCount++
lineMonthMap[lid].months[month].totalGrindAmount += Number(r.grindAmount || 0)
monthSet.add(month)
})
const months = [...monthSet].sort()
const rows = this.productionLines.map(line => {
const ld = lineMonthMap[line.lineId]
const row = { lineId: line.lineId, lineName: line.lineName, totalCount: 0, totalAmount: 0 }
months.forEach(m => {
if (ld && ld.months[m]) {
row[m + '_count'] = ld.months[m].grindCount
row[m + '_amount'] = Number(ld.months[m].totalGrindAmount.toFixed(2))
row.totalCount += ld.months[m].grindCount
row.totalAmount += ld.months[m].totalGrindAmount
} else {
row[m + '_count'] = 0
row[m + '_amount'] = 0
}
})
row.totalAmount = Number(row.totalAmount.toFixed(2))
return row
})
this.monthlyPivotMonths = months
this.monthlyPivot = rows
},
exportMonthlyPivot() {
const months = this.monthlyPivotMonths
const header1 = ['产线']
const header2 = ['']
months.forEach(m => { header1.push(m, ''); header2.push('磨削次数', '磨削量(mm)') })
header1.push('合计', '')
header2.push('磨削次数', '磨削量(mm)')
const rows = [header1, header2]
this.monthlyPivot.forEach(row => {
const r = [row.lineName]
months.forEach(m => { r.push(row[m + '_count'] || 0, (row[m + '_amount'] || 0).toFixed(2)) })
r.push(row.totalCount, row.totalAmount.toFixed(2))
rows.push(r)
})
const ws = XLSX.utils.aoa_to_sheet(rows)
// merge header row 1 for month groups
ws['!merges'] = []
let col = 1
months.forEach(() => { ws['!merges'].push({ s: { r: 0, c: col }, e: { r: 0, c: col + 1 } }); col += 2 })
ws['!merges'].push({ s: { r: 0, c: col }, e: { r: 0, c: col + 1 } })
const wb = XLSX.utils.book_new()
XLSX.utils.book_append_sheet(wb, ws, '月度产线汇总')
const buf = XLSX.write(wb, { bookType: 'xlsx', type: 'array' })
const blob = new Blob([buf], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `月度产线汇总_${this._beginTime || ''}_${this._endTime ? this._endTime.substring(0, 10) : ''}.xlsx`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
}
}
}
@@ -431,6 +611,7 @@ export default {
min-height: 100%;
}
.mb16 { margin-bottom: 16px; }
.mt16 { margin-top: 16px; }
/* 筛选栏 */
.filter-panel {

View File

@@ -26,7 +26,7 @@
<template slot-scope="{ row }">{{ row.CREW || row.crew || '—' }}</template>
</el-table-column>
<el-table-column label="钢种" min-width="80" show-overflow-tooltip>
<template slot-scope="{ row }">{{ row.ORDER_QUALITY || row.order_quality || row.GRADE || row.grade || '—' }}</template>
<template slot-scope="{ row }">{{ row.GRADE || row.grade || '—' }}</template>
</el-table-column>
<el-table-column label="来料厚度" width="68" align="right">
<template slot-scope="{ row }">{{ row.ENTRY_THICK || row.entry_thick || '—' }}</template>

View File

@@ -1,145 +1,134 @@
<template>
<div class="app-container">
<!-- 统计卡片 -->
<el-row :gutter="16" class="stat-row" v-if="activeTab === 'all'">
<el-col :span="6">
<div class="stat-card">
<div class="stat-num">{{ stats.totalFiles }}</div>
<div class="stat-label">文件总数</div>
</div>
</el-col>
<el-col :span="6">
<div class="stat-card public">
<div class="stat-num">{{ stats.publicFiles }}</div>
<div class="stat-label">公开文件</div>
</div>
</el-col>
<el-col :span="6">
<div class="stat-card private">
<div class="stat-num">{{ stats.privateFiles }}</div>
<div class="stat-label">私有文件</div>
</div>
</el-col>
<el-col :span="6">
<div class="stat-card">
<div class="stat-num">{{ stats.myFiles }}</div>
<div class="stat-label">我上传的</div>
</div>
</el-col>
</el-row>
<!-- 左右分栏布局可拖拽调节宽度 -->
<div class="file-layout" ref="layoutContainer">
<!-- 左侧文件列表 -->
<div class="file-left" :style="{ width: leftPanelWidth }">
<!-- 搜索栏 -->
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="80px">
<el-form-item label="文件名称" prop="fileName">
<el-input
v-model="queryParams.fileName"
placeholder="请输入文件名称"
clearable
style="width: 200px"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="订单编号" prop="orderNo">
<el-input
v-model="queryParams.orderNo"
placeholder="请输入订单编号"
clearable
style="width: 200px"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="所属部门" prop="dept">
<el-input
v-model="queryParams.dept"
placeholder="请输入部门"
clearable
style="width: 200px"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="文件类型" prop="fileType">
<el-select
v-model="queryParams.fileType"
placeholder="请选择文件类型"
clearable
style="width: 200px"
>
<el-option
v-for="item in dict.type.sys_file_type"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="可见范围" prop="scopeType" v-if="activeTab !== 'share' && activeTab !== 'related'">
<el-select
v-model="queryParams.scopeType"
placeholder="请选择可见范围"
clearable
style="width: 200px"
>
<el-option label="公开" :value="1" />
<el-option label="私有" :value="2" />
</el-select>
</el-form-item>
<el-form-item label="上传时间">
<el-date-picker
v-model="dateRange"
style="width: 240px"
value-format="yyyy-MM-dd HH:mm:ss"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="['00:00:00', '23:59:59']"
></el-date-picker>
</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>
<!-- 统计卡片两行每行两个 -->
<div class="summary-stat-container">
<el-card class="summary-card" shadow="hover">
<el-row :gutter="6">
<el-col :span="12">
<div class="summary-item">
<span class="item-label">文件总数</span>
<span class="item-value">{{ stats.totalFiles }}</span>
</div>
</el-col>
<el-col :span="12">
<div class="summary-item">
<span class="item-label">公开文件</span>
<span class="item-value">{{ stats.publicFiles }}</span>
</div>
</el-col>
</el-row>
<el-row :gutter="6" style="margin-top: 4px;">
<el-col :span="12">
<div class="summary-item">
<span class="item-label">私有文件</span>
<span class="item-value">{{ stats.privateFiles }}</span>
</div>
</el-col>
<el-col :span="12">
<div class="summary-item">
<span class="item-label">我上传的</span>
<span class="item-value">{{ stats.myFiles }}</span>
</div>
</el-col>
</el-row>
</el-card>
</div>
<!-- 文件类型筛选两行 -->
<div class="file-type-tabs">
<span
v-for="opt in fileTypeOptions"
:key="opt.value"
class="type-tab"
:class="{ active: (queryParams.fileType || '') === opt.value }"
@click="handleTypeTabClick(opt.value)"
>{{ opt.label }}</span>
</div>
<!-- 操作按钮 -->
<el-row :gutter="10" class="mb8">
<el-col :span="1.5" v-if="activeTab === 'my' || activeTab === 'all'">
<!-- 操作按钮 + 名称搜索 -->
<div class="toolbar-row">
<div class="toolbar-buttons">
<el-button
type="primary"
plain
icon="el-icon-plus"
size="mini"
v-if="activeTab === 'my' || activeTab === 'all'"
@click="handleAdd"
>上传文件</el-button>
</el-col>
<el-col :span="1.5" v-if="activeTab === 'my' || activeTab === 'all'">
</div>
<el-button
type="danger"
plain
icon="el-icon-delete"
size="mini"
v-if="activeTab === 'my' || activeTab === 'all'"
:disabled="multiple"
style="margin-right: auto;"
@click="handleDelete"
>删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="el-icon-download"
<div class="toolbar-search">
<el-input
v-model="queryParams.fileName"
style="flex: 1;"
prefix-icon="el-icon-search"
placeholder="按名称搜索"
clearable
size="mini"
@click="handleExport"
>导出</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
@change="handleQuery"
@keyup.enter.native="handleQuery"
@clear="handleQuery"
></el-input>
<el-button icon="el-icon-sort" @click="toggleQuery"></el-button>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery"></el-button>
</div>
</div>
<!-- 文件列表表格 -->
<KLPTable v-loading="loading" :data="fileList" @selection-change="handleSelectionChange" highlight-current-row @row-click="handleRowClick">
<!-- 高级搜索区域 -->
<div v-show="showQuery" class="advanced-search">
<el-form :model="queryParams" inline size="mini" class="advanced-search-form">
<el-form-item label="订单编号">
<el-input v-model="queryParams.orderNo" placeholder="订单编号" clearable @change="handleQuery" style="width: 140px;"></el-input>
</el-form-item>
<el-form-item label="所属部门">
<el-input v-model="queryParams.dept" placeholder="所属部门" clearable @change="handleQuery" style="width: 140px;"></el-input>
</el-form-item>
<el-form-item label="文件类型">
<el-select v-model="queryParams.fileType" placeholder="文件类型" clearable @change="handleQuery" style="width: 120px;">
<el-option v-for="item in dict.type.sys_file_type" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="可见范围">
<el-select v-model="queryParams.scopeType" placeholder="可见范围" clearable @change="handleQuery" style="width: 120px;">
<el-option label="公开" :value="1" />
<el-option label="私有" :value="2" />
</el-select>
</el-form-item>
<el-form-item label="上传时间">
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
format="yyyy-MM-dd"
value-format="yyyy-MM-dd"
clearable
style="width: 240px;"
@change="handleQuery"
></el-date-picker>
</el-form-item>
</el-form>
</div>
<!-- 文件列表表格固定高度可滚动 -->
<div class="file-table-wrapper">
<div class="file-table-scroll">
<KLPTable v-loading="loading" :data="fileList" @selection-change="handleSelectionChange" highlight-current-row @row-click="handleRowClick">
<el-table-column type="selection" width="55" align="center" v-if="activeTab === 'my' || activeTab === 'all'" />
<el-table-column label="文件名称" align="center" prop="fileName" :show-overflow-tooltip="true">
<template slot-scope="scope">
@@ -158,6 +147,7 @@
</template>
</el-table-column>
</KLPTable>
</div>
<pagination
v-show="total>0"
@@ -166,6 +156,8 @@
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
</div>
</div>
<!-- 拖拽分隔条 -->
<div class="resize-handle" @mousedown="startResize" :class="{ dragging: isDragging }"></div>
@@ -179,19 +171,13 @@
<div class="preview-header">
<span class="preview-filename" :title="selectedFile.fileName">{{ selectedFile.fileName }}</span>
<div>
<el-button size="mini" type="text" @click="handlePreview(selectedFile)">预览</el-button>
<el-button size="mini" type="warning" plain icon="el-icon-download" @click="handleExport">导出</el-button>
<el-button size="mini" plain icon="el-icon-info" @click="handleShowInfo(selectedFile)">查看</el-button>
<el-button size="mini" type="primary" icon="el-icon-download" @click="downloadFile(selectedFile)">下载</el-button>
<el-button v-if="canEdit(selectedFile)" size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(selectedFile)">编辑</el-button>
<el-button v-if="canEdit(selectedFile)" size="mini" type="text" icon="el-icon-delete" @click="handleDelete(selectedFile)">删除</el-button>
</div>
</div>
<div class="preview-meta">
<span>{{ formatFileSize(selectedFile.fileSize) }}</span>
<dict-tag :options="dict.type.sys_file_type" :value="selectedFile.fileType" />
<span>{{ selectedFile.createBy }}</span>
<span>{{ parseTime(selectedFile.createTime) }}</span>
</div>
<div class="preview-comment">
<div class="comment-bar" @click="commentExpanded = !commentExpanded">
<span><i class="el-icon-chat-dot-round"></i> 评论 ({{ comments.length }})</span>
@@ -362,6 +348,8 @@ export default {
multiple: true,
// 显示搜索条件
showSearch: true,
// 显示高级搜索
showQuery: false,
// 总条数
total: 0,
// 文件列表
@@ -441,6 +429,11 @@ export default {
}
},
computed: {
/** 文件类型筛选选项 */
fileTypeOptions() {
const dict = this.dict.type.sys_file_type || []
return [{ value: '', label: '全部' }, ...dict]
},
/** 根据文件后缀分类,决定右侧用哪个预览组件 */
fileTypeCategory() {
if (!this.selectedFile) return null
@@ -564,16 +557,25 @@ export default {
this.form.visibleUsers = []
}
},
/** 搜索按钮 */
/** 搜索 */
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
/** 切换高级搜索 */
toggleQuery() {
this.showQuery = !this.showQuery
},
/** 文件类型筛选切换 */
handleTypeTabClick(value) {
this.queryParams.fileType = value || undefined
this.queryParams.pageNum = 1
this.getList()
},
/** 重置按钮 */
resetQuery() {
this.dateRange = []
this.selectedFile = null
this.resetForm('queryForm')
this.queryParams = {
pageNum: 1,
pageSize: 20,
@@ -583,7 +585,7 @@ export default {
fileType: undefined,
scopeType: undefined
}
this.handleQuery()
this.getList()
},
/** 多选框 */
handleSelectionChange(selection) {
@@ -821,38 +823,111 @@ export default {
</script>
<style scoped>
.stat-row {
/* 操作工具栏 */
.toolbar-row {
display: flex;
align-items: center;
margin-bottom: 8px;
}
.stat-card {
.toolbar-buttons {
display: flex;
gap: 10px;
flex-shrink: 0;
}
.toolbar-search {
display: flex;
align-items: center;
gap: 5px;
margin-left: auto;
flex: 0 1 420px;
}
/* 高级搜索区域 */
.advanced-search {
background: #fff;
border-radius: 6px;
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 10px 16px;
margin-bottom: 8px;
}
.advanced-search-form {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.advanced-search-form /deep/ .el-form-item {
margin-bottom: 0;
}
/* 文件类型筛选(两行) */
.file-type-tabs {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 8px;
}
.type-tab {
display: inline-block;
padding: 3px 10px;
font-size: 12px;
color: #606266;
border: 1px solid #dcdfe6;
border-radius: 2px;
cursor: pointer;
user-select: none;
transition: all 0.2s;
}
.type-tab:hover {
color: #5F7BA0;
border-color: #5F7BA0;
}
.type-tab.active {
color: #fff;
background: #5F7BA0;
border-color: #5F7BA0;
}
/* 统计卡片 - 销售看板风格 */
.summary-stat-container {
margin-bottom: 10px;
}
.summary-stat-container .summary-card {
border: none;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.04);
}
.summary-stat-container .summary-card /deep/ .el-card__body {
padding: 10px;
}
.summary-item {
text-align: center;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
border-left: 3px solid #409eff;
padding: 6px 4px;
border: 1px solid #ebeef5;
border-radius: 2px;
}
.stat-card.public {
border-left-color: #67c23a;
}
.stat-card.private {
border-left-color: #e6a23c;
}
.stat-num {
font-size: 22px;
font-weight: 700;
color: #303133;
line-height: 1.2;
}
.stat-label {
.item-label {
display: block;
font-size: 12px;
color: #909399;
margin-top: 2px;
margin-bottom: 2px;
}
.item-value {
display: block;
font-size: 20px;
font-weight: bold;
color: #1f2d3d;
line-height: 1.2;
}
.upload-demo {
@@ -862,12 +937,15 @@ export default {
/* 左右分栏布局 */
.file-layout {
display: flex;
align-items: flex-start;
align-items: stretch;
gap: 0;
height: calc(100vh - 100px);
}
.file-left {
flex-shrink: 0;
display: flex;
flex-direction: column;
}
.file-right {
@@ -915,12 +993,9 @@ export default {
background: #fff;
border: 1px solid #ebeef5;
border-radius: 4px;
height: calc(100vh - 280px);
min-height: 450px;
height: 100%;
display: flex;
flex-direction: column;
position: sticky;
top: 16px;
}
.preview-empty {
@@ -948,17 +1023,6 @@ export default {
margin-right: 12px;
}
.preview-meta {
padding: 8px 16px;
border-bottom: 1px solid #ebeef5;
display: flex;
gap: 12px;
align-items: center;
flex-wrap: wrap;
font-size: 12px;
color: #909399;
}
.preview-comment {
border-bottom: 1px solid #ebeef5;
}
@@ -1042,7 +1106,6 @@ export default {
}
.preview-content {
flex: 1;
overflow: auto;
display: flex;
}
@@ -1057,6 +1120,21 @@ export default {
align-items: center;
height: 400px;
}
/* 左侧表格区域撑满剩余空间,分页置底 */
.file-table-wrapper {
flex: 1;
display: flex;
flex-direction: column;
border: 1px solid #ebeef5;
border-radius: 4px;
}
.file-table-scroll {
flex: 1;
overflow-y: auto;
}
</style>
<!-- 表格选中行高亮 scoped穿透 el-table -->

View File

@@ -39,8 +39,8 @@
<div class="file-layout" ref="layoutContainer">
<!-- 左侧文件列表 -->
<div class="file-left" :style="{ width: activeTab === 'share' || activeTab === 'related' ? leftPanelWidth : '100%' }">
<!-- 搜索栏 -->
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="80px">
<!-- 搜索栏 - 我的文件 -->
<el-form v-if="activeTab === 'my'" :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="80px">
<el-form-item label="文件名称" prop="fileName">
<el-input
v-model="queryParams.fileName"
@@ -112,8 +112,61 @@
</el-form-item>
</el-form>
<!-- 操作按钮 -->
<el-row :gutter="10" class="mb8">
<!-- 搜索栏 - 共享文件 / 与我相关 -->
<div v-else class="share-search-area">
<div class="toolbar-row">
<div class="toolbar-buttons">
<el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport">导出</el-button>
</div>
<div class="toolbar-search">
<el-input
v-model="queryParams.fileName"
style="flex: 1;"
prefix-icon="el-icon-search"
placeholder="按名称搜索"
clearable
size="mini"
@change="handleQuery"
@keyup.enter.native="handleQuery"
@clear="handleQuery"
></el-input>
<el-button icon="el-icon-sort" @click="toggleQuery"></el-button>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery"></el-button>
</div>
</div>
<div v-show="showQuery" class="advanced-search">
<el-form :model="queryParams" inline size="mini" class="advanced-search-form">
<el-form-item label="订单编号">
<el-input v-model="queryParams.orderNo" placeholder="订单编号" clearable @change="handleQuery" style="width: 140px;"></el-input>
</el-form-item>
<el-form-item label="所属部门">
<el-input v-model="queryParams.dept" placeholder="所属部门" clearable @change="handleQuery" style="width: 140px;"></el-input>
</el-form-item>
<el-form-item label="文件类型">
<el-select v-model="queryParams.fileType" placeholder="文件类型" clearable @change="handleQuery" style="width: 120px;">
<el-option v-for="item in dict.type.sys_file_type" :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="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
format="yyyy-MM-dd"
value-format="yyyy-MM-dd"
clearable
style="width: 240px;"
@change="handleQuery"
></el-date-picker>
</el-form-item>
</el-form>
</div>
</div>
<!-- 操作按钮仅我的文件 tab -->
<el-row v-if="activeTab === 'my'" :gutter="10" class="mb8">
<el-col :span="1.5" v-if="activeTab === 'my' || activeTab === 'all'">
<el-button
type="primary"
@@ -145,8 +198,10 @@
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<!-- 文件列表表格 -->
<KLPTable v-loading="loading" :data="fileList" @selection-change="handleSelectionChange" :highlight-current-row="activeTab === 'share' || activeTab === 'related'" @row-click="handleRowClick">
<!-- 文件列表表格固定高度可滚动 -->
<div :class="['file-table-wrapper', { scrolling: activeTab === 'share' || activeTab === 'related' }]">
<div class="file-table-scroll">
<KLPTable v-loading="loading" :data="fileList" @selection-change="handleSelectionChange" :highlight-current-row="activeTab === 'share' || activeTab === 'related'" @row-click="handleRowClick">
<el-table-column type="selection" width="55" align="center" v-if="activeTab === 'my' || activeTab === 'all'" />
<el-table-column label="文件名称" align="center" prop="fileName" :show-overflow-tooltip="true">
<template slot-scope="scope">
@@ -209,6 +264,7 @@
</template>
</el-table-column>
</KLPTable>
</div>
<pagination
v-show="total>0"
@@ -217,6 +273,8 @@
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
</div>
</div>
<!-- 拖拽分隔条共享文件 / 与我相关 tab -->
@@ -238,12 +296,6 @@
<el-button v-if="canEdit(selectedFile)" size="mini" type="text" icon="el-icon-delete" @click="handleDelete(selectedFile)">删除</el-button>
</div>
</div>
<div class="preview-meta">
<span>{{ formatFileSize(selectedFile.fileSize) }}</span>
<dict-tag :options="dict.type.sys_file_type" :value="selectedFile.fileType" />
<span>{{ selectedFile.createBy }}</span>
<span>{{ parseTime(selectedFile.createTime) }}</span>
</div>
<div class="preview-comment">
<div class="comment-bar" @click="commentExpanded = !commentExpanded">
<span><i class="el-icon-chat-dot-round"></i> 评论 ({{ comments.length }})</span>
@@ -417,6 +469,8 @@ export default {
multiple: true,
// 显示搜索条件
showSearch: true,
// 显示高级搜索
showQuery: false,
// 总条数
total: 0,
// 文件列表
@@ -623,10 +677,15 @@ export default {
this.queryParams.pageNum = 1
this.getList()
},
/** 切换高级搜索 */
toggleQuery() {
this.showQuery = !this.showQuery
},
/** 重置按钮 */
resetQuery() {
this.dateRange = []
this.selectedFile = null
this.showQuery = false
this.resetForm('queryForm')
this.queryParams = {
pageNum: 1,
@@ -928,12 +987,15 @@ export default {
/* 左右分栏布局 */
.file-layout {
display: flex;
align-items: flex-start;
align-items: stretch;
gap: 0;
height: calc(100vh - 100px);
}
.file-left {
flex-shrink: 0;
display: flex;
flex-direction: column;
}
.file-right {
@@ -981,12 +1043,9 @@ export default {
background: #fff;
border: 1px solid #ebeef5;
border-radius: 4px;
height: calc(100vh - 280px);
min-height: 450px;
height: 100%;
display: flex;
flex-direction: column;
position: sticky;
top: 16px;
}
.preview-empty {
@@ -1014,17 +1073,6 @@ export default {
margin-right: 12px;
}
.preview-meta {
padding: 8px 16px;
border-bottom: 1px solid #ebeef5;
display: flex;
gap: 12px;
align-items: center;
flex-wrap: wrap;
font-size: 12px;
color: #909399;
}
.preview-comment {
border-bottom: 1px solid #ebeef5;
}
@@ -1108,7 +1156,6 @@ export default {
}
.preview-content {
flex: 1;
overflow: auto;
display: flex;
}
@@ -1123,6 +1170,64 @@ export default {
align-items: center;
height: 400px;
}
/* 左侧表格区域撑满剩余空间可滚动 + 边框 */
.file-table-wrapper.scrolling {
flex: 1;
display: flex;
flex-direction: column;
border: 1px solid #ebeef5;
border-radius: 4px;
}
.file-table-wrapper.scrolling .file-table-scroll {
flex: 1;
overflow-y: auto;
}
/* 搜索栏(共享文件 / 与我相关 tab */
.share-search-area {
margin-bottom: 8px;
padding: 6px 0;
}
.toolbar-row {
display: flex;
align-items: center;
margin-bottom: 8px;
}
.toolbar-buttons {
display: flex;
gap: 10px;
flex-shrink: 0;
}
.toolbar-search {
display: flex;
align-items: center;
gap: 5px;
margin-left: auto;
flex: 0 1 420px;
}
.advanced-search {
background: #fff;
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 10px 16px;
margin-bottom: 8px;
}
.advanced-search-form {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.advanced-search-form /deep/ .el-form-item {
margin-bottom: 0;
}
</style>
<!-- 表格选中行高亮 scoped穿透 el-table -->

View File

@@ -1,7 +1,7 @@
<template>
<div class="app-container acid-do-container">
<div class="search-section">
<h3 class="page-title">综合钢卷修正</h3>
<h3 class="page-title">钢卷追踪</h3>
<el-form :model="materialQueryParams" ref="materialQueryForm" size="small" :inline="true" class="search-form">
<el-form-item label="入场钢卷号" prop="enterCoilNo">
<el-input v-model="materialQueryParams.enterCoilNo" placeholder="请输入入场钢卷号" clearable

View File

@@ -197,7 +197,7 @@ export default {
total: 0,
coilList: [],
statistics: {},
overdueDays: 20,
overdueDays: 60,
queryParams: {
pageNum: 1,
pageSize: 50,

View File

@@ -163,7 +163,10 @@
</el-descriptions>
<!-- 钢卷明细表格 -->
<h4 style="margin-top: 20px">钢卷明细</h4>
<div style="display: flex; align-items: center; justify-content: space-between; margin-top: 20px">
<h4 style="margin: 0">钢卷明细</h4>
<el-button type="primary" size="small" icon="el-icon-download" @click="handleExportDetail">导出</el-button>
</div>
<el-table :data="dialogList" border stripe max-height="500" style="width: 100%">
<el-table-column prop="enterCoilNo" label="入场钢卷号" align="center" min-width="150" show-overflow-tooltip />
<el-table-column prop="currentCoilNo" label="当前钢卷号" align="center" min-width="150" show-overflow-tooltip />
@@ -518,6 +521,15 @@ export default {
.finally(() => { this.dialogLoading = false; });
},
handleExportDetail() {
if (!this.currentWarehouse) return;
this.download('wms/materialCoil/export', {
warehouseId: this.currentWarehouse.warehouseId,
dataType: 1,
status: 0,
}, `钢卷明细_${this.currentWarehouse.warehouseName}_${new Date().getTime()}.xlsx`);
},
// ============ 弹窗数据透视图(班组 × 品质) ============
getQualityGroup(qs) {
if (!qs) return '未知';

View File

@@ -211,7 +211,14 @@
<el-input v-model="exportFormData.ename" placeholder="请输入姓名(可选)" clearable />
</el-form-item>
<el-form-item label="部门" prop="deptname">
<el-input v-model="exportFormData.deptname" placeholder="请输入部门(可选)" clearable />
<el-select v-model="exportFormData.deptname" placeholder="请选择部门(可选)" clearable filterable @change="handleDeptChange" style="width: 100%">
<el-option v-for="dept in deptList" :key="dept" :label="dept" :value="dept" />
</el-select>
</el-form-item>
<el-form-item label="员工" prop="enames" v-if="exportFormData.deptname">
<el-select v-model="exportFormData.enames" placeholder="请选择员工(可选,不选则导出整个部门)" multiple clearable filterable style="width: 100%">
<el-option v-for="emp in employeeList" :key="emp.name" :label="emp.name + ' (' + emp.jobType + ')'" :value="emp.name" />
</el-select>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
@@ -224,6 +231,7 @@
<script>
import { listRecords, getRecords, delRecords, addRecords, updateRecords, syncRecords, exportAttendanceReport } from "@/api/wms/attendance";
import { getDepts, getEmployeesByDept } from "@/api/wms/employeeInfo";
export default {
name: "Records",
@@ -247,12 +255,17 @@ export default {
exportDialogVisible: false,
// 导出loading
exportLoading: false,
// 部门列表
deptList: [],
// 员工列表(根据部门动态加载)
employeeList: [],
// 导出表单
exportFormData: {
dateRange: [],
pin: "",
ename: "",
deptname: ""
deptname: "",
enames: []
},
// 导出表单校验
exportRules: {
@@ -474,6 +487,12 @@ export default {
this.exportFormData.pin = "";
this.exportFormData.ename = "";
this.exportFormData.deptname = "";
this.exportFormData.enames = [];
this.employeeList = [];
// 加载部门列表
getDepts().then(res => {
this.deptList = res.data || [];
});
this.exportDialogVisible = true;
this.$nextTick(() => {
this.$refs.exportForm && this.$refs.exportForm.clearValidate();
@@ -484,35 +503,66 @@ export default {
}
});
},
/** 部门变更时加载对应员工 */
handleDeptChange(dept) {
this.exportFormData.enames = [];
if (dept) {
getEmployeesByDept(dept).then(res => {
this.employeeList = res.data || [];
});
} else {
this.employeeList = [];
}
},
/** 执行导出考勤记录表 */
doExportReport() {
this.$refs.exportForm.validate(valid => {
if (!valid) return;
const [startDate, endDate] = this.exportFormData.dateRange;
this.exportLoading = true;
exportAttendanceReport({
async doExportReport() {
const valid = await this.$refs.exportForm.validate().catch(() => false);
if (!valid) return;
const [startDate, endDate] = this.exportFormData.dateRange;
this.exportLoading = true;
try {
let enames = undefined;
if (this.exportFormData.deptname) {
// 选了部门:通过 wms_employee_info 查员工姓名来过滤
if (this.exportFormData.enames && this.exportFormData.enames.length > 0) {
enames = this.exportFormData.enames;
} else {
const res = await getEmployeesByDept(this.exportFormData.deptname);
const allEmps = res.data || [];
enames = allEmps.map(e => e.name);
if (enames.length === 0) {
this.$modal.msgWarning('该部门下没有在职员工');
this.exportLoading = false;
return;
}
}
}
const res = await exportAttendanceReport({
startTime: startDate,
endTime: endDate,
pin: this.exportFormData.pin || undefined,
ename: this.exportFormData.ename || undefined,
deptname: this.exportFormData.deptname || undefined
}).then(res => {
const blob = new Blob([res], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `考勤记录表_${startDate}_${endDate}.xlsx`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
this.$modal.msgSuccess("导出成功");
this.exportLoading = false;
this.exportDialogVisible = false;
}).catch(() => {
this.exportLoading = false;
pin: !enames ? (this.exportFormData.pin || undefined) : undefined,
ename: !enames ? (this.exportFormData.ename || undefined) : undefined,
enames: enames
});
});
const blob = new Blob([res], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `考勤记录表_${startDate}_${endDate}.xlsx`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
this.$modal.msgSuccess("导出成功");
this.exportLoading = false;
this.exportDialogVisible = false;
} catch (e) {
this.exportLoading = false;
}
}
}
};

View File

@@ -0,0 +1,589 @@
<template>
<div class="app-container" style="height: calc(100vh - 84px); display: flex;">
<!-- 左侧工艺列表 -->
<div class="left-panel" v-loading="processLoading"
style="width: 35%; border-right: 1px solid #e4e7ed; overflow-y: auto;">
<!-- 筛选区 -->
<div class="filter-section" style="padding: 10px; border-bottom: 1px solid #e4e7ed;">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px;">
<div style="display: flex; align-items: center; gap: 4px; flex-wrap: wrap;">
<el-input v-model="queryParams.processName" placeholder="工艺名称" clearable @keyup.enter.native="handleSearch"
style="width: 160px;" />
<el-button class="aps-btn-red" icon="el-icon-search" size="mini" @click="handleSearch">筛选</el-button>
<el-button icon="el-icon-refresh" size="mini" class="aps-btn-silver" @click="resetQuery">重置</el-button>
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<el-button class="aps-btn-red" icon="el-icon-plus" size="mini" @click="handleAddProcess">新增工艺</el-button>
<span style="font-size: 12px; color: #909399;">
<span class="aps-total-count">{{ total }}</span>
</span>
</div>
</div>
</div>
<!-- 列表区域 -->
<div class="custom-list">
<div class="list-body">
<div v-for="item in processList" :key="item.processId" class="list-item"
:class="{ 'list-item-active': currentProcess && currentProcess.processId === item.processId }"
@click="handleProcessClick(item)">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<div style="font-weight: bold; font-size: 14px;">{{ item.processName }}</div>
<div style="display: flex; gap: 4px;">
<el-button type="text" icon="el-icon-edit" size="mini" @click.stop="handleUpdateProcess(item)"></el-button>
<el-button type="text" icon="el-icon-delete" size="mini" style="color: #F56C6C;" @click.stop="handleDeleteProcess(item)"></el-button>
</div>
</div>
<div style="font-size: 12px; color: #909399; margin-bottom: 4px;">
{{ item.processDesc || '暂无描述' }}
</div>
<div style="font-size: 12px; color: #909399;">
备注: {{ item.remark || '-' }}
</div>
</div>
<div v-if="processList.length === 0 && !processLoading"
style="padding: 40px; text-align: center; color: #909399;">
暂无工艺数据
</div>
</div>
</div>
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
@pagination="getProcessList" style="padding: 10px; margin-bottom: 10px !important;" />
</div>
<!-- 右侧工艺步骤区域 -->
<div class="right-panel" v-if="currentProcess && currentProcess.processId"
style="flex: 1; display: flex; flex-direction: column;" v-loading="stepLoading">
<div class="detail-panel">
<!-- 工艺基本信息卡片 -->
<div class="detail-card">
<div class="detail-card-header">
<span>工艺基本信息</span>
</div>
<div class="detail-card-body">
<div class="form-grid-2">
<div class="form-field"><label>工艺名称</label>
<div class="field-value">{{ currentProcess.processName }}</div>
</div>
<div class="form-field"><label>工艺描述</label>
<div class="field-value">{{ currentProcess.processDesc || '-' }}</div>
</div>
<div class="form-field" style="grid-column:1/3;"><label>备注</label>
<div class="field-value">{{ currentProcess.remark || '-' }}</div>
</div>
</div>
</div>
</div>
<!-- 工艺步骤卡片 -->
<div class="detail-card">
<div class="detail-card-header">
<span>工艺步骤{{ stepList.length }} </span>
<el-button type="text" icon="el-icon-plus" size="mini" style="color: white;" @click="handleAddStep">新增步骤</el-button>
</div>
<div class="detail-card-body">
<div v-if="stepList.length > 0" class="process-timeline-wrap">
<el-timeline>
<el-timeline-item
v-for="(step, index) in stepList"
:key="step.stepId"
:timestamp="'步骤 ' + step.stepOrder"
placement="top"
:type="index === stepList.length - 1 ? 'success' : 'primary'"
:hollow="index !== stepList.length - 1"
size="large"
>
<div class="timeline-content">
<div class="timeline-header">
<span class="step-name">{{ step.stepName }}</span>
<div class="step-actions">
<el-button type="text" icon="el-icon-edit" size="mini" @click="handleUpdateStep(step)"></el-button>
<el-button type="text" icon="el-icon-delete" size="mini" style="color: #F56C6C;" @click="handleDeleteStep(step)"></el-button>
</div>
</div>
<div v-if="step.remark" class="step-remark">{{ step.remark }}</div>
</div>
</el-timeline-item>
</el-timeline>
</div>
<el-empty v-else description="暂无工艺步骤" />
</div>
</div>
</div>
</div>
<div v-else style="flex: 1; display: flex; flex-direction: column;">
<el-empty description="选择工艺后查看步骤" />
</div>
<!-- 新增/编辑工艺弹窗 -->
<el-dialog :title="processTitle" :visible.sync="processOpen" width="500px" append-to-body>
<el-form ref="processForm" :model="processForm" :rules="processRules" label-width="80px">
<el-form-item label="工艺名称" prop="processName">
<el-input v-model="processForm.processName" placeholder="请输入工艺名称" />
</el-form-item>
<el-form-item label="工艺描述" prop="processDesc">
<el-input v-model="processForm.processDesc" type="textarea" :rows="3" placeholder="请输入工艺描述" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="processForm.remark" placeholder="请输入备注" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" :loading="submitLoading" @click="submitProcessForm"> </el-button>
<el-button @click="cancelProcess"> </el-button>
</div>
</el-dialog>
<!-- 新增/编辑工艺步骤弹窗 -->
<el-dialog :title="stepTitle" :visible.sync="stepOpen" width="500px" append-to-body>
<el-form ref="stepForm" :model="stepForm" :rules="stepRules" label-width="80px">
<el-form-item label="步骤顺序" prop="stepOrder">
<el-input-number v-model="stepForm.stepOrder" :min="1" :max="99" controls-position="right" />
</el-form-item>
<el-form-item label="步骤名称" prop="stepName">
<el-input v-model="stepForm.stepName" placeholder="请输入步骤名称" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="stepForm.remark" placeholder="请输入备注" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" :loading="submitLoading" @click="submitStepForm"> </el-button>
<el-button @click="cancelStep"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listProcess, getProcess, addProcess, updateProcess, delProcess } from '@/api/aps/process'
import { listProcessStep, addProcessStep, updateProcessStep, delProcessStep } from '@/api/aps/processStep'
export default {
name: 'ApsProcesses',
data() {
return {
// 工艺相关
processLoading: false,
processList: [],
total: 0,
currentProcess: null,
queryParams: {
processName: '',
pageNum: 1,
pageSize: 10
},
processTitle: '',
processOpen: false,
processForm: {},
processRules: {
processName: [
{ required: true, message: '工艺名称不能为空', trigger: 'blur' }
]
},
// 工艺步骤相关
stepLoading: false,
submitLoading: false,
stepList: [],
stepTitle: '',
stepOpen: false,
stepForm: {},
stepRules: {
stepOrder: [
{ required: true, message: '步骤顺序不能为空', trigger: 'blur' }
],
stepName: [
{ required: true, message: '步骤名称不能为空', trigger: 'blur' }
]
}
}
},
created() {
this.getProcessList()
},
methods: {
/** 查询工艺列表 */
getProcessList() {
this.processLoading = true
listProcess(this.queryParams).then(res => {
this.processList = res.rows || []
this.total = res.total || 0
}).catch(() => {
this.processList = []
this.total = 0
}).finally(() => {
this.processLoading = false
})
},
/** 搜索 */
handleSearch() {
this.queryParams.pageNum = 1
this.getProcessList()
},
/** 重置 */
resetQuery() {
this.queryParams.processName = ''
this.handleSearch()
},
/** 点击工艺 */
handleProcessClick(process) {
this.currentProcess = process
this.getStepList(process.processId)
},
/** 查询工艺步骤列表 */
getStepList(processId) {
this.stepLoading = true
listProcessStep({ processId }).then(res => {
this.stepList = res.rows || []
}).catch(() => {
this.stepList = []
}).finally(() => {
this.stepLoading = false
})
},
/** 新增工艺 */
handleAddProcess() {
this.resetProcessForm()
this.processTitle = '新增工艺'
this.processOpen = true
},
/** 修改工艺 */
handleUpdateProcess(row) {
this.resetProcessForm()
const processId = row.processId || this.currentProcess.processId
getProcess(processId).then(res => {
this.processForm = res.data
this.processTitle = '修改工艺'
this.processOpen = true
})
},
/** 提交工艺表单 */
submitProcessForm() {
this.$refs['processForm'].validate(valid => {
if (valid) {
this.submitLoading = true
if (this.processForm.processId != undefined) {
updateProcess(this.processForm).then(response => {
this.$modal.msgSuccess('修改成功')
this.processOpen = false
this.getProcessList()
}).finally(() => {
this.submitLoading = false
})
} else {
addProcess(this.processForm).then(response => {
this.$modal.msgSuccess('新增成功')
this.processOpen = false
this.getProcessList()
}).finally(() => {
this.submitLoading = false
})
}
}
})
},
/** 删除工艺 */
handleDeleteProcess(row) {
const processIds = row.processId || this.currentProcess.processId
this.$modal.confirm('是否确认删除该工艺?').then(() => {
return delProcess(processIds)
}).then(() => {
this.getProcessList()
this.$modal.msgSuccess('删除成功')
this.currentProcess = null
this.stepList = []
}).catch(() => {})
},
/** 取消工艺弹窗 */
cancelProcess() {
this.processOpen = false
this.resetProcessForm()
},
/** 重置工艺表单 */
resetProcessForm() {
this.processForm = {
processId: undefined,
processName: undefined,
processDesc: undefined,
remark: undefined
}
this.resetForm('processForm')
},
/** 新增步骤 */
handleAddStep() {
this.resetStepForm()
this.stepForm.processId = this.currentProcess.processId
// 自动计算下一步骤顺序
if (this.stepList.length > 0) {
const maxOrder = Math.max(...this.stepList.map(s => s.stepOrder || 0))
this.stepForm.stepOrder = maxOrder + 1
} else {
this.stepForm.stepOrder = 1
}
this.stepTitle = '新增工艺步骤'
this.stepOpen = true
},
/** 修改步骤 */
handleUpdateStep(row) {
this.resetStepForm()
this.stepForm = { ...row }
this.stepTitle = '修改工艺步骤'
this.stepOpen = true
},
/** 提交步骤表单 */
submitStepForm() {
this.$refs['stepForm'].validate(valid => {
if (valid) {
this.submitLoading = true
if (this.stepForm.stepId != undefined) {
updateProcessStep(this.stepForm).then(response => {
this.$modal.msgSuccess('修改成功')
this.stepOpen = false
this.getStepList(this.currentProcess.processId)
}).finally(() => {
this.submitLoading = false
})
} else {
addProcessStep(this.stepForm).then(response => {
this.$modal.msgSuccess('新增成功')
this.stepOpen = false
this.getStepList(this.currentProcess.processId)
}).finally(() => {
this.submitLoading = false
})
}
}
})
},
/** 删除步骤 */
handleDeleteStep(row) {
const stepIds = row.stepId
this.$modal.confirm('是否确认删除该工艺步骤?').then(() => {
return delProcessStep(stepIds)
}).then(() => {
this.getStepList(this.currentProcess.processId)
this.$modal.msgSuccess('删除成功')
}).catch(() => {})
},
/** 取消步骤弹窗 */
cancelStep() {
this.stepOpen = false
this.resetStepForm()
},
/** 重置步骤表单 */
resetStepForm() {
this.stepForm = {
stepId: undefined,
processId: undefined,
stepOrder: undefined,
stepName: undefined,
remark: undefined
}
this.resetForm('stepForm')
}
}
}
</script>
<style scoped lang="scss">
@import './scss/aps-theme.scss';
.app-container {
overflow: hidden;
padding: 0;
}
.left-panel {
height: calc(100vh - 84px);
box-sizing: border-box;
overflow-y: auto;
}
.right-panel {
height: calc(100vh - 84px);
overflow: hidden;
min-height: 0;
}
.custom-list {
border: 1px solid #e4e7ed;
border-radius: 4px;
overflow: hidden;
}
.list-item {
padding: 12px;
border-bottom: 2px solid #dddddd;
cursor: pointer;
transition: background-color 0.2s;
}
.list-item:hover {
background-color: $aps-silver-1;
}
.list-item-active {
background-color: $aps-red-1;
border-left: 3px solid $aps-red-2;
}
.aps-total-count {
color: $aps-red-2;
font-weight: bold;
}
.aps-btn-red {
@include aps-btn-red;
}
.aps-btn-silver {
@include aps-btn-silver;
}
.detail-panel {
flex: 1;
overflow-y: scroll;
padding: 16px;
display: flex;
flex-direction: column;
gap: 16px;
background: $aps-bg;
min-height: 0;
}
.detail-card {
background: $aps-white;
border: 1px solid $aps-border;
border-radius: $aps-radius;
box-shadow: $aps-shadow-sm;
}
.detail-card-header {
background: linear-gradient(to right, $aps-red-2, $aps-red-3);
color: white;
padding: 8px 14px;
font-size: 13px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: space-between;
}
.detail-card-body {
padding: 14px;
}
.form-grid-2 {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px 16px;
}
.form-field {
display: flex;
flex-direction: column;
gap: 3px;
}
.form-field label {
font-size: 11px;
color: $aps-text-muted;
font-weight: 500;
}
.form-field .field-value {
font-size: 13px;
color: $aps-text;
padding: 4px 0;
border-bottom: 1px solid $aps-silver-mid;
}
.aps-product-table {
width: 100%;
::v-deep th {
background: $aps-silver-1 !important;
color: $aps-silver-5 !important;
font-weight: 600 !important;
}
}
// ====== 工艺步骤时间线 ======
.process-timeline-wrap {
padding: 20px 10px;
::v-deep .el-timeline {
padding-left: 10px;
}
::v-deep .el-timeline-item__wrapper {
padding-left: 20px;
}
::v-deep .el-timeline-item__timestamp {
font-size: 12px;
font-weight: 600;
color: $aps-text-muted;
}
::v-deep .el-timeline-item__tail {
border-left-color: $aps-silver-3;
}
::v-deep .el-timeline-item__node--primary {
background-color: $aps-red-2;
border-color: $aps-red-2;
}
::v-deep .el-timeline-item__node--success {
background-color: #67C23A;
border-color: #67C23A;
}
}
.timeline-content {
background: $aps-white;
border: 1px solid $aps-border;
border-radius: $aps-radius;
padding: 12px 16px;
transition: box-shadow 0.2s, border-color 0.2s;
&:hover {
border-color: $aps-red-2;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
}
.timeline-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.step-name {
font-size: 14px;
font-weight: 600;
color: $aps-text;
}
.step-actions {
display: flex;
gap: 4px;
opacity: 0;
transition: opacity 0.2s;
.timeline-content:hover & {
opacity: 1;
}
}
.step-remark {
margin-top: 8px;
font-size: 12px;
color: $aps-text-muted;
padding-top: 8px;
border-top: 1px dashed $aps-silver-3;
}
</style>

View File

@@ -7,7 +7,7 @@
<div class="filter-section" style="padding: 10px; border-bottom: 1px solid #e4e7ed;">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px;">
<div style="display: flex; align-items: center; gap: 4px; flex-wrap: wrap;">
<el-input v-model="queryParams.scheduleNo" placeholder="产单号" clearable @keyup.enter.native="handleSearch"
<el-input v-model="queryParams.scheduleNo" placeholder="产单号" clearable @keyup.enter.native="handleSearch"
style="width: 130px;" size="small" />
<el-input v-model="queryParams.customerName" placeholder="客户名" clearable
@keyup.enter.native="handleSearch" style="width: 130px;" size="small" />
@@ -58,15 +58,19 @@
<div class="detail-card-header">
<span>产需单信息</span>
<div style="display:flex; gap:6px;">
<button class="header-btn" :disabled="currentReq.scheduleStatus !== 0 && currentReq.scheduleStatus !== 3" @click="handleEdit(currentReq)">编辑</button>
<button v-if="currentReq.scheduleStatus === 0 || currentReq.scheduleStatus === 3" class="header-btn" style="background:rgba(255,255,255,0.4);border-color:rgba(255,255,255,0.7);" @click="handleDispatch">{{ currentReq.scheduleStatus === 3 ? '重新下发' : '下发' }}</button>
<button class="header-btn"
:disabled="currentReq.scheduleStatus !== 0 && currentReq.scheduleStatus !== 3"
@click="handleEdit(currentReq)">编辑</button>
<button v-if="currentReq.scheduleStatus === 0 || currentReq.scheduleStatus === 3" class="header-btn"
style="background:rgba(255,255,255,0.4);border-color:rgba(255,255,255,0.7);"
@click="handleDispatch">{{ currentReq.scheduleStatus === 3 ? '重新下发' : '下发' }}</button>
</div>
</div>
<div class="detail-card-body">
<table class="req-info-table">
<tbody>
<tr>
<td class="req-td-label">产单号</td>
<td class="req-td-label">单号</td>
<td class="req-td-value">{{ currentReq.scheduleNo }}</td>
<td class="req-td-label">生产日期</td>
<td class="req-td-value">{{ currentReq.prodDate }}</td>
@@ -75,7 +79,7 @@
<td class="req-td-label">排产状态</td>
<td class="req-td-value"><span class="aps-status-tag"
:class="'status-' + (currentReq.scheduleStatus || 0)">{{ statusMap[currentReq.scheduleStatus] ||
'未知' }}</span></td>
'未知' }}</span></td>
<td class="req-td-label">业务员</td>
<td class="req-td-value">{{ currentReq.businessUser }}</td>
</tr>
@@ -139,7 +143,8 @@
</tr>
<tr v-if="currentReq.returnReason">
<td class="req-td-label" style="color:#e74c3c;">驳回原因</td>
<td class="req-td-value" colspan="3" style="color:#e74c3c;background:#fdecea;">{{ currentReq.returnReason }}</td>
<td class="req-td-value" colspan="3" style="color:#e74c3c;background:#fdecea;">{{
currentReq.returnReason }}</td>
</tr>
</tbody>
</table>
@@ -190,8 +195,10 @@
<el-table-column label="操作" width="130" align="center" fixed="right">
<template slot-scope="scope">
<span style="white-space:nowrap;">
<el-button v-if="canEdit" type="text" size="small" style="color:#409EFF;" @click="handleDetailEdit(scope.row)">编辑</el-button>
<el-button v-if="canEdit" type="text" size="small" style="color:#ff4d4f;" @click="handleDetailDelete(scope.row)">删除</el-button>
<el-button v-if="canEdit" type="text" size="small" style="color:#409EFF;"
@click="handleDetailEdit(scope.row)">编辑</el-button>
<el-button v-if="canEdit" type="text" size="small" style="color:#ff4d4f;"
@click="handleDetailDelete(scope.row)">删除</el-button>
</span>
</template>
</el-table-column>
@@ -204,13 +211,15 @@
</el-row>
<!-- 新增/编辑对话框 -->
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="700px" append-to-body
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="900px" append-to-body
:close-on-click-modal="false">
<el-form ref="reqForm" :model="reqForm" :rules="reqRules" label-width="110px" size="small">
<!-- 第一行必填项 -->
<div style="display: flex; gap: 16px;">
<div style="flex: 1; min-width: 0;">
<el-form ref="reqForm" :model="reqForm" :rules="reqRules" label-width="80px" size="small">
<!-- 第一行必填项 -->
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="产单号" prop="scheduleNo">
<el-form-item label="产单号" prop="scheduleNo">
<el-input v-model="reqForm.scheduleNo" placeholder="自动生成或手动填写" />
</el-form-item>
</el-col>
@@ -282,19 +291,42 @@
<el-form-item label="交货重量偏差" prop="weightDeviation">
<el-input v-model="reqForm.weightDeviation" />
</el-form-item>
<el-form-item label="其他技术要求" prop="otherTechReq">
<el-input v-model="reqForm.otherTechReq" type="textarea" :rows="2" />
</el-form-item>
<el-form-item label="付款情况说明" prop="paymentDesc">
<el-input v-model="reqForm.paymentDesc" type="textarea" :rows="2" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="其他技术要求" prop="otherTechReq">
<el-input v-model="reqForm.otherTechReq" type="textarea" :rows="2" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="备注" prop="remark">
<el-input v-model="reqForm.remark" type="textarea" :rows="2" />
</el-form-item>
</el-form>
</div>
<!-- 右侧历史记录 -->
<div style="width: 200px; flex-shrink: 0;" v-if="!reqForm.scheduleId && scheduleNoHistory.length > 0">
<div style="font-size: 13px; font-weight: 600; color: #2c3e50; margin-bottom: 8px; padding-left: 8px; border-left: 3px solid #ff4d4f;">
历史记录
</div>
<div style="border: 1px solid #e4e7ed; border-radius: 4px; max-height: 450px; overflow-y: auto;">
<div v-for="item in scheduleNoHistory" :key="item.scheduleNo" class="history-item"
@click="useHistoryRecord(item)"
style="padding: 8px 10px; border-bottom: 1px solid #ecf0f1; cursor: pointer; font-size: 12px; transition: background 0.15s;">
<div style="font-weight: 500; color: #2c3e50;">{{ item.scheduleNo }}</div>
<div style="color: #7f8c8d; margin-top: 2px;">{{ item.customerName || '-' }}</div>
<div style="color: #b0b0b0; font-size: 11px;">{{ item.prodDate || '-' }}</div>
</div>
</div>
</div>
</div>
<div slot="footer" class="dialog-footer">
<el-button :loading="btnLoading" type="danger" @click="submitForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
@@ -376,7 +408,8 @@
<el-input v-model="detailForm.material" placeholder="请输入材质" />
</el-form-item>
<el-form-item label="排产吨数" prop="scheduleWeight">
<el-input-number v-model="detailForm.scheduleWeight" :min="0" :precision="3" :controls="false" style="width:100%" />
<el-input-number v-model="detailForm.scheduleWeight" :min="0" :precision="3" :controls="false"
style="width:100%" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="detailForm.remark" type="textarea" :rows="2" placeholder="可选" />
@@ -458,9 +491,10 @@ export default {
dialogVisible: false,
dialogTitle: '新增产需单',
btnLoading: false,
scheduleNoHistory: [],
reqForm: this.getEmptyForm(),
reqRules: {
scheduleNo: [{ required: true, message: '产单号不能为空', trigger: 'blur' }],
scheduleNo: [{ required: true, message: '产单号不能为空', trigger: 'blur' }],
prodDate: [{ required: true, message: '生产日期不能为空', trigger: 'change' }]
}
}
@@ -564,6 +598,17 @@ export default {
handleAdd() {
this.resetForm()
this.reqForm.scheduleNo = this.generateScheduleNo()
this.reqForm.prodDate = this.getTodayStr()
const lastForm = this.getLastFormData()
if (lastForm) {
Object.keys(lastForm).forEach(key => {
if (key !== 'scheduleId' && key !== 'scheduleNo' && key !== 'prodDate' && lastForm[key] !== undefined && lastForm[key] !== '' && lastForm[key] !== null) {
this.reqForm[key] = lastForm[key]
}
})
}
this.scheduleNoHistory = this.getScheduleNoHistory()
this.dialogTitle = '新增产需单'
this.dialogVisible = true
this.$nextTick(() => {
@@ -586,9 +631,14 @@ export default {
this.$refs.reqForm.validate(valid => {
if (!valid) return
this.btnLoading = true
const action = this.reqForm.scheduleId ? updateRequirement : addRequirement
const isNew = !this.reqForm.scheduleId
const action = isNew ? addRequirement : updateRequirement
action(this.reqForm).then(() => {
this.$modal.msgSuccess(this.reqForm.scheduleId ? '修改成功' : '新增成功')
this.$modal.msgSuccess(isNew ? '新增成功' : '修改成功')
if (isNew) {
this.saveHistoryRecord(this.reqForm)
this.saveLastFormData(this.reqForm)
}
this.dialogVisible = false
this.getList()
}).catch(() => {
@@ -615,6 +665,71 @@ export default {
}).catch(() => { })
},
getTodayStr() {
return new Date().toISOString().split('T')[0]
},
generateScheduleNo() {
const now = new Date()
const dateStr = now.getFullYear().toString() +
(now.getMonth() + 1).toString().padStart(2, '0') +
now.getDate().toString().padStart(2, '0')
const prefix = 'PCXD' + dateStr
const existingNums = this.reqList
.filter(item => item.scheduleNo && item.scheduleNo.startsWith(prefix))
.map(item => {
const seq = parseInt(item.scheduleNo.substring(prefix.length), 10)
return isNaN(seq) ? 0 : seq
})
const maxSeq = existingNums.length > 0 ? Math.max(...existingNums) : 0
const newSeq = (maxSeq + 1).toString().padStart(3, '0')
return prefix + newSeq
},
getScheduleNoHistory() {
try {
return JSON.parse(localStorage.getItem('aps_requirement_history') || '[]')
} catch (e) {
return []
}
},
saveHistoryRecord(formData) {
if (!formData.scheduleNo) return
const history = this.getScheduleNoHistory()
const idx = history.findIndex(item => item.scheduleNo === formData.scheduleNo)
if (idx >= 0) history.splice(idx, 1)
history.unshift({ ...formData })
delete history[0].scheduleId
if (history.length > 20) history.pop()
localStorage.setItem('aps_requirement_history', JSON.stringify(history))
},
useHistoryRecord(item) {
Object.keys(item).forEach(key => {
if (key !== 'scheduleId' && item[key] !== undefined) {
this.reqForm[key] = item[key]
}
})
this.$nextTick(() => {
this.$refs.reqForm && this.$refs.reqForm.clearValidate()
})
},
getLastFormData() {
try {
return JSON.parse(localStorage.getItem('aps_requirement_last_form') || 'null')
} catch (e) {
return null
}
},
saveLastFormData(formData) {
const data = { ...formData }
delete data.scheduleId
localStorage.setItem('aps_requirement_last_form', JSON.stringify(data))
},
openBindDialog() {
this.bindDialogVisible = true
this.selectedBindOrders = []
@@ -1223,4 +1338,8 @@ export default {
.aps-bind-dialog ::v-deep .el-dialog__body {
padding: 16px 20px 0 20px;
}
.history-item:hover {
background: #fdecea;
}
</style>

View File

@@ -15,6 +15,7 @@
<el-button size="small" class="aps-btn-red" icon="el-icon-search" @click="handleQuery">查询</el-button>
<el-tabs v-model="activeTab" size="small" style="margin:0 0 0 16px;" @tab-click="handleQuery">
<el-tab-pane label="待审核" name="pending" />
<el-tab-pane label="已接收" name="accepted" />
<el-tab-pane label="已排产" name="scheduled" />
</el-tabs>
<div v-if="summaryText" class="aps-sch-summary">
@@ -161,6 +162,104 @@
</div>
</div>
<!-- 已接收 Tab -->
<div v-loading="acceptedLoading" v-show="activeTab === 'accepted'" class="detail-card aps-sch-card">
<div class="detail-card-header">
<span>已接收产需单</span>
<span v-if="acceptedScheduleList.length > 0" style="font-weight:normal;font-size:12px;opacity:0.8;">
{{ acceptedScheduleList.length }} 个产需单
</span>
</div>
<div class="detail-card-body" style="padding:0;">
<div v-if="acceptedScheduleList.length > 0" class="aps-sch-list">
<div v-for="sch in acceptedScheduleList" :key="sch.scheduleId" class="aps-sch-item">
<div class="aps-sch-item-header">
<div class="aps-sch-item-header-left">
<span class="aps-sch-no">{{ sch.scheduleNo }}</span>
<span class="aps-status-tag status-2">已接收</span>
<span class="aps-sch-header-extra">
{{ sch.customerName }} · {{ sch.businessUser }} · {{ sch.deliveryCycle }}
</span>
</div>
</div>
<table class="req-info-table">
<tbody>
<tr>
<td class="req-td-label">排产单号</td><td class="req-td-value">{{ sch.scheduleNo }}</td>
<td class="req-td-label">生产日期</td><td class="req-td-value">{{ sch.prodDate }}</td>
</tr>
<tr>
<td class="req-td-label">业务员</td><td class="req-td-value">{{ sch.businessUser }}</td>
<td class="req-td-label">联系电话</td><td class="req-td-value">{{ sch.businessPhone }}</td>
</tr>
<tr>
<td class="req-td-label">订货单位</td><td class="req-td-value">{{ sch.customerName }}</td>
<td class="req-td-label">交货期()</td><td class="req-td-value">{{ sch.deliveryCycle }}</td>
</tr>
<tr>
<td class="req-td-label">品名</td><td class="req-td-value" colspan="3">{{ sch.productType }}</td>
</tr>
<tr>
<td class="req-td-label">产品用途</td><td class="req-td-value" colspan="3">{{ sch.usePurpose }}</td>
</tr>
<tr>
<td class="req-td-label">厚度公差</td><td class="req-td-value">{{ sch.thicknessTolerance }}</td>
<td class="req-td-label">宽度公差</td><td class="req-td-value">{{ sch.widthTolerance }}</td>
</tr>
<tr>
<td class="req-td-label">表面质量</td><td class="req-td-value">{{ sch.surfaceQuality }}</td>
<td class="req-td-label">表面处理</td><td class="req-td-value">{{ sch.surfaceTreatment }}</td>
</tr>
<tr>
<td class="req-td-label">内径尺寸</td><td class="req-td-value">{{ sch.innerDiameter }}</td>
<td class="req-td-label">外径要求</td><td class="req-td-value">{{ sch.outerDiameter }}</td>
</tr>
<tr>
<td class="req-td-label">包装要求</td><td class="req-td-value">{{ sch.packReq }}</td>
<td class="req-td-label">切边要求</td><td class="req-td-value">{{ sch.cutEdgeReq }}</td>
</tr>
<tr>
<td class="req-td-label">单件重量</td><td class="req-td-value">{{ sch.singleCoilWeight }}</td>
<td class="req-td-label">交货重量偏差</td><td class="req-td-value">{{ sch.weightDeviation }}</td>
</tr>
<tr v-if="sch.otherTechReq">
<td class="req-td-label">其他技术要求</td><td class="req-td-value" colspan="3">{{ sch.otherTechReq }}</td>
</tr>
<tr v-if="sch.paymentDesc">
<td class="req-td-label">付款情况说明</td><td class="req-td-value" colspan="3">{{ sch.paymentDesc }}</td>
</tr>
<tr v-if="sch.remark">
<td class="req-td-label">备注</td><td class="req-td-value" colspan="3">{{ sch.remark }}</td>
</tr>
</tbody>
</table>
<div v-if="(sch.detailList || []).length > 0" class="aps-sch-item-details">
<div class="aps-sch-detail-header">
<span class="aps-sch-detail-col col-spec">规格</span>
<span class="aps-sch-detail-col col-material">材质</span>
<span class="aps-sch-detail-col col-weight">排产吨数</span>
<span class="aps-sch-detail-col col-type">品名</span>
<span class="aps-sch-detail-col col-remark">备注</span>
</div>
<div v-for="(d, di) in sch.detailList" :key="di" class="aps-sch-detail-row" @click="handleDetailClick(sch, d)">
<span class="aps-sch-detail-col col-spec">{{ d.spec }}</span>
<span class="aps-sch-detail-col col-material">{{ d.material }}</span>
<span class="aps-sch-detail-col col-weight">{{ d.scheduleWeight }}</span>
<span class="aps-sch-detail-col col-type">{{ sch.productType || d.productType || '' }}</span>
<span class="aps-sch-detail-col col-remark">{{ d.remark }}</span>
</div>
</div>
<div v-else class="aps-sch-item-empty">暂无明细</div>
</div>
</div>
<div v-else-if="!acceptedLoading" style="padding:40px;text-align:center;color:#909399;">
{{ hasQueried ? '该日期暂无已接收产需单' : '请选择日期查询' }}
</div>
</div>
</div>
<!-- 已排产 Tab -->
<div v-loading="schLoading" v-show="activeTab === 'scheduled'" class="detail-card aps-sch-card">
<div class="detail-card-header">
@@ -169,85 +268,73 @@
<span v-if="scheduledItemList.length > 0" style="font-weight:normal;font-size:12px;opacity:0.8;">
{{ scheduledItemList.length }}
</span>
<span v-if="selectedItems.length >= 2" style="margin-left: auto;">
<el-button
size="mini"
type="warning"
icon="el-icon-link"
@click.stop="handleMergePrepare"
>
合并选中项 ({{ selectedItems.length }})
</el-button>
</span>
<span v-else style="margin-left: auto;">
<el-button
size="mini"
type="warning"
icon="el-icon-link"
disabled
>
合并选中项
</el-button>
</span>
</div>
</div>
<div element-loading-text="正在加载已排产数据..." class="detail-card-body" style="padding:0;">
<el-table
v-if="scheduledItemList.length > 0"
ref="scheduledTable"
:data="scheduledItemList"
border
size="small"
class="aps-table"
:row-class-name="getItemRowClassName"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="45" align="center" />
<el-table-column label="排产单号" prop="scheduleNo" min-width="140" show-overflow-tooltip />
<el-table-column label="生产日期" prop="prodDate" width="110" align="center" show-overflow-tooltip />
<el-table-column label="工序类型" prop="actionId" width="100" align="center" show-overflow-tooltip>
<template slot-scope="scope">
{{ getActionIdName(scope.row.actionId) }}
</template>
</el-table-column>
<el-table-column label="排产状态" prop="scheduleStatus" width="90" align="center">
<template slot-scope="scope">
<span class="aps-status-tag" :class="'status-' + (scope.row.scheduleStatus || 1)">{{ statusMap[scope.row.scheduleStatus] || '未知' }}</span>
</template>
</el-table-column>
<el-table-column label="规格" prop="spec" min-width="120" show-overflow-tooltip />
<el-table-column label="材质" prop="material" width="90" align="center" show-overflow-tooltip />
<el-table-column label="排产吨数" prop="scheduleWeight" width="100" align="right" show-overflow-tooltip />
<el-table-column label="品名项" prop="productItem" min-width="100" show-overflow-tooltip />
<el-table-column label="品名" prop="productType" min-width="90" align="center" show-overflow-tooltip />
<el-table-column label="订货单位" prop="customerName" min-width="140" show-overflow-tooltip />
<el-table-column label="业务员" prop="businessUser" width="90" align="center" show-overflow-tooltip />
<el-table-column label="联系电话" prop="businessPhone" width="110" align="center" show-overflow-tooltip />
<el-table-column label="交货期(天)" prop="deliveryCycle" width="100" align="center" show-overflow-tooltip />
<el-table-column label="产品用途" prop="usePurpose" min-width="120" show-overflow-tooltip />
<el-table-column label="厚度公差" prop="thicknessTolerance" min-width="100" show-overflow-tooltip />
<el-table-column label="宽度公差" prop="widthTolerance" min-width="100" show-overflow-tooltip />
<el-table-column label="表面质量" prop="surfaceQuality" min-width="100" show-overflow-tooltip />
<el-table-column label="表面处理" prop="surfaceTreatment" min-width="100" show-overflow-tooltip />
<el-table-column label="内径尺寸" prop="innerDiameter" width="90" align="center" show-overflow-tooltip />
<el-table-column label="外径要求" prop="outerDiameter" width="90" align="center" show-overflow-tooltip />
<el-table-column label="包装要求" prop="packReq" min-width="100" show-overflow-tooltip />
<el-table-column label="切边要求" prop="cutEdgeReq" min-width="100" show-overflow-tooltip />
<el-table-column label="单件重量" prop="singleCoilWeight" width="90" align="center" show-overflow-tooltip />
<el-table-column label="交货重量偏差" prop="weightDeviation" min-width="110" show-overflow-tooltip />
<el-table-column label="其他技术要求" prop="otherTechReq" min-width="130" show-overflow-tooltip />
<el-table-column label="付款情况说明" prop="paymentDesc" min-width="130" show-overflow-tooltip />
<el-table-column label="单行排产备注" prop="rowRemark" min-width="100" show-overflow-tooltip />
<el-table-column label="备注" prop="remark" min-width="100" show-overflow-tooltip />
<el-table-column label="操作" width="150" align="center" fixed="right">
<template slot-scope="scope">
<div style="display:flex; justify-content:center; gap:0;">
<el-button type="text" size="small" style="color:#409EFF;padding:0 6px;" @click="handleEditScheduled(scope.row)">编辑</el-button>
<el-button type="text" size="small" style="color:#ff4d4f;padding:0 6px;" @click="handleDeleteScheduled(scope.row)">删除</el-button>
</div>
</template>
</el-table-column>
</el-table>
<template v-if="scheduledItemList.length > 0">
<!-- 步骤工序 Tab -->
<div class="aps-step-tabs">
<span
v-for="tab in scheduledStepTabs"
:key="tab"
class="aps-step-tab"
:class="{ 'aps-step-tab-active': scheduledStepTab === tab }"
@click="scheduledStepTab = tab"
>
{{ tab }}
<span class="aps-step-tab-count">{{ scheduledItemList.filter(i => normalizeStepName(i.stepName) === tab).length }}</span>
</span>
</div>
<!-- 表格 -->
<el-table
ref="scheduledTable"
:data="currentStepItems"
border
size="small"
class="aps-table"
>
<el-table-column label="排产单号" prop="scheduleNo" min-width="140" show-overflow-tooltip />
<el-table-column label="生产日期" prop="prodDate" width="110" align="center" show-overflow-tooltip />
<el-table-column label="工序步骤" prop="stepName" width="120" align="center" show-overflow-tooltip />
<el-table-column label="排产状态" prop="scheduleStatus" width="90" align="center">
<template slot-scope="scope">
<span class="aps-status-tag" :class="'status-' + (scope.row.scheduleStatus || 1)">{{ statusMap[scope.row.scheduleStatus] || '未知' }}</span>
</template>
</el-table-column>
<el-table-column label="规格" prop="spec" min-width="120" show-overflow-tooltip />
<el-table-column label="材质" prop="material" width="90" align="center" show-overflow-tooltip />
<el-table-column label="排产吨数" prop="scheduleWeight" width="100" align="right" show-overflow-tooltip />
<el-table-column label="品名项" prop="productItem" min-width="100" show-overflow-tooltip />
<el-table-column label="品名" prop="productType" min-width="90" align="center" show-overflow-tooltip />
<el-table-column label="订货单位" prop="customerName" min-width="140" show-overflow-tooltip />
<el-table-column label="业务员" prop="businessUser" width="90" align="center" show-overflow-tooltip />
<el-table-column label="联系电话" prop="businessPhone" width="110" align="center" show-overflow-tooltip />
<el-table-column label="交货期(天)" prop="deliveryCycle" width="100" align="center" show-overflow-tooltip />
<el-table-column label="产品用途" prop="usePurpose" min-width="120" show-overflow-tooltip />
<el-table-column label="厚度公差" prop="thicknessTolerance" min-width="100" show-overflow-tooltip />
<el-table-column label="宽度公差" prop="widthTolerance" min-width="100" show-overflow-tooltip />
<el-table-column label="表面质量" prop="surfaceQuality" min-width="100" show-overflow-tooltip />
<el-table-column label="表面处理" prop="surfaceTreatment" min-width="100" show-overflow-tooltip />
<el-table-column label="内径尺寸" prop="innerDiameter" width="90" align="center" show-overflow-tooltip />
<el-table-column label="外径要求" prop="outerDiameter" width="90" align="center" show-overflow-tooltip />
<el-table-column label="包装要求" prop="packReq" min-width="100" show-overflow-tooltip />
<el-table-column label="切边要求" prop="cutEdgeReq" min-width="100" show-overflow-tooltip />
<el-table-column label="单件重量" prop="singleCoilWeight" width="90" align="center" show-overflow-tooltip />
<el-table-column label="交货重量偏差" prop="weightDeviation" min-width="110" show-overflow-tooltip />
<el-table-column label="其他技术要求" prop="otherTechReq" min-width="130" show-overflow-tooltip />
<el-table-column label="付款情况说明" prop="paymentDesc" min-width="130" show-overflow-tooltip />
<el-table-column label="单行排产备注" prop="rowRemark" min-width="100" show-overflow-tooltip />
<el-table-column label="备注" prop="remark" min-width="100" show-overflow-tooltip />
<el-table-column label="操作" width="150" align="center" fixed="right">
<template slot-scope="scope">
<div style="display:flex; justify-content:center; gap:0;">
<el-button type="text" size="small" style="color:#409EFF;padding:0 6px;" @click="handleEditScheduled(scope.row)">编辑</el-button>
<el-button type="text" size="small" style="color:#ff4d4f;padding:0 6px;" @click="handleDeleteScheduled(scope.row)">删除</el-button>
</div>
</template>
</el-table-column>
</el-table>
</template>
<div v-else-if="!schLoading" style="padding:40px;text-align:center;color:#909399;">
{{ hasQueried ? '该日期暂无已排产数据' : '请选择日期查询' }}
</div>
@@ -493,149 +580,48 @@
</div>
</el-dialog>
<!-- 合并排产明细对话框 -->
<!-- 接收产需单弹窗 - 配置工序 -->
<el-dialog
title="合并排产明细"
:visible.sync="mergeDialogVisible"
width="850px"
title="接收产需单 - 配置工序"
:visible.sync="receiveDialogVisible"
width="900px"
append-to-body
:close-on-click-modal="false"
>
<div style="margin-bottom:12px;font-size:13px;color:#e67e22;">
选中 {{ mergeForm.itemCount }} 条明细请选择一个作为合并模板下方字段将自动填充您可修改后确认合并
请为每条明细配置工序所有明细配置完成后才能接收
</div>
<!-- 选择模板 -->
<el-table :data="mergeSourceRows" border size="small" style="margin-bottom:12px;" @row-click="pickMergeTemplate">
<el-table-column width="50" align="center">
<el-table :data="receiveDetailList" border size="small" class="aps-table" max-height="400">
<el-table-column label="规格" prop="spec" min-width="120" show-overflow-tooltip />
<el-table-column label="材质" prop="material" width="90" align="center" show-overflow-tooltip />
<el-table-column label="排产吨数" prop="scheduleWeight" width="100" align="right" />
<el-table-column label="品名" prop="productType" min-width="90" align="center" show-overflow-tooltip />
<el-table-column label="配置工序" min-width="200">
<template slot-scope="scope">
<el-radio v-model="mergeTemplateIndex" :label="scope.$index" @click.native.stop />
<ProcessSelect
v-model="scope.row.processId"
placeholder="请选择工序"
@change="(val, data) => handleReceiveProcessChange(scope.row, val, data)"
/>
</template>
</el-table-column>
<el-table-column label="工序步骤" min-width="300">
<template slot-scope="scope">
<div v-if="scope.row.stepList && scope.row.stepList.length > 0" class="receive-steps-flow">
<template v-for="(step, index) in scope.row.stepList">
<span :key="step.stepId" class="receive-step-tag">{{ step.stepName }}</span>
<span v-if="index < scope.row.stepList.length - 1" :key="'arrow-' + index" class="receive-step-arrow"></span>
</template>
</div>
<span v-else style="color:#909399;font-size:12px;"></span>
</template>
</el-table-column>
<el-table-column label="排产单号" prop="scheduleNo" min-width="130" show-overflow-tooltip />
<el-table-column label="规格" prop="spec" min-width="100" show-overflow-tooltip />
<el-table-column label="材质" prop="material" width="80" show-overflow-tooltip />
<el-table-column label="排产吨数" prop="scheduleWeight" width="90" align="right" />
<el-table-column label="品名" prop="productType" min-width="80" show-overflow-tooltip />
<el-table-column label="订货单位" prop="customerName" min-width="120" show-overflow-tooltip />
</el-table>
<!-- 合并表单 -->
<el-form ref="mergeFormRef" :model="mergeForm" label-width="110px" size="small">
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="排产单号"><el-input v-model="mergeForm.scheduleNo" /></el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="工序类型">
<el-select v-model="mergeForm.actionId" placeholder="请选择工序类型" clearable filterable style="width:100%">
<el-option v-for="p in processOptions" :key="p.actionId" :label="p.name" :value="p.actionId" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="订货单位"><el-input v-model="mergeForm.customerName" /></el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="规格"><el-input v-model="mergeForm.spec" /></el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="材质"><el-input v-model="mergeForm.material" /></el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="排产吨数(累加)">
<el-input-number v-model="mergeForm.scheduleWeight" :min="0" :precision="3" :controls="false" style="width:100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="品名"><el-input v-model="mergeForm.productType" /></el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="品名项"><el-input v-model="mergeForm.productItem" /></el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="业务员"><el-input v-model="mergeForm.businessUser" /></el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="联系电话"><el-input v-model="mergeForm.businessPhone" /></el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="交货期(天)">
<el-input-number v-model="mergeForm.deliveryCycle" :min="0" :controls="false" style="width:100%" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="产品用途"><el-input v-model="mergeForm.usePurpose" /></el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="厚度公差"><el-input v-model="mergeForm.thicknessTolerance" /></el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="宽度公差"><el-input v-model="mergeForm.widthTolerance" /></el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="表面质量"><el-input v-model="mergeForm.surfaceQuality" /></el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="表面处理"><el-input v-model="mergeForm.surfaceTreatment" /></el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="内径尺寸"><el-input v-model="mergeForm.innerDiameter" /></el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="外径要求"><el-input v-model="mergeForm.outerDiameter" /></el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="包装要求"><el-input v-model="mergeForm.packReq" /></el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="切边要求"><el-input v-model="mergeForm.cutEdgeReq" /></el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="单件重量"><el-input v-model="mergeForm.singleCoilWeight" /></el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="交货重量偏差"><el-input v-model="mergeForm.weightDeviation" /></el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="其他技术要求"><el-input v-model="mergeForm.otherTechReq" /></el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="付款情况说明"><el-input v-model="mergeForm.paymentDesc" /></el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="备注"><el-input v-model="mergeForm.remark" /></el-form-item>
</el-col>
</el-row>
</el-form>
<div style="margin-top:8px;font-size:12px;color:#909399;">
合并后 schedule_detail_ids{{ mergeForm.scheduleDetailIds }}
</div>
<div slot="footer" class="dialog-footer">
<el-button :loading="mergeBtnLoading" type="warning" @click="confirmMerge"> </el-button>
<el-button @click="mergeDialogVisible = false"> </el-button>
<el-button :loading="receiveBtnLoading" type="primary" :disabled="!canConfirmReceive" @click="confirmReceive"> </el-button>
<el-button @click="receiveDialogVisible = false"> </el-button>
</div>
</el-dialog>
</div>
@@ -651,13 +637,14 @@ import {
listScheduleItem,
updateScheduleItem,
delScheduleItem,
receiveScheduleItem,
mergeScheduleItem
receiveScheduleItem
} from '@/api/aps/schedule'
import { PROCESSES } from '@/utils/meta'
import ProcessSelect from '@/components/KLPService/ProcessSelect/index.vue'
export default {
name: 'ApsSchedule',
components: { ProcessSelect },
data() {
const today = new Date()
const y = today.getFullYear()
@@ -676,26 +663,13 @@ export default {
statusMap: { 1: '待审核', 2: '已接收', 3: '已驳回' },
processOptions: PROCESSES,
// 已接收
acceptedScheduleList: [],
acceptedLoading: false,
// 已排产
scheduledItemList: [],
selectedItems: [],
sourceColorMap: {},
// 合并对话框
mergeDialogVisible: false,
mergeBtnLoading: false,
mergeTemplateIndex: 0,
mergeSourceRows: [],
mergeForm: {
itemCount: 0, scheduleNo: '', actionId: '', customerName: '', spec: '', material: '',
scheduleWeight: 0, productType: '', productItem: '', businessUser: '',
businessPhone: '', deliveryCycle: undefined, usePurpose: '',
thicknessTolerance: '', widthTolerance: '', surfaceQuality: '',
surfaceTreatment: '', innerDiameter: '', outerDiameter: '',
packReq: '', cutEdgeReq: '', singleCoilWeight: '',
weightDeviation: '', otherTechReq: '', paymentDesc: '',
remark: '', scheduleDetailIds: ''
},
scheduledStepTab: '',
// 驳回
rejectDialogVisible: false,
@@ -714,14 +688,37 @@ export default {
// 下钻
drillDialogVisible: false,
drillOrder: null
drillOrder: null,
// 接收产需单弹窗
receiveDialogVisible: false,
receiveBtnLoading: false,
receiveScheduleId: null,
receiveDetailList: []
}
},
watch: {
activeTab() {
this.selectedItems = []
computed: {
canConfirmReceive() {
if (!this.receiveDetailList || this.receiveDetailList.length === 0) return false
return this.receiveDetailList.every(d => d.processId)
},
// 已排产按步骤分组:去掉括号及括号内文字
scheduledStepTabs() {
const names = this.scheduledItemList
.map(item => this.normalizeStepName(item.stepName))
.filter(Boolean)
return [...new Set(names)]
},
// 当前选中的步骤tab对应的数据
currentStepItems() {
if (!this.scheduledStepTab) return this.scheduledItemList
const tab = this.scheduledStepTab
return this.scheduledItemList.filter(
item => this.normalizeStepName(item.stepName) === tab
)
}
},
watch: {},
created() {
this.handleQuery()
},
@@ -775,6 +772,8 @@ export default {
this.hasQueried = true
if (this.activeTab === 'pending') {
this.queryPending()
} else if (this.activeTab === 'accepted') {
this.queryAccepted()
} else {
this.queryScheduled()
}
@@ -808,25 +807,82 @@ export default {
})
},
// ====== 已接收 ======
queryAccepted() {
this.acceptedLoading = true
this.acceptedScheduleList = []
listRequirement({
prodDate: this.queryDate,
scheduleStatus: 2,
pageNum: 1,
pageSize: 999
}).then(res => {
this.acceptedScheduleList = res.rows || []
const totalCount = this.acceptedScheduleList.reduce((sum, sch) => sum + (sch.detailList || []).length, 0)
this.summaryText = `${this.acceptedScheduleList.length} 个已接收产需单,${totalCount} 条明细`
}).catch(() => {
this.acceptedScheduleList = []
this.summaryText = ''
}).finally(() => {
this.acceptedLoading = false
})
},
handleAccept(sch) {
const details = sch.detailList || []
if (details.length === 0) {
this.$message.warning('该产需单无可排产的明细')
return
}
this.$confirm(
`确认接收产需单「${sch.scheduleNo}」的全部排产明细吗?共 ${details.length} 条明细`,
'提示',
{ confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }
).then(() => {
this.$message.info('正在处理接收...')
receiveScheduleItem(sch.scheduleId).then(() => {
this.$modal.msgSuccess('接收成功,排产明细已写入')
this.queryPending()
}).catch(() => {
this.$modal.msgError('接收失败')
})
}).catch(() => {})
// 初始化接收明细列表,为每条明细添加 processId 和 stepList 字段
this.receiveScheduleId = sch.scheduleId
this.receiveDetailList = details.map(d => ({
...d,
processId: undefined,
stepList: []
}))
this.receiveDialogVisible = true
},
handleReceiveProcessChange(row, processId, processData) {
if (processData && processData.stepList) {
row.stepList = processData.stepList
} else {
row.stepList = []
}
},
confirmReceive() {
// 验证所有明细都配置了工序
const unconfigured = this.receiveDetailList.filter(d => !d.processId)
if (unconfigured.length > 0) {
this.$message.warning(`还有 ${unconfigured.length} 条明细未配置工序,请为所有明细配置工序`)
return
}
// 构建接收数据,传递每条明细的 processId 和 stepList
const receiveData = {
scheduleId: this.receiveScheduleId,
detailProcessList: this.receiveDetailList.map(d => ({
scheduleDetailId: d.scheduleDetailId || d.scheduleDetailIds,
processId: d.processId,
stepList: (d.stepList || []).map(s => ({
stepId: s.stepId,
stepOrder: s.stepOrder,
stepName: s.stepName
}))
}))
}
this.receiveBtnLoading = true
receiveScheduleItem(receiveData).then(() => {
this.$modal.msgSuccess('接收成功,排产明细已写入')
this.receiveDialogVisible = false
this.queryPending()
}).catch(() => {
this.$modal.msgError('接收失败')
}).finally(() => {
this.receiveBtnLoading = false
})
},
handleReject(sch) {
@@ -864,16 +920,14 @@ export default {
queryScheduled() {
this.schLoading = true
this.scheduledItemList = []
this.sourceColorMap = {}
this.selectedItems = []
this.scheduledStepTab = ''
listScheduleItem({ prodDate: this.queryDate, pageNum: 1, pageSize: 999 }).then(res => {
this.scheduledItemList = (res.rows || []).sort((a, b) => (a.scheduleNo || '').localeCompare(b.scheduleNo || ''))
try {
this.sourceColorMap = this.buildGroupColorMap(this.scheduledItemList)
} catch (e) {
console.error('buildGroupColorMap error:', e)
this.sourceColorMap = {}
// 默认选中第一个步骤tab
const tabs = this.scheduledStepTabs
if (tabs.length > 0) {
this.scheduledStepTab = tabs[0]
}
const totalWeight = this.scheduledItemList.reduce((sum, d) => {
const w = parseFloat(d.scheduleWeight)
@@ -934,98 +988,6 @@ export default {
}).catch(() => {})
},
// ====== 合并 ======
handleSelectionChange(rows) {
this.selectedItems = rows
},
getItemRowClassName({ row }) {
const key = row.scheduleNo
if (!key) return ''
const colorIndex = this.sourceColorMap[key]
return colorIndex !== undefined ? `merge-source-${colorIndex}` : ''
},
buildGroupColorMap(list) {
const map = {}
let colorIdx = 0
list.forEach(item => {
const key = item.scheduleNo
if (!key) return
if (!(key in map)) {
map[key] = colorIdx % 5
colorIdx++
}
})
return map
},
handleMergePrepare() {
if (this.selectedItems.length < 2) {
this.$message.warning('请至少选择2条排产明细进行合并')
return
}
this.mergeSourceRows = [...this.selectedItems]
this.mergeTemplateIndex = 0
this.pickMergeTemplate(this.mergeSourceRows[0])
this.mergeDialogVisible = true
},
pickMergeTemplate(row) {
const idx = this.mergeSourceRows.indexOf(row)
if (idx >= 0) this.mergeTemplateIndex = idx
this.mergeForm = {
itemCount: this.mergeSourceRows.length,
scheduleNo: row.scheduleNo || '',
actionId: row.actionId != null ? Number(row.actionId) : '',
customerName: row.customerName || '',
spec: row.spec || '',
material: row.material || '',
scheduleWeight: this.mergeSourceRows.reduce((sum, r) => sum + (parseFloat(r.scheduleWeight) || 0), 0),
productType: row.productType || '',
productItem: row.productItem || '',
businessUser: row.businessUser || '',
businessPhone: row.businessPhone || '',
deliveryCycle: row.deliveryCycle,
usePurpose: row.usePurpose || '',
thicknessTolerance: row.thicknessTolerance || '',
widthTolerance: row.widthTolerance || '',
surfaceQuality: row.surfaceQuality || '',
surfaceTreatment: row.surfaceTreatment || '',
innerDiameter: row.innerDiameter || '',
outerDiameter: row.outerDiameter || '',
packReq: row.packReq || '',
cutEdgeReq: row.cutEdgeReq || '',
singleCoilWeight: row.singleCoilWeight || '',
weightDeviation: row.weightDeviation || '',
otherTechReq: row.otherTechReq || '',
paymentDesc: row.paymentDesc || '',
remark: row.remark || '',
scheduleDetailIds: this.mergeSourceRows.map(r => r.scheduleDetailIds || '').filter(Boolean).join(',')
}
},
confirmMerge() {
this.mergeBtnLoading = true
const ids = this.mergeSourceRows.map(r => r.scheduleId)
const mergedBo = {
...this.mergeForm,
prodDate: this.queryDate,
scheduleStatus: 2
}
delete mergedBo.itemCount
mergeScheduleItem({ ids, mergedBo }).then(() => {
this.$modal.msgSuccess(`合并成功:${ids.length} 条明细合并为 1 条`)
this.mergeDialogVisible = false
this.selectedItems = []
this.queryScheduled()
}).catch(() => {
this.$modal.msgError('合并失败')
}).finally(() => {
this.mergeBtnLoading = false
})
},
// ====== 下钻 & 辅助方法 ======
scheduleTotalWeight(sch) {
const details = sch.detailList || []
@@ -1038,6 +1000,12 @@ export default {
return p ? p.name : (actionId || '')
},
// 去掉步骤名称中的括号及括号内文字:镀锌(毛化)=> 镀锌
normalizeStepName(name) {
if (!name) return ''
return name.replace(/[(][^)]*[)]/g, '').trim()
},
handleDetailClick(sch, detail) {
// 点击明细行查看来源订单
if (!sch || !sch.orderList || sch.orderList.length === 0) {
@@ -1350,20 +1318,85 @@ export default {
font-size: 12px;
}
// ====== 合并候选行颜色高亮(同一源排产单 → 同色) ======
::v-deep .el-table__body tr.merge-source-0 > td {
background-color: #f0f5ff !important;
// ====== 接收弹窗步骤展示 ======
.receive-steps-flow {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 2px;
}
::v-deep .el-table__body tr.merge-source-1 > td {
background-color: #f6ffed !important;
.receive-step-tag {
display: inline-block;
padding: 2px 8px;
background: $aps-silver-1;
border: 1px solid $aps-silver-mid;
border-radius: 3px;
font-size: 12px;
color: $aps-text;
}
::v-deep .el-table__body tr.merge-source-2 > td {
background-color: #fff7e6 !important;
.receive-step-arrow {
color: $aps-red-2;
font-size: 14px;
padding: 0 2px;
}
::v-deep .el-table__body tr.merge-source-3 > td {
background-color: #f9f0ff !important;
// ====== 已排产步骤工序 Tab ======
.aps-step-tabs {
display: flex;
flex-wrap: wrap;
padding: 8px 12px;
gap: 6px;
background: $aps-silver-1;
border-bottom: 1px solid $aps-border;
}
::v-deep .el-table__body tr.merge-source-4 > td {
background-color: #fff0f6 !important;
.aps-step-tab {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 14px;
font-size: 12px;
font-weight: 500;
color: $aps-text-muted;
background: $aps-white;
border: 1px solid $aps-silver-mid;
border-radius: $aps-radius;
cursor: pointer;
transition: all 0.2s;
user-select: none;
&:hover {
color: $aps-red-2;
border-color: $aps-red-2;
}
}
.aps-step-tab-active {
color: #fff;
background: $aps-red-2;
border-color: $aps-red-2;
&:hover {
color: #fff;
}
}
.aps-step-tab-count {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 18px;
height: 16px;
padding: 0 5px;
font-size: 10px;
font-weight: 600;
border-radius: 8px;
background: rgba(0, 0, 0, 0.1);
.aps-step-tab-active & {
background: rgba(255, 255, 255, 0.25);
}
}
</style>

View File

@@ -81,7 +81,7 @@ const NODE_EVENT_CONFIG = {
{ id: 'H', label: '加工完工质检', handler: 'handleOpen', params: { componentPath: 'mes/qc/qualityReview/index' } },
{ id: 'I', label: '登记质量缺陷', handler: 'handleOpen', params: { componentPath: 'wms/coil/abnormal/extend' } },
{ id: 'J', label: '质量等级判定', handler: 'handleOpen', params: { componentPath: 'wms/coil/abnormal/extend' } },
{ id: 'K', label: '单卷档案', handler: 'handleOpen', params: { componentPath: 'wms/coil/info/index' } },
// { id: 'K', label: '单卷档案', handler: 'handleOpen', params: { componentPath: 'wms/coil/info/index' } },
{ id: 'L', label: '编排发货计划', handler: 'handleOpen', params: { componentPath: 'wms/delivery/waybill/index' } },
{ id: 'M', label: '生成发货单', handler: 'handleOpen', params: { componentPath: 'wms/delivery/bills/index' } },
{ id: 'N', label: '发货质量校验', handler: 'handleOpen', params: { componentPath: 'mes/qc/qualityReview/index' } },

View File

@@ -1,8 +1,8 @@
<template>
<div v-if="enabled" class="section-container">
<div class="detail-section">
<div class="detail-section-label">Complaint Content · 投诉情况与客户诉求</div>
<div class="detail-section-text">{{ data.complaintContent || '-' }}</div>
<div class="complaint-main">
<div class="complaint-label">Complaint Content · 投诉情况与客户诉求</div>
<div class="complaint-body">{{ data.complaintContent || '-' }}</div>
</div>
<div class="detail-section" v-if="customerInfo">
@@ -35,29 +35,29 @@
<div class="detail-section-text">-</div>
</div>
<div class="detail-section">
<div class="detail-section-label">Downstream User · 下游使用用户</div>
<div class="detail-section-text">{{ data.downstreamUserName || '-' }}</div>
</div>
<div class="divider-label">Other Info · 其他信息</div>
<div class="detail-section">
<div class="detail-section-label">Phone · 电话</div>
<div class="detail-section-text">{{ data.phone || '-' }}</div>
</div>
<div class="detail-section">
<div class="detail-section-label">Project Name · 工程名称</div>
<div class="detail-section-text">{{ data.projectName || '-' }}</div>
</div>
<div class="detail-section">
<div class="detail-section-label">Project Location · 使用地点</div>
<div class="detail-section-text">{{ data.projectLocation || '-' }}</div>
</div>
<div class="detail-section">
<div class="detail-section-label">Product Usage · 产品用途</div>
<div class="detail-section-text">{{ data.productUsage || '-' }}</div>
<div class="secondary-grid">
<div class="secondary-item">
<span class="secondary-key">下游使用用户</span>
<span class="secondary-val">{{ data.downstreamUserName || '-' }}</span>
</div>
<div class="secondary-item">
<span class="secondary-key">电话</span>
<span class="secondary-val">{{ data.phone || '-' }}</span>
</div>
<div class="secondary-item">
<span class="secondary-key">工程名称</span>
<span class="secondary-val">{{ data.projectName || '-' }}</span>
</div>
<div class="secondary-item">
<span class="secondary-key">使用地点</span>
<span class="secondary-val">{{ data.projectLocation || '-' }}</span>
</div>
<div class="secondary-item">
<span class="secondary-key">产品用途</span>
<span class="secondary-val">{{ data.productUsage || '-' }}</span>
</div>
</div>
<div class="detail-section" v-if="data.file">
@@ -109,6 +109,37 @@ export default {
</script>
<style scoped>
.section-container {
margin-bottom: 2px;
}
/* ===== 投诉情况主体 — 突出显示 ===== */
.complaint-main {
margin-bottom: 18px;
padding: 16px 18px;
background: #fff;
border: 1px solid #cbd5e1;
border-left: 4px solid #1e293b;
}
.complaint-label {
font-size: 12px;
font-weight: 600;
color: #475569;
margin-bottom: 10px;
letter-spacing: 0.4px;
text-transform: uppercase;
}
.complaint-body {
font-size: 15px;
color: #1e293b;
line-height: 1.8;
word-break: break-all;
min-height: 60px;
white-space: pre-wrap;
}
.detail-section {
margin-bottom: 14px;
}
@@ -163,4 +194,45 @@ export default {
font-size: 13px;
color: #1e293b;
}
/* ===== 分隔标签 ===== */
.divider-label {
font-size: 11px;
font-weight: 600;
color: #94a3b8;
letter-spacing: 0.4px;
text-transform: uppercase;
margin: 16px 0 10px;
padding-bottom: 6px;
border-bottom: 1px dashed #e2e8f0;
}
/* ===== 次要信息网格 ===== */
.secondary-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px 16px;
padding: 10px 12px;
background: #f8fafc;
border: 1px solid #e2e8f0;
margin-bottom: 12px;
}
.secondary-item {
display: flex;
flex-direction: column;
gap: 2px;
}
.secondary-key {
font-size: 10px;
color: #94a3b8;
letter-spacing: 0.2px;
}
.secondary-val {
font-size: 12px;
color: #475569;
word-break: break-all;
}
</style>

View File

@@ -41,7 +41,7 @@
<el-tag v-else-if="item.flowStatus === 4" type="success" size="mini">全部办结</el-tag>
</div>
<div class="item-actions">
<el-button v-if="item.flowStatus === 4" size="mini" type="text" icon="el-icon-download"
<el-button size="mini" type="text" icon="el-icon-download"
@click.stop="handleExportPdf(item)" title="导出PDF"></el-button>
<el-button size="mini" type="text" icon="el-icon-edit" @click.stop="handleUpdate(item)"></el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click.stop="handleDelete(item)"></el-button>
@@ -70,7 +70,7 @@
<HeaderControlSection :complaintNo="currentRow.complaintNo" :flowStatus="currentRow.flowStatus"
:meta="currentRow">
<template #actions>
<el-button v-if="currentRow.flowStatus === 4" size="mini" type="text" icon="el-icon-download"
<el-button 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>

View File

@@ -149,7 +149,7 @@ export default {
customExportVisible: false,
columnGroups: {
'基本信息': ['itemTypeDesc', 'warehouseName', 'actualWarehouseName', 'dataTypeText'],
'钢卷号': ['enterCoilNo', 'supplierCoilNo', 'currentCoilNo'],
'钢卷号': ['enterCoilNo', 'supplierCoilNo', 'currentCoilNo', 'coilId'],
'时间': ['createTime', 'exportTime', 'exportBy'],
'物理属性': ['netWeight', 'length', 'specification', 'actualThickness', 'actualLength', 'actualWidth', 'theoreticalThickness', 'theoreticalLength'],
'材质属性': ['material', 'manufacturer', 'surfaceTreatmentDesc', 'zincLayer', 'packingStatus', 'temperGrade', 'coatingType', 'chromePlateCoilNo', 'rawMaterialThickness'],

View File

@@ -560,7 +560,7 @@ export default {
customExportType: 'output',
columnGroups: {
'基本信息': ['itemTypeDesc', 'warehouseName', 'actualWarehouseName', 'dataTypeText'],
'钢卷号': ['enterCoilNo', 'supplierCoilNo', 'currentCoilNo'],
'钢卷号': ['enterCoilNo', 'supplierCoilNo', 'currentCoilNo', 'coilId'],
'时间': ['createTime', 'exportTime', 'exportBy'],
'物理属性': ['netWeight', 'length', 'specification', 'actualThickness', 'actualLength', 'actualWidth', 'theoreticalThickness', 'theoreticalLength', 'scheduleThickness'],
'材质属性': ['material', 'manufacturer', 'surfaceTreatmentDesc', 'zincLayer', 'packingStatus', 'temperGrade', 'coatingType', 'chromePlateCoilNo', 'rawMaterialThickness'],