0c7cc21d8a51e164dd2fe4ce73ab566b3a9081a9..d0c13711e4a03409c28d7a01716218689e52c11c
2026-03-27 WXL (wul)
测试完成
d0c137 对比 | 目录
2026-03-27 WXL (wul)
测试完成
4c5d48 对比 | 目录
已删除1个文件
已重命名1个文件
已修改16个文件
已添加2个文件
5463 ■■■■ 文件已修改
src/views/Satisfaction/1.vue 1814 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/Satisfaction/sfstatistics/IndicatorStatistics.vue 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/Satisfaction/sfstatistics/components/FollowupStatistics.vue 387 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/Satisfaction/sfstatistics/components/SatisfactionStatistics.vue 1789 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/Satisfaction/sfstatistics/components/components/TopicDialog.vue 117 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/Satisfaction/sfstatistics/components/visitStatistics.vue 1030 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/followvisit/Continue/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/followvisit/OutpatientAgain/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/followvisit/Tracking/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/followvisit/again/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/followvisit/discharge/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/followvisit/discharge/outpatientService.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/followvisit/record/TracingInfo/index.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/followvisit/record/detailpage/index.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/followvisit/record/physical/index.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/followvisit/zbAgain/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/patient/propaganda/QuestionnaireTask.vue 73 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/patient/propaganda/index.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/sfstatistics/percentage/index.vue 149 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
指标统计整合页适配丽水省立同德.zip 补丁 | 查看 | 原始文档 | blame | 历史
src/views/Satisfaction/1.vue
ÎļþÒÑɾ³ý
src/views/Satisfaction/sfstatistics/IndicatorStatistics.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,66 @@
<!-- StatisticsMain.vue -->
<template>
  <div class="statistics-main">
    <el-tabs v-model="activeTab" @tab-click="handleTabChange">
      <el-tab-pane label="满意度统计" name="followup">
        <followup-statistics
          v-if="activeTab === 'followup'"
          ref="followupRef"
        />
      </el-tab-pane>
      <el-tab-pane label="复诊通知统计" name="visitStatistics">
        <visit-Statistics
          v-if="activeTab === 'visitStatistics'"
          ref="visitStatisticsRef"
        />
      </el-tab-pane>
    </el-tabs>
  </div>
</template>
<script>
import FollowupStatistics from "./components/FollowupStatistics.vue";
import visitStatistics from "./components/visitStatistics.vue";
import SatisfactionStatistics from "./components/SatisfactionStatistics.vue";
export default {
  name: "StatisticsMain",
  components: {
    FollowupStatistics,
    SatisfactionStatistics,
    visitStatistics,
  },
  data() {
    return {
      activeTab: "followup",
    };
  },
  methods: {
    handleTabChange(tab) {
      console.log("切换到:", tab.name);
    },
  },
};
</script>
<style lang="scss" scoped>
.statistics-main {
  padding: 20px;
  background: #fff;
  min-height: calc(100vh - 84px);
  ::v-deep .el-tabs__header {
    margin-bottom: 20px;
  }
  ::v-deep .el-tabs__item {
    font-size: 16px;
    font-weight: 500;
  }
  ::v-deep .el-tabs__nav-wrap::after {
    height: 1px;
  }
}
</style>
src/views/Satisfaction/sfstatistics/components/FollowupStatistics.vue
@@ -142,9 +142,11 @@
          v-if="queryParams.statisticaltype == 1"
          label="出院病区"
          align="center"
          sortable
          key="leavehospitaldistrictname"
          prop="leavehospitaldistrictname"
          :show-overflow-tooltip="true"
          :sort-method="sortChineseNumber"
          min-width="120"
        />
@@ -191,7 +193,12 @@
          min-width="100"
        >
          <template slot-scope="scope">
            <span v-if="scope.row.followUpRate !== null && scope.row.followUpRate !== undefined">
            <span
              v-if="
                scope.row.followUpRate !== null &&
                scope.row.followUpRate !== undefined
              "
            >
              {{ formatPercent(scope.row.followUpRate) }}
            </span>
            <span v-else>-</span>
@@ -241,24 +248,20 @@
          min-width="100"
        >
          <template slot-scope="scope">
            <span v-if="scope.row.joyTotal !== null && scope.row.joyTotal !== undefined">
            <span
              v-if="
                scope.row.joyTotal !== null && scope.row.joyTotal !== undefined
              "
            >
              {{ formatPercent(scope.row.joyTotal) }}
            </span>
            <span v-else>-</span>
          </template>
        </el-table-column>
        <el-table-column
          label="操作"
          align="center"
          fixed="right"
          width="120"
        >
        <el-table-column label="操作" align="center" fixed="right" width="120">
          <template slot-scope="scope">
            <el-button
              type="text"
              @click="getinfo(scope.row)"
            >
            <el-button type="text" @click="getinfo(scope.row)">
              <i class="el-icon-s-order" style="margin-right: 4px"></i>
              æŸ¥çœ‹è¯¦æƒ…
            </el-button>
@@ -303,15 +306,21 @@
      :close-on-click-modal="false"
    >
      <template #title>
        <div style="display: flex; align-items: center;">
          <i class="el-icon-s-data" style="margin-right: 8px; color: #409EFF;"></i>
        <div style="display: flex; align-items: center">
          <i
            class="el-icon-s-data"
            style="margin-right: 8px; color: #409eff"
          ></i>
          <span>{{ topicvalue.name }}</span>
          <span style="margin-left: 10px; color: #666; font-size: 14px;">满意度指标详情</span>
          <span style="margin-left: 10px; color: #666; font-size: 14px"
            >满意度指标详情</span
          >
        </div>
      </template>
      <topic-dialog
        v-if="topicVisible"
        :row-data="currentRow"
        :topicList="topiclist"
        :query-params="queryParams"
        @close="topicVisible = false"
      />
@@ -320,35 +329,39 @@
</template>
<script>
import { getSfStatisticsJoy, getSfStatisticsJoyInfo, selectTimelyRate } from "@/api/system/user";
import {
  getSfStatisticsJoy,
  getSfStatisticsJoyInfo,
  selectTimelyRate,
} from "@/api/system/user";
import ExcelJS from "exceljs";
import { saveAs } from "file-saver";
import SeedetailsDialog from './components/SeedetailsDialog.vue';
import TopicDialog from './components/TopicDialog.vue';
import SeedetailsDialog from "./components/SeedetailsDialog.vue";
import TopicDialog from "./components/TopicDialog.vue";
export default {
  name: 'FollowupStatistics',
  name: "FollowupStatistics",
  components: {
    SeedetailsDialog,
    TopicDialog
    TopicDialog,
  },
  data() {
    return {
      // æŸ¥è¯¢å‚æ•°
      queryParams: {
        statisticaltype: 1,
        leavehospitaldistrictcodes: [],
        leavehospitaldistrictcodes: ["all"],
        deptcodes: [],
        serviceType: [2],
        dateRange: [],
        pageNum: 1,
        pageSize: 20
        pageSize: 20,
      },
      // ç»Ÿè®¡ç±»åž‹åˆ—表
      Statisticallist: [
        { label: "病区统计", value: 1 },
        { label: "科室统计", value: 2 }
        { label: "科室统计", value: 2 },
      ],
      // ç—…区列表
@@ -384,44 +397,44 @@
      // æ»¡æ„åº¦è¯¦æƒ…数据
      topiclist: [],
      topicvalue: {
        name: ''
        name: "",
      },
      // æ—¥æœŸé€‰æ‹©å™¨é€‰é¡¹
      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();
        }
      }
        },
      },
    };
  },
@@ -442,20 +455,24 @@
      this.options = this.$store.getters.tasktypes || [];
      // èŽ·å–ç§‘å®¤åˆ—è¡¨
      this.flatArraydept = (this.$store.getters.belongDepts || []).map((dept) => {
        return {
          label: dept.deptName,
          value: dept.deptCode
        };
      });
      this.flatArraydept = (this.$store.getters.belongDepts || []).map(
        (dept) => {
          return {
            label: dept.deptName,
            value: dept.deptCode,
          };
        }
      );
      // èŽ·å–ç—…åŒºåˆ—è¡¨
      this.flatArrayhospit = (this.$store.getters.belongWards || []).map((ward) => {
        return {
          label: ward.districtName,
          value: ward.districtCode
        };
      });
      this.flatArrayhospit = (this.$store.getters.belongWards || []).map(
        (ward) => {
          return {
            label: ward.districtName,
            value: ward.districtCode,
          };
        }
      );
      // æ·»åŠ å…¨éƒ¨é€‰é¡¹
      this.flatArraydept.push({ label: "全部", value: "all" });
