Files
klp-oa/klp-ui/src/views/mes/qc/certificate/chemistry.vue
砂糖 ca285f78c6 feat(qc): 新增厂家卷号匹配功能,优化卷号录入体验
本次改动在检验任务、化学成分报告、物理性能报告模块中:
1. 支持通过入场卷号和厂家卷号双向搜索匹配
2. 新增多选卷号标签式展示与删除功能
3. 新增批量导入时厂家卷号自动匹配入场卷号的逻辑,包含多匹配结果弹窗选择
4. 在表格中新增厂家卷号展示列,更新导入模板支持厂家卷号字段
2026-06-01 16:40:13 +08:00

1071 lines
36 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="入场钢卷号" prop="coilNo">
<el-input
v-model="queryParams.coilNo"
placeholder="请输入入场钢卷号"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="炉号" prop="heatNo">
<el-input
v-model="queryParams.heatNo"
placeholder="请输入炉号"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="el-icon-plus"
size="mini"
@click="handleAdd"
>新增</el-button>
</el-col>
<el-col :span="1.5">
<coil-selector :use-trigger="true" multiple @confirm="handleBatchAdd"
:filters="{ selectType: 'product', status: 0 }">
<el-button type="primary" plain icon="el-icon-plus" size="mini">批量新增</el-button>
</coil-selector>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="el-icon-upload2"
size="mini"
@click="openImportDialog"
>导入</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
plain
icon="el-icon-edit"
size="mini"
:disabled="single"
@click="handleUpdate"
>修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="el-icon-delete"
size="mini"
:disabled="multiple"
@click="handleDelete"
>删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="el-icon-download"
size="mini"
@click="handleExport"
>导出</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="chemicalItemList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="入场钢卷号" align="center" prop="coilNo" />
<el-table-column label="炉号" align="center" prop="heatNo" />
<el-table-column label="碳(%)" align="center" prop="c" />
<el-table-column label="硅(%)" align="center" prop="si" />
<el-table-column label="锰(%)" align="center" prop="mn" />
<el-table-column label="磷(%)" align="center" prop="p" />
<el-table-column label="硫(%)" align="center" prop="s" />
<el-table-column label="酸溶铝(%)" align="center" prop="als" />
<el-table-column label="铝(%)" align="center" prop="al" />
<el-table-column label="钛(%)" align="center" prop="ti" />
<el-table-column label="铬(%)" align="center" prop="cr" />
<el-table-column label="镍(%)" align="center" prop="ni" />
<el-table-column label="铜(%)" align="center" prop="cu" />
<el-table-column label="氮(%)" align="center" prop="n" />
<el-table-column label="铁(%)" align="center" prop="fe" />
<el-table-column label="硼(%)" align="center" prop="b" />
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
>修改</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
>删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<!-- 添加或修改质量的化学成分明细对话框 -->
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item label="入场钢卷号" prop="coilNo">
<el-autocomplete
v-model="form.coilNo"
:fetch-suggestions="queryCoilNo"
placeholder="输入入场钢卷号搜索"
:loading="coilNoLoading"
:trigger-on-focus="false"
clearable
/>
<el-autocomplete
v-model="tempSupplierCoilNo"
:fetch-suggestions="querySupplierCoilNo"
placeholder="或输入厂家卷号自动匹配"
:loading="supplierCoilNoLoading"
:trigger-on-focus="false"
clearable
size="small"
@select="handleSelectSupplierCoilNo"
style="margin-top: 6px;"
/>
</el-form-item>
<el-form-item label="炉号" prop="heatNo">
<el-input v-model="form.heatNo" placeholder="请输入炉号" />
</el-form-item>
<el-form-item label="碳(%)" prop="c">
<el-input v-model="form.c" placeholder="请输入碳(%)" />
</el-form-item>
<el-form-item label="硅(%)" prop="si">
<el-input v-model="form.si" placeholder="请输入硅(%)" />
</el-form-item>
<el-form-item label="锰(%)" prop="mn">
<el-input v-model="form.mn" placeholder="请输入锰(%)" />
</el-form-item>
<el-form-item label="磷(%)" prop="p">
<el-input v-model="form.p" placeholder="请输入磷(%)" />
</el-form-item>
<el-form-item label="硫(%)" prop="s">
<el-input v-model="form.s" placeholder="请输入硫(%)" />
</el-form-item>
<el-form-item label="酸溶铝(%)" prop="als">
<el-input v-model="form.als" placeholder="请输入酸溶铝(%)" />
</el-form-item>
<el-form-item label="铝(%)" prop="al">
<el-input v-model="form.al" placeholder="请输入铝(%)" />
</el-form-item>
<el-form-item label="钛(%)" prop="ti">
<el-input v-model="form.ti" placeholder="请输入钛(%)" />
</el-form-item>
<el-form-item label="铬(%)" prop="cr">
<el-input v-model="form.cr" placeholder="请输入铬(%)" />
</el-form-item>
<el-form-item label="镍(%)" prop="ni">
<el-input v-model="form.ni" placeholder="请输入镍(%)" />
</el-form-item>
<el-form-item label="铜(%)" prop="cu">
<el-input v-model="form.cu" placeholder="请输入铜(%)" />
</el-form-item>
<el-form-item label="氮(%)" prop="n">
<el-input v-model="form.n" placeholder="请输入氮(%)" />
</el-form-item>
<el-form-item label="铁(%)" prop="fe">
<el-input v-model="form.fe" placeholder="请输入铁(%)" />
</el-form-item>
<el-form-item label="硼(%)" prop="b">
<el-input v-model="form.b" placeholder="请输入硼(%)" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" placeholder="请输入备注" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</el-dialog>
<!-- 导入对话框 -->
<el-dialog title="导入化学成分" :visible.sync="importDialogVisible" width="1100px" top="5vh" append-to-body>
<div class="import-container">
<!-- 步骤条 -->
<div class="import-steps">
<div class="step" :class="{ active: true, done: importStatus !== 'idle' && importStatus !== 'validated' }">
<div class="step-badge">1</div>
<div class="step-label">下载模板</div>
</div>
<div class="step-connector" :class="{ done: importStatus !== 'idle' }" />
<div class="step" :class="{ active: importStatus === 'validated' || importStatus === 'processing' || importStatus === 'finished', done: importStatus !== 'idle' && importStatus !== 'validated' }">
<div class="step-badge">2</div>
<div class="step-label">上传文件</div>
</div>
<div class="step-connector" :class="{ done: importStatus === 'processing' || importStatus === 'finished' }" />
<div class="step" :class="{ active: importStatus === 'validated' || importStatus === 'processing' || importStatus === 'finished', done: importStatus === 'processing' || importStatus === 'finished' }">
<div class="step-badge">3</div>
<div class="step-label">校验数据</div>
</div>
<div class="step-connector" :class="{ done: importStatus === 'processing' || importStatus === 'finished' }" />
<div class="step" :class="{ active: importStatus === 'processing' || importStatus === 'finished', done: importStatus === 'finished' }">
<div class="step-badge step-badge-success">4</div>
<div class="step-label">批量导入</div>
</div>
</div>
<!-- 工具栏 -->
<div class="import-toolbar">
<div class="toolbar-left">
<el-upload ref="importUpload" action="" :auto-upload="false" :show-file-list="false"
:on-change="importHandleFileChange" accept=".xlsx,.xls"
:disabled="importStatus !== 'idle' && importStatus !== 'validated'">
<el-button :type="importFile ? 'default' : 'primary'" :icon="importFile ? 'el-icon-document' : 'el-icon-upload2'">
{{ importFile ? importFile.name : '选择Excel文件' }}
</el-button>
</el-upload>
<el-button type="success" plain icon="el-icon-check" @click="importHandleValidate"
:disabled="!importFile || importValidateLoading || importStatus !== 'idle'" :loading="importValidateLoading">校验数据</el-button>
</div>
<div class="toolbar-right">
<el-button v-if="importStatus === 'validated'" type="primary" icon="el-icon-upload-success" @click="importStartImport" :loading="importLoading">开始导入</el-button>
<el-button plain icon="el-icon-download" @click="importDownloadTemplate">下载模板</el-button>
<el-button plain icon="el-icon-refresh" @click="importReset">重置</el-button>
</div>
</div>
<!-- 数据预览表格 -->
<div v-if="importTableData.length > 0" class="data-preview">
<el-table ref="importTable" :data="importTableData" border size="small" max-height="340" stripe
:row-class-name="importTableRowClassName">
<el-table-column label="#" width="48" type="index" align="center" />
<el-table-column prop="coilNo" label="入场钢卷号" width="140" />
<el-table-column prop="supplierCoilNo" label="厂家卷号" width="120" />
<el-table-column prop="c" label="C(%)" width="70" align="center" />
<el-table-column prop="si" label="Si(%)" width="70" align="center" />
<el-table-column prop="mn" label="Mn(%)" width="70" align="center" />
<el-table-column prop="p" label="P(%)" width="70" align="center" />
<el-table-column prop="s" label="S(%)" width="70" align="center" />
<el-table-column prop="als" label="Als(%)" width="70" align="center" />
<el-table-column prop="al" label="Al(%)" width="70" align="center" />
<el-table-column prop="ti" label="Ti(%)" width="70" align="center" />
<el-table-column prop="cr" label="Cr(%)" width="70" align="center" />
<el-table-column prop="ni" label="Ni(%)" width="70" align="center" />
<el-table-column prop="cu" label="Cu(%)" width="70" align="center" />
<el-table-column prop="n" label="N(%)" width="70" align="center" />
<el-table-column prop="fe" label="Fe(%)" width="70" align="center" />
<el-table-column prop="b" label="B(%)" width="70" align="center" />
</el-table>
</div>
<!-- 导入进度 -->
<div v-if="importStatus === 'processing'" class="import-progress">
<div class="progress-header">
<i class="el-icon-loading" />
<span>正在批量导入数据请稍候...</span>
</div>
<el-progress :percentage="importProgress" :stroke-width="8" />
</div>
<!-- 完成提示 -->
<div v-if="importStatus === 'finished'" class="result-panel result-success">
<div class="result-icon"><i class="el-icon-circle-check" /></div>
<div class="result-body">
<div class="result-title">导入完成</div>
<div class="result-desc">共成功导入 <b>{{ importImportedCount }}</b> 条化学成分数据</div>
</div>
</div>
<!-- 错误提示 -->
<div v-if="importStatus === 'error'" class="result-panel result-error">
<div class="result-icon"><i class="el-icon-circle-close" /></div>
<div class="result-body">
<div class="result-title">导入失败</div>
<div class="result-desc">{{ importErrorMsg }}</div>
</div>
</div>
</div>
</el-dialog>
<el-dialog title="选择对应入场卷号" :visible.sync="ambiguousVisible" width="600px" append-to-body :close-on-click-modal="false">
<div style="font-size: 12px; color: #909399; margin-bottom: 12px;">
厂家卷号匹配到多条入场卷号请为每行选择对应记录
</div>
<el-table :data="ambiguousRows" border size="small" max-height="350">
<el-table-column label="厂家卷号" prop="supplierCoilNo" width="140" />
<el-table-column label="选择入场卷号" min-width="280">
<template slot-scope="scope">
<el-select v-model="scope.row.selected" placeholder="请选择入场卷号" size="small" style="width: 100%;">
<el-option
v-for="opt in scope.row.options"
:key="opt"
:label="opt"
:value="opt"
/>
</el-select>
</template>
</el-table-column>
</el-table>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="confirmAmbiguous"> </el-button>
<el-button @click="ambiguousVisible = false"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import * as XLSX from 'xlsx';
import { listChemicalItem, getChemicalItem, delChemicalItem, addChemicalItem, updateChemicalItem, batchAddChemicalItem } from "@/api/mes/qc/chemicalItem";
import CoilSelector from "@/components/CoilSelector";
import { listMaterialCoil } from "@/api/wms/coil";
const CHEMI_TEMPLATE_HEADERS = ['入场钢卷号', '厂家卷号', 'C(%)', 'Si(%)', 'Mn(%)', 'P(%)', 'S(%)', 'Als(%)', 'Al(%)', 'Ti(%)', 'Cr(%)', 'Ni(%)', 'Cu(%)', 'N(%)', 'Fe(%)', 'B(%)'];
const CHEMI_HEADER_MAP = {
'入场钢卷号': 'coilNo',
'厂家卷号': 'supplierCoilNo',
'C(%)': 'c',
'Si(%)': 'si',
'Mn(%)': 'mn',
'P(%)': 'p',
'S(%)': 's',
'Als(%)': 'als',
'Al(%)': 'al',
'Ti(%)': 'ti',
'Cr(%)': 'cr',
'Ni(%)': 'ni',
'Cu(%)': 'cu',
'N(%)': 'n',
'Fe(%)': 'fe',
'B(%)': 'b'
};
export default {
name: "ChemicalItem",
components: {
CoilSelector
},
computed: {},
data() {
return {
// 按钮loading
buttonLoading: false,
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 显示搜索条件
showSearch: true,
// 总条数
total: 0,
// 质量的化学成分明细表格数据
chemicalItemList: [],
// 弹出层标题
title: "",
// 是否显示弹出层
open: false,
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
coilNo: undefined,
heatNo: undefined,
c: undefined,
si: undefined,
mn: undefined,
p: undefined,
s: undefined,
als: undefined,
al: undefined,
ti: undefined,
cr: undefined,
ni: undefined,
cu: undefined,
n: undefined,
fe: undefined,
b: undefined,
},
// 表单参数
form: {},
// 表单校验
rules: {
coilNo: [
{ required: true, message: "入场钢卷号不能为空", trigger: "blur" }
],
},
importDialogVisible: false,
importFile: null,
importRawData: [],
importTableData: [],
importErrorList: [],
importProgress: 0,
importImportedCount: 0,
importTotalCount: 0,
importStatus: 'idle', // idle | validated | matched | processing | finished | error
importErrorMsg: '',
importValidateLoading: false,
importLoading: false,
coilNoOptions: [],
coilNoLoading: false,
tempSupplierCoilNo: '',
supplierCoilNoLoading: false,
ambiguousVisible: false,
ambiguousRows: [],
};
},
created() {
this.getList();
},
methods: {
/** 查询质量的化学成分明细列表 */
getList() {
this.loading = true;
listChemicalItem(this.queryParams).then(response => {
this.chemicalItemList = response.rows;
this.total = response.total;
this.loading = false;
});
},
// 取消按钮
cancel() {
this.open = false;
this.reset();
},
// 表单重置
reset() {
this.form = {
itemId: undefined,
certificateId: undefined,
coilNo: undefined,
heatNo: undefined,
c: undefined,
si: undefined,
mn: undefined,
p: undefined,
s: undefined,
als: undefined,
al: undefined,
ti: undefined,
cr: undefined,
ni: undefined,
cu: undefined,
n: undefined,
fe: undefined,
b: undefined,
remark: undefined,
delFlag: undefined,
createTime: undefined,
createBy: undefined,
updateTime: undefined,
updateBy: undefined
};
this.resetForm("form");
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
/** 重置按钮操作 */
resetQuery() {
this.resetForm("queryForm");
this.handleQuery();
},
// 多选框选中数据
handleSelectionChange(selection) {
this.ids = selection.map(item => item.itemId)
this.single = selection.length!==1
this.multiple = !selection.length
},
/** 新增按钮操作 */
handleAdd() {
this.reset();
this.open = true;
this.title = "添加质量的化学成分明细";
},
handleSelect(coil) {
this.form.coilNo = coil.enterCoilNo;
},
queryCoilNo(queryString, cb) {
if (!queryString || queryString.length < 2) {
cb([]);
return;
}
this.coilNoLoading = true;
listMaterialCoil({
enterCoilNo: queryString,
pageNum: 1,
pageSize: 20
}).then(response => {
const options = (response.rows || []).map(item => ({
value: item.enterCoilNo,
label: item.enterCoilNo
}));
cb(options);
}).finally(() => {
this.coilNoLoading = false;
});
},
querySupplierCoilNo(queryString, cb) {
if (!queryString || queryString.length < 2) {
cb([]);
return;
}
this.supplierCoilNoLoading = true;
listMaterialCoil({
supplierCoilNo: queryString,
pageNum: 1,
pageSize: 20
}).then(response => {
const options = (response.rows || []).map(item => ({
value: item.supplierCoilNo,
label: item.supplierCoilNo,
enterCoilNo: item.enterCoilNo
}));
cb(options);
}).finally(() => {
this.supplierCoilNoLoading = false;
});
},
handleSelectSupplierCoilNo(item) {
if (item.enterCoilNo) {
this.form.coilNo = item.enterCoilNo;
this.$refs["form"].validateField('coilNo');
}
this.tempSupplierCoilNo = '';
},
handleBatchAdd(rows) {
this.loading = true;
this.buttonLoading = true;
const payload = rows.map(coil => ({
coilNo: coil.enterCoilNo,
}))
batchAddChemicalItem(payload).then(response => {
this.$modal.msgSuccess("新增成功");
this.getList();
}).finally(() => {
this.buttonLoading = false;
this.loading = false;
});
},
openImportDialog() {
this.importDialogVisible = true;
},
importHandleFileChange(file) {
if (this.importValidateLoading || this.importLoading) return;
this.importFile = file.raw;
this.importErrorList = [];
this.importStatus = 'idle';
this.importReadExcel();
},
async importReadExcel() {
if (!this.importFile) return;
try {
const reader = new FileReader();
reader.readAsArrayBuffer(this.importFile);
reader.onload = async (e) => {
try {
const data = new Uint8Array(e.target.result);
const wb = XLSX.read(data, { type: 'array' });
const ws = wb.Sheets[wb.SheetNames[0]];
const json = XLSX.utils.sheet_to_json(ws, { header: 1 });
this.importValidateHeaders(json[0]);
this.importRawData = json.slice(1).filter(r => r.some(c => c != null && c !== ''));
this.importFormatExcel();
this.$message.success(`成功解析Excel共读取到 ${this.importRawData.length} 条数据`);
} catch (err) { this.importHandleError('解析Excel失败' + err.message); }
};
} catch (err) { this.importHandleError('读取文件失败:' + err.message); }
},
importValidateHeaders(headers) {
this.importErrorList = [];
if (!headers || headers.length < CHEMI_TEMPLATE_HEADERS.length) {
this.importErrorList.push({ rowNum: 1, errorMsg: `表头数量不匹配,要求至少${CHEMI_TEMPLATE_HEADERS.length}` });
return;
}
CHEMI_TEMPLATE_HEADERS.forEach((h, i) => {
if (headers[i] !== h) this.importErrorList.push({ rowNum: 1, errorMsg: `${i+1}列表头错误,要求:"${h}",实际:"${headers[i]||''}"` });
});
if (this.importErrorList.length > 0) this.$message.error('Excel表头格式不符合要求请检查');
},
importFormatExcel() {
this.importTableData = [];
if (!this.importRawData.length) return;
this.importRawData.forEach((row, i) => {
const obj = {};
CHEMI_TEMPLATE_HEADERS.forEach((h, j) => { obj[CHEMI_HEADER_MAP[h]] = row[j] != null ? String(row[j]).trim() : ''; });
obj.rowNum = i + 2;
obj._status = 'pending';
this.importTableData.push(obj);
});
},
async importHandleValidate() {
if (!this.importFile || !this.importRawData.length) { this.$message.warning('暂无数据可校验'); return; }
this.importValidateLoading = true;
this.importErrorList = [];
this.ambiguousRows = [];
const dataRows = this.importRawData;
for (let i = 0; i < dataRows.length; i++) {
const rowNum = i + 2;
let coilNo = '', supplierCoilNo = '';
CHEMI_TEMPLATE_HEADERS.forEach((h, j) => {
if (CHEMI_HEADER_MAP[h] === 'coilNo') coilNo = dataRows[i][j];
if (CHEMI_HEADER_MAP[h] === 'supplierCoilNo') supplierCoilNo = dataRows[i][j];
});
coilNo = String(coilNo || '').trim();
supplierCoilNo = String(supplierCoilNo || '').trim();
if (!coilNo && !supplierCoilNo) {
this.importErrorList.push({ rowNum, errorMsg: '入场钢卷号和厂家卷号不能同时为空' });
}
}
// 处理仅填写厂家卷号的行
if (this.importErrorList.length === 0) {
this.importTableData.forEach((row, idx) => {
const cn = (row.coilNo || '').trim();
const sn = (row.supplierCoilNo || '').trim();
row._sno = sn;
row._idx = idx;
row._fillCoilNo = cn;
// 仅厂家卷号且无入场卷号的行需匹配
if (sn && !cn) row._needMatch = true;
});
const needMatchRows = this.importTableData.filter(r => r._needMatch);
if (needMatchRows.length > 0) {
const uniqueSnos = [...new Set(needMatchRows.map(r => r._sno))];
const multiMap = {}; // supplierCoilNo -> [enterCoilNo, ...]
try {
const results = await Promise.all(
uniqueSnos.map(sno => listMaterialCoil({ supplierCoilNo: sno, pageNum: 1, pageSize: 50 }))
);
uniqueSnos.forEach((sno, idx) => {
const rows = (results[idx] && results[idx].rows) || [];
const enterNos = [...new Set(rows.map(r => r.enterCoilNo).filter(Boolean))];
multiMap[sno] = enterNos;
});
} catch (err) {
this.importValidateLoading = false;
this.importHandleError('厂家卷号匹配失败:' + err.message);
return;
}
const ambiguousList = [];
needMatchRows.forEach(row => {
const candidates = multiMap[row._sno] || [];
if (candidates.length === 0) {
this.importErrorList.push({ rowNum: (row.rowNum || (row._idx + 2)), errorMsg: `厂家卷号"${row._sno}"未匹配到入场卷号` });
} else if (candidates.length === 1) {
row.coilNo = candidates[0];
} else {
ambiguousList.push({
_idx: row._idx,
supplierCoilNo: row._sno,
options: candidates,
selected: null
});
}
});
if (ambiguousList.length > 0) {
this.ambiguousRows = ambiguousList;
this.importValidateLoading = false;
this.ambiguousVisible = true;
return;
}
}
}
this.finishValidation();
},
confirmAmbiguous() {
const unselected = this.ambiguousRows.filter(r => !r.selected);
if (unselected.length > 0) {
this.$message.warning('请为所有行选择入场卷号');
return;
}
this.ambiguousRows.forEach(item => {
this.importTableData[item._idx].coilNo = item.selected;
});
this.ambiguousVisible = false;
this.importValidateLoading = true;
this.finishValidation();
},
finishValidation() {
if (this.importErrorList.length > 0) {
this.$message.error(`数据校验失败,共发现${this.importErrorList.length}条错误`);
} else {
this.importTableData.forEach(row => { row._status = 'valid'; });
this.$message.success('数据校验通过');
this.importStatus = 'validated';
}
this.importValidateLoading = false;
},
async importStartImport() {
const rows = this.importTableData.filter(r => r._status === 'valid');
if (!rows.length) { this.$message.warning('没有可导入的数据'); return; }
const ok = await this.$confirm(`确认导入 ${rows.length} 条数据?`, '导入确认', { confirmButtonText: '确认导入', cancelButtonText: '取消', type: 'warning' }).catch(() => false);
if (!ok) return;
this.importLoading = true;
this.importStatus = 'processing';
this.importProgress = 0;
this.importTotalCount = rows.length;
this.importErrorMsg = '';
try {
const payload = rows.map(row => {
const item = { coilId: null, coilNo: row.coilNo };
['c','si','mn','p','s','als','al','ti','cr','ni','cu','n','fe','b'].forEach(f => { if (row[f]) item[f] = row[f]; });
return item;
});
const addRes = await batchAddChemicalItem(payload);
if (addRes.code !== 200) throw new Error(addRes.msg || '批量导入失败');
this.importStatus = 'finished';
this.importImportedCount = payload.length;
this.importProgress = 100;
this.$message.success(`导入完成!共成功导入${payload.length}条数据`);
this.getList();
} catch (err) {
this.importHandleError(err.message);
} finally {
this.importLoading = false;
}
},
importReset() {
if (this.importValidateLoading || this.importLoading || this.importStatus === 'processing') { this.$message.warning('当前有操作正在进行中'); return; }
this.importFile = null;
this.importRawData = [];
this.importTableData = [];
this.importErrorList = [];
this.importProgress = 0;
this.importImportedCount = 0;
this.importTotalCount = 0;
this.importStatus = 'idle';
this.importErrorMsg = '';
this.$refs.importUpload?.clearFiles();
},
importDownloadTemplate() {
const data = [CHEMI_TEMPLATE_HEADERS, ['示例卷号', '示例厂家卷号', '0.05', '0.02', '0.30', '0.015', '0.008', '0.040', '0.04', '0.05', '0.03', '0.02', '0.03', '0.005', '98', '0.001']];
const wb = XLSX.utils.book_new();
const ws = XLSX.utils.aoa_to_sheet(data);
ws['!cols'] = [{ wch: 16 }, { wch: 16 }, { wch: 8 }, { wch: 8 }, { wch: 8 }, { wch: 8 }, { wch: 8 }, { wch: 8 }, { wch: 8 }, { wch: 8 }, { wch: 8 }, { wch: 8 }, { wch: 8 }, { wch: 8 }, { wch: 8 }, { wch: 8 }];
XLSX.utils.book_append_sheet(wb, ws, '导入模板');
XLSX.writeFile(wb, '化学成分导入模板.xlsx');
},
importHandleError(msg) {
this.$message.error(msg);
this.importStatus = 'error';
this.importErrorMsg = msg;
this.importValidateLoading = false;
this.importLoading = false;
},
importTableRowClassName() {
return '';
},
/** 修改按钮操作 */
handleUpdate(row) {
this.loading = true;
this.reset();
const itemId = row.itemId || this.ids
getChemicalItem(itemId).then(response => {
this.loading = false;
this.form = response.data;
this.open = true;
this.title = "修改质量的化学成分明细";
});
},
/** 提交按钮 */
submitForm() {
this.$refs["form"].validate(valid => {
if (valid) {
this.buttonLoading = true;
if (this.form.itemId != null) {
updateChemicalItem(this.form).then(response => {
this.$modal.msgSuccess("修改成功");
this.open = false;
this.getList();
}).finally(() => {
this.buttonLoading = false;
});
} else {
addChemicalItem(this.form).then(response => {
this.$modal.msgSuccess("新增成功");
this.open = false;
this.getList();
}).finally(() => {
this.buttonLoading = false;
});
}
}
});
},
/** 删除按钮操作 */
handleDelete(row) {
const itemIds = row.itemId || this.ids;
this.$modal.confirm('是否确认删除质量的化学成分明细编号为"' + itemIds + '"的数据项?').then(() => {
this.loading = true;
return delChemicalItem(itemIds);
}).then(() => {
this.loading = false;
this.getList();
this.$modal.msgSuccess("删除成功");
}).catch(() => {
}).finally(() => {
this.loading = false;
});
},
/** 导出按钮操作 */
handleExport() {
this.download('qc/chemicalItem/export', {
...this.queryParams
}, `chemicalItem_${new Date().getTime()}.xlsx`)
}
}
};
</script>
<style scoped>
.import-container {
padding: 0 4px;
}
.import-steps {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 24px;
padding: 20px 0 16px;
border-bottom: 1px solid #f0f0f0;
}
.step {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
opacity: 0.45;
transition: all 0.3s;
}
.step.active { opacity: 1; }
.step.done { opacity: 0.65; }
.step-badge {
width: 32px;
height: 32px;
border-radius: 50%;
background: #dcdfe6;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 600;
transition: all 0.3s;
}
.step.active .step-badge {
background: #409eff;
box-shadow: 0 2px 8px rgba(64,158,255,0.35);
}
.step.done .step-badge {
background: #67c23a;
}
.step-badge-warn { background: #e6a23c !important; }
.step.active .step-badge-warn { background: #e6a23c !important; box-shadow: 0 2px 8px rgba(230,162,60,0.35) !important; }
.step-badge-success { background: #67c23a !important; }
.step.active .step-badge-success { background: #67c23a !important; box-shadow: 0 2px 8px rgba(103,194,58,0.35) !important; }
.step-label {
font-size: 12px;
color: #606266;
white-space: nowrap;
}
.step.active .step-label { color: #303133; font-weight: 600; }
.step-connector {
width: 48px;
height: 2px;
background: #dcdfe6;
margin: 0 4px;
margin-bottom: 24px;
transition: all 0.3s;
}
.step-connector.done { background: #67c23a; }
.import-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 16px;
padding: 12px 16px;
background: #fafafa;
border-radius: 8px;
border: 1px solid #eee;
}
.toolbar-left, .toolbar-right {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.match-summary {
display: flex;
gap: 12px;
margin-bottom: 16px;
}
.summary-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 14px 8px;
border-radius: 8px;
background: #fafafa;
border: 1px solid #eee;
gap: 4px;
}
.summary-matched { border-color: #b7eb8f; background: #f6ffed; }
.summary-ambiguous { border-color: #ffe58f; background: #fffbe6; }
.summary-notfound { border-color: #ffa39e; background: #fff2f0; }
.summary-total { border-color: #d9d9d9; background: #fafafa; }
.summary-num {
font-size: 22px;
font-weight: 700;
line-height: 1;
}
.summary-matched .summary-num { color: #52c41a; }
.summary-ambiguous .summary-num { color: #faad14; }
.summary-notfound .summary-num { color: #ff4d4f; }
.summary-total .summary-num { color: #409eff; }
.summary-label {
font-size: 12px;
color: #606266;
}
.data-preview {
margin-bottom: 12px;
}
::v-deep .tag-clickable {
cursor: pointer;
}
::v-deep .tag-clickable:hover {
opacity: 0.8;
}
.import-progress {
padding: 24px;
background: #f6ffed;
border-radius: 8px;
border: 1px solid #b7eb8f;
margin-bottom: 12px;
}
.progress-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
font-size: 14px;
color: #52c41a;
font-weight: 500;
}
.result-panel {
display: flex;
align-items: center;
gap: 16px;
padding: 20px 24px;
border-radius: 8px;
margin-bottom: 12px;
}
.result-success {
background: #f6ffed;
border: 1px solid #b7eb8f;
}
.result-error {
background: #fff2f0;
border: 1px solid #ffa39e;
}
.result-icon {
font-size: 36px;
line-height: 1;
}
.result-success .result-icon { color: #52c41a; }
.result-error .result-icon { color: #ff4d4f; }
.result-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 4px;
}
.result-success .result-title { color: #389e0d; }
.result-error .result-title { color: #cf1322; }
.result-desc {
font-size: 13px;
color: #606266;
}
.candidate-hint {
margin-bottom: 12px;
padding: 10px 14px;
background: #fffbe6;
border: 1px solid #ffe58f;
border-radius: 6px;
font-size: 13px;
color: #ad6800;
display: flex;
align-items: center;
gap: 6px;
}
.candidate-footer {
margin-top: 10px;
text-align: center;
font-size: 12px;
color: #999;
}
</style>