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 | 1170 ++++++++++++++++++++++++++++++++++++++++++++--------------
 1 files changed, 883 insertions(+), 287 deletions(-)

diff --git a/src/views/Satisfaction/sfstatistics/components/DataOverview.vue b/src/views/Satisfaction/sfstatistics/components/DataOverview.vue
index 1987125..2cfa325 100644
--- a/src/views/Satisfaction/sfstatistics/components/DataOverview.vue
+++ b/src/views/Satisfaction/sfstatistics/components/DataOverview.vue
@@ -3,7 +3,7 @@
     <!-- 椤堕儴绛涢�夛細鏃堕棿鑼冨洿 + 绉戝/鐥呭尯锛屾帶鍒舵湰椤垫墍鏈夋暟鎹� -->
     <el-card shadow="never" class="overview-card filter-card">
       <el-form inline class="filter-form">
-        <el-form-item label="鏃堕棿鑼冨洿">
+        <el-form-item label="鍑洪櫌鏃ユ湡">
           <el-date-picker
             v-model="queryParams.dateRange"
             type="daterange"
@@ -14,49 +14,117 @@
             size="small"
           />
         </el-form-item>
-        <el-form-item label="绉戝/鐥呭尯">
+        <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.deptWard"
+            v-model="queryParams.deptCodes"
+            multiple
+            collapse-tags
             clearable
             filterable
-            placeholder="鍏ㄩ儴"
+            placeholder="鍏ㄩ儴绉戝"
             size="small"
             class="filter-select"
           >
-            <el-option-group label="绉戝">
-              <el-option
-                v-for="d in deptOptions"
-                :key="'dept:' + d"
-                :label="d"
-                :value="'dept:' + d"
-              />
-            </el-option-group>
-            <el-option-group label="鐥呭尯">
-              <el-option
-                v-for="w in wardOptions"
-                :key="'ward:' + w"
-                :label="w"
-                :value="'ward:' + w"
-              />
-            </el-option-group>
+            <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" @click="fetchData">鏌ヨ</el-button>
-          <el-button size="small" icon="el-icon-refresh" @click="handleReset">閲嶇疆</el-button>
+          <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>
@@ -72,11 +140,21 @@
           <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>
 
@@ -84,17 +162,59 @@
     <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 id="table2">
-        <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>
@@ -104,149 +224,336 @@
       <div slot="header" class="card-header">
         <span class="card-title">鍚勭瀹�/鐥呭尯婊℃剰搴﹁瘎鍒嗗姣�</span>
         <div class="header-right">
-          <el-radio-group v-model="statDimension" size="small" @change="onStatDimensionChange">
+          <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-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" :label="d" :value="'dept:' + d" />
+              <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" :label="w" :value="'ward:' + w" />
+              <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>
+          <el-button
+            type="warning"
+            size="small"
+            icon="el-icon-download"
+            @click="exportTable('table3')"
+            >瀵煎嚭Excel</el-button
+          >
         </div>
       </div>
-      <el-table id="table3" :data="table3Rows" border size="small" :header-cell-style="{ background: '#f5f7fa', color: '#303133' }">
-        <el-table-column prop="name" :label="statDimension === 'dept' ? '绉戝' : '鐥呭尯'" 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 = ['闈炲父婊℃剰', '婊℃剰', '涓�鑸�', '涓嶆弧鎰�', '涓嶉�傜敤'];
-const DEPTS = ['鍐呭垎娉岀', '鐨偆绉�', '鑲跨槫鍐呯', '娑堝寲鍐呯', '鑲惧唴绉�'];
-const WARDS = ['涓�鐥呭尯', '浜岀梾鍖�', '涓夌梾鍖�', '鍥涚梾鍖�', '浜旂梾鍖�'];
+// 婊℃剰搴﹂鐨勫浐瀹氶�夐」锛堥『搴忓嵆琛ㄦ牸鍒楅『搴忥級鈥斺�旇繖浜涢�夐」鐨勯鐩繘琛ㄤ竴锛屽叾浣欒繘琛ㄤ簩
+const FIXED_OPTIONS = ["寰堟弧鎰�", "婊℃剰", "涓�鑸�", "涓嶆弧鎰�", "寰堜笉婊℃剰", "鏈粡鍘�"];
+// 鏃犲垎鍊奸�夐」锛堝"鏈粡鍘�"锛夋槸鍚﹁鍏ョ患鍚堝緱鍒嗗垎姣嶏細榛樿 false锛岄伩鍏嶆媺浣庡緱鍒�
+const NO_SCORE_IN_DENOMINATOR = false;
 
