From d725dc65cf7f74df3aa3f5a0297fd6f8b767cdbc Mon Sep 17 00:00:00 2001
From: liusheng <337615773@qq.com>
Date: 星期五, 30 十月 2026 10:23:03 +0800
Subject: [PATCH] Merge branch 'subtask' of http://116.62.18.175:6699/r/~yxh/smartor-web into subtask

---
 src/views/Satisfaction/sfstatistics/components/SatisfactionStatistics.vue |  346 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 341 insertions(+), 5 deletions(-)

diff --git a/src/views/Satisfaction/sfstatistics/components/SatisfactionStatistics.vue b/src/views/Satisfaction/sfstatistics/components/SatisfactionStatistics.vue
index eda31ce..ec34803 100644
--- a/src/views/Satisfaction/sfstatistics/components/SatisfactionStatistics.vue
+++ b/src/views/Satisfaction/sfstatistics/components/SatisfactionStatistics.vue
@@ -14,7 +14,7 @@
           <el-form-item label="婊℃剰搴︾被鍨�" prop="satisfactionScope">
             <el-select
               v-model="satisfactionScope"
-              placeholder="榛樿鍦ㄩ櫌"
+              placeholder="榛樿绂婚櫌"
               clearable
               style="width: 140px"
               @change="handleSearch"
@@ -104,6 +104,14 @@
             </el-button>
             <el-button icon="el-icon-refresh" @click="handleReset">
               閲嶇疆
+            </el-button>
+            <el-button
+              type="warning"
+              icon="el-icon-download"
+              :loading="exporting"
+              @click="handleExportExcel"
+            >
+              瀵煎嚭Excel
             </el-button>
           </el-form-item>
         </el-form>
@@ -683,6 +691,9 @@
 
 <script>
 import * as echarts from "echarts";
