From a9d2b856e0f6be6475319c2dc36bf405c2f908fc Mon Sep 17 00:00:00 2001
From: WXL (wul) <wl_5969728@163.com>
Date: 星期二, 22 九月 2026 14:04:52 +0800
Subject: [PATCH] 测试完成

---
 src/views/Satisfaction/sfstatistics/components/DataOverview.vue | 1133 +++++++++++++++++++++++++++++++++++++++++++++++++++++-----
 1 files changed, 1,021 insertions(+), 112 deletions(-)

diff --git a/src/views/Satisfaction/sfstatistics/components/DataOverview.vue b/src/views/Satisfaction/sfstatistics/components/DataOverview.vue
index 18b5142..2cfa325 100644
--- a/src/views/Satisfaction/sfstatistics/components/DataOverview.vue
+++ b/src/views/Satisfaction/sfstatistics/components/DataOverview.vue
@@ -1,14 +1,130 @@
 <template>
   <div class="data-overview">
-    <!-- 琛ㄦ牸涓�锛氬悇鏈嶅姟椤圭洰婊℃剰搴︾粺璁� -->
+    <!-- 椤堕儴绛涢�夛細鏃堕棿鑼冨洿 + 绉戝/鐥呭尯锛屾帶鍒舵湰椤垫墍鏈夋暟鎹� -->
+    <el-card shadow="never" class="overview-card filter-card">
+      <el-form inline class="filter-form">
+        <el-form-item label="鍑洪櫌鏃ユ湡">
+          <el-date-picker
+            v-model="queryParams.dateRange"
+            type="daterange"
+            range-separator="鑷�"
+            start-placeholder="寮�濮嬫棩鏈�"
+            end-placeholder="缁撴潫鏃ユ湡"
+            value-format="yyyy-MM-dd"
+            size="small"
+          />
+        </el-form-item>
+        <el-form-item label="缁熻缁村害">
+          <el-radio-group
+            v-model="queryParams.statMode"
+            size="small"
+            @change="onStatModeChange"
+          >
+            <el-radio-button label="ward">鎸夌梾鍖虹粺璁�</el-radio-button>
+            <el-radio-button label="dept">鎸夌瀹ょ粺璁�</el-radio-button>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="绉戝" v-if="queryParams.statMode === 'dept'">
+          <el-select
+            v-model="queryParams.deptCodes"
+            multiple
+            collapse-tags
+            clearable
+            filterable
+            placeholder="鍏ㄩ儴绉戝"
+            size="small"
+            class="filter-select"
+          >
+            <el-option
+              v-for="d in deptOptions"
+              :key="d.value"
+              :label="d.label"
+              :value="d.value"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="鐥呭尯" v-else>
+          <el-select
+            v-model="queryParams.wardCodes"
+            multiple
+            collapse-tags
+            clearable
+            filterable
+            placeholder="鍏ㄩ儴鐥呭尯"
+            size="small"
+            class="filter-select"
+          >
+            <el-option
+              v-for="w in wardOptions"
+              :key="w.value"
+              :label="w.label"
+              :value="w.value"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="鏈嶅姟绫诲瀷">
+          <el-select
+            v-model="queryParams.serviceTypeList"
+            multiple
+            collapse-tags
+            clearable
+            filterable
+            placeholder="鍏ㄩ儴绫诲瀷"
+            size="small"
+            class="filter-select"
+          >
+            <el-option
+              v-for="s in serviceTypeOptions"
+              :key="s.value"
+              :label="s.label"
+              :value="s.value"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button
+            type="primary"
+            size="small"
+            icon="el-icon-search"
+            :loading="loading"
+            @click="fetchData"
+            >鏌ヨ</el-button
+          >
+          <el-button size="small" icon="el-icon-refresh" @click="handleReset"
+            >閲嶇疆</el-button
+          >
+        </el-form-item>
+      </el-form>
+    </el-card>
+
+    <!-- 琛ㄦ牸涓�锛氬悇鏈嶅姟椤圭洰婊℃剰搴︾粺璁★紙鍥哄畾鍏」閫夐」鐨勯鐩級 -->
     <el-card shadow="never" class="overview-card">
       <div slot="header" class="card-header">
         <span class="card-title">鍚勬湇鍔¢」鐩弧鎰忓害缁熻</span>
-        <el-button type="warning" size="small" icon="el-icon-download" @click="exportTable('table1')">瀵煎嚭Excel</el-button>
+        <el-button
+          type="warning"
+          size="small"
+          icon="el-icon-download"
+          @click="exportTable('table1')"
+          >瀵煎嚭Excel</el-button
+        >
       </div>
-      <el-table id="table1" :data="serviceSatisfaction" border size="small" :header-cell-style="{ background: '#f5f7fa', color: '#303133' }">
-        <el-table-column prop="category" label="鍒嗙被/棰樼洰" min-width="220" fixed="left" />
-        <el-table-column label="闈炲父婊℃剰" align="center">
+      <el-table
+        id="table1"
+        v-loading="loading"
+        element-loading-text="缁熻涓�..."
+        :data="serviceSatisfaction"
+        border
+        size="small"
+        :header-cell-style="{ background: '#f5f7fa', color: '#303133' }"
+      >
+        <el-table-column
+          prop="category"
+          label="鍒嗙被/棰樼洰"
+          min-width="220"
+          fixed="left"
+        />
+        <el-table-column label="寰堟弧鎰�" align="center">
           <el-table-column prop="veryGoodCount" label="鏁伴噺" width="90" />
           <el-table-column prop="veryGoodRate" label="姣斾緥" width="90" />
         </el-table-column>
@@ -24,127 +140,861 @@
           <el-table-column prop="badCount" label="鏁伴噺" width="90" />
           <el-table-column prop="badRate" label="姣斾緥" width="90" />
         </el-table-column>
-        <el-table-column label="涓嶉�傜敤" align="center">
+        <el-table-column label="寰堜笉婊℃剰" align="center">
+          <el-table-column prop="veryBadCount" label="鏁伴噺" width="90" />
+          <el-table-column prop="veryBadRate" label="姣斾緥" width="90" />
+        </el-table-column>
+        <el-table-column label="鏈粡鍘�" align="center">
           <el-table-column prop="naCount" label="鏁伴噺" width="90" />
           <el-table-column prop="naRate" label="姣斾緥" width="90" />
         </el-table-column>
-        <el-table-column prop="score" label="缁煎悎寰楀垎" width="100" align="center" />
+        <el-table-column
+          prop="score"
+          label="缁煎悎寰楀垎"
+          width="100"
+          align="center"
+          fixed="right"
+        />
       </el-table>
     </el-card>
 
-    <!-- 琛ㄦ牸浜岋細鍗曢�夐/澶氶�夐缁熻 -->
+    <!-- 琛ㄦ牸浜岋細鍗曢�夐/澶氶�夐缁熻锛堝叾浣欓鐩級 -->
     <el-card shadow="never" class="overview-card">
       <div slot="header" class="card-header">
         <span class="card-title">鍗曢�夐/澶氶�夐缁熻</span>
-        <el-button type="warning" size="small" icon="el-icon-download" @click="exportTable('table2')">瀵煎嚭Excel</el-button>
+        <div class="header-right">
+          <el-input
+            v-model="questionSearch"
+            size="small"
+            clearable
+            prefix-icon="el-icon-search"
+            placeholder="鎸夐鐩ā绯婃煡璇�"
+            class="search-input"
+          />
+          <el-button
+            type="warning"
+            size="small"
+            icon="el-icon-download"
+            @click="exportTable('table2')"
+            >瀵煎嚭Excel</el-button
+          >
+        </div>
       </div>
