WXL (wul)
3 天以前 dac4393e8af2646f544f6e1ca24dab11b40c8492
src/views/Satisfaction/sfstatistics/components/SatisfactionStatistics.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1809 @@
<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>