<template>
|
<div class="satisfaction-statistics">
|
<!-- 查询条件区域 -->
|
<div class="query-section">
|
<el-card shadow="never">
|
<el-form
|
:model="queryParams"
|
ref="queryForm"
|
size="medium"
|
:inline="true"
|
label-width="100px"
|
class="query-form"
|
>
|
<el-form-item label="统计类型" prop="patientSource">
|
<el-select
|
v-model="queryParams.type"
|
placeholder="请选择统计类型"
|
clearable
|
style="width: 100%"
|
>
|
<el-option label="问卷类型" :value="2" />
|
<el-option label="语音类型" :value="1" />
|
<el-option label="全部" :value="null" />
|
</el-select>
|
</el-form-item>
|
|
<el-form-item label="科室" prop="deptCode">
|
<el-select
|
v-model="queryParams.deptCode"
|
placeholder="请选择科室"
|
clearable
|
filterable
|
style="width: 200px"
|
@change="handleDeptChange"
|
>
|
<el-option
|
v-for="dept in deptList"
|
:key="dept.value"
|
:label="dept.label"
|
:value="dept.value"
|
/>
|
</el-select>
|
</el-form-item>
|
|
<el-form-item label="病区" prop="wardCode">
|
<el-select
|
v-model="queryParams.wardCode"
|
placeholder="请选择病区"
|
clearable
|
filterable
|
style="width: 200px"
|
@change="handleWardChange"
|
>
|
<el-option
|
v-for="ward in wardList"
|
:key="ward.value"
|
:label="ward.label"
|
:value="ward.value"
|
/>
|
</el-select>
|
</el-form-item>
|
|
<el-form-item label="统计时间" prop="dateRange">
|
<el-date-picker
|
v-model="queryParams.dateRange"
|
type="daterange"
|
range-separator="至"
|
start-placeholder="开始日期"
|
end-placeholder="结束日期"
|
value-format="yyyy-MM-dd"
|
:picker-options="pickerOptions"
|
style="width: 380px"
|
/>
|
</el-form-item>
|
|
<el-form-item>
|
<el-button
|
type="primary"
|
icon="el-icon-search"
|
@click="handleSearch"
|
:loading="loading"
|
>
|
查询
|
</el-button>
|
<el-button icon="el-icon-refresh" @click="handleReset">
|
重置
|
</el-button>
|
</el-form-item>
|
</el-form>
|
</el-card>
|
</div>
|
|
<!-- 满意度类型统计图表 -->
|
<div class="chart-section">
|
<el-card shadow="never">
|
<div class="chart-container">
|
<div class="chart-header">
|
<h3 class="chart-title">满意度类型填报比例统计</h3>
|
<div class="statistic-info">
|
<div class="statistic-item">
|
<span class="statistic-label">发送问卷总量:</span>
|
<span class="statistic-value">{{
|
totalSendCount.toLocaleString()
|
}}</span>
|
</div>
|
<div class="statistic-item">
|
<span class="statistic-label">回收问卷总量:</span>
|
<span class="statistic-value">{{
|
totalReceiveCount.toLocaleString()
|
}}</span>
|
</div>
|
<div class="statistic-item">
|
<span class="statistic-label">总体回收率:</span>
|
<span class="statistic-value">{{
|
formatPercent(overallRecoveryRate)
|
}}</span>
|
</div>
|
</div>
|
</div>
|
<div
|
id="satisfactionBarChart"
|
style="width: 100%; height: 400px"
|
></div>
|
</div>
|
</el-card>
|
</div>
|
|
<!-- Tab标签页 -->
|
<div class="tab-section">
|
<el-card shadow="never">
|
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
|
<el-tab-pane label="题目明细统计" name="questionDetail">
|
<!-- 题目明细表格 -->
|
<div class="detail-table-section">
|
<el-table
|
v-loading="detailLoading"
|
:data="questionDetailData"
|
:border="true"
|
style="width: 100%"
|
row-class-name="question-row"
|
>
|
<el-table-column type="expand" width="60">
|
<template slot-scope="{ row }">
|
<div class="option-detail">
|
<el-table
|
:data="row.options"
|
:border="true"
|
style="width: 100%"
|
class="inner-table"
|
>
|
<el-table-column
|
label="选项"
|
prop="optionText"
|
align="center"
|
min-width="200"
|
/>
|
<el-table-column
|
label="选择人数"
|
prop="chosenQuantity"
|
align="center"
|
min-width="120"
|
/>
|
<el-table-column
|
label="选择比例"
|
prop="chosenPercentage"
|
align="center"
|
min-width="120"
|
>
|
<template slot-scope="{ row: option }">
|
{{ formatPercent(option.chosenPercentage) }}
|
</template>
|
</el-table-column>
|
</el-table>
|
</div>
|
</template>
|
</el-table-column>
|
|
<el-table-column
|
label="序号"
|
type="index"
|
align="center"
|
width="60"
|
/>
|
|
<el-table-column
|
label="题目"
|
prop="scriptContent"
|
align="center"
|
min-width="300"
|
>
|
<template slot-scope="{ row }">
|
<span>{{ row.scriptContent }}</span>
|
<el-tag
|
:type="row.scriptType === 1 ? 'primary' : 'success'"
|
size="mini"
|
style="margin-left: 5px"
|
>
|
{{ row.scriptType === 1 ? "单选题" : "多选题" }}
|
</el-tag>
|
</template>
|
</el-table-column>
|
|
<el-table-column
|
label="平均得分"
|
prop="averageScore"
|
align="center"
|
width="120"
|
>
|
<template slot-scope="{ row }">
|
<span class="score-text">{{
|
row.averageScore.toFixed(1)
|
}}</span>
|
</template>
|
</el-table-column>
|
|
<el-table-column
|
label="最高得分"
|
prop="maxScore"
|
align="center"
|
width="120"
|
>
|
<template slot-scope="{ row }">
|
<span class="score-text">{{
|
row.maxScore.toFixed(1)
|
}}</span>
|
</template>
|
</el-table-column>
|
|
<el-table-column
|
label="最低得分"
|
prop="minScore"
|
align="center"
|
width="120"
|
>
|
<template slot-scope="{ row }">
|
<span class="score-text">{{
|
row.minScore.toFixed(1)
|
}}</span>
|
</template>
|
</el-table-column>
|
|
<el-table-column
|
label="答题人数"
|
prop="answerCount"
|
align="center"
|
width="100"
|
/>
|
|
<el-table-column
|
label="未答题人数"
|
prop="unanswerCount"
|
align="center"
|
width="100"
|
>
|
<template slot-scope="{ row }">
|
{{ row.noAnswerPerson }}
|
</template>
|
</el-table-column>
|
|
<el-table-column
|
label="答题率"
|
prop="answerRate"
|
align="center"
|
width="100"
|
>
|
<template slot-scope="{ row }">
|
{{ formatPercent(row.answerRate) }}
|
</template>
|
</el-table-column>
|
</el-table>
|
|
<!-- 综合得分行 -->
|
<div class="summary-row">
|
<div class="summary-content">
|
<div class="summary-item">
|
<span class="label">总答题人数:</span>
|
<span class="value">{{ totalAnswerCount }}</span>
|
</div>
|
<div class="summary-item">
|
<span class="label">总答题率:</span>
|
<span class="value">{{
|
formatPercent(totalAnswerRate)
|
}}</span>
|
</div>
|
</div>
|
</div>
|
|
<!-- 分页 -->
|
<div
|
class="pagination-section"
|
v-if="questionDetailData.length > 0"
|
>
|
<el-pagination
|
background
|
layout="total, sizes, prev, pager, next, jumper"
|
:current-page="detailQueryParams.pageNum"
|
:page-size="detailQueryParams.pageSize"
|
:page-sizes="[10, 20, 30]"
|
:total="detailTotal"
|
@size-change="handleDetailSizeChange"
|
@current-change="handleDetailPageChange"
|
/>
|
</div>
|
</div>
|
</el-tab-pane>
|
|
<el-tab-pane label="各类型统计明细" name="typeDetail">
|
<!-- 各类型统计明细表格 -->
|
<div class="type-detail-section">
|
<el-table
|
v-loading="typeDetailLoading"
|
:data="typeDetailData"
|
:border="true"
|
style="width: 100%"
|
class="type-detail-table"
|
>
|
<el-table-column
|
label="序号"
|
type="index"
|
align="center"
|
width="60"
|
/>
|
|
<el-table-column
|
label="满意度类型"
|
prop="typeName"
|
align="center"
|
min-width="150"
|
>
|
<template slot-scope="{ row }">
|
<div class="type-name-cell">
|
<span class="type-name">{{ row.typeName }}</span>
|
<el-tag
|
v-if="row.isSpecial"
|
type="warning"
|
size="mini"
|
style="margin-left: 5px"
|
>
|
特殊
|
</el-tag>
|
</div>
|
</template>
|
</el-table-column>
|
|
<el-table-column
|
label="发送问卷数"
|
prop="sendCount"
|
align="center"
|
width="120"
|
>
|
<template slot-scope="{ row }">
|
<span class="number-text">{{
|
row.sendCount.toLocaleString()
|
}}</span>
|
</template>
|
</el-table-column>
|
|
<el-table-column
|
label="回收问卷数"
|
prop="receiveCount"
|
align="center"
|
width="120"
|
>
|
<template slot-scope="{ row }">
|
<span class="number-text">{{
|
row.receiveCount.toLocaleString()
|
}}</span>
|
</template>
|
</el-table-column>
|
|
<el-table-column
|
label="回收率"
|
prop="recoveryRate"
|
align="center"
|
width="120"
|
>
|
<template slot-scope="{ row }">
|
<span
|
class="rate-text"
|
:class="getRateClass(row.recoveryRate)"
|
>
|
{{ formatPercent(row.recoveryRate) }}
|
</span>
|
</template>
|
</el-table-column>
|
|
<el-table-column
|
label="平均分"
|
prop="averageScore"
|
align="center"
|
width="120"
|
>
|
<template slot-scope="{ row }">
|
<span class="score-text">{{
|
row.averageScore.toFixed(1)
|
}}</span>
|
</template>
|
</el-table-column>
|
|
<el-table-column
|
label="满意度等级"
|
prop="satisfactionLevel"
|
align="center"
|
width="120"
|
>
|
<template slot-scope="{ row }">
|
<el-tag
|
:type="getLevelTagType(row.satisfactionLevel)"
|
effect="dark"
|
size="small"
|
>
|
{{ row.satisfactionLevel }}
|
</el-tag>
|
</template>
|
</el-table-column>
|
|
<el-table-column
|
label="趋势"
|
prop="trend"
|
align="center"
|
width="120"
|
>
|
<template slot-scope="{ row }">
|
<div class="trend-cell">
|
<i
|
v-if="row.trend === 'up'"
|
class="el-icon-top trend-up"
|
:style="{ color: '#67C23A' }"
|
/>
|
<i
|
v-else-if="row.trend === 'down'"
|
class="el-icon-bottom trend-down"
|
:style="{ color: '#F56C6C' }"
|
/>
|
<i
|
v-else
|
class="el-icon-minus trend-stable"
|
:style="{ color: '#909399' }"
|
/>
|
<span class="trend-text">{{
|
row.trend === "up"
|
? "上升"
|
: row.trend === "down"
|
? "下降"
|
: "稳定"
|
}}</span>
|
</div>
|
</template>
|
</el-table-column>
|
|
<el-table-column
|
label="操作"
|
align="center"
|
width="120"
|
fixed="right"
|
>
|
<template slot-scope="{ row }">
|
<el-button
|
type="text"
|
size="small"
|
@click="handleTypeDetail(row)"
|
>
|
详情
|
</el-button>
|
<el-button
|
type="text"
|
size="small"
|
@click="handleExportData(row)"
|
>
|
导出
|
</el-button>
|
</template>
|
</el-table-column>
|
</el-table>
|
|
<!-- 类型统计汇总 -->
|
<div class="type-summary-row">
|
<div class="type-summary-content">
|
<div class="type-summary-item">
|
<span class="label">类型总数:</span>
|
<span class="value">{{ typeDetailData.length }}</span>
|
</div>
|
<div class="type-summary-item">
|
<span class="label">平均回收率:</span>
|
<span class="value">{{
|
formatPercent(averageRecoveryRate)
|
}}</span>
|
</div>
|
<div class="type-summary-item">
|
<span class="label">类型平均分:</span>
|
<span class="value">{{ averageTypeScore.toFixed(1) }}</span>
|
</div>
|
<div class="type-summary-item">
|
<span class="label">高满意度类型:</span>
|
<span class="value high-count"
|
>{{ highSatisfactionCount }} 个</span
|
>
|
</div>
|
</div>
|
</div>
|
</div>
|
</el-tab-pane>
|
</el-tabs>
|
</el-card>
|
</div>
|
</div>
|
</template>
|
|
<script>
|
import * as echarts from "echarts";
|
import { statistics, satisfactionGraph } from "@/api/system/user";
|
import store from "@/store";
|
|
export default {
|
name: "SatisfactionStatistics",
|
data() {
|
return {
|
// 查询参数
|
queryParams: {
|
type: 2,
|
patientSource: "",
|
deptCode: "",
|
wardCode: "",
|
dateRange: [],
|
},
|
|
// 当前激活的tab
|
activeTab: "questionDetail",
|
|
// 患者来源选项
|
patientSourceList: [
|
{ value: "1", label: "门诊" },
|
{ value: "2", label: "住院" },
|
{ value: "3", label: "急诊" },
|
{ value: "4", label: "出院" },
|
],
|
|
// 科室列表
|
deptList: [],
|
|
// 病区列表
|
wardList: [],
|
|
// 图表实例
|
barChart: null,
|
|
// 加载状态
|
loading: false,
|
detailLoading: false,
|
typeDetailLoading: false,
|
|
// 题目明细数据
|
questionDetailData: [],
|
|
// 题目明细查询参数
|
detailQueryParams: {
|
pageNum: 1,
|
pageSize: 10,
|
},
|
|
// 题目明细总数
|
detailTotal: 0,
|
|
// 综合得分
|
totalScore: 0,
|
totalAnswerCount: 0,
|
totalAnswerRate: 0,
|
|
// 各类型统计明细数据
|
typeDetailData: [],
|
|
// 柱状图数据
|
chartData: [],
|
|
// 统计信息
|
totalSendCount: 0,
|
totalReceiveCount: 0,
|
overallRecoveryRate: 0,
|
|
// 类型统计汇总
|
averageRecoveryRate: 0,
|
averageTypeScore: 0,
|
highSatisfactionCount: 0,
|
|
// 日期选择器选项
|
pickerOptions: {
|
shortcuts: [
|
{
|
text: "最近一周",
|
onClick(picker) {
|
const end = new Date();
|
const start = new Date();
|
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
|
picker.$emit("pick", [start, end]);
|
},
|
},
|
{
|
text: "最近一个月",
|
onClick(picker) {
|
const end = new Date();
|
const start = new Date();
|
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
|
picker.$emit("pick", [start, end]);
|
},
|
},
|
{
|
text: "最近三个月",
|
onClick(picker) {
|
const end = new Date();
|
const start = new Date();
|
start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
|
picker.$emit("pick", [start, end]);
|
},
|
},
|
],
|
disabledDate(time) {
|
return time.getTime() > Date.now();
|
},
|
},
|
|
// 满意度类型数据
|
satisfactionTypes: [
|
{ id: 401, name: "出院满意度", color: "#36B37E" },
|
{ id: 402, name: "住院满意度", color: "#4CAF50" },
|
{ id: 403, name: "门诊满意度", color: "#409EFF" },
|
{ id: 404, name: "常用满意度", color: "#FF9D4D" },
|
],
|
|
// 新增:默认服务类型数组
|
defaultServiceTypes: ["6", "14", "15", "16"],
|
|
// 新增:基础模板问题ID集合
|
scriptIds: [],
|
|
// 新增:模板ID
|
templateId: null,
|
};
|
},
|
|
computed: {
|
// 计算查询开始时间
|
startTime() {
|
if (this.queryParams.dateRange && this.queryParams.dateRange[0]) {
|
return this.queryParams.dateRange[0];
|
}
|
// 默认最近7天
|
const date = new Date();
|
date.setDate(date.getDate() - 7);
|
return this.formatDate(date);
|
},
|
|
// 计算查询结束时间
|
endTime() {
|
if (this.queryParams.dateRange && this.queryParams.dateRange[1]) {
|
return this.queryParams.dateRange[1];
|
}
|
// 默认今天
|
return this.formatDate(new Date());
|
},
|
|
// 计算科室编码数组
|
deptCodes() {
|
if (this.queryParams.deptCode) {
|
return [this.queryParams.deptCode];
|
}
|
return this.deptList.map((dept) => dept.value);
|
},
|
|
// 计算病区编码数组
|
hospitalDistrictCodes() {
|
if (this.queryParams.wardCode) {
|
return [this.queryParams.wardCode];
|
}
|
return this.wardList.map((ward) => ward.value);
|
},
|
},
|
|
mounted() {
|
this.initData();
|
},
|
|
beforeDestroy() {
|
if (this.barChart) {
|
this.barChart.dispose();
|
this.barChart = null;
|
}
|
},
|
|
methods: {
|
// 格式化日期
|
formatDate(date) {
|
const year = date.getFullYear();
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
const day = String(date.getDate()).padStart(2, "0");
|
return `${year}-${month}-${day}`;
|
},
|
|
// 初始化数据
|
async initData() {
|
await this.getDeptList();
|
await this.getWardList();
|
this.initChart();
|
await this.loadData();
|
},
|
|
// 获取科室列表
|
getDeptList() {
|
return new Promise((resolve) => {
|
this.deptList = (this.$store.getters.belongDepts || []).map((dept) => {
|
return {
|
label: dept.deptName,
|
value: dept.deptCode,
|
};
|
});
|
resolve();
|
});
|
},
|
|
// 获取病区列表
|
getWardList() {
|
return new Promise((resolve) => {
|
this.wardList = (this.$store.getters.belongWards || []).map((ward) => {
|
return {
|
label: ward.districtName,
|
value: ward.districtCode,
|
};
|
});
|
resolve();
|
});
|
},
|
|
// 加载数据
|
async loadData() {
|
await Promise.all([
|
this.loadChartData(),
|
this.loadQuestionDetailData(),
|
this.loadTypeDetailData(),
|
]);
|
},
|
|
// 加载图表数据
|
async loadChartData() {
|
this.loading = true;
|
try {
|
const params = {
|
type: this.queryParams.type,
|
startTime: this.startTime,
|
endTime: this.endTime,
|
deptcodes: this.deptCodes,
|
hospitaldistrictcodes: this.hospitalDistrictCodes,
|
templateid: this.templateId,
|
};
|
|
const response = await satisfactionGraph(params);
|
|
if (response.code === 200) {
|
this.processChartData(response);
|
} else {
|
this.$message.error(response.msg || "获取图表数据失败");
|
// 使用mock数据
|
await this.generateMockChartData();
|
}
|
} catch (error) {
|
console.error("获取图表数据出错:", error);
|
this.$message.error("获取图表数据失败");
|
// 错误时使用mock数据
|
await this.generateMockChartData();
|
} finally {
|
this.loading = false;
|
}
|
},
|
|
// 处理图表数据
|
processChartData(apiData) {
|
if (!apiData || !apiData.rows || Object.keys(apiData.rows).length === 0) {
|
this.chartData = [];
|
this.totalSendCount = 0;
|
this.totalReceiveCount = 0;
|
this.overallRecoveryRate = 0;
|
this.renderChart([]);
|
return;
|
}
|
|
const chartData = [];
|
let totalSend = 0;
|
let totalReceive = 0;
|
let index = 0;
|
|
// 处理接口返回的满意度类型统计
|
Object.entries(apiData.rows).forEach(([typeName, typeStat]) => {
|
const sendCount = typeStat.subidAll || 0;
|
const receiveCount = typeStat.fillCountAll || 0;
|
const recoveryRate = typeStat.receiveRate || 0;
|
|
chartData.push({
|
name: typeName,
|
value: recoveryRate * 100, // 转换为百分比
|
sendCount: sendCount,
|
receiveCount: receiveCount,
|
averageScore: typeStat.averageScore || 0,
|
itemStyle: { color: this.getChartColor(index) },
|
});
|
|
totalSend += sendCount;
|
totalReceive += receiveCount;
|
index++;
|
});
|
|
this.totalSendCount = totalSend;
|
this.totalReceiveCount = totalReceive;
|
this.overallRecoveryRate = totalSend > 0 ? totalReceive / totalSend : 0;
|
this.chartData = chartData;
|
|
this.renderChart(chartData);
|
},
|
|
// 加载题目明细数据
|
async loadQuestionDetailData() {
|
this.detailLoading = true;
|
try {
|
const params = {
|
type: this.queryParams.type,
|
startTime: this.startTime,
|
endTime: this.endTime,
|
scriptids: this.scriptIds,
|
templateid: this.templateId,
|
};
|
|
const response = await statistics(params);
|
|
if (response.code === 200) {
|
this.processQuestionDetailData(response.rows);
|
} else {
|
this.$message.error(response.msg || "获取题目明细数据失败");
|
const mockData = await this.generateMockQuestionDetail();
|
this.questionDetailData = mockData.list;
|
this.detailTotal = mockData.total;
|
this.calculateSummary(mockData);
|
}
|
} catch (error) {
|
console.error("获取题目明细数据出错:", error);
|
this.$message.error("获取题目明细数据失败");
|
const mockData = await this.generateMockQuestionDetail();
|
this.questionDetailData = mockData.list;
|
this.detailTotal = mockData.total;
|
this.calculateSummary(mockData);
|
} finally {
|
this.detailLoading = false;
|
}
|
},
|
|
// 处理接口返回的题目明细数据
|
processQuestionDetailData(apiData) {
|
if (!apiData || !apiData.patSatisfactionDetailEntities) {
|
this.questionDetailData = [];
|
this.detailTotal = 0;
|
this.totalAnswerCount = 0;
|
this.totalAnswerRate = 0;
|
return;
|
}
|
|
const detailData = apiData.patSatisfactionDetailEntities.map((item) => {
|
const options = [];
|
if (item.matchedtextStats) {
|
Object.keys(item.matchedtextStats).forEach((key) => {
|
const stat = item.matchedtextStats[key];
|
options.push({
|
optionText: key,
|
chosenQuantity: stat.count || 0,
|
chosenPercentage: (stat.ratio || 0) / 100,
|
});
|
});
|
}
|
|
return {
|
scriptContent: item.scriptContent || "",
|
scriptType: 1,
|
answerPerson: item.answerPerson || 0,
|
noAnswerPerson: item.noAnswerPerson || 0,
|
answerCount: item.answerPerson || 0,
|
averageScore: item.averageScore || 0,
|
maxScore: item.maxScore || 0,
|
minScore: item.minScore || 0,
|
answerRate: item.answerRate || 0,
|
totalCount: (item.answerPerson || 0) + (item.noAnswerPerson || 0),
|
options: options,
|
};
|
});
|
|
const startIndex =
|
(this.detailQueryParams.pageNum - 1) * this.detailQueryParams.pageSize;
|
const endIndex = startIndex + this.detailQueryParams.pageSize;
|
const paginatedData = detailData.slice(startIndex, endIndex);
|
|
this.questionDetailData = paginatedData;
|
this.detailTotal = detailData.length;
|
this.totalAnswerCount = apiData.totalPerson || 0;
|
this.totalAnswerRate = apiData.totalAnswerRate || 0;
|
},
|
|
// 加载类型明细数据
|
async loadTypeDetailData() {
|
this.typeDetailLoading = true;
|
try {
|
const params = {
|
type: this.queryParams.type,
|
startTime: this.startTime,
|
endTime: this.endTime,
|
deptcodes: this.deptCodes,
|
hospitaldistrictcodes: this.hospitalDistrictCodes,
|
templateid: this.templateId,
|
};
|
|
const response = await satisfactionGraph(params);
|
|
if (response.code === 200) {
|
this.processTypeDetailData(response.data);
|
} else {
|
this.$message.error(response.msg || "获取类型明细数据失败");
|
const mockData = await this.generateMockTypeDetail();
|
this.typeDetailData = mockData;
|
this.calculateTypeSummary(mockData);
|
}
|
} catch (error) {
|
console.error("获取类型明细数据出错:", error);
|
this.$message.error("获取类型明细数据失败");
|
const mockData = await this.generateMockTypeDetail();
|
this.typeDetailData = mockData;
|
this.calculateTypeSummary(mockData);
|
} finally {
|
this.typeDetailLoading = false;
|
}
|
},
|
|
// 处理类型明细数据
|
processTypeDetailData(apiData) {
|
if (!apiData || !apiData.rows || Object.keys(apiData.rows).length === 0) {
|
this.typeDetailData = [];
|
this.calculateTypeSummary([]);
|
return;
|
}
|
|
const typeDetail = [];
|
Object.entries(apiData.rows).forEach(([typeName, typeStat], index) => {
|
const sendCount = typeStat.subidAll || 0;
|
const receiveCount = typeStat.fillCountAll || 0;
|
const recoveryRate = typeStat.receiveRate || 0;
|
const averageScore = typeStat.averageScore || 0;
|
|
typeDetail.push({
|
id: index + 1,
|
typeName: typeName,
|
isSpecial: false, // 根据实际情况判断
|
sendCount: sendCount,
|
receiveCount: receiveCount,
|
recoveryRate: recoveryRate,
|
averageScore: averageScore,
|
maxScore: 5, // 默认值
|
minScore: 0, // 默认值
|
satisfactionLevel: this.getSatisfactionLevel(averageScore),
|
trend: "stable", // 默认稳定
|
});
|
});
|
|
this.typeDetailData = typeDetail;
|
this.calculateTypeSummary(typeDetail);
|
},
|
|
// 根据平均分获取满意度等级
|
getSatisfactionLevel(score) {
|
if (score >= 4.5) return "优秀";
|
if (score >= 4.0) return "良好";
|
if (score >= 3.0) return "一般";
|
if (score >= 2.0) return "较差";
|
return "差";
|
},
|
|
// 获取趋势
|
getTrend(trend) {
|
if (trend > 0.1) return "up";
|
if (trend < -0.1) return "down";
|
return "stable";
|
},
|
|
// 计算综合得分
|
calculateSummary(data) {
|
let totalScore = 0;
|
let totalAnswerCount = 0;
|
let totalCount = 0;
|
|
data.list.forEach((item) => {
|
totalScore += item.averageScore;
|
totalAnswerCount += item.answerCount;
|
totalCount += item.totalCount;
|
});
|
|
this.totalScore =
|
data.list.length > 0 ? totalScore / data.list.length : 0;
|
this.totalAnswerCount = totalAnswerCount;
|
this.totalAnswerRate = totalCount > 0 ? totalAnswerCount / totalCount : 0;
|
},
|
|
// 计算类型统计汇总
|
calculateTypeSummary(data) {
|
if (data.length === 0) {
|
this.averageRecoveryRate = 0;
|
this.averageTypeScore = 0;
|
this.highSatisfactionCount = 0;
|
return;
|
}
|
|
let totalRecoveryRate = 0;
|
let totalTypeScore = 0;
|
let highCount = 0;
|
|
data.forEach((item) => {
|
totalRecoveryRate += item.recoveryRate;
|
totalTypeScore += item.averageScore;
|
if (
|
item.satisfactionLevel === "优秀" ||
|
item.satisfactionLevel === "良好"
|
) {
|
highCount++;
|
}
|
});
|
|
this.averageRecoveryRate = totalRecoveryRate / data.length;
|
this.averageTypeScore = totalTypeScore / data.length;
|
this.highSatisfactionCount = highCount;
|
},
|
|
// 初始化图表
|
initChart() {
|
const chartDom = document.getElementById("satisfactionBarChart");
|
if (!chartDom) return;
|
|
this.barChart = echarts.init(chartDom);
|
window.addEventListener("resize", this.handleChartResize);
|
},
|
|
// 渲染图表
|
renderChart(chartData) {
|
if (!this.barChart) return;
|
if (!chartData || chartData.length === 0) {
|
const emptyOption = {
|
title: {
|
text: "暂无数据",
|
left: "center",
|
top: "center",
|
textStyle: {
|
color: "#999",
|
fontSize: 16,
|
fontWeight: "normal"
|
}
|
},
|
xAxis: { show: false },
|
yAxis: { show: false }
|
};
|
this.barChart.setOption(emptyOption);
|
return;
|
}
|
const option = {
|
title: {
|
text: "",
|
left: "center",
|
},
|
tooltip: {
|
trigger: "axis",
|
axisPointer: {
|
type: "shadow",
|
},
|
formatter: (params) => {
|
const data = params[0];
|
return `
|
<div style="margin-bottom: 5px; font-weight: bold; color: #333;">
|
${data.name}
|
</div>
|
<div style="margin: 2px 0;">
|
<span style="display:inline-block;width:10px;height:10px;border-radius:2px;background:${
|
data.color
|
};margin-right:5px;"></span>
|
填报比例: <strong>${data.value.toFixed(1)}%</strong>
|
</div>
|
<div style="margin: 2px 0;">
|
<span style="display:inline-block;width:10px;height:10px;border-radius:2px;background:#eee;margin-right:5px;"></span>
|
发送问卷: <strong>${data.data.sendCount.toLocaleString()}</strong>
|
</div>
|
<div style="margin: 2px 0;">
|
<span style="display:inline-block;width:10px;height:10px;border-radius:2px;background:#eee;margin-right:5px;"></span>
|
回收问卷: <strong>${data.data.receiveCount.toLocaleString()}</strong>
|
</div>
|
`;
|
},
|
},
|
grid: {
|
left: "3%",
|
right: "4%",
|
bottom: "3%",
|
top: 60,
|
containLabel: true,
|
},
|
xAxis: {
|
type: "category",
|
data: chartData.map((item) => item.name),
|
axisLabel: {
|
interval: 0,
|
rotate: 0,
|
fontSize: 12,
|
color: "#666",
|
},
|
axisLine: {
|
lineStyle: {
|
color: "#DCDFE6",
|
},
|
},
|
axisTick: {
|
alignWithLabel: true,
|
},
|
},
|
yAxis: {
|
type: "value",
|
name: "填报比例 (%)",
|
min: 0,
|
max: 100,
|
axisLabel: {
|
formatter: "{value}%",
|
color: "#666",
|
},
|
axisLine: {
|
lineStyle: {
|
color: "#DCDFE6",
|
},
|
},
|
splitLine: {
|
lineStyle: {
|
type: "dashed",
|
color: "#E4E7ED",
|
},
|
},
|
},
|
series: [
|
{
|
name: "填报比例",
|
type: "bar",
|
barWidth: 40,
|
data: chartData,
|
itemStyle: {
|
color: (params) => {
|
return params.data.itemStyle.color;
|
},
|
},
|
label: {
|
show: true,
|
position: "top",
|
formatter: "{c}%",
|
fontSize: 12,
|
color: "#333",
|
},
|
},
|
],
|
};
|
|
this.barChart.setOption(option);
|
},
|
|
// 生成Mock图表数据
|
generateMockChartData() {
|
return new Promise((resolve) => {
|
setTimeout(() => {
|
const data = this.satisfactionTypes.map((type, index) => ({
|
name: type.name,
|
recoveryRate: Math.random() * 0.3 + 0.6,
|
sendCount: Math.floor(Math.random() * 3000) + 1500,
|
receiveCount: 0,
|
color: type.color,
|
}));
|
|
data.forEach((item) => {
|
item.receiveCount = Math.floor(item.sendCount * item.recoveryRate);
|
});
|
|
this.totalSendCount = data.reduce(
|
(sum, item) => sum + item.sendCount,
|
0
|
);
|
this.totalReceiveCount = data.reduce(
|
(sum, item) => sum + item.receiveCount,
|
0
|
);
|
this.overallRecoveryRate =
|
this.totalSendCount > 0
|
? this.totalReceiveCount / this.totalSendCount
|
: 0;
|
|
const chartData = data.map((item) => ({
|
name: item.name,
|
value: item.recoveryRate * 100,
|
sendCount: item.sendCount,
|
receiveCount: item.receiveCount,
|
itemStyle: { color: item.color },
|
}));
|
|
this.chartData = chartData;
|
this.renderChart(chartData);
|
resolve();
|
}, 300);
|
});
|
},
|
|
// 生成Mock题目详情数据
|
generateMockQuestionDetail() {
|
return new Promise((resolve) => {
|
setTimeout(() => {
|
const questions = [
|
{
|
scriptContent: "您对本次就医的整体满意程度?",
|
scriptType: 1,
|
answerPerson: 120,
|
noAnswerPerson: 30,
|
answerCount: 120,
|
totalCount: 150,
|
averageScore: 4.2,
|
maxScore: 5.0,
|
minScore: 1.0,
|
answerRate: 0.8,
|
options: [
|
{
|
optionText: "非常满意",
|
chosenQuantity: 60,
|
chosenPercentage: 0.5,
|
},
|
{
|
optionText: "满意",
|
chosenQuantity: 36,
|
chosenPercentage: 0.3,
|
},
|
{
|
optionText: "一般",
|
chosenQuantity: 18,
|
chosenPercentage: 0.15,
|
},
|
{
|
optionText: "不满意",
|
chosenQuantity: 6,
|
chosenPercentage: 0.05,
|
},
|
],
|
},
|
{
|
scriptContent: "您对医护人员的服务态度是否满意?",
|
scriptType: 1,
|
answerPerson: 145,
|
noAnswerPerson: 11,
|
answerCount: 145,
|
totalCount: 156,
|
averageScore: 4.5,
|
maxScore: 5,
|
minScore: 3,
|
answerRate: 0.93,
|
options: [
|
{
|
optionText: "非常满意",
|
chosenQuantity: 89,
|
chosenPercentage: 0.61,
|
},
|
{
|
optionText: "满意",
|
chosenQuantity: 45,
|
chosenPercentage: 0.31,
|
},
|
{
|
optionText: "一般",
|
chosenQuantity: 8,
|
chosenPercentage: 0.06,
|
},
|
{
|
optionText: "不满意",
|
chosenQuantity: 2,
|
chosenPercentage: 0.01,
|
},
|
{
|
optionText: "非常不满意",
|
chosenQuantity: 1,
|
chosenPercentage: 0.01,
|
},
|
],
|
},
|
];
|
|
const startIndex =
|
(this.detailQueryParams.pageNum - 1) *
|
this.detailQueryParams.pageSize;
|
const endIndex = startIndex + this.detailQueryParams.pageSize;
|
const paginatedData = questions.slice(startIndex, endIndex);
|
|
resolve({
|
list: paginatedData,
|
total: questions.length,
|
});
|
}, 300);
|
});
|
},
|
|
// 生成Mock类型明细数据
|
generateMockTypeDetail() {
|
return new Promise((resolve) => {
|
setTimeout(() => {
|
const types = [
|
{
|
id: 401,
|
typeName: "出院满意度",
|
isSpecial: false,
|
sendCount: 2850,
|
receiveCount: 2680,
|
recoveryRate: 0.94,
|
averageScore: 4.8,
|
maxScore: 5,
|
minScore: 3.8,
|
satisfactionLevel: "优秀",
|
trend: "up",
|
},
|
{
|
id: 402,
|
typeName: "住院满意度",
|
isSpecial: false,
|
sendCount: 2620,
|
receiveCount: 2405,
|
recoveryRate: 0.918,
|
averageScore: 4.6,
|
maxScore: 5,
|
minScore: 3.5,
|
satisfactionLevel: "优秀",
|
trend: "stable",
|
},
|
{
|
id: 403,
|
typeName: "门诊满意度",
|
isSpecial: false,
|
sendCount: 3780,
|
receiveCount: 3220,
|
recoveryRate: 0.852,
|
averageScore: 4.3,
|
maxScore: 5,
|
minScore: 2.5,
|
satisfactionLevel: "良好",
|
trend: "up",
|
},
|
{
|
id: 404,
|
typeName: "常用满意度",
|
isSpecial: true,
|
sendCount: 1950,
|
receiveCount: 1780,
|
recoveryRate: 0.913,
|
averageScore: 4.5,
|
maxScore: 5,
|
minScore: 3.2,
|
satisfactionLevel: "良好",
|
trend: "stable",
|
},
|
];
|
|
resolve(types);
|
}, 300);
|
});
|
},
|
|
// 获取图表颜色
|
getChartColor(index) {
|
const colors = [
|
"#36B37E",
|
"#4CAF50",
|
"#409EFF",
|
"#FF9D4D",
|
"#9B8DFF",
|
"#FF6B6B",
|
];
|
return colors[index % colors.length];
|
},
|
|
// 处理图表响应式
|
handleChartResize() {
|
if (this.barChart) {
|
this.barChart.resize();
|
}
|
},
|
|
// 处理查询
|
handleSearch() {
|
this.detailQueryParams.pageNum = 1;
|
this.loadData();
|
// 强制重新渲染图表
|
setTimeout(() => {
|
if (this.chartData.length === 0) {
|
this.renderChart([]);
|
}
|
}, 100);
|
},
|
|
// 处理重置
|
handleReset() {
|
this.$refs.queryForm.resetFields();
|
this.queryParams.dateRange = [];
|
this.detailQueryParams.pageNum = 1;
|
this.loadData();
|
},
|
|
// 处理Tab切换
|
handleTabClick(tab) {
|
if (tab.name === "typeDetail" && this.typeDetailData.length === 0) {
|
this.loadTypeDetailData();
|
}
|
},
|
|
// 处理明细分页大小变化
|
handleDetailSizeChange(size) {
|
this.detailQueryParams.pageSize = size;
|
this.detailQueryParams.pageNum = 1;
|
this.loadQuestionDetailData();
|
},
|
|
// 处理明细页码变化
|
handleDetailPageChange(page) {
|
this.detailQueryParams.pageNum = page;
|
this.loadQuestionDetailData();
|
},
|
|
// 处理类型详情
|
handleTypeDetail(row) {
|
this.$message.info(`查看类型详情:${row.typeName}`);
|
},
|
|
// 处理导出数据
|
handleExportData(row) {
|
this.$message.success(`正在导出 ${row.typeName} 数据...`);
|
},
|
|
// 格式化百分比
|
formatPercent(value) {
|
if (value === null || value === undefined) return "-";
|
const num = parseFloat(value);
|
if (isNaN(num)) return "-";
|
// 如果值小于1,认为是小数比例,需要乘以100
|
const percentValue = num < 1 ? num * 100 : num;
|
return `${percentValue.toFixed(2)}%`;
|
},
|
|
// 获取回收率样式类
|
getRateClass(rate) {
|
if (rate >= 0.9) return "rate-high";
|
if (rate >= 0.8) return "rate-medium";
|
return "rate-low";
|
},
|
|
// 获取满意度等级标签类型
|
getLevelTagType(level) {
|
const levelMap = {
|
优秀: "success",
|
良好: "primary",
|
一般: "warning",
|
较差: "danger",
|
差: "info",
|
};
|
return levelMap[level] || "info";
|
},
|
|
// 处理科室选择变化
|
handleDeptChange() {
|
this.loadData();
|
},
|
|
// 处理病区选择变化
|
handleWardChange() {
|
this.loadData();
|
},
|
},
|
};
|
</script>
|
|
<style lang="scss" scoped>
|
.satisfaction-statistics {
|
padding: 20px;
|
background-color: #f5f7fa;
|
min-height: 100vh;
|
|
.query-section {
|
margin-bottom: 20px;
|
|
.query-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;
|
}
|
}
|
}
|
}
|
|
.chart-section {
|
margin-bottom: 20px;
|
|
.chart-container {
|
.chart-header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
margin-bottom: 20px;
|
padding-bottom: 15px;
|
border-bottom: 1px solid #f0f0f0;
|
|
.chart-title {
|
margin: 0;
|
font-size: 16px;
|
font-weight: 600;
|
color: #303133;
|
}
|
|
.statistic-info {
|
display: flex;
|
gap: 30px;
|
align-items: center;
|
|
.statistic-item {
|
display: flex;
|
align-items: center;
|
gap: 8px;
|
|
.statistic-label {
|
font-size: 14px;
|
color: #606266;
|
}
|
|
.statistic-value {
|
font-size: 18px;
|
font-weight: 600;
|
color: #409eff;
|
}
|
}
|
}
|
}
|
}
|
}
|
|
.tab-section {
|
::v-deep .el-tabs__header {
|
margin-bottom: 0;
|
}
|
|
::v-deep .el-tabs__content {
|
padding: 20px 0 0 0;
|
}
|
}
|
|
.detail-table-section {
|
.option-detail {
|
padding: 15px;
|
background: #f8f9fa;
|
border-radius: 4px;
|
margin: 10px 0;
|
}
|
|
::v-deep .el-table {
|
th {
|
background-color: #f8f9fa;
|
font-weight: 600;
|
color: #333;
|
padding: 12px 0;
|
}
|
|
td {
|
padding: 12px 0;
|
}
|
|
.question-row {
|
td {
|
background-color: #fff;
|
}
|
|
&:hover {
|
td {
|
background-color: #f5f7fa;
|
}
|
}
|
}
|
}
|
|
.score-text {
|
font-weight: 600;
|
color: #1890ff;
|
font-size: 16px;
|
}
|
|
.summary-row {
|
margin-top: 20px;
|
padding: 20px;
|
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
border-radius: 8px;
|
border: 1px solid #dee2e6;
|
|
.summary-content {
|
display: flex;
|
justify-content: space-around;
|
align-items: center;
|
|
.summary-item {
|
text-align: center;
|
|
.label {
|
font-size: 16px;
|
color: #606266;
|
margin-right: 8px;
|
}
|
|
.value {
|
font-size: 24px;
|
font-weight: 600;
|
color: #409eff;
|
}
|
}
|
}
|
}
|
|
.pagination-section {
|
display: flex;
|
justify-content: center;
|
padding: 20px 0 0 0;
|
}
|
}
|
|
.type-detail-section {
|
.type-detail-table {
|
::v-deep .el-table__header-wrapper {
|
th {
|
background-color: #f0f7ff;
|
font-weight: 600;
|
color: #333;
|
}
|
}
|
|
.type-name-cell {
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
|
.type-name {
|
font-weight: 500;
|
}
|
}
|
|
.number-text {
|
font-weight: 600;
|
color: #333;
|
}
|
|
.rate-text {
|
font-weight: 600;
|
font-size: 14px;
|
|
&.rate-high {
|
color: #67c23a;
|
}
|
|
&.rate-medium {
|
color: #e6a23c;
|
}
|
|
&.rate-low {
|
color: #f56c6c;
|
}
|
}
|
|
.score-text {
|
font-weight: 600;
|
color: #409eff;
|
font-size: 15px;
|
}
|
|
.trend-cell {
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
gap: 5px;
|
|
.trend-up,
|
.trend-down,
|
.trend-stable {
|
font-size: 16px;
|
}
|
|
.trend-text {
|
font-size: 13px;
|
color: #666;
|
}
|
}
|
}
|
|
.type-summary-row {
|
margin-top: 20px;
|
padding: 20px;
|
background: linear-gradient(135deg, #f0f9ff 0%, #e6f7ff 100%);
|
border-radius: 8px;
|
border: 1px solid #d0ebff;
|
|
.type-summary-content {
|
display: flex;
|
justify-content: space-around;
|
align-items: center;
|
flex-wrap: wrap;
|
gap: 20px;
|
|
.type-summary-item {
|
text-align: center;
|
min-width: 150px;
|
|
.label {
|
font-size: 14px;
|
color: #606266;
|
margin-right: 8px;
|
}
|
|
.value {
|
font-size: 20px;
|
font-weight: 600;
|
color: #409eff;
|
}
|
|
.high-count {
|
color: #67c23a;
|
}
|
}
|
}
|
}
|
}
|
|
// 内层表格样式
|
.inner-table {
|
::v-deep .el-table__header-wrapper {
|
th {
|
background-color: #f0f7ff !important;
|
color: #333;
|
font-weight: 600;
|
}
|
}
|
|
::v-deep .el-table__body-wrapper {
|
tr {
|
background-color: #fff;
|
|
&:hover {
|
background-color: #f5f7fa;
|
}
|
}
|
}
|
}
|
}
|
|
@media (max-width: 768px) {
|
.satisfaction-statistics {
|
padding: 10px;
|
|
.query-section {
|
.query-form {
|
::v-deep .el-form-item {
|
width: 100%;
|
margin-right: 0;
|
margin-bottom: 10px;
|
}
|
}
|
}
|
|
.chart-section {
|
.chart-container {
|
.chart-header {
|
flex-direction: column;
|
align-items: flex-start;
|
gap: 15px;
|
|
.statistic-info {
|
width: 100%;
|
justify-content: space-between;
|
flex-wrap: wrap;
|
gap: 10px;
|
}
|
}
|
}
|
}
|
|
.detail-table-section {
|
.summary-content {
|
flex-direction: column;
|
gap: 15px;
|
}
|
}
|
|
.type-detail-section {
|
.type-summary-content {
|
flex-direction: column;
|
gap: 15px;
|
}
|
}
|
}
|
}
|
</style>
|