-      <div class="question-block" v-for="(q, qi) in questionStats" :key="qi">
-        <h4 class="q-title">{{ q.title }}</h4>
-        <el-table :data="q.options" border size="small" :header-cell-style="{ background: '#f5f7fa', color: '#303133' }">
-          <el-table-column prop="option" label="閫夐」" min-width="180" />
-          <el-table-column prop="count" label="鏁伴噺" width="100" align="center" />
-          <el-table-column prop="rate" label="姣斾緥" width="100" align="center" />
-        </el-table>
-        <div v-if="q.total" class="q-footer">鏈鏈夋晥濉啓浜烘锛�<b>{{ q.total }}</b></div>
+      <div id="table2" v-loading="loading" element-loading-text="缁熻涓�...">
+        <div class="table2-scroll">
+          <div
+            class="question-block"
+            v-for="(q, qi) in filteredQuestionStats"
+            :key="qi"
+          >
+            <h4 class="q-title">{{ q.title }}</h4>
+            <el-table
+              :data="q.options"
+              border
+              size="small"
+              :header-cell-style="{ background: '#f5f7fa', color: '#303133' }"
+            >
+              <el-table-column prop="option" label="閫夐」" min-width="180" />
+              <el-table-column
+                prop="count"
+                label="鏁伴噺"
+                width="100"
+                align="center"
+              />
+              <el-table-column
+                prop="rate"
+                label="姣斾緥"
+                width="100"
+                align="center"
+              />
+            </el-table>
+            <div v-if="q.total" class="q-footer">
+              鏈濉姤鎬婚噺锛�<b>{{ q.total }}</b>
+            </div>
+          </div>
+          <div v-if="!loading && !filteredQuestionStats.length" class="empty-tip">
+            {{ questionStats.length ? "鏈壘鍒板尮閰嶇殑棰樼洰" : "鏆傛棤鍗曢��/澶氶�夐缁熻鏁版嵁" }}
+          </div>
+        </div>
       </div>
     </el-card>
 
-    <!-- 琛ㄦ牸涓夛細鍚勭瀹ゆ弧鎰忓害璇勫垎瀵规瘮 -->
+    <!-- 琛ㄦ牸涓夛細鍚勭瀹�/鐥呭尯婊℃剰搴﹁瘎鍒嗗姣� -->
     <el-card shadow="never" class="overview-card">
       <div slot="header" class="card-header">
-        <span class="card-title">鍚勭瀹ゆ弧鎰忓害璇勫垎瀵规瘮</span>
-        <el-button type="warning" size="small" icon="el-icon-download" @click="exportTable('table3')">瀵煎嚭Excel</el-button>
+        <span class="card-title">鍚勭瀹�/鐥呭尯婊℃剰搴﹁瘎鍒嗗姣�</span>
+        <div class="header-right">
+          <el-radio-group
+            v-model="statDimension"
+            size="small"
+            @change="onStatDimensionChange"
+          >
+            <el-radio-button label="dept">鎸夌瀹�</el-radio-button>
+            <el-radio-button label="ward">鎸夌梾鍖�</el-radio-button>
+          </el-radio-group>
+          <el-select
+            v-model="statDeptWard"
+            size="small"
+            class="stat-select"
+            placeholder="娌跨敤椤堕儴鏉′欢"
+          >
+            <el-option label="娌跨敤椤堕儴鏉′欢" value="inherit" />
+            <template v-if="statDimension === 'dept'">
+              <el-option
+                v-for="d in deptOptions"
+                :key="'dept:' + d.value"
+                :label="d.label"
+                :value="'dept:' + d.value"
+              />
+            </template>
+            <template v-else>
+              <el-option
+                v-for="w in wardOptions"
+                :key="'ward:' + w.value"
+                :label="w.label"
+                :value="'ward:' + w.value"
+              />
+            </template>
+          </el-select>
+          <el-button
+            type="warning"
+            size="small"
+            icon="el-icon-download"
+            @click="exportTable('table3')"
+            >瀵煎嚭Excel</el-button
+          >
+        </div>
       </div>
-      <el-table id="table3" :data="deptComparison" border size="small" :header-cell-style="{ background: '#f5f7fa', color: '#303133' }">
-        <el-table-column prop="dept" label="绉戝"fixed="left" align="center" />
-        <el-table-column label="绗�1棰�" align="center">
-          <el-table-column prop="q1VeryGood" label="闈炲父婊℃剰" width="100" />
-          <el-table-column prop="q1Good" label="婊℃剰" width="90" />
-          <el-table-column prop="q1Normal" label="涓�鑸�" width="90" />
-          <el-table-column prop="q1Bad" label="涓嶆弧鎰�" width="90" />
-          <el-table-column prop="q1Score" label="寰楀垎" width="100" />
+      <el-table
+        id="table3"
+        v-loading="loading"
+        element-loading-text="缁熻涓�..."
+        :data="table3Rows"
+        border
+        size="small"
+        max-height="500"
+        :header-cell-style="{ background: '#f5f7fa', color: '#303133' }"
+      >
+        <el-table-column
+          prop="name"
+          :label="statDimension === 'dept' ? '绉戝' : '鐥呭尯'"
+          fixed="left"
+          align="center"
+        />
+        <el-table-column
+          v-for="q in table3Questions"
+          :key="q.title"
+          :label="q.title"
+          align="center"
+        >
+          <el-table-column
+            v-for="opt in q.options"
+            :key="opt"
+            :label="opt"
+            width="100"
+          >
+            <template slot-scope="{ row }">
+              {{
+                (row.cells[q.title] &&
+                  row.cells[q.title][opt] &&
+                  row.cells[q.title][opt].count) ||
+                  0
+              }}
+            </template>
+          </el-table-column>
+          <el-table-column label="寰楀垎" width="90">
+            <template slot-scope="{ row }">
+              {{ (row.cells[q.title] && row.cells[q.title]._score) || "0.00" }}
+            </template>
+          </el-table-column>
         </el-table-column>
-        <el-table-column label="绗�2棰�" align="center">
-          <el-table-column prop="q2VeryGood" label="闈炲父婊℃剰" width="100" />
-          <el-table-column prop="q2Good" label="婊℃剰" width="90" />
-          <el-table-column prop="q2Normal" label="涓�鑸�" width="90" />
-          <el-table-column prop="q2Bad" label="涓嶆弧鎰�" width="90" />
-          <el-table-column prop="q2Score" label="寰楀垎" width="100" />
+        <el-table-column label="缁煎悎寰楀垎" align="center">
+          <template slot-scope="{ row }">
+            {{ row.totalScore }}
+          </template>
         </el-table-column>
-        <el-table-column prop="totalScore" label="缁煎悎寰楀垎"  align="center" />
       </el-table>
     </el-card>
   </div>
 </template>
 
 <script>