-// 婊℃剰搴﹂锛堝浐瀹氫簲椤归�夐」锛宑ounts 椤哄簭瀵瑰簲 FIXED_OPTIONS锛�
-const SATISFACTION_BASE = [
-  ['璇婄枟杩囩▼涓紝鍖诲姟浜哄憳鏄惁娉ㄦ剰淇濇姢鎮ㄧ殑闅愮锛�', [3219, 1351, 142, 33, 0]],
-  ['鎮ㄥ鍖婚櫌鏈嶅姟璁炬柦鐨勬�讳綋鍗拌薄濡備綍锛�', [3110, 1369, 217, 49, 0]],
-  ['鍖婚櫌鐨勫帟鎵�鏄惁娓呮磥鏃犲紓鍛筹紵', [2973, 1504, 235, 33, 0]],
-  ['鍖婚櫌鍐呯殑璺爣鍜屾寚绀烘槸鍚︽槑纭紵', [3234, 1289, 196, 26, 0]],
-  ['鍖婚櫌鐨勭┖闂村竷灞�鏄惁渚垮埄锛�', [3037, 1460, 209, 39, 0]],
-  ['鐪嬬梾鏈熼棿锛屾偍鐨勬姳鎬ㄥ拰涓嶆弧鑳藉惁鍙婃椂寰楀埌鍥炲簲锛�', [3019, 1317, 294, 115, 0]],
-  ['鍖荤敓鏄惁鐢ㄦ偍鍚緱鎳傜殑鏂瑰紡瑙i噴闂锛�', [3402, 967, 254, 122, 0]],
-  ['鍖荤敓瀵规偍鏄惁灏婇噸锛�', [3495, 897, 249, 104, 0]],
-  ['鍖荤敓鏄惁浠旂粏鍊惧惉鎮ㄨ璇濓紵', [3422, 937, 251, 135, 0]],
-  ['瀵规寕鍙锋湇鍔′汉鍛樻湇鍔℃�佸害鏄惁婊℃剰', [3458, 1205, 52, 30, 0]],
-  ['鎮ㄥ棰勭害鎸傚彿鏄惁婊℃剰锛�', [3527, 1088, 72, 58, 0]],
-  ['鎶ゅ+瀵规偍鏄惁灏婇噸锛�', [3420, 1184, 113, 28, 0]],
-  ['鎶ゅ+鏄惁鐢ㄦ偍鍚緱鎳傜殑鏂瑰紡瑙i噴闂锛�', [3346, 1250, 122, 27, 0]],
-  ['鎶ゅ+鏄惁浠旂粏鍊惧惉鎮ㄨ璇濓紵', [3378, 1205, 134, 28, 0]],
+// 鏈嶅姟绫诲瀷瀛楀吀锛堟帴鍙� 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: "涓撶闅忚" },
 ];
 
-// 鍏朵綑棰樼洰锛堝崟閫�/澶氶�夛紝闈炲浐瀹氭弧鎰忓害閫夐」锛�
-const OTHER_BASE = [
-  {
-    title: '鎮ㄥ鎴戦櫌宸ヤ綔浜哄憳鐨勬湇鍔¤川閲忔弧鎰忎笉婊℃剰锛熴�愬垎鍊煎崟閫夐銆�',
-    type: 'single',
-    options: [['闈炲父婊℃剰', 1084], ['婊℃剰', 1959], ['涓�鑸�', 167], ['涓嶆弧鎰�', 153]],
-  },
-  {
-    title: '鎮ㄥ鍝儴鍒嗘湇鍔′笉婊℃剰锛熴�愬閫夐銆�',
-    type: 'multiple',
-    total: 408,
-    options: [['鍖荤敓璇婃柇鍜岃瘖鐤�', 131], ['鎶ょ悊鏈嶅姟', 47], ['鍏朵粬鍖诲姟浜哄憳鎶�鏈按骞�', 30], ['宸ヤ汉', 72]],
-  },
-  {
-    title: '鍖荤敓璇婃柇鍜屾不鐤椾腑鍝簺鏈嶅姟璐ㄩ噺涓嶆弧鎰忥紵銆愬閫夐銆�',
-    type: 'multiple',
-    total: 131,
-    options: [['鍖荤敓璇婃柇閿欒', 23], ['瀵规不鐤楁晥鏋滀笉婊℃剰', 72], ['鍏朵粬', 54]],
-  },
-];
+// 鏈嶅姟绫诲瀷榛樿閫変腑椤癸紙鎸� label 鍖归厤锛岄�傞厤涓嶅悓瀛楀吀鐨� value 宸紓锛�
+const DEFAULT_SERVICE_LABELS = ["浣忛櫌婊℃剰搴�", "闂ㄨ瘖婊℃剰搴�"];
 
