| | |
| | | </el-select> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="科室" prop="deptCode"> |
| | | <!-- 科室选择 --> |
| | | <el-form-item label="科室" prop="deptCodes"> |
| | | <el-select |
| | | v-model="queryParams.deptCode" |
| | | v-model="queryParams.deptCodes" |
| | | placeholder="请选择科室" |
| | | clearable |
| | | filterable |
| | | style="width: 200px" |
| | | multiple |
| | | collapse-tags |
| | | style="width: 300px" |
| | | @change="handleDeptChange" |
| | | > |
| | | <el-option |
| | |
| | | </el-select> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="病区" prop="wardCode"> |
| | | <!-- 病区选择 --> |
| | | <el-form-item label="病区" prop="wardCodes"> |
| | | <el-select |
| | | v-model="queryParams.wardCode" |
| | | v-model="queryParams.wardCodes" |
| | | placeholder="请选择病区" |
| | | clearable |
| | | filterable |
| | | style="width: 200px" |
| | | multiple |
| | | collapse-tags |
| | | style="width: 300px" |
| | | @change="handleWardChange" |
| | | > |
| | | <el-option |
| | |
| | | <el-card shadow="never"> |
| | | <div class="chart-container"> |
| | | <div class="chart-header"> |
| | | <h3 class="chart-title">满意度类型填报比例统计</h3> |
| | | <h3 class="chart-title">{{ getChartTitle() }}</h3> |
| | | <div class="statistic-info"> |
| | | <div class="statistic-item"> |
| | | <span class="statistic-label">发送问卷总量:</span> |
| | |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 标签页切换 --> |
| | | <el-tabs |
| | | v-model="activeChartTab" |
| | | @tab-click="handleChartTabClick" |
| | | class="chart-tabs" |
| | | > |
| | | <el-tab-pane |
| | | label="满意度类型" |
| | | name="satisfactionType" |
| | | ></el-tab-pane> |
| | | <el-tab-pane label="维度统计" name="dimension"></el-tab-pane> |
| | | </el-tabs> |
| | | |
| | | <div |
| | | id="satisfactionBarChart" |
| | | style="width: 100%; height: 400px" |
| | |
| | | </div> |
| | | </el-tab-pane> |
| | | |
| | | <el-tab-pane label="维度题目明细" name="dimensionDetail"> |
| | | <!-- 维度题目明细表格 --> |
| | | <div class="dimension-detail-section"> |
| | | <el-table |
| | | v-loading="dimensionDetailLoading" |
| | | :data="dimensionDetailData" |
| | | :border="true" |
| | | style="width: 100%" |
| | | row-class-name="dimension-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="dimensionName" |
| | | align="center" |
| | | width="120" |
| | | > |
| | | <template slot-scope="{ row }"> |
| | | <el-tag type="info" size="small"> |
| | | {{ getDimensionLabel(row.dimension) }} |
| | | </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="answerPerson" |
| | | align="center" |
| | | width="100" |
| | | /> |
| | | |
| | | <el-table-column |
| | | label="未答题人数" |
| | | prop="noAnswerPerson" |
| | | align="center" |
| | | width="100" |
| | | /> |
| | | |
| | | <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="dimension-summary-row"> |
| | | <div class="dimension-summary-content"> |
| | | <div class="dimension-summary-item"> |
| | | <span class="label">题目总数:</span> |
| | | <span class="value">{{ dimensionDetailData.length }}</span> |
| | | </div> |
| | | <div class="dimension-summary-item"> |
| | | <span class="label">维度平均分:</span> |
| | | <span class="value">{{ |
| | | dimensionAverageScore.toFixed(1) |
| | | }}</span> |
| | | </div> |
| | | <div class="dimension-summary-item"> |
| | | <span class="label">总体答题率:</span> |
| | | <span class="value">{{ |
| | | formatPercent(dimensionTotalAnswerRate) |
| | | }}</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </el-tab-pane> |
| | | |
| | | <el-tab-pane label="各类型统计明细" name="typeDetail"> |
| | | <!-- 各类型统计明细表格 --> |
| | | <div class="type-detail-section"> |
| | |
| | | </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" |
| | |
| | | |
| | | <script> |
| | | import * as echarts from "echarts"; |
| | | import { statistics, satisfactionGraph } from "@/api/system/user"; |
| | | import { |
| | | statistics, |
| | | satisfactionGraph, |
| | | statisticsByDimension, |
| | | satisfactionGraphDimension, |
| | | } from "@/api/system/user"; |
| | | import store from "@/store"; |
| | | |
| | | export default { |
| | | name: "SatisfactionStatistics", |
| | | dicts: ["dimensionality_type"], |
| | | data() { |
| | | return { |
| | | // 查询参数 |
| | | queryParams: { |
| | | type: 2, |
| | | patientSource: "", |
| | | deptCode: "", |
| | | wardCode: "", |
| | | deptCodes: [], // 改为数组,支持多选 |
| | | wardCodes: [], // 改为数组,支持多选 |
| | | dateRange: [], |
| | | }, |
| | | |
| | | // 当前激活的tab |
| | | activeTab: "questionDetail", |
| | | |
| | | // 当前激活的图表tab |
| | | activeChartTab: "satisfactionType", |
| | | // 患者来源选项 |
| | | patientSourceList: [ |
| | | { value: "1", label: "门诊" }, |
| | |
| | | loading: false, |
| | | detailLoading: false, |
| | | typeDetailLoading: false, |
| | | dimensionDetailLoading: false, |
| | | |
| | | // 题目明细数据 |
| | | questionDetailData: [], |
| | | |
| | | // 维度题目明细数据 |
| | | dimensionDetailData: [], |
| | | |
| | | // 题目明细查询参数 |
| | | detailQueryParams: { |
| | |
| | | // 题目明细总数 |
| | | detailTotal: 0, |
| | | |
| | | // 维度明细查询参数 |
| | | dimensionQueryParams: { |
| | | pageNum: 1, |
| | | pageSize: 10, |
| | | }, |
| | | |
| | | // 维度明细总数 |
| | | dimensionTotal: 0, |
| | | |
| | | // 综合得分 |
| | | totalScore: 0, |
| | | totalAnswerCount: 0, |
| | | totalAnswerRate: 0, |
| | | |
| | | // 维度统计汇总 |
| | | dimensionTotalAnswerCount: 0, |
| | | dimensionTotalAnswerRate: 0, |
| | | dimensionAverageScore: 0, |
| | | |
| | | // 各类型统计明细数据 |
| | | typeDetailData: [], |
| | |
| | | |
| | | // 计算科室编码数组 |
| | | deptCodes() { |
| | | if (this.queryParams.deptCode) { |
| | | return [this.queryParams.deptCode]; |
| | | if (this.queryParams.deptCodes && this.queryParams.deptCodes.length > 0) { |
| | | return this.queryParams.deptCodes; |
| | | } |
| | | return this.deptList.map((dept) => dept.value); |
| | | }, |
| | | |
| | | // 计算病区编码数组 |
| | | hospitalDistrictCodes() { |
| | | if (this.queryParams.wardCode) { |
| | | return [this.queryParams.wardCode]; |
| | | if (this.queryParams.wardCodes && this.queryParams.wardCodes.length > 0) { |
| | | return this.queryParams.wardCodes; |
| | | } |
| | | return this.wardList.map((ward) => ward.value); |
| | | }, |
| | |
| | | }); |
| | | }, |
| | | |
| | | // 获取图表标题 |
| | | getChartTitle() { |
| | | return this.activeChartTab === "satisfactionType" |
| | | ? "满意度类型填报比例统计" |
| | | : "维度填报比例统计"; |
| | | }, |
| | | |
| | | // 加载数据 |
| | | async loadData() { |
| | | await Promise.all([ |
| | |
| | | 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); |
| | | if (this.activeChartTab === "satisfactionType") { |
| | | await this.loadSatisfactionTypeChartData(); |
| | | } else { |
| | | this.$message.error(response.msg || "获取图表数据失败"); |
| | | // 使用mock数据 |
| | | await this.generateMockChartData(); |
| | | await this.loadDimensionChartData(); |
| | | } |
| | | } catch (error) { |
| | | console.error("获取图表数据出错:", error); |
| | |
| | | } |
| | | }, |
| | | |
| | | // 处理图表数据 |
| | | processChartData(apiData) { |
| | | // 加载满意度类型图表数据 |
| | | async loadSatisfactionTypeChartData() { |
| | | 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.processSatisfactionTypeChartData(response); |
| | | } else { |
| | | this.$message.error(response.msg || "获取图表数据失败"); |
| | | await this.generateMockChartData(); |
| | | } |
| | | }, |
| | | |
| | | // 加载维度图表数据 |
| | | async loadDimensionChartData() { |
| | | const params = { |
| | | type: this.queryParams.type, |
| | | startTime: this.startTime, |
| | | endTime: this.endTime, |
| | | deptcodes: this.deptCodes, // 使用计算属性 |
| | | hospitaldistrictcodes: this.hospitalDistrictCodes, // 使用计算属性 |
| | | templateid: this.templateId, |
| | | }; |
| | | |
| | | const response = await satisfactionGraphDimension(params); |
| | | if (response.code === 200) { |
| | | this.processDimensionChartData(response); |
| | | } else { |
| | | this.$message.error(response.msg || "获取维度图表数据失败"); |
| | | await this.generateMockDimensionChartData(); |
| | | } |
| | | }, |
| | | |
| | | // 处理满意度类型图表数据 |
| | | processSatisfactionTypeChartData(apiData) { |
| | | if (!apiData || !apiData.rows || Object.keys(apiData.rows).length === 0) { |
| | | this.chartData = []; |
| | | this.totalSendCount = 0; |
| | | this.totalReceiveCount = 0; |
| | | this.overallRecoveryRate = 0; |
| | | this.renderChart([]); |
| | | this.renderChart([], "satisfactionType"); |
| | | return; |
| | | } |
| | | |
| | |
| | | this.overallRecoveryRate = totalSend > 0 ? totalReceive / totalSend : 0; |
| | | this.chartData = chartData; |
| | | |
| | | this.renderChart(chartData); |
| | | this.renderChart(chartData, "satisfactionType"); |
| | | }, |
| | | |
| | | // 处理维度图表数据 |
| | | processDimensionChartData(apiData) { |
| | | if (!apiData || !apiData.rows || Object.keys(apiData.rows).length === 0) { |
| | | this.chartData = []; |
| | | this.totalSendCount = 0; |
| | | this.totalReceiveCount = 0; |
| | | this.overallRecoveryRate = 0; |
| | | this.renderChart([], "dimension"); |
| | | return; |
| | | } |
| | | |
| | | const chartData = []; |
| | | let totalSend = 0; |
| | | let totalReceive = 0; |
| | | let index = 0; |
| | | |
| | | // 处理接口返回的维度统计 |
| | | Object.entries(apiData.rows).forEach(([dimensionCode, dimensionStat]) => { |
| | | const sendCount = dimensionStat.subidAll || 0; |
| | | const receiveCount = dimensionStat.fillCountAll || 0; |
| | | const recoveryRate = dimensionStat.receiveRate || 0; |
| | | |
| | | // 获取维度标签 |
| | | const dimensionName = this.getDimensionLabel(dimensionCode); |
| | | |
| | | chartData.push({ |
| | | name: dimensionName, |
| | | value: (recoveryRate * 100).toFixed(2), // 转换为百分比 |
| | | sendCount: sendCount, |
| | | receiveCount: receiveCount, |
| | | averageScore: dimensionStat.averageScore || 0, |
| | | dimension: dimensionCode, |
| | | 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, "dimension"); |
| | | }, |
| | | |
| | | // 获取维度标签 |
| | | getDimensionLabel(dimensionCode) { |
| | | if (!this.dict.type.dimensionality_type) return dimensionCode; |
| | | const dimension = this.dict.type.dimensionality_type.find( |
| | | (item) => item.value.toString() === dimensionCode.toString() |
| | | ); |
| | | return dimension ? dimension.label : dimensionCode; |
| | | }, |
| | | |
| | | // 加载题目明细数据 |
| | |
| | | startTime: this.startTime, |
| | | endTime: this.endTime, |
| | | scriptids: this.scriptIds, |
| | | deptcodes: this.deptCodes, // 使用计算属性 |
| | | hospitaldistrictcodes: this.hospitalDistrictCodes, // 使用计算属性 |
| | | templateid: this.templateId, |
| | | }; |
| | | |
| | |
| | | this.totalAnswerRate = apiData.totalAnswerRate || 0; |
| | | }, |
| | | |
| | | // 加载维度明细数据 |
| | | async loadDimensionDetailData() { |
| | | this.dimensionDetailLoading = true; |
| | | try { |
| | | const params = { |
| | | type: this.queryParams.type, |
| | | startTime: this.startTime, |
| | | endTime: this.endTime, |
| | | deptcodes: this.deptCodes, // 使用计算属性 |
| | | hospitaldistrictcodes: this.hospitalDistrictCodes, // 使用计算属性 |
| | | templateid: this.templateId, |
| | | questionType: this.queryParams.type || null, |
| | | serviceTypes: this.defaultServiceTypes, |
| | | }; |
| | | |
| | | const response = await statisticsByDimension(params); |
| | | |
| | | if (response.code === 200) { |
| | | this.processDimensionDetailData(response.rows); |
| | | } else { |
| | | this.$message.error(response.msg || "获取维度明细数据失败"); |
| | | const mockData = await this.generateMockDimensionDetail(); |
| | | this.dimensionDetailData = mockData; |
| | | this.calculateDimensionSummary(mockData); |
| | | } |
| | | } catch (error) { |
| | | console.error("获取维度明细数据出错:", error); |
| | | this.$message.error("获取维度明细数据失败"); |
| | | const mockData = await this.generateMockDimensionDetail(); |
| | | this.dimensionDetailData = mockData; |
| | | this.calculateDimensionSummary(mockData); |
| | | } finally { |
| | | this.dimensionDetailLoading = false; |
| | | } |
| | | }, |
| | | |
| | | // 处理维度明细数据 |
| | | processDimensionDetailData(apiData) { |
| | | if (!apiData || !apiData.patSatisfactionDetailEntities) { |
| | | this.dimensionDetailData = []; |
| | | this.calculateDimensionSummary([]); |
| | | 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 || "", |
| | | dimension: item.dimension || "0", // 维度代码 |
| | | dimensionName: this.getDimensionLabel(item.dimension || "0"), // 维度名称 |
| | | 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, |
| | | }; |
| | | }); |
| | | |
| | | this.dimensionDetailData = detailData; |
| | | this.dimensionTotal = detailData.length; |
| | | this.calculateDimensionSummary(detailData); |
| | | }, |
| | | |
| | | // 加载类型明细数据 |
| | | async loadTypeDetailData() { |
| | | this.typeDetailLoading = true; |
| | |
| | | type: this.queryParams.type, |
| | | startTime: this.startTime, |
| | | endTime: this.endTime, |
| | | deptcodes: this.deptCodes, |
| | | hospitaldistrictcodes: this.hospitalDistrictCodes, |
| | | deptcodes: this.deptCodes, // 使用计算属性 |
| | | hospitaldistrictcodes: this.hospitalDistrictCodes, // 使用计算属性 |
| | | templateid: this.templateId, |
| | | }; |
| | | |
| | |
| | | |
| | | if (response.code === 200) { |
| | | this.processTypeDetailData(response); |
| | | console.log(11); |
| | | |
| | | } else { |
| | | this.$message.error(response.msg || "获取类型明细数据失败"); |
| | | const mockData = await this.generateMockTypeDetail(); |
| | |
| | | }); |
| | | |
| | | this.typeDetailData = typeDetail; |
| | | console.log(this.typeDetailData,'this.typeDetailData'); |
| | | |
| | | this.calculateTypeSummary(typeDetail); |
| | | }, |
| | | |
| | |
| | | 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"; |
| | | }, |
| | | |
| | | // 计算综合得分 |
| | |
| | | data.list.length > 0 ? totalScore / data.list.length : 0; |
| | | this.totalAnswerCount = totalAnswerCount; |
| | | this.totalAnswerRate = totalCount > 0 ? totalAnswerCount / totalCount : 0; |
| | | }, |
| | | |
| | | // 计算维度统计汇总 |
| | | calculateDimensionSummary(data) { |
| | | if (data.length === 0) { |
| | | this.dimensionTotalAnswerCount = 0; |
| | | this.dimensionTotalAnswerRate = 0; |
| | | this.dimensionAverageScore = 0; |
| | | return; |
| | | } |
| | | |
| | | let totalScore = 0; |
| | | let totalAnswerCount = 0; |
| | | let totalCount = 0; |
| | | |
| | | data.forEach((item) => { |
| | | totalScore += item.averageScore; |
| | | totalAnswerCount += item.answerPerson; |
| | | totalCount += item.answerPerson + item.noAnswerPerson; |
| | | }); |
| | | |
| | | this.dimensionAverageScore = |
| | | data.length > 0 ? totalScore / data.length : 0; |
| | | this.dimensionTotalAnswerCount = totalAnswerCount; |
| | | this.dimensionTotalAnswerRate = |
| | | totalCount > 0 ? totalAnswerCount / totalCount : 0; |
| | | }, |
| | | |
| | | // 计算类型统计汇总 |
| | |
| | | }, |
| | | |
| | | // 渲染图表 |
| | | renderChart(chartData) { |
| | | renderChart(chartData, chartType = "satisfactionType") { |
| | | if (!this.barChart) return; |
| | | if (!chartData || chartData.length === 0) { |
| | | const emptyOption = { |
| | |
| | | this.barChart.setOption(emptyOption); |
| | | return; |
| | | } |
| | | |
| | | const yAxisName = |
| | | chartType === "satisfactionType" ? "填报比例 (%)" : "填报比例 (%)"; |
| | | const tooltipFormatter = |
| | | chartType === "satisfactionType" |
| | | ? this.getSatisfactionTypeTooltipFormatter |
| | | : this.getDimensionTooltipFormatter; |
| | | |
| | | const option = { |
| | | title: { |
| | | text: "", |
| | |
| | | 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}%</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> |
| | | `; |
| | | }, |
| | | formatter: tooltipFormatter, |
| | | }, |
| | | grid: { |
| | | left: "3%", |
| | |
| | | }, |
| | | yAxis: { |
| | | type: "value", |
| | | name: "填报比例 (%)", |
| | | name: yAxisName, |
| | | min: 0, |
| | | max: 100, |
| | | axisLabel: { |
| | |
| | | this.barChart.setOption(option); |
| | | }, |
| | | |
| | | // 满意度类型tooltip格式化 |
| | | getSatisfactionTypeTooltipFormatter(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}%</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> |
| | | <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.averageScore.toFixed(1)}</strong> |
| | | </div> |
| | | `; |
| | | }, |
| | | |
| | | // 维度tooltip格式化 |
| | | getDimensionTooltipFormatter(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}%</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> |
| | | <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.averageScore.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.dimension}</strong> |
| | | </div> |
| | | `; |
| | | }, |
| | | |
| | | // 生成Mock图表数据 |
| | | generateMockChartData() { |
| | | return new Promise((resolve) => { |
| | |
| | | value: item.recoveryRate * 100, |
| | | sendCount: item.sendCount, |
| | | receiveCount: item.receiveCount, |
| | | averageScore: 4.2 + Math.random() * 0.8, |
| | | itemStyle: { color: item.color }, |
| | | })); |
| | | |
| | | this.chartData = chartData; |
| | | this.renderChart(chartData); |
| | | this.renderChart(chartData, "satisfactionType"); |
| | | resolve(); |
| | | }, 300); |
| | | }); |
| | | }, |
| | | |
| | | // 生成Mock维度图表数据 |
| | | generateMockDimensionChartData() { |
| | | return new Promise((resolve) => { |
| | | setTimeout(() => { |
| | | // 模拟维度数据 |
| | | const dimensionLabels = this.dict.type.dimensionality_type || [ |
| | | { value: "1", label: "维度1" }, |
| | | { value: "2", label: "维度2" }, |
| | | { value: "3", label: "维度3" }, |
| | | { value: "4", label: "维度4" }, |
| | | { value: "5", label: "维度5" }, |
| | | ]; |
| | | |
| | | const data = dimensionLabels.map((dim, index) => ({ |
| | | name: dim.label, |
| | | recoveryRate: Math.random() * 0.3 + 0.6, |
| | | sendCount: Math.floor(Math.random() * 2000) + 1000, |
| | | receiveCount: 0, |
| | | averageScore: 3.5 + Math.random() * 1.5, |
| | | dimension: dim.value, |
| | | color: this.getChartColor(index), |
| | | })); |
| | | |
| | | 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, |
| | | averageScore: item.averageScore, |
| | | dimension: item.dimension, |
| | | itemStyle: { color: item.color }, |
| | | })); |
| | | |
| | | this.chartData = chartData; |
| | | this.renderChart(chartData, "dimension"); |
| | | resolve(); |
| | | }, 300); |
| | | }); |
| | |
| | | }); |
| | | }, |
| | | |
| | | // 生成Mock维度详情数据 |
| | | generateMockDimensionDetail() { |
| | | return new Promise((resolve) => { |
| | | setTimeout(() => { |
| | | const questions = [ |
| | | { |
| | | scriptContent: "医护人员服务态度", |
| | | dimension: "1", |
| | | dimensionName: "服务态度", |
| | | 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, |
| | | }, |
| | | ], |
| | | }, |
| | | { |
| | | scriptContent: "医疗技术水平", |
| | | dimension: "2", |
| | | dimensionName: "技术水平", |
| | | 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, |
| | | }, |
| | | ], |
| | | }, |
| | | { |
| | | scriptContent: "就医环境设施", |
| | | dimension: "3", |
| | | dimensionName: "环境设施", |
| | | scriptType: 1, |
| | | answerPerson: 98, |
| | | noAnswerPerson: 22, |
| | | answerCount: 98, |
| | | totalCount: 120, |
| | | averageScore: 4.0, |
| | | maxScore: 5, |
| | | minScore: 2, |
| | | answerRate: 0.82, |
| | | options: [ |
| | | { |
| | | optionText: "非常满意", |
| | | chosenQuantity: 45, |
| | | chosenPercentage: 0.46, |
| | | }, |
| | | { |
| | | optionText: "满意", |
| | | chosenQuantity: 40, |
| | | chosenPercentage: 0.41, |
| | | }, |
| | | { |
| | | optionText: "一般", |
| | | chosenQuantity: 10, |
| | | chosenPercentage: 0.1, |
| | | }, |
| | | ], |
| | | }, |
| | | ]; |
| | | |
| | | resolve(questions); |
| | | }, 300); |
| | | }); |
| | | }, |
| | | |
| | | // 生成Mock类型明细数据 |
| | | generateMockTypeDetail() { |
| | | return new Promise((resolve) => { |
| | |
| | | "#FF9D4D", |
| | | "#9B8DFF", |
| | | "#FF6B6B", |
| | | "#66C2A5", |
| | | "#FC8D62", |
| | | "#8DA0CB", |
| | | "#E78AC3", |
| | | "#A6D854", |
| | | "#FFD92F", |
| | | ]; |
| | | return colors[index % colors.length]; |
| | | }, |
| | |
| | | } |
| | | }, |
| | | |
| | | // 处理图表Tab切换 |
| | | handleChartTabClick(tab) { |
| | | this.activeChartTab = tab.name; |
| | | this.loadChartData(); |
| | | }, |
| | | |
| | | // 处理查询 |
| | | handleSearch() { |
| | | this.detailQueryParams.pageNum = 1; |
| | | this.dimensionQueryParams.pageNum = 1; |
| | | this.loadData(); |
| | | // 强制重新渲染图表 |
| | | setTimeout(() => { |
| | | if (this.chartData.length === 0) { |
| | | this.renderChart([]); |
| | | this.renderChart([], this.activeChartTab); |
| | | } |
| | | }, 100); |
| | | }, |
| | |
| | | handleReset() { |
| | | this.$refs.queryForm.resetFields(); |
| | | this.queryParams.dateRange = []; |
| | | this.queryParams.deptCodes = []; // 重置为数组 |
| | | this.queryParams.wardCodes = []; // 重置为数组 |
| | | this.detailQueryParams.pageNum = 1; |
| | | this.dimensionQueryParams.pageNum = 1; |
| | | this.loadData(); |
| | | }, |
| | | |
| | | // 处理Tab切换 |
| | | handleTabClick(tab) { |
| | | if (tab.name === "typeDetail" && this.typeDetailData.length === 0) { |
| | | if ( |
| | | tab.name === "dimensionDetail" && |
| | | this.dimensionDetailData.length === 0 |
| | | ) { |
| | | this.loadDimensionDetailData(); |
| | | } else if ( |
| | | tab.name === "typeDetail" && |
| | | this.typeDetailData.length === 0 |
| | | ) { |
| | | this.loadTypeDetailData(); |
| | | } else if ( |
| | | tab.name === "questionDetail" && |
| | | this.questionDetailData.length === 0 |
| | | ) { |
| | | this.loadQuestionDetailData(); |
| | | } |
| | | }, |
| | | |
| | |
| | | handleDetailPageChange(page) { |
| | | this.detailQueryParams.pageNum = page; |
| | | this.loadQuestionDetailData(); |
| | | }, |
| | | |
| | | // 处理维度明细分页大小变化 |
| | | handleDimensionSizeChange(size) { |
| | | this.dimensionQueryParams.pageSize = size; |
| | | this.dimensionQueryParams.pageNum = 1; |
| | | this.loadDimensionDetailData(); |
| | | }, |
| | | |
| | | // 处理维度明细页码变化 |
| | | handleDimensionPageChange(page) { |
| | | this.dimensionQueryParams.pageNum = page; |
| | | this.loadDimensionDetailData(); |
| | | }, |
| | | |
| | | // 处理类型详情 |
| | |
| | | } |
| | | } |
| | | } |
| | | |
| | | .chart-tabs { |
| | | margin-bottom: 20px; |
| | | |
| | | ::v-deep .el-tabs__header { |
| | | margin-bottom: 0; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .tab-section { |
| | |
| | | } |
| | | } |
| | | |
| | | .detail-table-section { |
| | | .detail-table-section, |
| | | .dimension-detail-section { |
| | | .option-detail { |
| | | padding: 15px; |
| | | background: #f8f9fa; |
| | |
| | | padding: 12px 0; |
| | | } |
| | | |
| | | .question-row { |
| | | .question-row, |
| | | .dimension-row { |
| | | td { |
| | | background-color: #fff; |
| | | } |
| | |
| | | font-size: 16px; |
| | | } |
| | | |
| | | .summary-row { |
| | | .summary-row, |
| | | .dimension-summary-row { |
| | | margin-top: 20px; |
| | | padding: 20px; |
| | | background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); |
| | | border-radius: 8px; |
| | | border: 1px solid #dee2e6; |
| | | |
| | | .summary-content { |
| | | .summary-content, |
| | | .dimension-summary-content { |
| | | display: flex; |
| | | justify-content: space-around; |
| | | align-items: center; |
| | | |
| | | .summary-item { |
| | | .summary-item, |
| | | .dimension-summary-item { |
| | | text-align: center; |
| | | |
| | | .label { |
| | |
| | | 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 { |
| | |
| | | } |
| | | } |
| | | } |
| | | |
| | | /* 确保多选下拉框样式正确 */ |
| | | ::v-deep .el-select__tags { |
| | | max-width: 200px; |
| | | .el-tag { |
| | | margin: 2px 0 2px 6px; |
| | | } |
| | | } |
| | | @media (max-width: 768px) { |
| | | .satisfaction-statistics { |
| | | padding: 10px; |
| | |
| | | } |
| | | } |
| | | |
| | | .detail-table-section { |
| | | .summary-content { |
| | | .detail-table-section, |
| | | .dimension-detail-section { |
| | | .summary-content, |
| | | .dimension-summary-content { |
| | | flex-direction: column; |
| | | gap: 15px; |
| | | } |