@@ -469,11 +486,14 @@
        // å¤„理查询参数
        const params = {
          configKey: "joyCount",
          ...this.queryParams
          ...this.queryParams,
        };
        // å¤„理日期范围
        if (this.queryParams.dateRange && this.queryParams.dateRange.length === 2) {
        if (
          this.queryParams.dateRange &&
          this.queryParams.dateRange.length === 2
        ) {
          params.startTime = this.queryParams.dateRange[0];
          params.endTime = this.queryParams.dateRange[1];
        }
@@ -483,31 +503,215 @@
          // ç—…区统计
          if (params.leavehospitaldistrictcodes.includes("all")) {
            // å¦‚果选择了"全部",则移除"all"值
            params.leavehospitaldistrictcodes = params.leavehospitaldistrictcodes.filter(item => item !== "all");
            params.leavehospitaldistrictcodes =
              params.leavehospitaldistrictcodes.filter(
                (item) => item !== "all"
              );
            // å¦‚果需要传所有病区代码,可以从store中获取
            params.leavehospitaldistrictcodes = (this.$store.getters.belongWards || []).map(ward => ward.districtCode);
            params.leavehospitaldistrictcodes = (
              this.$store.getters.belongWards || []
            ).map((ward) => ward.districtCode);
          }
        } else if (params.statisticaltype == 2) {
          // ç§‘室统计
          if (params.deptcodes.includes("all")) {
            // å¦‚果选择了"全部",则移除"all"值
            params.deptcodes = params.deptcodes.filter(item => item !== "all");
            params.deptcodes = params.deptcodes.filter(
              (item) => item !== "all"
            );
            // å¦‚果需要传所有科室代码,可以从store中获取
            params.deptcodes = (this.$store.getters.belongDepts || []).map(dept => dept.deptCode);
            params.deptcodes = (this.$store.getters.belongDepts || []).map(
              (dept) => dept.deptCode
            );
          }
        }
        const response = await getSfStatisticsJoy(params);
        this.userList = response.data || [];
        this.userList = this.customSort(response.data) || [];
        this.total = response.total || 0;
      } catch (error) {
        console.error('获取统计列表失败:', error);
        this.$message.error('获取数据失败');
        console.error("获取统计列表失败:", error);
        this.$message.error("获取数据失败");
      } finally {
        this.loading = false;
      }
    },
    sortChineseNumber(aRow, bRow) {
      const a = aRow.leavehospitaldistrictname;
      const b = bRow.leavehospitaldistrictname;
      // ä¸­æ–‡æ•°å­—到阿拉伯数字的映射(扩展到45)
      const chineseNumMap = {
        ä¸€: 1,
        äºŒ: 2,
        ä¸‰: 3,
        å››: 4,
        äº”: 5,
        å…­: 6,
        ä¸ƒ: 7,
        å…«: 8,
        ä¹: 9,
        å: 10,
        åä¸€: 11,
        åäºŒ: 12,
        åä¸‰: 13,
        åå››: 14,
        åäº”: 15,
        åå…­: 16,
        åä¸ƒ: 17,
        åå…«: 18,
        åä¹: 19,
        äºŒå: 20,
        äºŒåä¸€: 21,
        äºŒåäºŒ: 22,
        äºŒåä¸‰: 23,
        äºŒåå››: 24,
        äºŒåäº”: 25,
        äºŒåå…­: 26,
        äºŒåä¸ƒ: 27,
        äºŒåå…«: 28,
        äºŒåä¹: 29,
        ä¸‰å: 30,
        ä¸‰åä¸€: 31,
        ä¸‰åäºŒ: 32,
        ä¸‰åä¸‰: 33,
        ä¸‰åå››: 34,
        ä¸‰åäº”: 35,
        ä¸‰åå…­: 36,
        ä¸‰åä¸ƒ: 37,
        ä¸‰åå…«: 38,
        ä¸‰åä¹: 39,
        å››å: 40,
        å››åä¸€: 41,
        å››åäºŒ: 42,
        å››åä¸‰: 43,
        å››åå››: 44,
        å››åäº”: 45,
      };
      // æå–中文数字
      const getNumberFromText = (text) => {
        if (!text || typeof text !== "string") return -1;
        // åŒ¹é…ä¸­æ–‡æ•°å­—,支持一到四十五
        const match = text.match(/^([一二三四五六七八九十]+)/);
        if (match && match[1]) {
          const chineseNum = match[1];
          return chineseNumMap[chineseNum] !== undefined
            ? chineseNumMap[chineseNum]
            : -1;
        }
        // å¦‚果没有匹配到中文数字,尝试匹配阿拉伯数字
        const arabicMatch = text.match(/^(\d+)/);
        if (arabicMatch && arabicMatch[1]) {
          const num = parseInt(arabicMatch[1], 10);
          return num >= 1 && num <= 45 ? num : -1;
        }
        return -1;
      };
      const numA = getNumberFromText(a);
      const numB = getNumberFromText(b);
      // å¤„理无法解析的情况
      if (numA === -1 && numB === -1) {
        return (a || "").localeCompare(b || "");
      }
      if (numA === -1) return 1;
      if (numB === -1) return -1;
      return numA - numB;
    },
    customSort(data) {
      // å®šä¹‰æ‚¨æœŸæœ›çš„病区顺序(扩展到四十五)
      const order = [
        "一",
        "二",
        "三",
        "四",
        "五",
        "六",
        "七",
        "八",
        "九",
        "十",
        "十一",
        "十二",
        "十三",
        "十四",
        "十五",
        "十六",
        "十七",
        "十八",
        "十九",
        "二十",
        "二十一",
        "二十二",
        "二十三",
        "二十四",
        "二十五",
        "二十六",
        "二十七",
        "二十八",
        "二十九",
        "三十",
        "三十一",
        "三十二",
        "三十三",
        "三十四",
        "三十五",
        "三十六",
        "三十七",
        "三十八",
        "三十九",
        "四十",
        "四十一",
        "四十二",
        "四十三",
        "四十四",
        "四十五",
      ];
      return data.sort((a, b) => {
        // æå–病区名称中的中文数字部分
        const getIndex = (name) => {
          if (!name || typeof name !== "string") return -1;
          // åŒ¹é…ä¸­æ–‡æ•°å­—
          const chineseMatch = name.match(/^([一二三四五六七八九十]+)/);
          if (chineseMatch && chineseMatch[1]) {
            return order.indexOf(chineseMatch[1]);
          }
          // åŒ¹é…é˜¿æ‹‰ä¼¯æ•°å­—
          const arabicMatch = name.match(/^(\d+)/);
          if (arabicMatch && arabicMatch[1]) {
            const num = parseInt(arabicMatch[1], 10);
            if (num >= 1 && num <= 45) {
              return num - 1; // å› ä¸ºæ•°ç»„索引从0开始
            }
          }
          return -1;
        };
        const indexA = getIndex(a.leavehospitaldistrictname);
        const indexB = getIndex(b.leavehospitaldistrictname);
        // æŽ’序逻辑
        if (indexA === -1 && indexB === -1) {
          return (a.leavehospitaldistrictname || "").localeCompare(
            b.leavehospitaldistrictname || ""
          );
        }
        if (indexA === -1) return 1;
        if (indexB === -1) return -1;
        return indexA - indexB;
      });
    },
    // å¤„理统计类型变化
    handleStatisticalTypeChange(value) {
      if (value === 1) {
@@ -534,7 +738,7 @@
        serviceType: [2],
        dateRange: [],
        pageNum: 1,
        pageSize: 20
        pageSize: 20,
      };
      this.getList();
    },
@@ -554,7 +758,7 @@
    // å¤„理行选择
    handleSelectionChange(selection) {
      this.ids = selection.map(item => item.id);
      this.ids = selection.map((item) => item.id);
      this.single = selection.length !== 1;
      this.multiple = !selection.length;
    },
