feat: 完成售后投诉流程全链路优化与功能完善
本次提交完成了多项核心优化与功能新增: 1. 重构投诉受理流程状态,移除冗余的执行反馈阶段,简化为4个流程节点 2. 新增售后单关联客户、下游用户、工程信息等扩展字段,完善基础信息采集 3. 新增部门结构化意见表单与预览组件,优化部门意见填写与展示流程 4. 新增售后意见汇总页面,支持流程阶段筛选与详情查看 5. 优化合同列表页面,新增重置筛选按钮与默认备注逻辑 6. 新增PDF导出功能,完善钢卷信息展示列 7. 修复逻辑删除级联问题,新增任务过滤逻辑保障数据一致性
This commit is contained in:
@@ -42,6 +42,30 @@ public class TsComplaintAccept extends BaseEntity {
|
||||
* 客户诉求
|
||||
*/
|
||||
private String customerDemand;
|
||||
/**
|
||||
* 关联客户ID
|
||||
*/
|
||||
private Long customerId;
|
||||
/**
|
||||
* 下游使用用户名称
|
||||
*/
|
||||
private String downstreamUserName;
|
||||
/**
|
||||
* 电话
|
||||
*/
|
||||
private String phone;
|
||||
/**
|
||||
* 使用的工程名称
|
||||
*/
|
||||
private String projectName;
|
||||
/**
|
||||
* 工程使用地点
|
||||
*/
|
||||
private String projectLocation;
|
||||
/**
|
||||
* 产品用途
|
||||
*/
|
||||
private String productUsage;
|
||||
/**
|
||||
* 客户照片凭证等
|
||||
*/
|
||||
@@ -64,7 +88,7 @@ public class TsComplaintAccept extends BaseEntity {
|
||||
private Date auditTime;
|
||||
/**
|
||||
* 流程阶段:
|
||||
1=待审核 2=部门意见填写中 3=待总负责人汇总方案 4=方案下发执行反馈中 5=全部办结
|
||||
1=待审核 2=部门意见填写中 3=待总负责人汇总方案 4=全部办结
|
||||
*/
|
||||
private Long flowStatus;
|
||||
/**
|
||||
|
||||
@@ -48,6 +48,36 @@ public class TsComplaintAcceptBo extends BaseEntity {
|
||||
*/
|
||||
private String customerDemand;
|
||||
|
||||
/**
|
||||
* 关联客户ID
|
||||
*/
|
||||
private Long customerId;
|
||||
|
||||
/**
|
||||
* 下游使用用户名称
|
||||
*/
|
||||
private String downstreamUserName;
|
||||
|
||||
/**
|
||||
* 电话
|
||||
*/
|
||||
private String phone;
|
||||
|
||||
/**
|
||||
* 使用的工程名称
|
||||
*/
|
||||
private String projectName;
|
||||
|
||||
/**
|
||||
* 工程使用地点
|
||||
*/
|
||||
private String projectLocation;
|
||||
|
||||
/**
|
||||
* 产品用途
|
||||
*/
|
||||
private String productUsage;
|
||||
|
||||
/**
|
||||
* 客户照片凭证等
|
||||
*/
|
||||
@@ -75,7 +105,7 @@ public class TsComplaintAcceptBo extends BaseEntity {
|
||||
|
||||
/**
|
||||
* 流程阶段:
|
||||
1=待审核 2=部门意见填写中 3=待总负责人汇总方案 4=方案下发执行反馈中 5=全部办结
|
||||
1=待审核 2=部门意见填写中 3=待总负责人汇总方案 4=全部办结
|
||||
*/
|
||||
private Long flowStatus;
|
||||
|
||||
|
||||
@@ -52,6 +52,42 @@ public class TsComplaintAcceptVo extends BaseEntity {
|
||||
@ExcelProperty(value = "客户诉求")
|
||||
private String customerDemand;
|
||||
|
||||
/**
|
||||
* 关联客户ID
|
||||
*/
|
||||
@ExcelProperty(value = "关联客户ID")
|
||||
private Long customerId;
|
||||
|
||||
/**
|
||||
* 下游使用用户名称
|
||||
*/
|
||||
@ExcelProperty(value = "下游使用用户名称")
|
||||
private String downstreamUserName;
|
||||
|
||||
/**
|
||||
* 电话
|
||||
*/
|
||||
@ExcelProperty(value = "电话")
|
||||
private String phone;
|
||||
|
||||
/**
|
||||
* 使用的工程名称
|
||||
*/
|
||||
@ExcelProperty(value = "使用的工程名称")
|
||||
private String projectName;
|
||||
|
||||
/**
|
||||
* 工程使用地点
|
||||
*/
|
||||
@ExcelProperty(value = "工程使用地点")
|
||||
private String projectLocation;
|
||||
|
||||
/**
|
||||
* 产品用途
|
||||
*/
|
||||
@ExcelProperty(value = "产品用途")
|
||||
private String productUsage;
|
||||
|
||||
/**
|
||||
* 客户照片凭证等
|
||||
*/
|
||||
@@ -84,7 +120,7 @@ public class TsComplaintAcceptVo extends BaseEntity {
|
||||
|
||||
/**
|
||||
* 流程阶段:
|
||||
1=待审核 2=部门意见填写中 3=待总负责人汇总方案 4=方案下发执行反馈中 5=全部办结
|
||||
1=待审核 2=部门意见填写中 3=待总负责人汇总方案 4=全部办结
|
||||
*/
|
||||
@ExcelProperty(value = "流程阶段")
|
||||
private Long flowStatus;
|
||||
|
||||
@@ -268,13 +268,27 @@ public class TsComplaintAcceptServiceImpl implements ITsComplaintAcceptService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除投诉受理单主
|
||||
* 批量删除投诉受理单主(级联删除关联的部门任务和执行反馈)
|
||||
*/
|
||||
@Override
|
||||
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
|
||||
if(isValid){
|
||||
//TODO 做一些业务上的校验,判断是否需要校验
|
||||
}
|
||||
if (ids == null || ids.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
// 级联逻辑删除关联的部门意见任务
|
||||
tsComplaintTaskMapper.delete(
|
||||
Wrappers.<TsComplaintTask>lambdaQuery()
|
||||
.in(TsComplaintTask::getAcceptId, ids)
|
||||
);
|
||||
// 级联逻辑删除关联的执行反馈记录
|
||||
tsPlanExecuteRelMapper.delete(
|
||||
Wrappers.<TsPlanExecuteRel>lambdaQuery()
|
||||
.in(TsPlanExecuteRel::getAcceptId, ids)
|
||||
);
|
||||
// 删除受理单本身
|
||||
return baseMapper.deleteBatchIds(ids) > 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,13 @@ public class TsComplaintTaskServiceImpl implements ITsComplaintTaskService {
|
||||
public TsComplaintTaskVo queryById(Long taskId){
|
||||
TsComplaintTaskVo vo = baseMapper.selectVoById(taskId);
|
||||
if (vo != null) {
|
||||
// 过滤已逻辑删除的售后单关联任务
|
||||
if (vo.getAcceptId() != null) {
|
||||
TsComplaintAcceptVo acceptVo = tsComplaintAcceptMapper.selectVoById(vo.getAcceptId());
|
||||
if (acceptVo == null) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
enrichWithAcceptInfo(Collections.singletonList(vo));
|
||||
}
|
||||
return vo;
|
||||
@@ -115,6 +122,7 @@ public class TsComplaintTaskServiceImpl implements ITsComplaintTaskService {
|
||||
lqw.eq(bo.getFillTime() != null, TsComplaintTask::getFillTime, bo.getFillTime());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getFillFile()), TsComplaintTask::getFillFile, bo.getFillFile());
|
||||
lqw.eq(bo.getRejectMark() != null, TsComplaintTask::getRejectMark, bo.getRejectMark());
|
||||
lqw.apply("accept_id IN (SELECT accept_id FROM ts_complaint_accept WHERE del_flag = 0)");
|
||||
return lqw;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,12 @@
|
||||
<result property="complaintDate" column="complaint_date"/>
|
||||
<result property="complaintContent" column="complaint_content"/>
|
||||
<result property="customerDemand" column="customer_demand"/>
|
||||
<result property="customerId" column="customer_id"/>
|
||||
<result property="downstreamUserName" column="downstream_user_name"/>
|
||||
<result property="phone" column="phone"/>
|
||||
<result property="projectName" column="project_name"/>
|
||||
<result property="projectLocation" column="project_location"/>
|
||||
<result property="productUsage" column="product_usage"/>
|
||||
<result property="file" column="file"/>
|
||||
<result property="auditStatus" column="audit_status"/>
|
||||
<result property="auditOpinion" column="audit_opinion"/>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<el-input v-model="queryParams.keyword" placeholder="请输入关键字" clearable
|
||||
@keyup.enter.native="handleQuery" style="width: 160px;" />
|
||||
<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-button icon="el-icon-sort" size="mini" @click="toggleMoreFilter"
|
||||
:type="showMoreFilter ? 'primary' : 'default'">
|
||||
</el-button>
|
||||
@@ -213,6 +214,7 @@ export default {
|
||||
},
|
||||
/** 重置按钮操作 */
|
||||
resetQuery() {
|
||||
this.queryParams.keyword = undefined;
|
||||
this.$refs["queryForm"].resetFields();
|
||||
this.queryParams.signDateStart = undefined;
|
||||
this.queryParams.signDateEnd = undefined;
|
||||
|
||||
@@ -298,7 +298,7 @@ export default {
|
||||
Object.assign(item, calculateProductFields(item, 'quantity'));
|
||||
});
|
||||
this.products = products;
|
||||
this.remark = data.remark || '净边料/毛边料、简包/裸包、卷重结算';
|
||||
this.remark = data.remark !== undefined ? data.remark : '净边料/毛边料、简包/裸包、卷重结算';
|
||||
this.productName = data.productName || '';
|
||||
} catch (error) {
|
||||
console.error('解析content失败:', error);
|
||||
|
||||
@@ -231,7 +231,6 @@
|
||||
<script>
|
||||
import { delOrder, listOrderPackaging, updateOrder, getOrder, addOrder } from "@/api/crm/order";
|
||||
import { addCustomer } from "@/api/crm/customer";
|
||||
import { getDicts, addData, updateData } from "@/api/system/dict/data";
|
||||
import { listDeliveryWaybill } from "@/api/wms/deliveryWaybill";
|
||||
import dayjs from "dayjs";
|
||||
import ContractList from "./components/ContractList.vue";
|
||||
|
||||
@@ -1,13 +1,63 @@
|
||||
<template>
|
||||
<div v-if="enabled" class="section-container">
|
||||
<div class="detail-section">
|
||||
<div class="detail-section-label">Complaint Content · 投诉情况</div>
|
||||
<div class="detail-section-label">Complaint Content · 投诉情况与客户诉求</div>
|
||||
<div class="detail-section-text">{{ data.complaintContent || '-' }}</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section" v-if="customerInfo">
|
||||
<div class="detail-section-label">Customer · 关联客户</div>
|
||||
<div class="customer-info-card">
|
||||
<div class="customer-row">
|
||||
<span class="customer-row-label">公司名称</span>
|
||||
<span class="customer-row-value">{{ customerInfo.companyName || '-' }}</span>
|
||||
</div>
|
||||
<div class="customer-row">
|
||||
<span class="customer-row-label">联系人</span>
|
||||
<span class="customer-row-value">{{ customerInfo.contactPerson || '-' }}</span>
|
||||
</div>
|
||||
<div class="customer-row">
|
||||
<span class="customer-row-label">联系方式</span>
|
||||
<span class="customer-row-value">{{ customerInfo.contactWay || '-' }}</span>
|
||||
</div>
|
||||
<div class="customer-row">
|
||||
<span class="customer-row-label">客户地址</span>
|
||||
<span class="customer-row-value">{{ customerInfo.address || '-' }}</span>
|
||||
</div>
|
||||
<div class="customer-row">
|
||||
<span class="customer-row-label">客户等级</span>
|
||||
<span class="customer-row-value">{{ customerInfo.customerLevel || '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-section" v-else>
|
||||
<div class="detail-section-label">Customer · 关联客户</div>
|
||||
<div class="detail-section-text">-</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<div class="detail-section-label">Customer Demand · 客户诉求</div>
|
||||
<div class="detail-section-text">{{ data.customerDemand || '-' }}</div>
|
||||
<div class="detail-section-label">Downstream User · 下游使用用户</div>
|
||||
<div class="detail-section-text">{{ data.downstreamUserName || '-' }}</div>
|
||||
</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>
|
||||
|
||||
<div class="detail-section" v-if="data.file">
|
||||
@@ -19,6 +69,7 @@
|
||||
|
||||
<script>
|
||||
import FileList from "@/components/FileList/index.vue";
|
||||
import { getCustomer } from "@/api/crm/customer";
|
||||
|
||||
export default {
|
||||
name: 'BasicInfoSection',
|
||||
@@ -32,6 +83,27 @@ export default {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
customerInfo: null
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
'data.customerId': {
|
||||
immediate: true,
|
||||
handler(val) {
|
||||
if (val) {
|
||||
getCustomer(val).then(response => {
|
||||
this.customerInfo = response.data || null;
|
||||
}).catch(() => {
|
||||
this.customerInfo = null;
|
||||
});
|
||||
} else {
|
||||
this.customerInfo = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -59,4 +131,38 @@ export default {
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #eeeae4;
|
||||
}
|
||||
|
||||
/* ===== 客户信息卡片 ===== */
|
||||
.customer-info-card {
|
||||
padding: 10px 14px;
|
||||
background: #faf8f5;
|
||||
border: 1px solid #e8e4de;
|
||||
border-radius: 2px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.customer-row {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
padding: 3px 0;
|
||||
}
|
||||
|
||||
.customer-row + .customer-row {
|
||||
border-top: 1px solid #f0ece6;
|
||||
}
|
||||
|
||||
.customer-row-label {
|
||||
flex-shrink: 0;
|
||||
width: 70px;
|
||||
font-family: 'Georgia', 'Times New Roman', serif;
|
||||
font-size: 11px;
|
||||
color: #8c8c8c;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.customer-row-value {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -11,15 +11,18 @@
|
||||
<el-table-column label="厂家卷号" align="center" prop="coilInfo.supplierCoilNo" width="140" show-overflow-tooltip />
|
||||
<el-table-column label="物料名称" align="center" prop="coilInfo.itemName" width="100" />
|
||||
<el-table-column label="规格" align="center" prop="coilInfo.specification" width="100" />
|
||||
<el-table-column label="制造时间" align="center" prop="coilInfo.createTime" width="90" />
|
||||
<el-table-column label="发货时间" align="center" prop="coilInfo.exportTime" width="90" />
|
||||
<el-table-column label="发货人" align="center" prop="coilInfo.exportBy" width="90" />
|
||||
<el-table-column label="材质" align="center" prop="coilInfo.material" width="100" />
|
||||
<el-table-column label="厂家" align="center" prop="coilInfo.manufacturer" width="90" />
|
||||
<el-table-column label="厂家" align="center" prop="coilInfo.manufacturer" width="60" />
|
||||
<el-table-column label="净重(t)" align="center" prop="coilInfo.netWeight" width="80" />
|
||||
<el-table-column label="质量状态" align="center" prop="coilInfo.qualityStatus" width="90">
|
||||
<el-table-column label="品质" align="center" prop="coilInfo.qualityStatus" width="60">
|
||||
<template slot-scope="scope">
|
||||
<dict-tag v-if="scope.row.coilInfo.qualityStatus" :options="dict.type.coil_quality_status" :value="scope.row.coilInfo.qualityStatus"></dict-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" align="center" prop="coilInfo.remark" width="140" show-overflow-tooltip />
|
||||
<el-table-column label="备注" align="center" prop="coilInfo.remark" show-overflow-tooltip />
|
||||
<el-table-column v-if="editable" label="操作" align="center" width="70" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" icon="el-icon-delete" @click="$emit('remove', scope.row)"></el-button>
|
||||
|
||||
@@ -17,8 +17,13 @@
|
||||
<el-tag v-else-if="item.taskStatus === 1" type="success" size="mini">已完成</el-tag>
|
||||
</div>
|
||||
<div class="opinion-card-body">
|
||||
<div v-if="item.deptOpinion" class="opinion-content" v-html="item.deptOpinion"></div>
|
||||
<div v-else class="opinion-empty">— No opinion yet · 暂无意见 —</div>
|
||||
<template v-if="isJsonOpinion(item.deptOpinion)">
|
||||
<DeptOpinionPreview :deptId="item.deptId" :jsonData="item.deptOpinion" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<div v-if="item.deptOpinion" class="opinion-content" v-html="item.deptOpinion"></div>
|
||||
<div v-else class="opinion-empty">— No opinion yet · 暂无意见 —</div>
|
||||
</template>
|
||||
<div v-if="item.fillFile" class="opinion-file">
|
||||
<FileList :ossIds="item.fillFile" />
|
||||
</div>
|
||||
@@ -35,10 +40,11 @@
|
||||
|
||||
<script>
|
||||
import FileList from "@/components/FileList/index.vue";
|
||||
import DeptOpinionPreview from "./DeptOpinionPreview.vue";
|
||||
|
||||
export default {
|
||||
name: 'DepartmentOpinionSection',
|
||||
components: { FileList },
|
||||
components: { FileList, DeptOpinionPreview },
|
||||
props: {
|
||||
enabled: {
|
||||
type: Boolean,
|
||||
@@ -50,6 +56,15 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isJsonOpinion(str) {
|
||||
if (!str) return false;
|
||||
try {
|
||||
const obj = JSON.parse(str);
|
||||
return obj && typeof obj === 'object' && !Array.isArray(obj);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
getDeptName(deptId) {
|
||||
const map = { 1: '生产部', 2: '质量部', 3: '销售部' };
|
||||
return map[deptId] || '部门' + deptId;
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<div class="dept-form-container">
|
||||
<!-- 生产部 deptId=1 -->
|
||||
<template v-if="deptId === 1">
|
||||
<el-form-item label="内部调查情况" prop="internalInvestigation">
|
||||
<el-input v-model="formData.internalInvestigation" type="textarea" :rows="3" placeholder="请描述内部调查情况" @input="emitJson" />
|
||||
</el-form-item>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="确认人" prop="internalConfirmer">
|
||||
<el-input v-model="formData.internalConfirmer" placeholder="请输入确认人" @input="emitJson" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="日期" prop="internalConfirmDate">
|
||||
<el-date-picker v-model="formData.internalConfirmDate" type="date" value-format="yyyy-MM-dd" placeholder="选择日期" style="width:100%" @change="emitJson" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="外部调查情况" prop="externalInvestigation">
|
||||
<el-input v-model="formData.externalInvestigation" type="textarea" :rows="3" placeholder="请描述外部调查情况" @input="emitJson" />
|
||||
</el-form-item>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="确认人" prop="externalConfirmer">
|
||||
<el-input v-model="formData.externalConfirmer" placeholder="请输入确认人" @input="emitJson" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="日期" prop="externalConfirmDate">
|
||||
<el-date-picker v-model="formData.externalConfirmDate" type="date" value-format="yyyy-MM-dd" placeholder="选择日期" style="width:100%" @change="emitJson" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="责任单位" prop="responsibleUnit">
|
||||
<el-input v-model="formData.responsibleUnit" placeholder="请输入责任单位" @input="emitJson" />
|
||||
</el-form-item>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="接收人" prop="receiver">
|
||||
<el-input v-model="formData.receiver" placeholder="请输入接收人" @input="emitJson" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="接受日期" prop="acceptDate">
|
||||
<el-date-picker v-model="formData.acceptDate" type="date" value-format="yyyy-MM-dd" placeholder="选择日期" style="width:100%" @change="emitJson" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<!-- 质量部 deptId=2 -->
|
||||
<template v-else-if="deptId === 2">
|
||||
<el-form-item label="产生原因" prop="cause">
|
||||
<el-input v-model="formData.cause" type="textarea" :rows="3" placeholder="请描述产生原因" @input="emitJson" />
|
||||
</el-form-item>
|
||||
<el-form-item label="流出原因" prop="escapeReason">
|
||||
<el-input v-model="formData.escapeReason" type="textarea" :rows="3" placeholder="请描述流出原因" @input="emitJson" />
|
||||
</el-form-item>
|
||||
<el-form-item label="纠正措施" prop="correctiveAction">
|
||||
<el-input v-model="formData.correctiveAction" type="textarea" :rows="3" placeholder="请输入纠正措施" @input="emitJson" />
|
||||
</el-form-item>
|
||||
<el-form-item label="预计整改完成日期" prop="rectifyDate">
|
||||
<el-date-picker v-model="formData.rectifyDate" type="date" value-format="yyyy-MM-dd" placeholder="选择日期" style="width:100%" @change="emitJson" />
|
||||
</el-form-item>
|
||||
<el-form-item label="主管意见" prop="supervisorOpinion">
|
||||
<el-input v-model="formData.supervisorOpinion" type="textarea" :rows="2" placeholder="请输入主管意见" @input="emitJson" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<!-- 销售部 deptId=3 -->
|
||||
<template v-else>
|
||||
<el-form-item label="处理意见" prop="handlingOpinion">
|
||||
<el-input v-model="formData.handlingOpinion" type="textarea" :rows="4" placeholder="请输入处理意见" @input="emitJson" />
|
||||
</el-form-item>
|
||||
<el-form-item label="领导意见" prop="leaderOpinion">
|
||||
<el-input v-model="formData.leaderOpinion" type="textarea" :rows="3" placeholder="请输入领导意见" @input="emitJson" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'DeptOpinionForm',
|
||||
props: {
|
||||
deptId: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
jsonData: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
formData: {}
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
jsonData: {
|
||||
immediate: true,
|
||||
handler(val) {
|
||||
this.parseJson(val);
|
||||
}
|
||||
},
|
||||
deptId() {
|
||||
this.parseJson(this.jsonData);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
parseJson(val) {
|
||||
const defaults = this.getDefaults();
|
||||
if (val) {
|
||||
try {
|
||||
const parsed = JSON.parse(val);
|
||||
this.formData = { ...defaults, ...parsed };
|
||||
} catch (e) {
|
||||
this.formData = { ...defaults };
|
||||
}
|
||||
} else {
|
||||
this.formData = { ...defaults };
|
||||
}
|
||||
},
|
||||
getDefaults() {
|
||||
if (this.deptId === 1) {
|
||||
return { internalInvestigation: '', internalConfirmer: '', internalConfirmDate: '', externalInvestigation: '', externalConfirmer: '', externalConfirmDate: '', responsibleUnit: '', receiver: '', acceptDate: '' };
|
||||
} else if (this.deptId === 2) {
|
||||
return { cause: '', escapeReason: '', correctiveAction: '', rectifyDate: '', supervisorOpinion: '' };
|
||||
} else {
|
||||
return { handlingOpinion: '', leaderOpinion: '' };
|
||||
}
|
||||
},
|
||||
emitJson() {
|
||||
this.$emit('input', JSON.stringify(this.formData));
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dept-form-container {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,137 @@
|
||||
<template>
|
||||
<div class="dept-preview-container">
|
||||
<template v-if="parsedData">
|
||||
<!-- 生产部 deptId=1 -->
|
||||
<template v-if="deptId === 1">
|
||||
<div class="preview-row" v-if="parsedData.internalInvestigation">
|
||||
<span class="preview-label">内部调查情况</span>
|
||||
<span class="preview-value">{{ parsedData.internalInvestigation }}</span>
|
||||
</div>
|
||||
<div class="preview-row" v-if="parsedData.internalConfirmer || parsedData.internalConfirmDate">
|
||||
<span class="preview-label">内部确认</span>
|
||||
<span class="preview-value">{{ parsedData.internalConfirmer || '-' }} / {{ parsedData.internalConfirmDate || '-' }}</span>
|
||||
</div>
|
||||
<div class="preview-row" v-if="parsedData.externalInvestigation">
|
||||
<span class="preview-label">外部调查情况</span>
|
||||
<span class="preview-value">{{ parsedData.externalInvestigation }}</span>
|
||||
</div>
|
||||
<div class="preview-row" v-if="parsedData.externalConfirmer || parsedData.externalConfirmDate">
|
||||
<span class="preview-label">外部确认</span>
|
||||
<span class="preview-value">{{ parsedData.externalConfirmer || '-' }} / {{ parsedData.externalConfirmDate || '-' }}</span>
|
||||
</div>
|
||||
<div class="preview-row" v-if="parsedData.responsibleUnit">
|
||||
<span class="preview-label">责任单位</span>
|
||||
<span class="preview-value">{{ parsedData.responsibleUnit }}</span>
|
||||
</div>
|
||||
<div class="preview-row" v-if="parsedData.receiver || parsedData.acceptDate">
|
||||
<span class="preview-label">接收</span>
|
||||
<span class="preview-value">{{ parsedData.receiver || '-' }} / {{ parsedData.acceptDate || '-' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 质量部 deptId=2 -->
|
||||
<template v-else-if="deptId === 2">
|
||||
<div class="preview-row" v-if="parsedData.cause">
|
||||
<span class="preview-label">产生原因</span>
|
||||
<span class="preview-value">{{ parsedData.cause }}</span>
|
||||
</div>
|
||||
<div class="preview-row" v-if="parsedData.escapeReason">
|
||||
<span class="preview-label">流出原因</span>
|
||||
<span class="preview-value">{{ parsedData.escapeReason }}</span>
|
||||
</div>
|
||||
<div class="preview-row" v-if="parsedData.correctiveAction">
|
||||
<span class="preview-label">纠正措施</span>
|
||||
<span class="preview-value">{{ parsedData.correctiveAction }}</span>
|
||||
</div>
|
||||
<div class="preview-row" v-if="parsedData.rectifyDate">
|
||||
<span class="preview-label">预计整改日期</span>
|
||||
<span class="preview-value">{{ parsedData.rectifyDate }}</span>
|
||||
</div>
|
||||
<div class="preview-row" v-if="parsedData.supervisorOpinion">
|
||||
<span class="preview-label">主管意见</span>
|
||||
<span class="preview-value">{{ parsedData.supervisorOpinion }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 销售部 deptId=3 -->
|
||||
<template v-else>
|
||||
<div class="preview-row" v-if="parsedData.handlingOpinion">
|
||||
<span class="preview-label">处理意见</span>
|
||||
<span class="preview-value">{{ parsedData.handlingOpinion }}</span>
|
||||
</div>
|
||||
<div class="preview-row" v-if="parsedData.leaderOpinion">
|
||||
<span class="preview-label">领导意见</span>
|
||||
<span class="preview-value">{{ parsedData.leaderOpinion }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<div v-else class="preview-empty">— No data · 暂无数据 —</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'DeptOpinionPreview',
|
||||
props: {
|
||||
deptId: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
jsonData: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
parsedData() {
|
||||
if (!this.jsonData) return null;
|
||||
try {
|
||||
return JSON.parse(this.jsonData);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dept-preview-container {
|
||||
font-size: 13px;
|
||||
color: #3a3a3a;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.preview-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 4px 0;
|
||||
border-bottom: 1px solid #f0ece6;
|
||||
}
|
||||
|
||||
.preview-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.preview-label {
|
||||
flex-shrink: 0;
|
||||
width: 100px;
|
||||
font-family: 'Georgia', 'Times New Roman', serif;
|
||||
font-size: 11px;
|
||||
color: #8c8c8c;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.preview-value {
|
||||
flex: 1;
|
||||
color: #1a1a1a;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.preview-empty {
|
||||
color: #bab5ae;
|
||||
font-size: 12px;
|
||||
font-style: italic;
|
||||
font-family: 'Georgia', 'Times New Roman', serif;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,621 @@
|
||||
<template>
|
||||
<el-dialog title="导出质量异议资料" :visible.sync="visible" width="90%" top="3vh" append-to-body destroy-on-close
|
||||
:close-on-click-modal="false" @close="handleClose">
|
||||
<div v-loading="loading" class="export-preview">
|
||||
<div ref="pdfContent" class="pdf-content">
|
||||
<!-- ========== 附件一:质量异议反馈单 ========== -->
|
||||
<div class="attachment-section">
|
||||
<h2 class="doc-title">质量异议反馈单</h2>
|
||||
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<td class="label" style="width:120px">异议反馈单位</td>
|
||||
<td colspan="5">{{ customer.companyName || '' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">联系人</td>
|
||||
<td style="width:180px">{{ customer.contactPerson || '' }}</td>
|
||||
<td class="label" style="width:100px">联系电话</td>
|
||||
<td colspan="3">{{ customer.contactWay || '' }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table class="form-table" style="margin-top:-1px">
|
||||
<tr>
|
||||
<td class="label" style="width:60px;vertical-align:middle" :rowspan="8">情况概述<br>(用户提供)</td>
|
||||
<td class="label" style="width:120px">产品名称</td>
|
||||
<td style="width:180px">{{ firstCoil.itemName || '' }}</td>
|
||||
<td class="label" style="width:110px">异议合同号</td>
|
||||
<td>{{ firstContract.contractCode || '' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">合同交货量</td>
|
||||
<td>{{ firstContract.orderAmount || '' }}</td>
|
||||
<td class="label">牌号/钢种</td>
|
||||
<td>{{ firstCoil.material || '' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">规格</td>
|
||||
<td>{{ firstCoil.specification || '' }}</td>
|
||||
<td class="label">异议量</td>
|
||||
<td>{{ coilWeightSummary }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">采购日期</td>
|
||||
<td>{{ detail.complaintDate | formatDate }}</td>
|
||||
<td class="label">使用日期</td>
|
||||
<td>{{ firstCoil.exportTime | formatDate }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">产品卷号</td>
|
||||
<td colspan="3">{{ coilNoSummary }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">产品生产日期</td>
|
||||
<td colspan="3">{{ firstCoil.createTime | formatDate }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">下游使用用户名称</td>
|
||||
<td>{{ detail.downstreamUserName || '' }}</td>
|
||||
<td class="label">电话</td>
|
||||
<td>{{ detail.phone || '' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">使用的工程名称</td>
|
||||
<td>{{ detail.projectName || '' }}</td>
|
||||
<td class="label">工程使用地点</td>
|
||||
<td>{{ detail.projectLocation || '' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label" style="vertical-align:middle">产品用途</td>
|
||||
<td colspan="3">{{ detail.productUsage || '' }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="complaint-desc">
|
||||
<div class="desc-label">产品异议描述:(缺陷描述、缺陷程度、缺陷部位、缺陷数量、发生缺陷的生产/使用工序、特殊说明等等)</div>
|
||||
<div class="desc-content">{{ detail.complaintContent || '' }}</div>
|
||||
</div>
|
||||
|
||||
<div class="stamp-area">
|
||||
<div>单位盖章:</div>
|
||||
<div style="margin-top:20px">日期:</div>
|
||||
</div>
|
||||
|
||||
<div class="company-section">
|
||||
<div class="company-header">科伦普公司填写</div>
|
||||
<table class="form-table" style="margin-top:0">
|
||||
<tr>
|
||||
<td class="label" style="width:120px">异议提交人</td>
|
||||
<td>{{ currentUser }}</td>
|
||||
<td class="label" style="width:80px">日期</td>
|
||||
<td>{{ today }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========== 附件二:质量投诉立案、确认及处置单 ========== -->
|
||||
<div class="page-break"></div>
|
||||
<div class="attachment-section">
|
||||
<h2 class="doc-title">附件 2</h2>
|
||||
<h3 class="sub-title">质量投诉立案、确认及处置单</h3>
|
||||
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<td class="label" style="width:120px">收到日期</td>
|
||||
<td style="width:180px">{{ detail.complaintDate | formatDate }}</td>
|
||||
<td class="label" style="width:100px">投诉单编号</td>
|
||||
<td>{{ detail.complaintNo || '' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">异议反馈单位</td>
|
||||
<td>{{ customer.companyName || '' }}</td>
|
||||
<td class="label">反馈人姓名</td>
|
||||
<td>{{ customer.contactPerson || '' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">信息接收人</td>
|
||||
<td>{{ detail.auditOpinion || '' }}</td>
|
||||
<td class="label">联系方式</td>
|
||||
<td>{{ customer.contactWay || '' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">订货用户及使用<br>用户名称及地址</td>
|
||||
<td colspan="3">{{ customer.companyName || '' }} {{ customer.address || '' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">产品名称</td>
|
||||
<td>{{ firstCoil.itemName || '' }}</td>
|
||||
<td class="label">合同号</td>
|
||||
<td>{{ firstContract.contractCode || '' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">合同交货量</td>
|
||||
<td>{{ firstContract.orderAmount || '' }}</td>
|
||||
<td class="label">未出库量</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">规格(mm)</td>
|
||||
<td>{{ firstCoil.specification || '' }}</td>
|
||||
<td class="label">产品钢种/牌号</td>
|
||||
<td>{{ firstCoil.material || '' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">异议量</td>
|
||||
<td>{{ coilWeightSummary }}</td>
|
||||
<td class="label">产品用途</td>
|
||||
<td>{{ detail.productUsage || '' }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="complaint-desc">
|
||||
<div class="desc-label">问题描述:(产品卷号、产品生产日期、缺陷名称、缺陷描述、缺陷程度、缺陷数量、发生问题的生产工序等以及附加缺陷照片)</div>
|
||||
<div class="desc-content">客户反馈:{{ detail.complaintContent || '' }}</div>
|
||||
</div>
|
||||
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<td class="label" style="width:100px;vertical-align:middle" :rowspan="2">问题内/<br>外部调<br>查、核<br>实、确认</td>
|
||||
<td class="label" style="width:60px">内部</td>
|
||||
<td>
|
||||
<div>{{ prodOpinion.internalInvestigation || '' }}</div>
|
||||
<div style="text-align:right;margin-top:8px">
|
||||
确认人:{{ prodOpinion.internalConfirmer || '' }} 日期:{{ prodOpinion.internalConfirmDate || '' }}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">外部</td>
|
||||
<td>
|
||||
<div>{{ prodOpinion.externalInvestigation || '' }}</div>
|
||||
<div style="text-align:right;margin-top:8px">
|
||||
确认人:{{ prodOpinion.externalConfirmer || '' }} 日期:{{ prodOpinion.externalConfirmDate || '' }}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table class="form-table" style="margin-top:-1px">
|
||||
<tr>
|
||||
<td class="label" style="width:100px">责任单位</td>
|
||||
<td style="width:200px">{{ prodOpinion.responsibleUnit || '' }}</td>
|
||||
<td class="label" style="width:100px">接收人/日期</td>
|
||||
<td>{{ prodOpinion.receiver || '' }} {{ prodOpinion.acceptDate || '' }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table class="form-table" style="margin-top:-1px">
|
||||
<tr>
|
||||
<td class="label" style="width:100px;vertical-align:middle" :rowspan="2">原因分析</td>
|
||||
<td class="label" style="width:70px">产生原因</td>
|
||||
<td>{{ qualityOpinion.cause || '' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">流出原因</td>
|
||||
<td>{{ qualityOpinion.escapeReason || '' }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table class="form-table" style="margin-top:-1px">
|
||||
<tr>
|
||||
<td class="label" style="width:100px">纠正/预防措施</td>
|
||||
<td>{{ qualityOpinion.correctiveAction || '' }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table class="form-table" style="margin-top:-1px">
|
||||
<tr>
|
||||
<td class="label" style="width:100px">整改计划完成时间</td>
|
||||
<td style="width:200px">{{ qualityOpinion.rectifyDate || '' }}</td>
|
||||
<td class="label" style="width:100px">责任人签字/日期</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table class="form-table" style="margin-top:-1px">
|
||||
<tr>
|
||||
<td class="label" style="width:120px">品质部主管审核</td>
|
||||
<td>{{ qualityOpinion.supervisorOpinion || '' }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table class="form-table" style="margin-top:-1px">
|
||||
<tr>
|
||||
<td class="label" style="width:70px;vertical-align:middle">意见</td>
|
||||
<td>
|
||||
<div style="text-align:right">签字:{{ qualityOpinion.internalConfirmer || '' }} 日期:{{ qualityOpinion.internalConfirmDate || '' }}</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label" style="vertical-align:middle">商务<br>处置</td>
|
||||
<td>
|
||||
<table class="inner-table">
|
||||
<tr>
|
||||
<td class="label" style="width:100px">销售人员处<br>意见</td>
|
||||
<td>{{ salesOpinion.handlingOpinion || '' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">销售部领导<br>审核意见</td>
|
||||
<td>{{ salesOpinion.leaderOpinion || '' }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label" style="vertical-align:middle">公司领导审批意<br>见</td>
|
||||
<td>
|
||||
<div style="min-height:40px">{{ detail.planContent || '' }}</div>
|
||||
<div style="text-align:right;margin-top:8px">签字: 日期:</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- ========== 附件三:缺陷图片 ========== -->
|
||||
<div class="page-break"></div>
|
||||
<div class="attachment-section">
|
||||
<h2 class="doc-title">附缺陷照片:</h2>
|
||||
<div v-if="defectImages.length > 0" class="defect-grid">
|
||||
<div v-for="(img, idx) in defectImages" :key="idx" class="defect-item">
|
||||
<img :src="img" class="defect-img" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-tip">暂无缺陷照片</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="visible = false">取 消</el-button>
|
||||
<el-button type="primary" :loading="exporting" @click="exportPdf">
|
||||
<i class="el-icon-download"></i> 导出PDF
|
||||
</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getComplaintAccept } from "@/api/flow/complaintAccept";
|
||||
import { listComplaintTask } from "@/api/flow/complaintTask";
|
||||
import { listAcceptCoilRel } from "@/api/flow/acceptCoilRel";
|
||||
import { getCustomer } from "@/api/crm/customer";
|
||||
import { listByIds } from "@/api/system/oss";
|
||||
import html2canvas from 'html2canvas';
|
||||
import jsPDF from 'jspdf';
|
||||
|
||||
export default {
|
||||
name: 'ExportPdfDialog',
|
||||
filters: {
|
||||
formatDate(val) {
|
||||
if (!val) return '';
|
||||
const d = new Date(val);
|
||||
if (isNaN(d.getTime())) return val;
|
||||
const y = d.getFullYear();
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
return `${y}.${m}.${day}`;
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
loading: false,
|
||||
exporting: false,
|
||||
acceptId: null,
|
||||
detail: {},
|
||||
customer: {},
|
||||
coilList: [],
|
||||
taskList: [],
|
||||
defectImages: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
currentUser() {
|
||||
return this.$store.state.user.name || '';
|
||||
},
|
||||
today() {
|
||||
const d = new Date();
|
||||
const y = d.getFullYear();
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
return `${y}.${m}.${day}`;
|
||||
},
|
||||
firstCoil() {
|
||||
if (this.coilList.length === 0) return {};
|
||||
const rel = this.coilList[0];
|
||||
return rel.coilInfo || {};
|
||||
},
|
||||
firstContract() {
|
||||
if (this.coilList.length === 0) return {};
|
||||
const rel = this.coilList[0];
|
||||
const orders = (rel.coilInfo && rel.coilInfo.orderList) || [];
|
||||
return orders[0] || {};
|
||||
},
|
||||
coilNoSummary() {
|
||||
return this.coilList
|
||||
.map(rel => (rel.coilInfo && rel.coilInfo.currentCoilNo) || '')
|
||||
.filter(Boolean)
|
||||
.join('、');
|
||||
},
|
||||
coilWeightSummary() {
|
||||
const weights = this.coilList
|
||||
.map(rel => parseFloat((rel.coilInfo && rel.coilInfo.netWeight) || 0))
|
||||
.filter(w => w > 0);
|
||||
if (weights.length === 0) return '';
|
||||
const total = weights.reduce((a, b) => a + b, 0);
|
||||
return total.toFixed(3) + 't';
|
||||
},
|
||||
prodOpinion() {
|
||||
return this.parseDeptOpinion(1);
|
||||
},
|
||||
qualityOpinion() {
|
||||
return this.parseDeptOpinion(2);
|
||||
},
|
||||
salesOpinion() {
|
||||
return this.parseDeptOpinion(3);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open(acceptId) {
|
||||
this.acceptId = acceptId;
|
||||
this.visible = true;
|
||||
this.loadData();
|
||||
},
|
||||
async loadData() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const [detailRes, taskRes, coilRes] = await Promise.all([
|
||||
getComplaintAccept(this.acceptId),
|
||||
listComplaintTask({ acceptId: this.acceptId, pageNum: 1, pageSize: 999 }),
|
||||
listAcceptCoilRel({ acceptId: this.acceptId, pageNum: 1, pageSize: 999 })
|
||||
]);
|
||||
this.detail = detailRes.data || {};
|
||||
this.taskList = (taskRes.rows || []).filter(t => t.rejectMark !== 1);
|
||||
this.coilList = coilRes.rows || [];
|
||||
|
||||
if (this.detail.customerId) {
|
||||
try {
|
||||
const custRes = await getCustomer(this.detail.customerId);
|
||||
this.customer = custRes.data || {};
|
||||
} catch (e) {
|
||||
this.customer = {};
|
||||
}
|
||||
}
|
||||
|
||||
if (this.detail.file) {
|
||||
try {
|
||||
const fileRes = await listByIds(this.detail.file);
|
||||
const files = fileRes.data || [];
|
||||
this.defectImages = files.map(f => f.url).filter(Boolean);
|
||||
} catch (e) {
|
||||
this.defectImages = [];
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载导出数据失败:', e);
|
||||
this.$modal.msgError('加载数据失败');
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
parseDeptOpinion(deptId) {
|
||||
const task = this.taskList.find(t => t.deptId === deptId);
|
||||
if (!task || !task.deptOpinion) return {};
|
||||
try {
|
||||
const obj = JSON.parse(task.deptOpinion);
|
||||
return obj && typeof obj === 'object' ? obj : {};
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
handleClose() {
|
||||
this.detail = {};
|
||||
this.customer = {};
|
||||
this.coilList = [];
|
||||
this.taskList = [];
|
||||
this.defectImages = [];
|
||||
},
|
||||
async exportPdf() {
|
||||
const element = this.$refs.pdfContent;
|
||||
if (!element) return;
|
||||
this.exporting = true;
|
||||
try {
|
||||
const canvas = await html2canvas(element, {
|
||||
scale: 2,
|
||||
useCORS: true,
|
||||
logging: false,
|
||||
backgroundColor: '#ffffff',
|
||||
windowWidth: 794
|
||||
});
|
||||
const imgData = canvas.toDataURL('image/png');
|
||||
const pdf = new jsPDF('p', 'mm', 'a4');
|
||||
const pdfWidth = pdf.internal.pageSize.getWidth();
|
||||
const pageHeight = pdf.internal.pageSize.getHeight();
|
||||
const imgWidth = pdfWidth;
|
||||
const imgHeight = (canvas.height * imgWidth) / canvas.width;
|
||||
|
||||
if (imgHeight <= pageHeight) {
|
||||
pdf.addImage(imgData, 'PNG', 0, 0, imgWidth, imgHeight);
|
||||
} else {
|
||||
let posY = 0;
|
||||
const ratio = pdfWidth / canvas.width;
|
||||
const pageCanvasHeight = pageHeight / ratio;
|
||||
while (posY < canvas.height) {
|
||||
if (posY > 0) pdf.addPage();
|
||||
const pieceCanvas = document.createElement('canvas');
|
||||
pieceCanvas.width = canvas.width;
|
||||
pieceCanvas.height = Math.min(pageCanvasHeight, canvas.height - posY);
|
||||
const ctx = pieceCanvas.getContext('2d');
|
||||
ctx.drawImage(canvas, 0, posY, canvas.width, pieceCanvas.height, 0, 0, canvas.width, pieceCanvas.height);
|
||||
const pieceData = pieceCanvas.toDataURL('image/png');
|
||||
pdf.addImage(pieceData, 'PNG', 0, 0, imgWidth, pieceCanvas.height * ratio);
|
||||
posY += pageCanvasHeight;
|
||||
}
|
||||
}
|
||||
pdf.save(`${this.detail.complaintNo || '售后单'}_质量异议全套资料.pdf`);
|
||||
this.$modal.msgSuccess('PDF导出成功');
|
||||
this.visible = false;
|
||||
} catch (err) {
|
||||
console.error('PDF导出失败:', err);
|
||||
this.$modal.msgError('PDF导出失败');
|
||||
} finally {
|
||||
this.exporting = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.export-preview {
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
background: #f5f5f5;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.pdf-content {
|
||||
width: 794px;
|
||||
margin: 0 auto;
|
||||
background: #ffffff;
|
||||
padding: 40px 50px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.attachment-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-break {
|
||||
page-break-after: always;
|
||||
break-after: page;
|
||||
height: 1px;
|
||||
margin: 20px 0;
|
||||
border-top: 1px dashed #ccc;
|
||||
}
|
||||
|
||||
.doc-title {
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
margin: 0 0 16px 0;
|
||||
font-family: 'SimSun', 'Noto Serif SC', serif;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
margin: 0 0 16px 0;
|
||||
font-family: 'SimSun', 'Noto Serif SC', serif;
|
||||
}
|
||||
|
||||
.form-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
border: 1px solid #000;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.form-table td {
|
||||
border: 1px solid #000;
|
||||
padding: 6px 8px;
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.form-table .label {
|
||||
background-color: #f5f5f5;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.inner-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.inner-table td {
|
||||
border: 1px solid #000;
|
||||
padding: 6px 8px;
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.complaint-desc {
|
||||
border: 1px solid #000;
|
||||
border-top: none;
|
||||
padding: 10px 12px;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
.desc-label {
|
||||
font-size: 12px;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.desc-content {
|
||||
font-size: 13px;
|
||||
line-height: 1.8;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.stamp-area {
|
||||
border: 1px solid #000;
|
||||
border-top: none;
|
||||
padding: 16px 12px;
|
||||
min-height: 60px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.company-section {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.company-header {
|
||||
background-color: #f5f5f5;
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
padding: 8px;
|
||||
border: 1px solid #000;
|
||||
}
|
||||
|
||||
.defect-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.defect-item {
|
||||
flex: 0 0 calc(50% - 6px);
|
||||
border: 1px solid #ddd;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.defect-img {
|
||||
width: 100%;
|
||||
display: block;
|
||||
object-fit: contain;
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
padding: 40px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
@@ -8,8 +8,6 @@
|
||||
<el-step title="待审核" icon="el-icon-document" />
|
||||
<el-step title="意见填写" icon="el-icon-edit-outline" />
|
||||
<el-step title="汇总方案" icon="el-icon-s-data" />
|
||||
<el-step title="执行反馈" icon="el-icon-s-promotion" />
|
||||
<el-step title="执行完成" icon="el-icon-success" />
|
||||
<el-step title="全部办结" icon="el-icon-circle-check" />
|
||||
</el-steps>
|
||||
<div class="current-status">
|
||||
@@ -34,15 +32,13 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
/**
|
||||
* el-steps 的 active 属性是从 0 开始索引的。
|
||||
* 当 status=1 时 active=0(待审核高亮),
|
||||
* status=2 时 active=1(已完成待审核+意见填写中高亮),依此类推。
|
||||
* status=5(执行完成)和 status=6(全部办结)共享第5步作为已完成状态。
|
||||
* el-steps active 从 0 开始。
|
||||
* 1=待审核→0, 2=意见填写→1, 3=汇总方案→2, 4=全部办结→3
|
||||
*/
|
||||
activeStep() {
|
||||
if (this.flowStatus == null) return -1;
|
||||
const v = Number(this.flowStatus);
|
||||
if (v >= 6) return 5; // 全部办结 -> 第5步已完成
|
||||
if (v >= 4) return 3;
|
||||
return v - 1;
|
||||
},
|
||||
flowStatusText() {
|
||||
@@ -50,9 +46,7 @@ export default {
|
||||
1: '待审核',
|
||||
2: '意见填写中',
|
||||
3: '待汇总方案',
|
||||
4: '执行反馈中',
|
||||
5: '执行完成',
|
||||
6: '全部办结'
|
||||
4: '全部办结'
|
||||
};
|
||||
return map[this.flowStatus] || '未知';
|
||||
},
|
||||
@@ -61,9 +55,7 @@ export default {
|
||||
1: 'info',
|
||||
2: 'warning',
|
||||
3: '',
|
||||
4: 'warning',
|
||||
5: 'success',
|
||||
6: 'success'
|
||||
4: 'success'
|
||||
};
|
||||
return map[this.flowStatus] || '';
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@
|
||||
<span v-if="meta.complaintDate" title="投诉日期"><i class="el-icon-date"></i> 投诉日期:{{ parseTime(meta.complaintDate, '{y}-{m}-{d}') }}</span>
|
||||
</div>
|
||||
|
||||
<slot name="flow-overview"></slot>
|
||||
|
||||
<slot name="basic-info"></slot>
|
||||
<slot name="contract-info"></slot>
|
||||
</div>
|
||||
@@ -58,9 +60,7 @@ export default {
|
||||
1: '待审核',
|
||||
2: '意见填写中',
|
||||
3: '待汇总方案',
|
||||
4: '执行反馈中',
|
||||
5: '执行完成',
|
||||
6: '全部办结'
|
||||
4: '全部办结'
|
||||
};
|
||||
return map[this.flowStatus] || '未知';
|
||||
},
|
||||
@@ -69,9 +69,7 @@ export default {
|
||||
1: 'info',
|
||||
2: 'warning',
|
||||
3: '',
|
||||
4: 'warning',
|
||||
5: 'success',
|
||||
6: 'success'
|
||||
4: 'success'
|
||||
};
|
||||
return map[this.flowStatus] || '';
|
||||
}
|
||||
|
||||
@@ -15,9 +15,7 @@
|
||||
<el-option label="待审核" :value="1" />
|
||||
<el-option label="意见填写中" :value="2" />
|
||||
<el-option label="待汇总方案" :value="3" />
|
||||
<el-option label="执行反馈中" :value="4" />
|
||||
<el-option label="执行完成" :value="5" />
|
||||
<el-option label="全部办结" :value="6" />
|
||||
<el-option label="全部办结" :value="4" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
@@ -40,11 +38,11 @@
|
||||
<el-tag v-if="item.flowStatus === 1" type="info" size="mini">待审核</el-tag>
|
||||
<el-tag v-else-if="item.flowStatus === 2" type="warning" size="mini">意见填写中</el-tag>
|
||||
<el-tag v-else-if="item.flowStatus === 3" size="mini">待汇总方案</el-tag>
|
||||
<el-tag v-else-if="item.flowStatus === 4" type="warning" size="mini">执行反馈中</el-tag>
|
||||
<el-tag v-else-if="item.flowStatus === 5" type="success" size="mini">执行完成</el-tag>
|
||||
<el-tag v-else-if="item.flowStatus === 6" type="success" size="mini">全部办结</el-tag>
|
||||
<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"
|
||||
@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>
|
||||
</div>
|
||||
@@ -76,7 +74,7 @@
|
||||
:disabled="pdfLoading" title="导出PDF">导出PDF</el-button> -->
|
||||
<el-button v-if="currentRow.flowStatus === 1" size="mini" type="primary" plain
|
||||
icon="el-icon-s-promotion" @click="handleOpinionDispatch">意见下发</el-button>
|
||||
<el-button v-if="currentRow.flowStatus === 3" size="mini" type="warning" plain
|
||||
<el-button v-if="false" size="mini" type="warning" plain
|
||||
icon="el-icon-s-promotion" @click="handleFeedbackDispatch">执行下发</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-refresh" @click="handleRefreshDetail"
|
||||
title="刷新详情">刷新</el-button>
|
||||
@@ -85,6 +83,10 @@
|
||||
@click="handleDelete(currentRow)">删除</el-button>
|
||||
</template>
|
||||
|
||||
<template #flow-overview>
|
||||
<FlowOverviewSection :flowStatus="currentRow.flowStatus" />
|
||||
</template>
|
||||
|
||||
<template #basic-info>
|
||||
<BasicInfoSection :data="currentRow" />
|
||||
</template>
|
||||
@@ -92,8 +94,6 @@
|
||||
|
||||
<el-divider />
|
||||
|
||||
<FlowOverviewSection :flowStatus="currentRow.flowStatus" />
|
||||
|
||||
<CoilInfoSection :coilList="coilList" :loading="coilLoading" :editable="currentRow.flowStatus === 1"
|
||||
@refresh="loadCoilList(currentRow.acceptId)"
|
||||
@remove="handleRemoveCoil">
|
||||
@@ -114,8 +114,11 @@
|
||||
<div class="section-gap" />
|
||||
<HandlingSchemeSection :content="currentRow.planContent" :editable="currentRow.flowStatus === 3" @save="handleSavePlan" />
|
||||
|
||||
<div class="section-gap" />
|
||||
<template v-if="false">
|
||||
<div class="section-gap" />
|
||||
<ExecutionFeedbackSection :executeList="executeList" @refresh="refreshExecuteList" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -131,10 +134,25 @@
|
||||
placeholder="请选择投诉日期" style="width:100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="投诉情况" prop="complaintContent">
|
||||
<el-input v-model="form.complaintContent" type="textarea" :rows="4" placeholder="请输入投诉情况描述" />
|
||||
<el-input v-model="form.complaintContent" type="textarea" :rows="4" placeholder="请输入投诉情况(含客户诉求)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="客户诉求" prop="customerDemand">
|
||||
<el-input v-model="form.customerDemand" type="textarea" :rows="3" placeholder="请输入客户诉求" />
|
||||
<el-form-item label="关联客户" prop="customerId">
|
||||
<CustomerSelect v-model="form.customerId" :style="{width:'100%'}" />
|
||||
</el-form-item>
|
||||
<el-form-item label="下游使用用户" prop="downstreamUserName">
|
||||
<el-input v-model="form.downstreamUserName" placeholder="请输入下游使用用户名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="电话" prop="phone">
|
||||
<el-input v-model="form.phone" placeholder="请输入电话" />
|
||||
</el-form-item>
|
||||
<el-form-item label="工程名称" prop="projectName">
|
||||
<el-input v-model="form.projectName" placeholder="请输入使用的工程名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="使用地点" prop="projectLocation">
|
||||
<el-input v-model="form.projectLocation" placeholder="请输入工程使用地点" />
|
||||
</el-form-item>
|
||||
<el-form-item label="产品用途" prop="productUsage">
|
||||
<el-input v-model="form.productUsage" placeholder="请输入产品用途" />
|
||||
</el-form-item>
|
||||
<el-form-item label="关联钢卷">
|
||||
<CoilSelector :multiple="true" :filters="{ status: 1 }" placeholder="选择钢卷" @confirm="handleCoilConfirm">
|
||||
@@ -170,6 +188,8 @@
|
||||
<el-button :loading="dispatchLoading" type="primary" @click="confirmFeedbackDispatch">确 定</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<ExportPdfDialog ref="exportPdfDialog" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -183,6 +203,7 @@ import jsPDF from 'jspdf';
|
||||
import CoilSelector from "@/components/CoilSelector/index.vue";
|
||||
import CurrentCoilNo from "@/components/KLPService/Renderer/CurrentCoilNo.vue";
|
||||
import DragResizePanel from "@/components/DragResizePanel/index.vue";
|
||||
import CustomerSelect from "@/components/KLPService/CustomerSelect/index.vue";
|
||||
import HeaderControlSection from "./components/HeaderControlSection.vue";
|
||||
import BasicInfoSection from "./components/BasicInfoSection.vue";
|
||||
import ContractInfoSection from "./components/ContractInfoSection.vue";
|
||||
@@ -191,13 +212,15 @@ import DepartmentOpinionSection from "./components/DepartmentOpinionSection.vue"
|
||||
import HandlingSchemeSection from "./components/HandlingSchemeSection.vue";
|
||||
import ExecutionFeedbackSection from "./components/ExecutionFeedbackSection.vue";
|
||||
import FlowOverviewSection from "./components/FlowOverviewSection.vue";
|
||||
import ExportPdfDialog from "./components/ExportPdfDialog.vue";
|
||||
|
||||
export default {
|
||||
name: "AftermarketObjection",
|
||||
components: {
|
||||
CoilSelector, CurrentCoilNo, DragResizePanel,
|
||||
CoilSelector, CurrentCoilNo, DragResizePanel, CustomerSelect,
|
||||
HeaderControlSection, BasicInfoSection, ContractInfoSection,
|
||||
CoilInfoSection, DepartmentOpinionSection, HandlingSchemeSection, ExecutionFeedbackSection, FlowOverviewSection
|
||||
CoilInfoSection, DepartmentOpinionSection, HandlingSchemeSection, ExecutionFeedbackSection, FlowOverviewSection,
|
||||
ExportPdfDialog
|
||||
},
|
||||
dicts: ['coil_quality_status'],
|
||||
data() {
|
||||
@@ -235,8 +258,7 @@ export default {
|
||||
rules: {
|
||||
complaintNo: [{ required: true, message: "请输入售后编号", trigger: "blur" }],
|
||||
complaintDate: [{ required: true, message: "请选择投诉日期", trigger: "blur" }],
|
||||
complaintContent: [{ required: true, message: "请输入投诉情况", trigger: "blur" }],
|
||||
customerDemand: [{ required: true, message: "请输入客户诉求", trigger: "blur" }]
|
||||
complaintContent: [{ required: true, message: "请输入投诉情况", trigger: "blur" }]
|
||||
}
|
||||
};
|
||||
},
|
||||
@@ -329,7 +351,12 @@ export default {
|
||||
complaintNo: undefined,
|
||||
complaintDate: undefined,
|
||||
complaintContent: undefined,
|
||||
customerDemand: undefined,
|
||||
customerId: undefined,
|
||||
downstreamUserName: undefined,
|
||||
phone: undefined,
|
||||
projectName: undefined,
|
||||
projectLocation: undefined,
|
||||
productUsage: undefined,
|
||||
file: undefined,
|
||||
auditStatus: undefined,
|
||||
auditOpinion: undefined,
|
||||
@@ -523,6 +550,9 @@ export default {
|
||||
this.$modal.msgSuccess("移除成功");
|
||||
this.loadCoilList(this.currentRow.acceptId);
|
||||
}).catch(() => { });
|
||||
},
|
||||
handleExportPdf(row) {
|
||||
this.$refs.exportPdfDialog.open(row.acceptId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -73,6 +73,10 @@
|
||||
<BasicInfoSection :data="acceptDetail" />
|
||||
</template>
|
||||
|
||||
<template #flow-overview>
|
||||
<FlowOverviewSection :flowStatus="acceptDetail.flowStatus" />
|
||||
</template>
|
||||
|
||||
<template #contract-info>
|
||||
<ContractInfoSection :coilList="dialogCoilList" />
|
||||
</template>
|
||||
@@ -80,20 +84,27 @@
|
||||
|
||||
<el-divider />
|
||||
|
||||
<FlowOverviewSection :flowStatus="acceptDetail.flowStatus" />
|
||||
|
||||
<CoilInfoSection :coilList="dialogCoilList" :loading="coilLoading" :editable="false" />
|
||||
|
||||
<div v-if="currentTask.taskStatus === 1 && opinionForm.deptOpinion" class="section-gap" />
|
||||
<div v-if="currentTask.taskStatus === 1 && opinionForm.deptOpinion" class="opinion-preview-wrapper">
|
||||
<div class="section-title">
|
||||
<span>已提交意见 <span class="en-sub">· Submitted Opinion</span></span>
|
||||
</div>
|
||||
<DeptOpinionPreview :deptId="currentTask.deptId" :jsonData="opinionForm.deptOpinion" />
|
||||
<div v-if="opinionForm.fillFile" class="opinion-file" style="margin-top:8px;">
|
||||
<FileList :ossIds="opinionForm.fillFile" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isEditable" class="section-gap" />
|
||||
<div v-if="isEditable" class="opinion-form-wrapper">
|
||||
<div class="opinion-form-section">
|
||||
<div class="section-title">
|
||||
<span>填写处理意见 <span class="en-sub">· Fill in Opinion</span></span>
|
||||
<span>{{ currentTask.taskStatus === 1 ? '修改处理意见' : '填写处理意见' }} <span class="en-sub">· {{ currentTask.taskStatus === 1 ? 'Edit Opinion' : 'Fill in Opinion' }}</span></span>
|
||||
</div>
|
||||
<el-form ref="opinionForm" :model="opinionForm" label-width="100px">
|
||||
<el-form-item label="处理意见" prop="deptOpinion">
|
||||
<editor v-model="opinionForm.deptOpinion" :min-height="200" />
|
||||
</el-form-item>
|
||||
<DeptOpinionForm :deptId="currentTask.deptId" :jsonData="opinionForm.deptOpinion" @input="val => opinionForm.deptOpinion = val" />
|
||||
<el-form-item label="意见文件">
|
||||
<file-upload v-model="opinionForm.fillFile" />
|
||||
</el-form-item>
|
||||
@@ -121,10 +132,13 @@ import BasicInfoSection from "./components/BasicInfoSection.vue";
|
||||
import CoilInfoSection from "./components/CoilInfoSection.vue";
|
||||
import ContractInfoSection from "./components/ContractInfoSection.vue";
|
||||
import FlowOverviewSection from "./components/FlowOverviewSection.vue";
|
||||
import DeptOpinionForm from "./components/DeptOpinionForm.vue";
|
||||
import DeptOpinionPreview from "./components/DeptOpinionPreview.vue";
|
||||
import FileList from "@/components/FileList/index.vue";
|
||||
|
||||
export default {
|
||||
name: "AftermarketOpinion",
|
||||
components: { DragResizePanel, HeaderControlSection, BasicInfoSection, CoilInfoSection, ContractInfoSection, FlowOverviewSection },
|
||||
components: { DragResizePanel, HeaderControlSection, BasicInfoSection, CoilInfoSection, ContractInfoSection, FlowOverviewSection, DeptOpinionForm, DeptOpinionPreview, FileList },
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
@@ -212,7 +226,7 @@ export default {
|
||||
},
|
||||
canEdit(row) {
|
||||
const status = (row.acceptInfo || {}).flowStatus;
|
||||
return (status === 2 || status === 3) && row.taskStatus === 0;
|
||||
return status === 2;
|
||||
},
|
||||
confirmCancel() {
|
||||
this.$modal.confirm('确认取消?已填写的内容将不会保存。').then(() => {
|
||||
@@ -235,7 +249,14 @@ export default {
|
||||
}).catch(() => { }).finally(() => { this.rejectLoading = false; });
|
||||
},
|
||||
submitOpinion() {
|
||||
if (!this.opinionForm.deptOpinion) {
|
||||
try {
|
||||
const data = JSON.parse(this.opinionForm.deptOpinion || '{}');
|
||||
const hasContent = Object.values(data).some(v => v);
|
||||
if (!hasContent) {
|
||||
this.$modal.msgWarning("请至少填写一项处理意见");
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
this.$modal.msgWarning("请填写处理意见");
|
||||
return;
|
||||
}
|
||||
@@ -457,6 +478,10 @@ export default {
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.opinion-preview-wrapper {
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.opinion-form-section {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
464
klp-ui/src/views/wms/post/objection/summary.vue
Normal file
464
klp-ui/src/views/wms/post/objection/summary.vue
Normal file
@@ -0,0 +1,464 @@
|
||||
<template>
|
||||
<div class="app-container objection-container">
|
||||
<DragResizePanel :initialSize="280" :minSize="280" :maxSize="600">
|
||||
<template #panelA>
|
||||
<div class="left-panel">
|
||||
<div class="panel-header">
|
||||
<div class="header-title">
|
||||
<i class="el-icon-s-data"></i>
|
||||
<span>意见汇总</span>
|
||||
<el-button size="mini" type="text" icon="el-icon-refresh" @click="getList" style="margin-left:4px;"
|
||||
title="刷新列表"></el-button>
|
||||
</div>
|
||||
<el-select v-model="queryParams.flowStatus" placeholder="全部阶段" clearable size="mini" @change="handleQuery"
|
||||
class="header-filter">
|
||||
<el-option label="待汇总方案" :value="3" />
|
||||
<el-option label="全部办结" :value="4" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<div class="search-row">
|
||||
<el-input v-model="queryParams.complaintNo" placeholder="搜索售后编号..." clearable prefix-icon="el-icon-search"
|
||||
size="small" @keyup.enter.native="handleQuery" @clear="handleQuery" />
|
||||
</div>
|
||||
|
||||
<div v-loading="loading" class="list-body">
|
||||
<div v-for="item in dataList" :key="item.acceptId" class="list-item"
|
||||
:class="{ active: currentRow && currentRow.acceptId === item.acceptId }" @click="handleRowClick(item)">
|
||||
<div class="item-main">
|
||||
<span class="item-title">{{ item.complaintNo }}</span>
|
||||
<span class="item-sub">{{ parseTime(item.complaintDate, '{y}-{m}-{d}') }}</span>
|
||||
</div>
|
||||
<div class="item-meta">
|
||||
<el-tag v-if="item.flowStatus === 3" size="mini">待汇总方案</el-tag>
|
||||
<el-tag v-else-if="item.flowStatus === 4" type="success" size="mini">全部办结</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="dataList.length === 0 && !loading" class="list-empty">
|
||||
<i class="el-icon-folder-opened"></i>
|
||||
<span>暂无待汇总的售后单</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-footer">
|
||||
<pagination :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
|
||||
@pagination="getList" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #panelB>
|
||||
<div class="right-panel">
|
||||
<div v-if="!currentRow" class="empty-tip">
|
||||
<i class="el-icon-info"></i>
|
||||
<span>请在左侧列表中选择一条售后单查看详情</span>
|
||||
</div>
|
||||
<div v-else v-loading="detailLoading" class="detail-content">
|
||||
<HeaderControlSection :complaintNo="currentRow.complaintNo" :flowStatus="currentRow.flowStatus"
|
||||
:meta="currentRow">
|
||||
<template #actions>
|
||||
<el-button v-if="currentRow.flowStatus === 3" size="mini" type="success" plain
|
||||
icon="el-icon-circle-check" :loading="completeLoading" @click="handleComplete">办 结</el-button>
|
||||
<el-button v-if="currentRow.flowStatus === 4" size="mini" type="primary" plain
|
||||
icon="el-icon-download" @click="handleExportPdf">导出</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-refresh" @click="handleRefreshDetail"
|
||||
title="刷新详情">刷新</el-button>
|
||||
</template>
|
||||
|
||||
<template #basic-info>
|
||||
<BasicInfoSection :data="currentRow" />
|
||||
</template>
|
||||
|
||||
<template #flow-overview>
|
||||
<FlowOverviewSection :flowStatus="currentRow.flowStatus" />
|
||||
</template>
|
||||
</HeaderControlSection>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<CoilInfoSection :coilList="coilList" :loading="coilLoading" :editable="false"
|
||||
@refresh="loadCoilList(currentRow.acceptId)" />
|
||||
|
||||
<div class="section-gap" />
|
||||
<ContractInfoSection :coilList="coilList" />
|
||||
|
||||
<div class="section-gap" />
|
||||
<DepartmentOpinionSection :taskList="taskList" @refresh="refreshTaskList" />
|
||||
|
||||
<div class="section-gap" />
|
||||
<HandlingSchemeSection :content="currentRow.planContent" :editable="currentRow.flowStatus === 3" @save="handleSavePlan" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</DragResizePanel>
|
||||
|
||||
<ExportPdfDialog ref="exportPdfDialog" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listComplaintAccept, getComplaintAccept, updateComplaintAccept } from "@/api/flow/complaintAccept";
|
||||
import { listComplaintTask } from "@/api/flow/complaintTask";
|
||||
import { listAcceptCoilRel } from "@/api/flow/acceptCoilRel";
|
||||
import DragResizePanel from "@/components/DragResizePanel/index.vue";
|
||||
import HeaderControlSection from "./components/HeaderControlSection.vue";
|
||||
import BasicInfoSection from "./components/BasicInfoSection.vue";
|
||||
import ContractInfoSection from "./components/ContractInfoSection.vue";
|
||||
import CoilInfoSection from "./components/CoilInfoSection.vue";
|
||||
import DepartmentOpinionSection from "./components/DepartmentOpinionSection.vue";
|
||||
import HandlingSchemeSection from "./components/HandlingSchemeSection.vue";
|
||||
import FlowOverviewSection from "./components/FlowOverviewSection.vue";
|
||||
import ExportPdfDialog from "./components/ExportPdfDialog.vue";
|
||||
|
||||
export default {
|
||||
name: "AftermarketSummary",
|
||||
components: {
|
||||
DragResizePanel,
|
||||
HeaderControlSection, BasicInfoSection, ContractInfoSection,
|
||||
CoilInfoSection, DepartmentOpinionSection, HandlingSchemeSection, FlowOverviewSection,
|
||||
ExportPdfDialog
|
||||
},
|
||||
dicts: ['coil_quality_status'],
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
detailLoading: false,
|
||||
coilLoading: false,
|
||||
completeLoading: false,
|
||||
total: 0,
|
||||
dataList: [],
|
||||
currentRow: null,
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
complaintNo: undefined,
|
||||
flowStatus: 3
|
||||
},
|
||||
coilList: [],
|
||||
taskList: []
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.getList();
|
||||
},
|
||||
methods: {
|
||||
getList() {
|
||||
this.loading = true;
|
||||
listComplaintAccept(this.queryParams).then(response => {
|
||||
this.dataList = response.rows;
|
||||
this.total = response.total;
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
handleQuery() {
|
||||
this.queryParams.pageNum = 1;
|
||||
this.getList();
|
||||
},
|
||||
handleRowClick(row) {
|
||||
this.currentRow = row;
|
||||
this.loadDetail(row.acceptId);
|
||||
},
|
||||
loadDetail(acceptId) {
|
||||
this.detailLoading = true;
|
||||
getComplaintAccept(acceptId).then(response => {
|
||||
this.currentRow = response.data;
|
||||
this.loadRelData(acceptId);
|
||||
}).finally(() => { this.detailLoading = false; });
|
||||
},
|
||||
loadRelData(acceptId) {
|
||||
this.loadCoilList(acceptId);
|
||||
listComplaintTask({ acceptId, pageNum: 1, pageSize: 999 }).then(r => {
|
||||
this.taskList = r.rows || [];
|
||||
});
|
||||
},
|
||||
loadCoilList(acceptId) {
|
||||
this.coilLoading = true;
|
||||
listAcceptCoilRel({ acceptId, pageNum: 1, pageSize: 999 }).then(r => {
|
||||
this.coilList = r.rows || [];
|
||||
}).finally(() => { this.coilLoading = false; });
|
||||
},
|
||||
handleRefreshDetail() {
|
||||
if (this.currentRow && this.currentRow.acceptId) {
|
||||
this.loadDetail(this.currentRow.acceptId);
|
||||
}
|
||||
},
|
||||
handleSavePlan(planContent) {
|
||||
updateComplaintAccept({ acceptId: this.currentRow.acceptId, planContent }).then(() => {
|
||||
this.$modal.msgSuccess("处理方案保存成功");
|
||||
this.loadDetail(this.currentRow.acceptId);
|
||||
});
|
||||
},
|
||||
refreshTaskList() {
|
||||
if (this.currentRow && this.currentRow.acceptId) {
|
||||
listComplaintTask({ acceptId: this.currentRow.acceptId, pageNum: 1, pageSize: 999 }).then(r => {
|
||||
this.taskList = r.rows || [];
|
||||
});
|
||||
}
|
||||
},
|
||||
handleComplete() {
|
||||
this.$modal.confirm('确认将此售后单办结?办结后部门将无法再修改意见。').then(() => {
|
||||
this.completeLoading = true;
|
||||
return updateComplaintAccept({ acceptId: this.currentRow.acceptId, flowStatus: 4 });
|
||||
}).then(() => {
|
||||
this.$modal.msgSuccess("已办结");
|
||||
this.completeLoading = false;
|
||||
this.loadDetail(this.currentRow.acceptId);
|
||||
this.getList();
|
||||
}).catch(() => { this.completeLoading = false; });
|
||||
},
|
||||
handleExportPdf() {
|
||||
this.$refs.exportPdfDialog.open(this.currentRow.acceptId);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.objection-container {
|
||||
height: calc(100vh - 84px);
|
||||
}
|
||||
|
||||
.left-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: #f5f7fa;
|
||||
border-right: 1px solid #e4e7ed;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 14px 8px;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.header-title i {
|
||||
color: #409eff;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.header-filter {
|
||||
width: 130px;
|
||||
}
|
||||
|
||||
.search-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 0 14px 10px;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.list-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 12px;
|
||||
margin-bottom: 2px;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.list-item:hover {
|
||||
background: #ebeef5;
|
||||
}
|
||||
|
||||
.list-item.active {
|
||||
background: #d9ecff;
|
||||
}
|
||||
|
||||
.list-item.active .item-title {
|
||||
color: #409eff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.item-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.item-title {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.item-sub {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.item-meta {
|
||||
flex-shrink: 0;
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
.list-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 0;
|
||||
color: #c0c4cc;
|
||||
font-size: 13px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.list-empty i {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.list-footer {
|
||||
border-top: 1px solid #e4e7ed;
|
||||
padding: 2px 8px 0;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
/* ========== 右侧面板 ========== */
|
||||
.right-panel {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
padding: 12px 16px;
|
||||
background: #faf8f5;
|
||||
}
|
||||
|
||||
.right-panel .detail-content {
|
||||
margin: 0 auto;
|
||||
background: #ffffff;
|
||||
padding: 28px 32px 36px;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.06), 0 2px 12px rgba(0,0,0,0.04);
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-family: 'Georgia', 'Times New Roman', 'Noto Serif SC', 'SimSun', serif;
|
||||
width: 100%;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #1a1a1a;
|
||||
margin: 22px 0 12px 0;
|
||||
padding: 0 0 10px 0;
|
||||
border-bottom: 1px solid #d4d0c8;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.section-title:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.section-gap {
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.plan-content {
|
||||
padding: 12px 16px;
|
||||
background: #faf8f5;
|
||||
border: 1px solid #e8e4de;
|
||||
border-radius: 2px;
|
||||
font-size: 13px;
|
||||
line-height: 1.8;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.empty-data {
|
||||
color: #8c8c8c;
|
||||
font-size: 13px;
|
||||
padding: 8px 0;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* 正式表格覆写 */
|
||||
.right-panel .el-table {
|
||||
border: 1px solid #e8e4de !important;
|
||||
border-radius: 2px !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
.right-panel .el-table thead th {
|
||||
background-color: #2c3e50 !important;
|
||||
color: #ffffff !important;
|
||||
font-weight: 600 !important;
|
||||
font-size: 11px !important;
|
||||
letter-spacing: 0.5px !important;
|
||||
border-bottom: none !important;
|
||||
font-family: 'Georgia', 'Times New Roman', serif;
|
||||
}
|
||||
|
||||
.right-panel .el-table thead th .cell {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.right-panel .el-table__body tr:hover > td {
|
||||
background-color: #f7f5f0 !important;
|
||||
}
|
||||
|
||||
.right-panel .el-table--border td {
|
||||
border-right: 1px solid #f0ece6 !important;
|
||||
}
|
||||
|
||||
.right-panel .el-table--border th {
|
||||
border-right: 1px solid #3a5166 !important;
|
||||
}
|
||||
|
||||
.right-panel .el-table td {
|
||||
padding: 6px 4px !important;
|
||||
color: #3a3a3a !important;
|
||||
}
|
||||
|
||||
.right-panel .el-divider--horizontal {
|
||||
margin: 8px 0 4px;
|
||||
background-color: #e0dcd6;
|
||||
}
|
||||
|
||||
.right-panel .el-tag {
|
||||
border-radius: 2px;
|
||||
font-family: 'Georgia', 'Times New Roman', serif;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.right-panel .el-tag--mini {
|
||||
padding: 0 6px;
|
||||
line-height: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.right-panel .el-tag--small {
|
||||
padding: 0 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -72,12 +72,14 @@
|
||||
<template #basic-info>
|
||||
<BasicInfoSection :data="acceptDetail" />
|
||||
</template>
|
||||
|
||||
<template #flow-overview>
|
||||
<FlowOverviewSection :flowStatus="acceptDetail.flowStatus" />
|
||||
</template>
|
||||
</HeaderControlSection>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<FlowOverviewSection :flowStatus="acceptDetail.flowStatus" />
|
||||
|
||||
<CoilInfoSection :coilList="dialogCoilList" :loading="coilLoading" :editable="false" />
|
||||
|
||||
<ContractInfoSection :coilList="dialogCoilList" />
|
||||
|
||||
Reference in New Issue
Block a user