<template>
|
<el-dialog
|
:title="title"
|
:visible.sync="dialogVisible"
|
width="900px"
|
top="5vh"
|
class="exception-detail-dialog"
|
@close="handleClose"
|
>
|
<!-- 基本信息 -->
|
<div class="info-section">
|
<div class="section-title">基本信息</div>
|
<el-row :gutter="20">
|
<el-col :span="8">
|
<div class="info-item">
|
<span class="label">患者姓名:</span>
|
<span class="value">{{ currentRecord.patientName }}</span>
|
</div>
|
</el-col>
|
<el-col :span="8">
|
<div class="info-item">
|
<span class="label">性别:</span>
|
<span class="value">{{ currentRecord.gender === 1 ? '男' : '女' }}</span>
|
</div>
|
</el-col>
|
<el-col :span="8">
|
<div class="info-item">
|
<span class="label">年龄:</span>
|
<span class="value">{{ currentRecord.age }}岁</span>
|
</div>
|
</el-col>
|
<el-col :span="8">
|
<div class="info-item">
|
<span class="label">联系方式:</span>
|
<span class="value">{{ currentRecord.phone }}</span>
|
</div>
|
</el-col>
|
<el-col :span="8">
|
<div class="info-item">
|
<span class="label">出院科室:</span>
|
<span class="value">{{ currentRecord.dischargeDept }}</span>
|
</div>
|
</el-col>
|
<el-col :span="8">
|
<div class="info-item">
|
<span class="label">出院病区:</span>
|
<span class="value">{{ currentRecord.dischargeWard }}</span>
|
</div>
|
</el-col>
|
<el-col :span="8">
|
<div class="info-item">
|
<span class="label">填写时间:</span>
|
<span class="value">{{ currentRecord.fillTime }}</span>
|
</div>
|
</el-col>
|
<el-col :span="8">
|
<div class="info-item">
|
<span class="label">负责科室:</span>
|
<el-tag type="primary">{{ currentRecord.responsibilityDept }}</el-tag>
|
</div>
|
</el-col>
|
<el-col :span="8">
|
<div class="info-item">
|
<span class="label">处理状态:</span>
|
<el-tag
|
:type="getStatusTagType(currentRecord.processStatus)"
|
effect="dark"
|
>
|
{{ getStatusText(currentRecord.processStatus) }}
|
</el-tag>
|
</div>
|
</el-col>
|
</el-row>
|
</div>
|
|
<!-- 问卷详情 -->
|
<div class="questionnaire-section">
|
<div class="section-title">问卷填写详情</div>
|
<div class="questionnaire-content">
|
<div class="question-item" v-for="(question, index) in questionnaireData" :key="index">
|
<div class="question-header">
|
<span class="question-index">{{ index + 1 }}.</span>
|
<span class="question-text">{{ question.question }}</span>
|
<el-tag
|
size="mini"
|
:type="question.type === 1 ? 'primary' : 'success'"
|
class="question-type"
|
>
|
{{ question.type === 1 ? '单选题' : '多选题' }}
|
</el-tag>
|
</div>
|
<div class="question-options">
|
<el-radio-group
|
v-model="question.answer"
|
v-if="question.type === 1"
|
disabled
|
>
|
<el-radio
|
v-for="option in question.options"
|
:key="option.value"
|
:label="option.value"
|
:class="{ 'unsatisfactory-option': isUnsatisfactoryOption(option.value) }"
|
>
|
{{ option.text }}
|
</el-radio>
|
</el-radio-group>
|
<el-checkbox-group
|
v-model="question.answer"
|
v-else
|
disabled
|
>
|
<el-checkbox
|
v-for="option in question.options"
|
:key="option.value"
|
:label="option.value"
|
:class="{ 'unsatisfactory-option': isUnsatisfactoryOption(option.value) }"
|
>
|
{{ option.text }}
|
</el-checkbox>
|
</el-checkbox-group>
|
</div>
|
<div v-if="question.additional" class="additional-remark">
|
<div class="remark-label">补充说明:</div>
|
<div class="remark-content">{{ question.additional }}</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
|
<!-- 处理记录 -->
|
<div class="process-section">
|
<div class="section-title">处理记录</div>
|
<div class="process-timeline" v-if="processRecords.length > 0">
|
<el-timeline>
|
<el-timeline-item
|
v-for="(record, index) in processRecords"
|
:key="index"
|
:timestamp="record.time"
|
placement="top"
|
>
|
<el-card>
|
<div class="process-item">
|
<div class="process-header">
|
<span class="process-user">{{ record.user }}</span>
|
<el-tag
|
size="small"
|
:type="getStatusTagType(record.status)"
|
>
|
{{ getStatusText(record.status) }}
|
</el-tag>
|
</div>
|
<div class="process-content">
|
<div v-if="record.reportDepts && record.reportDepts.length > 0" class="process-depts">
|
<span class="label">报备科室:</span>
|
<el-tag
|
v-for="dept in record.reportDepts"
|
:key="dept"
|
size="small"
|
type="info"
|
class="dept-tag"
|
>
|
{{ dept }}
|
</el-tag>
|
</div>
|
<div v-if="record.remark" class="process-remark">
|
<span class="label">处理备注:</span>
|
<span class="content">{{ record.remark }}</span>
|
</div>
|
<div v-if="record.attachments && record.attachments.length > 0" class="process-attachments">
|
<span class="label">附件:</span>
|
<el-button
|
v-for="file in record.attachments"
|
:key="file.id"
|
type="text"
|
size="small"
|
icon="el-icon-document"
|
@click="handlePreviewFile(file)"
|
>
|
{{ file.name }}
|
</el-button>
|
</div>
|
</div>
|
</div>
|
</el-card>
|
</el-timeline-item>
|
</el-timeline>
|
</div>
|
<div v-else class="no-record">
|
暂无处理记录
|
</div>
|
</div>
|
|
<span slot="footer" class="dialog-footer">
|
<el-button
|
type="primary"
|
icon="el-icon-edit"
|
@click="handleProcess"
|
v-if="currentRecord.processStatus !== 2"
|
>
|
处理异常
|
</el-button>
|
<el-button @click="dialogVisible = false">关闭</el-button>
|
</span>
|
|
<!-- 处理对话框 -->
|
<el-dialog
|
title="处理异常反馈"
|
:visible.sync="processDialogVisible"
|
width="600px"
|
center
|
append-to-body
|
>
|
<el-form
|
:model="processForm"
|
:rules="processRules"
|
ref="processForm"
|
label-width="100px"
|
size="medium"
|
>
|
<el-form-item label="处理状态" prop="status">
|
<el-select
|
v-model="processForm.status"
|
placeholder="请选择处理状态"
|
style="width: 100%"
|
>
|
<el-option label="处理中" :value="1" />
|
<el-option label="已处理" :value="2" />
|
<el-option label="已驳回" :value="3" />
|
</el-select>
|
</el-form-item>
|
|
<el-form-item label="报备科室" prop="reportDepts">
|
<el-select
|
v-model="processForm.reportDepts"
|
placeholder="请选择报备科室"
|
multiple
|
filterable
|
collapse-tags
|
style="width: 100%"
|
>
|
<el-option
|
v-for="dept in deptList"
|
:key="dept.id"
|
:label="dept.name"
|
:value="dept.id"
|
/>
|
</el-select>
|
</el-form-item>
|
|
<el-form-item label="处理备注" prop="remark">
|
<el-input
|
v-model="processForm.remark"
|
type="textarea"
|
:rows="4"
|
placeholder="请输入处理备注(最多500字)"
|
maxlength="500"
|
show-word-limit
|
/>
|
</el-form-item>
|
|
<el-form-item label="附件上传">
|
<el-upload
|
class="upload-demo"
|
action="#"
|
:on-preview="handleFilePreview"
|
:on-remove="handleFileRemove"
|
:before-remove="beforeFileRemove"
|
:limit="3"
|
:on-exceed="handleFileExceed"
|
:file-list="fileList"
|
>
|
<el-button size="small" type="primary">点击上传</el-button>
|
<div slot="tip" class="el-upload__tip">支持上传图片、文档等附件,单个文件不超过10MB</div>
|
</el-upload>
|
</el-form-item>
|
</el-form>
|
<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>
|
</template>
|
|
<script>
|
export default {
|
name: 'ExceptionDetailDialog',
|
props: {
|
// 是否显示对话框
|
visible: {
|
type: Boolean,
|
default: false
|
},
|
// 记录ID
|
recordId: {
|
type: [Number, String],
|
default: null
|
},
|
// 对话框标题
|
title: {
|
type: String,
|
default: '异常反馈详情'
|
}
|
},
|
data() {
|
return {
|
// 当前记录
|
currentRecord: {},
|
|
// 问卷数据
|
questionnaireData: [],
|
|
// 处理记录
|
processRecords: [],
|
|
// 科室列表
|
deptList: [
|
{ id: 1, name: '心血管内科' },
|
{ id: 2, name: '神经内科' },
|
{ id: 3, name: '普外科' },
|
{ id: 4, name: '骨科' },
|
{ id: 5, name: '妇产科' },
|
{ id: 6, name: '儿科' },
|
{ id: 7, name: '急诊科' },
|
{ id: 8, name: '呼吸内科' }
|
],
|
|
// 处理对话框
|
processDialogVisible: false,
|
processing: false,
|
processForm: {
|
status: '',
|
reportDepts: [],
|
remark: ''
|
},
|
processRules: {
|
status: [
|
{ required: true, message: '请选择处理状态', trigger: 'change' }
|
],
|
remark: [
|
{ required: true, message: '请输入处理备注', trigger: 'blur' },
|
{ min: 5, max: 500, message: '备注长度在 5 到 500 个字符', trigger: 'blur' }
|
]
|
},
|
fileList: [],
|
|
// 加载状态
|
loading: false
|
};
|
},
|
|
computed: {
|
dialogVisible: {
|
get() {
|
return this.visible;
|
},
|
set(val) {
|
this.$emit('update:visible', val);
|
}
|
}
|
},
|
|
watch: {
|
visible: {
|
immediate: true,
|
handler(val) {
|
if (val && this.recordId) {
|
this.loadData();
|
}
|
}
|
}
|
},
|
|
methods: {
|
// 加载数据
|
async loadData() {
|
this.loading = true;
|
try {
|
await Promise.all([
|
this.loadRecordDetail(),
|
this.loadQuestionnaireData(),
|
this.loadProcessRecords()
|
]);
|
} finally {
|
this.loading = false;
|
}
|
},
|
|
// 加载记录详情
|
async loadRecordDetail() {
|
return new Promise(resolve => {
|
setTimeout(() => {
|
// 根据不同的recordId返回不同的mock数据
|
const mockRecords = {
|
1: {
|
id: 1,
|
patientName: '张先生',
|
gender: 1,
|
age: 45,
|
phone: '13800138000',
|
dischargeDept: '心血管内科',
|
dischargeWard: '内科一病区',
|
fillTime: '2024-01-15 10:30:25',
|
responsibilityDept: '心血管内科',
|
processStatus: 0
|
},
|
2: {
|
id: 2,
|
patientName: '李女士',
|
gender: 0,
|
age: 38,
|
phone: '13900139000',
|
dischargeDept: '神经内科',
|
dischargeWard: '内科二病区',
|
fillTime: '2024-01-14 16:20:10',
|
responsibilityDept: '神经内科',
|
processStatus: 0
|
},
|
3: {
|
id: 3,
|
patientName: '王先生',
|
gender: 1,
|
age: 52,
|
phone: '13700137000',
|
dischargeDept: '普外科',
|
dischargeWard: '外科一病区',
|
fillTime: '2024-01-13 09:15:45',
|
responsibilityDept: '普外科',
|
processStatus: 1
|
}
|
};
|
|
this.currentRecord = mockRecords[this.recordId] || {
|
id: 1,
|
patientName: '张先生',
|
gender: 1,
|
age: 45,
|
phone: '13800138000',
|
dischargeDept: '心血管内科',
|
dischargeWard: '内科一病区',
|
fillTime: '2024-01-15 10:30:25',
|
responsibilityDept: '心血管内科',
|
processStatus: 0
|
};
|
resolve();
|
}, 300);
|
});
|
},
|
|
// 加载问卷数据
|
async loadQuestionnaireData() {
|
return new Promise(resolve => {
|
setTimeout(() => {
|
this.questionnaireData = [
|
{
|
question: '您对医护人员的服务态度是否满意?',
|
type: 1,
|
options: [
|
{ value: '非常满意', text: '非常满意' },
|
{ value: '满意', text: '满意' },
|
{ value: '一般', text: '一般' },
|
{ value: '不满意', text: '不满意' },
|
{ value: '非常不满意', text: '非常不满意' }
|
],
|
answer: '不满意',
|
additional: '医生查房时间太短,沟通不够充分,对病情解释不够详细'
|
},
|
{
|
question: '您对医生的诊疗水平和技术能力评价如何?',
|
type: 1,
|
options: [
|
{ value: '非常专业', text: '非常专业' },
|
{ value: '比较专业', text: '比较专业' },
|
{ value: '一般', text: '一般' },
|
{ value: '不够专业', text: '不够专业' },
|
{ value: '非常不专业', text: '非常不专业' }
|
],
|
answer: '比较专业',
|
additional: ''
|
},
|
{
|
question: '您对医院的环境和卫生状况是否满意?',
|
type: 1,
|
options: [
|
{ value: '非常满意', text: '非常满意' },
|
{ value: '满意', text: '满意' },
|
{ value: '一般', text: '一般' },
|
{ value: '不满意', text: '不满意' },
|
{ value: '非常不满意', text: '非常不满意' }
|
],
|
answer: '一般',
|
additional: ''
|
},
|
{
|
question: '您认为医护人员与您的沟通是否充分?',
|
type: 1,
|
options: [
|
{ value: '非常充分', text: '非常充分' },
|
{ value: '比较充分', text: '比较充分' },
|
{ value: '一般', text: '一般' },
|
{ value: '不够充分', text: '不够充分' },
|
{ value: '非常不充分', text: '非常不充分' }
|
],
|
answer: '不够充分',
|
additional: '医生讲解病情时语速太快,没有给足够的时间提问'
|
},
|
{
|
question: '您对等待就诊和治疗的时间是否满意?',
|
type: 1,
|
options: [
|
{ value: '非常满意', text: '非常满意' },
|
{ value: '满意', text: '满意' },
|
{ value: '一般', text: '一般' },
|
{ value: '不满意', text: '不满意' },
|
{ value: '非常不满意', text: '非常不满意' }
|
],
|
answer: '不满意',
|
additional: '预约的9点,实际10点才见到医生'
|
}
|
];
|
resolve();
|
}, 300);
|
});
|
},
|
|
// 加载处理记录
|
async loadProcessRecords() {
|
return new Promise(resolve => {
|
setTimeout(() => {
|
this.processRecords = [
|
{
|
id: 1,
|
time: '2024-01-15 14:20:30',
|
user: '张医生',
|
status: 1, // 处理中
|
reportDepts: ['医务科', '护理部'],
|
remark: '已收到反馈,正在安排相关人员核查情况',
|
attachments: [
|
{ id: 1, name: '初步调查记录.docx' },
|
{ id: 2, name: '患者沟通记录.jpg' }
|
]
|
},
|
{
|
id: 2,
|
time: '2024-01-15 10:45:12',
|
user: '系统',
|
status: 0, // 待处理
|
remark: '系统自动识别为异常反馈,已分配到责任科室',
|
attachments: []
|
}
|
];
|
resolve();
|
}, 300);
|
});
|
},
|
|
// 判断是否为不满意选项
|
isUnsatisfactoryOption(value) {
|
const unsatisfactoryValues = [
|
'不满意',
|
'非常不满意',
|
'不够专业',
|
'非常不专业',
|
'不够充分',
|
'非常不充分'
|
];
|
return unsatisfactoryValues.includes(value);
|
},
|
|
// 获取状态标签类型
|
getStatusTagType(status) {
|
switch (status) {
|
case 0: return 'warning'; // 待处理
|
case 1: return 'primary'; // 处理中
|
case 2: return 'success'; // 已处理
|
case 3: return 'danger'; // 已驳回
|
default: return 'info';
|
}
|
},
|
|
// 获取状态文本
|
getStatusText(status) {
|
switch (status) {
|
case 0: return '待处理';
|
case 1: return '处理中';
|
case 2: return '已处理';
|
case 3: return '已驳回';
|
default: return '未知';
|
}
|
},
|
|
// 处理异常
|
handleProcess() {
|
this.processForm = {
|
status: this.currentRecord.processStatus === 0 ? 1 : this.currentRecord.processStatus,
|
reportDepts: [],
|
remark: ''
|
};
|
this.processDialogVisible = true;
|
},
|
|
// 提交处理
|
async submitProcess() {
|
this.$refs.processForm.validate(async (valid) => {
|
if (valid) {
|
this.processing = true;
|
try {
|
// Mock API调用
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
this.$message.success('处理提交成功');
|
this.processDialogVisible = false;
|
|
// 重新加载数据
|
await this.loadData();
|
|
// 触发父组件刷新
|
this.$emit('processed');
|
} finally {
|
this.processing = false;
|
}
|
}
|
});
|
},
|
|
// 预览文件
|
handlePreviewFile(file) {
|
this.$message.info(`预览文件: ${file.name}`);
|
},
|
|
// 处理对话框关闭
|
handleClose() {
|
this.$emit('close');
|
},
|
|
// 文件上传相关方法
|
handleFilePreview(file) {
|
console.log('预览文件:', file);
|
},
|
|
handleFileRemove(file, fileList) {
|
console.log('移除文件:', file, fileList);
|
},
|
|
beforeFileRemove(file) {
|
return this.$confirm(`确定移除 ${file.name}?`);
|
},
|
|
handleFileExceed(files, fileList) {
|
this.$message.warning(`当前限制选择 3 个文件,本次选择了 ${files.length} 个文件,共选择了 ${files.length + fileList.length} 个文件`);
|
}
|
}
|
};
|
</script>
|
|
<style lang="scss" scoped>
|
.exception-detail-dialog {
|
::v-deep .el-dialog {
|
max-height: 85vh;
|
display: flex;
|
flex-direction: column;
|
|
.el-dialog__body {
|
flex: 1;
|
overflow-y: auto;
|
padding: 20px;
|
}
|
}
|
|
.info-section {
|
margin-bottom: 20px;
|
padding: 20px;
|
background: #f8f9fa;
|
border-radius: 8px;
|
border: 1px solid #ebeef5;
|
|
.section-title {
|
font-size: 16px;
|
font-weight: 600;
|
color: #303133;
|
margin-bottom: 15px;
|
padding-bottom: 10px;
|
border-bottom: 2px solid #409EFF;
|
}
|
|
.info-item {
|
margin-bottom: 12px;
|
display: flex;
|
align-items: center;
|
|
.label {
|
font-size: 14px;
|
color: #606266;
|
min-width: 80px;
|
font-weight: 500;
|
}
|
|
.value {
|
font-size: 14px;
|
color: #303133;
|
font-weight: 500;
|
}
|
}
|
}
|
|
.questionnaire-section {
|
margin-bottom: 20px;
|
padding: 20px;
|
background: #fff;
|
border-radius: 8px;
|
border: 1px solid #ebeef5;
|
|
.section-title {
|
font-size: 16px;
|
font-weight: 600;
|
color: #303133;
|
margin-bottom: 15px;
|
padding-bottom: 10px;
|
border-bottom: 2px solid #409EFF;
|
}
|
|
.questionnaire-content {
|
.question-item {
|
margin-bottom: 20px;
|
padding: 15px;
|
border-radius: 6px;
|
border: 1px solid #ebeef5;
|
transition: all 0.3s;
|
|
&:hover {
|
border-color: #409EFF;
|
box-shadow: 0 2px 12px 0 rgba(64, 158, 255, 0.1);
|
}
|
|
.question-header {
|
display: flex;
|
align-items: center;
|
margin-bottom: 15px;
|
padding-bottom: 10px;
|
border-bottom: 1px dashed #dcdfe6;
|
|
.question-index {
|
font-weight: 600;
|
color: #409EFF;
|
margin-right: 8px;
|
font-size: 15px;
|
}
|
|
.question-text {
|
flex: 1;
|
font-size: 15px;
|
color: #303133;
|
font-weight: 500;
|
line-height: 1.5;
|
}
|
|
.question-type {
|
margin-left: 10px;
|
}
|
}
|
|
.question-options {
|
::v-deep .el-radio-group {
|
display: flex;
|
flex-direction: column;
|
gap: 10px;
|
}
|
|
::v-deep .el-checkbox-group {
|
display: flex;
|
flex-wrap: wrap;
|
gap: 15px;
|
}
|
|
::v-deep .el-radio,
|
::v-deep .el-checkbox {
|
margin: 0;
|
padding: 8px 12px;
|
border-radius: 4px;
|
border: 1px solid #ebeef5;
|
transition: all 0.3s;
|
|
&:hover {
|
background: #f5f7fa;
|
}
|
|
&.unsatisfactory-option {
|
border-color: #e6a23c;
|
background: #fdf6ec;
|
}
|
}
|
}
|
|
.additional-remark {
|
margin-top: 15px;
|
padding: 12px;
|
background: #f0f9ff;
|
border-radius: 6px;
|
border-left: 4px solid #409EFF;
|
|
.remark-label {
|
font-size: 13px;
|
color: #606266;
|
font-weight: 500;
|
margin-bottom: 5px;
|
}
|
|
.remark-content {
|
font-size: 14px;
|
color: #303133;
|
line-height: 1.6;
|
}
|
}
|
}
|
}
|
}
|
|
.process-section {
|
.section-title {
|
font-size: 16px;
|
font-weight: 600;
|
color: #303133;
|
margin-bottom: 15px;
|
padding-bottom: 10px;
|
border-bottom: 2px solid #409EFF;
|
}
|
|
.process-timeline {
|
::v-deep .el-timeline-item {
|
padding-bottom: 20px;
|
|
.el-timeline-item__timestamp {
|
font-size: 13px;
|
color: #909399;
|
}
|
}
|
|
.process-item {
|
.process-header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
margin-bottom: 10px;
|
|
.process-user {
|
font-size: 14px;
|
font-weight: 600;
|
color: #409EFF;
|
}
|
}
|
|
.process-content {
|
.process-depts {
|
margin-bottom: 8px;
|
|
.label {
|
font-size: 13px;
|
color: #606266;
|
margin-right: 5px;
|
}
|
|
.dept-tag {
|
margin-right: 5px;
|
margin-bottom: 5px;
|
}
|
}
|
|
.process-remark {
|
margin-bottom: 8px;
|
|
.label {
|
font-size: 13px;
|
color: #606266;
|
margin-right: 5px;
|
}
|
|
.content {
|
font-size: 13px;
|
color: #303133;
|
line-height: 1.5;
|
}
|
}
|
|
.process-attachments {
|
.label {
|
font-size: 13px;
|
color: #606266;
|
margin-right: 5px;
|
}
|
|
::v-deep .el-button {
|
margin-right: 8px;
|
margin-bottom: 5px;
|
}
|
}
|
}
|
}
|
}
|
|
.no-record {
|
text-align: center;
|
padding: 40px 0;
|
color: #909399;
|
font-style: italic;
|
background: #f8f9fa;
|
border-radius: 6px;
|
}
|
}
|
|
.dialog-footer {
|
display: flex;
|
justify-content: flex-end;
|
align-items: center;
|
gap: 10px;
|
}
|
}
|
</style>
|