<template>
|
<div class="satisfaction-exception-config">
|
<!-- 页面标题 -->
|
<div class="page-header">
|
<div class="header-content">
|
<h2 class="page-title">满意度题目异常处理配置</h2>
|
<p class="page-description">
|
基于模板配置满意度题目的责任科室和报备科室
|
</p>
|
</div>
|
</div>
|
|
<!-- 模板选择区域 -->
|
<div class="template-section">
|
<el-card shadow="never">
|
<div class="template-header">
|
<h3 class="template-title">模板选择</h3>
|
<p class="template-tip">请先选择模板类型和具体模板</p>
|
</div>
|
|
<el-form
|
:model="templateForm"
|
:rules="templateRules"
|
ref="templateForm"
|
label-width="120px"
|
size="medium"
|
>
|
<el-row :gutter="20">
|
<el-col :span="8">
|
<el-form-item label="模板类型" prop="templateType">
|
<el-select
|
v-model="templateForm.templateType"
|
placeholder="请选择模板类型"
|
clearable
|
@change="handleTemplateTypeChange"
|
style="width: 100%"
|
>
|
<el-option label="问卷模板" :value="1" />
|
<el-option label="语音模板" :value="2" />
|
</el-select>
|
</el-form-item>
|
</el-col>
|
|
<el-col :span="8">
|
<el-form-item
|
label="选择模板"
|
prop="templateId"
|
:rules="
|
templateForm.templateType
|
? [
|
{
|
required: true,
|
message: '请选择模板',
|
trigger: 'change',
|
},
|
]
|
: []
|
"
|
>
|
<el-select
|
v-model="templateForm.templateId"
|
placeholder="请选择模板"
|
clearable
|
filterable
|
:disabled="
|
!templateForm.templateType || templateOptionsLoading
|
"
|
@change="handleTemplateChange"
|
style="width: 100%"
|
>
|
<el-option
|
v-for="template in filteredTemplateOptions"
|
:key="template.id"
|
:label="template.templateName"
|
:value="template.id"
|
/>
|
<div
|
v-if="templateOptionsLoading"
|
slot="empty"
|
class="select-loading"
|
>
|
<i class="el-icon-loading"></i>
|
<span>加载中...</span>
|
</div>
|
</el-select>
|
</el-form-item>
|
</el-col>
|
|
<el-col :span="8">
|
<el-form-item>
|
<el-button
|
type="primary"
|
icon="el-icon-search"
|
@click="handleLoadTemplate"
|
:loading="templateLoading"
|
:disabled="!templateForm.templateId"
|
>
|
加载模板题目
|
</el-button>
|
<el-button icon="el-icon-refresh" @click="handleResetTemplate">
|
重置
|
</el-button>
|
</el-form-item>
|
</el-col>
|
</el-row>
|
</el-form>
|
</el-card>
|
</div>
|
|
<!-- 模板信息 -->
|
<div v-if="currentTemplateInfo" class="template-info-section">
|
<el-card shadow="never">
|
<div class="template-info">
|
<div class="info-left">
|
<h3 class="template-name">
|
{{ currentTemplateInfo.templateName }}
|
</h3>
|
<div class="template-meta">
|
<span class="meta-item">
|
<i class="el-icon-s-order"></i>
|
模板类型:{{
|
templateForm.templateType === 1 ? "问卷模板" : "语音模板"
|
}}
|
</span>
|
<span class="meta-item">
|
<i class="el-icon-s-management"></i>
|
题目总数:{{ currentTemplateInfo.questionCount || 0 }}
|
</span>
|
<span class="meta-item">
|
<i class="el-icon-star-on"></i>
|
满意度题目:{{ satisfactionQuestionsCount }}
|
</span>
|
</div>
|
</div>
|
<div class="info-right">
|
<el-tag
|
:type="
|
currentTemplateInfo.templateStatus === 1 ? 'success' : 'info'
|
"
|
size="medium"
|
>
|
{{ currentTemplateInfo.templateStatus === 1 ? "启用" : "停用" }}
|
</el-tag>
|
</div>
|
</div>
|
</el-card>
|
</div>
|
|
<!-- 搜索区域(题目筛选) -->
|
<div v-if="questionList.length > 0" class="search-section">
|
<el-card shadow="never" class="search-container">
|
<el-form :model="queryParams" :inline="true" size="medium">
|
<el-form-item label="问题主题">
|
<el-input
|
v-model="queryParams.scriptTopic"
|
placeholder="请输入问题主题"
|
clearable
|
@keyup.enter.native="handleQuery"
|
/>
|
</el-form-item>
|
<el-form-item label="问题内容">
|
<el-input
|
v-model="queryParams.scriptContent"
|
placeholder="请输入问题内容"
|
clearable
|
@keyup.enter.native="handleQuery"
|
/>
|
</el-form-item>
|
<el-form-item>
|
<el-button
|
type="primary"
|
icon="el-icon-search"
|
@click="handleQuery"
|
>
|
筛选题目
|
</el-button>
|
<el-button icon="el-icon-refresh" @click="resetQuery">
|
重置筛选
|
</el-button>
|
</el-form-item>
|
</el-form>
|
</el-card>
|
</div>
|
|
<!-- 配置列表 -->
|
<div class="config-content">
|
<!-- 批量操作栏 -->
|
<div v-if="questionList.length > 0" class="batch-actions-card">
|
<el-card shadow="never">
|
<div class="batch-actions">
|
<el-button
|
type="success"
|
icon="el-icon-check"
|
:loading="batchSaving"
|
:disabled="!hasChanges || batchSaving"
|
@click="handleBatchSave"
|
size="medium"
|
>
|
{{ batchSaving ? "批量保存中..." : "批量保存配置" }}
|
</el-button>
|
<span v-if="changedCount > 0" class="change-count">
|
有 {{ changedCount }} 项配置需要保存
|
</span>
|
<div class="total-count">
|
共 {{ filteredQuestionList.length }} 条记录
|
</div>
|
</div>
|
</el-card>
|
</div>
|
|
<div v-if="loading" class="loading-wrapper">
|
<div class="loading-spinner">
|
<i class="el-icon-loading"></i>
|
<span>加载中...</span>
|
</div>
|
</div>
|
|
<div
|
v-else-if="questionList.length === 0 && templateForm.templateId"
|
class="empty-wrapper"
|
>
|
<el-empty description="该模板中暂无满意度题目">
|
<p class="empty-tip">
|
请选择其他模板或检查模板中是否包含满意度类型题目(分类ID:
|
404,405,406)
|
</p>
|
</el-empty>
|
</div>
|
|
<!-- 一行一行的卡片列表 -->
|
<div v-else-if="filteredQuestionList.length > 0" class="question-list">
|
<div
|
v-for="(question, index) in filteredQuestionList"
|
:key="question.id"
|
class="question-item"
|
>
|
<el-card
|
shadow="hover"
|
class="question-card"
|
:class="{ 'has-changes': question.hasChanges }"
|
>
|
<!-- 卡片头部 -->
|
<div class="card-header">
|
<div class="header-left">
|
<div class="question-index">
|
<span class="index-number">{{ index + 1 }}</span>
|
<div class="index-line"></div>
|
</div>
|
<div class="question-basic-info">
|
<div class="question-title-section">
|
<h3 class="question-topic" :title="question.scriptTopic">
|
{{ question.scriptTopic || "无主题" }}
|
</h3>
|
<div class="question-tags">
|
<dict-tag
|
:options="askvaluetype"
|
:value="question.scriptType"
|
size="small"
|
/>
|
<el-tag
|
v-if="question.targetname"
|
size="small"
|
type="info"
|
>
|
{{ question.targetname }}
|
</el-tag>
|
</div>
|
</div>
|
<div class="question-content-section">
|
<span class="content-label">题目内容:</span>
|
<span class="content-text">{{
|
question.scriptContent
|
}}</span>
|
</div>
|
</div>
|
</div>
|
<div class="header-right">
|
<el-button
|
type="text"
|
icon="el-icon-view"
|
@click="previewQuestion(question)"
|
size="small"
|
>
|
预览
|
</el-button>
|
</div>
|
</div>
|
|
<!-- 异常处理配置 -->
|
<div class="config-section">
|
<div class="config-title">
|
<i class="el-icon-setting"></i>
|
<span>异常处理配置</span>
|
</div>
|
|
<el-form
|
:model="question.exceptionConfig"
|
:rules="configRules"
|
ref="configForm"
|
label-width="100px"
|
size="small"
|
class="config-form"
|
>
|
<div class="config-fields">
|
<!-- 责任科室(多选) -->
|
<div class="config-field">
|
<el-form-item
|
label="责任科室"
|
prop="responsibilityDept"
|
class="config-item"
|
>
|
<el-select
|
v-model="question.exceptionConfig.responsibilityDept"
|
placeholder="请选择责任科室"
|
filterable
|
clearable
|
multiple
|
collapse-tags
|
style="width: 100%"
|
@change="handleConfigChange(question)"
|
>
|
<el-option
|
v-for="dept in deptOptions"
|
:key="dept.id"
|
:label="dept.label"
|
:value="dept.deptCode"
|
/>
|
</el-select>
|
<div class="config-tip">
|
负责处理该题目反馈的科室,可多选
|
</div>
|
</el-form-item>
|
</div>
|
|
<!-- 报备科室(多选) -->
|
<div class="config-field">
|
<el-form-item
|
label="报备科室"
|
prop="reportDept"
|
class="config-item"
|
>
|
<el-select
|
v-model="question.exceptionConfig.reportDept"
|
placeholder="请选择报备科室"
|
filterable
|
clearable
|
multiple
|
style="width: 100%"
|
@change="handleConfigChange(question)"
|
>
|
<el-option
|
v-for="dept in deptOptions"
|
:key="dept.id"
|
:label="dept.label"
|
:value="dept.deptCode"
|
/>
|
</el-select>
|
<div class="config-tip">
|
需要接收异常反馈的科室,可多选
|
</div>
|
</el-form-item>
|
</div>
|
</div>
|
|
<!-- 当前配置信息 -->
|
<div v-if="question.hasChanges" class="current-config">
|
<div class="config-preview">
|
<div class="preview-item">
|
<span class="preview-label">责任科室:</span>
|
<span class="preview-value">
|
{{
|
getDeptNames(
|
question.exceptionConfig.responsibilityDept || []
|
).join(", ")
|
}}
|
</span>
|
</div>
|
<div class="preview-item">
|
<span class="preview-label">报备科室:</span>
|
<span class="preview-value">
|
{{
|
getDeptNames(
|
question.exceptionConfig.reportDept || []
|
).join(", ")
|
}}
|
</span>
|
</div>
|
</div>
|
</div>
|
|
<!-- 配置状态和操作按钮 -->
|
<div class="config-footer">
|
<div v-if="question.saveStatus" class="save-status">
|
<el-alert
|
:type="question.saveStatus.type"
|
:title="question.saveStatus.message"
|
:closable="false"
|
show-icon
|
:effect="
|
question.saveStatus.type === 'success'
|
? 'dark'
|
: 'light'
|
"
|
size="small"
|
/>
|
</div>
|
|
<div class="config-actions">
|
<el-button
|
type="primary"
|
:loading="question.saving"
|
:disabled="!question.hasChanges"
|
@click="saveSingleConfig(question)"
|
size="small"
|
icon="el-icon-check"
|
>
|
{{ question.saving ? "保存中..." : "保存配置" }}
|
</el-button>
|
<el-button
|
v-if="question.hasChanges"
|
type="text"
|
@click="resetSingleConfig(question)"
|
size="small"
|
>
|
重置
|
</el-button>
|
</div>
|
</div>
|
</el-form>
|
</div>
|
</el-card>
|
</div>
|
</div>
|
</div>
|
|
<!-- 题目预览对话框 -->
|
<el-dialog
|
title="题目预览"
|
:visible.sync="previewVisible"
|
width="600px"
|
center
|
>
|
<div v-if="currentPreview" class="preview-wrapper">
|
<div class="preview-header">
|
<h4>{{ currentPreview.scriptTopic || "无主题" }}</h4>
|
<div class="preview-tags">
|
<dict-tag
|
:options="askvaluetype"
|
:value="currentPreview.scriptType"
|
size="small"
|
/>
|
<el-tag v-if="currentPreview.targetname" size="small" type="info">
|
{{ currentPreview.targetname }}
|
</el-tag>
|
</div>
|
</div>
|
<!-- 模板题目展示 -->
|
|
<div class="preview-content" v-if="templateForm.templateType == 1">
|
<p class="preview-question">{{ currentPreview.scriptContent }}</p>
|
|
<div
|
v-if="
|
currentPreview.scriptType != 3 && currentPreview.scriptType != 4
|
"
|
class="preview-options"
|
>
|
<el-radio-group v-model="previewAnswer">
|
<el-radio
|
v-for="(
|
option, idx
|
) in currentPreview.svyLibTemplateTargetoptions || []"
|
:key="idx"
|
:label="option.optioncontent"
|
class="option-item"
|
>
|
{{ option.optioncontent }}
|
</el-radio>
|
</el-radio-group>
|
</div>
|
|
<div v-else class="preview-textarea">
|
<el-input
|
type="textarea"
|
placeholder="请输入回答"
|
v-model="previewAnswer"
|
:rows="4"
|
/>
|
</div>
|
</div>
|
<!-- 语音题目展示 -->
|
<div class="preview-content" v-else>
|
<p class="preview-question">{{ currentPreview.scriptContent }}</p>
|
|
<div
|
v-if="
|
currentPreview.scriptType != 3 && currentPreview.scriptType != 4
|
"
|
class="preview-options"
|
>
|
<el-radio-group v-model="previewAnswer">
|
<el-radio
|
v-for="(
|
option, idx
|
) in currentPreview.ivrLibaScriptTargetoptionList || []"
|
:key="idx"
|
:label="option.targetvalue"
|
class="option-item"
|
>
|
{{ option.targetvalue }}
|
</el-radio>
|
</el-radio-group>
|
</div>
|
|
<div v-else class="preview-textarea">
|
<el-input
|
type="textarea"
|
placeholder="请输入回答"
|
v-model="previewAnswer"
|
:rows="4"
|
/>
|
</div>
|
</div>
|
</div>
|
<span slot="footer" class="dialog-footer">
|
<el-button @click="previewVisible = false">关闭</el-button>
|
</span>
|
</el-dialog>
|
|
<!-- 保存成功提示 -->
|
<el-dialog
|
title="保存成功"
|
:visible.sync="saveSuccessVisible"
|
width="400px"
|
center
|
>
|
<div class="success-content">
|
<i class="el-icon-success success-icon"></i>
|
<p class="success-text">配置已成功保存!</p>
|
</div>
|
<span slot="footer" class="dialog-footer">
|
<el-button type="primary" @click="saveSuccessVisible = false"
|
>确定</el-button
|
>
|
</span>
|
</el-dialog>
|
</div>
|
</template>
|
|
<script>
|
import {
|
compileissue,
|
compileQtemplate,
|
compileFollowup,
|
getQtemplatelist,
|
getFollowuplist,
|
getvFollowup,
|
getQtemplateobj,
|
selectInfoByConditiony,
|
} from "@/api/AiCentre/index";
|
import { deptTreeSelect } from "@/api/system/user";
|
import store from "@/store";
|
import Pagination from "@/components/Pagination";
|
|
export default {
|
name: "SatisfactionExceptionConfig",
|
components: { Pagination },
|
data() {
|
return {
|
// 模板表单
|
templateForm: {
|
templateType: "",
|
templateId: "",
|
},
|
templateRules: {
|
templateType: [
|
{ required: true, message: "请选择模板类型", trigger: "change" },
|
],
|
},
|
|
// 模板选项
|
questionnaireTemplates: [], // 问卷模板列表
|
followupTemplates: [], // 语音模板列表
|
templateOptionsLoading: false,
|
|
// 当前模板信息
|
currentTemplateInfo: null,
|
templateLoading: false,
|
|
// 查询参数
|
queryParams: {
|
scriptTopic: "",
|
scriptContent: "",
|
},
|
|
// 数据列表
|
questionList: [],
|
loading: false,
|
batchSaving: false,
|
|
// 字典数据
|
askvaluetype: store.getters.askvaluetype || [],
|
qyoptions: store.getters.usable || [],
|
|
// 科室选项
|
deptOptions: [],
|
|
// 预览相关
|
previewVisible: false,
|
currentPreview: null,
|
previewAnswer: "",
|
|
// 保存相关
|
saveSuccessVisible: false,
|
hasChanges: false,
|
changedCount: 0,
|
|
// 满意度分类ID
|
satisfactionCategoryIds: ["404", "405", "406"],
|
|
// 表单验证规则
|
configRules: {
|
responsibilityDept: [
|
{
|
required: true,
|
message: "请至少选择一个责任科室",
|
trigger: "change",
|
},
|
{
|
validator: (rule, value, callback) => {
|
if (!value || value.length === 0) {
|
callback(new Error("请至少选择一个责任科室"));
|
} else {
|
callback();
|
}
|
},
|
trigger: "change",
|
},
|
],
|
reportDept: [
|
{
|
required: true,
|
message: "请至少选择一个报备科室",
|
trigger: "change",
|
},
|
{
|
validator: (rule, value, callback) => {
|
if (!value || value.length === 0) {
|
callback(new Error("请至少选择一个报备科室"));
|
} else {
|
callback();
|
}
|
},
|
trigger: "change",
|
},
|
],
|
},
|
};
|
},
|
computed: {
|
// 根据模板类型过滤模板选项
|
filteredTemplateOptions() {
|
if (this.templateForm.templateType === 1) {
|
return this.questionnaireTemplates;
|
} else if (this.templateForm.templateType === 2) {
|
return this.followupTemplates;
|
}
|
return [];
|
},
|
|
// 满意度题目数量
|
satisfactionQuestionsCount() {
|
return this.questionList.filter((q) =>
|
this.satisfactionCategoryIds.includes(q.categoryid?.toString())
|
).length;
|
},
|
|
// 筛选后的题目列表
|
filteredQuestionList() {
|
let filtered = this.questionList;
|
|
// 筛选满意度题目
|
filtered = filtered.filter((q) =>
|
this.satisfactionCategoryIds.includes(q.categoryid?.toString())
|
);
|
|
// 应用搜索条件
|
if (this.queryParams.scriptTopic) {
|
const keyword = this.queryParams.scriptTopic.toLowerCase();
|
filtered = filtered.filter(
|
(q) => q.scriptTopic && q.scriptTopic.toLowerCase().includes(keyword)
|
);
|
}
|
|
if (this.queryParams.scriptContent) {
|
const keyword = this.queryParams.scriptContent.toLowerCase();
|
filtered = filtered.filter(
|
(q) =>
|
q.scriptContent && q.scriptContent.toLowerCase().includes(keyword)
|
);
|
}
|
|
return filtered;
|
},
|
},
|
created() {
|
this.getDeptOptions();
|
this.loadAllTemplates();
|
},
|
methods: {
|
/** 加载所有模板列表 */
|
loadAllTemplates() {
|
this.templateOptionsLoading = true;
|
|
// 并行加载问卷模板和语音模板
|
Promise.all([
|
this.loadQuestionnaireTemplates(),
|
this.loadFollowupTemplates(),
|
]).finally(() => {
|
this.templateOptionsLoading = false;
|
});
|
},
|
|
/** 查询科室列表 */
|
getDeptOptions() {
|
deptTreeSelect()
|
.then((res) => {
|
if (res.code == 200) {
|
this.deptOptions = this.flattenArray(res.data) || [];
|
}
|
})
|
.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;
|
},
|
|
/** 根据科室编码获取科室名称 */
|
getDeptName(deptCode) {
|
if (!deptCode) return "";
|
const dept = this.deptOptions.find((d) => d.deptCode === deptCode);
|
return dept ? dept.label : deptCode;
|
},
|
|
/** 根据科室编码数组获取科室名称数组 */
|
getDeptNames(deptCodes) {
|
if (!Array.isArray(deptCodes) || deptCodes.length === 0) return [];
|
return deptCodes
|
.map((code) => this.getDeptName(code))
|
.filter((name) => name && name.trim());
|
},
|
|
/** 模板类型变更 */
|
handleTemplateTypeChange() {
|
this.templateForm.templateId = "";
|
this.currentTemplateInfo = null;
|
this.questionList = [];
|
},
|
|
/** 加载问卷模板列表 */
|
loadQuestionnaireTemplates() {
|
return new Promise((resolve) => {
|
getQtemplatelist({ pageSize: 1000 })
|
.then((res) => {
|
if (res.code === 200) {
|
this.questionnaireTemplates = (res.rows || []).map((item) => ({
|
id: item.svyid,
|
templateName: item.svyname,
|
isavailable: item.isavailable,
|
}));
|
} else {
|
this.$message.error(res.msg || "加载问卷模板失败");
|
}
|
resolve();
|
})
|
.catch((error) => {
|
console.error("加载问卷模板失败:", error);
|
this.$message.error("加载问卷模板失败");
|
resolve();
|
});
|
});
|
},
|
|
/** 加载语音模板列表 */
|
loadFollowupTemplates() {
|
return new Promise((resolve) => {
|
getFollowuplist({ pageSize: 1000 })
|
.then((res) => {
|
if (res.code === 200) {
|
this.followupTemplates = (res.rows || []).map((item) => ({
|
id: item.id,
|
templateName: item.templateName,
|
isavailable: item.isavailable,
|
}));
|
} else {
|
this.$message.error(res.msg || "加载语音模板失败");
|
}
|
resolve();
|
})
|
.catch((error) => {
|
console.error("加载语音模板失败:", error);
|
this.$message.error("加载语音模板失败");
|
resolve();
|
});
|
});
|
},
|
|
/** 模板选择变更 */
|
handleTemplateChange(templateId) {
|
if (templateId) {
|
const selectedTemplate = this.filteredTemplateOptions.find(
|
(t) => t.id === templateId
|
);
|
if (selectedTemplate) {
|
this.currentTemplateInfo = {
|
templateName: selectedTemplate.templateName,
|
templateStatus: selectedTemplate.isavailable,
|
questionCount: 0,
|
};
|
}
|
} else {
|
this.currentTemplateInfo = null;
|
this.questionList = [];
|
}
|
},
|
|
/** 加载模板详情和题目 */
|
handleLoadTemplate() {
|
this.$refs.templateForm.validate((valid) => {
|
if (!valid) {
|
this.$message.warning("请先选择模板");
|
return;
|
}
|
|
this.templateLoading = true;
|
this.loading = true;
|
this.questionList = [];
|
|
if (this.templateForm.templateType === 1) {
|
this.loadQuestionnaireTemplateDetail();
|
} else if (this.templateForm.templateType === 2) {
|
this.loadFollowupTemplateDetail();
|
}
|
});
|
},
|
|
/** 加载问卷模板详情 */
|
loadQuestionnaireTemplateDetail() {
|
getQtemplateobj({ svyid: this.templateForm.templateId })
|
.then((res) => {
|
this.templateLoading = false;
|
this.loading = false;
|
|
if (res.code === 200 && res.rows && res.rows.length > 0) {
|
const templateDetail = res.rows[0];
|
|
// 更新模板信息
|
this.currentTemplateInfo = {
|
...templateDetail,
|
templateName: templateDetail.svyname,
|
templateStatus: templateDetail.isavailable,
|
questionCount: templateDetail.svyTemplateLibScripts?.length || 0,
|
};
|
|
// 提取题目列表
|
const questions = templateDetail.svyTemplateLibScripts || [];
|
this.processQuestions(questions);
|
|
this.$message.success(`成功加载 ${questions.length} 个题目`);
|
} else {
|
this.$message.error(res.msg || "加载模板详情失败");
|
}
|
})
|
.catch((error) => {
|
this.templateLoading = false;
|
this.loading = false;
|
console.error("加载问卷模板详情失败:", error);
|
this.$message.error("加载模板详情失败");
|
});
|
},
|
|
/** 加载语音模板详情 */
|
loadFollowupTemplateDetail() {
|
getvFollowup({ id: this.templateForm.templateId })
|
.then((res) => {
|
this.templateLoading = false;
|
this.loading = false;
|
|
if (res.code === 200) {
|
const templateDetail = res.data;
|
|
// 更新模板信息
|
this.currentTemplateInfo = {
|
...this.currentTemplateInfo,
|
templateName: templateDetail.templateName,
|
templateStatus: templateDetail.isavailable,
|
questionCount:
|
templateDetail.ivrLibaTemplateScriptVOList?.length || 0,
|
};
|
|
// 提取题目列表
|
const questions = templateDetail.ivrLibaTemplateScriptVOList || [];
|
this.processQuestions(questions);
|
|
this.$message.success(`成功加载 ${questions.length} 个题目`);
|
} else {
|
this.$message.error(res.msg || "加载模板详情失败");
|
}
|
})
|
.catch((error) => {
|
this.templateLoading = false;
|
this.loading = false;
|
console.error("加载语音模板详情失败:", error);
|
this.$message.error("加载模板详情失败");
|
});
|
},
|
|
/** 处理题目数据 */
|
processQuestions(questions) {
|
this.questionList = questions.map((question) => {
|
// 解析责任科室和报备科室
|
let exceptionConfig = {
|
responsibilityDept: [], // 责任科室编码数组
|
reportDept: [], // 报备科室编码数组
|
};
|
|
// 从题目顶层字段读取数据
|
if (question.dutyDeptCode) {
|
// 从逗号分隔的字符串转为数组
|
exceptionConfig.responsibilityDept = question.dutyDeptCode
|
.split(",")
|
.map((code) => code.trim())
|
.filter((code) => code);
|
}
|
|
if (question.reportDeptCode) {
|
exceptionConfig.reportDept = question.reportDeptCode
|
.split(",")
|
.map((code) => code.trim())
|
.filter((code) => code);
|
}
|
|
return {
|
...question,
|
// 统一字段名
|
id: question.id || question.scriptId,
|
scriptTopic: question.scriptTopic || question.scriptTopic,
|
scriptContent: question.scriptContent || question.scriptContent,
|
scriptType: question.scriptType,
|
isavailable: question.isavailable,
|
targetname: question.targetname,
|
categoryid: question.categoryid || question.categoryid,
|
originalConfig: JSON.parse(JSON.stringify(exceptionConfig)),
|
exceptionConfig: exceptionConfig,
|
hasChanges: false,
|
saving: false,
|
saveStatus: null,
|
};
|
});
|
|
this.updateChangedStatus();
|
},
|
|
/** 重置模板选择 */
|
handleResetTemplate() {
|
this.templateForm = {
|
templateType: "",
|
templateId: "",
|
};
|
this.currentTemplateInfo = null;
|
this.questionList = [];
|
this.resetQuery();
|
this.$refs.templateForm?.clearValidate();
|
},
|
|
/** 配置变更处理 */
|
handleConfigChange(question) {
|
this.$nextTick(() => {
|
const index = this.questionList.findIndex((q) => q.id === question.id);
|
if (index !== -1) {
|
const formRef = this.$refs.configForm && this.$refs.configForm[index];
|
if (formRef) {
|
formRef.validate((valid) => {
|
if (valid) {
|
question.hasChanges = !this.isConfigEqual(
|
question.exceptionConfig,
|
question.originalConfig
|
);
|
this.updateChangedStatus();
|
}
|
});
|
}
|
}
|
});
|
},
|
|
/** 比较配置是否改变 */
|
isConfigEqual(config1, config2) {
|
if (!config1 || !config2) return false;
|
|
const responsibility1 = [...(config1.responsibilityDept || [])]
|
.sort()
|
.join(",")
|
.toLowerCase();
|
const responsibility2 = [...(config2.responsibilityDept || [])]
|
.sort()
|
.join(",")
|
.toLowerCase();
|
const report1 = [...(config1.reportDept || [])]
|
.sort()
|
.join(",")
|
.toLowerCase();
|
const report2 = [...(config2.reportDept || [])]
|
.sort()
|
.join(",")
|
.toLowerCase();
|
|
return responsibility1 === responsibility2 && report1 === report2;
|
},
|
|
/** 更新变更状态 */
|
updateChangedStatus() {
|
const changedItems = this.questionList.filter((q) => q.hasChanges);
|
this.changedCount = changedItems.length;
|
this.hasChanges = changedItems.length > 0;
|
},
|
|
/** 保存单个题目配置 */
|
async saveSingleConfig(question) {
|
if (!question.hasChanges) return;
|
|
const index = this.questionList.findIndex((q) => q.id === question.id);
|
if (index === -1) return;
|
|
const formRef = this.$refs.configForm && this.$refs.configForm[index];
|
if (!formRef) return;
|
|
const valid = await formRef.validate();
|
if (!valid) {
|
this.$message.warning("请先完成必填项");
|
return;
|
}
|
|
question.saving = true;
|
question.saveStatus = null;
|
|
try {
|
// 获取当前模板详情
|
let templateDetail;
|
if (this.templateForm.templateType === 1) {
|
// 问卷模板
|
const res = await getQtemplateobj({
|
svyid: this.templateForm.templateId,
|
});
|
if (res.code !== 200 || !res.rows || res.rows.length === 0) {
|
throw new Error(res.msg || "获取模板详情失败");
|
}
|
templateDetail = res.rows[0];
|
} else if (this.templateForm.templateType === 2) {
|
// 语音模板
|
const res = await getvFollowup({ id: this.templateForm.templateId });
|
if (res.code !== 200) {
|
throw new Error(res.msg || "获取模板详情失败");
|
}
|
templateDetail = res.data;
|
}
|
|
// 更新题目配置
|
let updatedTemplateDetail = { ...templateDetail };
|
let questionsField =
|
this.templateForm.templateType === 1
|
? "svyTemplateLibScripts"
|
: "ivrLibaTemplateScriptVOList";
|
|
const questions = updatedTemplateDetail[questionsField] || [];
|
const questionIndex = questions.findIndex((q) => q.id === question.id);
|
|
if (questionIndex === -1) {
|
throw new Error("未找到题目");
|
}
|
|
// 获取科室名称
|
const responsibilityDeptNames = this.getDeptNames(
|
question.exceptionConfig.responsibilityDept
|
);
|
const reportDeptNames = this.getDeptNames(
|
question.exceptionConfig.reportDept
|
);
|
|
// 直接更新题目顶层字段
|
questions[questionIndex] = {
|
...questions[questionIndex],
|
// 设置Excel要求的字段
|
dutyDeptCode: question.exceptionConfig.responsibilityDept.join(","),
|
dutyDeptName: responsibilityDeptNames.join(","),
|
reportDeptCode: question.exceptionConfig.reportDept.join(","),
|
reportDeptName: reportDeptNames.join(","),
|
};
|
|
// 更新模板
|
updatedTemplateDetail[questionsField] = questions;
|
|
// 保存模板
|
let response;
|
if (this.templateForm.templateType === 1) {
|
response = await compileQtemplate({
|
...updatedTemplateDetail,
|
id: this.templateForm.templateId,
|
isoperation: 2,
|
});
|
} else {
|
response = await compileFollowup({
|
...updatedTemplateDetail,
|
id: this.templateForm.templateId,
|
isoperation: 2,
|
});
|
}
|
|
if (response.code === 200) {
|
this.handleSaveSuccess(question);
|
} else {
|
throw new Error(response.msg || "保存失败");
|
}
|
} catch (error) {
|
console.error("保存失败:", error);
|
question.saveStatus = {
|
type: "error",
|
message: error.message || "保存失败,请稍后重试",
|
};
|
this.$message.error(error.message || "保存失败,请稍后重试");
|
} finally {
|
question.saving = false;
|
}
|
},
|
|
/** 处理保存成功 */
|
/** 处理保存成功 */
|
handleSaveSuccess(question) {
|
// 同时更新题目顶层字段
|
const responsibilityDeptNames = this.getDeptNames(
|
question.exceptionConfig.responsibilityDept
|
);
|
const reportDeptNames = this.getDeptNames(
|
question.exceptionConfig.reportDept
|
);
|
|
// 更新题目本身的字段
|
question.dutyDeptCode =
|
question.exceptionConfig.responsibilityDept.join(",");
|
question.dutyDeptName = responsibilityDeptNames.join(",");
|
question.reportDeptCode = question.exceptionConfig.reportDept.join(",");
|
question.reportDeptName = reportDeptNames.join(",");
|
|
// 更新原始配置
|
question.originalConfig = JSON.parse(
|
JSON.stringify(question.exceptionConfig)
|
);
|
question.hasChanges = false;
|
question.saveStatus = {
|
type: "success",
|
message: "配置保存成功",
|
};
|
|
this.updateChangedStatus();
|
this.$message.success("配置保存成功");
|
|
// 5秒后清除成功提示
|
setTimeout(() => {
|
question.saveStatus = null;
|
}, 5000);
|
},
|
|
/** 重置单个题目配置 */
|
/** 重置单个题目配置 */
|
resetSingleConfig(question) {
|
this.$confirm("确定要重置当前题目的配置吗?", "提示", {
|
confirmButtonText: "确定",
|
cancelButtonText: "取消",
|
type: "warning",
|
})
|
.then(() => {
|
question.exceptionConfig = JSON.parse(
|
JSON.stringify(question.originalConfig)
|
);
|
// 同时重置题目顶层字段
|
const responsibilityDeptNames = this.getDeptNames(
|
question.exceptionConfig.responsibilityDept
|
);
|
const reportDeptNames = this.getDeptNames(
|
question.exceptionConfig.reportDept
|
);
|
|
question.dutyDeptCode =
|
question.exceptionConfig.responsibilityDept.join(",");
|
question.dutyDeptName = responsibilityDeptNames.join(",");
|
question.reportDeptCode =
|
question.exceptionConfig.reportDept.join(",");
|
question.reportDeptName = reportDeptNames.join(",");
|
|
question.hasChanges = false;
|
question.saveStatus = null;
|
this.updateChangedStatus();
|
this.$message.success("配置已重置");
|
})
|
.catch(() => {});
|
},
|
|
/** 批量保存配置 */
|
async handleBatchSave() {
|
if (!this.hasChanges || this.batchSaving) return;
|
|
this.$confirm("确定要保存所有修改过的配置吗?", "批量保存", {
|
confirmButtonText: "确定",
|
cancelButtonText: "取消",
|
type: "warning",
|
})
|
.then(async () => {
|
this.batchSaving = true;
|
|
const changedQuestions = this.questionList.filter(
|
(q) => q.hasChanges
|
);
|
const results = [];
|
|
for (const question of changedQuestions) {
|
try {
|
await this.saveSingleConfig(question);
|
results.push({
|
id: question.id,
|
success:
|
!question.hasChanges &&
|
question.saveStatus?.type === "success",
|
});
|
} catch (error) {
|
results.push({
|
id: question.id,
|
success: false,
|
});
|
}
|
}
|
|
this.batchSaving = false;
|
|
const successCount = results.filter((r) => r.success).length;
|
const failCount = results.length - successCount;
|
|
if (failCount === 0) {
|
this.saveSuccessVisible = true;
|
this.$message.success(`成功保存 ${successCount} 个配置`);
|
} else {
|
this.$message.warning(
|
`成功保存 ${successCount} 个,失败 ${failCount} 个`
|
);
|
}
|
})
|
.catch(() => {
|
this.batchSaving = false;
|
});
|
},
|
|
/** 预览题目 */
|
previewQuestion(question) {
|
this.currentPreview = { ...question };
|
this.previewAnswer = "";
|
this.previewVisible = true;
|
},
|
|
/** 搜索 */
|
handleQuery() {
|
// 仅筛选显示,不需要重新加载
|
},
|
|
/** 重置搜索 */
|
resetQuery() {
|
this.queryParams = {
|
scriptTopic: "",
|
scriptContent: "",
|
};
|
},
|
},
|
};
|
</script>
|
|
<style lang="scss" scoped>
|
.satisfaction-exception-config {
|
min-height: 100%;
|
background-color: #f5f7fa;
|
padding: 20px;
|
|
.page-header {
|
margin-bottom: 20px;
|
padding: 20px;
|
background: linear-gradient(135deg, #409eff 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;
|
opacity: 0.9;
|
font-size: 14px;
|
}
|
}
|
}
|
|
.template-section {
|
margin-bottom: 20px;
|
|
.template-header {
|
margin-bottom: 20px;
|
|
.template-title {
|
margin: 0 0 8px 0;
|
font-size: 16px;
|
font-weight: 600;
|
color: #303133;
|
}
|
|
.template-tip {
|
margin: 0;
|
color: #909399;
|
font-size: 13px;
|
}
|
}
|
|
.select-loading {
|
text-align: center;
|
padding: 10px;
|
color: #909399;
|
|
i {
|
margin-right: 8px;
|
}
|
}
|
}
|
|
.template-info-section {
|
margin-bottom: 20px;
|
|
.template-info {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
padding: 5px 0;
|
|
.info-left {
|
.template-name {
|
margin: 0 0 10px 0;
|
font-size: 18px;
|
font-weight: 600;
|
color: #303133;
|
}
|
|
.template-meta {
|
display: flex;
|
gap: 20px;
|
flex-wrap: wrap;
|
|
.meta-item {
|
display: flex;
|
align-items: center;
|
gap: 5px;
|
font-size: 13px;
|
color: #606266;
|
|
i {
|
font-size: 14px;
|
}
|
}
|
}
|
}
|
}
|
}
|
|
.search-section {
|
margin-bottom: 20px;
|
|
.search-container {
|
border-radius: 8px;
|
|
.el-form {
|
display: flex;
|
flex-wrap: wrap;
|
gap: 16px;
|
align-items: center;
|
}
|
}
|
}
|
|
.config-content {
|
.batch-actions-card {
|
margin-bottom: 20px;
|
|
.batch-actions {
|
display: flex;
|
align-items: center;
|
gap: 20px;
|
padding: 8px 0;
|
|
.change-count {
|
color: #e6a23c;
|
font-size: 14px;
|
font-weight: 500;
|
}
|
|
.total-count {
|
margin-left: auto;
|
color: #909399;
|
font-size: 14px;
|
}
|
}
|
}
|
|
.loading-wrapper {
|
display: flex;
|
justify-content: center;
|
align-items: center;
|
min-height: 400px;
|
|
.loading-spinner {
|
text-align: center;
|
color: #409eff;
|
|
i {
|
font-size: 24px;
|
margin-right: 8px;
|
}
|
|
span {
|
font-size: 16px;
|
}
|
}
|
}
|
|
.empty-wrapper {
|
min-height: 400px;
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
justify-content: center;
|
|
.empty-tip {
|
margin-top: 10px;
|
color: #909399;
|
font-size: 13px;
|
text-align: center;
|
}
|
}
|
|
.question-list {
|
display: flex;
|
flex-direction: column;
|
gap: 16px;
|
}
|
|
.question-item {
|
.question-card {
|
border-radius: 8px;
|
border: 1px solid #ebeef5;
|
transition: all 0.3s ease;
|
|
&.has-changes {
|
border-color: #409eff;
|
box-shadow: 0 2px 12px 0 rgba(64, 158, 255, 0.1);
|
}
|
|
&:hover {
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
}
|
|
.card-header {
|
display: flex;
|
justify-content: space-between;
|
align-items: flex-start;
|
margin-bottom: 20px;
|
padding-bottom: 20px;
|
border-bottom: 1px solid #f0f0f0;
|
|
.header-left {
|
display: flex;
|
gap: 20px;
|
flex: 1;
|
|
.question-index {
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
min-width: 40px;
|
|
.index-number {
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
width: 32px;
|
height: 32px;
|
background: #409eff;
|
color: white;
|
border-radius: 50%;
|
font-size: 14px;
|
font-weight: 600;
|
margin-bottom: 8px;
|
}
|
|
.index-line {
|
width: 2px;
|
height: 100%;
|
background: #e0e0e0;
|
border-radius: 1px;
|
}
|
}
|
|
.question-basic-info {
|
flex: 1;
|
|
.question-title-section {
|
margin-bottom: 12px;
|
|
.question-topic {
|
margin: 0 0 8px 0;
|
font-size: 16px;
|
font-weight: 600;
|
color: #303133;
|
line-height: 1.4;
|
}
|
|
.question-tags {
|
display: flex;
|
gap: 8px;
|
flex-wrap: wrap;
|
}
|
}
|
|
.question-content-section {
|
display: flex;
|
align-items: flex-start;
|
gap: 8px;
|
|
.content-label {
|
color: #606266;
|
font-size: 13px;
|
font-weight: 500;
|
min-width: 80px;
|
}
|
|
.content-text {
|
color: #303133;
|
font-size: 13px;
|
line-height: 1.6;
|
flex: 1;
|
}
|
}
|
}
|
}
|
|
.header-right {
|
flex-shrink: 0;
|
}
|
}
|
|
.config-section {
|
.config-title {
|
display: flex;
|
align-items: center;
|
gap: 8px;
|
margin-bottom: 20px;
|
padding: 8px 12px;
|
background: #f8f9fa;
|
border-radius: 4px;
|
|
i {
|
color: #409eff;
|
font-size: 16px;
|
}
|
|
span {
|
color: #303133;
|
font-weight: 600;
|
font-size: 14px;
|
}
|
}
|
|
.config-form {
|
.config-fields {
|
display: grid;
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
gap: 20px;
|
margin-bottom: 20px;
|
|
.config-field {
|
.config-item {
|
margin-bottom: 0;
|
|
:deep(.el-form-item__label) {
|
font-weight: 500;
|
color: #606266;
|
padding-right: 12px;
|
}
|
|
.config-tip {
|
font-size: 12px;
|
color: #909399;
|
margin-top: 4px;
|
line-height: 1.4;
|
}
|
}
|
}
|
}
|
|
.current-config {
|
margin-bottom: 20px;
|
padding: 15px;
|
background: #f0f9ff;
|
border-radius: 6px;
|
border: 1px solid #d0ebff;
|
|
.config-preview {
|
.preview-item {
|
display: flex;
|
align-items: flex-start;
|
margin-bottom: 8px;
|
|
&:last-child {
|
margin-bottom: 0;
|
}
|
|
.preview-label {
|
font-size: 13px;
|
color: #606266;
|
font-weight: 500;
|
min-width: 80px;
|
}
|
|
.preview-value {
|
font-size: 13px;
|
color: #303133;
|
line-height: 1.5;
|
flex: 1;
|
}
|
}
|
}
|
}
|
|
.config-footer {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
padding-top: 20px;
|
border-top: 1px dashed #dcdfe6;
|
|
.save-status {
|
flex: 1;
|
margin-right: 20px;
|
|
.el-alert {
|
padding: 8px 16px;
|
border-radius: 4px;
|
}
|
}
|
|
.config-actions {
|
display: flex;
|
align-items: center;
|
gap: 12px;
|
flex-shrink: 0;
|
|
.el-button {
|
min-width: 100px;
|
}
|
}
|
}
|
}
|
}
|
}
|
}
|
}
|
|
.preview-wrapper {
|
.preview-header {
|
margin-bottom: 20px;
|
|
h4 {
|
margin: 0 0 12px 0;
|
color: #303133;
|
font-size: 18px;
|
font-weight: 600;
|
}
|
|
.preview-tags {
|
display: flex;
|
gap: 8px;
|
flex-wrap: wrap;
|
}
|
}
|
|
.preview-content {
|
.preview-question {
|
margin-bottom: 20px;
|
padding: 16px;
|
background: #f8f9fa;
|
border-radius: 4px;
|
color: #606266;
|
line-height: 1.6;
|
}
|
|
.preview-options {
|
.option-item {
|
display: block;
|
margin-bottom: 12px;
|
padding: 12px;
|
border-radius: 4px;
|
border: 1px solid #ebeef5;
|
transition: all 0.3s;
|
|
&:hover {
|
background: #f5f7fa;
|
border-color: #409eff;
|
}
|
|
&:last-child {
|
margin-bottom: 0;
|
}
|
}
|
}
|
|
.preview-textarea {
|
.el-textarea__inner {
|
resize: none;
|
}
|
}
|
}
|
}
|
|
.success-content {
|
text-align: center;
|
padding: 20px 0;
|
|
.success-icon {
|
color: #67c23a;
|
font-size: 48px;
|
margin-bottom: 20px;
|
}
|
|
.success-text {
|
font-size: 16px;
|
color: #606266;
|
margin: 0;
|
}
|
}
|
}
|
|
@media (max-width: 768px) {
|
.satisfaction-exception-config {
|
padding: 12px;
|
|
.page-header {
|
padding: 16px;
|
margin-bottom: 16px;
|
}
|
|
.template-info {
|
flex-direction: column;
|
align-items: flex-start;
|
gap: 10px;
|
}
|
|
.search-card {
|
margin-bottom: 16px;
|
}
|
|
.config-content {
|
.batch-actions-card {
|
margin-bottom: 16px;
|
}
|
|
.question-item {
|
.question-card {
|
.card-header {
|
flex-direction: column;
|
gap: 12px;
|
|
.header-left {
|
flex-direction: column;
|
gap: 12px;
|
|
.question-index {
|
flex-direction: row;
|
align-items: center;
|
min-width: auto;
|
|
.index-number {
|
margin-bottom: 0;
|
margin-right: 12px;
|
}
|
|
.index-line {
|
width: 100%;
|
height: 2px;
|
}
|
}
|
}
|
}
|
|
.config-section {
|
.config-form {
|
.config-fields {
|
grid-template-columns: 1fr;
|
gap: 16px;
|
}
|
|
.current-config {
|
padding: 12px;
|
}
|
|
.config-footer {
|
flex-direction: column;
|
align-items: stretch;
|
gap: 12px;
|
|
.save-status {
|
margin-right: 0;
|
margin-bottom: 8px;
|
}
|
}
|
}
|
}
|
}
|
}
|
}
|
}
|
}
|
</style>
|