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