refactor(wms/flow): 调整客诉流程为串行模式并替换质量部为技术部

1.  将原并行的质量部+生产部流程改为技术部→生产部→陈总→吴部长的串行流程
2.  替换所有质量部相关描述为技术部,更新部门映射配置
3.  重构流程状态推进逻辑,新增getNextDept方法简化流程推进
4.  新增表单本地暂存功能,优化列表项UI布局
5.  更新流程步骤展示、状态文案和标签颜色配置
This commit is contained in:
王文昊
2026-07-13 13:29:49 +08:00
parent b0a332fda1
commit 3fd207a42c
15 changed files with 327 additions and 196 deletions

View File

@@ -99,7 +99,7 @@ public class TsComplaintAcceptController extends BaseController {
}
/**
* 发起流程:创建质量部+生产部并行任务
* 发起流程(串行):创建技术部任务
*
* @param acceptId 受理单ID
*/

View File

@@ -48,7 +48,7 @@ public interface ITsComplaintAcceptService {
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
/**
* 发起流程:创建质量部+生产部并行任务更新flowStatus=1
* 发起流程(串行):创建技术部任务更新flowStatus=1
*/
Boolean opinionDispatch(Long acceptId);
}

View File

@@ -115,7 +115,7 @@ public class TsComplaintAcceptServiceImpl implements ITsComplaintAcceptService {
}).collect(java.util.stream.Collectors.toList());
tsAcceptCoilRelMapper.insertBatch(relList);
}
// 自动创建质量部+生产部并行任务,发起流程
// 自动创建技术部任务,发起串行流程
opinionDispatch(add.getAcceptId());
}
return flag;
@@ -132,30 +132,23 @@ public class TsComplaintAcceptServiceImpl implements ITsComplaintAcceptService {
}
/**
* 发起流程
* 创建质量部(deptId=2) + 生产部(deptId=1) 两个并行代办任务
* 更新受理单flow_status=1质量部·生产部处理中)
* 发起流程(串行)
* 创建技术部(deptId=2) 代办任务
* 更新受理单flow_status=1技术部处理中)
*/
@Override
public Boolean opinionDispatch(Long acceptId) {
if (acceptId == null) {
return false;
}
// 1. 创建质量部task deptId=2
TsComplaintTask qaTask = new TsComplaintTask();
qaTask.setAcceptId(acceptId);
qaTask.setDeptId(2L);
qaTask.setTaskStatus(0L);
qaTask.setRejectMark(0L);
tsComplaintTaskMapper.insert(qaTask);
// 2. 创建生产部task deptId=1
TsComplaintTask prodTask = new TsComplaintTask();
prodTask.setAcceptId(acceptId);
prodTask.setDeptId(1L);
prodTask.setTaskStatus(0L);
prodTask.setRejectMark(0L);
tsComplaintTaskMapper.insert(prodTask);
// 3. 更新受理单流程状态为1质量部·生产部处理中
// 1. 创建技术部task deptId=2
TsComplaintTask techTask = new TsComplaintTask();
techTask.setAcceptId(acceptId);
techTask.setDeptId(2L);
techTask.setTaskStatus(0L);
techTask.setRejectMark(0L);
tsComplaintTaskMapper.insert(techTask);
// 2. 更新受理单流程状态为1技术部处理中
LambdaUpdateWrapper<TsComplaintAccept> uw = Wrappers.lambdaUpdate();
uw.eq(TsComplaintAccept::getAcceptId, acceptId)
.set(TsComplaintAccept::getFlowStatus, 1L);

View File

@@ -157,10 +157,11 @@ public class TsComplaintTaskServiceImpl implements ITsComplaintTaskService {
}
/**
* 同步受理单流程状态 — 串行推进逻辑
* Step 1: 质量部(deptId=2) + 生产部(deptId=1) 都完成 → 创建陈总task(deptId=4), flowStatus=2
* Step 2: 陈总完成 → 创建吴部长task(deptId=3), flowStatus=3
* Step 3: 吴部长完成 → flowStatus=4 已办结
* 同步受理单流程状态 — 严格串行推进逻辑
* Step 1: 技术部(deptId=2)完成 → 创建生产部task(deptId=1), flowStatus=2
* Step 2: 生产部(deptId=1)完成 → 创建陈总task(deptId=4), flowStatus=3
* Step 3: 陈总(deptId=4)完成 → 创建吴部长task(deptId=3), flowStatus=4
* Step 4: 吴部长(deptId=3)完成 → flowStatus=5 已办结
*/
private void syncAcceptFlowStatus(Long acceptId) {
if (acceptId == null) {
@@ -178,52 +179,49 @@ public class TsComplaintTaskServiceImpl implements ITsComplaintTaskService {
Map<Long, List<TsComplaintTask>> taskByDept = taskList.stream()
.collect(Collectors.groupingBy(TsComplaintTask::getDeptId));
// Step 1: 质量部(deptId=2) 和 生产部(deptId=1) 是否都完成
boolean qaDone = taskByDept.containsKey(2L) &&
taskByDept.get(2L).stream().allMatch(t -> t.getTaskStatus() != null && t.getTaskStatus() == 1L);
boolean prodDone = taskByDept.containsKey(1L) &&
taskByDept.get(1L).stream().allMatch(t -> t.getTaskStatus() != null && t.getTaskStatus() == 1L);
if (qaDone && prodDone) {
// 两者都完成 → 检查陈总节点(deptId=4)是否存在
boolean chenExists = taskByDept.containsKey(4L);
if (!chenExists) {
// 创建陈总task deptId=4
TsComplaintTask chenTask = new TsComplaintTask();
chenTask.setAcceptId(acceptId);
chenTask.setDeptId(4L);
chenTask.setRejectMark(0L);
chenTask.setTaskStatus(0L);
baseMapper.insert(chenTask);
updateFlowStatus(acceptId, 2L); // 待陈总审批
} else {
// 陈总节点已存在,检查是否完成
boolean chenDone = taskByDept.get(4L).stream()
.allMatch(t -> t.getTaskStatus() != null && t.getTaskStatus() == 1L);
if (chenDone) {
// 陈总完成 → 检查吴部长节点(deptId=3)是否存在
boolean wuExists = taskByDept.containsKey(3L);
if (!wuExists) {
// 创建吴部长task deptId=3
TsComplaintTask wuTask = new TsComplaintTask();
wuTask.setAcceptId(acceptId);
wuTask.setDeptId(3L);
wuTask.setRejectMark(0L);
wuTask.setTaskStatus(0L);
baseMapper.insert(wuTask);
updateFlowStatus(acceptId, 3L); // 待吴部长处理
} else {
// 吴部长已完成 → 办结
boolean wuDone = taskByDept.get(3L).stream()
.allMatch(t -> t.getTaskStatus() != null && t.getTaskStatus() == 1L);
if (wuDone) {
updateFlowStatus(acceptId, 4L); // 已办结
}
}
// 辅助方法:检查某部门的所有任务是否都完成
java.util.function.BiFunction<Long, Long, Boolean> isDeptDone = (deptId, nextFlowStatus) -> {
if (!taskByDept.containsKey(deptId)) {
return false;
}
boolean done = taskByDept.get(deptId).stream()
.allMatch(t -> t.getTaskStatus() != null && t.getTaskStatus() == 1L);
if (done) {
// 检查是否已存在下一节点,若不存在则创建
Long nextDeptId = getNextDept(deptId);
if (nextDeptId != null && !taskByDept.containsKey(nextDeptId)) {
TsComplaintTask nextTask = new TsComplaintTask();
nextTask.setAcceptId(acceptId);
nextTask.setDeptId(nextDeptId);
nextTask.setRejectMark(0L);
nextTask.setTaskStatus(0L);
baseMapper.insert(nextTask);
updateFlowStatus(acceptId, nextFlowStatus);
} else if (nextDeptId == null) {
// 最后一步(吴部长完成)→ 办结
updateFlowStatus(acceptId, 5L);
}
}
}
// 质/产未全部完成时不做任何事,等待
return done;
};
// 按串行顺序依次推进:技术部→生产部→陈总→吴部长→办结
// flowStatus: 1→2→3→4→5
isDeptDone.apply(2L, 2L); // 技术部完成 → 生产部, flowStatus=2
isDeptDone.apply(1L, 3L); // 生产部完成 → 陈总, flowStatus=3
isDeptDone.apply(4L, 4L); // 陈总完成 → 吴部长, flowStatus=4
isDeptDone.apply(3L, 5L); // 吴部长完成 → 办结, flowStatus=5
}
/**
* 获取串行流程中当前部门完成后应创建的下一个部门
* 2(技术部) → 1(生产部) → 4(陈总) → 3(吴部长) → null(办结)
*/
private Long getNextDept(Long deptId) {
if (deptId == 2L) return 1L; // 技术部 → 生产部
if (deptId == 1L) return 4L; // 生产部 → 陈总
if (deptId == 4L) return 3L; // 陈总 → 吴部长
return null; // 吴部长 → 办结
}
/**

View File

@@ -104,10 +104,10 @@ const NODE_EVENT_CONFIG = {
],
afterSales: [
{ id: 'A', label: '创建售后单', handler: 'handleOpen', params: { componentPath: 'wms/post/objection/index' } },
{ id: 'C', label: '质量部处理', handler: 'handleOpen', params: { componentPath: 'wms/post/objection/opinion' } },
{ id: 'D', label: '生产部处理', handler: 'handleOpen', params: { componentPath: 'wms/post/objection/opinion' } },
{ id: 'G', label: '陈总审批', handler: 'handleOpen', params: { componentPath: 'wms/post/objection/summary' } },
{ id: 'H', label: '吴部长(销售部)处理', handler: 'handleOpen', params: { componentPath: 'wms/post/objection/opinion' } },
{ id: 'B', label: '技术部处理', handler: 'handleOpen', params: { componentPath: 'wms/post/objection/opinion' } },
{ id: 'C', label: '生产部处理', handler: 'handleOpen', params: { componentPath: 'wms/post/objection/opinion' } },
{ id: 'D', label: '陈总审批', handler: 'handleOpen', params: { componentPath: 'wms/post/objection/summary' } },
{ id: 'E', label: '吴部长(销售部)处理', handler: 'handleOpen', params: { componentPath: 'wms/post/objection/opinion' } },
],
inventoryCheck: [
{ id: 'A', label: '创建盘库计划', handler: 'handleOpen', params: { componentPath: 'wms/post/InvCount/index' } },
@@ -246,21 +246,16 @@ graph TD
afterSales: `
graph TD
A["<b>创建售后单</b><br/>销售内勤发起"]:::step
A --> B["<b>质量部·生产部</b><br/>并行处理"]:::fork
B --> C["<b>质量部</b><br/>出具处理意见"]:::dept1
B --> D["<b>生产部</b><br/>出具处理意见"]:::dept2
C --> F{"两部门<br/>均已完成?"}:::decision
D --> F
F -->|是| G["<b>陈总审批</b>"]:::dept3
G --> H["<b>吴部长</b><br/>(销售部)<br/>处理客诉"]:::approve
H --> J(["<b>售后单封存</b><br/>流程结束"]):::end
A --> B["<b>技术部处理</b><br/>出具技术意见"]:::dept1
B --> C["<b>生产部处理</b><br/>出具处理意见"]:::dept2
C --> D["<b>陈总审批</b><br/>审阅意见并审批"]:::dept3
D --> E["<b>吴部长</b><br/>(销售部)<br/>根据审批意见处理客诉"]:::approve
E --> F(["<b>售后单办结</b><br/>流程结束"]):::end
classDef step fill:#409eff,stroke:#337ecc,color:#fff,stroke-width:2px
classDef fork fill:#f0f5ff,stroke:#409eff,color:#303133,stroke-width:2px
classDef dept1 fill:#e6fffa,stroke:#00b4a0,color:#303133,stroke-width:2px
classDef dept2 fill:#fff7e6,stroke:#fa8c16,color:#303133,stroke-width:2px
classDef dept3 fill:#fff0f6,stroke:#eb2f96,color:#303133,stroke-width:2px
classDef decision fill:#f9f0ff,stroke:#722ed1,color:#303133,stroke-width:2px
classDef approve fill:#e6f7ff,stroke:#1890ff,color:#303133,stroke-width:2px
classDef end fill:#dcf7e8,stroke:#52c41a,color:#303133,stroke-width:2px,rx:10,ry:10
linkStyle default stroke:#bfbfbf,stroke-width:2px

View File

@@ -66,7 +66,7 @@ export default {
}
},
getDeptName(deptId) {
const map = { 1: '生产部', 2: '质量部', 3: '销售部(吴部长)', 4: '陈总审批' };
const map = { 1: '生产部', 2: '技术部', 3: '销售部(吴部长)', 4: '陈总审批' };
return map[deptId] || '部门' + deptId;
},
parseTime(time, option) {

View File

@@ -49,7 +49,7 @@
</el-row>
</template>
<!-- 质量 deptId=2 -->
<!-- 技术 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" />
@@ -72,7 +72,7 @@
<template v-else-if="deptId === 4">
<el-form-item label="审阅意见" prop="reviewOpinion">
<el-input v-model="formData.reviewOpinion" type="textarea" :rows="4"
placeholder="请审阅质量部、生产部意见后填写审批意见" @input="emitJson" />
placeholder="请审阅技术部、生产部意见后填写审批意见" @input="emitJson" />
</el-form-item>
<el-form-item label="审批结论" prop="approvalResult">
<el-radio-group v-model="formData.approvalResult" @change="emitJson">
@@ -91,8 +91,8 @@
<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 label="处理方案" prop="leaderOpinion">
<el-input v-model="formData.leaderOpinion" type="textarea" :rows="3" placeholder="请输入处理方案" @input="emitJson" />
</el-form-item>
</template>
</div>

View File

@@ -29,7 +29,7 @@
</div>
</template>
<!-- 质量 deptId=2 -->
<!-- 技术 deptId=2 -->
<template v-else-if="deptId === 2">
<div class="preview-row" v-if="parsedData.cause">
<span class="preview-label">产生原因</span>

View File

@@ -51,7 +51,7 @@ export default {
},
methods: {
getDeptName(deptId) {
const map = { 1: '生产部', 2: '质量部', 3: '销售部' };
const map = { 1: '生产部', 2: '技术部', 3: '销售部' };
return map[deptId] || '部门' + deptId;
},
parseTime(time, option) {

View File

@@ -5,7 +5,8 @@
<span>流程总览 <span class="en-sub">· Process Overview</span></span>
</div>
<el-steps :active="activeStep" align-center class="flow-steps">
<el-step title="质量部·生产部" icon="el-icon-s-operation" />
<el-step title="技术部处理" icon="el-icon-s-operation" />
<el-step title="生产部处理" icon="el-icon-s-operation" />
<el-step title="陈总审批" icon="el-icon-s-check" />
<el-step title="吴部长处理" icon="el-icon-s-custom" />
<el-step title="已办结" icon="el-icon-circle-check" />
@@ -33,29 +34,31 @@ export default {
computed: {
/**
* el-steps active 从 0 开始。
* 1=技术部·生产部→0, 2=陈总审批→1, 3=吴部长处理→2, 4=已办结→3
* 1=技术部处理→0, 2=生产部处理→1, 3=陈总审批→2, 4=吴部长处理→3, 5=已办结→4
*/
activeStep() {
if (this.flowStatus == null) return -1;
const v = Number(this.flowStatus);
if (v >= 4) return 3;
if (v >= 5) return 4;
return v - 1;
},
flowStatusText() {
const map = {
1: '质量部·生产部处理中',
2: '待陈总审批',
3: '待吴部长处理',
4: '已办结'
1: '技术部处理中',
2: '生产部处理中',
3: '待陈总审批',
4: '待吴部长处理',
5: '已办结'
};
return map[this.flowStatus] || '未知';
},
tagType() {
const map = {
1: 'warning',
2: 'primary',
3: 'warning',
4: 'success'
2: 'warning',
3: 'primary',
4: 'warning',
5: 'success'
};
return map[this.flowStatus] || '';
}

View File

@@ -57,10 +57,11 @@ export default {
computed: {
flowStatusText() {
const map = {
1: '待审核',
2: '意见填写中',
3: '待汇总方案',
4: '全部办结'
1: '技术部处理中',
2: '生产部处理中',
3: '待陈总审批',
4: '待吴部长处理',
5: '已办结'
};
return map[this.flowStatus] || '未知';
},
@@ -68,8 +69,9 @@ export default {
const map = {
1: 'info',
2: 'warning',
3: '',
4: 'success'
3: 'warning',
4: '',
5: 'success'
};
return map[this.flowStatus] || '';
}

View File

@@ -12,10 +12,11 @@
</div>
<el-select v-model="queryParams.flowStatus" placeholder="全部阶段" clearable size="mini" @change="handleQuery"
class="header-filter">
<el-option label="质量部·生产部处理" :value="1" />
<el-option label="待陈总审批" :value="2" />
<el-option label="待吴部长处理" :value="3" />
<el-option label="已办结" :value="4" />
<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-select>
</div>
@@ -30,21 +31,22 @@
<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">
<div class="item-row-top">
<span class="item-title">{{ item.complaintNo }}</span>
<el-tag v-if="item.flowStatus === 1" type="warning" 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" type="primary" 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>
</div>
<div class="item-row-bottom">
<span class="item-sub">{{ parseTime(item.complaintDate, '{y}-{m}-{d}') }}</span>
</div>
<div class="item-meta">
<el-tag v-if="item.flowStatus === 1" type="warning" size="mini">质量部·生产部处理中</el-tag>
<el-tag v-else-if="item.flowStatus === 2" type="primary" size="mini">待陈总审批</el-tag>
<el-tag v-else-if="item.flowStatus === 3" type="warning" 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 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 class="item-actions">
<el-button size="mini" type="text" icon="el-icon-download"
@click.stop="handleExportPdf(item)" title="导出PDF"></el-button>
<el-button size="mini" type="text" icon="el-icon-edit" @click.stop="handleUpdate(item)"></el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click.stop="handleDelete(item)"></el-button>
</div>
</div>
</div>
<div v-if="dataList.length === 0 && !loading" class="list-empty">
@@ -108,7 +110,7 @@
<DepartmentOpinionSection :taskList="taskList" @refresh="refreshTaskList" />
<div class="section-gap" />
<HandlingSchemeSection :content="currentRow.planContent" :editable="currentRow.flowStatus === 3" @save="handleSavePlan" />
<HandlingSchemeSection :content="currentRow.planContent" :editable="currentRow.flowStatus === 4" @save="handleSavePlan" />
<div class="section-gap" />
<template v-if="false">
@@ -170,8 +172,9 @@
</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>
<el-button v-if="form.acceptId" :loading="draftLoading" @click="saveDraft"> </el-button>
<el-button :loading="buttonLoading" type="primary" @click="submitForm"> </el-button>
</div>
</el-dialog>
@@ -222,6 +225,7 @@ export default {
data() {
return {
buttonLoading: false,
draftLoading: false,
pdfLoading: false,
loading: true,
detailLoading: false,
@@ -248,7 +252,7 @@ export default {
selectedDeptIds: [],
deptOptions: [
{ deptId: 1, deptName: '生产部' },
{ deptId: 2, deptName: '质量部' },
{ deptId: 2, deptName: '技术部' },
{ deptId: 3, deptName: '销售部' }
],
rules: {
@@ -306,7 +310,7 @@ export default {
}).finally(() => { this.coilLoading = false; });
},
getDeptName(deptId) {
const map = { 1: '生产部', 2: '质量部', 3: '销售部' };
const map = { 1: '生产部', 2: '技术部', 3: '销售部' };
return map[deptId] || '部门' + deptId;
},
handleAdd() {
@@ -332,6 +336,19 @@ export default {
handleUpdate(row) {
this.reset();
const acceptId = row.acceptId;
// 检查是否有本地暂存
const draftKey = 'draft_' + acceptId;
const cached = localStorage.getItem(draftKey);
if (cached) {
try {
const draft = JSON.parse(cached);
this.form = draft.form;
this.formCoilList = draft.formCoilList || [];
this.open = true;
this.title = "修改售后单(已恢复未保存内容)";
return;
} catch (e) { /* ignore */ }
}
getComplaintAccept(acceptId).then(response => {
this.form = response.data;
listAcceptCoilRel({ acceptId, pageNum: 1, pageSize: 999 }).then(r => {
@@ -370,6 +387,21 @@ export default {
this.open = false;
this.reset();
},
saveDraft() {
this.draftLoading = true;
const draftKey = 'draft_' + this.form.acceptId;
localStorage.setItem(draftKey, JSON.stringify({
form: this.form,
formCoilList: this.formCoilList
}));
this.$modal.msgSuccess("已暂存");
this.draftLoading = false;
this.open = false;
},
clearDraft(acceptId) {
const draftKey = 'draft_' + acceptId;
localStorage.removeItem(draftKey);
},
submitForm() {
this.$refs["form"].validate(valid => {
if (valid) {
@@ -377,6 +409,7 @@ export default {
const formData = { ...this.form, coilIds: this.formCoilList.map(c => c.coilId) };
if (this.form.acceptId != null) {
updateComplaintAccept(formData).then(() => {
this.clearDraft(this.form.acceptId);
this.$modal.msgSuccess("修改成功");
this.open = false;
this.getList();
@@ -610,8 +643,8 @@ export default {
.list-item {
display: flex;
align-items: center;
padding: 10px 12px;
flex-direction: column;
padding: 8px 12px;
margin-bottom: 2px;
cursor: pointer;
border-radius: 6px;
@@ -631,15 +664,23 @@ export default {
font-weight: 600;
}
.item-main {
flex: 1;
min-width: 0;
.item-row-top {
display: flex;
flex-direction: column;
gap: 2px;
align-items: center;
justify-content: space-between;
gap: 6px;
}
.item-row-bottom {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 2px;
}
.item-title {
flex: 1;
min-width: 0;
font-size: 13px;
font-weight: 500;
color: #303133;
@@ -654,8 +695,7 @@ export default {
}
.item-meta {
flex-shrink: 0;
margin: 0 8px;
display: none;
}
.item-actions {

View File

@@ -25,19 +25,19 @@
<div v-loading="loading" class="list-body">
<div v-for="item in taskList" :key="item.taskId" class="list-item"
:class="{ active: currentTask && currentTask.taskId === item.taskId }" @click="handleRowClick(item)">
<div class="item-main">
<div class="item-row-top">
<span class="item-title">{{ (item.acceptInfo || {}).complaintNo || '' }}</span>
<span class="item-sub">{{ parseTime((item.acceptInfo || {}).complaintDate, '{y}-{m}-{d}') }}</span>
</div>
<div class="item-meta">
<el-tag v-if="item.rejectMark === 1" type="danger" size="mini">已驳回</el-tag>
<el-tag v-else-if="item.rejectMark === 2" type="info" size="mini">已隐藏</el-tag>
<el-tag v-else-if="item.taskStatus === 0" type="warning" size="mini">待填写</el-tag>
<el-tag v-else-if="item.taskStatus === 1" type="success" size="mini">已完成</el-tag>
</div>
<div class="item-actions">
<el-button v-if="canEdit(item)" size="mini" type="text" icon="el-icon-edit" @click.stop="handleRowClick(item)"></el-button>
<el-button v-else size="mini" type="text" icon="el-icon-view" @click.stop="handleRowClick(item)"></el-button>
<div class="item-row-bottom">
<span class="item-sub">{{ parseTime((item.acceptInfo || {}).complaintDate, '{y}-{m}-{d}') }}</span>
<div class="item-actions">
<el-button v-if="canEdit(item)" size="mini" type="text" icon="el-icon-edit" @click.stop="handleRowClick(item)"></el-button>
<el-button v-else size="mini" type="text" icon="el-icon-view" @click.stop="handleRowClick(item)"></el-button>
</div>
</div>
</div>
<div v-if="taskList.length === 0 && !loading" class="list-empty">
@@ -116,6 +116,7 @@
</el-form-item>
</el-form>
<div class="form-actions">
<el-button :loading="draftLoading" @click="saveDraft"> </el-button>
<el-button :loading="submitLoading" type="primary" @click="submitOpinion"> </el-button>
</div>
</div>
@@ -151,6 +152,7 @@ export default {
detailLoading: false,
coilLoading: false,
submitLoading: false,
draftLoading: false,
total: 0,
queryParams: {
@@ -211,6 +213,17 @@ export default {
this.dialogCoilList = r.rows || [];
}).finally(() => { this.coilLoading = false; this.detailLoading = false; });
// 检查本地暂存
const draftKey = 'draft_opinion_' + row.taskId;
const cached = localStorage.getItem(draftKey);
if (cached) {
try {
const draft = JSON.parse(cached);
this.opinionForm = { deptOpinion: draft.deptOpinion || '', fillFile: draft.fillFile || '' };
return;
} catch (e) { /* ignore */ }
}
getComplaintTask(row.taskId).then(response => {
const task = response.data || {};
this.opinionForm = {
@@ -236,13 +249,19 @@ export default {
}).finally(() => { this.coilLoading = false; });
},
getDeptLabel() {
const map = { 1: '生产部', 2: '质量部', 3: '销售部(吴部长)', 4: '陈总审批' };
const map = { 1: '生产部', 2: '技术部', 3: '销售部(吴部长)', 4: '陈总审批' };
return map[this.queryParams.deptId] || '';
},
canEdit(row) {
const status = (row.acceptInfo || {}).flowStatus;
// 只要节点task状态为待处理(0)就可编辑
return status >= 1 && status <= 3;
const deptId = row.deptId;
// 技术部(deptId=2) 仅在 flowStatus=1 时可编辑
// 生产部(deptId=1) 仅在 flowStatus=2 时可编辑
// 吴部长(deptId=3) 仅在 flowStatus=4 时可编辑
if (deptId === 2 && status === 1) return true;
if (deptId === 1 && status === 2) return true;
if (deptId === 3 && status === 4) return true;
return false;
},
loadAllTasks() {
if (!this.currentTask.acceptId) return;
@@ -252,7 +271,7 @@ export default {
pageSize: 999
}).then(r => {
const tasks = r.rows || [];
// 按 deptId 排序: 质量部(2)→生产部(1)→陈总(4)
// 按 deptId 排序: 技术部(2)→生产部(1)→陈总(4)
const order = { 2: 1, 1: 2, 4: 3 };
tasks.sort((a, b) => (order[a.deptId] || 99) - (order[b.deptId] || 99));
this.allTaskList = tasks;
@@ -285,11 +304,29 @@ export default {
taskStatus: 1,
fillTime: this.parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}')
}).then(() => {
this.clearDraft(this.currentTask.taskId);
this.$modal.msgSuccess("已提交,流程已流转至下一节点");
this.getList();
this.currentTask = {};
}).finally(() => { this.submitLoading = false; });
}
},
saveDraft() {
if (!this.currentTask.taskId) {
this.$modal.msgWarning("请先在左侧选择一条任务");
return;
}
this.draftLoading = true;
const draftKey = 'draft_opinion_' + this.currentTask.taskId;
localStorage.setItem(draftKey, JSON.stringify({
deptOpinion: this.opinionForm.deptOpinion,
fillFile: this.opinionForm.fillFile
}));
this.$modal.msgSuccess("已暂存");
this.draftLoading = false;
},
clearDraft(taskId) {
localStorage.removeItem('draft_opinion_' + taskId);
},
}
};
</script>
@@ -350,8 +387,8 @@ export default {
.list-item {
display: flex;
align-items: center;
padding: 10px 12px;
flex-direction: column;
padding: 8px 12px;
margin-bottom: 2px;
cursor: pointer;
border-radius: 6px;
@@ -371,15 +408,23 @@ export default {
font-weight: 600;
}
.item-main {
flex: 1;
min-width: 0;
.item-row-top {
display: flex;
flex-direction: column;
gap: 3px;
align-items: center;
justify-content: space-between;
gap: 6px;
}
.item-row-bottom {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 2px;
}
.item-title {
flex: 1;
min-width: 0;
font-size: 13px;
font-weight: 500;
color: #303133;
@@ -393,9 +438,12 @@ export default {
color: #909399;
}
.item-main {
display: none;
}
.item-meta {
flex-shrink: 0;
margin: 0 8px;
display: none;
}
.item-actions {

View File

@@ -12,8 +12,8 @@
</div>
<el-select v-model="queryParams.flowStatus" placeholder="全部阶段" clearable size="mini" @change="handleQuery"
class="header-filter">
<el-option label="待审批" :value="2" />
<el-option label="已办结" :value="4" />
<el-option label="待审批" :value="3" />
<el-option label="已办结" :value="5" />
</el-select>
</div>
@@ -25,13 +25,13 @@
<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">
<div class="item-row-top">
<span class="item-title">{{ item.complaintNo }}</span>
<span class="item-sub">{{ parseTime(item.complaintDate, '{y}-{m}-{d}') }}</span>
<el-tag v-if="item.flowStatus === 3" type="primary" size="mini">待陈总审批</el-tag>
<el-tag v-else-if="item.flowStatus === 5" type="success" size="mini">已办结</el-tag>
</div>
<div class="item-meta">
<el-tag v-if="item.flowStatus === 2" type="primary" size="mini">待陈总审批</el-tag>
<el-tag v-else-if="item.flowStatus === 4" type="success" size="mini">已办结</el-tag>
<div class="item-row-bottom">
<span class="item-sub">{{ parseTime(item.complaintDate, '{y}-{m}-{d}') }}</span>
</div>
</div>
<div v-if="dataList.length === 0 && !loading" class="list-empty">
@@ -57,7 +57,7 @@
<HeaderControlSection :complaintNo="currentRow.complaintNo" :flowStatus="currentRow.flowStatus"
:meta="currentRow">
<template #actions>
<el-button v-if="currentRow.flowStatus === 4" size="mini" type="primary" plain
<el-button v-if="currentRow.flowStatus === 5" 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>
@@ -83,14 +83,15 @@
<div class="section-gap" />
<DepartmentOpinionSection :taskList="taskList" @refresh="refreshTaskList" />
<div v-if="currentRow.flowStatus === 2" class="section-gap" />
<div v-if="currentRow.flowStatus === 2" class="approval-section">
<div v-if="currentRow.flowStatus === 3" class="section-gap" />
<div v-if="currentRow.flowStatus === 3" class="approval-section">
<div class="section-title">
<span>审批意见 <span class="en-sub">· Approval Opinion</span></span>
</div>
<el-input v-model="approvalOpinion" type="textarea" :rows="4"
placeholder="请填写审批意见(审阅质量部、生产部意见后)" />
placeholder="请填写审批意见(审阅技术部、生产部意见后)" />
<div class="form-actions">
<el-button :loading="draftLoading" @click="saveDraft"> </el-button>
<el-button type="primary" plain icon="el-icon-circle-check" :loading="completeLoading"
@click="handleComplete">提交审批</el-button>
</div>
@@ -132,6 +133,7 @@ export default {
detailLoading: false,
coilLoading: false,
completeLoading: false,
draftLoading: false,
total: 0,
dataList: [],
currentRow: null,
@@ -139,7 +141,7 @@ export default {
pageNum: 1,
pageSize: 10,
complaintNo: undefined,
flowStatus: 2
flowStatus: 3
},
coilList: [],
taskList: [],
@@ -168,6 +170,16 @@ export default {
},
loadDetail(acceptId) {
this.detailLoading = true;
// 检查本地暂存
const draftKey = 'draft_summary_' + acceptId;
const cached = localStorage.getItem(draftKey);
if (cached) {
try {
this.approvalOpinion = cached;
} catch (e) { /* ignore */ }
} else {
this.approvalOpinion = '';
}
getComplaintAccept(acceptId).then(response => {
this.currentRow = response.data;
this.loadRelData(acceptId);
@@ -223,6 +235,7 @@ export default {
throw new Error('no chen task');
}
}).then(() => {
this.clearDraft(this.currentRow.acceptId);
this.$modal.msgSuccess("审批已提交,已流转至吴部长处理");
this.completeLoading = false;
this.approvalOpinion = '';
@@ -230,6 +243,23 @@ export default {
this.getList();
}).catch(() => { this.completeLoading = false; });
},
saveDraft() {
if (!this.currentRow || !this.currentRow.acceptId) {
this.$modal.msgWarning("请先在左侧选择一条售后单");
return;
}
if (!this.approvalOpinion) {
this.$modal.msgWarning("请先填写审批意见再暂存");
return;
}
this.draftLoading = true;
localStorage.setItem('draft_summary_' + this.currentRow.acceptId, this.approvalOpinion);
this.$modal.msgSuccess("已暂存");
this.draftLoading = false;
},
clearDraft(acceptId) {
localStorage.removeItem('draft_summary_' + acceptId);
},
handleExportPdf() {
this.$refs.exportPdfDialog.open(this.currentRow.acceptId);
}
@@ -292,8 +322,8 @@ export default {
.list-item {
display: flex;
align-items: center;
padding: 10px 12px;
flex-direction: column;
padding: 8px 12px;
margin-bottom: 2px;
cursor: pointer;
border-radius: 6px;
@@ -313,15 +343,23 @@ export default {
font-weight: 600;
}
.item-main {
flex: 1;
min-width: 0;
.item-row-top {
display: flex;
flex-direction: column;
gap: 3px;
align-items: center;
justify-content: space-between;
gap: 6px;
}
.item-row-bottom {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 2px;
}
.item-title {
flex: 1;
min-width: 0;
font-size: 13px;
font-weight: 500;
color: #303133;
@@ -335,9 +373,12 @@ export default {
color: #909399;
}
.item-main {
display: none;
}
.item-meta {
flex-shrink: 0;
margin: 0 8px;
display: none;
}
.list-empty {

View File

@@ -25,19 +25,19 @@
<div v-loading="loading" class="list-body">
<div v-for="item in dataList" :key="item.relId" class="list-item"
:class="{ active: currentRel && currentRel.relId === item.relId }" @click="handleRowClick(item)">
<div class="item-main">
<div class="item-row-top">
<span class="item-title">{{ (item.acceptInfo || {}).complaintNo || '' }}</span>
<span class="item-sub">{{ getDeptName(item.deptId) }} · {{ parseTime((item.acceptInfo || {}).complaintDate, '{y}-{m}-{d}') }}</span>
</div>
<div class="item-meta">
<el-tag v-if="item.rejectMark === 1" type="danger" size="mini">已驳回</el-tag>
<el-tag v-else-if="item.rejectMark === 2" type="info" size="mini">已隐藏</el-tag>
<el-tag v-else-if="item.executeStatus === 0" type="warning" size="mini">待执行</el-tag>
<el-tag v-else-if="item.executeStatus === 1" type="success" size="mini">已反馈</el-tag>
</div>
<div class="item-actions">
<el-button v-if="item.executeStatus === 0" size="mini" type="text" icon="el-icon-edit" @click.stop="handleRowClick(item)"></el-button>
<el-button v-else size="mini" type="text" icon="el-icon-view" @click.stop="handleRowClick(item)"></el-button>
<div class="item-row-bottom">
<span class="item-sub">{{ getDeptName(item.deptId) }} · {{ parseTime((item.acceptInfo || {}).complaintDate, '{y}-{m}-{d}') }}</span>
<div class="item-actions">
<el-button v-if="item.executeStatus === 0" size="mini" type="text" icon="el-icon-edit" @click.stop="handleRowClick(item)"></el-button>
<el-button v-else size="mini" type="text" icon="el-icon-view" @click.stop="handleRowClick(item)"></el-button>
</div>
</div>
</div>
<div v-if="dataList.length === 0 && !loading" class="list-empty">
@@ -183,11 +183,11 @@ export default {
}).finally(() => { this.loading = false; });
},
getDeptName(deptId) {
const map = { 1: '生产部', 2: '质量部', 3: '销售部' };
const map = { 1: '生产部', 2: '技术部', 3: '销售部' };
return map[deptId] || '部门' + deptId;
},
getDeptLabel() {
const map = { 1: '生产部', 2: '质量部', 3: '销售部' };
const map = { 1: '生产部', 2: '技术部', 3: '销售部' };
return map[this.currentRel.deptId] || '';
},
handleQuery() {
@@ -326,8 +326,8 @@ export default {
.list-item {
display: flex;
align-items: center;
padding: 10px 12px;
flex-direction: column;
padding: 8px 12px;
margin-bottom: 2px;
cursor: pointer;
border-radius: 6px;
@@ -347,15 +347,23 @@ export default {
font-weight: 600;
}
.item-main {
flex: 1;
min-width: 0;
.item-row-top {
display: flex;
flex-direction: column;
gap: 3px;
align-items: center;
justify-content: space-between;
gap: 6px;
}
.item-row-bottom {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 2px;
}
.item-title {
flex: 1;
min-width: 0;
font-size: 13px;
font-weight: 500;
color: #303133;
@@ -369,9 +377,12 @@ export default {
color: #909399;
}
.item-main {
display: none;
}
.item-meta {
flex-shrink: 0;
margin: 0 8px;
display: none;
}
.item-actions {