<template>
|
<div class="batch-process">
|
<!-- 页面标题 -->
|
<div class="page-header">
|
<div class="header-content">
|
<h2 class="page-title">异常批量处理</h2>
|
<p class="page-description">批量处理选中的异常反馈</p>
|
<div class="header-actions">
|
<el-button
|
type="primary"
|
icon="el-icon-check"
|
@click="handleBatchSubmit"
|
:loading="batchProcessing"
|
:disabled="selectedExceptionIds.length === 0"
|
>
|
批量提交处理 ({{ selectedExceptionIds.length }})
|
</el-button>
|
<el-button type="warning" icon="el-icon-back" @click="handleGoBack">
|
返回异常列表
|
</el-button>
|
</div>
|
</div>
|
</div>
|
|
<!-- 异常列表 -->
|
<div class="list-section">
|
<el-card shadow="never">
|
<div class="filter-section">
|
<el-form
|
:model="filterParams"
|
:inline="true"
|
size="medium"
|
class="filter-form"
|
>
|
<el-form-item label="负责科室">
|
<el-select
|
v-model="filterParams.todeptcode"
|
placeholder="请选择科室"
|
clearable
|
filterable
|
style="width: 200px"
|
>
|
<el-option
|
v-for="dept in deptList"
|
:key="dept.deptCode"
|
:label="dept.label"
|
:value="dept.deptCode"
|
/>
|
</el-select>
|
</el-form-item>
|
<el-form-item label="处理状态">
|
<el-select
|
v-model="filterParams.handleFlag"
|
placeholder="请选择状态"
|
clearable
|
style="width: 200px"
|
>
|
<el-option label="未处理" :value="'0'" />
|
<el-option label="已处理" :value="'1'" />
|
</el-select>
|
</el-form-item>
|
<el-form-item label="满意度类型">
|
<el-select
|
v-model="filterParams.templateType"
|
placeholder="请选择模板类型"
|
clearable
|
style="width: 200px"
|
>
|
<el-option label="语音模板" :value="1" />
|
<el-option label="问卷模板" :value="2" />
|
</el-select>
|
</el-form-item>
|
<el-form-item>
|
<el-button
|
type="primary"
|
icon="el-icon-search"
|
@click="handleFilter"
|
>
|
筛选
|
</el-button>
|
<el-button icon="el-icon-refresh" @click="handleResetFilter">
|
重置
|
</el-button>
|
</el-form-item>
|
</el-form>
|
</div>
|
|
<el-table
|
v-loading="loading"
|
:data="exceptionList"
|
:border="true"
|
style="width: 100%"
|
@selection-change="handleSelectionChange"
|
row-key="id"
|
class="exception-table"
|
>
|
<el-table-column type="selection" width="55" align="center" />
|
|
<el-table-column
|
label="序号"
|
type="index"
|
width="60"
|
align="center"
|
/>
|
|
<el-table-column
|
label="负责科室"
|
prop="todeptname"
|
width="200"
|
align="center"
|
>
|
<template slot-scope="{ row }">
|
<el-tag type="primary" v-if="row.todeptname">{{
|
row.todeptname
|
}}</el-tag>
|
<span v-else class="no-data">未分配</span>
|
</template>
|
</el-table-column>
|
|
<el-table-column label="不满意详情" min-width="250" align="center">
|
<template slot-scope="{ row }">
|
<div class="detail-content">
|
<div class="question-text">
|
<strong>问题:</strong>{{ row.questiontext }}
|
</div>
|
<div class="answer-text">
|
<strong>回答:</strong>{{ row.asrtext || "无回答" }}
|
</div>
|
<div class="matched-text" v-if="row.matchedtext">
|
<strong>解析值:</strong>{{ row.matchedtext }}
|
</div>
|
</div>
|
</template>
|
</el-table-column>
|
|
<el-table-column label="患者信息" width="300" align="center">
|
<template slot-scope="{ row }">
|
<div class="patient-info">
|
<div class="patient-row">
|
<div class="patient-item">
|
<span class="label">姓名:</span>
|
<span class="value">{{ row.patdescJson.sendname }}</span>
|
</div>
|
<div class="patient-item">
|
<span class="label">性别:</span>
|
<span class="value">{{ row.patdescJson.sex }}</span>
|
</div>
|
<div class="patient-item">
|
<span class="label">年龄:</span>
|
<span class="value">{{ row.patdescJson.age }}岁</span>
|
</div>
|
</div>
|
<div class="patient-row">
|
<div class="patient-item full-width">
|
<span class="label">电话:</span>
|
<span class="value">{{ row.patdescJson.phone }}</span>
|
</div>
|
</div>
|
</div>
|
</template>
|
</el-table-column>
|
|
<el-table-column label="填写信息" width="180" align="center">
|
<template slot-scope="{ row }">
|
<div class="fill-info">
|
<div class="info-item">
|
<span class="label">填报时间:</span>
|
<span class="value time">{{
|
formatDateTime(row.createTime)
|
}}</span>
|
</div>
|
<div v-if="row.recordurl" class="info-item">
|
<el-button
|
type="text"
|
size="small"
|
@click="handlePlayAudio(row.recordurl)"
|
icon="el-icon-headset"
|
>
|
播放录音
|
</el-button>
|
</div>
|
</div>
|
</template>
|
</el-table-column>
|
|
<el-table-column
|
label="处理状态"
|
prop="handleFlag"
|
width="100"
|
align="center"
|
>
|
<template slot-scope="{ row }">
|
<el-tag :type="getStatusTagType(row.handleFlag)" effect="dark">
|
{{ getStatusText(row.handleFlag) }}
|
</el-tag>
|
</template>
|
</el-table-column>
|
|
<el-table-column label="最新处理信息" width="180" align="center">
|
<template slot-scope="{ row }">
|
<div v-if="row.handleTime" class="handle-info">
|
<div class="info-item">
|
<span class="label">处理人:</span>
|
<span class="value">{{ row.handleBy || "系统" }}</span>
|
</div>
|
<div class="info-item">
|
<span class="label">处理时间:</span>
|
<span class="value time">{{
|
formatDateTime(row.handleTime)
|
}}</span>
|
</div>
|
<div class="info-item">
|
<span class="label">处理说明:</span>
|
<span class="value">{{ formatHandledesc(row.handledesc) }}</span>
|
</div>
|
</div>
|
<span v-else class="no-data">未处理</span>
|
</template>
|
</el-table-column>
|
|
<el-table-column
|
label="操作"
|
width="210"
|
align="center"
|
fixed="right"
|
>
|
<template slot-scope="{ row }">
|
<el-button
|
type="primary"
|
size="small"
|
icon="el-icon-view"
|
@click="handleViewDetail(row)"
|
>
|
查看详情
|
</el-button>
|
<el-button
|
type="warning"
|
size="small"
|
icon="el-icon-edit"
|
@click="handleProcess(row)"
|
:disabled="row.handleresult === 'resolved'"
|
>
|
处理
|
</el-button>
|
</template>
|
</el-table-column>
|
</el-table>
|
|
<!-- 分页 -->
|
<div class="pagination-section">
|
<el-pagination
|
background
|
layout="total, sizes, prev, pager, next, jumper"
|
:current-page="filterParams.pageNum"
|
:page-size="filterParams.pageSize"
|
:page-sizes="[10, 20, 30, 50]"
|
:total="total"
|
@size-change="handleSizeChange"
|
@current-change="handlePageChange"
|
/>
|
</div>
|
</el-card>
|
</div>
|
|
<!-- 处理对话框 -->
|
<el-dialog
|
title="处理异常反馈"
|
:visible.sync="processDialogVisible"
|
width="60%"
|
top="6vh"
|
center
|
>
|
<div class="flow-dialog-body">
|
<!-- 左半部分:处理信息 -->
|
<div class="flow-left">
|
<el-form
|
:model="processForm"
|
:rules="processRules"
|
ref="processForm"
|
label-width="100px"
|
size="medium"
|
>
|
<el-form-item label="是否流转" prop="isFlow">
|
<el-switch
|
v-model="processForm.isFlow"
|
active-value="1"
|
inactive-value="0"
|
active-text="流转"
|
inactive-text="不流转"
|
/>
|
</el-form-item>
|
|
<el-form-item label="处理状态" prop="handleFlag">
|
<el-select
|
v-model="processForm.handleFlag"
|
placeholder="请选择处理状态"
|
style="width: 100%"
|
>
|
<el-option label="已处理" :value="'1'" />
|
<el-option label="取消处理" :value="'0'" />
|
</el-select>
|
</el-form-item>
|
|
<el-form-item label="报备科室" prop="ccdepts">
|
<el-select
|
v-model="processForm.ccdepts"
|
placeholder="请选择报备科室"
|
multiple
|
filterable
|
collapse-tags
|
style="width: 100%"
|
:disabled="processForm.handleFlag !== '1'"
|
>
|
<el-option
|
v-for="dept in deptList"
|
:key="dept.deptCode"
|
:label="dept.label"
|
:value="dept.deptCode"
|
/>
|
</el-select>
|
</el-form-item>
|
|
<el-form-item label="处理结果" prop="handleresult">
|
<el-select
|
v-model="processForm.handleresult"
|
placeholder="请选择处理结果"
|
style="width: 100%"
|
:disabled="processForm.handleFlag !== '1'"
|
>
|
<el-option label="已解决" value="resolved" />
|
<el-option label="已解释" value="explained" />
|
<el-option label="已转交" value="transferred" />
|
<el-option label="需改进" value="improvement" />
|
<el-option label="已驳回" value="rejected" />
|
</el-select>
|
</el-form-item>
|
|
<el-form-item label="处理说明" prop="handledesc">
|
<el-input
|
v-model="processForm.handledesc"
|
type="textarea"
|
:rows="4"
|
placeholder="请输入处理说明(最多500字)"
|
maxlength="500"
|
show-word-limit
|
:disabled="processForm.handleFlag !== '1'"
|
/>
|
</el-form-item>
|
|
<el-form-item
|
label="最终意见"
|
prop="finaloption"
|
v-if="hasQualityPermission"
|
>
|
<el-input
|
v-model="processForm.finaloption"
|
type="textarea"
|
:rows="3"
|
placeholder="请输入最终处理意见(最多300字)"
|
maxlength="300"
|
show-word-limit
|
/>
|
</el-form-item>
|
</el-form>
|
</div>
|
|
<!-- 右半部分:流转时间线(选中流转后展开) -->
|
<div class="flow-right" v-if="processForm.isFlow === '1'">
|
<el-timeline>
|
<el-timeline-item type="primary" placement="top" color="#5788fe">
|
<div class="flow-step-title">第一步 · 流转发起</div>
|
<el-form label-width="90px" size="small" class="flow-step-form">
|
<el-form-item label="流转人">
|
<el-input
|
v-model="processForm.flowPerson"
|
placeholder="流转人(默认当前用户)"
|
/>
|
</el-form-item>
|
<el-form-item label="流转人编号">
|
<el-input
|
v-model="processForm.flowPersonCode"
|
placeholder="流转人编号(默认当前用户工号)"
|
/>
|
</el-form-item>
|
<el-form-item label="流转时间">
|
<el-date-picker
|
v-model="processForm.flowTime"
|
type="datetime"
|
value-format="yyyy-MM-dd HH:mm:ss"
|
placeholder="流转时间"
|
style="width: 100%"
|
/>
|
</el-form-item>
|
<el-form-item label="流转说明">
|
<el-input
|
v-model="processForm.flowDesc"
|
type="textarea"
|
:rows="2"
|
placeholder="请输入流转说明"
|
/>
|
</el-form-item>
|
<el-form-item label="流转科室">
|
<el-select
|
v-model="processForm.flowDepts"
|
multiple
|
filterable
|
collapse-tags
|
placeholder="请选择流转科室"
|
style="width: 100%"
|
@change="onFlowDeptChange"
|
>
|
<el-option
|
v-for="dept in deptList"
|
:key="dept.deptCode"
|
:label="dept.label"
|
:value="dept.deptCode"
|
/>
|
</el-select>
|
</el-form-item>
|
<el-form-item label="流转科室code">
|
<el-select
|
v-model="processForm.flowDeptCodes"
|
multiple
|
disabled
|
placeholder="选择科室后自动生成"
|
style="width: 100%"
|
>
|
<el-option
|
v-for="code in processForm.flowDeptCodes"
|
:key="code"
|
:label="code"
|
:value="code"
|
/>
|
</el-select>
|
</el-form-item>
|
<el-form-item label="推送负责人">
|
<el-select
|
v-model="processForm.pushUsers"
|
multiple
|
filterable
|
collapse-tags
|
placeholder="请选择推送负责人"
|
style="width: 100%"
|
@change="onPushUserChange"
|
>
|
<el-option
|
v-for="u in userList"
|
:key="u.userId"
|
:label="u.nickName || u.userName"
|
:value="u.userId"
|
/>
|
</el-select>
|
</el-form-item>
|
</el-form>
|
</el-timeline-item>
|
|
<el-timeline-item type="primary" placement="top" color="#e6a23c">
|
<div class="flow-step-title">第二步 · 科室反馈</div>
|
<el-table
|
:data="processForm.flowFeedbacks"
|
border
|
size="mini"
|
v-if="processForm.flowFeedbacks.length > 0"
|
>
|
<el-table-column
|
label="科室"
|
prop="deptName"
|
width="120"
|
align="center"
|
/>
|
<el-table-column label="是否同意" width="130" align="center">
|
<template slot-scope="scope">
|
<el-select
|
v-model="scope.row.agree"
|
placeholder="请选择"
|
size="mini"
|
>
|
<el-option label="同意" value="1" />
|
<el-option label="不同意" value="0" />
|
</el-select>
|
</template>
|
</el-table-column>
|
<el-table-column label="具体说明" min-width="200" align="center">
|
<template slot-scope="scope">
|
<el-input
|
v-model="scope.row.desc"
|
placeholder="请输入具体说明"
|
size="mini"
|
/>
|
</template>
|
</el-table-column>
|
</el-table>
|
<div v-else class="flow-empty-tip">
|
请先在第一步选择流转科室
|
</div>
|
</el-timeline-item>
|
|
<el-timeline-item type="primary" placement="top" color="#67c23a">
|
<div class="flow-step-title">第三步 · 处理结果</div>
|
<el-form label-width="90px" size="small" class="flow-step-form">
|
<el-form-item label="处理人">
|
<el-input
|
v-model="processForm.handleBy"
|
placeholder="处理人"
|
/>
|
</el-form-item>
|
<el-form-item label="处理时间">
|
<el-date-picker
|
v-model="processForm.handleTime"
|
type="datetime"
|
value-format="yyyy-MM-dd HH:mm:ss"
|
placeholder="处理时间"
|
style="width: 100%"
|
/>
|
</el-form-item>
|
</el-form>
|
</el-timeline-item>
|
</el-timeline>
|
</div>
|
</div>
|
<span slot="footer" class="dialog-footer">
|
<el-button @click="processDialogVisible = false">取消</el-button>
|
<el-button type="primary" @click="submitProcess" :loading="processing">
|
提交处理
|
</el-button>
|
</span>
|
</el-dialog>
|
|
<!-- 批量处理对话框 -->
|
<el-dialog
|
title="批量处理异常反馈"
|
:visible.sync="batchDialogVisible"
|
width="600px"
|
center
|
>
|
<el-form
|
:model="batchProcessForm"
|
:rules="processRules"
|
ref="batchProcessForm"
|
label-width="100px"
|
size="medium"
|
>
|
<el-form-item label="处理状态" prop="handleFlag">
|
<el-select
|
v-model="batchProcessForm.handleFlag"
|
placeholder="请选择处理状态"
|
style="width: 100%"
|
>
|
<el-option label="已处理" :value="'1'" />
|
<el-option label="取消处理" :value="'0'" />
|
</el-select>
|
</el-form-item>
|
|
<el-form-item label="报备科室" prop="ccdepts">
|
<el-select
|
v-model="batchProcessForm.ccdepts"
|
placeholder="请选择报备科室"
|
multiple
|
filterable
|
collapse-tags
|
style="width: 100%"
|
:disabled="batchProcessForm.handleFlag !== '1'"
|
>
|
<el-option
|
v-for="dept in deptList"
|
:key="dept.deptCode"
|
:label="dept.label"
|
:value="dept.deptCode"
|
/>
|
</el-select>
|
</el-form-item>
|
|
<el-form-item label="处理结果" prop="handleresult">
|
<el-select
|
v-model="batchProcessForm.handleresult"
|
placeholder="请选择处理结果"
|
style="width: 100%"
|
:disabled="batchProcessForm.handleFlag !== '1'"
|
>
|
<el-option label="已解决" value="resolved" />
|
<el-option label="已解释" value="explained" />
|
<el-option label="已转交" value="transferred" />
|
<el-option label="需改进" value="improvement" />
|
<el-option label="已驳回" value="rejected" />
|
</el-select>
|
</el-form-item>
|
|
<el-form-item label="处理说明" prop="handledesc">
|
<el-input
|
v-model="batchProcessForm.handledesc"
|
type="textarea"
|
:rows="4"
|
placeholder="请输入处理说明(最多500字)"
|
maxlength="500"
|
show-word-limit
|
:disabled="batchProcessForm.handleFlag !== '1'"
|
/>
|
</el-form-item>
|
</el-form>
|
<span slot="footer" class="dialog-footer">
|
<el-button @click="batchDialogVisible = false">取消</el-button>
|
<el-button
|
type="primary"
|
@click="submitBatchProcess"
|
:loading="batchProcessing"
|
>
|
批量提交 ({{ selectedExceptionIds.length }})
|
</el-button>
|
</span>
|
</el-dialog>
|
<!-- 进度对话框 -->
|
<el-dialog
|
title="批量处理进度"
|
:visible.sync="batchProgress.visible"
|
width="400px"
|
:close-on-click-modal="false"
|
:show-close="false"
|
:close-on-press-escape="false"
|
>
|
<div class="progress-content">
|
<el-progress
|
:percentage="batchProgress.percentage"
|
:status="batchProgress.percentage === 100 ? 'success' : ''"
|
/>
|
<div class="progress-info">
|
已处理 {{ batchProgress.processed }}/{{ batchProgress.total }} 条记录
|
</div>
|
</div>
|
</el-dialog>
|
<!-- 异常详情弹框 -->
|
<Details-anomaly
|
:visible="detailDialogVisible"
|
:record-id="selectedRecordId"
|
:title="detailDialogTitle"
|
:record-data="selectedRecordData"
|
@update:visible="handleDetailDialogClose"
|
@processed="handleProcessed"
|
@close="handleDetailDialogClose"
|
/>
|
|
<!-- 录音播放器 -->
|
<audio
|
v-if="audioUrl"
|
:src="audioUrl"
|
ref="audioPlayer"
|
controls
|
style="display: none"
|
/>
|
</div>
|
</template>
|
|
<script>
|
import DetailsAnomaly from "./components/DetailsAnomaly.vue";
|
import { tracelist, traceedit } from "@/api/AiCentre/index";
|
import dayjs from "dayjs";
|
import { deptTreeSelect, listUser } from "@/api/system/user";
|
|
export default {
|
name: "BatchProcess",
|
components: {
|
DetailsAnomaly,
|
},
|
data() {
|
return {
|
// 详情弹框相关
|
detailDialogVisible: false,
|
selectedRecordId: null,
|
selectedRecordData: null,
|
detailDialogTitle: "异常反馈详情",
|
|
// 音频播放
|
audioUrl: "",
|
|
// 当前处理的异常ID
|
currentExceptionId: null,
|
|
// 批量选中的异常ID
|
selectedExceptionIds: [],
|
|
// 过滤参数
|
filterParams: {
|
todeptcode: "",
|
handleFlag: "",
|
templateType: null,
|
scriptids: null,
|
pageNum: 1,
|
pageSize: 10,
|
},
|
|
// 加载状态
|
loading: false,
|
processing: false,
|
batchProcessing: false,
|
|
// 权限控制
|
hasQualityPermission: false, // 是否具有质管权限
|
|
// 科室列表
|
deptList: [],
|
// 用户列表(流转负责人候选)
|
userList: [],
|
|
// 异常列表数据
|
exceptionList: [],
|
total: 0,
|
|
// 处理对话框
|
processDialogVisible: false,
|
processForm: {
|
handleFlag: "",
|
ccdepts: [],
|
handleresult: "",
|
handledesc: "",
|
finaloption: "",
|
// 流转相关
|
isFlow: "0",
|
flowPerson: "",
|
flowPersonCode: "",
|
flowTime: "",
|
flowDesc: "",
|
flowDepts: [],
|
flowDeptNames: [],
|
flowDeptCodes: [],
|
pushUsers: [],
|
pushUserNames: [],
|
pushUserNos: [],
|
flowFeedbacks: [],
|
handleBy: "",
|
handleTime: "",
|
},
|
batchProgress: {
|
visible: false,
|
percentage: 0,
|
processed: 0,
|
total: 0,
|
},
|
processRules: {
|
handleFlag: [
|
{ required: true, message: "请选择处理状态", trigger: "change" },
|
],
|
handleresult: [
|
{
|
required: true,
|
message: "请选择处理结果",
|
trigger: "change",
|
validator: (rule, value, callback) => {
|
if (this.processForm.handleFlag === "1" && !value) {
|
callback(new Error("请选择处理结果"));
|
} else {
|
callback();
|
}
|
},
|
},
|
],
|
handledesc: [
|
{
|
required: true,
|
message: "请输入处理说明",
|
trigger: "blur",
|
validator: (rule, value, callback) => {
|
if (
|
this.processForm.handleFlag === "1" &&
|
(!value || value.trim().length < 3)
|
) {
|
callback(new Error("处理说明至少3个字符"));
|
} else {
|
callback();
|
}
|
},
|
},
|
],
|
},
|
|
// 批量处理对话框
|
batchDialogVisible: false,
|
batchProcessForm: {
|
handleFlag: "",
|
ccdepts: [],
|
handleresult: "",
|
handledesc: "",
|
},
|
};
|
},
|
|
created() {
|
// 从路由参数获取问题ID
|
this.filterParams.scriptids =
|
this.$route.query.questionId || this.$route.query.questionIds || null;
|
// if (this.$route.query.questionId) {
|
// } else if (this.$route.query.questionIds) {
|
// console.log(
|
// this.$route.query.questionIds,
|
// "this.$route.query.questionIds"
|
// );
|
|
this.filterParams.templateType = Number(this.$route.query.type) || null;
|
|
// this.filterParams.scriptid = null;
|
// }
|
this.hasQualityPermission = this.checkQualityPermission();
|
},
|
|
mounted() {
|
this.loadExceptionList();
|
this.getDeptOptions();
|
this.getUserOptions();
|
},
|
|
methods: {
|
// 格式化日期时间
|
formatDateTime(dateTime) {
|
if (!dateTime) return "";
|
try {
|
const date = new Date(dateTime);
|
if (isNaN(date.getTime())) {
|
return dateTime;
|
}
|
return (
|
date.toLocaleDateString().replace(/\//g, "-") +
|
" " +
|
date.toTimeString().split(" ")[0]
|
);
|
} catch (error) {
|
console.error("日期格式化错误:", error);
|
return dateTime;
|
}
|
},
|
/** 查询科室列表 */
|
getDeptOptions() {
|
deptTreeSelect()
|
.then((res) => {
|
if (res.code == 200) {
|
this.deptList = this.flattenArray(res.data) || [];
|
}
|
})
|
.catch((error) => {
|
console.error("获取科室列表失败:", error);
|
this.$message.error("获取科室列表失败");
|
});
|
},
|
/** 查询用户列表(流转负责人候选) */
|
getUserOptions() {
|
listUser({ pageNum: 1, pageSize: 999 })
|
.then((res) => {
|
if (res.code == 200) {
|
this.userList = res.rows || [];
|
}
|
})
|
.catch((error) => {
|
console.error("获取用户列表失败:", error);
|
this.$message.error("获取用户列表失败");
|
});
|
},
|
flattenArray(multiArray) {
|
let result = [];
|
|
function flatten(element) {
|
if (element.children && element.children.length > 0) {
|
element.children.forEach((child) => flatten(child));
|
} else {
|
let item = JSON.parse(JSON.stringify(element));
|
result.push(item);
|
}
|
}
|
|
multiArray.forEach((element) => flatten(element));
|
return result;
|
},
|
// 解析患者描述信息
|
parsePatDesc(patdesc) {
|
if (!patdesc) return [];
|
|
try {
|
const parts = patdesc.split("|");
|
const items = [];
|
|
if (parts[0]) items.push({ label: "姓名", value: parts[0] });
|
if (parts[1]) items.push({ label: "电话", value: parts[1] });
|
if (parts[2]) items.push({ label: "科室", value: parts[2] });
|
|
return items;
|
} catch (error) {
|
console.error("解析患者信息失败:", error);
|
return [];
|
}
|
},
|
|
// 检查质管权限
|
checkQualityPermission() {
|
// 这里可以根据实际权限系统实现
|
const userRoles = this.$store.getters.roles || [];
|
return (
|
userRoles.includes("quality_manager") || userRoles.includes("admin")
|
);
|
},
|
|
// 获取状态标签类型
|
getStatusTagType(handleFlag) {
|
switch (handleFlag) {
|
case "0":
|
return "warning"; // 未处理
|
case "1":
|
return "success"; // 已处理
|
default:
|
return "info";
|
}
|
},
|
|
// 获取状态文本
|
getStatusText(handleFlag) {
|
switch (handleFlag) {
|
case "0":
|
return "未处理";
|
case "1":
|
return "已处理";
|
default:
|
return "未知";
|
}
|
},
|
|
// 播放录音
|
handlePlayAudio(url) {
|
this.audioUrl = url;
|
this.$nextTick(() => {
|
const audioPlayer = this.$refs.audioPlayer;
|
if (audioPlayer) {
|
audioPlayer.play().catch((error) => {
|
console.error("播放失败:", error);
|
this.$message.error("音频播放失败");
|
});
|
}
|
});
|
},
|
|
// 构建查询参数
|
buildQueryParams() {
|
const params = {
|
pageNum: this.filterParams.pageNum,
|
pageSize: this.filterParams.pageSize,
|
};
|
|
if (this.filterParams.todeptcode) {
|
params.todeptcode = this.filterParams.todeptcode;
|
}
|
|
if (this.filterParams.handleFlag !== "") {
|
params.handleFlag = this.filterParams.handleFlag;
|
}
|
|
if (this.filterParams.templateType) {
|
params.templateType = this.filterParams.templateType;
|
}
|
|
// if (this.filterParams.scriptid) {
|
// params.scriptid = this.filterParams.scriptid;
|
// }
|
if (this.filterParams.scriptids) {
|
params.scriptids = this.filterParams.scriptids.split(",");
|
}
|
|
return params;
|
},
|
|
// 加载异常列表
|
async loadExceptionList() {
|
this.loading = true;
|
try {
|
const params = this.buildQueryParams();
|
const response = await tracelist(params);
|
|
if (response && response.code === 200) {
|
this.exceptionList = response.rows || [];
|
this.total = response.total || 0;
|
} else {
|
this.exceptionList = [];
|
this.total = 0;
|
this.$message.error(response?.msg || "加载异常列表失败");
|
}
|
} catch (error) {
|
console.error("加载异常列表失败:", error);
|
this.$message.error("加载异常列表失败,请稍后重试");
|
this.exceptionList = [];
|
this.total = 0;
|
} finally {
|
this.loading = false;
|
}
|
},
|
|
// 处理筛选
|
handleFilter() {
|
this.filterParams.pageNum = 1;
|
this.loadExceptionList();
|
},
|
|
// 重置筛选
|
handleResetFilter() {
|
this.filterParams = {
|
todeptcode: "",
|
handleFlag: "",
|
scriptids:
|
this.$route.query.questionId || this.$route.query.questionIds || null,
|
templateType: Number(this.$route.query.type) || null,
|
|
pageNum: 1,
|
pageSize: 10,
|
};
|
this.selectedExceptionIds = [];
|
this.loadExceptionList();
|
},
|
|
// 处理选择变化
|
handleSelectionChange(selection) {
|
this.selectedExceptionIds = selection.map((item) => item.id);
|
},
|
|
// 处理批量提交
|
handleBatchSubmit() {
|
if (this.selectedExceptionIds.length === 0) {
|
this.$message.warning("请先选择要处理的异常反馈");
|
return;
|
}
|
|
// 重置批量处理表单
|
this.batchProcessForm = {
|
handleFlag: "",
|
ccdepts: [],
|
handleresult: "",
|
handledesc: "",
|
};
|
|
this.batchDialogVisible = true;
|
},
|
|
// 返回异常列表
|
handleGoBack() {
|
// this.$router.push("/satisfaction/exception/list");
|
this.$router.push("/Intelligentcenter/dispose");
|
},
|
|
// 查看详情
|
handleViewDetail(row) {
|
this.selectedRecordId = row.id;
|
this.selectedRecordData = row;
|
|
// 生成弹框标题
|
let title = "异常反馈详情";
|
if (row.patdesc) {
|
const patientName = row.patdescJson.sendname;
|
if (patientName) {
|
title = `${patientName} - ${title}`;
|
}
|
}
|
this.detailDialogTitle = title;
|
|
this.detailDialogVisible = true;
|
},
|
|
// 处理详情弹框关闭
|
handleDetailDialogClose() {
|
this.detailDialogVisible = false;
|
this.selectedRecordId = null;
|
this.selectedRecordData = null;
|
},
|
|
// 处理完成后的回调
|
handleProcessed() {
|
this.loadExceptionList();
|
},
|
|
// 处理单个异常
|
handleProcess(row) {
|
this.currentExceptionId = row.id;
|
|
const nickName = this.$store.state.user.nickName || "";
|
const now = dayjs().format("YYYY-MM-DD HH:mm:ss");
|
|
// 是否流转(后端 circulationStatus:0 不流转 / 1 流转)
|
const isFlow = String(row.circulationStatus) === "1" ? "1" : "0";
|
|
// 报备科室(兼容数组或逗号分隔字符串)
|
const ccdepts = Array.isArray(row.ccdepts)
|
? row.ccdepts
|
: row.ccdepts
|
? row.ccdepts.split(",")
|
: [];
|
|
// 流转科室 code / 名称
|
const flowDeptCodes = row.circulationDeptCode
|
? row.circulationDeptCode.split(",")
|
: [];
|
const flowDeptNames = row.circulationDeptName
|
? row.circulationDeptName.split(",")
|
: [];
|
|
// 推送负责人:后端存工号(userName),下拉 value 用 userId,需回映
|
const pushUserNos = row.circulationDeptPersonCode
|
? row.circulationDeptPersonCode.split(",")
|
: [];
|
const pushUserNames = row.circulationDeptPersonName
|
? row.circulationDeptPersonName.split(",")
|
: [];
|
const pushUsers = pushUserNos.map((no) => {
|
const u = this.userList.find((item) => item.userName === no);
|
return u ? u.userId : no;
|
});
|
|
// 科室反馈:从 handledesc JSON 中还原
|
let flowFeedbacks = [];
|
try {
|
const parsed =
|
typeof row.handledesc === "string" &&
|
row.handledesc.trim().startsWith("{")
|
? JSON.parse(row.handledesc)
|
: null;
|
if (parsed && parsed.flow && Array.isArray(parsed.flow.feedbacks)) {
|
flowFeedbacks = parsed.flow.feedbacks;
|
}
|
} catch (e) {
|
// handledesc 非 JSON,忽略
|
}
|
|
// 流转时间:后端 Date 字段,统一转成 yyyy-MM-dd HH:mm:ss 供日期选择器回显
|
let flowTime = now;
|
if (row.circulationTime) {
|
const d = new Date(
|
typeof row.circulationTime === "number"
|
? row.circulationTime
|
: String(row.circulationTime).replace(" ", "T")
|
);
|
if (!isNaN(d.getTime())) {
|
const pad = (n) => String(n).padStart(2, "0");
|
flowTime =
|
d.getFullYear() +
|
"-" +
|
pad(d.getMonth() + 1) +
|
"-" +
|
pad(d.getDate()) +
|
" " +
|
pad(d.getHours()) +
|
":" +
|
pad(d.getMinutes()) +
|
":" +
|
pad(d.getSeconds());
|
}
|
}
|
|
// 初始化表单数据
|
this.processForm = {
|
handleFlag: "1",
|
ccdepts,
|
handleresult: row.handleresult || "",
|
handledesc: this.formatHandledesc(row.handledesc),
|
finaloption: row.finaloption || "",
|
// 流转相关
|
isFlow,
|
flowPerson: row.circulationPersonName || nickName,
|
flowPersonCode:
|
row.circulationPersonCode || this.$store.state.user.userName || "",
|
flowTime,
|
flowDesc: row.circulationExplain || "",
|
flowDepts: [...flowDeptCodes],
|
flowDeptNames: [...flowDeptNames],
|
flowDeptCodes: [...flowDeptCodes],
|
pushUsers,
|
pushUserNames: [...pushUserNames],
|
pushUserNos: [...pushUserNos],
|
flowFeedbacks,
|
handleBy: nickName,
|
handleTime: now,
|
};
|
|
this.processDialogVisible = true;
|
},
|
|
// 流转科室变化:同步科室名称、科室code,并生成反馈表格
|
onFlowDeptChange(codes) {
|
this.processForm.flowDeptCodes = [...codes];
|
this.processForm.flowDeptNames = codes.map((code) => {
|
const dept = this.deptList.find((item) => item.deptCode === code);
|
return dept ? dept.label : code;
|
});
|
// 生成/维护第二步反馈表格(保留已填写的反馈)
|
this.processForm.flowFeedbacks = codes.map((code) => {
|
const existing = this.processForm.flowFeedbacks.find(
|
(item) => item.deptCode === code
|
);
|
if (existing) return existing;
|
const dept = this.deptList.find((item) => item.deptCode === code);
|
return {
|
deptCode: code,
|
deptName: dept ? dept.label : code,
|
agree: "",
|
desc: "",
|
};
|
});
|
},
|
|
// 推送负责人变化:同步负责人昵称、工号
|
onPushUserChange(ids) {
|
this.processForm.pushUserNames = ids.map((id) => {
|
const user = this.userList.find((u) => String(u.userId) === String(id));
|
return user ? user.nickName || user.userName : "";
|
});
|
this.processForm.pushUserNos = ids.map((id) => {
|
const user = this.userList.find((u) => String(u.userId) === String(id));
|
return user ? user.userName || "" : "";
|
});
|
},
|
|
// 组装 handledesc:流转时以 JSON 形式保存原处理说明与流转说明、科室反馈
|
buildHandledesc() {
|
if (this.processForm.isFlow !== "1") {
|
return this.processForm.handledesc;
|
}
|
const flow = {
|
feedbacks: this.processForm.flowFeedbacks.map((f) => ({
|
deptCode: f.deptCode,
|
deptName: f.deptName,
|
agree: f.agree,
|
desc: f.desc,
|
})),
|
};
|
return JSON.stringify({
|
desc: this.processForm.handledesc,
|
flow,
|
});
|
},
|
|
// 解析 handledesc:若是流转 JSON 则取其中的处理说明,否则原样返回
|
formatHandledesc(val) {
|
if (!val) return "";
|
if (typeof val !== "string") return val;
|
const trimmed = val.trim();
|
if (trimmed.startsWith("{")) {
|
try {
|
const obj = JSON.parse(trimmed);
|
if (obj && obj.desc != null) return obj.desc;
|
} catch (e) {
|
// 不是合法 JSON,按原文本返回
|
}
|
}
|
return val;
|
},
|
|
// 流转时间转为带时区的 ISO 格式,供后端 Date 类型字段反序列化
|
toISODateTime(str) {
|
if (!str) return null;
|
const d = new Date(String(str).replace(" ", "T"));
|
if (isNaN(d.getTime())) return null;
|
const pad = (n) => String(n).padStart(2, "0");
|
const offset = -d.getTimezoneOffset();
|
const sign = offset >= 0 ? "+" : "-";
|
const abs = Math.abs(offset);
|
const tz = sign + pad(Math.floor(abs / 60)) + ":" + pad(abs % 60);
|
return (
|
d.getFullYear() +
|
"-" + pad(d.getMonth() + 1) +
|
"-" + pad(d.getDate()) +
|
"T" + pad(d.getHours()) +
|
":" + pad(d.getMinutes()) +
|
":" + pad(d.getSeconds()) +
|
"." + String(d.getMilliseconds()).padStart(3, "0") +
|
tz
|
);
|
},
|
|
// 提交处理
|
async submitProcess() {
|
this.$refs.processForm.validate(async (valid) => {
|
if (!valid) {
|
return;
|
}
|
|
// 流转时校验必须选择流转科室
|
if (
|
this.processForm.isFlow === "1" &&
|
(!this.processForm.flowDepts || this.processForm.flowDepts.length === 0)
|
) {
|
this.$message.warning("请先选择流转科室");
|
return;
|
}
|
|
this.processing = true;
|
|
try {
|
// 准备提交数据
|
const submitData = {
|
id: this.currentExceptionId,
|
handleFlag: this.processForm.handleFlag,
|
handleresult: this.processForm.handleresult,
|
handledesc: this.buildHandledesc(),
|
finaloption: this.processForm.finaloption,
|
handleBy:
|
this.processForm.handleBy || this.$store.state.user.nickName,
|
handleTime:
|
this.processForm.handleTime ||
|
dayjs().format("YYYY-MM-DD HH:mm:ss"),
|
// 将数组转换为逗号分隔的字符串
|
ccdepts: Array.isArray(this.processForm.ccdepts)
|
? this.processForm.ccdepts.join(",")
|
: this.processForm.ccdepts,
|
// 流转相关主表字段(后端已定稿)
|
circulationPersonName: this.processForm.flowPerson,
|
circulationPersonCode:
|
this.processForm.flowPersonCode ||
|
this.$store.state.user.userName ||
|
"",
|
circulationStatus: this.processForm.isFlow,
|
circulationDeptName: this.processForm.flowDeptNames.join(","),
|
circulationDeptCode: this.processForm.flowDeptCodes.join(","),
|
circulationDeptPersonName: this.processForm.pushUserNames.join(","),
|
circulationDeptPersonCode: this.processForm.pushUserNos.join(","),
|
circulationExplain: this.processForm.flowDesc,
|
circulationTime: this.toISODateTime(this.processForm.flowTime),
|
};
|
// TODO: 这里需要调用实际的处理接口
|
await traceedit(submitData);
|
|
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
this.$message.success("处理提交成功");
|
this.processDialogVisible = false;
|
this.loadExceptionList();
|
} catch (error) {
|
console.error("处理提交失败:", error);
|
this.$message.error("处理提交失败,请稍后重试");
|
} finally {
|
this.processing = false;
|
}
|
});
|
},
|
|
// 提交批量处理
|
async submitBatchProcess() {
|
this.$refs.batchProcessForm.validate(async (valid) => {
|
if (!valid) {
|
return;
|
}
|
|
this.batchProcessing = true;
|
// 显示进度条
|
this.batchProgress = {
|
visible: true,
|
percentage: 0,
|
processed: 0,
|
total: this.selectedExceptionIds.length,
|
};
|
try {
|
// 准备批量提交数据
|
const processData = {
|
handleFlag: this.batchProcessForm.handleFlag,
|
handleresult: this.batchProcessForm.handleresult,
|
handledesc: this.batchProcessForm.handledesc,
|
ccdepts: Array.isArray(this.batchProcessForm.ccdepts)
|
? this.batchProcessForm.ccdepts.join(",")
|
: this.batchProcessForm.ccdepts,
|
};
|
|
// 控制并发数
|
const CONCURRENT_LIMIT = 10; // 同时最多3个请求
|
const totalCount = this.selectedExceptionIds.length;
|
const results = [];
|
let successCount = 0;
|
let failCount = 0;
|
|
this.$message.info(`开始批量处理 ${totalCount} 条记录...`);
|
|
// 分组处理
|
for (
|
let i = 0;
|
i < this.selectedExceptionIds.length;
|
i += CONCURRENT_LIMIT
|
) {
|
const batchIds = this.selectedExceptionIds.slice(
|
i,
|
i + CONCURRENT_LIMIT
|
);
|
|
// 并发处理当前批次
|
const batchPromises = batchIds.map((id) =>
|
traceedit({
|
id: id,
|
...processData,
|
})
|
.then((result) => ({
|
id,
|
success: result && result.code === 200,
|
error: result?.msg,
|
}))
|
.catch((error) => ({
|
id,
|
success: false,
|
error: error.message,
|
}))
|
);
|
|
const batchResults = await Promise.all(batchPromises);
|
results.push(...batchResults);
|
|
// 更新统计
|
batchResults.forEach((result) => {
|
if (result.success) {
|
successCount++;
|
} else {
|
failCount++;
|
console.error(`处理记录 ${result.id} 失败:`, result.error);
|
}
|
});
|
// 更新进度
|
this.batchProgress.processed = i + 1;
|
this.batchProgress.percentage = Math.round(
|
((i + 1) / totalCount) * 100
|
);
|
// 显示进度
|
console.log(
|
`进度: ${Math.min(
|
i + CONCURRENT_LIMIT,
|
totalCount
|
)}/${totalCount}`
|
);
|
}
|
|
// 处理结果提示
|
if (successCount === totalCount) {
|
this.$message.success(`已成功处理全部 ${totalCount} 条异常反馈`);
|
} else {
|
this.$message.warning(
|
`已处理 ${successCount} 条,失败 ${failCount} 条异常反馈`
|
);
|
}
|
|
this.batchDialogVisible = false;
|
this.selectedExceptionIds = [];
|
this.loadExceptionList();
|
} catch (error) {
|
console.error("批量处理失败:", error);
|
this.$message.error("批量处理失败,请稍后重试");
|
} finally {
|
this.batchProcessing = false;
|
this.batchProgress.visible = false;
|
}
|
});
|
},
|
|
// 分页大小变化
|
handleSizeChange(size) {
|
this.filterParams.pageSize = size;
|
this.filterParams.pageNum = 1;
|
this.loadExceptionList();
|
},
|
|
// 页码变化
|
handlePageChange(page) {
|
this.filterParams.pageNum = page;
|
this.loadExceptionList();
|
},
|
},
|
};
|
</script>
|
|
<style lang="scss" scoped>
|
.batch-process {
|
padding: 20px;
|
background-color: #f5f7fa;
|
min-height: 100vh;
|
|
.page-header {
|
margin-bottom: 20px;
|
padding: 20px;
|
background: linear-gradient(135deg, #5788fe 0%, #66b1ff 100%);
|
border-radius: 8px;
|
color: white;
|
|
.header-content {
|
.page-title {
|
margin: 0 0 8px 0;
|
font-size: 20px;
|
font-weight: 600;
|
}
|
|
.page-description {
|
margin: 0 0 20px 0;
|
opacity: 0.9;
|
font-size: 14px;
|
}
|
|
.header-actions {
|
display: flex;
|
gap: 10px;
|
}
|
}
|
}
|
|
.list-section {
|
.filter-section {
|
margin-bottom: 20px;
|
|
.filter-form {
|
display: flex;
|
flex-wrap: wrap;
|
align-items: center;
|
|
::v-deep .el-form-item {
|
margin-bottom: 0;
|
margin-right: 20px;
|
|
&:last-child {
|
margin-right: 0;
|
}
|
}
|
}
|
}
|
|
.exception-table {
|
::v-deep .el-table__header-wrapper {
|
th {
|
background-color: #f8f9fa;
|
font-weight: 600;
|
color: #333;
|
}
|
}
|
|
.detail-content {
|
text-align: left;
|
font-size: 12px;
|
line-height: 1.5;
|
|
.question-text {
|
color: #303133;
|
margin-bottom: 5px;
|
font-weight: 500;
|
}
|
|
.answer-text {
|
color: #f56c6c;
|
margin-bottom: 5px;
|
}
|
|
.matched-text {
|
color: #e6a23c;
|
font-style: italic;
|
}
|
|
strong {
|
color: #606266;
|
font-weight: 600;
|
}
|
}
|
|
.patient-info {
|
.patient-row {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
margin-bottom: 8px;
|
|
&:last-child {
|
margin-bottom: 0;
|
}
|
|
.patient-item {
|
flex: 1;
|
display: flex;
|
justify-content: flex-start;
|
align-items: center;
|
padding: 0 5px;
|
|
&.full-width {
|
flex: 1 0 100%;
|
margin-left: 0;
|
margin-right: 0;
|
}
|
|
.label {
|
font-size: 12px;
|
color: #606266;
|
margin-right: 5px;
|
white-space: nowrap;
|
}
|
|
.value {
|
font-size: 12px;
|
color: #333;
|
font-weight: 500;
|
text-align: right;
|
word-break: break-all;
|
}
|
}
|
}
|
}
|
|
.fill-info,
|
.handle-info {
|
font-size: 12px;
|
|
.info-item {
|
display: flex;
|
justify-content: flex-start;
|
align-items: center;
|
margin-bottom: 5px;
|
padding: 2px 0;
|
|
.label {
|
color: #606266;
|
min-width: 50px;
|
}
|
|
.value {
|
color: #333;
|
font-weight: 500;
|
// text-align: right;
|
flex: 1;
|
|
&.time {
|
color: #909399;
|
font-size: 11px;
|
}
|
}
|
}
|
}
|
|
.no-data {
|
color: #909399;
|
font-style: italic;
|
font-size: 12px;
|
}
|
}
|
|
.pagination-section {
|
display: flex;
|
justify-content: center;
|
padding: 20px 0 0 0;
|
}
|
}
|
}
|
|
// 处理弹框左右布局
|
.flow-dialog-body {
|
display: flex;
|
align-items: stretch;
|
|
.flow-left {
|
flex: 1;
|
min-width: 320px;
|
padding-right: 24px;
|
border-right: 1px solid #ebeef5;
|
}
|
|
.flow-right {
|
flex: 1;
|
min-width: 320px;
|
padding-left: 24px;
|
max-height: 60vh;
|
overflow-y: auto;
|
|
.flow-step-title {
|
font-weight: 600;
|
color: #303133;
|
margin-bottom: 12px;
|
font-size: 14px;
|
}
|
|
.flow-step-form {
|
::v-deep .el-form-item {
|
margin-bottom: 12px;
|
}
|
}
|
|
.flow-empty-tip {
|
padding: 20px;
|
text-align: center;
|
color: #909399;
|
font-size: 13px;
|
}
|
}
|
}
|
|
@media (max-width: 768px) {
|
.batch-process {
|
padding: 10px;
|
|
.page-header {
|
.header-actions {
|
flex-direction: column;
|
align-items: stretch;
|
}
|
}
|
|
.list-section {
|
.filter-section {
|
.filter-form {
|
::v-deep .el-form-item {
|
width: 100%;
|
margin-right: 0;
|
margin-bottom: 10px;
|
}
|
}
|
}
|
}
|
}
|
}
|
</style>
|