+import * as XLSX from "xlsx";
+import ExcelJS from "exceljs";
+import { saveAs } from "file-saver";
 import {
   statistics,
   satisfactionGraph,
@@ -698,8 +709,8 @@
     return {
       // 婊℃剰搴︾被鍨嬶細leave=绂婚櫌婊℃剰搴︼紙leaveldeptcodes/leavehospitaldistrictcodes锛夛紝
       // hospital=鍦ㄩ櫌婊℃剰搴︼紙deptcodes/hospitaldistrictcodes锛�
-      // 榛樿鍦ㄩ櫌锛氫繚鎸佽椤靛師鏈夎姹傚弬鏁颁笉鍙�
-      satisfactionScope: "hospital",
+      // 榛樿绂婚櫌婊℃剰搴�
+      satisfactionScope: "leave",
       // 鏌ヨ鍙傛暟
       queryParams: {
         type: 2,
@@ -737,6 +748,8 @@
 
       // 棰樼洰鏄庣粏鏁版嵁
       questionDetailData: [],
+      // 棰樼洰鏄庣粏鍏ㄩ噺鏁版嵁锛堝鍑虹敤锛屼笉鍙楀垎椤靛奖鍝嶏級
+      questionDetailAll: [],
 
       // 缁村害棰樼洰鏄庣粏鏁版嵁
       dimensionDetailData: [],
@@ -771,6 +784,9 @@
 
       // 鍚勭被鍨嬬粺璁℃槑缁嗘暟鎹�
       typeDetailData: [],
+
+      // 瀵煎嚭涓紙鎸夐挳 loading锛�
+      exporting: false,
 
       // 鏌辩姸鍥炬暟鎹�
       chartData: [],
@@ -1229,6 +1245,8 @@
       const endIndex = startIndex + this.detailQueryParams.pageSize;
       const paginatedData = detailData.slice(startIndex, endIndex);
 
+      // 瀵煎嚭鐢細淇濈暀涓嶅垎椤电殑鍏ㄩ噺鏁版嵁
+      this.questionDetailAll = detailData;
       this.questionDetailData = paginatedData;
       this.detailTotal = detailData.length;
       this.totalAnswerCount = apiData.totalPerson || 0;
@@ -2059,6 +2077,8 @@
     // 澶勭悊閲嶇疆
     handleReset() {
       this.$refs.queryForm.resetFields();
+      // 閲嶇疆鍚庡洖鍒伴粯璁ょ殑绂婚櫌婊℃剰搴�
+      this.satisfactionScope = "leave";
       this.queryParams.dateRange = [];
       this.queryParams.deptCodes = []; // 閲嶇疆涓烘暟缁�
       this.queryParams.wardCodes = []; // 閲嶇疆涓烘暟缁�
@@ -2120,11 +2140,327 @@
       this.$message.info(`鏌ョ湅绫诲瀷璇︽儏锛�${row.typeName}`);
     },
 
-    // 澶勭悊瀵煎嚭鏁版嵁
+    // 澶勭悊瀵煎嚭鏁版嵁锛氭寜椤甸潰杩欏紶绫诲瀷琛ㄧ殑鍒楀鍑鸿琛�
     handleExportData(row) {
-      this.$message.success(`姝e湪瀵煎嚭 ${row.typeName} 鏁版嵁...`);
+      if (!row) {
+        this.$message.warning("娌℃湁鍙鍑虹殑鏁版嵁");
+        return;
+      }
+      try {
+        const data = [
+          [`婊℃剰搴︾被鍨嬬粺璁★紙${row.typeName || ""}锛塦],
+          ["搴忓彿", "婊℃剰搴︾被鍨�", "鍙戦�侀棶鍗锋暟", "鍥炴敹闂嵎鏁�", "鍥炴敹鐜�", "骞冲潎鍒�", "婊℃剰搴︾瓑绾�"],
+          [
+            row.id,
+            row.typeName || "",
+            Number(row.sendCount) || 0,
+            Number(row.receiveCount) || 0,
+            this.formatPercent(row.recoveryRate),
+            Number(row.averageScore || 0).toFixed(1),
+            row.satisfactionLevel || "",
+          ],
+        ];
+        const ws = XLSX.utils.aoa_to_sheet(data);
+        ws["!merges"] = [{ s: { r: 0, c: 0 }, e: { r: 0, c: 6 } }];
+        ws["!cols"] = [
+          { wch: 8 },
+          { wch: 22 },
+          { wch: 14 },
+          { wch: 14 },
+          { wch: 12 },
+          { wch: 10 },
+          { wch: 12 },
+        ];
+        const wb = XLSX.utils.book_new();
+        XLSX.utils.book_append_sheet(wb, ws, "Sheet1");
+        XLSX.writeFile(wb, `婊℃剰搴︾被鍨嬬粺璁${row.typeName || "瀵煎嚭"}.xlsx`);
+        this.$message.success("瀵煎嚭鎴愬姛");
+      } catch (e) {
+        console.error("瀵煎嚭澶辫触:", e);
+        this.$message.error("瀵煎嚭澶辫触锛�" + (e.message || e));
+      }
     },
 
+    // 瀵煎嚭锛氫袱寮犲浘琛紙婊℃剰搴︾被鍨嬨�佺淮搴︾粺璁★紝鍥惧拰椤甸潰涓�鑷达級+ 涓夊紶鏄庣粏琛紙鍏ㄩ噺锛屼笉鍒嗛〉锛夛紝澶� sheet
+    async handleExportExcel() {
+      if (this.exporting) return;
+      this.exporting = true;
+      try {
+        // 涓夊紶鏄庣粏琛ㄦ槸鍒囧埌瀵瑰簲椤电鎵嶅姞杞界殑锛屽鍑哄墠鍏堣ˉ榻愭暟鎹紝閬垮厤瀵煎嚭绌鸿〃
+        await this.ensureDetailDataLoaded();
+        const wb = new ExcelJS.Workbook();
+
+        // 1銆�2锛氬浘琛� sheet锛堟弧鎰忓害绫诲瀷 / 缁村害缁熻锛�
+        const typeInfo = await this.captureChartImage("satisfactionType");
+        this.addChartSheet(wb, "婊℃剰搴︾被鍨�", "婊℃剰搴︾被鍨嬪~鎶ユ瘮渚嬬粺璁�", typeInfo);
+        const dimInfo = await this.captureChartImage("dimension");
+        this.addChartSheet(wb, "缁村害缁熻", "缁村害濉姤姣斾緥缁熻", dimInfo);
+
+        // 3锛氶鐩槑缁嗙粺璁★紙鍏ㄩ噺锛�
+        const qList =
+          this.questionDetailAll && this.questionDetailAll.length
+            ? this.questionDetailAll
+            : this.questionDetailData || [];
+        this.addTableSheet(
+          wb,
+          "棰樼洰鏄庣粏缁熻",
+          ["搴忓彿", "棰樼洰", "骞冲潎寰楀垎", "鏈�楂樺緱鍒�", "鏈�浣庡緱鍒�", "绛旈浜烘暟", "鏈瓟棰樹汉鏁�", "绛旈鐜�"],
+          qList.map((r, i) => [
+            i + 1,
+            r.scriptContent || "",
+            this.fix1(r.averageScore),
+            this.fix1(r.maxScore),
+            this.fix1(r.minScore),
+            r.answerCount || 0,
+            r.noAnswerPerson || 0,
+            this.formatPercent(r.answerRate),
+          ])
+        );
+
+        // 4锛氱淮搴﹂鐩槑缁嗭紙搴忓彿銆侀鐩�佺淮搴︺�佸钩鍧�/鏈�楂�/鏈�浣庡緱鍒嗐�佺瓟棰樹汉鏁般�佹湭绛旈浜烘暟銆佺瓟棰樼巼锛�
+        this.addTableSheet(
+          wb,
+          "缁村害棰樼洰鏄庣粏",
+          [
+            "搴忓彿",
+            "棰樼洰",
+            "缁村害",
+            "骞冲潎寰楀垎",
+            "鏈�楂樺緱鍒�",
+            "鏈�浣庡緱鍒�",
+            "绛旈浜烘暟",
+            "鏈瓟棰樹汉鏁�",
+            "绛旈鐜�",
+          ],
+          (this.dimensionDetailData || []).map((r, i) => [
+            i + 1,
+            r.scriptContent || "",
+            r.dimension ? this.getDimensionLabel(r.dimension) : r.dimensionName || "",
+            this.fix1(r.averageScore),
+            this.fix1(r.maxScore),
+            this.fix1(r.minScore),
+            r.answerPerson || 0,
+            r.noAnswerPerson || 0,
+            this.formatPercent(r.answerRate),
+          ])
+        );
+
+        // 5锛氬悇绫诲瀷缁熻鏄庣粏
+        this.addTableSheet(
+          wb,
+          "鍚勭被鍨嬬粺璁℃槑缁�",
+          ["搴忓彿", "婊℃剰搴︾被鍨�", "鍙戦�侀棶鍗锋暟", "鍥炴敹闂嵎鏁�", "鍥炴敹鐜�", "骞冲潎鍒�", "婊℃剰搴︾瓑绾�"],
+          (this.typeDetailData || []).map((r, i) => [
+            i + 1,
+            r.typeName || "",
+            r.sendCount || 0,
+            r.receiveCount || 0,
+            this.formatPercent(r.recoveryRate),
+            this.fix1(r.averageScore),
+            r.satisfactionLevel || "",
+          ])
+        );
+
+        const buffer = await wb.xlsx.writeBuffer();
+        const blob = new Blob([buffer], {
+          type:
+            "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+        });
+        const suffix =
+          this.startTime && this.endTime
+            ? `_${String(this.startTime).slice(0, 10)}_${String(this.endTime).slice(0, 10)}`
+            : "";
+        saveAs(blob, `婊℃剰搴︽槑缁嗙粺璁�${suffix}.xlsx`);
+        this.$message.success("瀵煎嚭鎴愬姛");
+      } catch (e) {
+        console.error("瀵煎嚭澶辫触:", e);
+        this.$message.error("瀵煎嚭澶辫触锛�" + (e.message || e));
+      } finally {
+        this.exporting = false;
+      }
+    },
+
+    // 瀵煎嚭鍓嶈ˉ榻愪笁寮犳槑缁嗚〃鐨勬暟鎹紙椤甸潰鏄垏椤电鎵嶅姞杞斤紝娌″垏杩囩殑琛ㄦ槸绌虹殑锛�
+    async ensureDetailDataLoaded() {
+      const tasks = [];
+      if (!this.questionDetailAll || !this.questionDetailAll.length) {
+        tasks.push(this.loadQuestionDetailData());
+      }
+      if (!this.dimensionDetailData || !this.dimensionDetailData.length) {
+        tasks.push(this.loadDimensionDetailData());
+      }
+      if (!this.typeDetailData || !this.typeDetailData.length) {
+        tasks.push(this.loadTypeDetailData());
+      }
+      if (tasks.length) {
+        await Promise.all(tasks);
+      }
+    },
+
+    // 淇濈暀涓�浣嶅皬鏁帮紝绌哄�兼樉绀� -
+    fix1(v) {
+      if (v === null || v === undefined || v === "") return "-";
+      const n = Number(v);
+      return isNaN(n) ? "-" : n.toFixed(1);
+    },
+
+    // 鍥捐〃 sheet锛氭爣棰� + 姹囨�� + 鍥捐〃鍥剧墖
+    addChartSheet(wb, sheetName, title, info) {
+      const ws = wb.addWorksheet(sheetName, {
+        views: [{ showGridLines: false }],
+      });
+      ws.getCell("A1").value = title;
+      ws.getCell("A1").font = { size: 14, bold: true };
+      ws.mergeCells("A1:F1");
+      const rate =
+        info && info.totalSend
+          ? this.formatPercent(info.totalReceive / info.totalSend)
+          : "-";
+      ws.getCell("A2").value = `鍙戦�侀鐩�婚噺锛�${(info && info.totalSend) || 0}    鍥炴敹棰樼洰鎬婚噺锛�${(info && info.totalReceive) || 0}    鎬讳綋鍥炴敹鐜囷細${rate}`;
+      ws.mergeCells("A2:F2");
+      for (let c = 1; c <= 6; c++) {
+        ws.getColumn(c).width = 18;
+      }
+      if (info && info.url) {
+        const imgId = wb.addImage({
+          base64: info.url.split(",")[1],
+          extension: "png",
+        });
+        ws.addImage(imgId, {
+          tl: { col: 0.1, row: 3.1 },
+          ext: { width: 940, height: 420 },
+        });
+      } else {
+        ws.getCell("A4").value = "鏆傛棤鏁版嵁";
+      }
+      return ws;
+    },
+
+    // 鏅�氳〃鏍� sheet锛氭爣棰� + 琛ㄥご + 鏁版嵁
+    addTableSheet(wb, sheetName, headers, rows) {
+      const ws = wb.addWorksheet(sheetName);
+      ws.addRow([sheetName]).font = { size: 14, bold: true };
+      ws.mergeCells(1, 1, 1, Math.max(headers.length, 1));
+      const headerRow = ws.addRow(headers);
+      headerRow.eachCell((cell) => {
+        cell.font = { bold: true };
+        cell.alignment = { horizontal: "center", vertical: "middle" };
+        cell.fill = {
+          type: "pattern",
+          pattern: "solid",
+          fgColor: { argb: "FFF5F7FA" },
+        };
+      });
+      if (!rows || !rows.length) {
+        ws.addRow(["鏆傛棤鏁版嵁"]);
+      } else {
+        rows.forEach((r) => {
+          const row = ws.addRow(r);
+          row.eachCell((cell) => {
+            cell.alignment = { horizontal: "center", vertical: "middle" };
+          });
+        });
+      }
+      ws.columns.forEach((col, i) => {
+        col.width = i === 1 ? 40 : 14;
+      });
+      return ws;
+    },
+
+    // 鍙栨煇涓浘琛ㄩ〉绛剧殑鏁版嵁锛堜笉鏀瑰彉椤甸潰鐘舵�侊級
+    async fetchChartRows(chartType) {
+      const rows = [];
+      let totalSend = 0;
+      let totalReceive = 0;
+      let index = 0;
+
+      if (chartType === "dimension") {
+        const params = {
+          type: this.queryParams.type,
+          startTime: this.startTime,
+          endTime: this.endTime,
+          ...this.buildScopeParams(),
+          templateid: this.templateId,
+          questionType: this.queryParams.type || null,
+          serviceTypes: this.defaultServiceTypes,
+          dimensionList: this.dimensionList,
+        };
+        const res = await satisfactionGraphDimension(params);
+        Object.entries((res && res.rows) || {}).forEach(([code, stat]) => {
+          const sendCount = stat.subidAll || 0;
+          const receiveCount = stat.fillCountAll || 0;
+          const rate = stat.receiveRate || 0;
+          rows.push({
+            name: this.getDimensionLabel(code),
+            value: (rate * 100).toFixed(2),
+            sendCount: sendCount,
+            receiveCount: receiveCount,
+            averageScore: stat.averageScore || 0,
+            dimension: code,
+            itemStyle: { color: this.getChartColor(index++) },
+          });
+          totalSend += sendCount;
+          totalReceive += receiveCount;
+        });
+      } else {
+        const params = {
+          type: this.queryParams.type,
+          startTime: this.startTime,
+          endTime: this.endTime,
+          ...this.buildScopeParams(),
+          templateid: this.templateId,
+        };
+        const res = await satisfactionGraph(params);
+        Object.entries((res && res.rows) || {}).forEach(([name, stat]) => {
+          const sendCount = stat.subidAll || 0;
+          const receiveCount = stat.fillCountAll || 0;
+          const rate = stat.receiveRate || 0;
+          rows.push({
+            name: name,
+            value: (rate * 100).toFixed(2),
+            sendCount: sendCount,
+            receiveCount: receiveCount,
+            averageScore: stat.averageScore || 0,
+            itemStyle: { color: this.getChartColor(index++) },
+          });
+          totalSend += sendCount;
+          totalReceive += receiveCount;
+        });
+      }
+      return { rows, totalSend, totalReceive };
+    },
+
+    // 绂诲睆娓叉煋鍥捐〃骞跺鍑� base64 鍥剧墖锛堝鐢ㄩ〉闈� renderChart 鐨勬牱寮忥紝淇濊瘉鍥惧拰椤甸潰涓�鑷达級
+    async captureChartImage(chartType) {
+      const info = await this.fetchChartRows(chartType);
+      if (!info.rows.length) {
+        return { ...info, url: null };
+      }
+      const div = document.createElement("div");
+      div.style.cssText =
+        "position:absolute;left:-99999px;top:0;width:1000px;height:400px;";
+      document.body.appendChild(div);
+      const inst = echarts.init(div);
+      const prevChart = this.barChart;
+      try {
+        this.barChart = inst;
+        // 鍏抽棴鍔ㄧ敾锛氬惁鍒� getDataURL 鎴埌鐨勬槸鏌卞瓙杩樻病闀垮嚭鏉ョ殑绗竴甯э紙鍥鹃噷鏈夊潗鏍囪酱浣嗘病鏈夋煴瀛愶級
+        inst.setOption({ animation: false });
+        this.renderChart(info.rows, chartType);
+        // 绛変竴甯э紝纭繚 canvas 宸茬粡鐢诲畬
+        await new Promise((resolve) => setTimeout(resolve, 120));
+        const url = inst.getDataURL({
+          type: "png",
+          pixelRatio: 2,
+          backgroundColor: "#fff",
+        });
+        return { ...info, url };
+      } finally {
+        this.barChart = prevChart;
+        inst.dispose();
+        document.body.removeChild(div);
+      }
+    },
     // 鏍煎紡鍖栫櫨鍒嗘瘮
     formatPercent(value) {
       if (value === null || value === undefined) return "-";

--
Gitblit v1.9.3