-// 缁熶竴鎺ュ彛杩斿洖鐨勯鐩紙妯℃嫙锛夛細鍥哄畾浜旈」閫夐」 => 琛ㄤ竴锛屽叾浣� => 琛ㄤ簩
-const RAW_QUESTIONS = [
-  ...SATISFACTION_BASE.map(([title, counts]) => ({
-    title,
-    type: 'single',
-    options: FIXED_OPTIONS.map((opt, i) => ({ option: opt, count: counts[i] })),
-  })),
-  ...OTHER_BASE.map((q) => ({
-    title: q.title,
-    type: q.type,
-    total: q.total,
-    options: q.options.map(([option, count]) => ({ option, count })),
-  })),
-];
+// 鍏滃簳锛歴tore 涓棤绉戝/鐥呭尯鏁版嵁鏃朵娇鐢紙琛ㄤ笁瀵规瘮 + 绛涢�変笅鎷夛級
+const DEPTS = ["鍐呭垎娉岀", "鐨偆绉�", "鑲跨槫鍐呯", "娑堝寲鍐呯", "鑲惧唴绉�"];
+const WARDS = ["涓�鐥呭尯", "浜岀梾鍖�", "涓夌梾鍖�", "鍥涚梾鍖�", "浜旂梾鍖�"];
 