-import XLSX from 'xlsx';
+import { sltdMydTotalByScore,statQuestionOptionByKsOrBq } from "@/api/AiCentre/satisfaction";
+import XLSX from "xlsx";
+
+// 婊℃剰搴﹂鐨勫浐瀹氶�夐」锛堥『搴忓嵆琛ㄦ牸鍒楅『搴忥級鈥斺�旇繖浜涢�夐」鐨勯鐩繘琛ㄤ竴锛屽叾浣欒繘琛ㄤ簩
+const FIXED_OPTIONS = ["寰堟弧鎰�", "婊℃剰", "涓�鑸�", "涓嶆弧鎰�", "寰堜笉婊℃剰", "鏈粡鍘�"];
+// 鏃犲垎鍊奸�夐」锛堝"鏈粡鍘�"锛夋槸鍚﹁鍏ョ患鍚堝緱鍒嗗垎姣嶏細榛樿 false锛岄伩鍏嶆媺浣庡緱鍒�
+const NO_SCORE_IN_DENOMINATOR = false;
+
+// 鏈嶅姟绫诲瀷瀛楀吀锛堟帴鍙� serviceType锛�
+const SERVICE_TYPES = [
+  { value: "1", label: "鐩戞祴璇勪及" },
+  { value: "2", label: "鍑洪櫌闅忚" },
+  { value: "3", label: "闂ㄨ瘖闅忚" },
+  { value: "4", label: "瀹f暀鍏虫��" },
+  { value: "5", label: "澶嶈瘖閫氱煡" },
+  { value: "6", label: "婊℃剰搴﹁皟鏌�" },
+  { value: "7", label: "鎮h�呮姤鍛�" },
+  { value: "8", label: "鍏朵粬閫氱煡" },
+  { value: "9", label: "浣撴闅忚" },
+  { value: "10", label: "鍖绘妧闅忚" },
+  { value: "11", label: "褰卞儚涓撶闅忚" },
+  { value: "12", label: "蹇冪數涓撶闅忚" },
+  { value: "13", label: "涓撶闅忚" },
+];
+
+// 鏈嶅姟绫诲瀷榛樿閫変腑椤癸紙鎸� label 鍖归厤锛岄�傞厤涓嶅悓瀛楀吀鐨� value 宸紓锛�
+const DEFAULT_SERVICE_LABELS = ["浣忛櫌婊℃剰搴�", "闂ㄨ瘖婊℃剰搴�"];
+
+// 鍏滃簳锛歴tore 涓棤绉戝/鐥呭尯鏁版嵁鏃朵娇鐢紙琛ㄤ笁瀵规瘮 + 绛涢�変笅鎷夛級
+const DEPTS = ["鍐呭垎娉岀", "鐨偆绉�", "鑲跨槫鍐呯", "娑堝寲鍐呯", "鑲惧唴绉�"];
+const WARDS = ["涓�鐥呭尯", "浜岀梾鍖�", "涓夌梾鍖�", "鍥涚梾鍖�", "浜旂梾鍖�"];
+
+/**
+ * 鎺ュ彛鏁版嵁 鈫� 椤甸潰鍐呴儴棰樼洰缁撴瀯
+ * 鍏ュ弬锛氭帴鍙� data锛圠ist<QuestionResultDTO>锛�
+ * 鍑哄弬锛歔{ title, type, total, options: [{ option, count, score }] }]
+ * 鍚屼竴棰樼洰鍙兘鍒嗘暎鍦ㄥ涓换鍔★紙taskname锛夐噷锛岃繖閲屾寜銆岄鐩� + 閫夐」銆嶅悎骞剁疮鍔犳暟閲�
+ */
+function normalizeQuestions(list) {
+  const map = new Map();
+
+  // 鍏ュ弬鍙兘鏄暟缁� / {list} / {rows} / 鍗曞璞★紝缁熶竴瑙勬暣鎴愭暟缁�
+  const arr = Array.isArray(list)
+    ? list
+    : (list &&
+      (Array.isArray(list.list)
+        ? list.list
+        : Array.isArray(list.rows)
+        ? list.rows
+        : [list])) ||
+      [];
+
+  // 棰樼洰鏂囨湰瀛楁鍊欓�夛紙鏈郴缁熼鐩唴瀹圭粺涓�鍙� scriptContent锛屽吋瀹瑰巻鍙插懡鍚嶏級
+  const pickTitle = (item) => {
+    const keys = [
+      "scriptContent",
+      "questionText",
+      "qeustionText",
+      "questiontext",
+      "scriptcontent",
+      "title",
+      "name",
+      "questionName",
+    ];
+    for (const k of keys) {
+      if (item && item[k] != null && String(item[k]).trim()) {
+        return String(item[k]).trim();
+      }
+    }
+    return "";
+  };
+
+  // 閫夐」鏄庣粏鍒楄〃瀛楁鍊欓��
+  const pickDetails = (item) => {
+    const keys = [
+      "subtaskDetailRatioExportList",
+      "details",
+      "optionList",
+      "options",
+      "optionResultList",
+      "subtaskDetailRatioExportlist",
+      "list",
+    ];
+    for (const k of keys) {
+      if (item && Array.isArray(item[k])) return item[k];
+    }
+    return [];
+  };
+
+  // 閫夐」鏂囨湰瀛楁鍊欓��
+  const pickOption = (d) => {
+    const keys = [
+      "optionresult",
+      "optioncontent",
+      "optionText",
+      "targetvalue",
+      "optionName",
+      "name",
+      "option",
+      "content",
+    ];
+    for (const k of keys) {
+      if (d && d[k] != null && String(d[k]).trim()) {
+        return String(d[k]).trim();
+      }
+    }
+    return "";
+  };
+
+  // 鏁伴噺瀛楁鍊欓��
+  const pickCount = (d) => {
+    const keys = ["count", "chosenQuantity", "num", "total", "chosenquantity"];
+    for (const k of keys) {
+      if (d && d[k] != null && !isNaN(Number(d[k]))) {
+        return Number(d[k]) || 0;
+      }
+    }
+    return 0;
+  };
+
+  arr.forEach((item) => {
+    if (!item || typeof item !== "object") return;
+    const title = pickTitle(item) || "鏈懡鍚嶉鐩�";
+    const details = pickDetails(item);
+    let node = map.get(title);
+    if (!node) {
+      node = { title, type: "", options: new Map() };
+      map.set(title, node);
+    }
+
+    details.forEach((d) => {
+      const option = pickOption(d);
+      if (!option) return;
+      const count = pickCount(d);
+      const rawScore = Number(d && d.score);
+      const score = isNaN(rawScore) ? null : rawScore;
+      // 棰樺瀷锛氫紭鍏� scriptType锛�1鍗曢�� 2澶氶�� 4闂瓟锛夛紝鍏舵 valueType锛�1鍗曢�� 2澶氶�� 3闂瓟 4濉┖ 5鍏跺畠锛�
+      const typeVal = String(
+        (item && item.scriptType) ||
+          (d && d.scriptType) ||
+          (d && d.valueType) ||
+          (item && item.valueType) ||
+          ""
+      );
+      if (!node.type) {
+        node.type =
+          typeVal === "2" ? "multiple" : typeVal === "1" ? "single" : "qa";
+      }
+
+      const old = node.options.get(option);
+      if (old) {
+        old.count += count;
+        if (old.score == null) old.score = score;
+      } else {
+        node.options.set(option, { option, count, score });
+      }
+    });
+  });
+
+  return Array.from(map.values()).map((n) => {
+    const options = Array.from(n.options.values());
+    const item = { title: n.title, type: n.type, options };
+    // 澶氶�夐锛氱敤鍚勯�夐」鏁伴噺涔嬪拰浣滀负濉姤鎬婚噺锛堟帴鍙f湭鍗曠嫭鎻愪緵浜烘锛�
+    if (n.type === "multiple")
+      item.total = options.reduce((s, o) => s + o.count, 0);
+    return item;
+  });
+}
 
 export default {
   name: "DataOverview",
   data() {
     return {
-      serviceSatisfaction: [
-        { category: "璇婄枟杩囩▼涓紝鍖诲姟浜哄憳鏄惁娉ㄦ剰淇濇姢鎮ㄧ殑闅愮锛�", veryGoodCount: 3219, veryGoodRate: "67.84%", goodCount: 1351, goodRate: "28.47%", normalCount: 142, normalRate: "2.99%", badCount: 33, badRate: "0.70%", naCount: 0, naRate: "0.00%", score: "87.82" },
-        { category: "鎮ㄥ鍖婚櫌鏈嶅姟璁炬柦鐨勬�讳綋鍗拌薄濡備綍锛�", veryGoodCount: 3110, veryGoodRate: "65.54%", goodCount: 1369, goodRate: "28.85%", normalCount: 217, normalRate: "4.57%", badCount: 49, badRate: "1.03%", naCount: 0, naRate: "0.00%", score: "86.30" },
-        { category: "鍖婚櫌鐨勫帟鎵�鏄惁娓呮磥鏃犲紓鍛筹紵", veryGoodCount: 2973, veryGoodRate: "62.66%", goodCount: 1504, goodRate: "31.70%", normalCount: 235, normalRate: "4.95%", badCount: 33, badRate: "0.70%", naCount: 0, naRate: "0.00%", score: "85.44" },
-        { category: "鍖婚櫌鍐呯殑璺爣鍜屾寚绀烘槸鍚︽槑纭紵", veryGoodCount: 3234, veryGoodRate: "68.16%", goodCount: 1289, goodRate: "27.17%", normalCount: 196, normalRate: "4.13%", badCount: 26, badRate: "0.55%", naCount: 0, naRate: "0.00%", score: "87.64" },
-        { category: "鍖婚櫌鐨勭┖闂村竷灞�鏄惁渚垮埄锛�", veryGoodCount: 3037, veryGoodRate: "64.00%", goodCount: 1460, goodRate: "30.77%", normalCount: 209, normalRate: "4.40%", badCount: 39, badRate: "0.82%", naCount: 0, naRate: "0.00%", score: "85.99" },
-        { category: "鐪嬬梾鏈熼棿锛屾偍鐨勬姳鎬ㄥ拰涓嶆弧鑳藉惁鍙婃椂寰楀埌鍥炲簲锛�", veryGoodCount: 3019, veryGoodRate: "63.62%", goodCount: 1317, goodRate: "27.76%", normalCount: 294, normalRate: "6.20%", badCount: 115, badRate: "2.42%", naCount: 0, naRate: "0.00%", score: "84.19" },
-        { category: "鍖荤敓鏄惁鐢ㄦ偍鍚緱鎳傜殑鏂瑰紡瑙i噴闂锛�", veryGoodCount: 3402, veryGoodRate: "71.70%", goodCount: 967, goodRate: "20.38%", normalCount: 254, normalRate: "5.35%", badCount: 122, badRate: "2.57%", naCount: 0, naRate: "0.00%", score: "87.07" },
-        { category: "鍖荤敓瀵规偍鏄惁灏婇噸锛�", veryGoodCount: 3495, veryGoodRate: "73.66%", goodCount: 897, goodRate: "18.90%", normalCount: 249, normalRate: "5.25%", badCount: 104, badRate: "2.19%", naCount: 0, naRate: "0.00%", score: "88.01" },
-        { category: "鍖荤敓鏄惁浠旂粏鍊惧惉鎮ㄨ璇濓紵", veryGoodCount: 3422, veryGoodRate: "72.12%", goodCount: 937, goodRate: "19.75%", normalCount: 251, normalRate: "5.29%", badCount: 135, badRate: "2.85%", naCount: 0, naRate: "0.00%", score: "87.05" },
-        { category: "瀵规寕鍙锋湇鍔′汉鍛樻湇鍔℃�佸害鏄惁婊℃剰", veryGoodCount: 3458, veryGoodRate: "72.88%", goodCount: 1205, goodRate: "25.40%", normalCount: 52, normalRate: "1.10%", badCount: 30, badRate: "0.63%", naCount: 0, naRate: "0.00%", score: "90.17" },
-        { category: "鎮ㄥ棰勭害鎸傚彿鏄惁婊℃剰锛�", veryGoodCount: 3527, veryGoodRate: "74.33%", goodCount: 1088, goodRate: "22.93%", normalCount: 72, normalRate: "1.52%", badCount: 58, badRate: "1.22%", naCount: 0, naRate: "0.00%", score: "90.12" },
-        { category: "鎶ゅ+瀵规偍鏄惁灏婇噸锛�", veryGoodCount: 3420, veryGoodRate: "72.08%", goodCount: 1184, goodRate: "24.95%", normalCount: 113, normalRate: "2.38%", badCount: 28, badRate: "0.59%", naCount: 0, naRate: "0.00%", score: "89.50" },
-        { category: "鎶ゅ+鏄惁鐢ㄦ偍鍚緱鎳傜殑鏂瑰紡瑙i噴闂锛�", veryGoodCount: 3346, veryGoodRate: "70.52%", goodCount: 1250, goodRate: "26.34%", normalCount: 122, normalRate: "2.57%", badCount: 27, badRate: "0.57%", naCount: 0, naRate: "0.00%", score: "88.94" },
-        { category: "鎶ゅ+鏄惁浠旂粏鍊惧惉鎮ㄨ璇濓紵", veryGoodCount: 3378, veryGoodRate: "71.19%", goodCount: 1205, goodRate: "25.40%", normalCount: 134, normalRate: "2.82%", badCount: 28, badRate: "0.59%", naCount: 0, naRate: "0.00%", score: "89.06" },
-        { category: "鎬昏", veryGoodCount: 54863, veryGoodRate: "69.02%", goodCount: 20800, goodRate: "26.17%", normalCount: 2920, normalRate: "3.67%", badCount: 908, badRate: "1.14%", naCount: 0, naRate: "0.00%", score: "87.66" },
-      ],
-      questionStats: [
-        {
-          title: "绗�1棰橈細鎮ㄥ鎴戦櫌宸ヤ綔浜哄憳鐨勬湇鍔¤川閲忔弧鎰忎笉婊℃剰锛熴�愬垎鍊煎崟閫夐銆�- 缁煎悎寰楀垎",
-          options: [
-            { option: "闈炲父婊℃剰", count: 1084, rate: "75.66%" },
-            { option: "婊℃剰", count: 1959, rate: "20.92%" },
-            { option: "涓�鑸�", count: 167, rate: "1.78%" },
-            { option: "涓嶆弧鎰�", count: 153, rate: "1.63%" },
-          ],
-          total: 3363
-        },
-        {
-          title: "绗�2棰橈細鎮ㄥ鍝儴鍒嗘湇鍔′笉婊℃剰锛熴�愬閫夐銆�",
-          options: [
-            { option: "鍖荤敓璇婃柇鍜岃瘖鐤�", count: 131, rate: "32.11%" },
-            { option: "鎶ょ悊鏈嶅姟", count: 47, rate: "11.52%" },
-            { option: "鍏朵粬鍖诲姟浜哄憳鎶�鏈按骞�", count: 30, rate: "7.33%" },
-            { option: "宸ヤ汉", count: 72, rate: "17.65%" },
-          ],
-          total: 408
-        },
-        {
-          title: "绗�3棰橈細鍖荤敓璇婃柇鍜屾不鐤椾腑鍝簺鏈嶅姟璐ㄩ噺涓嶆弧鎰忥紵銆愬閫夐銆�",
-          options: [
-            { option: "鍖荤敓璇婃柇閿欒", count: 23, rate: "15.44%" },
-            { option: "瀵规不鐤楁晥鏋滀笉婊℃剰", count: 72, rate: "48.32%" },
-            { option: "鍏朵粬", count: 54, rate: "36.24%" },
-          ],
-          total: 131
-        },
-      ],
-      deptComparison: [
-        { dept: "鍐呭垎娉岀", q1VeryGood: 103, q1Good: 34, q1Normal: 0, q1Bad: 0, q1Score: "377.00", q2VeryGood: 101, q2Good: 35, q2Normal: 1, q2Bad: 0, q2Score: "374.00", totalScore: "75.25" },
-        { dept: "鐨偆绉�", q1VeryGood: 112, q1Good: 25, q1Normal: 2, q1Bad: 3, q1Score: "388.00", q2VeryGood: 111, q2Good: 27, q2Normal: 3, q2Bad: 1, q2Score: "390.00", totalScore: "76.12" },
-        { dept: "鑲跨槫鍐呯", q1VeryGood: 24, q1Good: 12, q1Normal: 2, q1Bad: 0, q1Score: "98.00", q2VeryGood: 25, q2Good: 12, q2Normal: 1, q2Bad: 0, q2Score: "100.00", totalScore: "70.22" },
-        { dept: "娑堝寲鍐呯", q1VeryGood: 180, q1Good: 52, q1Normal: 1, q1Bad: 0, q1Score: "645.00", q2VeryGood: 168, q2Good: 62, q2Normal: 3, q2Bad: 0, q2Score: "631.00", totalScore: "73.56" },
-        { dept: "鑲惧唴绉�", q1VeryGood: 21, q1Good: 4, q1Normal: 0, q1Bad: 0, q1Score: "71.00", q2VeryGood: 19, q2Good: 6, q2Normal: 0, q2Bad: 0, q2Score: "65.00", totalScore: "68.50" },
-      ],
+      // 椤堕儴鍏ㄥ眬绛涢��
+      queryParams: {
+        dateRange: [],
+        deptCodes: [], // 绉戝缂栫爜闆嗗悎 => leaveldeptcodes
+        wardCodes: [], // 鍑洪櫌鐥呭尯缂栫爜闆嗗悎 => leavehospitaldistrictcodes锛堟帴鍙e繀濉級
+        serviceTypeList: [], // 鏈嶅姟绫诲瀷澶氶�夛紝鎻愪氦鏃剁敤閫楀彿鎷兼帴
+        deptOrDistrict: "1", // 绉戝/鐥呭尯缁勫悎鏂瑰紡锛岄粯璁� 1锛氱闄㈢梾鍖� and 绂婚櫌绉戝
+        statMode: "ward", // 缁熻缁村害锛歸ard=鎸夌梾鍖虹粺璁� dept=鎸夌瀹ょ粺璁�
+      },
+      SERVICE_TYPES,
+      loading: false,
+
+      // 琛ㄤ簩锛氶鐩ā绯婃煡璇㈠叧閿瓧锛堢函鍓嶇杩囨护锛屼笉璇锋眰鍚庣锛�
+      questionSearch: "",
+
+      // 琛ㄦ牸涓夛細缁熻缁村害 + 鐙珛绉戝/鐥呭尯鏉′欢锛�'inherit' = 娌跨敤椤堕儴锛�
+      statDimension: "ward",
+      statDeptWard: "inherit",
+
+      // 鎺ュ彛杩斿洖鐨勫師濮嬫暟鎹紙宸插綊涓�鍖栫殑棰樼洰鍒楄〃锛�
+      questionList: [],
+      // 琛ㄤ笁锛氱瀹�/鐥呭尯璇勫垎瀵规瘮锛坮ows=琛屾暟鎹紝questions=鍔ㄦ�佸垪锛�
+      deptComparison: { rows: [], questions: [] },
+      wardComparison: { rows: [], questions: [] },
     };
   },
+  computed: {
+    // 绉戝閫夐」锛氫紭鍏堝彇褰撳墠鐢ㄦ埛鎵�灞炵瀹わ紝鏃犲垯鍏滃簳
+    deptOptions() {
+      const list = this.$store.getters.belongDepts || [];
+      return list.length
+        ? list.map((d) => ({ label: d.deptName, value: d.deptCode }))
+        : DEPTS.map((d) => ({ label: d, value: d }));
+    },
+    // 鐥呭尯閫夐」锛氫紭鍏堝彇褰撳墠鐢ㄦ埛鎵�灞炵梾鍖猴紝鏃犲垯鍏滃簳
+    wardOptions() {
+      const list = this.$store.getters.belongWards || [];
+      return list.length
+        ? list.map((w) => ({ label: w.districtName, value: w.districtCode }))
+        : WARDS.map((w) => ({ label: w, value: w }));
+    },
+    // 鏈嶅姟绫诲瀷涓嬫媺锛氬彇 store 鐨勯殢璁跨被鍨嬪瓧鍏� tasktypes锛屽彇涓嶅埌鏃跺厹搴曟湰鍦� SERVICE_TYPES
+    serviceTypeOptions() {
+      const toOptions = (list) =>
+        (Array.isArray(list) ? list : [])
+          .map((i) => ({
+            label: String(i.label || "").trim(),
+            value: String(i.value),
+          }))
+          .filter((i) => i.label && i.value && i.value !== "undefined");
+      const taskTypes = toOptions(this.$store.getters.tasktypes);
+      return taskTypes.length ? taskTypes : SERVICE_TYPES;
+    },
+    // 鏈�夌梾鍖烘椂鐨勯粯璁ゆ煡璇㈣寖鍥达細鍏ㄩ儴鐥呭尯缂栫爜
+    defaultWardCodes() {
+      return Array.from(new Set(this.wardOptions.map((w) => w.value)));
+    },
+    // 鏈�夌瀹ゆ椂鐨勯粯璁ゆ煡璇㈣寖鍥达細鍏ㄩ儴绉戝缂栫爜
+    defaultDeptCodes() {
+      return Array.from(new Set(this.deptOptions.map((d) => d.value)));
+    },
+    // 鍥哄畾鍏」閫夐」鐨勯鐩� => 琛ㄤ竴
+    satisfactionQuestions() {
+      return this.questionList.filter((q) => this.isSatisfactionQuestion(q));
+    },
+    // 鍏朵綑棰樼洰 => 琛ㄤ簩
+    otherQuestions() {
+      return this.questionList.filter((q) => !this.isSatisfactionQuestion(q));
+    },
+    // 琛ㄤ竴锛氭弧鎰忓害缁熻锛堝惈鎬昏琛岋級
+    serviceSatisfaction() {
+      // 鍙栨寚瀹氶�夐」鐨� { count, score }锛屾湭杩斿洖鍒欒椤逛负 0
+      const pick = (q, name) => {
+        const hit = q.options.find((o) => o.option === name);
+        return hit
+          ? { count: Number(hit.count) || 0, score: hit.score }
+          : { count: 0, score: null };
+      };
+      const NAMES = FIXED_OPTIONS;
+
+      const rows = this.satisfactionQuestions.map((q) => {
+        const veryGood = pick(q, "寰堟弧鎰�");
+        const good = pick(q, "婊℃剰");
+        const normal = pick(q, "涓�鑸�");
+        const bad = pick(q, "涓嶆弧鎰�");
+        const veryBad = pick(q, "寰堜笉婊℃剰");
+        const na = pick(q, "鏈粡鍘�");
+        const items = [veryGood, good, normal, bad, veryBad, na];
+        const total = items.reduce((s, it) => s + it.count, 0);
+        return {
+          category: q.title,
+          veryGoodCount: veryGood.count,
+          veryGoodRate: this.rateOf(veryGood.count, total),
+          goodCount: good.count,
+          goodRate: this.rateOf(good.count, total),
+          normalCount: normal.count,
+          normalRate: this.rateOf(normal.count, total),
+          badCount: bad.count,
+          badRate: this.rateOf(bad.count, total),
+          veryBadCount: veryBad.count,
+          veryBadRate: this.rateOf(veryBad.count, total),
+          naCount: na.count,
+          naRate: this.rateOf(na.count, total),
+          score: this.scoreOfItems(items),
+        };
+      });
+
+      // 鎬昏琛岋細鍚勯鍚岄�夐」鏁伴噺绱姞锛屽垎鍊兼部鐢ㄨ閫夐」鍒嗗��
+      const sum = NAMES.map(() => ({ count: 0, score: null }));
+      this.satisfactionQuestions.forEach((q) => {
+        NAMES.forEach((name, i) => {
+          const it = pick(q, name);
+          sum[i].count += it.count;
+          if (sum[i].score == null) sum[i].score = it.score;
+        });
+      });
+      const total = sum.reduce((s, it) => s + it.count, 0);
+      rows.push({
+        category: "鎬昏",
+        veryGoodCount: sum[0].count,
+        veryGoodRate: this.rateOf(sum[0].count, total),
+        goodCount: sum[1].count,
+        goodRate: this.rateOf(sum[1].count, total),
+        normalCount: sum[2].count,
+        normalRate: this.rateOf(sum[2].count, total),
+        badCount: sum[3].count,
+        badRate: this.rateOf(sum[3].count, total),
+        veryBadCount: sum[4].count,
+        veryBadRate: this.rateOf(sum[4].count, total),
+        naCount: sum[5].count,
+        naRate: this.rateOf(sum[5].count, total),
+        score: this.scoreOfItems(sum),
+      });
+      return rows;
+    },
+    // 琛ㄤ簩锛氬崟閫�/澶氶�夌粺璁�
+    questionStats() {
+      return this.otherQuestions.map((q) => {
+        const total = this.questionTotal(q);
+        return {
+          title: q.title,
+          total,
+          options: q.options.map((o) => ({
+            option: o.option,
+            count: o.count,
+            rate: this.rateOf(o.count, total),
+          })),
+        };
+      });
+    },
+    // 琛ㄤ簩锛氭寜棰樼洰妯$硦鏌ヨ锛堢函鍓嶇杩囨护锛屼笉璇锋眰鍚庣锛�
+    filteredQuestionStats() {
+      const kw = (this.questionSearch || "").trim().toLowerCase();
+      if (!kw) return this.questionStats;
+      return this.questionStats.filter((q) =>
+        (q.title || "").toLowerCase().indexOf(kw) > -1
+      );
+    },
+    // 琛ㄤ笁锛氭寜缁村害 + 鐙珛鏉′欢杩囨护鍚庣殑琛�
+    table3Rows() {
+      const comp =
+        this.statDimension === "dept"
+          ? this.deptComparison
+          : this.wardComparison;
+      const list = (comp && comp.rows) || [];
+      const names = this.statFilterNames();
+      if (!names || !names.length) return list;
+      return list.filter((r) => names.indexOf(r.name) > -1);
+    },
+    // 琛ㄤ笁锛氬綋鍓嶇淮搴︿笅鐨勯鐩垪锛堝姩鎬佸垪锛�
+    table3Questions() {
+      const comp =
+        this.statDimension === "dept"
+          ? this.deptComparison
+          : this.wardComparison;
+      return (comp && comp.questions) || [];
+    },
+  },
+  created() {
+    if (!this.queryParams.dateRange || !this.queryParams.dateRange.length) {
+      this.queryParams.dateRange = this.defaultRange();
+    }
+    this.queryParams.serviceTypeList = this.defaultServiceTypes();
+  },
+  mounted() {
+    this.fetchData();
+  },
   methods: {
+    // 榛樿閫変腑鐨勬湇鍔$被鍨嬶紙鎸� label 鍖归厤锛屽彇涓嶅埌灏辫繑鍥炵┖鏁扮粍 = 涓嶉檺锛�
+    defaultServiceTypes() {
+      const hit = DEFAULT_SERVICE_LABELS.map((label) => {
+        const item = this.serviceTypeOptions.find((o) => o.label === label);
+        return item ? item.value : null;
+      }).filter((v) => v != null);
+      return Array.from(new Set(hit));
+    },
+    // 琛ㄤ笁鐙珛鏉′欢锛�'inherit' 鏃舵部鐢ㄩ《閮ㄦ墍閫夛紱杩斿洖閫変腑鐨勫悕绉版暟缁勶紙鎺ュ彛鍙洖鍚嶇О锛夛紝绌烘暟缁勮〃绀轰笉闄�
+    statFilterNames() {
+      const opts =
+        this.statDimension === "dept" ? this.deptOptions : this.wardOptions;
+      const nameOf = (code) => {
+        const hit = opts.find((o) => String(o.value) === String(code));
+        return hit ? hit.label : null;
+      };
+      if (this.statDeptWard === "inherit") {
+        const codes =
+          this.statDimension === "dept"
+            ? this.queryParams.deptCodes
+            : this.queryParams.wardCodes;
+        return (codes || []).map(nameOf).filter(Boolean);
+      }
+      // 鍒囨崲缁村害鍚庯紝鑻ュ凡閫夋潯浠朵笉灞炰簬褰撳墠缁村害鍒欒涓轰笉闄�
+      if (this.statDeptWard.startsWith(this.statDimension + ":")) {
+        const name = nameOf(this.statDeptWard.slice(this.statDimension.length + 1));
+        return name ? [name] : [];
+      }
+      return [];
+    },
+    onStatDimensionChange() {
+      // 鍒囨崲缁村害鍚庯紝鑻ュ綋鍓嶅凡閫夋潯浠朵笉灞炰簬鏂扮淮搴︼紝鍒欐仮澶嶁�滄部鐢ㄩ《閮ㄢ��
+      if (
+        this.statDeptWard !== "inherit" &&
+        !this.statDeptWard.startsWith(this.statDimension + ":")
+      ) {
+        this.statDeptWard = "inherit";
+      }
+    },
+    handleReset() {
+      this.queryParams.dateRange = this.defaultRange();
+      this.queryParams.deptCodes = [];
+      this.queryParams.wardCodes = [];
+      this.queryParams.statMode = "ward";
+      this.statDimension = "ward";
+      this.statDeptWard = "inherit";
+      this.queryParams.serviceTypeList = this.defaultServiceTypes();
+      this.fetchData();
+    },
+    // 鍒囨崲缁熻缁村害锛堢瀹�/鐥呭尯锛夋椂娓呯┖鍙︿竴缁村害鐨勯�夋嫨锛屼繚璇佷簰鏂�
+    onStatModeChange() {
+      if (this.queryParams.statMode === "dept") {
+        this.queryParams.wardCodes = [];
+      } else {
+        this.queryParams.deptCodes = [];
+      }
+      // 鍗曞悜鍚屾鍒般�屽悇绉戝/鐥呭尯婊℃剰搴﹁瘎鍒嗗姣斻�嶈〃锛氶《閮� 鈫� 琛ㄤ笁
+      this.statDimension = this.queryParams.statMode;
+      this.onStatDimensionChange();
+    },
+    // 鍒ゆ柇棰樼洰鏄惁涓烘弧鎰忓害鍏」閫夐」棰橈細鎵�鏈夐�夐」閮借惤鍦ㄥ浐瀹氬叚椤瑰唴鍗冲綊琛ㄤ竴
+    isSatisfactionQuestion(q) {
+      if (!q.options || !q.options.length) return false;
+      return q.options.every((o) => FIXED_OPTIONS.includes(o.option));
+    },
+    questionTotal(q) {
+      if (q.total != null) return q.total;
+      return q.options.reduce((s, o) => s + o.count, 0);
+    },
+    rateOf(count, total) {
+      if (!total) return "0.00%";
+      return ((count / total) * 100).toFixed(2) + "%";
+    },
+    /**
+     * 缁煎悎寰楀垎锛堢湡瀹炲彛寰勶級锛毼�(鍚勯�夐」鍒嗗�� 脳 濉姤閲�) / 危(濉姤閲�)
+     * items: [{ count, score }]锛宻core 鍙栨帴鍙h繑鍥炵殑 score 瀛楁
+     * 鏃犲垎鍊奸�夐」锛堝"鏈粡鍘�"锛夐粯璁や笉璁″叆鍒嗗瓙涓庡垎姣嶏紝閬垮厤鎷変綆寰楀垎
+     */
+    scoreOfItems(items) {
+      let numerator = 0;
+      let denominator = 0;
+      items.forEach((it) => {
+        const count = Number(it && it.count) || 0;
+        const score = Number(it && it.score);
+        const hasScore = !isNaN(score);
+        if (!hasScore && !NO_SCORE_IN_DENOMINATOR) return;
+        numerator += (hasScore ? score : 0) * count;
+        denominator += count;
+      });
+      if (!denominator) return "0.00";
+      return (numerator / denominator).toFixed(2);
+    },
+    defaultRange() {
+      const end = new Date();
+      const start = new Date();
+      start.setDate(start.getDate() - 91);
+      const f = (d) => {
+        const y = d.getFullYear();
+        const m = String(d.getMonth() + 1).padStart(2, "0");
+        const day = String(d.getDate()).padStart(2, "0");
+        return `${y}-${m}-${day}`;
+      };
+      return [f(start), f(end)];
+    },
+    // 鎻愮ず淇℃伅锛堝吋瀹� $modal 涓� $message锛�
+    notify(msg, type) {
+      const map = {
+        warning: ["msgWarning", "warning"],
+        error: ["msgError", "error"],
+        success: ["msgSuccess", "success"],
+      };
+      const [modalFn, msgFn] = map[type] || map.success;
+      if (this.$modal && this.$modal[modalFn]) {
+        this.$modal[modalFn](msg);
+      } else if (this.$message && this.$message[msgFn]) {
+        this.$message[msgFn](msg);
+      }
+    },
+    // 鎷夊彇婊℃剰搴︾粺璁℃暟鎹紙鎺ュ彛锛�/smartor/serviceSubtaskDetail/sltdMydTotalByScore锛�
+    async fetchData() {
+      const range = this.queryParams.dateRange || [];
+      if (range.length !== 2 || !range[0] || !range[1]) {
+        this.notify("璇峰厛閫夋嫨鍑洪櫌鏃ユ湡鑼冨洿", "warning");
+        return;
+      }
+
+      // 鐥呭尯 => leavehospitaldistrictcodes锛岀瀹� => leaveldeptcodes
+      // 绉戝/鐥呭尯浜掓枼锛氭寜鐥呭尯鍙紶 leavehospitaldistrictcodes锛屾寜绉戝鍙紶 leaveldeptcodes
+      const wards = this.queryParams.wardCodes || [];
+      const depts = this.queryParams.deptCodes || [];
+
+      const payload = {
+        deptOrDistrict: this.queryParams.deptOrDistrict || "1",
+        startFinishTime: range[0],
+        endFinishTime: range[1],
+      };
+      if (this.queryParams.statMode === "dept") {
+        payload.leaveldeptcodes = depts.length
+          ? Array.from(new Set(depts))
+          : this.defaultDeptCodes;
+      } else {
+        payload.leavehospitaldistrictcodes = wards.length
+          ? Array.from(new Set(wards))
+          : this.defaultWardCodes;
+      }
+      // 鏈嶅姟绫诲瀷澶氶�夛細閫楀彿鍒嗛殧锛堟帴鍙e瓧娈典负 serviceType 瀛楃涓诧級
+      const serviceTypes = this.queryParams.serviceTypeList || [];
+      if (serviceTypes.length) payload.serviceType = serviceTypes.join(",");
+      payload.serviceTypeList = serviceTypes;
+      this.loading = true;
+      try {
+        const res = await sltdMydTotalByScore(payload);
+        // 鎺掓煡鐢細鎵撳嵃鎺ュ彛鍘熷杩斿洖锛屼究浜庢牳瀵瑰瓧娈靛悕锛堢‘璁ゆ棤璇悗鍙垹闄わ級
+        console.log(
+          "[sltdMydTotalByScore] code=",
+          res && res.code,
+          "data=",
+          JSON.stringify(res && res.data).slice(0, 2000)
+        );
+        if (res && res.code === 200) {
+          this.questionList = normalizeQuestions(res.data);
+          if (!this.questionList.length)
+            this.notify("褰撳墠鏉′欢涓嬫殏鏃犵粺璁℃暟鎹�", "warning");
+        } else {
+          this.questionList = [];
+          this.notify((res && res.msg) || "缁熻鏁版嵁鑾峰彇澶辫触", "error");
+        }
+        // 琛ㄤ笁锛氬悇绉戝/鐥呭尯婊℃剰搴﹁瘎鍒嗗姣旓紙鐪熷疄鎺ュ彛锛屽唴閮ㄨ嚜琛屽鐞嗛敊璇級
+        await this.fetchComparison();
+      } catch (e) {
+        this.questionList = [];
+        this.notify("缁熻鏁版嵁璇锋眰澶辫触锛岃绋嶅悗閲嶈瘯", "error");
+      } finally {
+        this.loading = false;
+      }
+    },
+    // 鎷夊彇鍚勭瀹�/鐥呭尯婊℃剰搴﹁瘎鍒嗗姣旓紙鎺ュ彛锛�/smartor/serviceSubtaskDetail/statQuestionOptionByKsOrBq锛�
+    async fetchComparison() {
+      const range = this.queryParams.dateRange || [];
+      if (range.length !== 2 || !range[0] || !range[1]) {
+        this.deptComparison = { rows: [], questions: [] };
+        this.wardComparison = { rows: [], questions: [] };
+        return;
+      }
+      const base = {
+        deptOrDistrict: this.queryParams.deptOrDistrict || "1",
+        startFinishTime: range[0],
+        endFinishTime: range[1],
+      };
+      try {
+        // 鎸夌瀹ょ粺璁★紙leaveldeptcodes 浼樺厛锛変笌鎸夌梾鍖虹粺璁★紙leavehospitaldistrictcodes锛夊悇鏌ヤ竴娆�
+        const [deptRes, wardRes] = await Promise.all([
+          statQuestionOptionByKsOrBq({ ...base, leaveldeptcodes: this.defaultDeptCodes }),
+          statQuestionOptionByKsOrBq({ ...base, leavehospitaldistrictcodes: this.defaultWardCodes }),
+        ]);
+        // 鎺掓煡鐢細鎵撳嵃鎺ュ彛鍘熷杩斿洖锛屼究浜庢牳瀵逛袱绾у祵濂� Map 缁撴瀯锛堢‘璁ゆ棤璇悗鍙垹闄わ級
+        console.log(
+          "[statQuestionOptionByKsOrBq] dept code=",
+          deptRes && deptRes.code,
+          "data=",
+          JSON.stringify(deptRes && deptRes.data).slice(0, 2000),
+          "| ward code=",
+          wardRes && wardRes.code,
+          "data=",
+          JSON.stringify(wardRes && wardRes.data).slice(0, 2000)
+        );
+        this.deptComparison = this.buildComparison(
+          deptRes && deptRes.code === 200 ? deptRes.data : {},
+          "dept"
+        );
+        this.wardComparison = this.buildComparison(
+          wardRes && wardRes.code === 200 ? wardRes.data : {},
+          "ward"
+        );
+      } catch (e) {
+        this.deptComparison = { rows: [], questions: [] };
+        this.wardComparison = { rows: [], questions: [] };
+        console.error("鑾峰彇绉戝/鐥呭尯璇勫垎瀵规瘮澶辫触:", e);
+      }
+    },
+    // 缁勮涓鸿〃涓夋墍闇�鐨勮/鍒楃粨鏋勶細琛�=绉戝/鐥呭尯锛屽垪=棰樼洰脳閫夐」 + 寰楀垎 + 缁煎悎寰楀垎
+    // 鏂版帴鍙h繑鍥炰袱绾у祵濂� Map锛歿 绉戝/鐥呭尯鍚嶇О: { 棰樼洰鍐呭: [QuestionCountKsOrBqDTO] } }
+    // 鍏煎鏃ф墎骞虫暟缁勶細[{ questionText, deptname, leavehospitaldistrictname, targetvalue, answerCount, singleScore }]
+    buildComparison(data, mode) {
+      const questions = [];
+      const qMap = new Map();
+      const rowMap = new Map();
+
+      // 灏嗕竴鏉� (鍚嶇О, 棰樼洰, 閫夐」, 鏁伴噺, 寰楀垎) 鏄庣粏绱姞杩涜/鍒楃粨鏋�
+      const collect = (name, title, option, count, single) => {
+        if (!title || !name) return;
+        let q = qMap.get(title);
+        if (!q) {
+          q = { title, options: [] };
+          qMap.set(title, q);
+          questions.push(q);
+        }
+        if (option && q.options.indexOf(option) === -1) q.options.push(option);
+
+        let row = rowMap.get(name);
+        if (!row) {
+          row = { name, code: this.codeFor(mode, name), cells: {} };
+          rowMap.set(name, row);
+        }
+        if (!row.cells[title]) row.cells[title] = {};
+        const cell = row.cells[title][option] || { count: 0, score: 0 };
+        cell.count += count;
+        cell.score += single;
+        row.cells[title][option] = cell;
+      };
+
+      if (Array.isArray(data)) {
+        // 鏃х粨鏋勶細鎵佸钩鏁扮粍
+        data.forEach((it) => {
+          const title = it.questionText || it.qeustionText || "";
+          const name =
+            mode === "dept"
+              ? it.deptname || ""
+              : it.leavehospitaldistrictname || "";
+          const option = it.targetvalue || it.asrtext || "";
+          collect(name, title, option, Number(it.answerCount) || 0, Number(it.singleScore) || 0);
+        });
+      } else if (data && typeof data === "object") {
+        // 鏂扮粨鏋勶細涓ょ骇宓屽 Map锛岀涓�灞� key=绉戝/鐥呭尯鍚嶇О锛岀浜屽眰 key=棰樼洰锛寁alue=閫夐」鏄庣粏鍒楄〃
+        Object.keys(data).forEach((name) => {
+          const questionMap = data[name] || {};
+          Object.keys(questionMap).forEach((title) => {
+            const list = Array.isArray(questionMap[title]) ? questionMap[title] : [];
+            list.forEach((it) => {
+              const option = it.targetvalue || it.asrtext || "";
+              collect(name, title, option, Number(it.answerCount) || 0, Number(it.singleScore) || 0);
+            });
+          });
+        });
+      }
+
+      const rows = Array.from(rowMap.values());
+      rows.forEach((row) => {
+        let totalCount = 0;
+        let totalScore = 0;
+        questions.forEach((q) => {
+          const cells = row.cells[q.title] || {};
+          let qCount = 0;
+          let qScore = 0;
+          q.options.forEach((opt) => {
+            const c = cells[opt];
+            if (c) {
+              qCount += c.count;
+              qScore += c.score;
+            }
+          });
+          // 璇ラ鐩姞鏉冨钩鍧囧垎锛毼� singleScore / 危 answerCount
+          cells._score = qCount ? (qScore / qCount).toFixed(2) : "0.00";
+          totalCount += qCount;
+          totalScore += qScore;
+          row.cells[q.title] = cells;
+        });
+        row.totalScore = totalCount ? (totalScore / totalCount).toFixed(2) : "0.00";
+      });
+
+      return { rows, questions };
+    },
+    // 鍚嶇О 鈫� 缂栫爜锛堢敤浜庤〃涓夋寜椤堕儴鎵�閫夌紪鐮佽繃婊わ級
+    codeFor(mode, name) {
+      const opts = mode === "dept" ? this.deptOptions : this.wardOptions;
+      const hit = opts.find((o) => String(o.label) === String(name));
+      return hit ? hit.value : name;
+    },
     exportTable(tableId) {
-      const tableNames = { table1: '鍚勬湇鍔¢」鐩弧鎰忓害缁熻', table2: '鍗曢�夐澶氶�夐缁熻', table3: '鍚勭瀹ゆ弧鎰忓害璇勫垎瀵规瘮' };
-      const name = tableNames[tableId] || '缁熻鏁版嵁';
+      const tableNames = {
+        table1: "鍚勬湇鍔¢」鐩弧鎰忓害缁熻",
+        table2: "鍗曢�夐澶氶�夐缁熻",
+        table3: "鍚勭瀹ゆ弧鎰忓害璇勫垎瀵规瘮",
+      };
+      const name = tableNames[tableId] || "缁熻鏁版嵁";
       const table = document.getElementById(tableId);
       if (!table) return;
 
@@ -152,10 +1002,10 @@
         // 浠庤〃鏍兼彁鍙栨暟鎹瀯寤哄甫鏍峰紡鐨� Excel
         const wb = XLSX.utils.book_new();
         const data = [];
-        table.querySelectorAll('tr').forEach(tr => {
+        table.querySelectorAll("tr").forEach((tr) => {
           const row = [];
-          tr.querySelectorAll('th,td').forEach(td => {
-            row.push((td.innerText || '').trim());
+          tr.querySelectorAll("th,td").forEach((td) => {
+            row.push((td.innerText || "").trim());
           });
           if (row.length) data.push(row);
         });
@@ -163,21 +1013,25 @@
         const ws = XLSX.utils.aoa_to_sheet(data);
         // 璁剧疆鍒楀
         const colWidths = data[0] ? data[0].map(() => ({ wch: 16 })) : [];
-        ws['!cols'] = colWidths;
+        ws["!cols"] = colWidths;
 
-        XLSX.utils.book_append_sheet(wb, ws, 'Sheet1');
-        XLSX.writeFile(wb, name + '.xlsx');
+        XLSX.utils.book_append_sheet(wb, ws, "Sheet1");
+        XLSX.writeFile(wb, name + ".xlsx");
       } catch (e) {
         // fallback: CSV
-        let csv = '锘�';
-        table.querySelectorAll('tr').forEach(tr => {
+        let csv = "锘�";
+        table.querySelectorAll("tr").forEach((tr) => {
           const row = [];
-          tr.querySelectorAll('th,td').forEach(td => row.push('"' + (td.innerText || '').replace(/"/g, '""') + '"'));
-          csv += row.join(',') + '\n';
+          tr.querySelectorAll("th,td").forEach((td) =>
+            row.push('"' + (td.innerText || "").replace(/"/g, '""') + '"')
+          );
+          csv += row.join(",") + "\n";
         });
-        const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
-        const link = document.createElement('a');
-        link.href = URL.createObjectURL(blob); link.download = name + '.csv'; link.click();
+        const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
+        const link = document.createElement("a");
+        link.href = URL.createObjectURL(blob);
+        link.download = name + ".csv";
+        link.click();
       }
     },
   },
@@ -185,11 +1039,66 @@
 </script>
 
 <style lang="scss" scoped>
-.data-overview { .overview-card { margin-bottom: 20px; } }
-.card-header { display: flex; justify-content: space-between; align-items: center; }
-.card-title { font-size: 16px; font-weight: 600; color: #303133; }
-.question-block { margin-bottom: 24px;
-  .q-title { margin: 0 0 12px 0; font-size: 14px; color: #606266; }
-  .q-footer { margin-top: 8px; font-size: 13px; color: #909399; text-align: right; }
+.data-overview {
+  .overview-card {
+    margin-bottom: 20px;
+  }
+}
+.filter-card {
+  .filter-form {
+    margin-bottom: -10px;
+  }
+  .filter-select {
+    width: 220px;
+  }
+  .filter-select-sm {
+    width: 150px;
+  }
+}
+.empty-tip {
+  padding: 40px 0;
+  text-align: center;
+  color: #909399;
+  font-size: 13px;
+}
+.table2-scroll {
+  max-height: 560px;
+  overflow-y: auto;
+}
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+.card-title {
+  font-size: 16px;
+  font-weight: 600;
+  color: #303133;
+}
+.header-right {
+  display: flex;
+  align-items: center;
+  .stat-select {
+    width: 180px;
+    margin: 0 12px;
+  }
+  .search-input {
+    width: 200px;
+    margin-right: 12px;
+  }
+}
+.question-block {
+  margin-bottom: 24px;
+  .q-title {
+    margin: 0 0 12px 0;
+    font-size: 14px;
+    color: #606266;
+  }
+  .q-footer {
+    margin-top: 8px;
+    font-size: 13px;
+    color: #909399;
+    text-align: right;
+  }
 }
 </style>

--
Gitblit v1.9.3