| | |
| | | <template> |
| | | <div class="satisfaction-statistics"> |
| | | <!-- 查询条件区域 --> |
| | | <div class="query-section"> |
| | | <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.patientSource" |
| | | placeholder="请选择患者来源" |
| | | clearable |
| | | style="width: 200px" |
| | | > |
| | | <el-option |
| | | v-for="source in patientSourceList" |
| | | :key="source.value" |
| | | :label="source.label" |
| | | :value="source.value" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="科室" prop="deptCode"> |
| | | <el-select |
| | | v-model="queryParams.deptCode" |
| | | placeholder="请选择科室" |
| | | clearable |
| | | filterable |
| | | style="width: 200px" |
| | | > |
| | | <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" |
| | | > |
| | | <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> |
| | | </div> |
| | | |
| | | <!-- 满意度分类统计图表 --> |
| | | <div class="chart-section"> |
| | | <div class="chart-container"> |
| | | <div class="chart-title">满意度类型统计</div> |
| | | <div id="satisfactionBarChart" style="width: 100%; height: 400px"></div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 题目明细表格 --> |
| | | <div class="detail-table-section"> |
| | | <div class="section-title">题目明细统计</div> |
| | | |
| | | <el-table |
| | | v-loading="detailLoading" |
| | | :data="questionDetailData" |
| | | :border="true" |
| | | style="width: 100%" |
| | | row-class-name="question-row" |
| | | > |
| | | <el-table-column |
| | | type="expand" |
| | | width="60" |
| | | <el-card shadow="never"> |
| | | <el-form |
| | | :model="queryParams" |
| | | ref="queryForm" |
| | | size="medium" |
| | | :inline="true" |
| | | label-width="100px" |
| | | class="query-form" |
| | | > |
| | | <template slot-scope="{ row }"> |
| | | <div class="option-detail"> |
| | | <el-form-item label="患者来源" prop="patientSource"> |
| | | <el-select |
| | | v-model="queryParams.patientSource" |
| | | placeholder="请选择患者来源" |
| | | clearable |
| | | style="width: 200px" |
| | | > |
| | | <el-option |
| | | v-for="source in patientSourceList" |
| | | :key="source.value" |
| | | :label="source.label" |
| | | :value="source.value" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="科室" prop="deptCode"> |
| | | <el-select |
| | | v-model="queryParams.deptCode" |
| | | placeholder="请选择科室" |
| | | clearable |
| | | filterable |
| | | style="width: 200px" |
| | | > |
| | | <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" |
| | | > |
| | | <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 |
| | | :data="row.options" |
| | | v-loading="detailLoading" |
| | | :data="questionDetailData" |
| | | :border="true" |
| | | style="width: 100%" |
| | | class="inner-table" |
| | | 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="选项" |
| | | prop="optionText" |
| | | label="序号" |
| | | type="index" |
| | | align="center" |
| | | min-width="200" |
| | | width="60" |
| | | /> |
| | | |
| | | <el-table-column |
| | | label="选择人数" |
| | | prop="chosenQuantity" |
| | | label="题目" |
| | | prop="scriptContent" |
| | | align="center" |
| | | min-width="120" |
| | | /> |
| | | <el-table-column |
| | | label="选择比例" |
| | | prop="chosenPercentage" |
| | | align="center" |
| | | min-width="120" |
| | | min-width="300" |
| | | > |
| | | <template slot-scope="{ row: option }"> |
| | | {{ formatPercent(option.chosenPercentage) }} |
| | | <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.totalCount - row.answerCount }} |
| | | </template> |
| | | </el-table-column> |
| | | |
| | | <el-table-column |
| | | label="答题率" |
| | | prop="answerRate" |
| | | align="center" |
| | | width="100" |
| | | > |
| | | <template slot-scope="{ row }"> |
| | | {{ formatPercent(row.answerCount / row.totalCount) }} |
| | | </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">{{ totalScore.toFixed(1) }}</span> |
| | | </div> |
| | | <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> |
| | | </template> |
| | | </el-table-column> |
| | | </el-tab-pane> |
| | | |
| | | <el-table-column |
| | | label="序号" |
| | | type="index" |
| | | align="center" |
| | | width="60" |
| | | /> |
| | | <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="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="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="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="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="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="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="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="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="answerCount" |
| | | align="center" |
| | | width="100" |
| | | /> |
| | | <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="unanswerCount" |
| | | align="center" |
| | | width="100" |
| | | > |
| | | <template slot-scope="{ row }"> |
| | | {{ row.totalCount - row.answerCount }} |
| | | </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="answerRate" |
| | | align="center" |
| | | width="100" |
| | | > |
| | | <template slot-scope="{ row }"> |
| | | {{ formatPercent(row.answerCount / row.totalCount) }} |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <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> |
| | | |
| | | <!-- 综合得分行 --> |
| | | <div class="summary-row"> |
| | | <div class="summary-content"> |
| | | <div class="summary-item"> |
| | | <span class="label">综合得分:</span> |
| | | <span class="value">{{ totalScore.toFixed(1) }}</span> |
| | | </div> |
| | | <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> |
| | | <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> |
| | | |
| | | <!-- 分页 --> |
| | | <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" |
| | | /> |
| | | <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"; |
| | | |
| | | export default { |
| | | name: 'SatisfactionStatistics', |
| | | name: "SatisfactionStatistics", |
| | | data() { |
| | | return { |
| | | // 查询参数 |
| | | queryParams: { |
| | | patientSource: '', |
| | | deptCode: '', |
| | | wardCode: '', |
| | | dateRange: [] |
| | | patientSource: "", |
| | | deptCode: "", |
| | | wardCode: "", |
| | | dateRange: [], |
| | | }, |
| | | |
| | | // 当前激活的tab |
| | | activeTab: "questionDetail", |
| | | |
| | | // 患者来源选项 |
| | | patientSourceList: [ |
| | | { value: '1', label: '门诊' }, |
| | | { value: '2', label: '住院' }, |
| | | { value: '3', label: '急诊' }, |
| | | { value: '4', label: '体检' } |
| | | { value: "1", label: "门诊" }, |
| | | { value: "2", label: "住院" }, |
| | | { value: "3", label: "急诊" }, |
| | | { value: "4", label: "出院" }, |
| | | ], |
| | | |
| | | // 科室列表 |
| | |
| | | // 加载状态 |
| | | loading: false, |
| | | detailLoading: false, |
| | | typeDetailLoading: false, |
| | | |
| | | // 题目明细数据 |
| | | questionDetailData: [], |
| | |
| | | // 题目明细查询参数 |
| | | detailQueryParams: { |
| | | pageNum: 1, |
| | | pageSize: 10 |
| | | pageSize: 10, |
| | | }, |
| | | |
| | | // 题目明细总数 |
| | |
| | | totalAnswerCount: 0, |
| | | totalAnswerRate: 0, |
| | | |
| | | // 各类型统计明细数据 |
| | | typeDetailData: [], |
| | | |
| | | // 统计信息 |
| | | totalSendCount: 12560, |
| | | totalReceiveCount: 10240, |
| | | overallRecoveryRate: 0, |
| | | |
| | | // 类型统计汇总 |
| | | averageRecoveryRate: 0, |
| | | averageTypeScore: 0, |
| | | highSatisfactionCount: 0, |
| | | |
| | | // 日期选择器选项 |
| | | pickerOptions: { |
| | | shortcuts: [ |
| | | { |
| | | text: '最近一周', |
| | | text: "最近一周", |
| | | onClick(picker) { |
| | | const end = new Date(); |
| | | const start = new Date(); |
| | | start.setTime(start.getTime() - 3600 * 1000 * 24 * 7); |
| | | picker.$emit('pick', [start, end]); |
| | | } |
| | | picker.$emit("pick", [start, end]); |
| | | }, |
| | | }, |
| | | { |
| | | text: '最近一个月', |
| | | text: "最近一个月", |
| | | onClick(picker) { |
| | | const end = new Date(); |
| | | const start = new Date(); |
| | | start.setTime(start.getTime() - 3600 * 1000 * 24 * 30); |
| | | picker.$emit('pick', [start, end]); |
| | | } |
| | | picker.$emit("pick", [start, end]); |
| | | }, |
| | | }, |
| | | { |
| | | text: '最近三个月', |
| | | text: "最近三个月", |
| | | onClick(picker) { |
| | | const end = new Date(); |
| | | const start = new Date(); |
| | | start.setTime(start.getTime() - 3600 * 1000 * 24 * 90); |
| | | picker.$emit('pick', [start, end]); |
| | | } |
| | | } |
| | | picker.$emit("pick", [start, end]); |
| | | }, |
| | | }, |
| | | ], |
| | | disabledDate(time) { |
| | | return time.getTime() > Date.now(); |
| | | } |
| | | }, |
| | | }, |
| | | |
| | | // Mock数据 - 满意度分类 |
| | | mockSatisfactionCategories: ['服务态度', '技术水平', '环境设施', '沟通效果', '等待时间', '收费合理性'], |
| | | // Mock数据 - 满意度类型 |
| | | mockSatisfactionTypes: [ |
| | | { name: '非常满意', value: 85, color: '#36B37E' }, |
| | | { name: '满意', value: 72, color: '#4CAF50' }, |
| | | { name: '一般', value: 60, color: '#FF9D4D' }, |
| | | { name: '不满意', value: 15, color: '#FF5C5C' }, |
| | | { name: '非常不满意', value: 5, color: '#F44336' } |
| | | ] |
| | | // 满意度类型数据 |
| | | satisfactionTypes: [ |
| | | { id: 401, name: "出院满意度", color: "#36B37E" }, |
| | | { id: 402, name: "住院满意度", color: "#4CAF50" }, |
| | | { id: 403, name: "门诊满意度", color: "#409EFF" }, |
| | | { id: 404, name: "常用满意度", color: "#FF9D4D" }, |
| | | ], |
| | | }; |
| | | }, |
| | | |
| | | mounted() { |
| | | this.initData(); |
| | | this.initChart(); |
| | | }, |
| | | |
| | | beforeDestroy() { |
| | |
| | | async initData() { |
| | | await this.getDeptList(); |
| | | await this.getWardList(); |
| | | this.initChart(); |
| | | await this.loadData(); |
| | | }, |
| | | |
| | | // 获取科室列表 |
| | | getDeptList() { |
| | | // 模拟API调用获取科室列表 |
| | | return new Promise((resolve) => { |
| | | setTimeout(() => { |
| | | this.deptList = [ |
| | | { value: 'dept001', label: '心血管内科' }, |
| | | { value: 'dept002', label: '神经内科' }, |
| | | { value: 'dept003', label: '普外科' }, |
| | | { value: 'dept004', label: '骨科' }, |
| | | { value: 'dept005', label: '妇产科' }, |
| | | { value: 'dept006', label: '儿科' } |
| | | { value: "dept001", label: "心血管内科" }, |
| | | { value: "dept002", label: "神经内科" }, |
| | | { value: "dept003", label: "普外科" }, |
| | | { value: "dept004", label: "骨科" }, |
| | | { value: "dept005", label: "妇产科" }, |
| | | { value: "dept006", label: "儿科" }, |
| | | ]; |
| | | resolve(); |
| | | }, 100); |
| | |
| | | |
| | | // 获取病区列表 |
| | | getWardList() { |
| | | // 模拟API调用获取病区列表 |
| | | return new Promise((resolve) => { |
| | | setTimeout(() => { |
| | | this.wardList = [ |
| | | { value: 'ward001', label: '内科一病区' }, |
| | | { value: 'ward002', label: '内科二病区' }, |
| | | { value: 'ward003', label: '外科一病区' }, |
| | | { value: 'ward004', label: '外科二病区' }, |
| | | { value: 'ward005', label: '妇产科病区' }, |
| | | { value: 'ward006', label: '儿科病区' } |
| | | { value: "ward001", label: "内科一病区" }, |
| | | { value: "ward002", label: "内科二病区" }, |
| | | { value: "ward003", label: "外科一病区" }, |
| | | { value: "ward004", label: "外科二病区" }, |
| | | { value: "ward005", label: "妇产科病区" }, |
| | | { value: "ward006", label: "儿科病区" }, |
| | | ]; |
| | | resolve(); |
| | | }, 100); |
| | |
| | | async loadData() { |
| | | await Promise.all([ |
| | | this.loadChartData(), |
| | | this.loadQuestionDetailData() |
| | | this.loadQuestionDetailData(), |
| | | this.loadTypeDetailData(), |
| | | ]); |
| | | }, |
| | | |
| | | // 加载图表数据 |
| | | loadChartData() { |
| | | async loadChartData() { |
| | | this.loading = true; |
| | | return new Promise((resolve) => { |
| | | setTimeout(() => { |
| | | this.renderChart(this.generateChartData()); |
| | | this.loading = false; |
| | | resolve(); |
| | | }, 500); |
| | | }); |
| | | try { |
| | | // 模拟API调用 |
| | | const chartData = await this.generateChartData(); |
| | | this.renderChart(chartData); |
| | | |
| | | // 计算总体回收率 |
| | | this.overallRecoveryRate = this.totalReceiveCount / this.totalSendCount; |
| | | } finally { |
| | | this.loading = false; |
| | | } |
| | | }, |
| | | |
| | | // 加载题目明细数据 |
| | | loadQuestionDetailData() { |
| | | async loadQuestionDetailData() { |
| | | this.detailLoading = true; |
| | | return new Promise((resolve) => { |
| | | setTimeout(() => { |
| | | const mockData = this.generateMockQuestionDetail(); |
| | | this.questionDetailData = mockData.list; |
| | | this.detailTotal = mockData.total; |
| | | try { |
| | | const mockData = await this.generateMockQuestionDetail(); |
| | | this.questionDetailData = mockData.list; |
| | | this.detailTotal = mockData.total; |
| | | this.calculateSummary(mockData); |
| | | } finally { |
| | | this.detailLoading = false; |
| | | } |
| | | }, |
| | | |
| | | // 计算综合得分 |
| | | this.calculateSummary(mockData); |
| | | this.detailLoading = false; |
| | | resolve(); |
| | | }, 500); |
| | | }); |
| | | // 加载类型明细数据 |
| | | async loadTypeDetailData() { |
| | | this.typeDetailLoading = true; |
| | | try { |
| | | const mockData = await this.generateMockTypeDetail(); |
| | | this.typeDetailData = mockData; |
| | | |
| | | // 计算类型统计汇总 |
| | | this.calculateTypeSummary(mockData); |
| | | } finally { |
| | | this.typeDetailLoading = false; |
| | | } |
| | | }, |
| | | |
| | | // 计算综合得分 |
| | |
| | | let totalAnswerCount = 0; |
| | | let totalCount = 0; |
| | | |
| | | data.list.forEach(item => { |
| | | 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.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 echarts = require('echarts'); |
| | | const chartDom = document.getElementById('satisfactionBarChart'); |
| | | const chartDom = document.getElementById("satisfactionBarChart"); |
| | | if (!chartDom) return; |
| | | |
| | | this.barChart = echarts.init(chartDom); |
| | | |
| | | // 监听窗口变化 |
| | | window.addEventListener('resize', this.handleChartResize); |
| | | window.addEventListener("resize", this.handleChartResize); |
| | | }, |
| | | |
| | | // 生成图表数据 |
| | | generateChartData() { |
| | | const categories = this.mockSatisfactionCategories; |
| | | const series = this.mockSatisfactionTypes.map(type => ({ |
| | | name: type.name, |
| | | type: 'bar', |
| | | barWidth: 25, |
| | | stack: '满意度', |
| | | data: categories.map(() => Math.floor(Math.random() * 20) + 10), // 随机数据 |
| | | itemStyle: { |
| | | color: type.color |
| | | } |
| | | })); |
| | | return new Promise((resolve) => { |
| | | setTimeout(() => { |
| | | const data = this.satisfactionTypes.map((type) => ({ |
| | | name: type.name, |
| | | recoveryRate: Math.random() * 0.3 + 0.6, // 60%-90%的回收率 |
| | | sendCount: Math.floor(Math.random() * 3000) + 1500, // 1500-4500 |
| | | receiveCount: 0, |
| | | color: type.color, |
| | | })); |
| | | |
| | | return { |
| | | categories, |
| | | legend: this.mockSatisfactionTypes.map(type => type.name), |
| | | series |
| | | }; |
| | | // 计算回收数量 |
| | | 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 |
| | | ); |
| | | |
| | | resolve({ |
| | | data: data.map((item) => ({ |
| | | name: item.name, |
| | | value: item.recoveryRate * 100, // 转换为百分比 |
| | | sendCount: item.sendCount, |
| | | receiveCount: item.receiveCount, |
| | | itemStyle: { color: item.color }, |
| | | })), |
| | | }); |
| | | }, 300); |
| | | }); |
| | | }, |
| | | |
| | | // 渲染图表 |
| | |
| | | |
| | | const option = { |
| | | title: { |
| | | text: '满意度类型统计', |
| | | left: 'center', |
| | | textStyle: { |
| | | fontSize: 16, |
| | | fontWeight: 'normal', |
| | | color: '#333' |
| | | } |
| | | text: "", |
| | | left: "center", |
| | | }, |
| | | tooltip: { |
| | | trigger: 'axis', |
| | | trigger: "axis", |
| | | axisPointer: { |
| | | type: 'shadow' |
| | | type: "shadow", |
| | | }, |
| | | formatter: (params) => { |
| | | let result = `<div style="margin-bottom: 5px; font-weight: bold;">${params[0].name}</div>`; |
| | | let total = 0; |
| | | |
| | | params.forEach(param => { |
| | | result += `<div style="margin: 2px 0;"> |
| | | <span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:${param.color};margin-right:5px;"></span> |
| | | ${param.seriesName}: <strong>${param.value}%</strong> |
| | | </div>`; |
| | | total += param.value; |
| | | }); |
| | | |
| | | result += `<div style="margin-top: 5px; padding-top: 5px; border-top: 1px solid #eee;"> |
| | | <strong>总计: ${total}%</strong> |
| | | </div>`; |
| | | return result; |
| | | } |
| | | }, |
| | | legend: { |
| | | data: chartData.legend, |
| | | top: 20, |
| | | textStyle: { |
| | | fontSize: 12, |
| | | color: '#666' |
| | | } |
| | | 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: 80, |
| | | containLabel: true |
| | | left: "3%", |
| | | right: "4%", |
| | | bottom: "3%", |
| | | top: 60, |
| | | containLabel: true, |
| | | }, |
| | | xAxis: { |
| | | type: 'category', |
| | | data: chartData.categories, |
| | | type: "category", |
| | | data: chartData.data.map((item) => item.name), |
| | | axisLabel: { |
| | | interval: 0, |
| | | rotate: 0, |
| | | fontSize: 12, |
| | | color: '#666' |
| | | color: "#666", |
| | | }, |
| | | axisLine: { |
| | | lineStyle: { |
| | | color: '#DCDFE6' |
| | | } |
| | | color: "#DCDFE6", |
| | | }, |
| | | }, |
| | | axisTick: { |
| | | alignWithLabel: true |
| | | } |
| | | alignWithLabel: true, |
| | | }, |
| | | }, |
| | | yAxis: { |
| | | type: 'value', |
| | | name: '百分比 (%)', |
| | | type: "value", |
| | | name: "填报比例 (%)", |
| | | min: 0, |
| | | max: 100, |
| | | axisLabel: { |
| | | formatter: '{value}%', |
| | | color: '#666' |
| | | formatter: "{value}%", |
| | | color: "#666", |
| | | }, |
| | | axisLine: { |
| | | lineStyle: { |
| | | color: '#DCDFE6' |
| | | } |
| | | color: "#DCDFE6", |
| | | }, |
| | | }, |
| | | splitLine: { |
| | | lineStyle: { |
| | | type: 'dashed', |
| | | color: '#E4E7ED' |
| | | } |
| | | } |
| | | type: "dashed", |
| | | color: "#E4E7ED", |
| | | }, |
| | | }, |
| | | }, |
| | | series: chartData.series |
| | | series: [ |
| | | { |
| | | name: "填报比例", |
| | | type: "bar", |
| | | barWidth: 40, |
| | | data: chartData.data, |
| | | itemStyle: { |
| | | color: (params) => { |
| | | return params.data.itemStyle.color; |
| | | }, |
| | | }, |
| | | label: { |
| | | show: true, |
| | | position: "top", |
| | | formatter: "{c}%", |
| | | fontSize: 12, |
| | | color: "#333", |
| | | }, |
| | | }, |
| | | ], |
| | | }; |
| | | |
| | | this.barChart.setOption(option); |
| | |
| | | |
| | | // 生成Mock题目详情数据 |
| | | generateMockQuestionDetail() { |
| | | const questions = [ |
| | | { |
| | | scriptContent: '您对医护人员的服务态度是否满意', |
| | | scriptType: 1, // 1: 单选题, 2: 多选题 |
| | | totalCount: 156, |
| | | answerCount: 145, |
| | | averageScore: 4.5, |
| | | maxScore: 5, |
| | | minScore: 3, |
| | | 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 } |
| | | ] |
| | | }, |
| | | { |
| | | scriptContent: '您对医生的诊疗水平和技术能力评价如何', |
| | | scriptType: 1, |
| | | totalCount: 156, |
| | | answerCount: 142, |
| | | averageScore: 4.7, |
| | | maxScore: 5, |
| | | minScore: 3, |
| | | options: [ |
| | | { optionText: '非常专业', chosenQuantity: 95, chosenPercentage: 0.67 }, |
| | | { optionText: '比较专业', chosenQuantity: 40, chosenPercentage: 0.28 }, |
| | | { optionText: '一般', chosenQuantity: 5, chosenPercentage: 0.04 }, |
| | | { optionText: '不够专业', chosenQuantity: 2, chosenPercentage: 0.01 }, |
| | | { optionText: '非常不专业', chosenQuantity: 0, chosenPercentage: 0 } |
| | | ] |
| | | }, |
| | | { |
| | | scriptContent: '您对医院的环境和卫生状况是否满意', |
| | | scriptType: 1, |
| | | totalCount: 156, |
| | | answerCount: 138, |
| | | averageScore: 4.3, |
| | | maxScore: 5, |
| | | minScore: 2, |
| | | options: [ |
| | | { optionText: '非常满意', chosenQuantity: 75, chosenPercentage: 0.54 }, |
| | | { optionText: '满意', chosenQuantity: 50, chosenPercentage: 0.36 }, |
| | | { optionText: '一般', chosenQuantity: 10, chosenPercentage: 0.07 }, |
| | | { optionText: '不满意', chosenQuantity: 3, chosenPercentage: 0.02 }, |
| | | { optionText: '非常不满意', chosenQuantity: 0, chosenPercentage: 0 } |
| | | ] |
| | | }, |
| | | { |
| | | scriptContent: '您认为医护人员与您的沟通是否充分', |
| | | scriptType: 1, |
| | | totalCount: 156, |
| | | answerCount: 140, |
| | | averageScore: 4.6, |
| | | maxScore: 5, |
| | | minScore: 3, |
| | | options: [ |
| | | { optionText: '沟通非常充分', chosenQuantity: 85, chosenPercentage: 0.61 }, |
| | | { optionText: '沟通比较充分', chosenQuantity: 45, chosenPercentage: 0.32 }, |
| | | { optionText: '沟通一般', chosenQuantity: 8, chosenPercentage: 0.06 }, |
| | | { optionText: '沟通不够充分', chosenQuantity: 2, chosenPercentage: 0.01 }, |
| | | { optionText: '沟通非常不充分', chosenQuantity: 0, chosenPercentage: 0 } |
| | | ] |
| | | }, |
| | | { |
| | | scriptContent: '您对等待就诊和治疗的时间是否满意', |
| | | scriptType: 1, |
| | | totalCount: 156, |
| | | answerCount: 135, |
| | | averageScore: 4.0, |
| | | maxScore: 5, |
| | | minScore: 1, |
| | | options: [ |
| | | { optionText: '等待时间很短', chosenQuantity: 60, chosenPercentage: 0.44 }, |
| | | { optionText: '等待时间合理', chosenQuantity: 55, chosenPercentage: 0.41 }, |
| | | { optionText: '等待时间较长', chosenQuantity: 15, chosenPercentage: 0.11 }, |
| | | { optionText: '等待时间很长', chosenQuantity: 5, chosenPercentage: 0.04 }, |
| | | { optionText: '无法忍受的等待', chosenQuantity: 0, chosenPercentage: 0 } |
| | | ] |
| | | }, |
| | | { |
| | | scriptContent: '您对医院收费的透明度和合理性评价如何', |
| | | scriptType: 1, |
| | | totalCount: 156, |
| | | answerCount: 130, |
| | | averageScore: 4.2, |
| | | maxScore: 5, |
| | | minScore: 2, |
| | | options: [ |
| | | { optionText: '非常透明合理', chosenQuantity: 70, chosenPercentage: 0.54 }, |
| | | { optionText: '比较透明合理', chosenQuantity: 45, chosenPercentage: 0.35 }, |
| | | { optionText: '一般', chosenQuantity: 10, chosenPercentage: 0.08 }, |
| | | { optionText: '不太透明', chosenQuantity: 5, chosenPercentage: 0.04 }, |
| | | { optionText: '非常不透明', chosenQuantity: 0, chosenPercentage: 0 } |
| | | ] |
| | | }, |
| | | { |
| | | scriptContent: '您会向亲友推荐我们医院吗', |
| | | scriptType: 1, |
| | | totalCount: 156, |
| | | answerCount: 148, |
| | | averageScore: 4.8, |
| | | maxScore: 5, |
| | | minScore: 3, |
| | | options: [ |
| | | { optionText: '非常愿意推荐', chosenQuantity: 100, chosenPercentage: 0.68 }, |
| | | { optionText: '比较愿意推荐', chosenQuantity: 40, chosenPercentage: 0.27 }, |
| | | { optionText: '一般', chosenQuantity: 6, chosenPercentage: 0.04 }, |
| | | { optionText: '不太愿意推荐', chosenQuantity: 2, chosenPercentage: 0.01 }, |
| | | { optionText: '绝对不会推荐', chosenQuantity: 0, chosenPercentage: 0 } |
| | | ] |
| | | }, |
| | | { |
| | | scriptContent: '您对以下哪些方面比较满意(多选)', |
| | | scriptType: 2, // 多选题 |
| | | totalCount: 156, |
| | | answerCount: 150, |
| | | averageScore: 4.4, |
| | | maxScore: 5, |
| | | minScore: 3, |
| | | options: [ |
| | | { optionText: '医疗技术水平', chosenQuantity: 120, chosenPercentage: 0.8 }, |
| | | { optionText: '服务态度', chosenQuantity: 110, chosenPercentage: 0.73 }, |
| | | { optionText: '环境卫生', chosenQuantity: 90, chosenPercentage: 0.6 }, |
| | | { optionText: '医疗设备', chosenQuantity: 85, chosenPercentage: 0.57 }, |
| | | { optionText: '收费透明度', chosenQuantity: 70, chosenPercentage: 0.47 }, |
| | | { optionText: '等待时间', chosenQuantity: 60, chosenPercentage: 0.4 } |
| | | ] |
| | | }, |
| | | { |
| | | scriptContent: '您认为医院哪些方面需要改进(多选)', |
| | | scriptType: 2, |
| | | totalCount: 156, |
| | | answerCount: 125, |
| | | averageScore: 3.8, |
| | | maxScore: 5, |
| | | minScore: 2, |
| | | options: [ |
| | | { optionText: '等待时间过长', chosenQuantity: 80, chosenPercentage: 0.64 }, |
| | | { optionText: '就诊流程复杂', chosenQuantity: 70, chosenPercentage: 0.56 }, |
| | | { optionText: '费用较高', chosenQuantity: 60, chosenPercentage: 0.48 }, |
| | | { optionText: '停车困难', chosenQuantity: 50, chosenPercentage: 0.4 }, |
| | | { optionText: '指引标识不清', chosenQuantity: 40, chosenPercentage: 0.32 }, |
| | | { optionText: '网络预约不便', chosenQuantity: 30, chosenPercentage: 0.24 } |
| | | ] |
| | | }, |
| | | { |
| | | scriptContent: '您对本次住院的整体体验评分', |
| | | scriptType: 1, |
| | | totalCount: 156, |
| | | answerCount: 152, |
| | | averageScore: 4.6, |
| | | maxScore: 5, |
| | | minScore: 3, |
| | | options: [ |
| | | { optionText: '5分(非常满意)', chosenQuantity: 90, chosenPercentage: 0.59 }, |
| | | { optionText: '4分(满意)', chosenQuantity: 50, chosenPercentage: 0.33 }, |
| | | { optionText: '3分(一般)', chosenQuantity: 10, chosenPercentage: 0.07 }, |
| | | { optionText: '2分(不满意)', chosenQuantity: 2, chosenPercentage: 0.01 }, |
| | | { optionText: '1分(非常不满意)', chosenQuantity: 0, chosenPercentage: 0 } |
| | | ] |
| | | } |
| | | ]; |
| | | return new Promise((resolve) => { |
| | | setTimeout(() => { |
| | | const questions = [ |
| | | { |
| | | scriptContent: "您对医护人员的服务态度是否满意", |
| | | scriptType: 1, |
| | | totalCount: 156, |
| | | answerCount: 145, |
| | | averageScore: 4.5, |
| | | maxScore: 5, |
| | | minScore: 3, |
| | | 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, |
| | | }, |
| | | ], |
| | | }, |
| | | { |
| | | scriptContent: "您对医生的诊疗水平和技术能力评价如何", |
| | | scriptType: 1, |
| | | totalCount: 156, |
| | | answerCount: 142, |
| | | averageScore: 4.7, |
| | | maxScore: 5, |
| | | minScore: 3, |
| | | options: [ |
| | | { |
| | | optionText: "非常专业", |
| | | chosenQuantity: 95, |
| | | chosenPercentage: 0.67, |
| | | }, |
| | | { |
| | | optionText: "比较专业", |
| | | chosenQuantity: 40, |
| | | chosenPercentage: 0.28, |
| | | }, |
| | | { |
| | | optionText: "一般", |
| | | chosenQuantity: 5, |
| | | chosenPercentage: 0.04, |
| | | }, |
| | | { |
| | | optionText: "不够专业", |
| | | chosenQuantity: 2, |
| | | chosenPercentage: 0.01, |
| | | }, |
| | | { |
| | | optionText: "非常不专业", |
| | | chosenQuantity: 0, |
| | | chosenPercentage: 0, |
| | | }, |
| | | ], |
| | | }, |
| | | { |
| | | scriptContent: "您对医院的环境和卫生状况是否满意", |
| | | scriptType: 1, |
| | | totalCount: 156, |
| | | answerCount: 138, |
| | | averageScore: 4.3, |
| | | maxScore: 5, |
| | | minScore: 2, |
| | | options: [ |
| | | { |
| | | optionText: "非常满意", |
| | | chosenQuantity: 75, |
| | | chosenPercentage: 0.54, |
| | | }, |
| | | { |
| | | optionText: "满意", |
| | | chosenQuantity: 50, |
| | | chosenPercentage: 0.36, |
| | | }, |
| | | { |
| | | optionText: "一般", |
| | | chosenQuantity: 10, |
| | | chosenPercentage: 0.07, |
| | | }, |
| | | { |
| | | optionText: "不满意", |
| | | chosenQuantity: 3, |
| | | chosenPercentage: 0.02, |
| | | }, |
| | | { |
| | | optionText: "非常不满意", |
| | | chosenQuantity: 0, |
| | | chosenPercentage: 0, |
| | | }, |
| | | ], |
| | | }, |
| | | ]; |
| | | |
| | | // 分页处理 |
| | | const startIndex = (this.detailQueryParams.pageNum - 1) * this.detailQueryParams.pageSize; |
| | | const endIndex = startIndex + this.detailQueryParams.pageSize; |
| | | const paginatedData = questions.slice(startIndex, endIndex); |
| | | const startIndex = |
| | | (this.detailQueryParams.pageNum - 1) * |
| | | this.detailQueryParams.pageSize; |
| | | const endIndex = startIndex + this.detailQueryParams.pageSize; |
| | | const paginatedData = questions.slice(startIndex, endIndex); |
| | | |
| | | return { |
| | | list: paginatedData, |
| | | total: questions.length |
| | | }; |
| | | resolve({ |
| | | list: paginatedData, |
| | | total: questions.length, |
| | | }); |
| | | }, 300); |
| | | }); |
| | | }, |
| | | |
| | | // 生成Mock类型明细数据 |
| | | generateMockTypeDetail() { |
| | | return new Promise((resolve) => { |
| | | setTimeout(() => { |
| | | // 在 generateMockTypeDetail 方法中替换为: |
| | | 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); |
| | | }); |
| | | }, |
| | | |
| | | // 处理图表响应式 |
| | |
| | | this.loadData(); |
| | | }, |
| | | |
| | | // 处理Tab切换 |
| | | handleTabClick(tab) { |
| | | if (tab.name === "typeDetail" && this.typeDetailData.length === 0) { |
| | | this.loadTypeDetailData(); |
| | | } |
| | | }, |
| | | |
| | | // 处理明细分页大小变化 |
| | | handleDetailSizeChange(size) { |
| | | this.detailQueryParams.pageSize = size; |
| | |
| | | this.loadQuestionDetailData(); |
| | | }, |
| | | |
| | | // 处理类型详情 |
| | | handleTypeDetail(row) { |
| | | this.$message.info(`查看类型详情:${row.typeName}`); |
| | | // 这里可以跳转到详情页面或打开详情对话框 |
| | | }, |
| | | |
| | | // 处理导出数据 |
| | | handleExportData(row) { |
| | | this.$message.success(`正在导出 ${row.typeName} 数据...`); |
| | | // 这里可以实现导出逻辑 |
| | | }, |
| | | |
| | | // 格式化百分比 |
| | | formatPercent(value) { |
| | | if (value === null || value === undefined) return '-'; |
| | | if (value === null || value === undefined) return "-"; |
| | | const num = parseFloat(value); |
| | | if (isNaN(num)) return '-'; |
| | | if (isNaN(num)) return "-"; |
| | | return `${(num * 100).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"; |
| | | }, |
| | | }, |
| | | }; |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | .satisfaction-statistics { |
| | | padding: 20px; |
| | | background-color: #f5f7fa; |
| | | min-height: 100vh; |
| | | |
| | | .query-section { |
| | | background: #fff; |
| | | padding: 20px; |
| | | border-radius: 4px; |
| | | margin-bottom: 20px; |
| | | |
| | | .query-form { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | align-items: center; |
| | | |
| | | ::v-deep .el-form-item { |
| | | margin-bottom: 20px; |
| | | margin-bottom: 0; |
| | | margin-right: 20px; |
| | | |
| | | &:not(:last-child) { |
| | | margin-right: 20px; |
| | | &:last-child { |
| | | margin-right: 0; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | .chart-section { |
| | | background: #fff; |
| | | padding: 20px; |
| | | border-radius: 4px; |
| | | margin-bottom: 20px; |
| | | |
| | | .chart-container { |
| | | width: 100%; |
| | | |
| | | .chart-title { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: #333; |
| | | .chart-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | margin-bottom: 20px; |
| | | padding-bottom: 10px; |
| | | padding-bottom: 15px; |
| | | border-bottom: 1px solid #f0f0f0; |
| | | } |
| | | |
| | | #satisfactionBarChart { |
| | | width: 100%; |
| | | height: 400px; |
| | | .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; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | .detail-table-section { |
| | | background: #fff; |
| | | padding: 20px; |
| | | border-radius: 4px; |
| | | |
| | | .section-title { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: #333; |
| | | margin-bottom: 20px; |
| | | padding-bottom: 10px; |
| | | border-bottom: 1px solid #f0f0f0; |
| | | .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: 20px; |
| | | padding: 15px; |
| | | background: #f8f9fa; |
| | | border-radius: 4px; |
| | | margin: 10px 0; |
| | |
| | | background-color: #f8f9fa; |
| | | font-weight: 600; |
| | | color: #333; |
| | | padding: 12px 0; |
| | | } |
| | | |
| | | td { |
| | | padding: 12px 0; |
| | | } |
| | | |
| | | .question-row { |
| | |
| | | margin-top: 20px; |
| | | padding: 20px; |
| | | background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); |
| | | border-radius: 4px; |
| | | border-radius: 8px; |
| | | border: 1px solid #dee2e6; |
| | | |
| | | .summary-content { |
| | |
| | | |
| | | .label { |
| | | font-size: 16px; |
| | | color: #666; |
| | | color: #606266; |
| | | margin-right: 8px; |
| | | } |
| | | |
| | | .value { |
| | | font-size: 24px; |
| | | font-weight: 600; |
| | | color: #1890ff; |
| | | color: #409eff; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | .pagination-section { |
| | | display: flex; |
| | | justify-content: center; |
| | | padding: 20px 0 0 0; |
| | | } |
| | | } |
| | | |
| | | .pagination-section { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | background: #fff; |
| | | padding: 20px; |
| | | border-radius: 4px; |
| | | margin-top: 20px; |
| | | .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; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 内层表格样式 |
| | |
| | | } |
| | | } |
| | | } |
| | | |
| | | @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> |