-// 纭畾鎬т吉闅忔満锛堜究浜庣瓫閫夋潯浠跺浐瀹氭椂缁撴灉绋冲畾锛�
-function hashString(str) {
-  let h = 2166136261;
-  for (let i = 0; i < str.length; i++) {
-    h ^= str.charCodeAt(i);
-    h = Math.imul(h, 16777619);
-  }
-  return h >>> 0;
-}
-function mulberry32(a) {
-  return function () {
-    a |= 0;
-    a = (a + 0x6d2b79f5) | 0;
-    let t = Math.imul(a ^ (a >>> 15), 1 | a);
-    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
-    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
+/**
+ * 鎺ュ彛鏁版嵁 鈫� 椤甸潰鍐呴儴棰樼洰缁撴瀯
+ * 鍏ュ弬锛氭帴鍙� 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',
+  name: "DataOverview",
   data() {
     return {
       // 椤堕儴鍏ㄥ眬绛涢��
       queryParams: {
         dateRange: [],
-        deptWard: '',
+        deptCodes: [], // 绉戝缂栫爜闆嗗悎 => leaveldeptcodes
+        wardCodes: [], // 鍑洪櫌鐥呭尯缂栫爜闆嗗悎 => leavehospitaldistrictcodes锛堟帴鍙e繀濉級
+        serviceTypeList: [], // 鏈嶅姟绫诲瀷澶氶�夛紝鎻愪氦鏃剁敤閫楀彿鎷兼帴
+        deptOrDistrict: "1", // 绉戝/鐥呭尯缁勫悎鏂瑰紡锛岄粯璁� 1锛氱闄㈢梾鍖� and 绂婚櫌绉戝
+        statMode: "ward", // 缁熻缁村害锛歸ard=鎸夌梾鍖虹粺璁� dept=鎸夌瀹ょ粺璁�
       },
-      deptOptions: DEPTS,
-      wardOptions: WARDS,
+      SERVICE_TYPES,
+      loading: false,
+
+      // 琛ㄤ簩锛氶鐩ā绯婃煡璇㈠叧閿瓧锛堢函鍓嶇杩囨护锛屼笉璇锋眰鍚庣锛�
+      questionSearch: "",
 
       // 琛ㄦ牸涓夛細缁熻缁村害 + 鐙珛绉戝/鐥呭尯鏉′欢锛�'inherit' = 娌跨敤椤堕儴锛�
-      statDimension: 'dept',
-      statDeptWard: 'inherit',
+      statDimension: "ward",
+      statDeptWard: "inherit",
 
-      // 缁熶竴鎺ュ彛杩斿洖鐨勬暟鎹紙妯℃嫙锛�
+      // 鎺ュ彛杩斿洖鐨勫師濮嬫暟鎹紙宸插綊涓�鍖栫殑棰樼洰鍒楄〃锛�
       questionList: [],
-      deptComparison: [],
-      wardComparison: [],
+      // 琛ㄤ笁锛氱瀹�/鐥呭尯璇勫垎瀵规瘮锛坮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));
     },
@@ -256,54 +563,67 @@
     },
     // 琛ㄤ竴锛氭弧鎰忓害缁熻锛堝惈鎬昏琛岋級
     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 m = {};
-        q.options.forEach((o) => (m[o.option] = o.count));
-        const veryGood = m['闈炲父婊℃剰'] || 0;
-        const good = m['婊℃剰'] || 0;
-        const normal = m['涓�鑸�'] || 0;
-        const bad = m['涓嶆弧鎰�'] || 0;
-        const na = m['涓嶉�傜敤'] || 0;
-        const total = veryGood + good + normal + bad + na;
+        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,
-          veryGoodRate: this.rateOf(veryGood, total),
-          goodCount: good,
-          goodRate: this.rateOf(good, total),
-          normalCount: normal,
-          normalRate: this.rateOf(normal, total),
-          badCount: bad,
-          badRate: this.rateOf(bad, total),
-          naCount: na,
-          naRate: this.rateOf(na, total),
-          score: this.scoreOf([veryGood, good, normal, bad, na]),
+          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 = { veryGood: 0, good: 0, normal: 0, bad: 0, na: 0 };
-      rows.forEach((r) => {
-        sum.veryGood += r.veryGoodCount;
-        sum.good += r.goodCount;
-        sum.normal += r.normalCount;
-        sum.bad += r.badCount;
-        sum.na += r.naCount;
+      // 鎬昏琛岋細鍚勯鍚岄�夐」鏁伴噺绱姞锛屽垎鍊兼部鐢ㄨ閫夐」鍒嗗��
+      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.veryGood + sum.good + sum.normal + sum.bad + sum.na;
+      const total = sum.reduce((s, it) => s + it.count, 0);
       rows.push({
-        category: '鎬昏',
-        veryGoodCount: sum.veryGood,
-        veryGoodRate: this.rateOf(sum.veryGood, total),
-        goodCount: sum.good,
-        goodRate: this.rateOf(sum.good, total),
-        normalCount: sum.normal,
-        normalRate: this.rateOf(sum.normal, total),
-        badCount: sum.bad,
-        badRate: this.rateOf(sum.bad, total),
-        naCount: sum.na,
-        naRate: this.rateOf(sum.na, total),
-        score: this.scoreOf([sum.veryGood, sum.good, sum.normal, sum.bad, sum.na]),
+        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;
     },
@@ -322,138 +642,359 @@
         };
       });
     },
+    // 琛ㄤ簩锛氭寜棰樼洰妯$硦鏌ヨ锛堢函鍓嶇杩囨护锛屼笉璇锋眰鍚庣锛�
+    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 list = this.statDimension === 'dept' ? this.deptComparison : this.wardComparison;
-      const f = this.statFilter();
-      if (!f) return list;
-      const [type, name] = f.split(':');
-      if (type !== this.statDimension) return list;
-      return list.filter((r) => r.name === name);
+      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) || [];
     },
   },
-  mounted() {
+  created() {
     if (!this.queryParams.dateRange || !this.queryParams.dateRange.length) {
       this.queryParams.dateRange = this.defaultRange();
     }
+    this.queryParams.serviceTypeList = this.defaultServiceTypes();
+  },
+  mounted() {
     this.fetchData();
   },
   methods: {
-    // 琛ㄤ笁鐙珛鏉′欢锛�'inherit' 鏃舵部鐢ㄩ《閮ㄦ潯浠�
-    statFilter() {
-      const v = this.statDeptWard === 'inherit' ? this.queryParams.deptWard : this.statDeptWard;
-      return v || '';
+    // 榛樿閫変腑鐨勬湇鍔$被鍨嬶紙鎸� 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';
+      if (
+        this.statDeptWard !== "inherit" &&
+        !this.statDeptWard.startsWith(this.statDimension + ":")
+      ) {
+        this.statDeptWard = "inherit";
       }
     },
     handleReset() {
       this.queryParams.dateRange = this.defaultRange();
-      this.queryParams.deptWard = '';
+      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.length !== FIXED_OPTIONS.length) return false;
-      const set = q.options.map((o) => o.option);
-      return FIXED_OPTIONS.every((o) => set.includes(o));
+      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) + '%';
+      if (!total) return "0.00%";
+      return ((count / total) * 100).toFixed(2) + "%";
     },
-    // 缁煎悎寰楀垎锛氶潪甯告弧鎰�5鍒嗐�佹弧鎰�4鍒嗐�佷竴鑸�3鍒嗐�佷笉婊℃剰2鍒嗭紝鎹㈢畻鐧惧垎鍒讹紙涓嶉�傜敤涓嶈鍏ワ級鈥斺�斿崰浣嶅叕寮忥紝鎺ュ彛灏辩华鍚庡彲鎸夊悗绔彛寰勬浛鎹�
-    scoreOf(counts) {
-      const [a, b, c, d] = counts;
-      const total = a + b + c + d;
-      if (!total) return '0.00';
-      return (((a * 5 + b * 4 + c * 3 + d * 2) / total) * 20).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() - 29);
+      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');
+        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)];
     },
-    dateRangeDays(range) {
-      if (Array.isArray(range) && range.length === 2 && range[0] && range[1]) {
-        const ms = new Date(range[1]).getTime() - new Date(range[0]).getTime();
-        return Math.max(1, Math.round(ms / 86400000));
+    // 鎻愮ず淇℃伅锛堝吋瀹� $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);
       }
-      return 30;
     },
-    // 鎷夊彇鏁版嵁锛堝綋鍓嶄负妯℃嫙鏁版嵁锛屾帴鍙e氨缁悗鏇挎崲涓虹湡瀹炶姹傦級
-    fetchData() {
-      const data = this.mockFetch(this.queryParams);
-      this.questionList = data.questionList;
-      this.deptComparison = data.deptComparison;
-      this.wardComparison = data.wardComparison;
-    },
-    mockFetch(params) {
-      const seed = hashString(JSON.stringify(params));
-      const rnd = mulberry32(seed);
-      const days = this.dateRangeDays(params.dateRange);
-      const timeFactor = Math.max(0.05, days / 30);
-      const scopeFactor = params.deptWard ? 0.18 : 1;
-      const jitter = () => 0.9 + rnd() * 0.2;
+    // 鎷夊彇婊℃剰搴︾粺璁℃暟鎹紙鎺ュ彛锛�/smartor/serviceSubtaskDetail/sltdMydTotalByScore锛�
+    async fetchData() {
+      const range = this.queryParams.dateRange || [];
+      if (range.length !== 2 || !range[0] || !range[1]) {
+        this.notify("璇峰厛閫夋嫨鍑洪櫌鏃ユ湡鑼冨洿", "warning");
+        return;
+      }
 
-      const questionList = RAW_QUESTIONS.map((q) => {
-        const options = q.options.map((o) => ({
-          option: o.option,
-          count: Math.round(o.count * timeFactor * scopeFactor * jitter()),
-        }));
-        const item = { title: q.title, type: q.type, options };
-        if (q.type === 'multiple' && q.total != null) {
-          item.total = Math.round(q.total * timeFactor * scopeFactor);
+      // 鐥呭尯 => 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");
         }
-        return item;
+        // 琛ㄤ笁锛氬悇绉戝/鐥呭尯婊℃剰搴﹁瘎鍒嗗姣旓紙鐪熷疄鎺ュ彛锛屽唴閮ㄨ嚜琛屽鐞嗛敊璇級
+        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";
       });
 
-      const deptComparison = DEPTS.map((d) => this.genCompareRow(d, rnd));
-      const wardComparison = WARDS.map((w) => this.genCompareRow(w, rnd));
-
-      return { questionList, deptComparison, wardComparison };
+      return { rows, questions };
     },
-    genCompareRow(name, rnd) {
-      const r = (lo, hi) => Math.round(lo + rnd() * (hi - lo));
-      const q1VeryGood = r(20, 180);
-      const q1Good = r(5, 55);
-      const q1Normal = r(0, 4);
-      const q1Bad = r(0, 3);
-      const q2VeryGood = r(18, 175);
-      const q2Good = r(5, 55);
-      const q2Normal = r(0, 4);
-      const q2Bad = r(0, 3);
-      return {
-        name,
-        q1VeryGood, q1Good, q1Normal, q1Bad,
-        q1Score: this.scoreOf([q1VeryGood, q1Good, q1Normal, q1Bad]),
-        q2VeryGood, q2Good, q2Normal, q2Bad,
-        q2Score: this.scoreOf([q2VeryGood, q2Good, q2Normal, q2Bad]),
-        totalScore: this.scoreOf([
-          q1VeryGood + q2VeryGood,
-          q1Good + q2Good,
-          q1Normal + q2Normal,
-          q1Bad + q2Bad,
-        ]),
-      };
+    // 鍚嶇О 鈫� 缂栫爜锛堢敤浜庤〃涓夋寜椤堕儴鎵�閫夌紪鐮佽繃婊わ級
+    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;
 
@@ -461,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);
         });
@@ -472,22 +1013,24 @@
         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');
+        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.download = name + ".csv";
         link.click();
       }
     },
@@ -496,13 +1039,66 @@
 </script>
 
 <style lang="scss" scoped>
-.data-overview { .overview-card { margin-bottom: 20px; } }
-.filter-card { .filter-form { margin-bottom: -10px; } .filter-select { width: 200px; } }
-.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; } }
-.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