@@ -568,9 +772,9 @@
    // æ ¼å¼åŒ–百分比
    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)}%`;
    },
@@ -583,17 +787,19 @@
    // æŸ¥çœ‹æ»¡æ„åº¦è¯¦æƒ…
    async getinfo(row) {
      this.currentRow = row;
      this.topicVisible = true;
      try {
        // å¤„理查询参数
        const params = {
          configKey: "joyCount",
          ...this.queryParams
          ...this.queryParams,
        };
        // å¤„理日期范围
        if (this.queryParams.dateRange && this.queryParams.dateRange.length === 2) {
        if (
          this.queryParams.dateRange &&
          this.queryParams.dateRange.length === 2
        ) {
          params.startTime = this.queryParams.dateRange[0];
          params.endTime = this.queryParams.dateRange[1];
        }
@@ -608,16 +814,18 @@
        const response = await getSfStatisticsJoyInfo(params);
        this.topiclist = response.data || [];
      this.topicVisible = true;
      } catch (error) {
        console.error('获取满意度详情失败:', error);
        this.$message.error('获取详情失败');
        console.error("获取满意度详情失败:", error);
        this.$message.error("获取详情失败");
      }
    },
    // å¯¼å‡ºæ•°æ®
    async handleExport() {
      if (!this.userList.length) {
        this.$message.warning('没有数据可导出');
        this.$message.warning("没有数据可导出");
        return;
      }
@@ -628,7 +836,10 @@
        let dateRangeString = "";
        let sheetNameSuffix = "";
        if (this.queryParams.dateRange && this.queryParams.dateRange.length === 2) {
        if (
          this.queryParams.dateRange &&
          this.queryParams.dateRange.length === 2
        ) {
          const startDateFormatted = this.queryParams.dateRange[0];
          const endDateFormatted = this.queryParams.dateRange[1];
          dateRangeString = `${startDateFormatted}至${endDateFormatted}`;
@@ -650,26 +861,34 @@
        // å®šä¹‰æ ·å¼
        const titleStyle = {
          font: { name: "微软雅黑", size: 16, bold: true },
          fill: { type: "pattern", pattern: "solid", fgColor: { argb: "FFE6F3FF" } },
          fill: {
            type: "pattern",
            pattern: "solid",
            fgColor: { argb: "FFE6F3FF" },
          },
          alignment: { vertical: "middle", horizontal: "center" },
          border: {
            top: { style: "thin", color: { argb: "FFD0D0D0" } },
            left: { style: "thin", color: { argb: "FFD0D0D0" } },
            bottom: { style: "thin", color: { argb: "FFD0D0D0" } },
            right: { style: "thin", color: { argb: "FFD0D0D0" } }
          }
            right: { style: "thin", color: { argb: "FFD0D0D0" } },
          },
        };
        const headerStyle = {
          font: { name: "微软雅黑", size: 11, bold: true },
          fill: { type: "pattern", pattern: "solid", fgColor: { argb: "FFF5F7FA" } },
          fill: {
            type: "pattern",
            pattern: "solid",
            fgColor: { argb: "FFF5F7FA" },
          },
          alignment: { vertical: "middle", horizontal: "center" },
          border: {
            top: { style: "thin", color: { argb: "FFD0D0D0" } },
            left: { style: "thin", color: { argb: "FFD0D0D0" } },
            bottom: { style: "thin", color: { argb: "FFD0D0D0" } },
            right: { style: "thin", color: { argb: "FFD0D0D0" } }
          }
            right: { style: "thin", color: { argb: "FFD0D0D0" } },
          },
        };
        const cellStyle = {
@@ -679,8 +898,8 @@
            top: { style: "thin", color: { argb: "FFD0D0D0" } },
            left: { style: "thin", color: { argb: "FFD0D0D0" } },
            bottom: { style: "thin", color: { argb: "FFD0D0D0" } },
            right: { style: "thin", color: { argb: "FFD0D0D0" } }
          }
            right: { style: "thin", color: { argb: "FFD0D0D0" } },
          },
        };
        // æ·»åŠ æ€»æ ‡é¢˜
@@ -700,7 +919,7 @@
          "及时率",
          "满意度题目总量",
          "满意度填报量",
          "完成比率"
          "完成比率",
        ];
        const headerRow = worksheet.addRow(headers);
@@ -712,7 +931,9 @@
        // æ·»åŠ æ•°æ®è¡Œ
        this.userList.forEach((item) => {
          const dataRow = worksheet.addRow([
            this.queryParams.statisticaltype == 1 ? item.leavehospitaldistrictname : item.deptname,
            this.queryParams.statisticaltype == 1
              ? item.leavehospitaldistrictname
              : item.deptname,
            item.dischargeCount || 0,
            item.nonFollowUp || 0,
            item.followUpNeeded || 0,
@@ -720,7 +941,7 @@
            item.rate ? this.formatPercent(item.rate) : "0%",
            item.joyAllCount || 0,
            item.joyCount || 0,
            item.joyTotal ? this.formatPercent(item.joyTotal) : "0%"
            item.joyTotal ? this.formatPercent(item.joyTotal) : "0%",
          ]);
          dataRow.eachCell((cell) => {
@@ -739,13 +960,13 @@
          { width: 12 },
          { width: 15 },
          { width: 15 },
          { width: 12 }
          { width: 12 },
        ];
        // ç”Ÿæˆå¹¶ä¸‹è½½æ–‡ä»¶
        const buffer = await workbook.xlsx.writeBuffer();
        const blob = new Blob([buffer], {
          type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
          type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        });
        saveAs(blob, excelName);
@@ -756,8 +977,8 @@
      } finally {
        this.loading = false;
      }
    }
  }
    },
  },
};
</script>
src/views/Satisfaction/sfstatistics/components/SatisfactionStatistics.vue
@@ -1,293 +1,566 @@
<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: "出院" },
      ],
      // ç§‘室列表
@@ -302,6 +575,7 @@
      // åŠ è½½çŠ¶æ€
      loading: false,
      detailLoading: false,
      typeDetailLoading: false,
      // é¢˜ç›®æ˜Žç»†æ•°æ®
      questionDetailData: [],
@@ -309,7 +583,7 @@
      // é¢˜ç›®æ˜Žç»†æŸ¥è¯¢å‚æ•°
      detailQueryParams: {
        pageNum: 1,
        pageSize: 10
        pageSize: 10,
      },
      // é¢˜ç›®æ˜Žç»†æ€»æ•°
@@ -320,58 +594,67 @@
      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() {
@@ -386,21 +669,21 @@
    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);
@@ -409,16 +692,15 @@
    // èŽ·å–ç—…åŒºåˆ—è¡¨
    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);
@@ -429,37 +711,51 @@
    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;
      }
    },
    // è®¡ç®—综合得分
@@ -468,48 +764,94 @@
      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);
      });
    },
    // æ¸²æŸ“图表
@@ -518,92 +860,103 @@
      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);
@@ -611,180 +964,196 @@
    // ç”Ÿæˆ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);
      });
    },
    // å¤„理图表响应式
@@ -808,6 +1177,13 @@
      this.loadData();
    },
    // å¤„理Tab切换
    handleTabClick(tab) {
      if (tab.name === "typeDetail" && this.typeDetailData.length === 0) {
        this.loadTypeDetailData();
      }
    },
    // å¤„理明细分页大小变化
    handleDetailSizeChange(size) {
      this.detailQueryParams.pageSize = size;
@@ -821,80 +1197,131 @@
      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;
@@ -905,6 +1332,11 @@
        background-color: #f8f9fa;
        font-weight: 600;
        color: #333;
        padding: 12px 0;
      }
      td {
        padding: 12px 0;
      }
      .question-row {
@@ -930,7 +1362,7 @@
      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 {
@@ -943,27 +1375,129 @@
          .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;
          }
        }
      }
    }
  }
  // å†…层表格样式
@@ -987,4 +1521,51 @@
    }
  }
}
@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>
src/views/Satisfaction/sfstatistics/components/components/TopicDialog.vue
@@ -2,13 +2,14 @@
  <div class="topic-dialog">
    <div class="topicdia">
      <div style="overflow-x: hidden; overflow-y: auto; max-height: 65vh">
        <!-- ä¿®æ”¹è¿™é‡Œï¼šä½¿ç”¨ processedTopicList è€Œä¸æ˜¯ topicList -->
        <div
          v-for="(item, index) in topiclist"
          :key="index"
          v-for="(item, index) in processedTopicList"
          :key="item.scriptid"
          class="ttaabbcc"
        >
          <div class="describe">
            ç¬¬{{ index + 1 }}题: {{ item.scriptContent }}?
            ç¬¬{{ index + 1 }}题: {{ item.scriptContent }}
            <span>[{{ item.scriptType == 1 ? "单选题" : "多选题" }}]</span>
          </div>
          <div>
@@ -24,7 +25,11 @@
                label="选择人数"
                align="center"
                min-width="120"
              />
              >
                <template slot-scope="{ row }">
                  {{ row.chosenQuantity || 0 }}
                </template>
              </el-table-column>
              <el-table-column
                prop="chosenPercentage"
                label="比例"
@@ -32,8 +37,13 @@
                min-width="120"
              >
                <template slot-scope="{ row }">
                  <span v-if="row.chosenPercentage !== null && row.chosenPercentage !== undefined">
                    {{ formatPercent(row.chosenPercentage) }}
                  <span
                    v-if="
                      row.chosenPercentage !== null &&
                      row.chosenPercentage !== undefined
                    "
                  >
                    {{ (Number(row.chosenPercentage) * 100).toFixed(2) }}%
                  </span>
                  <span v-else>-</span>
                </template>
@@ -44,7 +54,20 @@
      </div>
    </div>
    <div slot="footer" class="dialog-footer" style="text-align: center; padding-top: 20px;">
    <!-- å¦‚果没有数据 -->
    <div
      v-if="!processedTopicList.length"
      class="no-data"
      style="text-align: center; padding: 50px 0"
    >
      <el-empty description="暂无数据"></el-empty>
    </div>
    <div
      slot="footer"
      class="dialog-footer"
      style="text-align: center; padding-top: 20px"
    >
      <el-button @click="handleClose">关 é—­</el-button>
    </div>
  </div>
@@ -52,52 +75,86 @@
<script>
export default {
  name: 'TopicDialog',
  name: "TopicDialog",
  props: {
    rowData: {
      type: Object,
      default: () => ({})
      default: () => ({}),
    },
    queryParams: {
      type: Object,
      default: () => ({})
    }
      default: () => ({}),
    },
    topicList: {
      type: [Array, Object],
      default: () => ({}),
    },
  },
  data() {
    return {
      topiclist: []
      processedTopicList: [], // å¤„理后的数据
    };
  },
  mounted() {
    this.loadData();
  watch: {
    // ç›‘听父组件传递的数据变化
    topicList: {
      immediate: true,
      handler(newVal) {
        console.log("TopicDialog接收到父组件数据:", newVal);
        this.processTopicList(newVal);
      },
    },
  },
  mounted() {
    console.log("TopicDialog mounted, props:", this.$props);
  },
  methods: {
    // åŠ è½½æ•°æ®
    async loadData() {
      try {
        // è¿™é‡Œä»Žçˆ¶ç»„件传递数据,不需要重新调用API
        this.topiclist = this.$parent.topiclist || [];
      } catch (error) {
        console.error('加载题目详情失败:', error);
        this.$message.error('加载题目详情失败');
    // å¤„理topicList数据
    processTopicList(data) {
      console.log("开始处理数据:", data);
      if (!data || typeof data !== "object") {
        this.processedTopicList = [];
        return;
      }
      // å°†å¯¹è±¡è½¬æ¢ä¸ºæ•°ç»„
      const result = [];
      Object.keys(data).forEach((key) => {
        const item = data[key];
        if (item && item.scriptContent) {
          // æ·±æ‹·è´item,避免修改原数据
          const processedItem = JSON.parse(JSON.stringify(item));
          // è¿‡æ»¤details,只保留有选项文本的
          if (processedItem.details && Array.isArray(processedItem.details)) {
            processedItem.details = processedItem.details.filter(
              (detail) => detail && detail.optionText
            );
          }
          result.push(processedItem);
        }
      });
      console.log("处理后的数据:", result);
      this.processedTopicList = result;
    },
    // æ ¼å¼åŒ–百分比
    formatPercent(value) {
      if (value === null || value === undefined) return '-';
      if (value === null || value === undefined) return "-";
      const num = parseFloat(value);
      if (isNaN(num)) return '-';
      return `${(num * 100).toFixed(2)}%`;
      if (isNaN(num)) return "-";
      return `${num.toFixed(2)}%`; // æ³¨æ„ï¼šä½ çš„æ•°æ®ä¸­ç™¾åˆ†æ¯”已经是0-100的形式
    },
    // å…³é—­å¯¹è¯æ¡†
    handleClose() {
      this.$emit('close');
    }
  }
      this.$emit("close");
    },
  },
};
</script>
src/views/Satisfaction/sfstatistics/components/visitStatistics.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1030 @@
<template>
  <div class="followup-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="statisticaltype">
          <el-select
            v-model="queryParams.statisticaltype"
            placeholder="请选择统计类型"
            clearable
            @change="handleStatisticalTypeChange"
          >
            <el-option
              v-for="item in Statisticallist"
              :key="item.value"
              :label="item.label"
              :value="item.value"
            />
          </el-select>
        </el-form-item>
        <!-- ç—…区选择 -->
        <el-form-item
          v-if="queryParams.statisticaltype == 1"
          label="病区"
          prop="leavehospitaldistrictcodes"
        >
          <el-select
            v-model="queryParams.leavehospitaldistrictcodes"
            placeholder="请选择病区"
            multiple
            collapse-tags
            filterable
            clearable
            style="width: 300px"
          >
            <el-option
              v-for="item in flatArrayhospit"
              :key="item.value"
              :label="item.label"
              :value="item.value"
            />
          </el-select>
        </el-form-item>
        <!-- ç§‘室选择 -->
        <el-form-item
          v-if="queryParams.statisticaltype == 2"
          label="科室"
          prop="deptcodes"
        >
          <el-select
            v-model="queryParams.deptcodes"
            placeholder="请选择科室"
            multiple
            collapse-tags
            filterable
            clearable
            style="width: 300px"
          >
            <el-option
              v-for="item in flatArraydept"
              :key="item.value"
              :label="item.label"
              :value="item.value"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="服务类型" prop="serviceType">
          <el-select
            v-model="queryParams.serviceType"
            placeholder="请选择服务类型"
            multiple
            collapse-tags
            clearable
            style="width: 300px"
          >
            <el-option
              v-for="item in options"
              :key="item.value"
              :label="item.label"
              :value="item.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="handleQuery"
            :loading="loading"
          >
            æœç´¢
          </el-button>
          <el-button icon="el-icon-refresh" @click="resetQuery">
            é‡ç½®
          </el-button>
          <el-button
            type="warning"
            icon="el-icon-download"
            @click="handleExport"
            :disabled="!userList.length"
          >
            å¯¼å‡º
          </el-button>
        </el-form-item>
      </el-form>
    </div>
    <div class="table-section">
      <el-table
        v-loading="loading"
        :data="userList"
        :border="true"
        style="width: 100%"
        @selection-change="handleSelectionChange"
        :row-key="getRowKey"
      >
        <!-- ç—…区列 -->
        <el-table-column
          v-if="queryParams.statisticaltype == 1"
          label="出院病区"
          align="center"
          sortable
          key="leavehospitaldistrictname"
          prop="leavehospitaldistrictname"
          :show-overflow-tooltip="true"
          :sort-method="sortChineseNumber"
          min-width="120"
        />
        <!-- ç§‘室列 -->
        <el-table-column
          v-if="queryParams.statisticaltype == 2"
          label="科室"
          align="center"
          key="deptname"
          prop="deptname"
          :show-overflow-tooltip="true"
          min-width="120"
        />
        <el-table-column
          label="出院人次"
          align="center"
          key="dischargeCount"
          prop="dischargeCount"
          min-width="100"
        />
        <el-table-column
          label="无需随访人次"
          align="center"
          key="nonFollowUp"
          prop="nonFollowUp"
          min-width="120"
        />
        <el-table-column
          label="应随访人次"
          align="center"
          key="followUpNeeded"
          prop="followUpNeeded"
          min-width="120"
        />
        <el-table-column
          label="随访率"
          align="center"
          key="followUpRate"
          prop="followUpRate"
          min-width="100"
        >
          <template slot-scope="scope">
            <span
              v-if="
                scope.row.followUpRate !== null &&
                scope.row.followUpRate !== undefined
              "
            >
              {{ formatPercent(scope.row.followUpRate) }}
            </span>
            <span v-else>-</span>
          </template>
        </el-table-column>
        <el-table-column
          label="及时率"
          align="center"
          key="rate"
          prop="rate"
          min-width="100"
        >
          <template slot-scope="scope">
            <el-button
              v-if="scope.row.rate !== null && scope.row.rate !== undefined"
              type="text"
              @click="handleSeedetails(scope.row)"
            >
              {{ formatPercent(scope.row.rate) }}
            </el-button>
            <span v-else style="color: #909399">-</span>
          </template>
        </el-table-column>
        <el-table-column
          label="复诊通知题目总量"
          align="center"
          key="joyAllCount"
          prop="joyAllCount"
          min-width="140"
        />
        <el-table-column
          label="复诊通知填报量"
          align="center"
          key="joyCount"
          prop="joyCount"
          min-width="120"
        />
        <el-table-column
          label="完成比率"
          align="center"
          key="joyTotal"
          prop="joyTotal"
          min-width="100"
        >
          <template slot-scope="scope">
            <span
              v-if="
                scope.row.joyTotal !== null && scope.row.joyTotal !== undefined
              "
            >
              {{ formatPercent(scope.row.joyTotal) }}
            </span>
            <span v-else>-</span>
          </template>
        </el-table-column>
        <el-table-column label="操作" align="center" fixed="right" width="120">
          <template slot-scope="scope">
            <el-button type="text" @click="getinfo(scope.row)">
              <i class="el-icon-s-order" style="margin-right: 4px"></i>
              æŸ¥çœ‹è¯¦æƒ…
            </el-button>
          </template>
        </el-table-column>
      </el-table>
    </div>
    <!-- åˆ†é¡µ -->
    <div class="pagination-section" v-if="total > 0">
      <el-pagination
        background
        layout="total, sizes, prev, pager, next, jumper"
        :current-page="queryParams.pageNum"
        :page-size="queryParams.pageSize"
        :page-sizes="[10, 20, 30, 50]"
        :total="total"
        @size-change="handleSizeChange"
        @current-change="handlePageChange"
      />
    </div>
    <!-- æœªåŠæ—¶éšè®¿è¯¦æƒ…对话框 -->
    <el-dialog
      title="未及时随访患者服务"
      :visible.sync="SeedetailsVisible"
      width="80%"
      :close-on-click-modal="false"
    >
      <SeedetailsDialog
        v-if="SeedetailsVisible"
        :row-data="currentRow"
        :query-params="queryParams"
        @close="SeedetailsVisible = false"
      />
    </el-dialog>
    <!-- å¤è¯Šé€šçŸ¥è¯¦æƒ…对话框 -->
    <el-dialog
      :visible.sync="topicVisible"
      width="60%"
      :close-on-click-modal="false"
    >
      <template #title>
        <div style="display: flex; align-items: center">
          <i
            class="el-icon-s-data"
            style="margin-right: 8px; color: #409eff"
          ></i>
          <span>{{ topicvalue.name }}</span>
          <span style="margin-left: 10px; color: #666; font-size: 14px"
            >复诊通知指标详情</span
          >
        </div>
      </template>
      <topic-dialog
        v-if="topicVisible"
        :row-data="currentRow"
        :topicList="topiclist"
        :query-params="queryParams"
        @close="topicVisible = false"
      />
    </el-dialog>
  </div>
</template>
<script>
import {
  getSfStatisticsJoy,
  getSfStatisticsJoyInfo,
  selectTimelyRate,
} from "@/api/system/user";
import ExcelJS from "exceljs";
import { saveAs } from "file-saver";
import SeedetailsDialog from "./components/SeedetailsDialog.vue";
import TopicDialog from "./components/TopicDialog.vue";
export default {
  name: "FollowupStatistics",
  components: {
    SeedetailsDialog,
    TopicDialog,
  },
  data() {
    return {
      // æŸ¥è¯¢å‚æ•°
      queryParams: {
        statisticaltype: 1,
        leavehospitaldistrictcodes: ["all"],
        deptcodes: [],
        serviceType: [2],
        dateRange: [],
        pageNum: 1,
        pageSize: 20,
      },
      // ç»Ÿè®¡ç±»åž‹åˆ—表
      Statisticallist: [
        { label: "病区统计", value: 1 },
        { label: "科室统计", value: 2 },
      ],
      // ç—…区列表
      flatArrayhospit: [],
      // ç§‘室列表
      flatArraydept: [],
      // æœåŠ¡ç±»åž‹é€‰é¡¹
      options: [],
      // è¡¨æ ¼æ•°æ®
      userList: [],
      // æ€»æ¡æ•°
      total: 0,
      // åŠ è½½çŠ¶æ€
      loading: false,
      // é€‰ä¸­çš„行
      ids: [],
      single: true,
      multiple: true,
      // å½“前操作的行
      currentRow: null,
      // å¯¹è¯æ¡†æ˜¾ç¤ºæŽ§åˆ¶
      SeedetailsVisible: false,
      topicVisible: false,
      // å¤è¯Šé€šçŸ¥è¯¦æƒ…数据
      topiclist: [],
      topicvalue: {
        name: "",
      },
      // æ—¥æœŸé€‰æ‹©å™¨é€‰é¡¹
      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();
        },
      },
    };
  },
  created() {
    this.initData();
  },
  methods: {
    // åˆå§‹åŒ–数据
    async initData() {
      await this.getDeptTree();
      await this.getList();
    },
    // èŽ·å–ç§‘å®¤æ ‘
    getDeptTree() {
      // èŽ·å–æœåŠ¡ç±»åž‹
      this.options = this.$store.getters.tasktypes || [];
      // èŽ·å–ç§‘å®¤åˆ—è¡¨
      this.flatArraydept = (this.$store.getters.belongDepts || []).map(
        (dept) => {
          return {
            label: dept.deptName,
            value: dept.deptCode,
          };
        }
      );
      // èŽ·å–ç—…åŒºåˆ—è¡¨
      this.flatArrayhospit = (this.$store.getters.belongWards || []).map(
        (ward) => {
          return {
            label: ward.districtName,
            value: ward.districtCode,
          };
        }
      );
      // æ·»åŠ å…¨éƒ¨é€‰é¡¹
      this.flatArraydept.push({ label: "全部", value: "all" });
      this.flatArrayhospit.push({ label: "全部", value: "all" });
    },
    // èŽ·å–ç»Ÿè®¡åˆ—è¡¨
    async getList() {
      this.loading = true;
      try {
        // å¤„理查询参数
        const params = {
          configKey: "returnVisitCount",
          ...this.queryParams,
        };
        // å¤„理日期范围
        if (
          this.queryParams.dateRange &&
          this.queryParams.dateRange.length === 2
        ) {
          params.startTime = this.queryParams.dateRange[0];
          params.endTime = this.queryParams.dateRange[1];
        }
        // å¤„理病区/科室选择
        if (params.statisticaltype == 1) {
          // ç—…区统计
          if (params.leavehospitaldistrictcodes.includes("all")) {
            // å¦‚果选择了"全部",则移除"all"值
            params.leavehospitaldistrictcodes =
              params.leavehospitaldistrictcodes.filter(
                (item) => item !== "all"
              );
            // å¦‚果需要传所有病区代码,可以从store中获取
            params.leavehospitaldistrictcodes = (
              this.$store.getters.belongWards || []
            ).map((ward) => ward.districtCode);
          }
        } else if (params.statisticaltype == 2) {
          // ç§‘室统计
          if (params.deptcodes.includes("all")) {
            // å¦‚果选择了"全部",则移除"all"值
            params.deptcodes = params.deptcodes.filter(
              (item) => item !== "all"
            );
            // å¦‚果需要传所有科室代码,可以从store中获取
            params.deptcodes = (this.$store.getters.belongDepts || []).map(
              (dept) => dept.deptCode
            );
          }
        }
        const response = await getSfStatisticsJoy(params);
        this.userList = this.customSort(response.data) || [];
        this.total = response.total || 0;
      } catch (error) {
        console.error("获取统计列表失败:", error);
        this.$message.error("获取数据失败");
      } finally {
        this.loading = false;
      }
    },
    sortChineseNumber(aRow, bRow) {
      const a = aRow.leavehospitaldistrictname;
      const b = bRow.leavehospitaldistrictname;
      // ä¸­æ–‡æ•°å­—到阿拉伯数字的映射(扩展到45)
      const chineseNumMap = {
        ä¸€: 1,
        äºŒ: 2,
        ä¸‰: 3,
        å››: 4,
        äº”: 5,
        å…­: 6,
        ä¸ƒ: 7,
        å…«: 8,
        ä¹: 9,
        å: 10,
        åä¸€: 11,
        åäºŒ: 12,
        åä¸‰: 13,
        åå››: 14,
        åäº”: 15,
        åå…­: 16,
        åä¸ƒ: 17,
        åå…«: 18,
        åä¹: 19,
        äºŒå: 20,
        äºŒåä¸€: 21,
        äºŒåäºŒ: 22,
        äºŒåä¸‰: 23,
        äºŒåå››: 24,
        äºŒåäº”: 25,
        äºŒåå…­: 26,
        äºŒåä¸ƒ: 27,
        äºŒåå…«: 28,
        äºŒåä¹: 29,
        ä¸‰å: 30,
        ä¸‰åä¸€: 31,
        ä¸‰åäºŒ: 32,
        ä¸‰åä¸‰: 33,
        ä¸‰åå››: 34,
        ä¸‰åäº”: 35,
        ä¸‰åå…­: 36,
        ä¸‰åä¸ƒ: 37,
        ä¸‰åå…«: 38,
        ä¸‰åä¹: 39,
        å››å: 40,
        å››åä¸€: 41,
        å››åäºŒ: 42,
        å››åä¸‰: 43,
        å››åå››: 44,
        å››åäº”: 45,
      };
      // æå–中文数字
      const getNumberFromText = (text) => {
        if (!text || typeof text !== "string") return -1;
        // åŒ¹é…ä¸­æ–‡æ•°å­—,支持一到四十五
        const match = text.match(/^([一二三四五六七八九十]+)/);
        if (match && match[1]) {
          const chineseNum = match[1];
          return chineseNumMap[chineseNum] !== undefined
            ? chineseNumMap[chineseNum]
            : -1;
        }
        // å¦‚果没有匹配到中文数字,尝试匹配阿拉伯数字
        const arabicMatch = text.match(/^(\d+)/);
        if (arabicMatch && arabicMatch[1]) {
          const num = parseInt(arabicMatch[1], 10);
          return num >= 1 && num <= 45 ? num : -1;
        }
        return -1;
      };
      const numA = getNumberFromText(a);
      const numB = getNumberFromText(b);
      // å¤„理无法解析的情况
      if (numA === -1 && numB === -1) {
        return (a || "").localeCompare(b || "");
      }
      if (numA === -1) return 1;
      if (numB === -1) return -1;
      return numA - numB;
    },
    customSort(data) {
      // å®šä¹‰æ‚¨æœŸæœ›çš„病区顺序(扩展到四十五)
      const order = [
        "一",
        "二",
        "三",
        "四",
        "五",
        "六",
        "七",
        "八",
        "九",
        "十",
        "十一",
        "十二",
        "十三",
        "十四",
        "十五",
        "十六",
        "十七",
        "十八",
        "十九",
        "二十",
        "二十一",
        "二十二",
        "二十三",
        "二十四",
        "二十五",
        "二十六",
        "二十七",
        "二十八",
        "二十九",
        "三十",
        "三十一",
        "三十二",
        "三十三",
        "三十四",
        "三十五",
        "三十六",
        "三十七",
        "三十八",
        "三十九",
        "四十",
        "四十一",
        "四十二",
        "四十三",
        "四十四",
        "四十五",
      ];
      return data.sort((a, b) => {
        // æå–病区名称中的中文数字部分
        const getIndex = (name) => {
          if (!name || typeof name !== "string") return -1;
          // åŒ¹é…ä¸­æ–‡æ•°å­—
          const chineseMatch = name.match(/^([一二三四五六七八九十]+)/);
          if (chineseMatch && chineseMatch[1]) {
            return order.indexOf(chineseMatch[1]);
          }
          // åŒ¹é…é˜¿æ‹‰ä¼¯æ•°å­—
          const arabicMatch = name.match(/^(\d+)/);
          if (arabicMatch && arabicMatch[1]) {
            const num = parseInt(arabicMatch[1], 10);
            if (num >= 1 && num <= 45) {
              return num - 1; // å› ä¸ºæ•°ç»„索引从0开始
            }
          }
          return -1;
        };
        const indexA = getIndex(a.leavehospitaldistrictname);
        const indexB = getIndex(b.leavehospitaldistrictname);
        // æŽ’序逻辑
        if (indexA === -1 && indexB === -1) {
          return (a.leavehospitaldistrictname || "").localeCompare(
            b.leavehospitaldistrictname || ""
          );
        }
        if (indexA === -1) return 1;
        if (indexB === -1) return -1;
        return indexA - indexB;
      });
    },
    // å¤„理统计类型变化
    handleStatisticalTypeChange(value) {
      if (value === 1) {
        this.queryParams.deptcodes = [];
      } else {
        this.queryParams.leavehospitaldistrictcodes = [];
      }
      this.queryParams.pageNum = 1;
      this.getList();
    },
    // å¤„理查询
    handleQuery() {
      this.queryParams.pageNum = 1;
      this.getList();
    },
    // é‡ç½®æŸ¥è¯¢
    resetQuery() {
      this.queryParams = {
        statisticaltype: 1,
        leavehospitaldistrictcodes: [],
        deptcodes: [],
        serviceType: [2],
        dateRange: [],
        pageNum: 1,
        pageSize: 20,
      };
      this.getList();
    },
    // å¤„理分页大小变化
    handleSizeChange(size) {
      this.queryParams.pageSize = size;
      this.queryParams.pageNum = 1;
      this.getList();
    },
    // å¤„理页码变化
    handlePageChange(page) {
      this.queryParams.pageNum = page;
      this.getList();
    },
    // å¤„理行选择
    handleSelectionChange(selection) {
      this.ids = selection.map((item) => item.id);
      this.single = selection.length !== 1;
      this.multiple = !selection.length;
    },
    // èŽ·å–è¡Œkey
    getRowKey(row) {
      return row.statisticaltype === 1
        ? row.leavehospitaldistrictcode
        : row.deptcode;
    },
    // æ ¼å¼åŒ–百分比
    formatPercent(value) {
      if (value === null || value === undefined) return "-";
      const num = parseFloat(value);
      if (isNaN(num)) return "-";
      return `${(num * 100).toFixed(2)}%`;
    },
    // æŸ¥çœ‹æœªåŠæ—¶éšè®¿è¯¦æƒ…
    handleSeedetails(row) {
      this.currentRow = row;
      this.SeedetailsVisible = true;
    },
    // æŸ¥çœ‹å¤è¯Šé€šçŸ¥è¯¦æƒ…
    async getinfo(row) {
      this.currentRow = row;
      try {
        // å¤„理查询参数
        const params = {
          configKey: "returnVisitCount",
          ...this.queryParams,
        };
        // å¤„理日期范围
        if (
          this.queryParams.dateRange &&
          this.queryParams.dateRange.length === 2
        ) {
          params.startTime = this.queryParams.dateRange[0];
          params.endTime = this.queryParams.dateRange[1];
        }
        if (this.queryParams.statisticaltype == 1) {
          this.topicvalue.name = row.leavehospitaldistrictname;
          params.leavehospitaldistrictcodes = [row.leavehospitaldistrictcode];
        } else {
          this.topicvalue.name = row.deptname;
          params.deptcodes = [row.deptcode];
        }
        const response = await getSfStatisticsJoyInfo(params);
        this.topiclist = response.data || [];
        console.log(this.topiclist);
        this.topicVisible = true;
      } catch (error) {
        console.error("获取复诊通知详情失败:", error);
        this.$message.error("获取详情失败");
      }
    },
    // å¯¼å‡ºæ•°æ®
    async handleExport() {
      if (!this.userList.length) {
        this.$message.warning("没有数据可导出");
        return;
      }
      try {
        this.loading = true;
        // æž„建日期范围字符串
        let dateRangeString = "";
        let sheetNameSuffix = "";
        if (
          this.queryParams.dateRange &&
          this.queryParams.dateRange.length === 2
        ) {
          const startDateFormatted = this.queryParams.dateRange[0];
          const endDateFormatted = this.queryParams.dateRange[1];
          dateRangeString = `${startDateFormatted}至${endDateFormatted}`;
          sheetNameSuffix = `${startDateFormatted}至${endDateFormatted}`;
        } else {
          const now = new Date();
          const currentMonth = now.getMonth() + 1;
          dateRangeString = `${currentMonth}月`;
          sheetNameSuffix = `${currentMonth}月`;
        }
        const excelName = `随访统计表_${dateRangeString}.xlsx`;
        const worksheetName = `随访统计_${sheetNameSuffix}`;
        // åˆ›å»ºExcel工作簿
        const workbook = new ExcelJS.Workbook();
        const worksheet = workbook.addWorksheet(worksheetName);
        // å®šä¹‰æ ·å¼
        const titleStyle = {
          font: { name: "微软雅黑", size: 16, bold: true },
          fill: {
            type: "pattern",
            pattern: "solid",
            fgColor: { argb: "FFE6F3FF" },
          },
          alignment: { vertical: "middle", horizontal: "center" },
          border: {
            top: { style: "thin", color: { argb: "FFD0D0D0" } },
            left: { style: "thin", color: { argb: "FFD0D0D0" } },
            bottom: { style: "thin", color: { argb: "FFD0D0D0" } },
            right: { style: "thin", color: { argb: "FFD0D0D0" } },
          },
        };
        const headerStyle = {
          font: { name: "微软雅黑", size: 11, bold: true },
          fill: {
            type: "pattern",
            pattern: "solid",
            fgColor: { argb: "FFF5F7FA" },
          },
          alignment: { vertical: "middle", horizontal: "center" },
          border: {
            top: { style: "thin", color: { argb: "FFD0D0D0" } },
            left: { style: "thin", color: { argb: "FFD0D0D0" } },
            bottom: { style: "thin", color: { argb: "FFD0D0D0" } },
            right: { style: "thin", color: { argb: "FFD0D0D0" } },
          },
        };
        const cellStyle = {
          font: { name: "宋体", size: 10 },
          alignment: { vertical: "middle", horizontal: "center" },
          border: {
            top: { style: "thin", color: { argb: "FFD0D0D0" } },
            left: { style: "thin", color: { argb: "FFD0D0D0" } },
            bottom: { style: "thin", color: { argb: "FFD0D0D0" } },
            right: { style: "thin", color: { argb: "FFD0D0D0" } },
          },
        };
        // æ·»åŠ æ€»æ ‡é¢˜
        worksheet.mergeCells(1, 1, 1, 10);
        const titleCell = worksheet.getCell(1, 1);
        titleCell.value = `随访统计表(${sheetNameSuffix})`;
        titleCell.style = titleStyle;
        worksheet.getRow(1).height = 35;
        // æ·»åŠ è¡¨å¤´
        const headers = [
          this.queryParams.statisticaltype == 1 ? "出院病区" : "科室",
          "出院人次",
          "无需随访人次",
          "应随访人次",
          "随访率",
          "及时率",
          "复诊通知题目总量",
          "复诊通知填报量",
          "完成比率",
        ];
        const headerRow = worksheet.addRow(headers);
        headerRow.eachCell((cell) => {
          cell.style = headerStyle;
        });
        headerRow.height = 25;
        // æ·»åŠ æ•°æ®è¡Œ
        this.userList.forEach((item) => {
          const dataRow = worksheet.addRow([
            this.queryParams.statisticaltype == 1
              ? item.leavehospitaldistrictname
              : item.deptname,
            item.dischargeCount || 0,
            item.nonFollowUp || 0,
            item.followUpNeeded || 0,
            item.followUpRate || "0%",
            item.rate ? this.formatPercent(item.rate) : "0%",
            item.joyAllCount || 0,
            item.joyCount || 0,
            item.joyTotal ? this.formatPercent(item.joyTotal) : "0%",
          ]);
          dataRow.eachCell((cell) => {
            cell.style = cellStyle;
          });
          dataRow.height = 22;
        });
        // è®¾ç½®åˆ—宽
        worksheet.columns = [
          { width: 20 },
          { width: 12 },
          { width: 12 },
          { width: 12 },
          { width: 12 },
          { width: 12 },
          { width: 15 },
          { width: 15 },
          { width: 12 },
        ];
        // ç”Ÿæˆå¹¶ä¸‹è½½æ–‡ä»¶
        const buffer = await workbook.xlsx.writeBuffer();
        const blob = new Blob([buffer], {
          type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        });
        saveAs(blob, excelName);
        this.$message.success("导出成功");
      } catch (error) {
        console.error("导出失败:", error);
        this.$message.error(`导出失败: ${error.message}`);
      } finally {
        this.loading = false;
      }
    },
  },
};
</script>
<style lang="scss" scoped>
.followup-statistics {
  .query-section {
    background: #fff;
    padding: 20px;
    border-radius: 4px;
    margin-bottom: 20px;
    .query-form {
      display: flex;
      flex-wrap: wrap;
      ::v-deep .el-form-item {
        margin-bottom: 20px;
        &:not(:last-child) {
          margin-right: 20px;
        }
      }
    }
  }
  .table-section {
    background: #fff;
    padding: 20px;
    border-radius: 4px;
    margin-bottom: 20px;
    ::v-deep .el-table {
      th {
        background-color: #f8f9fa;
        font-weight: 600;
        color: #333;
      }
    }
  }
  .pagination-section {
    display: flex;
    justify-content: flex-end;
    background: #fff;
    padding: 20px;
    border-radius: 4px;
  }
}
</style>
src/views/followvisit/Continue/index.vue
@@ -1875,7 +1875,7 @@
          this.zcform.remark =
            this.zcform.remark + "【" + this.getCurrentTime() + "】";
          let form = structuredClone(this.zcform);
          form.longSendTime = this.formatTime(form.date1);
          form.visitTime = this.formatTime(form.date1);
          form.finishtime = "";
          if (form.resource) {
            if (form.resource == 2) {
src/views/followvisit/OutpatientAgain/index.vue
@@ -1654,7 +1654,7 @@
          this.zcform.remark =
            this.zcform.remark + "【" + this.getCurrentTime() + "】";
          let form = structuredClone(this.zcform);
          form.longSendTime = this.formatTime(form.date1);
          form.visitTime = this.formatTime(form.date1);
          form.finishtime = "";
          if (form.resource) {
            if (form.resource == 2) {
src/views/followvisit/Tracking/index.vue
@@ -1593,7 +1593,7 @@
          this.zcform.remark =
            this.zcform.remark + "【" + this.getCurrentTime() + "】";
          let form = structuredClone(this.zcform);
          form.longSendTime = this.formatTime(form.date1);
          form.visitTime = this.formatTime(form.date1);
          form.finishtime = "";
          if (form.resource) {
            if (form.resource == 2) {
src/views/followvisit/again/index.vue
@@ -1655,7 +1655,7 @@
          this.zcform.remark =
            this.zcform.remark + "【" + this.getCurrentTime() + "】";
          let form = structuredClone(this.zcform);
          form.longSendTime = this.formatTime(form.date1);
          form.visitTime = this.formatTime(form.date1);
          form.finishtime = "";
          if (form.resource) {
            if (form.resource == 2) {
src/views/followvisit/discharge/index.vue
@@ -2062,7 +2062,7 @@
          this.zcform.remark =
            this.zcform.remark + "【" + this.getCurrentTime() + "】";
          let form = structuredClone(this.zcform);
          form.longSendTime = this.formatTime(form.date1);
          form.visitTime = this.formatTime(form.date1);
          form.finishtime = "";
          if (form.resource) {
            if (form.resource == 2) {
src/views/followvisit/discharge/outpatientService.vue
@@ -1626,7 +1626,7 @@
          this.zcform.remark =
            this.zcform.remark + "【" + this.getCurrentTime() + "】";
          let form = structuredClone(this.zcform);
          form.longSendTime = this.formatTime(form.date1);
          form.visitTime = this.formatTime(form.date1);
          form.finishtime = "";
          if (form.resource) {
            if (form.resource == 2) {
src/views/followvisit/record/TracingInfo/index.vue
@@ -1875,7 +1875,7 @@
    // å†æ¬¡éšè®¿æ•°æ®æ›´æ›¿
    formtidy() {
      this.form.visitType2 = this.form.visitType;
      this.form.date2 = this.form.longSendTime;
      this.form.date2 = this.form.visitTime;
      // this.form.date1 = this.setCurrentDate();
      this.form.remark2 = this.form.remark;
    },
@@ -1909,7 +1909,7 @@
          this.logsheetlist = res.rows[0].serviceSubtaskList;
          this.templateid = this.form.templateid;
          this.selectedTag = this.form.excep;
          const targetDate = new Date(this.form.longSendTime); // ç›®æ ‡æ—¥æœŸ
          const targetDate = new Date(this.form.visitTime); // ç›®æ ‡æ—¥æœŸ
          const now = new Date(); // å½“前时间
          if (now < targetDate && this.form.sendstate == 2) {
            this.$confirm("当前服务未到发送时间请谨慎修改", "提示", {
@@ -2245,7 +2245,7 @@
          this.form.remark =
            this.form.remark + "【" + this.getCurrentTime() + "】";
          let form = structuredClone(this.form);
          form.longSendTime = this.formatTime(form.date1);
          form.visitTime = this.formatTime(form.date1);
          form.finishtime = "";
          if (form.resource) {
            if (form.resource == 2) {
src/views/followvisit/record/detailpage/index.vue
@@ -2047,7 +2047,7 @@
    // å†æ¬¡éšè®¿æ•°æ®æ›´æ›¿
    formtidy() {
      this.form.visitType2 = this.form.visitType;
      this.form.date2 = this.form.longSendTime;
      this.form.date2 = this.form.visitTime;
      // this.form.date1 = this.setCurrentDate();
      this.form.remark2 = this.form.remark;
    },
@@ -2081,7 +2081,7 @@
          this.logsheetlist = res.rows[0].serviceSubtaskList;
          this.templateid = this.form.templateid;
          this.selectedTag = this.form.excep;
          const targetDate = new Date(this.form.longSendTime); // ç›®æ ‡æ—¥æœŸ
          const targetDate = new Date(this.form.visitTime); // ç›®æ ‡æ—¥æœŸ
          const now = new Date(); // å½“前时间
          if (now < targetDate && this.form.sendstate == 2) {
            this.$confirm("当前服务未到发送时间请谨慎修改", "提示", {
@@ -2450,7 +2450,7 @@
          this.form.remark =
            this.form.remark + "【" + this.getCurrentTime() + "】";
          let form = structuredClone(this.form);
          form.longSendTime = this.formatTime(form.date1);
          form.visitTime = this.formatTime(form.date1);
          form.finishtime = "";
          if (form.resource) {
            if (form.resource == 2) {
src/views/followvisit/record/physical/index.vue
@@ -1000,7 +1000,7 @@
    // å†æ¬¡éšè®¿æ•°æ®æ›´æ›¿
    formtidy() {
      this.form.visitType2 = this.form.visitType;
      this.form.date2 = this.form.longSendTime;
      this.form.date2 = this.form.visitTime;
      this.form.remark2 = this.form.remark;
    },
    // èŽ·å–æ‚£è€…è®°å½•
@@ -1024,7 +1024,7 @@
          }
          this.logsheetlist = res.rows[0].serviceSubtaskList;
          this.templateid = this.logsheetlist[0].templateid;
          const targetDate = new Date(this.form.longSendTime); // ç›®æ ‡æ—¥æœŸ
          const targetDate = new Date(this.form.visitTime); // ç›®æ ‡æ—¥æœŸ
          const now = new Date(); // å½“前时间
          this.form.endtime = this.formatTime(this.form.endtime);
          if (now < targetDate && this.form.sendstate == 2) {
@@ -1169,7 +1169,7 @@
          this.form.remark =
            this.form.remark + "【" + this.getCurrentTime() + "】";
          let form = structuredClone(this.form);
          form.longSendTime = this.formatTime(form.date1);
          form.visitTime = this.formatTime(form.date1);
          form.finishtime = "";
          if (form.resource) {
            if (form.resource == 2) {
src/views/followvisit/zbAgain/index.vue
@@ -1643,7 +1643,7 @@
          this.zcform.remark =
            this.zcform.remark + "【" + this.getCurrentTime() + "】";
          let form = structuredClone(this.zcform);
          form.longSendTime = this.formatTime(form.date1);
          form.visitTime = this.formatTime(form.date1);
          form.finishtime = "";
          if (form.resource) {
            if (form.resource == 2) {
src/views/patient/propaganda/QuestionnaireTask.vue
@@ -296,12 +296,11 @@
                  <el-col :span="20"
                    ><el-form-item label="适用手术" prop="region">
                      <el-select
                        v-model="operationcodes"
                        v-model="form.oplevelcode"
                        style="width: 400px"
                        @remove-tag="removeopera"
                        size="medium"
                        :remote-method="remoteopcode"
                        multiple
                        filterable
                        remote
                        placeholder="请选择手术"
@@ -309,8 +308,8 @@
                        <el-option
                          class="ruleFormaa"
                          v-for="item in baseoperaList"
                          :label="item.opdesc"
                          :value="item.opcode"
                          :label="item.label"
                          :value="item.value"
                        >
                        </el-option>
                      </el-select> </el-form-item
@@ -943,14 +942,18 @@
      dialogVisiblepatientjb: false, //添加疾病弹框
      deptcodesWards: [], //科室数据
      leavehospitaldistrictcodes: [], //病区数据
      operationcodes: [], //手术数据
      illnesscodes: [], //疾病数据
      radio: 1,
      checkboxlist: [],
      tableLabel: [],
      questionList: [],
      donorchargeList: [],
      baseoperaList: [],
      baseoperaList: [
        { value: "1", label: "一级手术" },
        { value: "2", label: "二级手术" },
        { value: "3", label: "三级手术" },
        { value: "4", label: "四级手术" },
      ],
      usable: [
        { value: "0", label: "可用" },
        { value: "1", label: "停用" },
@@ -1431,16 +1434,16 @@
        ];
        if (this.form.appltype == 1) {
          this.leavehospitaldistrictcodes = [];
          this.operationcodes = [];
          this.form.oplevelcode = null;
          this.illnesscodes = [];
        } else if (this.form.appltype == 2) {
          this.deptcodesWards = [];
          this.operationcodes = [];
          this.form.oplevelcode = null;
          this.illnesscodes = [];
        } else if (this.form.appltype == 3) {
          this.deptcodesWards = [];
          this.leavehospitaldistrictcodes = [];
          this.operationcodes = [];
          this.form.oplevelcode = null;
        } else if (this.form.appltype == 4) {
          this.deptcodesWards = [];
          this.illnesscodes = [];
@@ -1460,7 +1463,7 @@
          this.deptcodesWards[0] ||
          this.leavehospitaldistrictcodes[0] ||
          this.diagglist[0] ||
          this.operationcodes[0] ||
          this.form.oplevelcode ||
          this.form.longTask == 2 ||
          this.serviceType == 3
        ) {
@@ -1511,7 +1514,7 @@
        this.form.deptcode = this.deptcodesWards.join(",");
        this.form.leavehospitaldistrictcode =
          this.leavehospitaldistrictcodes.join(",");
        this.form.opcode = this.operationcodes.join(",");
        // this.form.opcode = this.operationcodes.join(",");
        this.form.icd10code = this.diagglist
          .map((item) => item.icdcode)
          .join(",");
@@ -1636,36 +1639,36 @@
      }).then((res) => {
        this.donorchargeList = res.rows;
      });
      getbaseopera({
        pageNum: 1,
        pageSize: 1000,
      }).then((res) => {
        this.baseoperaList = res.rows;
      });
      // getbaseopera({
      //   pageNum: 1,
      //   pageSize: 1000,
      // }).then((res) => {
      //   this.baseoperaList = res.rows;
      // });
    },
    // æ‰‹æœ¯æŸ¥è¯¢
    remoteopcode(name) {
      if (name) {
        getbaseopera({
          pageNum: 1,
          pageSize: 1000,
          opdesc: name,
        }).then((res) => {
          this.baseoperaList = res.rows;
        });
      }
      // if (name) {
      //   getbaseopera({
      //     pageNum: 1,
      //     pageSize: 1000,
      //     opdesc: name,
      //   }).then((res) => {
      //     this.baseoperaList = res.rows;
      //   });
      // }
    },
    // ç–¾ç—…查询
    remotedonor(name) {
      if (name) {
        getbaseopera({
          pageNum: 1,
          pageSize: 1000,
          opdesc: name,
        }).then((res) => {
          this.baseoperaList = res.rows;
        });
      }
      // if (name) {
      //   getbaseopera({
      //     pageNum: 1,
      //     pageSize: 1000,
      //     opdesc: name,
      //   }).then((res) => {
      //     this.baseoperaList = res.rows;
      //   });
      // }
    },
    // å¤„理问题层变量
    Variablehandling(arr, type) {
src/views/patient/propaganda/index.vue
@@ -259,11 +259,11 @@
          label="应宣教日期"
          width="200"
          align="center"
          key="longSendTime"
          prop="longSendTime"
          key="visitTime"
          prop="visitTime"
        >
          <template slot-scope="scope">
            <span>{{ formatTime(scope.row.longSendTime) }}</span>
            <span>{{ formatTime(scope.row.visitTime) }}</span>
          </template></el-table-column
        >
        <el-table-column
src/views/sfstatistics/percentage/index.vue
@@ -1511,43 +1511,97 @@
      getSfStatistics(params).then((response) => {
        this.loading = false;
        // this.total = response.total;
        this.total = response.total;
        // this.userList = response.data;
        this.userList = this.customSort(response.data);
      });
    },
    sortChineseNumber(a, b) {
      // æå–中文数字
      const chineseNumbers = [
        "一",
        "二",
        "三",
        "四",
        "五",
        "六",
        "七",
        "八",
        "九",
        "十",
        "十一",
        "十二",
      ];
    sortChineseNumber(aRow, bRow) {
      const a = aRow.leavehospitaldistrictname;
      const b = bRow.leavehospitaldistrictname;
      // ä»Žå­—符串中提取病区数字,如"四病区" -> "四"
      // ä¸­æ–‡æ•°å­—到阿拉伯数字的映射(扩展到45)
      const chineseNumMap = {
        ä¸€: 1,
        äºŒ: 2,
        ä¸‰: 3,
        å››: 4,
        äº”: 5,
        å…­: 6,
        ä¸ƒ: 7,
        å…«: 8,
        ä¹: 9,
        å: 10,
        åä¸€: 11,
        åäºŒ: 12,
        åä¸‰: 13,
        åå››: 14,
        åäº”: 15,
        åå…­: 16,
        åä¸ƒ: 17,
        åå…«: 18,
        åä¹: 19,
        äºŒå: 20,
        äºŒåä¸€: 21,
        äºŒåäºŒ: 22,
        äºŒåä¸‰: 23,
        äºŒåå››: 24,
        äºŒåäº”: 25,
        äºŒåå…­: 26,
        äºŒåä¸ƒ: 27,
        äºŒåå…«: 28,
        äºŒåä¹: 29,
        ä¸‰å: 30,
        ä¸‰åä¸€: 31,
        ä¸‰åäºŒ: 32,
        ä¸‰åä¸‰: 33,
        ä¸‰åå››: 34,
        ä¸‰åäº”: 35,
        ä¸‰åå…­: 36,
        ä¸‰åä¸ƒ: 37,
        ä¸‰åå…«: 38,
        ä¸‰åä¹: 39,
        å››å: 40,
        å››åä¸€: 41,
        å››åäºŒ: 42,
        å››åä¸‰: 43,
        å››åå››: 44,
        å››åäº”: 45,
      };
      // æå–中文数字
      const getNumberFromText = (text) => {
        if (!text) return -1;
        if (!text || typeof text !== "string") return -1;
        // åŒ¹é…ä¸­æ–‡æ•°å­—,支持一到四十五
        const match = text.match(/^([一二三四五六七八九十]+)/);
        if (match && match[1]) {
          return chineseNumbers.indexOf(match[1]);
          const chineseNum = match[1];
          return chineseNumMap[chineseNum] !== undefined
            ? chineseNumMap[chineseNum]
            : -1;
        }
        // å¦‚果没有匹配到中文数字,尝试匹配阿拉伯数字
        const arabicMatch = text.match(/^(\d+)/);
        if (arabicMatch && arabicMatch[1]) {
          const num = parseInt(arabicMatch[1], 10);
          return num >= 1 && num <= 45 ? num : -1;
        }
        return -1;
      };
      const numA = getNumberFromText(a);
      const numB = getNumberFromText(b);
      if (numA === -1 && numB === -1) return 0;
      if (numA === -1) return 1; // æ— æ³•解析的放到后面
      if (numB === -1) return -1; // æ— æ³•解析的放到后面
      // å¤„理无法解析的情况
      if (numA === -1 && numB === -1) {
        return (a || "").localeCompare(b || "");
      }
      if (numA === -1) return 1;
      if (numB === -1) return -1;
      return numA - numB;
    },
@@ -1565,8 +1619,9 @@
      }
    },
    customSort(data) {
      // å®šä¹‰æ‚¨æœŸæœ›çš„病区顺序(扩展到三十)
      // å®šä¹‰æ‚¨æœŸæœ›çš„病区顺序(扩展到四十五)
      const order = [
        "一",
        "二",
        "三",
        "四",
@@ -1596,21 +1651,55 @@
        "二十八",
        "二十九",
        "三十",
        "三十一",
        "三十二",
        "三十三",
        "三十四",
        "三十五",
        "三十六",
        "三十七",
        "三十八",
        "三十九",
        "四十",
        "四十一",
        "四十二",
        "四十三",
        "四十四",
        "四十五",
      ];
      return data.sort((a, b) => {
        // æå–病区名称中的中文数字部分[6](@ref)
        // æå–病区名称中的中文数字部分
        const getIndex = (name) => {
          const numStr = name.match(
            /^(二|三|四|五|六|七|八|九|十|十一|十二|十三|十四|十五|十六|十七|十八|十九|二十|二十一|二十二|二十三|二十四|二十五|二十六|二十七|二十八|二十九|三十)/
          )?.[1];
          return order.indexOf(numStr);
          if (!name || typeof name !== "string") return -1;
          // åŒ¹é…ä¸­æ–‡æ•°å­—
          const chineseMatch = name.match(/^([一二三四五六七八九十]+)/);
          if (chineseMatch && chineseMatch[1]) {
            return order.indexOf(chineseMatch[1]);
          }
          // åŒ¹é…é˜¿æ‹‰ä¼¯æ•°å­—
          const arabicMatch = name.match(/^(\d+)/);
          if (arabicMatch && arabicMatch[1]) {
            const num = parseInt(arabicMatch[1], 10);
            if (num >= 1 && num <= 45) {
              return num - 1; // å› ä¸ºæ•°ç»„索引从0开始
            }
          }
          return -1;
        };
        const indexA = getIndex(a.leavehospitaldistrictname);
        const indexB = getIndex(b.leavehospitaldistrictname);
        // å¦‚果都在定义的顺序中,按定义顺序排;否则,未定义的排在后面[2](@ref)
        // æŽ’序逻辑
        if (indexA === -1 && indexB === -1) {
          return (a.leavehospitaldistrictname || "").localeCompare(
            b.leavehospitaldistrictname || ""
          );
        }
        if (indexA === -1) return 1;
        if (indexB === -1) return -1;
        return indexA - indexB;
Ö¸±êͳ¼ÆÕûºÏÒ³ÊÊÅäÀöˮʡÁ¢Í¬µÂ.zip
Binary files differ