WXL (wul)
5 天以前 b4fe2c16274a91fee4fb26d91374f95fbe2dbcec
src/views/Satisfaction/sfstatistics/components/visitStatistics.vue
@@ -383,6 +383,7 @@
      // 加载状态
      loading: false,
      orgname: "",
      // 选中的行
      ids: [],
@@ -441,6 +442,7 @@
  },
  created() {
    this.orgname = localStorage.getItem("orgname");
    this.initData();
  },
@@ -824,6 +826,113 @@
      }
    },
    // 获取指定病区/科室的题目详情
    async fetchTopicDetail(row) {
      const params = {
        configKey: "returnVisitCount",
        ...this.queryParams,
      };
      // 导出需要该病区/科室的全部题目,覆盖主表分页参数
      params.pageNum = 1;
      params.pageSize = 1000;
      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) {
        params.leavehospitaldistrictcodes = [row.leavehospitaldistrictcode];
      } else {
        params.deptcodes = [row.deptcode];
      }
      const response = await getSfStatisticsJoyInfo(params);
      return response.data || {};
    },
    // 主表导出行数据
    getMainExportRow(item) {
      return [
        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%",
      ];
    },
    // 计算文本显示宽度(中文按 2、英文/数字按 1)
    textWidth(str) {
      if (!str) return 0;
      let width = 0;
      for (const ch of String(str)) {
        width += ch.charCodeAt(0) > 255 ? 2 : 1;
      }
      return width;
    },
    // 丽水市中医院:拉取所有科室题目明细,扁平化为「题目选项」列结构
    async buildLishuiTopicColumns() {
      const topicMap = new Map();
      const rowQuantities = [];
      for (const item of this.userList) {
        const topiclist = await this.fetchTopicDetail(item);
        const qMap = new Map();
        if (topiclist && Object.keys(topiclist).length) {
          Object.keys(topiclist).forEach((key) => {
            const topic = topiclist[key];
            if (!topic || !topic.scriptContent) return;
            const tKey =
              topic.scriptid != null && topic.scriptid !== ""
                ? String(topic.scriptid)
                : topic.scriptContent;
            if (!topicMap.has(tKey)) {
              topicMap.set(tKey, {
                key: tKey,
                scriptContent: topic.scriptContent,
                options: [],
                optionSet: new Set(),
              });
            }
            const t = topicMap.get(tKey);
            const topicQ = new Map();
            (topic.details || []).forEach((detail) => {
              if (!detail || !detail.optionText) return;
              if (!t.optionSet.has(detail.optionText)) {
                t.optionSet.add(detail.optionText);
                t.options.push(detail.optionText);
              }
              topicQ.set(detail.optionText, Number(detail.chosenQuantity) || 0);
            });
            qMap.set(tKey, topicQ);
          });
        }
        rowQuantities.push(qMap);
      }
      const topics = Array.from(topicMap.values());
      // 没有选项的题目默认填充一个空选项列,避免表头/数据错位
      topics.forEach((t) => {
        if (!t.options.length) {
          t.options = [""];
        }
      });
      return { topics, rowQuantities };
    },
    // 导出数据
    async handleExport() {
      if (!this.userList.length) {
@@ -916,7 +1025,11 @@
            pattern: "solid",
            fgColor: { argb: "FFF5F7FA" },
          },
          alignment: { vertical: "middle", horizontal: "center" },
          alignment: {
            vertical: "middle",
            horizontal: "center",
            wrapText: true,
          },
          border: {
            top: { style: "thin", color: { argb: "FFD0D0D0" } },
            left: { style: "thin", color: { argb: "FFD0D0D0" } },
@@ -936,15 +1049,17 @@
          },
        };
        // 添加总标题
        worksheet.mergeCells(1, 1, 1, 10);
        const titleCell = worksheet.getCell(1, 1);
        titleCell.value = `复诊通知统计表(${sheetNameSuffix})`;
        titleCell.style = titleStyle;
        worksheet.getRow(1).height = 35;
        // 丽水市中医院:预先拉取题目明细,扁平化为「题目选项」列
        let lishuiTopics = [];
        let lishuiRowQuantities = [];
        if (isLishuiHospital) {
          const built = await this.buildLishuiTopicColumns();
          lishuiTopics = built.topics;
          lishuiRowQuantities = built.rowQuantities;
        }
        // 添加表头
        const headers = [
        // 主表列
        const mainHeaders = [
          this.queryParams.statisticaltype == 1 ? "出院病区" : "科室",
          "出院人次",
          "无需随访人次",
@@ -955,47 +1070,161 @@
          "复诊通知填报量",
          "完成比率",
        ];
        const mainColumnCount = mainHeaders.length;
        const headerRow = worksheet.addRow(headers);
        headerRow.eachCell((cell) => {
          cell.style = headerStyle;
        });
        headerRow.height = 25;
        // 总列数(丽水市中医院在末尾追加题目选项列)
        const topicColumnCount = lishuiTopics.reduce(
          (sum, t) => sum + (t.options.length || 1),
          0
        );
        const totalColumns = mainColumnCount + topicColumnCount;
        // 添加数据行
        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%",
          ]);
        // 添加总标题
        worksheet.mergeCells(1, 1, 1, totalColumns);
        const titleCell = worksheet.getCell(1, 1);
        titleCell.value = `复诊通知统计表(${sheetNameSuffix})`;
        titleCell.style = titleStyle;
        worksheet.getRow(1).height = 35;
          dataRow.eachCell((cell) => {
            cell.style = cellStyle;
        if (isLishuiHospital && lishuiTopics.length) {
          // 计算每个题目的选项列宽(选项文字自适应,题干过长时摊宽列、必要时换行)
          const topicColumns = lishuiTopics.map((topic) => {
            const n = topic.options.length || 1;
            const baseWidths = topic.options.map((opt) =>
              opt === "" ? 10 : Math.max(10, this.textWidth(opt) + 4)
            );
            const sumWidth = baseWidths.reduce((s, w) => s + w, 0);
            const topicWidth = this.textWidth(topic.scriptContent) + 4;
            const groupWidth = Math.max(sumWidth, topicWidth);
            const extra = (groupWidth - sumWidth) / n;
            const widths = baseWidths.map((w) => w + extra);
            const lines = Math.max(1, Math.ceil(topicWidth / groupWidth));
            return { widths, lines };
          });
          dataRow.height = 22;
        });
          const maxHeaderLines = topicColumns.reduce(
            (m, t) => Math.max(m, t.lines),
            1
          );
        // 设置列宽
        worksheet.columns = [
          { width: 20 },
          { width: 12 },
          { width: 12 },
          { width: 12 },
          { width: 12 },
          { width: 12 },
          { width: 15 },
          { width: 15 },
          { width: 12 },
        ];
          // 两行表头:第一行 = 主表列 + 题目名;第二行 = 选项名
          const headerRow1 = worksheet.addRow([]);
          const headerRow2 = worksheet.addRow([]);
          mainHeaders.forEach((h, i) => {
            const col = i + 1;
            headerRow1.getCell(col).value = h;
            headerRow1.getCell(col).style = headerStyle;
            headerRow2.getCell(col).style = headerStyle;
            worksheet.mergeCells(
              headerRow1.number,
              col,
              headerRow2.number,
              col
            );
          });
          let colIndex = mainColumnCount + 1;
          lishuiTopics.forEach((topic) => {
            const optCount = topic.options.length || 1;
            for (let k = 0; k < optCount; k++) {
              headerRow1.getCell(colIndex + k).style = headerStyle;
            }
            headerRow1.getCell(colIndex).value = topic.scriptContent;
            if (optCount > 1) {
              worksheet.mergeCells(
                headerRow1.number,
                colIndex,
                headerRow1.number,
                colIndex + optCount - 1
              );
            }
            topic.options.forEach((opt, k) => {
              headerRow2.getCell(colIndex + k).value = opt;
              headerRow2.getCell(colIndex + k).style = headerStyle;
            });
            colIndex += optCount;
          });
          // 表头高度:题干换行时按最长题目预留
          headerRow1.height = Math.max(30, 15 * maxHeaderLines + 8);
          headerRow2.height = 25;
          // 数据行
          this.userList.forEach((item, idx) => {
            const row = worksheet.addRow([]);
            const main = this.getMainExportRow(item);
            main.forEach((v, i) => {
              const cell = row.getCell(i + 1);
              cell.value = v;
              cell.style = cellStyle;
            });
            const qMap = lishuiRowQuantities[idx] || new Map();
            let cIndex = mainColumnCount + 1;
            for (const topic of lishuiTopics) {
              const topicQ = qMap.get(topic.key);
              topic.options.forEach((opt) => {
                const cell = row.getCell(cIndex);
                if (opt === "") {
                  // 空选项占位列不填数字
                  cell.value = "";
                } else {
                  const val = topicQ ? topicQ.get(opt) : undefined;
                  cell.value = val === undefined || val === null ? 0 : val;
                }
                cell.style = cellStyle;
                cIndex++;
              });
            }
            row.height = 22;
          });
          // 列宽:主表列 + 题目选项列
          const columns = [
            { width: 20 },
            { width: 12 },
            { width: 12 },
            { width: 12 },
            { width: 12 },
            { width: 12 },
            { width: 15 },
            { width: 15 },
            { width: 12 },
          ];
          lishuiTopics.forEach((topic, ti) => {
            topicColumns[ti].widths.forEach((w) => {
              columns.push({ width: w });
            });
          });
          worksheet.columns = columns;
        } else {
          // 其他医院:单行表头
          const headerRow = worksheet.addRow(mainHeaders);
          headerRow.eachCell((cell) => {
            cell.style = headerStyle;
          });
          headerRow.height = 25;
          this.userList.forEach((item) => {
            const dataRow = worksheet.addRow(this.getMainExportRow(item));
            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();